3 * https://www.mediawiki.org/wiki/OOUI
5 * Copyright 2011–2018 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2018-06-07T21:36:30Z
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 the file selection widget's select file button
381 'ooui-selectfile-button-select': 'Select a file',
382 // Label for the file selection widget if file selection is not supported
383 'ooui-selectfile-not-supported': 'File selection is not supported',
384 // Label for the file selection widget when no file is currently selected
385 'ooui-selectfile-placeholder': 'No file is selected',
386 // Label for the file selection widget's drop target
387 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
391 * Get a localized message.
393 * After the message key, message parameters may optionally be passed. In the default implementation,
394 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
395 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
396 * they support unnamed, ordered message parameters.
398 * In environments that provide a localization system, this function should be overridden to
399 * return the message translated in the user's language. The default implementation always returns
400 * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
404 * var i, iLen, button,
405 * messagePath = 'oojs-ui/dist/i18n/',
406 * languages = [ $.i18n().locale, 'ur', 'en' ],
409 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
410 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
413 * $.i18n().load( languageMap ).done( function() {
414 * // Replace the built-in `msg` only once we've loaded the internationalization.
415 * // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
416 * // you put off creating any widgets until this promise is complete, no English
417 * // will be displayed.
418 * OO.ui.msg = $.i18n;
420 * // A button displaying "OK" in the default locale
421 * button = new OO.ui.ButtonWidget( {
422 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
425 * $( 'body' ).append( button.$element );
427 * // A button displaying "OK" in Urdu
428 * $.i18n().locale = 'ur';
429 * button = new OO.ui.ButtonWidget( {
430 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
433 * $( 'body' ).append( button.$element );
436 * @param {string} key Message key
437 * @param {...Mixed} [params] Message parameters
438 * @return {string} Translated message with parameters substituted
440 OO
.ui
.msg = function ( key
) {
441 var message
= messages
[ key
],
442 params
= Array
.prototype.slice
.call( arguments
, 1 );
443 if ( typeof message
=== 'string' ) {
444 // Perform $1 substitution
445 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
446 var i
= parseInt( n
, 10 );
447 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
450 // Return placeholder if message not found
451 message
= '[' + key
+ ']';
458 * Package a message and arguments for deferred resolution.
460 * Use this when you are statically specifying a message and the message may not yet be present.
462 * @param {string} key Message key
463 * @param {...Mixed} [params] Message parameters
464 * @return {Function} Function that returns the resolved message when executed
466 OO
.ui
.deferMsg = function () {
467 var args
= arguments
;
469 return OO
.ui
.msg
.apply( OO
.ui
, args
);
476 * If the message is a function it will be executed, otherwise it will pass through directly.
478 * @param {Function|string} msg Deferred message, or message text
479 * @return {string} Resolved message
481 OO
.ui
.resolveMsg = function ( msg
) {
482 if ( $.isFunction( msg
) ) {
489 * @param {string} url
492 OO
.ui
.isSafeUrl = function ( url
) {
493 // Keep this function in sync with php/Tag.php
494 var i
, protocolWhitelist
;
496 function stringStartsWith( haystack
, needle
) {
497 return haystack
.substr( 0, needle
.length
) === needle
;
500 protocolWhitelist
= [
501 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
502 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
503 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
510 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
511 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
516 // This matches '//' too
517 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
520 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
528 * Check if the user has a 'mobile' device.
530 * For our purposes this means the user is primarily using an
531 * on-screen keyboard, touch input instead of a mouse and may
532 * have a physically small display.
534 * It is left up to implementors to decide how to compute this
535 * so the default implementation always returns false.
537 * @return {boolean} User is on a mobile device
539 OO
.ui
.isMobile = function () {
544 * Get the additional spacing that should be taken into account when displaying elements that are
545 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
546 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
548 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
549 * the extra spacing from that edge of viewport (in pixels)
551 OO
.ui
.getViewportSpacing = function () {
561 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
562 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
564 * @return {jQuery} Default overlay node
566 OO
.ui
.getDefaultOverlay = function () {
567 if ( !OO
.ui
.$defaultOverlay
) {
568 OO
.ui
.$defaultOverlay
= $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
569 $( 'body' ).append( OO
.ui
.$defaultOverlay
);
571 return OO
.ui
.$defaultOverlay
;
579 * Namespace for OOUI mixins.
581 * Mixins are named according to the type of object they are intended to
582 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
583 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
584 * is intended to be mixed in to an instance of OO.ui.Widget.
592 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
593 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
594 * connected to them and can't be interacted with.
600 * @param {Object} [config] Configuration options
601 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
602 * to the top level (e.g., the outermost div) of the element. See the [OOUI documentation on MediaWiki][2]
604 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
605 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
606 * @cfg {string} [text] Text to insert
607 * @cfg {Array} [content] An array of content elements to append (after #text).
608 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
609 * Instances of OO.ui.Element will have their $element appended.
610 * @cfg {jQuery} [$content] Content elements to append (after #text).
611 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
612 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
613 * Data can also be specified with the #setData method.
615 OO
.ui
.Element
= function OoUiElement( config
) {
616 if ( OO
.ui
.isDemo
) {
617 this.initialConfig
= config
;
619 // Configuration initialization
620 config
= config
|| {};
624 this.elementId
= null;
626 this.data
= config
.data
;
627 this.$element
= config
.$element
||
628 $( document
.createElement( this.getTagName() ) );
629 this.elementGroup
= null;
632 if ( Array
.isArray( config
.classes
) ) {
633 this.$element
.addClass( config
.classes
.join( ' ' ) );
636 this.setElementId( config
.id
);
639 this.$element
.text( config
.text
);
641 if ( config
.content
) {
642 // The `content` property treats plain strings as text; use an
643 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
644 // appropriate $element appended.
645 this.$element
.append( config
.content
.map( function ( v
) {
646 if ( typeof v
=== 'string' ) {
647 // Escape string so it is properly represented in HTML.
648 return document
.createTextNode( v
);
649 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
652 } else if ( v
instanceof OO
.ui
.Element
) {
658 if ( config
.$content
) {
659 // The `$content` property treats plain strings as HTML.
660 this.$element
.append( config
.$content
);
666 OO
.initClass( OO
.ui
.Element
);
668 /* Static Properties */
671 * The name of the HTML tag used by the element.
673 * The static value may be ignored if the #getTagName method is overridden.
679 OO
.ui
.Element
.static.tagName
= 'div';
684 * Reconstitute a JavaScript object corresponding to a widget created
685 * by the PHP implementation.
687 * @param {string|HTMLElement|jQuery} idOrNode
688 * A DOM id (if a string) or node for the widget to infuse.
689 * @param {Object} [config] Configuration options
690 * @return {OO.ui.Element}
691 * The `OO.ui.Element` corresponding to this (infusable) document node.
692 * For `Tag` objects emitted on the HTML side (used occasionally for content)
693 * the value returned is a newly-created Element wrapping around the existing
696 OO
.ui
.Element
.static.infuse = function ( idOrNode
, config
) {
697 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, config
, false );
698 // Verify that the type matches up.
699 // FIXME: uncomment after T89721 is fixed, see T90929.
701 if ( !( obj instanceof this['class'] ) ) {
702 throw new Error( 'Infusion type mismatch!' );
709 * Implementation helper for `infuse`; skips the type check and has an
710 * extra property so that only the top-level invocation touches the DOM.
713 * @param {string|HTMLElement|jQuery} idOrNode
714 * @param {Object} [config] Configuration options
715 * @param {jQuery.Promise} [domPromise] A promise that will be resolved
716 * when the top-level widget of this infusion is inserted into DOM,
717 * replacing the original node; only used internally.
718 * @return {OO.ui.Element}
720 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, config
, domPromise
) {
721 // look for a cached result of a previous infusion.
722 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
723 if ( typeof idOrNode
=== 'string' ) {
725 $elem
= $( document
.getElementById( id
) );
727 $elem
= $( idOrNode
);
728 id
= $elem
.attr( 'id' );
730 if ( !$elem
.length
) {
731 if ( typeof idOrNode
=== 'string' ) {
732 error
= 'Widget not found: ' + idOrNode
;
733 } else if ( idOrNode
&& idOrNode
.selector
) {
734 error
= 'Widget not found: ' + idOrNode
.selector
;
736 error
= 'Widget not found';
738 throw new Error( error
);
740 if ( $elem
[ 0 ].oouiInfused
) {
741 $elem
= $elem
[ 0 ].oouiInfused
;
743 data
= $elem
.data( 'ooui-infused' );
746 if ( data
=== true ) {
747 throw new Error( 'Circular dependency! ' + id
);
750 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
751 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
752 // restore dynamic state after the new element is re-inserted into DOM under infused parent
753 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
754 infusedChildren
= $elem
.data( 'ooui-infused-children' );
755 if ( infusedChildren
&& infusedChildren
.length
) {
756 infusedChildren
.forEach( function ( data
) {
757 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
758 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
764 data
= $elem
.attr( 'data-ooui' );
766 throw new Error( 'No infusion data found: ' + id
);
769 data
= JSON
.parse( data
);
773 if ( !( data
&& data
._
) ) {
774 throw new Error( 'No valid infusion data found: ' + id
);
776 if ( data
._
=== 'Tag' ) {
777 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
778 return new OO
.ui
.Element( $.extend( {}, config
, { $element
: $elem
} ) );
780 parts
= data
._
.split( '.' );
781 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
782 if ( cls
=== undefined ) {
783 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
786 // Verify that we're creating an OO.ui.Element instance
789 while ( parent
!== undefined ) {
790 if ( parent
=== OO
.ui
.Element
) {
795 parent
= parent
.parent
;
798 if ( parent
!== OO
.ui
.Element
) {
799 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
804 domPromise
= top
.promise();
806 $elem
.data( 'ooui-infused', true ); // prevent loops
807 data
.id
= id
; // implicit
808 infusedChildren
= [];
809 data
= OO
.copy( data
, null, function deserialize( value
) {
811 if ( OO
.isPlainObject( value
) ) {
813 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, config
, domPromise
);
814 infusedChildren
.push( infused
);
815 // Flatten the structure
816 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
817 infused
.$element
.removeData( 'ooui-infused-children' );
820 if ( value
.html
!== undefined ) {
821 return new OO
.ui
.HtmlSnippet( value
.html
);
825 // allow widgets to reuse parts of the DOM
826 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
827 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
828 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
830 // eslint-disable-next-line new-cap
831 obj
= new cls( $.extend( {}, config
, data
) );
832 // If anyone is holding a reference to the old DOM element,
833 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
834 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
835 $elem
[ 0 ].oouiInfused
= obj
.$element
;
836 // now replace old DOM with this new DOM.
838 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
839 // so only mutate the DOM if we need to.
840 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
841 $elem
.replaceWith( obj
.$element
);
845 obj
.$element
.data( 'ooui-infused', obj
);
846 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
847 // set the 'data-ooui' attribute so we can identify infused widgets
848 obj
.$element
.attr( 'data-ooui', '' );
849 // restore dynamic state after the new element is inserted into DOM
850 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
855 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
857 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
858 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
859 * constructor, which will be given the enhanced config.
862 * @param {HTMLElement} node
863 * @param {Object} config
866 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
871 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
872 * (and its children) that represent an Element of the same class and the given configuration,
873 * generated by the PHP implementation.
875 * This method is called just before `node` is detached from the DOM. The return value of this
876 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
877 * is inserted into DOM to replace `node`.
880 * @param {HTMLElement} node
881 * @param {Object} config
884 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
889 * Get a jQuery function within a specific document.
892 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
893 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
895 * @return {Function} Bound jQuery function
897 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
898 function wrapper( selector
) {
899 return $( selector
, wrapper
.context
);
902 wrapper
.context
= this.getDocument( context
);
905 wrapper
.$iframe
= $iframe
;
912 * Get the document of an element.
915 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
916 * @return {HTMLDocument|null} Document object
918 OO
.ui
.Element
.static.getDocument = function ( obj
) {
919 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
920 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
921 // Empty jQuery selections might have a context
928 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
933 * Get the window of an element or document.
936 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
937 * @return {Window} Window object
939 OO
.ui
.Element
.static.getWindow = function ( obj
) {
940 var doc
= this.getDocument( obj
);
941 return doc
.defaultView
;
945 * Get the direction of an element or document.
948 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
949 * @return {string} Text direction, either 'ltr' or 'rtl'
951 OO
.ui
.Element
.static.getDir = function ( obj
) {
954 if ( obj
instanceof jQuery
) {
957 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
958 isWin
= obj
.document
!== undefined;
959 if ( isDoc
|| isWin
) {
965 return $( obj
).css( 'direction' );
969 * Get the offset between two frames.
971 * TODO: Make this function not use recursion.
974 * @param {Window} from Window of the child frame
975 * @param {Window} [to=window] Window of the parent frame
976 * @param {Object} [offset] Offset to start with, used internally
977 * @return {Object} Offset object, containing left and top properties
979 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
980 var i
, len
, frames
, frame
, rect
;
986 offset
= { top
: 0, left
: 0 };
988 if ( from.parent
=== from ) {
992 // Get iframe element
993 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
994 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
995 if ( frames
[ i
].contentWindow
=== from ) {
1001 // Recursively accumulate offset values
1003 rect
= frame
.getBoundingClientRect();
1004 offset
.left
+= rect
.left
;
1005 offset
.top
+= rect
.top
;
1006 if ( from !== to
) {
1007 this.getFrameOffset( from.parent
, offset
);
1014 * Get the offset between two elements.
1016 * The two elements may be in a different frame, but in that case the frame $element is in must
1017 * be contained in the frame $anchor is in.
1020 * @param {jQuery} $element Element whose position to get
1021 * @param {jQuery} $anchor Element to get $element's position relative to
1022 * @return {Object} Translated position coordinates, containing top and left properties
1024 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1025 var iframe
, iframePos
,
1026 pos
= $element
.offset(),
1027 anchorPos
= $anchor
.offset(),
1028 elementDocument
= this.getDocument( $element
),
1029 anchorDocument
= this.getDocument( $anchor
);
1031 // If $element isn't in the same document as $anchor, traverse up
1032 while ( elementDocument
!== anchorDocument
) {
1033 iframe
= elementDocument
.defaultView
.frameElement
;
1035 throw new Error( '$element frame is not contained in $anchor frame' );
1037 iframePos
= $( iframe
).offset();
1038 pos
.left
+= iframePos
.left
;
1039 pos
.top
+= iframePos
.top
;
1040 elementDocument
= iframe
.ownerDocument
;
1042 pos
.left
-= anchorPos
.left
;
1043 pos
.top
-= anchorPos
.top
;
1048 * Get element border sizes.
1051 * @param {HTMLElement} el Element to measure
1052 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1054 OO
.ui
.Element
.static.getBorders = function ( el
) {
1055 var doc
= el
.ownerDocument
,
1056 win
= doc
.defaultView
,
1057 style
= win
.getComputedStyle( el
, null ),
1059 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1060 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1061 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1062 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1073 * Get dimensions of an element or window.
1076 * @param {HTMLElement|Window} el Element to measure
1077 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1079 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1081 doc
= el
.ownerDocument
|| el
.document
,
1082 win
= doc
.defaultView
;
1084 if ( win
=== el
|| el
=== doc
.documentElement
) {
1087 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1089 top
: $win
.scrollTop(),
1090 left
: $win
.scrollLeft()
1092 scrollbar
: { right
: 0, bottom
: 0 },
1096 bottom
: $win
.innerHeight(),
1097 right
: $win
.innerWidth()
1103 borders
: this.getBorders( el
),
1105 top
: $el
.scrollTop(),
1106 left
: $el
.scrollLeft()
1109 right
: $el
.innerWidth() - el
.clientWidth
,
1110 bottom
: $el
.innerHeight() - el
.clientHeight
1112 rect
: el
.getBoundingClientRect()
1118 * Get the number of pixels that an element's content is scrolled to the left.
1120 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1121 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1123 * This function smooths out browser inconsistencies (nicely described in the README at
1124 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1125 * with Firefox's 'scrollLeft', which seems the sanest.
1129 * @param {HTMLElement|Window} el Element to measure
1130 * @return {number} Scroll position from the left.
1131 * If the element's direction is LTR, this is a positive number between `0` (initial scroll position)
1132 * and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1133 * If the element's direction is RTL, this is a negative number between `0` (initial scroll position)
1134 * and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1136 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1137 var rtlScrollType
= null;
1140 var $definer
= $( '<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>' ),
1141 definer
= $definer
[ 0 ];
1143 $definer
.appendTo( 'body' );
1144 if ( definer
.scrollLeft
> 0 ) {
1146 rtlScrollType
= 'default';
1148 definer
.scrollLeft
= 1;
1149 if ( definer
.scrollLeft
=== 0 ) {
1150 // Firefox, old Opera
1151 rtlScrollType
= 'negative';
1153 // Internet Explorer, Edge
1154 rtlScrollType
= 'reverse';
1160 return function getScrollLeft( el
) {
1161 var isRoot
= el
.window
=== el
||
1162 el
=== el
.ownerDocument
.body
||
1163 el
=== el
.ownerDocument
.documentElement
,
1164 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1165 // All browsers use the correct scroll type ('negative') on the root, so don't
1166 // do any fixups when looking at the root element
1167 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1169 if ( direction
=== 'rtl' ) {
1170 if ( rtlScrollType
=== null ) {
1173 if ( rtlScrollType
=== 'reverse' ) {
1174 scrollLeft
= -scrollLeft
;
1175 } else if ( rtlScrollType
=== 'default' ) {
1176 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1185 * Get the root scrollable element of given element's document.
1187 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1188 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1189 * lets us use 'body' or 'documentElement' based on what is working.
1191 * https://code.google.com/p/chromium/issues/detail?id=303131
1194 * @param {HTMLElement} el Element to find root scrollable parent for
1195 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1196 * depending on browser
1198 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1199 var scrollTop
, body
;
1201 if ( OO
.ui
.scrollableElement
=== undefined ) {
1202 body
= el
.ownerDocument
.body
;
1203 scrollTop
= body
.scrollTop
;
1206 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1207 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1208 if ( Math
.round( body
.scrollTop
) === 1 ) {
1209 body
.scrollTop
= scrollTop
;
1210 OO
.ui
.scrollableElement
= 'body';
1212 OO
.ui
.scrollableElement
= 'documentElement';
1216 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1220 * Get closest scrollable container.
1222 * Traverses up until either a scrollable element or the root is reached, in which case the root
1223 * scrollable element will be returned (see #getRootScrollableElement).
1226 * @param {HTMLElement} el Element to find scrollable container for
1227 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1228 * @return {HTMLElement} Closest scrollable container
1230 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1232 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1233 // 'overflow-y' have different values, so we need to check the separate properties.
1234 props
= [ 'overflow-x', 'overflow-y' ],
1235 $parent
= $( el
).parent();
1237 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1238 props
= [ 'overflow-' + dimension
];
1241 // Special case for the document root (which doesn't really have any scrollable container, since
1242 // it is the ultimate scrollable container, but this is probably saner than null or exception)
1243 if ( $( el
).is( 'html, body' ) ) {
1244 return this.getRootScrollableElement( el
);
1247 while ( $parent
.length
) {
1248 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1249 return $parent
[ 0 ];
1253 val
= $parent
.css( props
[ i
] );
1254 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be
1255 // scrolled in that direction, but they can actually be scrolled programatically. The user can
1256 // unintentionally perform a scroll in such case even if the application doesn't scroll
1257 // programatically, e.g. when jumping to an anchor, or when using built-in find functionality.
1258 // This could cause funny issues...
1259 if ( val
=== 'auto' || val
=== 'scroll' ) {
1260 return $parent
[ 0 ];
1263 $parent
= $parent
.parent();
1265 // The element is unattached... return something mostly sane
1266 return this.getRootScrollableElement( el
);
1270 * Scroll element into view.
1273 * @param {HTMLElement} el Element to scroll into view
1274 * @param {Object} [config] Configuration options
1275 * @param {string} [config.duration='fast'] jQuery animation duration value
1276 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1277 * to scroll in both directions
1278 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1280 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1281 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1282 deferred
= $.Deferred();
1284 // Configuration initialization
1285 config
= config
|| {};
1288 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1289 $container
= $( container
);
1290 elementDimensions
= this.getDimensions( el
);
1291 containerDimensions
= this.getDimensions( container
);
1292 $window
= $( this.getWindow( el
) );
1294 // Compute the element's position relative to the container
1295 if ( $container
.is( 'html, body' ) ) {
1296 // If the scrollable container is the root, this is easy
1298 top
: elementDimensions
.rect
.top
,
1299 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1300 left
: elementDimensions
.rect
.left
,
1301 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1304 // Otherwise, we have to subtract el's coordinates from container's coordinates
1306 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1307 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1308 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1309 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1313 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1314 if ( position
.top
< 0 ) {
1315 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1316 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1317 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1320 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1321 if ( position
.left
< 0 ) {
1322 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1323 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1324 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1327 if ( !$.isEmptyObject( animations
) ) {
1328 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1329 $container
.queue( function ( next
) {
1336 return deferred
.promise();
1340 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1341 * and reserve space for them, because it probably doesn't.
1343 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1344 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1345 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1346 * and then reattach (or show) them back.
1349 * @param {HTMLElement} el Element to reconsider the scrollbars on
1351 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1352 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1353 // Save scroll position
1354 scrollLeft
= el
.scrollLeft
;
1355 scrollTop
= el
.scrollTop
;
1356 // Detach all children
1357 while ( el
.firstChild
) {
1358 nodes
.push( el
.firstChild
);
1359 el
.removeChild( el
.firstChild
);
1362 void el
.offsetHeight
;
1363 // Reattach all children
1364 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1365 el
.appendChild( nodes
[ i
] );
1367 // Restore scroll position (no-op if scrollbars disappeared)
1368 el
.scrollLeft
= scrollLeft
;
1369 el
.scrollTop
= scrollTop
;
1375 * Toggle visibility of an element.
1377 * @param {boolean} [show] Make element visible, omit to toggle visibility
1381 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1382 show
= show
=== undefined ? !this.visible
: !!show
;
1384 if ( show
!== this.isVisible() ) {
1385 this.visible
= show
;
1386 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1387 this.emit( 'toggle', show
);
1394 * Check if element is visible.
1396 * @return {boolean} element is visible
1398 OO
.ui
.Element
.prototype.isVisible = function () {
1399 return this.visible
;
1405 * @return {Mixed} Element data
1407 OO
.ui
.Element
.prototype.getData = function () {
1414 * @param {Mixed} data Element data
1417 OO
.ui
.Element
.prototype.setData = function ( data
) {
1423 * Set the element has an 'id' attribute.
1425 * @param {string} id
1428 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1429 this.elementId
= id
;
1430 this.$element
.attr( 'id', id
);
1435 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1436 * and return its value.
1440 OO
.ui
.Element
.prototype.getElementId = function () {
1441 if ( this.elementId
=== null ) {
1442 this.setElementId( OO
.ui
.generateElementId() );
1444 return this.elementId
;
1448 * Check if element supports one or more methods.
1450 * @param {string|string[]} methods Method or list of methods to check
1451 * @return {boolean} All methods are supported
1453 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1457 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1458 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1459 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1464 return methods
.length
=== support
;
1468 * Update the theme-provided classes.
1470 * @localdoc This is called in element mixins and widget classes any time state changes.
1471 * Updating is debounced, minimizing overhead of changing multiple attributes and
1472 * guaranteeing that theme updates do not occur within an element's constructor
1474 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1475 OO
.ui
.theme
.queueUpdateElementClasses( this );
1479 * Get the HTML tag name.
1481 * Override this method to base the result on instance information.
1483 * @return {string} HTML tag name
1485 OO
.ui
.Element
.prototype.getTagName = function () {
1486 return this.constructor.static.tagName
;
1490 * Check if the element is attached to the DOM
1492 * @return {boolean} The element is attached to the DOM
1494 OO
.ui
.Element
.prototype.isElementAttached = function () {
1495 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1499 * Get the DOM document.
1501 * @return {HTMLDocument} Document object
1503 OO
.ui
.Element
.prototype.getElementDocument = function () {
1504 // Don't cache this in other ways either because subclasses could can change this.$element
1505 return OO
.ui
.Element
.static.getDocument( this.$element
);
1509 * Get the DOM window.
1511 * @return {Window} Window object
1513 OO
.ui
.Element
.prototype.getElementWindow = function () {
1514 return OO
.ui
.Element
.static.getWindow( this.$element
);
1518 * Get closest scrollable container.
1520 * @return {HTMLElement} Closest scrollable container
1522 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1523 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1527 * Get group element is in.
1529 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1531 OO
.ui
.Element
.prototype.getElementGroup = function () {
1532 return this.elementGroup
;
1536 * Set group element is in.
1538 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1541 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1542 this.elementGroup
= group
;
1547 * Scroll element into view.
1549 * @param {Object} [config] Configuration options
1550 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1552 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1554 !this.isElementAttached() ||
1555 !this.isVisible() ||
1556 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1558 return $.Deferred().resolve();
1560 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1564 * Restore the pre-infusion dynamic state for this widget.
1566 * This method is called after #$element has been inserted into DOM. The parameter is the return
1567 * value of #gatherPreInfuseState.
1570 * @param {Object} state
1572 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1576 * Wraps an HTML snippet for use with configuration values which default
1577 * to strings. This bypasses the default html-escaping done to string
1583 * @param {string} [content] HTML content
1585 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1587 this.content
= content
;
1592 OO
.initClass( OO
.ui
.HtmlSnippet
);
1599 * @return {string} Unchanged HTML snippet.
1601 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1602 return this.content
;
1606 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1607 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1608 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1609 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1610 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1614 * @extends OO.ui.Element
1615 * @mixins OO.EventEmitter
1618 * @param {Object} [config] Configuration options
1620 OO
.ui
.Layout
= function OoUiLayout( config
) {
1621 // Configuration initialization
1622 config
= config
|| {};
1624 // Parent constructor
1625 OO
.ui
.Layout
.parent
.call( this, config
);
1627 // Mixin constructors
1628 OO
.EventEmitter
.call( this );
1631 this.$element
.addClass( 'oo-ui-layout' );
1636 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1637 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1640 * Widgets are compositions of one or more OOUI elements that users can both view
1641 * and interact with. All widgets can be configured and modified via a standard API,
1642 * and their state can change dynamically according to a model.
1646 * @extends OO.ui.Element
1647 * @mixins OO.EventEmitter
1650 * @param {Object} [config] Configuration options
1651 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1652 * appearance reflects this state.
1654 OO
.ui
.Widget
= function OoUiWidget( config
) {
1655 // Initialize config
1656 config
= $.extend( { disabled
: false }, config
);
1658 // Parent constructor
1659 OO
.ui
.Widget
.parent
.call( this, config
);
1661 // Mixin constructors
1662 OO
.EventEmitter
.call( this );
1665 this.disabled
= null;
1666 this.wasDisabled
= null;
1669 this.$element
.addClass( 'oo-ui-widget' );
1670 this.setDisabled( !!config
.disabled
);
1675 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1676 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1683 * A 'disable' event is emitted when the disabled state of the widget changes
1684 * (i.e. on disable **and** enable).
1686 * @param {boolean} disabled Widget is disabled
1692 * A 'toggle' event is emitted when the visibility of the widget changes.
1694 * @param {boolean} visible Widget is visible
1700 * Check if the widget is disabled.
1702 * @return {boolean} Widget is disabled
1704 OO
.ui
.Widget
.prototype.isDisabled = function () {
1705 return this.disabled
;
1709 * Set the 'disabled' state of the widget.
1711 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1713 * @param {boolean} disabled Disable widget
1716 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1719 this.disabled
= !!disabled
;
1720 isDisabled
= this.isDisabled();
1721 if ( isDisabled
!== this.wasDisabled
) {
1722 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1723 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1724 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1725 this.emit( 'disable', isDisabled
);
1726 this.updateThemeClasses();
1728 this.wasDisabled
= isDisabled
;
1734 * Update the disabled state, in case of changes in parent widget.
1738 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1739 this.setDisabled( this.disabled
);
1744 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1747 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1750 * @return {string|null} The ID of the labelable element
1752 OO
.ui
.Widget
.prototype.getInputId = function () {
1757 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1758 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1759 * override this method to provide intuitive, accessible behavior.
1761 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1762 * Individual widgets may override it too.
1764 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1767 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1778 OO
.ui
.Theme
= function OoUiTheme() {
1779 this.elementClassesQueue
= [];
1780 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1785 OO
.initClass( OO
.ui
.Theme
);
1790 * Get a list of classes to be applied to a widget.
1792 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1793 * otherwise state transitions will not work properly.
1795 * @param {OO.ui.Element} element Element for which to get classes
1796 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1798 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1799 return { on
: [], off
: [] };
1803 * Update CSS classes provided by the theme.
1805 * For elements with theme logic hooks, this should be called any time there's a state change.
1807 * @param {OO.ui.Element} element Element for which to update classes
1809 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1810 var $elements
= $( [] ),
1811 classes
= this.getElementClasses( element
);
1813 if ( element
.$icon
) {
1814 $elements
= $elements
.add( element
.$icon
);
1816 if ( element
.$indicator
) {
1817 $elements
= $elements
.add( element
.$indicator
);
1821 .removeClass( classes
.off
.join( ' ' ) )
1822 .addClass( classes
.on
.join( ' ' ) );
1828 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1830 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1831 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1834 this.elementClassesQueue
= [];
1838 * Queue #updateElementClasses to be called for this element.
1840 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1841 * to make them synchronous.
1843 * @param {OO.ui.Element} element Element for which to update classes
1845 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1846 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1847 // the most common case (this method is often called repeatedly for the same element).
1848 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1851 this.elementClassesQueue
.push( element
);
1852 this.debouncedUpdateQueuedElementClasses();
1856 * Get the transition duration in milliseconds for dialogs opening/closing
1858 * The dialog should be fully rendered this many milliseconds after the
1859 * ready process has executed.
1861 * @return {number} Transition duration in milliseconds
1863 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1868 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1869 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1870 * order in which users will navigate through the focusable elements via the "tab" key.
1873 * // TabIndexedElement is mixed into the ButtonWidget class
1874 * // to provide a tabIndex property.
1875 * var button1 = new OO.ui.ButtonWidget( {
1879 * var button2 = new OO.ui.ButtonWidget( {
1883 * var button3 = new OO.ui.ButtonWidget( {
1887 * var button4 = new OO.ui.ButtonWidget( {
1891 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1897 * @param {Object} [config] Configuration options
1898 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1899 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1900 * functionality will be applied to it instead.
1901 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1902 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1903 * to remove the element from the tab-navigation flow.
1905 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1906 // Configuration initialization
1907 config
= $.extend( { tabIndex
: 0 }, config
);
1910 this.$tabIndexed
= null;
1911 this.tabIndex
= null;
1914 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1917 this.setTabIndex( config
.tabIndex
);
1918 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1923 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1928 * Set the element that should use the tabindex functionality.
1930 * This method is used to retarget a tabindex mixin so that its functionality applies
1931 * to the specified element. If an element is currently using the functionality, the mixin’s
1932 * effect on that element is removed before the new element is set up.
1934 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1937 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1938 var tabIndex
= this.tabIndex
;
1939 // Remove attributes from old $tabIndexed
1940 this.setTabIndex( null );
1941 // Force update of new $tabIndexed
1942 this.$tabIndexed
= $tabIndexed
;
1943 this.tabIndex
= tabIndex
;
1944 return this.updateTabIndex();
1948 * Set the value of the tabindex.
1950 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
1953 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1954 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
1956 if ( this.tabIndex
!== tabIndex
) {
1957 this.tabIndex
= tabIndex
;
1958 this.updateTabIndex();
1965 * Update the `tabindex` attribute, in case of changes to tab index or
1971 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1972 if ( this.$tabIndexed
) {
1973 if ( this.tabIndex
!== null ) {
1974 // Do not index over disabled elements
1975 this.$tabIndexed
.attr( {
1976 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1977 // Support: ChromeVox and NVDA
1978 // These do not seem to inherit aria-disabled from parent elements
1979 'aria-disabled': this.isDisabled().toString()
1982 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1989 * Handle disable events.
1992 * @param {boolean} disabled Element is disabled
1994 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1995 this.updateTabIndex();
1999 * Get the value of the tabindex.
2001 * @return {number|null} Tabindex value
2003 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2004 return this.tabIndex
;
2008 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2010 * If the element already has an ID then that is returned, otherwise unique ID is
2011 * generated, set on the element, and returned.
2013 * @return {string|null} The ID of the focusable element
2015 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2018 if ( !this.$tabIndexed
) {
2021 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2025 id
= this.$tabIndexed
.attr( 'id' );
2026 if ( id
=== undefined ) {
2027 id
= OO
.ui
.generateElementId();
2028 this.$tabIndexed
.attr( 'id', id
);
2035 * Whether the node is 'labelable' according to the HTML spec
2036 * (i.e., whether it can be interacted with through a `<label for="…">`).
2037 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2040 * @param {jQuery} $node
2043 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2045 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2046 tagName
= $node
.prop( 'tagName' ).toLowerCase();
2048 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2051 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2058 * Focus this element.
2062 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2063 if ( !this.isDisabled() ) {
2064 this.$tabIndexed
.focus();
2070 * Blur this element.
2074 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2075 this.$tabIndexed
.blur();
2080 * @inheritdoc OO.ui.Widget
2082 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2087 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2088 * interface element that can be configured with access keys for accessibility.
2089 * See the [OOUI documentation on MediaWiki] [1] for examples.
2091 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2097 * @param {Object} [config] Configuration options
2098 * @cfg {jQuery} [$button] The button element created by the class.
2099 * If this configuration is omitted, the button element will use a generated `<a>`.
2100 * @cfg {boolean} [framed=true] Render the button with a frame
2102 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2103 // Configuration initialization
2104 config
= config
|| {};
2107 this.$button
= null;
2109 this.active
= config
.active
!== undefined && config
.active
;
2110 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
2111 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2112 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2113 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
2114 this.onClickHandler
= this.onClick
.bind( this );
2115 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2118 this.$element
.addClass( 'oo-ui-buttonElement' );
2119 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2120 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2125 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2127 /* Static Properties */
2130 * Cancel mouse down events.
2132 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
2133 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
2134 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
2139 * @property {boolean}
2141 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2146 * A 'click' event is emitted when the button element is clicked.
2154 * Set the button element.
2156 * This method is used to retarget a button mixin so that its functionality applies to
2157 * the specified button element instead of the one created by the class. If a button element
2158 * is already set, the method will remove the mixin’s effect on that element.
2160 * @param {jQuery} $button Element to use as button
2162 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2163 if ( this.$button
) {
2165 .removeClass( 'oo-ui-buttonElement-button' )
2166 .removeAttr( 'role accesskey' )
2168 mousedown
: this.onMouseDownHandler
,
2169 keydown
: this.onKeyDownHandler
,
2170 click
: this.onClickHandler
,
2171 keypress
: this.onKeyPressHandler
2175 this.$button
= $button
2176 .addClass( 'oo-ui-buttonElement-button' )
2178 mousedown
: this.onMouseDownHandler
,
2179 keydown
: this.onKeyDownHandler
,
2180 click
: this.onClickHandler
,
2181 keypress
: this.onKeyPressHandler
2184 // Add `role="button"` on `<a>` elements, where it's needed
2185 // `toUppercase()` is added for XHTML documents
2186 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2187 this.$button
.attr( 'role', 'button' );
2192 * Handles mouse down events.
2195 * @param {jQuery.Event} e Mouse down event
2197 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2198 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2201 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2202 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2203 // reliably remove the pressed class
2204 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
2205 // Prevent change of focus unless specifically configured otherwise
2206 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2212 * Handles mouse up events.
2215 * @param {MouseEvent} e Mouse up event
2217 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
2218 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2221 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2222 // Stop listening for mouseup, since we only needed this once
2223 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
2227 * Handles mouse click events.
2230 * @param {jQuery.Event} e Mouse click event
2233 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2234 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2235 if ( this.emit( 'click' ) ) {
2242 * Handles key down events.
2245 * @param {jQuery.Event} e Key down event
2247 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2248 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2251 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2252 // Run the keyup handler no matter where the key is when the button is let go, so we can
2253 // reliably remove the pressed class
2254 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
2258 * Handles key up events.
2261 * @param {KeyboardEvent} e Key up event
2263 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
2264 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2267 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2268 // Stop listening for keyup, since we only needed this once
2269 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
2273 * Handles key press events.
2276 * @param {jQuery.Event} e Key press event
2279 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2280 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2281 if ( this.emit( 'click' ) ) {
2288 * Check if button has a frame.
2290 * @return {boolean} Button is framed
2292 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2297 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2299 * @param {boolean} [framed] Make button framed, omit to toggle
2302 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2303 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2304 if ( framed
!== this.framed
) {
2305 this.framed
= framed
;
2307 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2308 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2309 this.updateThemeClasses();
2316 * Set the button's active state.
2318 * The active state can be set on:
2320 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2321 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2322 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2325 * @param {boolean} value Make button active
2328 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2329 this.active
= !!value
;
2330 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2331 this.updateThemeClasses();
2336 * Check if the button is active
2339 * @return {boolean} The button is active
2341 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2346 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2347 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2348 * items from the group is done through the interface the class provides.
2349 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2351 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2354 * @mixins OO.EmitterList
2358 * @param {Object} [config] Configuration options
2359 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2360 * is omitted, the group element will use a generated `<div>`.
2362 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2363 // Configuration initialization
2364 config
= config
|| {};
2366 // Mixin constructors
2367 OO
.EmitterList
.call( this, config
);
2373 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2378 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2385 * A change event is emitted when the set of selected items changes.
2387 * @param {OO.ui.Element[]} items Items currently in the group
2393 * Set the group element.
2395 * If an element is already set, items will be moved to the new element.
2397 * @param {jQuery} $group Element to use as group
2399 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2402 this.$group
= $group
;
2403 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2404 this.$group
.append( this.items
[ i
].$element
);
2409 * Find an item by its data.
2411 * Only the first item with matching data will be returned. To return all matching items,
2412 * use the #findItemsFromData method.
2414 * @param {Object} data Item data to search for
2415 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2417 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2419 hash
= OO
.getHash( data
);
2421 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2422 item
= this.items
[ i
];
2423 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2432 * Find items by their data.
2434 * All items with matching data will be returned. To return only the first match, use the #findItemFromData method instead.
2436 * @param {Object} data Item data to search for
2437 * @return {OO.ui.Element[]} Items with equivalent data
2439 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2441 hash
= OO
.getHash( data
),
2444 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2445 item
= this.items
[ i
];
2446 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2455 * Add items to the group.
2457 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2458 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2460 * @param {OO.ui.Element[]} items An array of items to add to the group
2461 * @param {number} [index] Index of the insertion point
2464 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2466 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2468 this.emit( 'change', this.getItems() );
2475 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2476 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2477 this.insertItemElements( items
, newIndex
);
2480 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2488 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2489 item
.setElementGroup( this );
2490 this.insertItemElements( item
, index
);
2493 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2499 * Insert elements into the group
2502 * @param {OO.ui.Element} itemWidget Item to insert
2503 * @param {number} index Insertion index
2505 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2506 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2507 this.$group
.append( itemWidget
.$element
);
2508 } else if ( index
=== 0 ) {
2509 this.$group
.prepend( itemWidget
.$element
);
2511 this.items
[ index
].$element
.before( itemWidget
.$element
);
2516 * Remove the specified items from a group.
2518 * Removed items are detached (not removed) from the DOM so that they may be reused.
2519 * To remove all items from a group, you may wish to use the #clearItems method instead.
2521 * @param {OO.ui.Element[]} items An array of items to remove
2524 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2525 var i
, len
, item
, index
;
2527 // Remove specific items elements
2528 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2530 index
= this.items
.indexOf( item
);
2531 if ( index
!== -1 ) {
2532 item
.setElementGroup( null );
2533 item
.$element
.detach();
2538 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2540 this.emit( 'change', this.getItems() );
2545 * Clear all items from the group.
2547 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2548 * To remove only a subset of items from a group, use the #removeItems method.
2552 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2555 // Remove all item elements
2556 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2557 this.items
[ i
].setElementGroup( null );
2558 this.items
[ i
].$element
.detach();
2562 OO
.EmitterList
.prototype.clearItems
.call( this );
2564 this.emit( 'change', this.getItems() );
2569 * IconElement is often mixed into other classes to generate an icon.
2570 * Icons are graphics, about the size of normal text. They are used to aid the user
2571 * in locating a control or to convey information in a space-efficient way. See the
2572 * [OOUI documentation on MediaWiki] [1] for a list of icons
2573 * included in the library.
2575 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2581 * @param {Object} [config] Configuration options
2582 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2583 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2584 * the icon element be set to an existing icon instead of the one generated by this class, set a
2585 * value using a jQuery selection. For example:
2587 * // Use a <div> tag instead of a <span>
2589 * // Use an existing icon element instead of the one generated by the class
2590 * $icon: this.$element
2591 * // Use an icon element from a child widget
2592 * $icon: this.childwidget.$element
2593 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2594 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2595 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2596 * by the user's language.
2598 * Example of an i18n map:
2600 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2601 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2602 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2603 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2604 * text. The icon title is displayed when users move the mouse over the icon.
2606 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2607 // Configuration initialization
2608 config
= config
|| {};
2613 this.iconTitle
= null;
2616 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2617 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2618 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2623 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2625 /* Static Properties */
2628 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2629 * for i18n purposes and contains a `default` icon name and additional names keyed by
2630 * language code. The `default` name is used when no icon is keyed by the user's language.
2632 * Example of an i18n map:
2634 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2636 * Note: the static property will be overridden if the #icon configuration is used.
2640 * @property {Object|string}
2642 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2645 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2646 * function that returns title text, or `null` for no title.
2648 * The static property will be overridden if the #iconTitle configuration is used.
2652 * @property {string|Function|null}
2654 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2659 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2660 * applies to the specified icon element instead of the one created by the class. If an icon
2661 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2662 * and mixin methods will no longer affect the element.
2664 * @param {jQuery} $icon Element to use as icon
2666 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2669 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2670 .removeAttr( 'title' );
2674 .addClass( 'oo-ui-iconElement-icon' )
2675 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2676 if ( this.iconTitle
!== null ) {
2677 this.$icon
.attr( 'title', this.iconTitle
);
2680 this.updateThemeClasses();
2684 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2685 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2688 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2689 * by language code, or `null` to remove the icon.
2692 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2693 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2694 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2696 if ( this.icon
!== icon
) {
2698 if ( this.icon
!== null ) {
2699 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2701 if ( icon
!== null ) {
2702 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2708 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2709 this.updateThemeClasses();
2715 * Set the icon title. Use `null` to remove the title.
2717 * @param {string|Function|null} iconTitle A text string used as the icon title,
2718 * a function that returns title text, or `null` for no title.
2721 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2723 ( typeof iconTitle
=== 'function' || ( typeof iconTitle
=== 'string' && iconTitle
.length
) ) ?
2724 OO
.ui
.resolveMsg( iconTitle
) : null;
2726 if ( this.iconTitle
!== iconTitle
) {
2727 this.iconTitle
= iconTitle
;
2729 if ( this.iconTitle
!== null ) {
2730 this.$icon
.attr( 'title', iconTitle
);
2732 this.$icon
.removeAttr( 'title' );
2741 * Get the symbolic name of the icon.
2743 * @return {string} Icon name
2745 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2750 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2752 * @return {string} Icon title text
2754 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2755 return this.iconTitle
;
2759 * IndicatorElement is often mixed into other classes to generate an indicator.
2760 * Indicators are small graphics that are generally used in two ways:
2762 * - To draw attention to the status of an item. For example, an indicator might be
2763 * used to show that an item in a list has errors that need to be resolved.
2764 * - To clarify the function of a control that acts in an exceptional way (a button
2765 * that opens a menu instead of performing an action directly, for example).
2767 * For a list of indicators included in the library, please see the
2768 * [OOUI documentation on MediaWiki] [1].
2770 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2776 * @param {Object} [config] Configuration options
2777 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2778 * configuration is omitted, the indicator element will use a generated `<span>`.
2779 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
2780 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
2782 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2783 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2784 * or a function that returns title text. The indicator title is displayed when users move
2785 * the mouse over the indicator.
2787 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2788 // Configuration initialization
2789 config
= config
|| {};
2792 this.$indicator
= null;
2793 this.indicator
= null;
2794 this.indicatorTitle
= null;
2797 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2798 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2799 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2804 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2806 /* Static Properties */
2809 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
2810 * The static property will be overridden if the #indicator configuration is used.
2814 * @property {string|null}
2816 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2819 * A text string used as the indicator title, a function that returns title text, or `null`
2820 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2824 * @property {string|Function|null}
2826 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2831 * Set the indicator element.
2833 * If an element is already set, it will be cleaned up before setting up the new element.
2835 * @param {jQuery} $indicator Element to use as indicator
2837 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2838 if ( this.$indicator
) {
2840 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2841 .removeAttr( 'title' );
2844 this.$indicator
= $indicator
2845 .addClass( 'oo-ui-indicatorElement-indicator' )
2846 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2847 if ( this.indicatorTitle
!== null ) {
2848 this.$indicator
.attr( 'title', this.indicatorTitle
);
2851 this.updateThemeClasses();
2855 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null` to remove the indicator.
2857 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2860 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2861 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2863 if ( this.indicator
!== indicator
) {
2864 if ( this.$indicator
) {
2865 if ( this.indicator
!== null ) {
2866 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2868 if ( indicator
!== null ) {
2869 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2872 this.indicator
= indicator
;
2875 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2876 this.updateThemeClasses();
2882 * Set the indicator title.
2884 * The title is displayed when a user moves the mouse over the indicator.
2886 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2887 * `null` for no indicator title
2890 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2892 ( typeof indicatorTitle
=== 'function' || ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ) ?
2893 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2895 if ( this.indicatorTitle
!== indicatorTitle
) {
2896 this.indicatorTitle
= indicatorTitle
;
2897 if ( this.$indicator
) {
2898 if ( this.indicatorTitle
!== null ) {
2899 this.$indicator
.attr( 'title', indicatorTitle
);
2901 this.$indicator
.removeAttr( 'title' );
2910 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
2912 * @return {string} Symbolic name of indicator
2914 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2915 return this.indicator
;
2919 * Get the indicator title.
2921 * The title is displayed when a user moves the mouse over the indicator.
2923 * @return {string} Indicator title text
2925 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2926 return this.indicatorTitle
;
2930 * LabelElement is often mixed into other classes to generate a label, which
2931 * helps identify the function of an interface element.
2932 * See the [OOUI documentation on MediaWiki] [1] for more information.
2934 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2940 * @param {Object} [config] Configuration options
2941 * @cfg {jQuery} [$label] The label element created by the class. If this
2942 * configuration is omitted, the label element will use a generated `<span>`.
2943 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2944 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2945 * in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2946 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2948 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2949 // Configuration initialization
2950 config
= config
|| {};
2957 this.setLabel( config
.label
|| this.constructor.static.label
);
2958 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2963 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2968 * @event labelChange
2969 * @param {string} value
2972 /* Static Properties */
2975 * The label text. The label can be specified as a plaintext string, a function that will
2976 * produce a string in the future, or `null` for no label. The static value will
2977 * be overridden if a label is specified with the #label config option.
2981 * @property {string|Function|null}
2983 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2985 /* Static methods */
2988 * Highlight the first occurrence of the query in the given text
2990 * @param {string} text Text
2991 * @param {string} query Query to find
2992 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2993 * @return {jQuery} Text with the first match of the query
2994 * sub-string wrapped in highlighted span
2996 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2999 $result
= $( '<span>' );
3003 qLen
= query
.length
;
3004 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
3005 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
3010 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
3013 if ( !query
.length
|| offset
=== -1 ) {
3014 $result
.text( text
);
3017 document
.createTextNode( text
.slice( 0, offset
) ),
3019 .addClass( 'oo-ui-labelElement-label-highlight' )
3020 .text( text
.slice( offset
, offset
+ query
.length
) ),
3021 document
.createTextNode( text
.slice( offset
+ query
.length
) )
3024 return $result
.contents();
3030 * Set the label element.
3032 * If an element is already set, it will be cleaned up before setting up the new element.
3034 * @param {jQuery} $label Element to use as label
3036 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
3037 if ( this.$label
) {
3038 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
3041 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
3042 this.setLabelContent( this.label
);
3048 * An empty string will result in the label being hidden. A string containing only whitespace will
3049 * be converted to a single ` `.
3051 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
3052 * text; or null for no label
3055 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
3056 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
3057 label
= ( ( typeof label
=== 'string' || label
instanceof jQuery
) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
3059 if ( this.label
!== label
) {
3060 if ( this.$label
) {
3061 this.setLabelContent( label
);
3064 this.emit( 'labelChange' );
3067 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
);
3073 * Set the label as plain text with a highlighted query
3075 * @param {string} text Text label to set
3076 * @param {string} query Substring of text to highlight
3077 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
3080 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
3081 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
3087 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
3088 * text; or null for no label
3090 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
3095 * Set the content of the label.
3097 * Do not call this method until after the label element has been set by #setLabelElement.
3100 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
3101 * text; or null for no label
3103 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
3104 if ( typeof label
=== 'string' ) {
3105 if ( label
.match( /^\s*$/ ) ) {
3106 // Convert whitespace only string to a single non-breaking space
3107 this.$label
.html( ' ' );
3109 this.$label
.text( label
);
3111 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
3112 this.$label
.html( label
.toString() );
3113 } else if ( label
instanceof jQuery
) {
3114 this.$label
.empty().append( label
);
3116 this.$label
.empty();
3121 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3122 * additional functionality to an element created by another class. The class provides
3123 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3124 * which are used to customize the look and feel of a widget to better describe its
3125 * importance and functionality.
3127 * The library currently contains the following styling flags for general use:
3129 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
3130 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
3132 * The flags affect the appearance of the buttons:
3135 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3136 * var button1 = new OO.ui.ButtonWidget( {
3137 * label: 'Progressive',
3138 * flags: 'progressive'
3140 * var button2 = new OO.ui.ButtonWidget( {
3141 * label: 'Destructive',
3142 * flags: 'destructive'
3144 * $( 'body' ).append( button1.$element, button2.$element );
3146 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
3147 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3149 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3155 * @param {Object} [config] Configuration options
3156 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary') to apply.
3157 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3158 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3159 * @cfg {jQuery} [$flagged] The flagged element. By default,
3160 * the flagged functionality is applied to the element created by the class ($element).
3161 * If a different element is specified, the flagged functionality will be applied to it instead.
3163 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3164 // Configuration initialization
3165 config
= config
|| {};
3169 this.$flagged
= null;
3172 this.setFlags( config
.flags
);
3173 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3180 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3181 * parameter contains the name of each modified flag and indicates whether it was
3184 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3185 * that the flag was added, `false` that the flag was removed.
3191 * Set the flagged element.
3193 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3194 * If an element is already set, the method will remove the mixin’s effect on that element.
3196 * @param {jQuery} $flagged Element that should be flagged
3198 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3199 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3200 return 'oo-ui-flaggedElement-' + flag
;
3203 if ( this.$flagged
) {
3204 this.$flagged
.removeClass( classNames
);
3207 this.$flagged
= $flagged
.addClass( classNames
);
3211 * Check if the specified flag is set.
3213 * @param {string} flag Name of flag
3214 * @return {boolean} The flag is set
3216 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3217 // This may be called before the constructor, thus before this.flags is set
3218 return this.flags
&& ( flag
in this.flags
);
3222 * Get the names of all flags set.
3224 * @return {string[]} Flag names
3226 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3227 // This may be called before the constructor, thus before this.flags is set
3228 return Object
.keys( this.flags
|| {} );
3237 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3238 var flag
, className
,
3241 classPrefix
= 'oo-ui-flaggedElement-';
3243 for ( flag
in this.flags
) {
3244 className
= classPrefix
+ flag
;
3245 changes
[ flag
] = false;
3246 delete this.flags
[ flag
];
3247 remove
.push( className
);
3250 if ( this.$flagged
) {
3251 this.$flagged
.removeClass( remove
.join( ' ' ) );
3254 this.updateThemeClasses();
3255 this.emit( 'flag', changes
);
3261 * Add one or more flags.
3263 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3264 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3265 * be added (`true`) or removed (`false`).
3269 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3270 var i
, len
, flag
, className
,
3274 classPrefix
= 'oo-ui-flaggedElement-';
3276 if ( typeof flags
=== 'string' ) {
3277 className
= classPrefix
+ flags
;
3279 if ( !this.flags
[ flags
] ) {
3280 this.flags
[ flags
] = true;
3281 add
.push( className
);
3283 } else if ( Array
.isArray( flags
) ) {
3284 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3286 className
= classPrefix
+ flag
;
3288 if ( !this.flags
[ flag
] ) {
3289 changes
[ flag
] = true;
3290 this.flags
[ flag
] = true;
3291 add
.push( className
);
3294 } else if ( OO
.isPlainObject( flags
) ) {
3295 for ( flag
in flags
) {
3296 className
= classPrefix
+ flag
;
3297 if ( flags
[ flag
] ) {
3299 if ( !this.flags
[ flag
] ) {
3300 changes
[ flag
] = true;
3301 this.flags
[ flag
] = true;
3302 add
.push( className
);
3306 if ( this.flags
[ flag
] ) {
3307 changes
[ flag
] = false;
3308 delete this.flags
[ flag
];
3309 remove
.push( className
);
3315 if ( this.$flagged
) {
3317 .addClass( add
.join( ' ' ) )
3318 .removeClass( remove
.join( ' ' ) );
3321 this.updateThemeClasses();
3322 this.emit( 'flag', changes
);
3328 * TitledElement is mixed into other classes to provide a `title` attribute.
3329 * Titles are rendered by the browser and are made visible when the user moves
3330 * the mouse over the element. Titles are not visible on touch devices.
3333 * // TitledElement provides a 'title' attribute to the
3334 * // ButtonWidget class
3335 * var button = new OO.ui.ButtonWidget( {
3336 * label: 'Button with Title',
3337 * title: 'I am a button'
3339 * $( 'body' ).append( button.$element );
3345 * @param {Object} [config] Configuration options
3346 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3347 * If this config is omitted, the title functionality is applied to $element, the
3348 * element created by the class.
3349 * @cfg {string|Function} [title] The title text or a function that returns text. If
3350 * this config is omitted, the value of the {@link #static-title static title} property is used.
3352 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3353 // Configuration initialization
3354 config
= config
|| {};
3357 this.$titled
= null;
3361 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3362 this.setTitledElement( config
.$titled
|| this.$element
);
3367 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3369 /* Static Properties */
3372 * The title text, a function that returns text, or `null` for no title. The value of the static property
3373 * is overridden if the #title config option is used.
3377 * @property {string|Function|null}
3379 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3384 * Set the titled element.
3386 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3387 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3389 * @param {jQuery} $titled Element that should use the 'titled' functionality
3391 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3392 if ( this.$titled
) {
3393 this.$titled
.removeAttr( 'title' );
3396 this.$titled
= $titled
;
3405 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3408 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3409 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3410 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3412 if ( this.title
!== title
) {
3421 * Update the title attribute, in case of changes to title or accessKey.
3426 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3427 var title
= this.getTitle();
3428 if ( this.$titled
) {
3429 if ( title
!== null ) {
3430 // Only if this is an AccessKeyedElement
3431 if ( this.formatTitleWithAccessKey
) {
3432 title
= this.formatTitleWithAccessKey( title
);
3434 this.$titled
.attr( 'title', title
);
3436 this.$titled
.removeAttr( 'title' );
3445 * @return {string} Title string
3447 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3452 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3453 * Accesskeys allow an user to go to a specific element by using
3454 * a shortcut combination of a browser specific keys + the key
3458 * // AccessKeyedElement provides an 'accesskey' attribute to the
3459 * // ButtonWidget class
3460 * var button = new OO.ui.ButtonWidget( {
3461 * label: 'Button with Accesskey',
3464 * $( 'body' ).append( button.$element );
3470 * @param {Object} [config] Configuration options
3471 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3472 * If this config is omitted, the accesskey functionality is applied to $element, the
3473 * element created by the class.
3474 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3475 * this config is omitted, no accesskey will be added.
3477 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3478 // Configuration initialization
3479 config
= config
|| {};
3482 this.$accessKeyed
= null;
3483 this.accessKey
= null;
3486 this.setAccessKey( config
.accessKey
|| null );
3487 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3489 // If this is also a TitledElement and it initialized before we did, we may have
3490 // to update the title with the access key
3491 if ( this.updateTitle
) {
3498 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3500 /* Static Properties */
3503 * The access key, a function that returns a key, or `null` for no accesskey.
3507 * @property {string|Function|null}
3509 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3514 * Set the accesskeyed element.
3516 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3517 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3519 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3521 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3522 if ( this.$accessKeyed
) {
3523 this.$accessKeyed
.removeAttr( 'accesskey' );
3526 this.$accessKeyed
= $accessKeyed
;
3527 if ( this.accessKey
) {
3528 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3535 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3538 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3539 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3541 if ( this.accessKey
!== accessKey
) {
3542 if ( this.$accessKeyed
) {
3543 if ( accessKey
!== null ) {
3544 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3546 this.$accessKeyed
.removeAttr( 'accesskey' );
3549 this.accessKey
= accessKey
;
3551 // Only if this is a TitledElement
3552 if ( this.updateTitle
) {
3563 * @return {string} accessKey string
3565 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3566 return this.accessKey
;
3570 * Add information about the access key to the element's tooltip label.
3571 * (This is only public for hacky usage in FieldLayout.)
3573 * @param {string} title Tooltip label for `title` attribute
3576 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3579 if ( !this.$accessKeyed
) {
3580 // Not initialized yet; the constructor will call updateTitle() which will rerun this function
3583 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the single key
3584 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3585 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3587 accessKey
= this.getAccessKey();
3590 title
+= ' [' + accessKey
+ ']';
3596 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3597 * feels, and functionality can be customized via the class’s configuration options
3598 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3601 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3604 * // A button widget
3605 * var button = new OO.ui.ButtonWidget( {
3606 * label: 'Button with Icon',
3608 * iconTitle: 'Remove'
3610 * $( 'body' ).append( button.$element );
3612 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3615 * @extends OO.ui.Widget
3616 * @mixins OO.ui.mixin.ButtonElement
3617 * @mixins OO.ui.mixin.IconElement
3618 * @mixins OO.ui.mixin.IndicatorElement
3619 * @mixins OO.ui.mixin.LabelElement
3620 * @mixins OO.ui.mixin.TitledElement
3621 * @mixins OO.ui.mixin.FlaggedElement
3622 * @mixins OO.ui.mixin.TabIndexedElement
3623 * @mixins OO.ui.mixin.AccessKeyedElement
3626 * @param {Object} [config] Configuration options
3627 * @cfg {boolean} [active=false] Whether button should be shown as active
3628 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3629 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3630 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3632 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3633 // Configuration initialization
3634 config
= config
|| {};
3636 // Parent constructor
3637 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3639 // Mixin constructors
3640 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3641 OO
.ui
.mixin
.IconElement
.call( this, config
);
3642 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3643 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3644 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3645 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3646 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3647 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3652 this.noFollow
= false;
3655 this.connect( this, { disable
: 'onDisable' } );
3658 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3660 .addClass( 'oo-ui-buttonWidget' )
3661 .append( this.$button
);
3662 this.setActive( config
.active
);
3663 this.setHref( config
.href
);
3664 this.setTarget( config
.target
);
3665 this.setNoFollow( config
.noFollow
);
3670 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3671 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3672 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3673 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3674 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3675 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3676 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3677 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3678 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3680 /* Static Properties */
3686 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3692 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3697 * Get hyperlink location.
3699 * @return {string} Hyperlink location
3701 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3706 * Get hyperlink target.
3708 * @return {string} Hyperlink target
3710 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3715 * Get search engine traversal hint.
3717 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3719 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3720 return this.noFollow
;
3724 * Set hyperlink location.
3726 * @param {string|null} href Hyperlink location, null to remove
3728 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3729 href
= typeof href
=== 'string' ? href
: null;
3730 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3734 if ( href
!== this.href
) {
3743 * Update the `href` attribute, in case of changes to href or
3749 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3750 if ( this.href
!== null && !this.isDisabled() ) {
3751 this.$button
.attr( 'href', this.href
);
3753 this.$button
.removeAttr( 'href' );
3760 * Handle disable events.
3763 * @param {boolean} disabled Element is disabled
3765 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3770 * Set hyperlink target.
3772 * @param {string|null} target Hyperlink target, null to remove
3774 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3775 target
= typeof target
=== 'string' ? target
: null;
3777 if ( target
!== this.target
) {
3778 this.target
= target
;
3779 if ( target
!== null ) {
3780 this.$button
.attr( 'target', target
);
3782 this.$button
.removeAttr( 'target' );
3790 * Set search engine traversal hint.
3792 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3794 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3795 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3797 if ( noFollow
!== this.noFollow
) {
3798 this.noFollow
= noFollow
;
3800 this.$button
.attr( 'rel', 'nofollow' );
3802 this.$button
.removeAttr( 'rel' );
3809 // Override method visibility hints from ButtonElement
3820 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3821 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3822 * removed, and cleared from the group.
3825 * // Example: A ButtonGroupWidget with two buttons
3826 * var button1 = new OO.ui.PopupButtonWidget( {
3827 * label: 'Select a category',
3830 * $content: $( '<p>List of categories...</p>' ),
3835 * var button2 = new OO.ui.ButtonWidget( {
3838 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3839 * items: [button1, button2]
3841 * $( 'body' ).append( buttonGroup.$element );
3844 * @extends OO.ui.Widget
3845 * @mixins OO.ui.mixin.GroupElement
3848 * @param {Object} [config] Configuration options
3849 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3851 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3852 // Configuration initialization
3853 config
= config
|| {};
3855 // Parent constructor
3856 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3858 // Mixin constructors
3859 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3862 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3863 if ( Array
.isArray( config
.items
) ) {
3864 this.addItems( config
.items
);
3870 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3871 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3873 /* Static Properties */
3879 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
3888 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
3889 if ( !this.isDisabled() ) {
3890 if ( this.items
[ 0 ] ) {
3891 this.items
[ 0 ].focus();
3900 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
3905 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3906 * which creates a label that identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
3907 * for a list of icons included in the library.
3910 * // An icon widget with a label
3911 * var myIcon = new OO.ui.IconWidget( {
3915 * // Create a label.
3916 * var iconLabel = new OO.ui.LabelWidget( {
3919 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3921 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
3924 * @extends OO.ui.Widget
3925 * @mixins OO.ui.mixin.IconElement
3926 * @mixins OO.ui.mixin.TitledElement
3927 * @mixins OO.ui.mixin.FlaggedElement
3930 * @param {Object} [config] Configuration options
3932 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3933 // Configuration initialization
3934 config
= config
|| {};
3936 // Parent constructor
3937 OO
.ui
.IconWidget
.parent
.call( this, config
);
3939 // Mixin constructors
3940 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3941 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3942 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3945 this.$element
.addClass( 'oo-ui-iconWidget' );
3950 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3951 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3952 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3953 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3955 /* Static Properties */
3961 OO
.ui
.IconWidget
.static.tagName
= 'span';
3964 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3965 * attention to the status of an item or to clarify the function within a control. For a list of
3966 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
3969 * // Example of an indicator widget
3970 * var indicator1 = new OO.ui.IndicatorWidget( {
3971 * indicator: 'required'
3974 * // Create a fieldset layout to add a label
3975 * var fieldset = new OO.ui.FieldsetLayout();
3976 * fieldset.addItems( [
3977 * new OO.ui.FieldLayout( indicator1, { label: 'A required indicator:' } )
3979 * $( 'body' ).append( fieldset.$element );
3981 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3984 * @extends OO.ui.Widget
3985 * @mixins OO.ui.mixin.IndicatorElement
3986 * @mixins OO.ui.mixin.TitledElement
3989 * @param {Object} [config] Configuration options
3991 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
3992 // Configuration initialization
3993 config
= config
|| {};
3995 // Parent constructor
3996 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
3998 // Mixin constructors
3999 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
4000 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
4003 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4008 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4009 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4010 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4012 /* Static Properties */
4018 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4021 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4022 * be configured with a `label` option that is set to a string, a label node, or a function:
4024 * - String: a plaintext string
4025 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4026 * label that includes a link or special styling, such as a gray color or additional graphical elements.
4027 * - Function: a function that will produce a string in the future. Functions are used
4028 * in cases where the value of the label is not currently defined.
4030 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
4031 * will come into focus when the label is clicked.
4034 * // Examples of LabelWidgets
4035 * var label1 = new OO.ui.LabelWidget( {
4036 * label: 'plaintext label'
4038 * var label2 = new OO.ui.LabelWidget( {
4039 * label: $( '<a href="default.html">jQuery label</a>' )
4041 * // Create a fieldset layout with fields for each example
4042 * var fieldset = new OO.ui.FieldsetLayout();
4043 * fieldset.addItems( [
4044 * new OO.ui.FieldLayout( label1 ),
4045 * new OO.ui.FieldLayout( label2 )
4047 * $( 'body' ).append( fieldset.$element );
4050 * @extends OO.ui.Widget
4051 * @mixins OO.ui.mixin.LabelElement
4052 * @mixins OO.ui.mixin.TitledElement
4055 * @param {Object} [config] Configuration options
4056 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4057 * Clicking the label will focus the specified input field.
4059 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4060 // Configuration initialization
4061 config
= config
|| {};
4063 // Parent constructor
4064 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4066 // Mixin constructors
4067 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
4068 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4071 this.input
= config
.input
;
4075 if ( this.input
.getInputId() ) {
4076 this.$element
.attr( 'for', this.input
.getInputId() );
4078 this.$label
.on( 'click', function () {
4079 this.input
.simulateLabelClick();
4083 this.$element
.addClass( 'oo-ui-labelWidget' );
4088 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4089 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4090 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4092 /* Static Properties */
4098 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4101 * PendingElement is a mixin that is used to create elements that notify users that something is happening
4102 * and that they should wait before proceeding. The pending state is visually represented with a pending
4103 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
4104 * field of a {@link OO.ui.TextInputWidget text input widget}.
4106 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
4107 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
4108 * in process dialogs.
4111 * function MessageDialog( config ) {
4112 * MessageDialog.parent.call( this, config );
4114 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4116 * MessageDialog.static.name = 'myMessageDialog';
4117 * MessageDialog.static.actions = [
4118 * { action: 'save', label: 'Done', flags: 'primary' },
4119 * { label: 'Cancel', flags: 'safe' }
4122 * MessageDialog.prototype.initialize = function () {
4123 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4124 * this.content = new OO.ui.PanelLayout( { padded: true } );
4125 * 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>' );
4126 * this.$body.append( this.content.$element );
4128 * MessageDialog.prototype.getBodyHeight = function () {
4131 * MessageDialog.prototype.getActionProcess = function ( action ) {
4132 * var dialog = this;
4133 * if ( action === 'save' ) {
4134 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4135 * return new OO.ui.Process()
4137 * .next( function () {
4138 * dialog.getActions().get({actions: 'save'})[0].popPending();
4141 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4144 * var windowManager = new OO.ui.WindowManager();
4145 * $( 'body' ).append( windowManager.$element );
4147 * var dialog = new MessageDialog();
4148 * windowManager.addWindows( [ dialog ] );
4149 * windowManager.openWindow( dialog );
4155 * @param {Object} [config] Configuration options
4156 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4158 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4159 // Configuration initialization
4160 config
= config
|| {};
4164 this.$pending
= null;
4167 this.setPendingElement( config
.$pending
|| this.$element
);
4172 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4177 * Set the pending element (and clean up any existing one).
4179 * @param {jQuery} $pending The element to set to pending.
4181 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4182 if ( this.$pending
) {
4183 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4186 this.$pending
= $pending
;
4187 if ( this.pending
> 0 ) {
4188 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4193 * Check if an element is pending.
4195 * @return {boolean} Element is pending
4197 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4198 return !!this.pending
;
4202 * Increase the pending counter. The pending state will remain active until the counter is zero
4203 * (i.e., the number of calls to #pushPending and #popPending is the same).
4207 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4208 if ( this.pending
=== 0 ) {
4209 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4210 this.updateThemeClasses();
4218 * Decrease the pending counter. The pending state will remain active until the counter is zero
4219 * (i.e., the number of calls to #pushPending and #popPending is the same).
4223 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4224 if ( this.pending
=== 1 ) {
4225 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4226 this.updateThemeClasses();
4228 this.pending
= Math
.max( 0, this.pending
- 1 );
4234 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4235 * in the document (for example, in an OO.ui.Window's $overlay).
4237 * The elements's position is automatically calculated and maintained when window is resized or the
4238 * page is scrolled. If you reposition the container manually, you have to call #position to make
4239 * sure the element is still placed correctly.
4241 * As positioning is only possible when both the element and the container are attached to the DOM
4242 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4243 * the #toggle method to display a floating popup, for example.
4249 * @param {Object} [config] Configuration options
4250 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4251 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4252 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4253 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4254 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4255 * 'top': Align the top edge with $floatableContainer's top edge
4256 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4257 * 'center': Vertically align the center with $floatableContainer's center
4258 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4259 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4260 * 'after': Directly after $floatableContainer, algining f's start edge with fC's end edge
4261 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4262 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4263 * 'center': Horizontally align the center with $floatableContainer's center
4264 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4267 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4268 // Configuration initialization
4269 config
= config
|| {};
4272 this.$floatable
= null;
4273 this.$floatableContainer
= null;
4274 this.$floatableWindow
= null;
4275 this.$floatableClosestScrollable
= null;
4276 this.floatableOutOfView
= false;
4277 this.onFloatableScrollHandler
= this.position
.bind( this );
4278 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4281 this.setFloatableContainer( config
.$floatableContainer
);
4282 this.setFloatableElement( config
.$floatable
|| this.$element
);
4283 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4284 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4285 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ? true : !!config
.hideWhenOutOfView
;
4291 * Set floatable element.
4293 * If an element is already set, it will be cleaned up before setting up the new element.
4295 * @param {jQuery} $floatable Element to make floatable
4297 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4298 if ( this.$floatable
) {
4299 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4300 this.$floatable
.css( { left
: '', top
: '' } );
4303 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4308 * Set floatable container.
4310 * The element will be positioned relative to the specified container.
4312 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4314 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4315 this.$floatableContainer
= $floatableContainer
;
4316 if ( this.$floatable
) {
4322 * Change how the element is positioned vertically.
4324 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4326 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4327 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4328 throw new Error( 'Invalid value for vertical position: ' + position
);
4330 if ( this.verticalPosition
!== position
) {
4331 this.verticalPosition
= position
;
4332 if ( this.$floatable
) {
4339 * Change how the element is positioned horizontally.
4341 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4343 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4344 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4345 throw new Error( 'Invalid value for horizontal position: ' + position
);
4347 if ( this.horizontalPosition
!== position
) {
4348 this.horizontalPosition
= position
;
4349 if ( this.$floatable
) {
4356 * Toggle positioning.
4358 * Do not turn positioning on until after the element is attached to the DOM and visible.
4360 * @param {boolean} [positioning] Enable positioning, omit to toggle
4363 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4364 var closestScrollableOfContainer
;
4366 if ( !this.$floatable
|| !this.$floatableContainer
) {
4370 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4372 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4373 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4374 this.warnedUnattached
= true;
4377 if ( this.positioning
!== positioning
) {
4378 this.positioning
= positioning
;
4380 this.needsCustomPosition
=
4381 this.verticalPostion
!== 'below' ||
4382 this.horizontalPosition
!== 'start' ||
4383 !OO
.ui
.contains( this.$floatableContainer
[ 0 ], this.$floatable
[ 0 ] );
4385 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
4386 // If the scrollable is the root, we have to listen to scroll events
4387 // on the window because of browser inconsistencies.
4388 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4389 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
4392 if ( positioning
) {
4393 this.$floatableWindow
= $( this.getElementWindow() );
4394 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4396 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4397 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4399 // Initial position after visible
4402 if ( this.$floatableWindow
) {
4403 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4404 this.$floatableWindow
= null;
4407 if ( this.$floatableClosestScrollable
) {
4408 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4409 this.$floatableClosestScrollable
= null;
4412 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4420 * Check whether the bottom edge of the given element is within the viewport of the given container.
4423 * @param {jQuery} $element
4424 * @param {jQuery} $container
4427 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4428 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
, rightEdgeInBounds
,
4429 startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4430 direction
= $element
.css( 'direction' );
4432 elemRect
= $element
[ 0 ].getBoundingClientRect();
4433 if ( $container
[ 0 ] === window
) {
4434 viewportSpacing
= OO
.ui
.getViewportSpacing();
4438 right
: document
.documentElement
.clientWidth
,
4439 bottom
: document
.documentElement
.clientHeight
4441 contRect
.top
+= viewportSpacing
.top
;
4442 contRect
.left
+= viewportSpacing
.left
;
4443 contRect
.right
-= viewportSpacing
.right
;
4444 contRect
.bottom
-= viewportSpacing
.bottom
;
4446 contRect
= $container
[ 0 ].getBoundingClientRect();
4449 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4450 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4451 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4452 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4453 if ( direction
=== 'rtl' ) {
4454 startEdgeInBounds
= rightEdgeInBounds
;
4455 endEdgeInBounds
= leftEdgeInBounds
;
4457 startEdgeInBounds
= leftEdgeInBounds
;
4458 endEdgeInBounds
= rightEdgeInBounds
;
4461 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4464 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4467 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4470 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4474 // The other positioning values are all about being inside the container,
4475 // so in those cases all we care about is that any part of the container is visible.
4476 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4477 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4481 * Check if the floatable is hidden to the user because it was offscreen.
4483 * @return {boolean} Floatable is out of view
4485 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4486 return this.floatableOutOfView
;
4490 * Position the floatable below its container.
4492 * This should only be done when both of them are attached to the DOM and visible.
4496 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4497 if ( !this.positioning
) {
4502 // To continue, some things need to be true:
4503 // The element must actually be in the DOM
4504 this.isElementAttached() && (
4505 // The closest scrollable is the current window
4506 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4507 // OR is an element in the element's DOM
4508 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4511 // Abort early if important parts of the widget are no longer attached to the DOM
4515 this.floatableOutOfView
= this.hideWhenOutOfView
&& !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4516 if ( this.floatableOutOfView
) {
4517 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4520 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4523 if ( !this.needsCustomPosition
) {
4527 this.$floatable
.css( this.computePosition() );
4529 // We updated the position, so re-evaluate the clipping state.
4530 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4531 // will not notice the need to update itself.)
4532 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4533 // it not listen to the right events in the right places?
4542 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4543 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4544 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4546 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4548 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4549 var isBody
, scrollableX
, scrollableY
, containerPos
,
4550 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4551 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4552 direction
= this.$floatableContainer
.css( 'direction' ),
4553 $offsetParent
= this.$floatable
.offsetParent();
4555 if ( $offsetParent
.is( 'html' ) ) {
4556 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4557 // <html> element, but they do work on the <body>
4558 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4560 isBody
= $offsetParent
.is( 'body' );
4561 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' || $offsetParent
.css( 'overflow-x' ) === 'auto';
4562 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' || $offsetParent
.css( 'overflow-y' ) === 'auto';
4564 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4565 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4566 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
4567 // or if it isn't scrollable
4568 scrollTop
= scrollableY
&& !isBody
? $offsetParent
.scrollTop() : 0;
4569 scrollLeft
= scrollableX
&& !isBody
? OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4571 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4572 // if the <body> has a margin
4573 containerPos
= isBody
?
4574 this.$floatableContainer
.offset() :
4575 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4576 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4577 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4578 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4579 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4581 if ( this.verticalPosition
=== 'below' ) {
4582 newPos
.top
= containerPos
.bottom
;
4583 } else if ( this.verticalPosition
=== 'above' ) {
4584 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4585 } else if ( this.verticalPosition
=== 'top' ) {
4586 newPos
.top
= containerPos
.top
;
4587 } else if ( this.verticalPosition
=== 'bottom' ) {
4588 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4589 } else if ( this.verticalPosition
=== 'center' ) {
4590 newPos
.top
= containerPos
.top
+
4591 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4594 if ( this.horizontalPosition
=== 'before' ) {
4595 newPos
.end
= containerPos
.start
;
4596 } else if ( this.horizontalPosition
=== 'after' ) {
4597 newPos
.start
= containerPos
.end
;
4598 } else if ( this.horizontalPosition
=== 'start' ) {
4599 newPos
.start
= containerPos
.start
;
4600 } else if ( this.horizontalPosition
=== 'end' ) {
4601 newPos
.end
= containerPos
.end
;
4602 } else if ( this.horizontalPosition
=== 'center' ) {
4603 newPos
.left
= containerPos
.left
+
4604 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4607 if ( newPos
.start
!== undefined ) {
4608 if ( direction
=== 'rtl' ) {
4609 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.start
;
4611 newPos
.left
= newPos
.start
;
4613 delete newPos
.start
;
4615 if ( newPos
.end
!== undefined ) {
4616 if ( direction
=== 'rtl' ) {
4617 newPos
.left
= newPos
.end
;
4619 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.end
;
4624 // Account for scroll position
4625 if ( newPos
.top
!== '' ) {
4626 newPos
.top
+= scrollTop
;
4628 if ( newPos
.bottom
!== '' ) {
4629 newPos
.bottom
-= scrollTop
;
4631 if ( newPos
.left
!== '' ) {
4632 newPos
.left
+= scrollLeft
;
4634 if ( newPos
.right
!== '' ) {
4635 newPos
.right
-= scrollLeft
;
4638 // Account for scrollbar gutter
4639 if ( newPos
.bottom
!== '' ) {
4640 newPos
.bottom
-= horizScrollbarHeight
;
4642 if ( direction
=== 'rtl' ) {
4643 if ( newPos
.left
!== '' ) {
4644 newPos
.left
-= vertScrollbarWidth
;
4647 if ( newPos
.right
!== '' ) {
4648 newPos
.right
-= vertScrollbarWidth
;
4656 * Element that can be automatically clipped to visible boundaries.
4658 * Whenever the element's natural height changes, you have to call
4659 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4660 * clipping correctly.
4662 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4663 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4664 * then #$clippable will be given a fixed reduced height and/or width and will be made
4665 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4666 * but you can build a static footer by setting #$clippableContainer to an element that contains
4667 * #$clippable and the footer.
4673 * @param {Object} [config] Configuration options
4674 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4675 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4676 * omit to use #$clippable
4678 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4679 // Configuration initialization
4680 config
= config
|| {};
4683 this.$clippable
= null;
4684 this.$clippableContainer
= null;
4685 this.clipping
= false;
4686 this.clippedHorizontally
= false;
4687 this.clippedVertically
= false;
4688 this.$clippableScrollableContainer
= null;
4689 this.$clippableScroller
= null;
4690 this.$clippableWindow
= null;
4691 this.idealWidth
= null;
4692 this.idealHeight
= null;
4693 this.onClippableScrollHandler
= this.clip
.bind( this );
4694 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4697 if ( config
.$clippableContainer
) {
4698 this.setClippableContainer( config
.$clippableContainer
);
4700 this.setClippableElement( config
.$clippable
|| this.$element
);
4706 * Set clippable element.
4708 * If an element is already set, it will be cleaned up before setting up the new element.
4710 * @param {jQuery} $clippable Element to make clippable
4712 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4713 if ( this.$clippable
) {
4714 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4715 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4716 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4719 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4724 * Set clippable container.
4726 * This is the container that will be measured when deciding whether to clip. When clipping,
4727 * #$clippable will be resized in order to keep the clippable container fully visible.
4729 * If the clippable container is unset, #$clippable will be used.
4731 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4733 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4734 this.$clippableContainer
= $clippableContainer
;
4735 if ( this.$clippable
) {
4743 * Do not turn clipping on until after the element is attached to the DOM and visible.
4745 * @param {boolean} [clipping] Enable clipping, omit to toggle
4748 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4749 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4751 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4752 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4753 this.warnedUnattached
= true;
4756 if ( this.clipping
!== clipping
) {
4757 this.clipping
= clipping
;
4759 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4760 // If the clippable container is the root, we have to listen to scroll events and check
4761 // jQuery.scrollTop on the window because of browser inconsistencies
4762 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4763 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4764 this.$clippableScrollableContainer
;
4765 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4766 this.$clippableWindow
= $( this.getElementWindow() )
4767 .on( 'resize', this.onClippableWindowResizeHandler
);
4768 // Initial clip after visible
4771 this.$clippable
.css( {
4779 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4781 this.$clippableScrollableContainer
= null;
4782 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4783 this.$clippableScroller
= null;
4784 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4785 this.$clippableWindow
= null;
4793 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4795 * @return {boolean} Element will be clipped to the visible area
4797 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4798 return this.clipping
;
4802 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4804 * @return {boolean} Part of the element is being clipped
4806 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4807 return this.clippedHorizontally
|| this.clippedVertically
;
4811 * Check if the right of the element is being clipped by the nearest scrollable container.
4813 * @return {boolean} Part of the element is being clipped
4815 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4816 return this.clippedHorizontally
;
4820 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4822 * @return {boolean} Part of the element is being clipped
4824 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4825 return this.clippedVertically
;
4829 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4831 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4832 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4834 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4835 this.idealWidth
= width
;
4836 this.idealHeight
= height
;
4838 if ( !this.clipping
) {
4839 // Update dimensions
4840 this.$clippable
.css( { width
: width
, height
: height
} );
4842 // While clipping, idealWidth and idealHeight are not considered
4846 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4847 * ClippableElement will clip the opposite side when reducing element's width.
4849 * Classes that mix in ClippableElement should override this to return 'right' if their
4850 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
4851 * If your class also mixes in FloatableElement, this is handled automatically.
4853 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4854 * always in pixels, even if they were unset or set to 'auto'.)
4856 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
4858 * @return {string} 'left' or 'right'
4860 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
4861 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
4868 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4869 * ClippableElement will clip the opposite side when reducing element's width.
4871 * Classes that mix in ClippableElement should override this to return 'bottom' if their
4872 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
4873 * If your class also mixes in FloatableElement, this is handled automatically.
4875 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4876 * always in pixels, even if they were unset or set to 'auto'.)
4878 * When in doubt, 'top' is a sane fallback.
4880 * @return {string} 'top' or 'bottom'
4882 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
4883 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
4890 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4891 * when the element's natural height changes.
4893 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4894 * overlapped by, the visible area of the nearest scrollable container.
4896 * Because calling clip() when the natural height changes isn't always possible, we also set
4897 * max-height when the element isn't being clipped. This means that if the element tries to grow
4898 * beyond the edge, something reasonable will happen before clip() is called.
4902 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
4903 var extraHeight
, extraWidth
, viewportSpacing
,
4904 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
4905 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
4906 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
4907 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
4908 // Extra tolerance so that the sloppy code below doesn't result in results that are off
4909 // by one or two pixels. (And also so that we have space to display drop shadows.)
4910 // Chosen by fair dice roll.
4913 if ( !this.clipping
) {
4914 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4918 function rectIntersection( a
, b
) {
4920 out
.top
= Math
.max( a
.top
, b
.top
);
4921 out
.left
= Math
.max( a
.left
, b
.left
);
4922 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
4923 out
.right
= Math
.min( a
.right
, b
.right
);
4927 viewportSpacing
= OO
.ui
.getViewportSpacing();
4929 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
4930 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
4931 // Dimensions of the browser window, rather than the element!
4935 right
: document
.documentElement
.clientWidth
,
4936 bottom
: document
.documentElement
.clientHeight
4938 viewportRect
.top
+= viewportSpacing
.top
;
4939 viewportRect
.left
+= viewportSpacing
.left
;
4940 viewportRect
.right
-= viewportSpacing
.right
;
4941 viewportRect
.bottom
-= viewportSpacing
.bottom
;
4943 $viewport
= this.$clippableScrollableContainer
;
4944 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
4945 // Convert into a plain object
4946 viewportRect
= $.extend( {}, viewportRect
);
4949 // Account for scrollbar gutter
4950 direction
= $viewport
.css( 'direction' );
4951 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
4952 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
4953 viewportRect
.bottom
-= horizScrollbarHeight
;
4954 if ( direction
=== 'rtl' ) {
4955 viewportRect
.left
+= vertScrollbarWidth
;
4957 viewportRect
.right
-= vertScrollbarWidth
;
4960 // Add arbitrary tolerance
4961 viewportRect
.top
+= buffer
;
4962 viewportRect
.left
+= buffer
;
4963 viewportRect
.right
-= buffer
;
4964 viewportRect
.bottom
-= buffer
;
4966 $item
= this.$clippableContainer
|| this.$clippable
;
4968 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
4969 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
4971 itemRect
= $item
[ 0 ].getBoundingClientRect();
4972 // Convert into a plain object
4973 itemRect
= $.extend( {}, itemRect
);
4975 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
4976 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
4977 if ( this.getHorizontalAnchorEdge() === 'right' ) {
4978 itemRect
.left
= viewportRect
.left
;
4980 itemRect
.right
= viewportRect
.right
;
4982 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
4983 itemRect
.top
= viewportRect
.top
;
4985 itemRect
.bottom
= viewportRect
.bottom
;
4988 availableRect
= rectIntersection( viewportRect
, itemRect
);
4990 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
4991 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
4992 // It should never be desirable to exceed the dimensions of the browser viewport... right?
4993 desiredWidth
= Math
.min( desiredWidth
,
4994 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
4995 desiredHeight
= Math
.min( desiredHeight
,
4996 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
4997 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
4998 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
4999 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5000 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5001 clipWidth
= allotedWidth
< naturalWidth
;
5002 clipHeight
= allotedHeight
< naturalHeight
;
5005 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5006 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5007 this.$clippable
.css( 'overflowX', 'scroll' );
5008 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5009 this.$clippable
.css( {
5010 width
: Math
.max( 0, allotedWidth
),
5014 this.$clippable
.css( {
5016 width
: this.idealWidth
|| '',
5017 maxWidth
: Math
.max( 0, allotedWidth
)
5021 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5022 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5023 this.$clippable
.css( 'overflowY', 'scroll' );
5024 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5025 this.$clippable
.css( {
5026 height
: Math
.max( 0, allotedHeight
),
5030 this.$clippable
.css( {
5032 height
: this.idealHeight
|| '',
5033 maxHeight
: Math
.max( 0, allotedHeight
)
5037 // If we stopped clipping in at least one of the dimensions
5038 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5039 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5042 this.clippedHorizontally
= clipWidth
;
5043 this.clippedVertically
= clipHeight
;
5049 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5050 * By default, each popup has an anchor that points toward its origin.
5051 * Please see the [OOUI documentation on Mediawiki] [1] for more information and examples.
5053 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5056 * // A popup widget.
5057 * var popup = new OO.ui.PopupWidget( {
5058 * $content: $( '<p>Hi there!</p>' ),
5063 * $( 'body' ).append( popup.$element );
5064 * // To display the popup, toggle the visibility to 'true'.
5065 * popup.toggle( true );
5067 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5070 * @extends OO.ui.Widget
5071 * @mixins OO.ui.mixin.LabelElement
5072 * @mixins OO.ui.mixin.ClippableElement
5073 * @mixins OO.ui.mixin.FloatableElement
5076 * @param {Object} [config] Configuration options
5077 * @cfg {number} [width=320] Width of popup in pixels
5078 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
5079 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5080 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5081 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5082 * of $floatableContainer
5083 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5084 * of $floatableContainer
5085 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5086 * endwards (right/left) to the vertical center of $floatableContainer
5087 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5088 * startwards (left/right) to the vertical center of $floatableContainer
5089 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5090 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
5091 * as possible while still keeping the anchor within the popup;
5092 * if position is before/after, move the popup as far downwards as possible.
5093 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
5094 * as possible while still keeping the anchor within the popup;
5095 * if position in before/after, move the popup as far upwards as possible.
5096 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center
5097 * of the popup with the center of $floatableContainer.
5098 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5099 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5100 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5101 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5102 * desired direction to display the popup without clipping
5103 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5104 * See the [OOUI docs on MediaWiki][3] for an example.
5105 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5106 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
5107 * @cfg {jQuery} [$content] Content to append to the popup's body
5108 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5109 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5110 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5111 * This config option is only relevant if #autoClose is set to `true`. See the [OOUI documentation on MediaWiki][2]
5113 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5114 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5116 * @cfg {boolean} [padded=false] Add padding to the popup's body
5118 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5119 // Configuration initialization
5120 config
= config
|| {};
5122 // Parent constructor
5123 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5125 // Properties (must be set before ClippableElement constructor call)
5126 this.$body
= $( '<div>' );
5127 this.$popup
= $( '<div>' );
5129 // Mixin constructors
5130 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5131 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
5132 $clippable
: this.$body
,
5133 $clippableContainer
: this.$popup
5135 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5138 this.$anchor
= $( '<div>' );
5139 // If undefined, will be computed lazily in computePosition()
5140 this.$container
= config
.$container
;
5141 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5142 this.autoClose
= !!config
.autoClose
;
5143 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
5144 this.transitionTimeout
= null;
5145 this.anchored
= false;
5146 this.width
= config
.width
!== undefined ? config
.width
: 320;
5147 this.height
= config
.height
!== undefined ? config
.height
: null;
5148 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
5149 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5152 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5153 this.setAlignment( config
.align
|| 'center' );
5154 this.setPosition( config
.position
|| 'below' );
5155 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5156 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5157 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5159 .addClass( 'oo-ui-popupWidget-popup' )
5160 .append( this.$body
);
5162 .addClass( 'oo-ui-popupWidget' )
5163 .append( this.$popup
, this.$anchor
);
5164 // Move content, which was added to #$element by OO.ui.Widget, to the body
5165 // FIXME This is gross, we should use '$body' or something for the config
5166 if ( config
.$content
instanceof jQuery
) {
5167 this.$body
.append( config
.$content
);
5170 if ( config
.padded
) {
5171 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5174 if ( config
.head
) {
5175 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
5176 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
5177 this.$head
= $( '<div>' )
5178 .addClass( 'oo-ui-popupWidget-head' )
5179 .append( this.$label
, this.closeButton
.$element
);
5180 this.$popup
.prepend( this.$head
);
5183 if ( config
.$footer
) {
5184 this.$footer
= $( '<div>' )
5185 .addClass( 'oo-ui-popupWidget-footer' )
5186 .append( config
.$footer
);
5187 this.$popup
.append( this.$footer
);
5190 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5191 // that reference properties not initialized at that time of parent class construction
5192 // TODO: Find a better way to handle post-constructor setup
5193 this.visible
= false;
5194 this.$element
.addClass( 'oo-ui-element-hidden' );
5199 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5200 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5201 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5202 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5209 * The popup is ready: it is visible and has been positioned and clipped.
5215 * Handles mouse down events.
5218 * @param {MouseEvent} e Mouse down event
5220 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
5223 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5225 this.toggle( false );
5230 * Bind mouse down listener.
5234 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
5235 // Capture clicks outside popup
5236 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
5240 * Handles close button click events.
5244 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5245 if ( this.isVisible() ) {
5246 this.toggle( false );
5251 * Unbind mouse down listener.
5255 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
5256 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
5260 * Handles key down events.
5263 * @param {KeyboardEvent} e Key down event
5265 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5267 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5270 this.toggle( false );
5272 e
.stopPropagation();
5277 * Bind key down listener.
5281 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
5282 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5286 * Unbind key down listener.
5290 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
5291 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5295 * Show, hide, or toggle the visibility of the anchor.
5297 * @param {boolean} [show] Show anchor, omit to toggle
5299 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5300 show
= show
=== undefined ? !this.anchored
: !!show
;
5302 if ( this.anchored
!== show
) {
5304 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5305 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5307 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5308 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5310 this.anchored
= show
;
5315 * Change which edge the anchor appears on.
5317 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5319 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5320 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5321 throw new Error( 'Invalid value for edge: ' + edge
);
5323 if ( this.anchorEdge
!== null ) {
5324 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5326 this.anchorEdge
= edge
;
5327 if ( this.anchored
) {
5328 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5333 * Check if the anchor is visible.
5335 * @return {boolean} Anchor is visible
5337 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5338 return this.anchored
;
5342 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5343 * `.toggle( true )` after its #$element is attached to the DOM.
5345 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5346 * it in the right place and with the right dimensions only work correctly while it is attached.
5347 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5348 * strictly enforced, so currently it only generates a warning in the browser console.
5353 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5354 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5355 show
= show
=== undefined ? !this.isVisible() : !!show
;
5357 change
= show
!== this.isVisible();
5359 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5360 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5361 this.warnedUnattached
= true;
5363 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5364 // Fall back to the parent node if the floatableContainer is not set
5365 this.setFloatableContainer( this.$element
.parent() );
5368 if ( change
&& show
&& this.autoFlip
) {
5369 // Reset auto-flipping before showing the popup again. It's possible we no longer need to flip
5370 // (e.g. if the user scrolled).
5371 this.isAutoFlipped
= false;
5375 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5378 this.togglePositioning( show
&& !!this.$floatableContainer
);
5381 if ( this.autoClose
) {
5382 this.bindMouseDownListener();
5383 this.bindKeyDownListener();
5385 this.updateDimensions();
5386 this.toggleClipping( true );
5388 if ( this.autoFlip
) {
5389 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5390 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5391 // If opening the popup in the normal direction causes it to be clipped, open
5392 // in the opposite one instead
5393 normalHeight
= this.$element
.height();
5394 this.isAutoFlipped
= !this.isAutoFlipped
;
5396 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5397 // If that also causes it to be clipped, open in whichever direction
5398 // we have more space
5399 oppositeHeight
= this.$element
.height();
5400 if ( oppositeHeight
< normalHeight
) {
5401 this.isAutoFlipped
= !this.isAutoFlipped
;
5407 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5408 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5409 // If opening the popup in the normal direction causes it to be clipped, open
5410 // in the opposite one instead
5411 normalWidth
= this.$element
.width();
5412 this.isAutoFlipped
= !this.isAutoFlipped
;
5413 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5414 // which causes positioning to be off. Toggle clipping back and fort to work around.
5415 this.toggleClipping( false );
5417 this.toggleClipping( true );
5418 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5419 // If that also causes it to be clipped, open in whichever direction
5420 // we have more space
5421 oppositeWidth
= this.$element
.width();
5422 if ( oppositeWidth
< normalWidth
) {
5423 this.isAutoFlipped
= !this.isAutoFlipped
;
5424 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5425 // which causes positioning to be off. Toggle clipping back and fort to work around.
5426 this.toggleClipping( false );
5428 this.toggleClipping( true );
5435 this.emit( 'ready' );
5437 this.toggleClipping( false );
5438 if ( this.autoClose
) {
5439 this.unbindMouseDownListener();
5440 this.unbindKeyDownListener();
5449 * Set the size of the popup.
5451 * Changing the size may also change the popup's position depending on the alignment.
5453 * @param {number} width Width in pixels
5454 * @param {number} height Height in pixels
5455 * @param {boolean} [transition=false] Use a smooth transition
5458 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5460 this.height
= height
!== undefined ? height
: null;
5461 if ( this.isVisible() ) {
5462 this.updateDimensions( transition
);
5467 * Update the size and position.
5469 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5470 * be called automatically.
5472 * @param {boolean} [transition=false] Use a smooth transition
5475 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5478 // Prevent transition from being interrupted
5479 clearTimeout( this.transitionTimeout
);
5481 // Enable transition
5482 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5488 // Prevent transitioning after transition is complete
5489 this.transitionTimeout
= setTimeout( function () {
5490 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5493 // Prevent transitioning immediately
5494 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5501 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5502 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
, anchorPos
,
5503 anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
, floatablePos
,
5504 offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5506 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5507 popupPositionOppositeMap
= {
5515 'force-left': 'backwards',
5516 'force-right': 'forwards'
5519 'force-left': 'forwards',
5520 'force-right': 'backwards'
5532 backwards
: this.anchored
? 'before' : 'end'
5540 if ( !this.$container
) {
5541 // Lazy-initialize $container if not specified in constructor
5542 this.$container
= $( this.getClosestScrollableElementContainer() );
5544 direction
= this.$container
.css( 'direction' );
5546 // Set height and width before we do anything else, since it might cause our measurements
5547 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5550 height
: this.height
!== null ? this.height
: 'auto'
5553 align
= alignMap
[ direction
][ this.align
] || this.align
;
5554 popupPosition
= this.popupPosition
;
5555 if ( this.isAutoFlipped
) {
5556 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5559 // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
5560 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5561 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5562 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5563 near
= vertical
? 'top' : 'left';
5564 far
= vertical
? 'bottom' : 'right';
5565 sizeProp
= vertical
? 'Height' : 'Width';
5566 popupSize
= vertical
? ( this.height
|| this.$popup
.height() ) : this.width
;
5568 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5569 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5570 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5573 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5574 // Find out which property FloatableElement used for positioning, and adjust that value
5575 positionProp
= vertical
?
5576 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5577 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5579 // Figure out where the near and far edges of the popup and $floatableContainer are
5580 floatablePos
= this.$floatableContainer
.offset();
5581 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5582 // Measure where the offsetParent is and compute our position based on that and parentPosition
5583 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5584 { top
: 0, left
: 0 } :
5585 this.$element
.offsetParent().offset();
5587 if ( positionProp
=== near
) {
5588 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5589 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5591 popupPos
[ far
] = offsetParentPos
[ near
] +
5592 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5593 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5596 if ( this.anchored
) {
5597 // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
5598 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5599 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5601 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
5602 // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
5603 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5604 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5605 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5606 // Not enough space for the anchor on the start side; pull the popup startwards
5607 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5608 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5609 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5610 // Not enough space for the anchor on the end side; pull the popup endwards
5611 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5612 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5614 positionAdjustment
= 0;
5617 positionAdjustment
= 0;
5620 // Check if the popup will go beyond the edge of this.$container
5621 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5622 { top
: 0, left
: 0 } :
5623 this.$container
.offset();
5624 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5625 if ( this.$container
[ 0 ] === document
.documentElement
) {
5626 viewportSpacing
= OO
.ui
.getViewportSpacing();
5627 containerPos
[ near
] += viewportSpacing
[ near
];
5628 containerPos
[ far
] -= viewportSpacing
[ far
];
5630 // Take into account how much the popup will move because of the adjustments we're going to make
5631 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5632 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5633 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5634 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5635 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5636 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5637 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5638 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5639 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5640 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5643 if ( this.anchored
) {
5644 // Adjust anchorOffset for positionAdjustment
5645 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5647 // Position the anchor
5648 anchorCss
[ start
] = anchorOffset
;
5649 this.$anchor
.css( anchorCss
);
5652 // Move the popup if needed
5653 parentPosition
[ positionProp
] += positionAdjustment
;
5655 return parentPosition
;
5659 * Set popup alignment
5661 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5662 * `backwards` or `forwards`.
5664 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5665 // Validate alignment
5666 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5669 this.align
= 'center';
5675 * Get popup alignment
5677 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5678 * `backwards` or `forwards`.
5680 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5685 * Change the positioning of the popup.
5687 * @param {string} position 'above', 'below', 'before' or 'after'
5689 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5690 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5693 this.popupPosition
= position
;
5698 * Get popup positioning.
5700 * @return {string} 'above', 'below', 'before' or 'after'
5702 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5703 return this.popupPosition
;
5707 * Set popup auto-flipping.
5709 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
5710 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5711 * desired direction to display the popup without clipping
5713 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
5714 autoFlip
= !!autoFlip
;
5716 if ( this.autoFlip
!== autoFlip
) {
5717 this.autoFlip
= autoFlip
;
5722 * Get an ID of the body element, this can be used as the
5723 * `aria-describedby` attribute for an input field.
5725 * @return {string} The ID of the body element
5727 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
5728 var id
= this.$body
.attr( 'id' );
5729 if ( id
=== undefined ) {
5730 id
= OO
.ui
.generateElementId();
5731 this.$body
.attr( 'id', id
);
5737 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5738 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5739 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5740 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5746 * @param {Object} [config] Configuration options
5747 * @cfg {Object} [popup] Configuration to pass to popup
5748 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5750 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5751 // Configuration initialization
5752 config
= config
|| {};
5755 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5758 $floatableContainer
: this.$element
5762 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5772 * @return {OO.ui.PopupWidget} Popup widget
5774 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5779 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5780 * which is used to display additional information or options.
5783 * // Example of a popup button.
5784 * var popupButton = new OO.ui.PopupButtonWidget( {
5785 * label: 'Popup button with options',
5788 * $content: $( '<p>Additional options here.</p>' ),
5790 * align: 'force-left'
5793 * // Append the button to the DOM.
5794 * $( 'body' ).append( popupButton.$element );
5797 * @extends OO.ui.ButtonWidget
5798 * @mixins OO.ui.mixin.PopupElement
5801 * @param {Object} [config] Configuration options
5802 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
5803 * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
5804 * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
5805 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
5807 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
5808 // Configuration initialization
5809 config
= config
|| {};
5811 // Parent constructor
5812 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
5814 // Mixin constructors
5815 OO
.ui
.mixin
.PopupElement
.call( this, config
);
5818 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
5821 this.connect( this, { click
: 'onAction' } );
5825 .addClass( 'oo-ui-popupButtonWidget' );
5827 .addClass( 'oo-ui-popupButtonWidget-popup' )
5828 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
5829 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
5830 this.$overlay
.append( this.popup
.$element
);
5835 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
5836 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
5841 * Handle the button action being triggered.
5845 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
5846 this.popup
.toggle();
5850 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
5852 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
5857 * @mixins OO.ui.mixin.GroupElement
5860 * @param {Object} [config] Configuration options
5862 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
5863 // Mixin constructors
5864 OO
.ui
.mixin
.GroupElement
.call( this, config
);
5869 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
5874 * Set the disabled state of the widget.
5876 * This will also update the disabled state of child widgets.
5878 * @param {boolean} disabled Disable widget
5881 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
5885 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
5886 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
5888 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
5890 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5891 this.items
[ i
].updateDisabled();
5899 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
5901 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
5902 * allows bidirectional communication.
5904 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
5912 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
5919 * Check if widget is disabled.
5921 * Checks parent if present, making disabled state inheritable.
5923 * @return {boolean} Widget is disabled
5925 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
5926 return this.disabled
||
5927 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
5931 * Set group element is in.
5933 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
5936 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
5938 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
5939 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
5941 // Initialize item disabled states
5942 this.updateDisabled();
5948 * OptionWidgets are special elements that can be selected and configured with data. The
5949 * data is often unique for each option, but it does not have to be. OptionWidgets are used
5950 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
5951 * and examples, please see the [OOUI documentation on MediaWiki][1].
5953 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
5956 * @extends OO.ui.Widget
5957 * @mixins OO.ui.mixin.ItemWidget
5958 * @mixins OO.ui.mixin.LabelElement
5959 * @mixins OO.ui.mixin.FlaggedElement
5960 * @mixins OO.ui.mixin.AccessKeyedElement
5963 * @param {Object} [config] Configuration options
5965 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
5966 // Configuration initialization
5967 config
= config
|| {};
5969 // Parent constructor
5970 OO
.ui
.OptionWidget
.parent
.call( this, config
);
5972 // Mixin constructors
5973 OO
.ui
.mixin
.ItemWidget
.call( this );
5974 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5975 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
5976 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
5979 this.selected
= false;
5980 this.highlighted
= false;
5981 this.pressed
= false;
5985 .data( 'oo-ui-optionWidget', this )
5986 // Allow programmatic focussing (and by accesskey), but not tabbing
5987 .attr( 'tabindex', '-1' )
5988 .attr( 'role', 'option' )
5989 .attr( 'aria-selected', 'false' )
5990 .addClass( 'oo-ui-optionWidget' )
5991 .append( this.$label
);
5996 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
5997 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
5998 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
5999 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6000 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6002 /* Static Properties */
6005 * Whether this option can be selected. See #setSelected.
6009 * @property {boolean}
6011 OO
.ui
.OptionWidget
.static.selectable
= true;
6014 * Whether this option can be highlighted. See #setHighlighted.
6018 * @property {boolean}
6020 OO
.ui
.OptionWidget
.static.highlightable
= true;
6023 * Whether this option can be pressed. See #setPressed.
6027 * @property {boolean}
6029 OO
.ui
.OptionWidget
.static.pressable
= true;
6032 * Whether this option will be scrolled into view when it is selected.
6036 * @property {boolean}
6038 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6043 * Check if the option can be selected.
6045 * @return {boolean} Item is selectable
6047 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6048 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6052 * Check if the option can be highlighted. A highlight indicates that the option
6053 * may be selected when a user presses enter or clicks. Disabled items cannot
6056 * @return {boolean} Item is highlightable
6058 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6059 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6063 * Check if the option can be pressed. The pressed state occurs when a user mouses
6064 * down on an item, but has not yet let go of the mouse.
6066 * @return {boolean} Item is pressable
6068 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6069 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6073 * Check if the option is selected.
6075 * @return {boolean} Item is selected
6077 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6078 return this.selected
;
6082 * Check if the option is highlighted. A highlight indicates that the
6083 * item may be selected when a user presses enter or clicks.
6085 * @return {boolean} Item is highlighted
6087 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6088 return this.highlighted
;
6092 * Check if the option is pressed. The pressed state occurs when a user mouses
6093 * down on an item, but has not yet let go of the mouse. The item may appear
6094 * selected, but it will not be selected until the user releases the mouse.
6096 * @return {boolean} Item is pressed
6098 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6099 return this.pressed
;
6103 * Set the option’s selected state. In general, all modifications to the selection
6104 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6105 * method instead of this method.
6107 * @param {boolean} [state=false] Select option
6110 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6111 if ( this.constructor.static.selectable
) {
6112 this.selected
= !!state
;
6114 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6115 .attr( 'aria-selected', state
.toString() );
6116 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6117 this.scrollElementIntoView();
6119 this.updateThemeClasses();
6125 * Set the option’s highlighted state. In general, all programmatic
6126 * modifications to the highlight should be handled by the
6127 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6128 * method instead of this method.
6130 * @param {boolean} [state=false] Highlight option
6133 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6134 if ( this.constructor.static.highlightable
) {
6135 this.highlighted
= !!state
;
6136 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6137 this.updateThemeClasses();
6143 * Set the option’s pressed state. In general, all
6144 * programmatic modifications to the pressed state should be handled by the
6145 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6146 * method instead of this method.
6148 * @param {boolean} [state=false] Press option
6151 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6152 if ( this.constructor.static.pressable
) {
6153 this.pressed
= !!state
;
6154 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6155 this.updateThemeClasses();
6161 * Get text to match search strings against.
6163 * The default implementation returns the label text, but subclasses
6164 * can override this to provide more complex behavior.
6166 * @return {string|boolean} String to match search string against
6168 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6169 var label
= this.getLabel();
6170 return typeof label
=== 'string' ? label
: this.$label
.text();
6174 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6175 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6176 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6179 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
6180 * information, please see the [OOUI documentation on MediaWiki][1].
6183 * // Example of a select widget with three options
6184 * var select = new OO.ui.SelectWidget( {
6186 * new OO.ui.OptionWidget( {
6188 * label: 'Option One',
6190 * new OO.ui.OptionWidget( {
6192 * label: 'Option Two',
6194 * new OO.ui.OptionWidget( {
6196 * label: 'Option Three',
6200 * $( 'body' ).append( select.$element );
6202 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6206 * @extends OO.ui.Widget
6207 * @mixins OO.ui.mixin.GroupWidget
6210 * @param {Object} [config] Configuration options
6211 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6212 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6213 * the [OOUI documentation on MediaWiki] [2] for examples.
6214 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6216 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6217 // Configuration initialization
6218 config
= config
|| {};
6220 // Parent constructor
6221 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6223 // Mixin constructors
6224 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
6227 this.pressed
= false;
6228 this.selecting
= null;
6229 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
6230 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
6231 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
6232 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
6233 this.keyPressBuffer
= '';
6234 this.keyPressBufferTimer
= null;
6235 this.blockMouseOverEvents
= 0;
6238 this.connect( this, {
6242 focusin
: this.onFocus
.bind( this ),
6243 mousedown
: this.onMouseDown
.bind( this ),
6244 mouseover
: this.onMouseOver
.bind( this ),
6245 mouseleave
: this.onMouseLeave
.bind( this )
6250 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
6251 .attr( 'role', 'listbox' );
6252 this.setFocusOwner( this.$element
);
6253 if ( Array
.isArray( config
.items
) ) {
6254 this.addItems( config
.items
);
6260 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6261 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6268 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6270 * @param {OO.ui.OptionWidget|null} item Highlighted item
6276 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6277 * pressed state of an option.
6279 * @param {OO.ui.OptionWidget|null} item Pressed item
6285 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
6287 * @param {OO.ui.OptionWidget|null} item Selected item
6292 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6293 * @param {OO.ui.OptionWidget} item Chosen item
6299 * An `add` event is emitted when options are added to the select with the #addItems method.
6301 * @param {OO.ui.OptionWidget[]} items Added items
6302 * @param {number} index Index of insertion point
6308 * A `remove` event is emitted when options are removed from the select with the #clearItems
6309 * or #removeItems methods.
6311 * @param {OO.ui.OptionWidget[]} items Removed items
6317 * Handle focus events
6320 * @param {jQuery.Event} event
6322 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6324 if ( event
.target
=== this.$element
[ 0 ] ) {
6325 // This widget was focussed, e.g. by the user tabbing to it.
6326 // The styles for focus state depend on one of the items being selected.
6327 if ( !this.findSelectedItem() ) {
6328 item
= this.findFirstSelectableItem();
6331 if ( event
.target
.tabIndex
=== -1 ) {
6332 // One of the options got focussed (and the event bubbled up here).
6333 // They can't be tabbed to, but they can be activated using accesskeys.
6334 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6335 item
= this.findTargetItem( event
);
6337 // There is something actually user-focusable in one of the labels of the options, and the
6338 // user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change the focus).
6344 if ( item
.constructor.static.highlightable
) {
6345 this.highlightItem( item
);
6347 this.selectItem( item
);
6351 if ( event
.target
!== this.$element
[ 0 ] ) {
6352 this.$focusOwner
.focus();
6357 * Handle mouse down events.
6360 * @param {jQuery.Event} e Mouse down event
6362 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6365 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6366 this.togglePressed( true );
6367 item
= this.findTargetItem( e
);
6368 if ( item
&& item
.isSelectable() ) {
6369 this.pressItem( item
);
6370 this.selecting
= item
;
6371 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
6372 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
6379 * Handle mouse up events.
6382 * @param {MouseEvent} e Mouse up event
6384 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
6387 this.togglePressed( false );
6388 if ( !this.selecting
) {
6389 item
= this.findTargetItem( e
);
6390 if ( item
&& item
.isSelectable() ) {
6391 this.selecting
= item
;
6394 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6395 this.pressItem( null );
6396 this.chooseItem( this.selecting
);
6397 this.selecting
= null;
6400 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
6401 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
6407 * Handle mouse move events.
6410 * @param {MouseEvent} e Mouse move event
6412 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
6415 if ( !this.isDisabled() && this.pressed
) {
6416 item
= this.findTargetItem( e
);
6417 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6418 this.pressItem( item
);
6419 this.selecting
= item
;
6425 * Handle mouse over events.
6428 * @param {jQuery.Event} e Mouse over event
6430 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6432 if ( this.blockMouseOverEvents
) {
6435 if ( !this.isDisabled() ) {
6436 item
= this.findTargetItem( e
);
6437 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6443 * Handle mouse leave events.
6446 * @param {jQuery.Event} e Mouse over event
6448 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6449 if ( !this.isDisabled() ) {
6450 this.highlightItem( null );
6456 * Handle key down events.
6459 * @param {KeyboardEvent} e Key down event
6461 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
6464 currentItem
= this.findHighlightedItem() || this.findSelectedItem();
6466 if ( !this.isDisabled() && this.isVisible() ) {
6467 switch ( e
.keyCode
) {
6468 case OO
.ui
.Keys
.ENTER
:
6469 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6470 // Was only highlighted, now let's select it. No-op if already selected.
6471 this.chooseItem( currentItem
);
6476 case OO
.ui
.Keys
.LEFT
:
6477 this.clearKeyPressBuffer();
6478 nextItem
= this.findRelativeSelectableItem( currentItem
, -1 );
6481 case OO
.ui
.Keys
.DOWN
:
6482 case OO
.ui
.Keys
.RIGHT
:
6483 this.clearKeyPressBuffer();
6484 nextItem
= this.findRelativeSelectableItem( currentItem
, 1 );
6487 case OO
.ui
.Keys
.ESCAPE
:
6488 case OO
.ui
.Keys
.TAB
:
6489 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6490 currentItem
.setHighlighted( false );
6492 this.unbindKeyDownListener();
6493 this.unbindKeyPressListener();
6494 // Don't prevent tabbing away / defocusing
6500 if ( nextItem
.constructor.static.highlightable
) {
6501 this.highlightItem( nextItem
);
6503 this.chooseItem( nextItem
);
6505 this.scrollItemIntoView( nextItem
);
6510 e
.stopPropagation();
6516 * Bind key down listener.
6520 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
6521 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
6525 * Unbind key down listener.
6529 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
6530 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
6534 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6536 * @param {OO.ui.OptionWidget} item Item to scroll into view
6538 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6540 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
6541 // and around 100-150 ms after it is finished.
6542 this.blockMouseOverEvents
++;
6543 item
.scrollElementIntoView().done( function () {
6544 setTimeout( function () {
6545 widget
.blockMouseOverEvents
--;
6551 * Clear the key-press buffer
6555 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6556 if ( this.keyPressBufferTimer
) {
6557 clearTimeout( this.keyPressBufferTimer
);
6558 this.keyPressBufferTimer
= null;
6560 this.keyPressBuffer
= '';
6564 * Handle key press events.
6567 * @param {KeyboardEvent} e Key press event
6569 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
6570 var c
, filter
, item
;
6572 if ( !e
.charCode
) {
6573 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6574 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6579 if ( String
.fromCodePoint
) {
6580 c
= String
.fromCodePoint( e
.charCode
);
6582 c
= String
.fromCharCode( e
.charCode
);
6585 if ( this.keyPressBufferTimer
) {
6586 clearTimeout( this.keyPressBufferTimer
);
6588 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6590 item
= this.findHighlightedItem() || this.findSelectedItem();
6592 if ( this.keyPressBuffer
=== c
) {
6593 // Common (if weird) special case: typing "xxxx" will cycle through all
6594 // the items beginning with "x".
6596 item
= this.findRelativeSelectableItem( item
, 1 );
6599 this.keyPressBuffer
+= c
;
6602 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6603 if ( !item
|| !filter( item
) ) {
6604 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6607 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6608 this.highlightItem( item
);
6610 this.chooseItem( item
);
6612 this.scrollItemIntoView( item
);
6616 e
.stopPropagation();
6620 * Get a matcher for the specific string
6623 * @param {string} s String to match against items
6624 * @param {boolean} [exact=false] Only accept exact matches
6625 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6627 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
6630 if ( s
.normalize
) {
6633 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
6634 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
6638 re
= new RegExp( re
, 'i' );
6639 return function ( item
) {
6640 var matchText
= item
.getMatchText();
6641 if ( matchText
.normalize
) {
6642 matchText
= matchText
.normalize();
6644 return re
.test( matchText
);
6649 * Bind key press listener.
6653 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
6654 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
6658 * Unbind key down listener.
6660 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6665 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
6666 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
6667 this.clearKeyPressBuffer();
6671 * Visibility change handler
6674 * @param {boolean} visible
6676 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6678 this.clearKeyPressBuffer();
6683 * Get the closest item to a jQuery.Event.
6686 * @param {jQuery.Event} e
6687 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6689 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
6690 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
6691 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
6694 return $option
.data( 'oo-ui-optionWidget' ) || null;
6698 * Find selected item.
6700 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6702 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
6705 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6706 if ( this.items
[ i
].isSelected() ) {
6707 return this.items
[ i
];
6714 * Find highlighted item.
6716 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6718 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
6721 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6722 if ( this.items
[ i
].isHighlighted() ) {
6723 return this.items
[ i
];
6730 * Toggle pressed state.
6732 * Press is a state that occurs when a user mouses down on an item, but
6733 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
6734 * until the user releases the mouse.
6736 * @param {boolean} pressed An option is being pressed
6738 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
6739 if ( pressed
=== undefined ) {
6740 pressed
= !this.pressed
;
6742 if ( pressed
!== this.pressed
) {
6744 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
6745 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
6746 this.pressed
= pressed
;
6751 * Highlight an option. If the `item` param is omitted, no options will be highlighted
6752 * and any existing highlight will be removed. The highlight is mutually exclusive.
6754 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
6758 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
6759 var i
, len
, highlighted
,
6762 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6763 highlighted
= this.items
[ i
] === item
;
6764 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
6765 this.items
[ i
].setHighlighted( highlighted
);
6771 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
6773 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
6775 this.emit( 'highlight', item
);
6782 * Fetch an item by its label.
6784 * @param {string} label Label of the item to select.
6785 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6786 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
6788 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
6790 len
= this.items
.length
,
6791 filter
= this.getItemMatcher( label
, true );
6793 for ( i
= 0; i
< len
; i
++ ) {
6794 item
= this.items
[ i
];
6795 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6802 filter
= this.getItemMatcher( label
, false );
6803 for ( i
= 0; i
< len
; i
++ ) {
6804 item
= this.items
[ i
];
6805 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6821 * Programmatically select an option by its label. If the item does not exist,
6822 * all options will be deselected.
6824 * @param {string} [label] Label of the item to select.
6825 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6829 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
6830 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
6831 if ( label
=== undefined || !itemFromLabel
) {
6832 return this.selectItem();
6834 return this.selectItem( itemFromLabel
);
6838 * Programmatically select an option by its data. If the `data` parameter is omitted,
6839 * or if the item does not exist, all options will be deselected.
6841 * @param {Object|string} [data] Value of the item to select, omit to deselect all
6845 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
6846 var itemFromData
= this.findItemFromData( data
);
6847 if ( data
=== undefined || !itemFromData
) {
6848 return this.selectItem();
6850 return this.selectItem( itemFromData
);
6854 * Programmatically select an option by its reference. If the `item` parameter is omitted,
6855 * all options will be deselected.
6857 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
6861 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
6862 var i
, len
, selected
,
6865 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6866 selected
= this.items
[ i
] === item
;
6867 if ( this.items
[ i
].isSelected() !== selected
) {
6868 this.items
[ i
].setSelected( selected
);
6873 if ( item
&& !item
.constructor.static.highlightable
) {
6875 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
6877 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
6880 this.emit( 'select', item
);
6889 * Press is a state that occurs when a user mouses down on an item, but has not
6890 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
6891 * releases the mouse.
6893 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
6897 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
6898 var i
, len
, pressed
,
6901 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6902 pressed
= this.items
[ i
] === item
;
6903 if ( this.items
[ i
].isPressed() !== pressed
) {
6904 this.items
[ i
].setPressed( pressed
);
6909 this.emit( 'press', item
);
6918 * Note that ‘choose’ should never be modified programmatically. A user can choose
6919 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
6920 * use the #selectItem method.
6922 * This method is identical to #selectItem, but may vary in subclasses that take additional action
6923 * when users choose an item with the keyboard or mouse.
6925 * @param {OO.ui.OptionWidget} item Item to choose
6929 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
6931 this.selectItem( item
);
6932 this.emit( 'choose', item
);
6939 * Find an option by its position relative to the specified item (or to the start of the option array,
6940 * if item is `null`). The direction in which to search through the option array is specified with a
6941 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6942 * `null` if there are no options in the array.
6944 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6945 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6946 * @param {Function} [filter] Only consider items for which this function returns
6947 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
6948 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
6950 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
6951 var currentIndex
, nextIndex
, i
,
6952 increase
= direction
> 0 ? 1 : -1,
6953 len
= this.items
.length
;
6955 if ( item
instanceof OO
.ui
.OptionWidget
) {
6956 currentIndex
= this.items
.indexOf( item
);
6957 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
6959 // If no item is selected and moving forward, start at the beginning.
6960 // If moving backward, start at the end.
6961 nextIndex
= direction
> 0 ? 0 : len
- 1;
6964 for ( i
= 0; i
< len
; i
++ ) {
6965 item
= this.items
[ nextIndex
];
6967 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
6968 ( !filter
|| filter( item
) )
6972 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
6978 * Find the next selectable item or `null` if there are no selectable items.
6979 * Disabled options and menu-section markers and breaks are not selectable.
6981 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
6983 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
6984 return this.findRelativeSelectableItem( null, 1 );
6988 * Add an array of options to the select. Optionally, an index number can be used to
6989 * specify an insertion point.
6991 * @param {OO.ui.OptionWidget[]} items Items to add
6992 * @param {number} [index] Index to insert items after
6996 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
6998 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7000 // Always provide an index, even if it was omitted
7001 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7007 * Remove the specified array of options from the select. Options will be detached
7008 * from the DOM, not removed, so they can be reused later. To remove all options from
7009 * the select, you may wish to use the #clearItems method instead.
7011 * @param {OO.ui.OptionWidget[]} items Items to remove
7015 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7018 // Deselect items being removed
7019 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7021 if ( item
.isSelected() ) {
7022 this.selectItem( null );
7027 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7029 this.emit( 'remove', items
);
7035 * Clear all options from the select. Options will be detached from the DOM, not removed,
7036 * so that they can be reused later. To remove a subset of options from the select, use
7037 * the #removeItems method.
7042 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7043 var items
= this.items
.slice();
7046 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7049 this.selectItem( null );
7051 this.emit( 'remove', items
);
7057 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7059 * Currently this is just used to set `aria-activedescendant` on it.
7062 * @param {jQuery} $focusOwner
7064 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7065 this.$focusOwner
= $focusOwner
;
7069 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7070 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
7071 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7072 * options. For more information about options and selects, please see the
7073 * [OOUI documentation on MediaWiki][1].
7076 * // Decorated options in a select widget
7077 * var select = new OO.ui.SelectWidget( {
7079 * new OO.ui.DecoratedOptionWidget( {
7081 * label: 'Option with icon',
7084 * new OO.ui.DecoratedOptionWidget( {
7086 * label: 'Option with indicator',
7091 * $( 'body' ).append( select.$element );
7093 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7096 * @extends OO.ui.OptionWidget
7097 * @mixins OO.ui.mixin.IconElement
7098 * @mixins OO.ui.mixin.IndicatorElement
7101 * @param {Object} [config] Configuration options
7103 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7104 // Parent constructor
7105 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7107 // Mixin constructors
7108 OO
.ui
.mixin
.IconElement
.call( this, config
);
7109 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7113 .addClass( 'oo-ui-decoratedOptionWidget' )
7114 .prepend( this.$icon
)
7115 .append( this.$indicator
);
7120 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7121 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7122 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7125 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7126 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7127 * the [OOUI documentation on MediaWiki] [1] for more information.
7129 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7132 * @extends OO.ui.DecoratedOptionWidget
7135 * @param {Object} [config] Configuration options
7137 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7138 // Parent constructor
7139 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7142 this.checkIcon
= new OO
.ui
.IconWidget( {
7144 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7149 .prepend( this.checkIcon
.$element
)
7150 .addClass( 'oo-ui-menuOptionWidget' );
7155 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7157 /* Static Properties */
7163 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7166 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
7167 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
7170 * var myDropdown = new OO.ui.DropdownWidget( {
7173 * new OO.ui.MenuSectionOptionWidget( {
7176 * new OO.ui.MenuOptionWidget( {
7178 * label: 'Welsh Corgi'
7180 * new OO.ui.MenuOptionWidget( {
7182 * label: 'Standard Poodle'
7184 * new OO.ui.MenuSectionOptionWidget( {
7187 * new OO.ui.MenuOptionWidget( {
7194 * $( 'body' ).append( myDropdown.$element );
7197 * @extends OO.ui.DecoratedOptionWidget
7200 * @param {Object} [config] Configuration options
7202 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7203 // Parent constructor
7204 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7207 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' )
7208 .removeAttr( 'role aria-selected' );
7213 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7215 /* Static Properties */
7221 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7227 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7230 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7231 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7232 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
7233 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7234 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7235 * and customized to be opened, closed, and displayed as needed.
7237 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7238 * mouse outside the menu.
7240 * Menus also have support for keyboard interaction:
7242 * - Enter/Return key: choose and select a menu option
7243 * - Up-arrow key: highlight the previous menu option
7244 * - Down-arrow key: highlight the next menu option
7245 * - Esc key: hide the menu
7247 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7249 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7250 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7253 * @extends OO.ui.SelectWidget
7254 * @mixins OO.ui.mixin.ClippableElement
7255 * @mixins OO.ui.mixin.FloatableElement
7258 * @param {Object} [config] Configuration options
7259 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
7260 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
7261 * and {@link OO.ui.mixin.LookupElement LookupElement}
7262 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7263 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
7264 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
7265 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
7266 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
7267 * that button, unless the button (or its parent widget) is passed in here.
7268 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7269 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7270 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7271 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7272 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7273 * @cfg {number} [width] Width of the menu
7275 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7276 // Configuration initialization
7277 config
= config
|| {};
7279 // Parent constructor
7280 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7282 // Mixin constructors
7283 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
7284 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7286 // Initial vertical positions other than 'center' will result in
7287 // the menu being flipped if there is not enough space in the container.
7288 // Store the original position so we know what to reset to.
7289 this.originalVerticalPosition
= this.verticalPosition
;
7292 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7293 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7294 this.filterFromInput
= !!config
.filterFromInput
;
7295 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7296 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7297 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7298 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7299 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7300 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7301 this.width
= config
.width
;
7304 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7305 if ( config
.widget
) {
7306 this.setFocusOwner( config
.widget
.$tabIndexed
);
7309 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7310 // that reference properties not initialized at that time of parent class construction
7311 // TODO: Find a better way to handle post-constructor setup
7312 this.visible
= false;
7313 this.$element
.addClass( 'oo-ui-element-hidden' );
7318 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7319 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7320 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7327 * The menu is ready: it is visible and has been positioned and clipped.
7330 /* Static properties */
7333 * Positions to flip to if there isn't room in the container for the
7334 * menu in a specific direction.
7336 * @property {Object.<string,string>}
7338 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7348 * Handles document mouse down events.
7351 * @param {MouseEvent} e Mouse down event
7353 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7357 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7362 this.toggle( false );
7369 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
7370 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7372 if ( !this.isDisabled() && this.isVisible() ) {
7373 switch ( e
.keyCode
) {
7374 case OO
.ui
.Keys
.LEFT
:
7375 case OO
.ui
.Keys
.RIGHT
:
7376 // Do nothing if a text field is associated, arrow keys will be handled natively
7377 if ( !this.$input
) {
7378 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
7381 case OO
.ui
.Keys
.ESCAPE
:
7382 case OO
.ui
.Keys
.TAB
:
7383 if ( currentItem
) {
7384 currentItem
.setHighlighted( false );
7386 this.toggle( false );
7387 // Don't prevent tabbing away, prevent defocusing
7388 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7390 e
.stopPropagation();
7394 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
7401 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7402 * or after items were added/removed (always).
7406 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7407 var i
, item
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7408 firstItemFound
= false,
7410 len
= this.items
.length
,
7411 showAll
= !this.isVisible(),
7414 if ( this.$input
&& this.filterFromInput
) {
7415 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
7416 exactFilter
= this.getItemMatcher( this.$input
.val(), true );
7418 // Hide non-matching options, and also hide section headers if all options
7419 // in their section are hidden.
7420 for ( i
= 0; i
< len
; i
++ ) {
7421 item
= this.items
[ i
];
7422 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7424 // If the previous section was empty, hide its header
7425 section
.toggle( showAll
|| !sectionEmpty
);
7428 sectionEmpty
= true;
7429 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7430 visible
= showAll
|| filter( item
);
7431 exactMatch
= exactMatch
|| exactFilter( item
);
7432 anyVisible
= anyVisible
|| visible
;
7433 sectionEmpty
= sectionEmpty
&& !visible
;
7434 item
.toggle( visible
);
7435 if ( this.highlightOnFilter
&& visible
&& !firstItemFound
) {
7436 // Highlight the first item in the list
7437 this.highlightItem( item
);
7438 firstItemFound
= true;
7442 // Process the final section
7444 section
.toggle( showAll
|| !sectionEmpty
);
7447 if ( anyVisible
&& this.items
.length
&& !exactMatch
) {
7448 this.scrollItemIntoView( this.items
[ 0 ] );
7451 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7454 // Reevaluate clipping
7461 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
7462 if ( this.$input
) {
7463 this.$input
.on( 'keydown', this.onKeyDownHandler
);
7465 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
7472 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
7473 if ( this.$input
) {
7474 this.$input
.off( 'keydown', this.onKeyDownHandler
);
7476 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
7483 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
7484 if ( this.$input
) {
7485 if ( this.filterFromInput
) {
7486 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7487 this.updateItemVisibility();
7490 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
7497 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
7498 if ( this.$input
) {
7499 if ( this.filterFromInput
) {
7500 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7501 this.updateItemVisibility();
7504 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
7511 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
7513 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
7514 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
7516 * @param {OO.ui.OptionWidget} item Item to choose
7519 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7520 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7521 if ( this.hideOnChoose
) {
7522 this.toggle( false );
7530 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7532 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7534 this.updateItemVisibility();
7542 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7544 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7546 this.updateItemVisibility();
7554 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7556 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
7558 this.updateItemVisibility();
7564 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7565 * `.toggle( true )` after its #$element is attached to the DOM.
7567 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7568 * it in the right place and with the right dimensions only work correctly while it is attached.
7569 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7570 * strictly enforced, so currently it only generates a warning in the browser console.
7575 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7576 var change
, originalHeight
, flippedHeight
;
7578 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7579 change
= visible
!== this.isVisible();
7581 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7582 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7583 this.warnedUnattached
= true;
7586 if ( change
&& visible
) {
7587 // Reset position before showing the popup again. It's possible we no longer need to flip
7588 // (e.g. if the user scrolled).
7589 this.setVerticalPosition( this.originalVerticalPosition
);
7593 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7599 this.setIdealSize( this.width
);
7600 } else if ( this.$floatableContainer
) {
7601 this.$clippable
.css( 'width', 'auto' );
7603 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
7604 // Dropdown is smaller than handle so expand to width
7605 this.$floatableContainer
[ 0 ].offsetWidth
:
7606 // Dropdown is larger than handle so auto size
7609 this.$clippable
.css( 'width', '' );
7612 this.togglePositioning( !!this.$floatableContainer
);
7613 this.toggleClipping( true );
7615 this.bindKeyDownListener();
7616 this.bindKeyPressListener();
7619 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
7620 this.originalVerticalPosition
!== 'center'
7622 // If opening the menu in one direction causes it to be clipped, flip it
7623 originalHeight
= this.$element
.height();
7624 this.setVerticalPosition(
7625 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
7627 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
7628 // If flipping also causes it to be clipped, open in whichever direction
7629 // we have more space
7630 flippedHeight
= this.$element
.height();
7631 if ( originalHeight
> flippedHeight
) {
7632 this.setVerticalPosition( this.originalVerticalPosition
);
7636 // Note that we do not flip the menu's opening direction if the clipping changes
7637 // later (e.g. after the user scrolls), that seems like it would be annoying
7639 this.$focusOwner
.attr( 'aria-expanded', 'true' );
7641 if ( this.findSelectedItem() ) {
7642 this.$focusOwner
.attr( 'aria-activedescendant', this.findSelectedItem().getElementId() );
7643 this.findSelectedItem().scrollElementIntoView( { duration
: 0 } );
7647 if ( this.autoHide
) {
7648 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7651 this.emit( 'ready' );
7653 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7654 this.unbindKeyDownListener();
7655 this.unbindKeyPressListener();
7656 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7657 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7658 this.togglePositioning( false );
7659 this.toggleClipping( false );
7667 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
7668 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
7669 * users can interact with it.
7671 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7672 * OO.ui.DropdownInputWidget instead.
7675 * // Example: A DropdownWidget with a menu that contains three options
7676 * var dropDown = new OO.ui.DropdownWidget( {
7677 * label: 'Dropdown menu: Select a menu option',
7680 * new OO.ui.MenuOptionWidget( {
7684 * new OO.ui.MenuOptionWidget( {
7688 * new OO.ui.MenuOptionWidget( {
7696 * $( 'body' ).append( dropDown.$element );
7698 * dropDown.getMenu().selectItemByData( 'b' );
7700 * dropDown.getMenu().findSelectedItem().getData(); // returns 'b'
7702 * For more information, please see the [OOUI documentation on MediaWiki] [1].
7704 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7707 * @extends OO.ui.Widget
7708 * @mixins OO.ui.mixin.IconElement
7709 * @mixins OO.ui.mixin.IndicatorElement
7710 * @mixins OO.ui.mixin.LabelElement
7711 * @mixins OO.ui.mixin.TitledElement
7712 * @mixins OO.ui.mixin.TabIndexedElement
7715 * @param {Object} [config] Configuration options
7716 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.MenuSelectWidget menu select widget}
7717 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
7718 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
7719 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
7720 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
7722 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
7723 // Configuration initialization
7724 config
= $.extend( { indicator
: 'down' }, config
);
7726 // Parent constructor
7727 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
7729 // Properties (must be set before TabIndexedElement constructor call)
7730 this.$handle
= $( '<span>' );
7731 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
7733 // Mixin constructors
7734 OO
.ui
.mixin
.IconElement
.call( this, config
);
7735 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7736 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7737 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
7738 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
7741 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
7743 $floatableContainer
: this.$element
7748 click
: this.onClick
.bind( this ),
7749 keydown
: this.onKeyDown
.bind( this ),
7750 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
7751 keypress
: this.menu
.onKeyPressHandler
,
7752 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
7754 this.menu
.connect( this, {
7755 select
: 'onMenuSelect',
7756 toggle
: 'onMenuToggle'
7761 .addClass( 'oo-ui-dropdownWidget-handle' )
7764 'aria-owns': this.menu
.getElementId(),
7765 'aria-autocomplete': 'list'
7767 .append( this.$icon
, this.$label
, this.$indicator
);
7769 .addClass( 'oo-ui-dropdownWidget' )
7770 .append( this.$handle
);
7771 this.$overlay
.append( this.menu
.$element
);
7776 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
7777 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
7778 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
7779 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
7780 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
7781 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7788 * @return {OO.ui.MenuSelectWidget} Menu of widget
7790 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
7795 * Handles menu select events.
7798 * @param {OO.ui.MenuOptionWidget} item Selected menu item
7800 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
7804 this.setLabel( null );
7808 selectedLabel
= item
.getLabel();
7810 // If the label is a DOM element, clone it, because setLabel will append() it
7811 if ( selectedLabel
instanceof jQuery
) {
7812 selectedLabel
= selectedLabel
.clone();
7815 this.setLabel( selectedLabel
);
7819 * Handle menu toggle events.
7822 * @param {boolean} isVisible Open state of the menu
7824 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
7825 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
7828 this.$element
.hasClass( 'oo-ui-dropdownWidget-open' ).toString()
7833 * Handle mouse click events.
7836 * @param {jQuery.Event} e Mouse click event
7838 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
7839 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7846 * Handle key down events.
7849 * @param {jQuery.Event} e Key down event
7851 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
7853 !this.isDisabled() &&
7855 e
.which
=== OO
.ui
.Keys
.ENTER
||
7857 e
.which
=== OO
.ui
.Keys
.SPACE
&&
7858 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
7859 // Space only closes the menu is the user is not typing to search.
7860 this.menu
.keyPressBuffer
=== ''
7863 !this.menu
.isVisible() &&
7865 e
.which
=== OO
.ui
.Keys
.UP
||
7866 e
.which
=== OO
.ui
.Keys
.DOWN
7877 * RadioOptionWidget is an option widget that looks like a radio button.
7878 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
7879 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
7881 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
7884 * @extends OO.ui.OptionWidget
7887 * @param {Object} [config] Configuration options
7889 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
7890 // Configuration initialization
7891 config
= config
|| {};
7893 // Properties (must be done before parent constructor which calls #setDisabled)
7894 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
7896 // Parent constructor
7897 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
7900 // Remove implicit role, we're handling it ourselves
7901 this.radio
.$input
.attr( 'role', 'presentation' );
7903 .addClass( 'oo-ui-radioOptionWidget' )
7904 .attr( 'role', 'radio' )
7905 .attr( 'aria-checked', 'false' )
7906 .removeAttr( 'aria-selected' )
7907 .prepend( this.radio
.$element
);
7912 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
7914 /* Static Properties */
7920 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
7926 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
7932 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
7938 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
7945 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
7946 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
7948 this.radio
.setSelected( state
);
7950 .attr( 'aria-checked', state
.toString() )
7951 .removeAttr( 'aria-selected' );
7959 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
7960 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
7962 this.radio
.setDisabled( this.isDisabled() );
7968 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
7969 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
7970 * an interface for adding, removing and selecting options.
7971 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7973 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7974 * OO.ui.RadioSelectInputWidget instead.
7977 * // A RadioSelectWidget with RadioOptions.
7978 * var option1 = new OO.ui.RadioOptionWidget( {
7980 * label: 'Selected radio option'
7983 * var option2 = new OO.ui.RadioOptionWidget( {
7985 * label: 'Unselected radio option'
7988 * var radioSelect=new OO.ui.RadioSelectWidget( {
7989 * items: [ option1, option2 ]
7992 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
7993 * radioSelect.selectItem( option1 );
7995 * $( 'body' ).append( radioSelect.$element );
7997 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8001 * @extends OO.ui.SelectWidget
8002 * @mixins OO.ui.mixin.TabIndexedElement
8005 * @param {Object} [config] Configuration options
8007 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8008 // Parent constructor
8009 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8011 // Mixin constructors
8012 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8016 focus
: this.bindKeyDownListener
.bind( this ),
8017 blur
: this.unbindKeyDownListener
.bind( this )
8022 .addClass( 'oo-ui-radioSelectWidget' )
8023 .attr( 'role', 'radiogroup' );
8028 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8029 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8032 * MultioptionWidgets are special elements that can be selected and configured with data. The
8033 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8034 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8035 * and examples, please see the [OOUI documentation on MediaWiki][1].
8037 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Multioptions
8040 * @extends OO.ui.Widget
8041 * @mixins OO.ui.mixin.ItemWidget
8042 * @mixins OO.ui.mixin.LabelElement
8045 * @param {Object} [config] Configuration options
8046 * @cfg {boolean} [selected=false] Whether the option is initially selected
8048 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8049 // Configuration initialization
8050 config
= config
|| {};
8052 // Parent constructor
8053 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8055 // Mixin constructors
8056 OO
.ui
.mixin
.ItemWidget
.call( this );
8057 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8060 this.selected
= null;
8064 .addClass( 'oo-ui-multioptionWidget' )
8065 .append( this.$label
);
8066 this.setSelected( config
.selected
);
8071 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8072 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8073 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8080 * A change event is emitted when the selected state of the option changes.
8082 * @param {boolean} selected Whether the option is now selected
8088 * Check if the option is selected.
8090 * @return {boolean} Item is selected
8092 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8093 return this.selected
;
8097 * Set the option’s selected state. In general, all modifications to the selection
8098 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
8099 * method instead of this method.
8101 * @param {boolean} [state=false] Select option
8104 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8106 if ( this.selected
!== state
) {
8107 this.selected
= state
;
8108 this.emit( 'change', state
);
8109 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8115 * MultiselectWidget allows selecting multiple options from a list.
8117 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
8119 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8123 * @extends OO.ui.Widget
8124 * @mixins OO.ui.mixin.GroupWidget
8127 * @param {Object} [config] Configuration options
8128 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8130 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8131 // Parent constructor
8132 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8134 // Configuration initialization
8135 config
= config
|| {};
8137 // Mixin constructors
8138 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8141 this.aggregate( { change
: 'select' } );
8142 // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
8143 // by GroupElement only when items are added/removed
8144 this.connect( this, { select
: [ 'emit', 'change' ] } );
8147 if ( config
.items
) {
8148 this.addItems( config
.items
);
8150 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8151 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8152 .append( this.$group
);
8157 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8158 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8165 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8171 * A select event is emitted when an item is selected or deselected.
8177 * Find options that are selected.
8179 * @return {OO.ui.MultioptionWidget[]} Selected options
8181 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8182 return this.items
.filter( function ( item
) {
8183 return item
.isSelected();
8188 * Find the data of options that are selected.
8190 * @return {Object[]|string[]} Values of selected options
8192 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8193 return this.findSelectedItems().map( function ( item
) {
8199 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8201 * @param {OO.ui.MultioptionWidget[]} items Items to select
8204 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8205 this.items
.forEach( function ( item
) {
8206 var selected
= items
.indexOf( item
) !== -1;
8207 item
.setSelected( selected
);
8213 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8215 * @param {Object[]|string[]} datas Values of items to select
8218 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8221 items
= datas
.map( function ( data
) {
8222 return widget
.findItemFromData( data
);
8224 this.selectItems( items
);
8229 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8230 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8231 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8233 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8236 * @extends OO.ui.MultioptionWidget
8239 * @param {Object} [config] Configuration options
8241 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8242 // Configuration initialization
8243 config
= config
|| {};
8245 // Properties (must be done before parent constructor which calls #setDisabled)
8246 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8248 // Parent constructor
8249 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8252 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8253 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8257 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8258 .prepend( this.checkbox
.$element
);
8263 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8265 /* Static Properties */
8271 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8276 * Handle checkbox selected state change.
8280 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8281 this.setSelected( this.checkbox
.isSelected() );
8287 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8288 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8289 this.checkbox
.setSelected( state
);
8296 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8297 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8298 this.checkbox
.setDisabled( this.isDisabled() );
8305 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8306 this.checkbox
.focus();
8310 * Handle key down events.
8313 * @param {jQuery.Event} e
8315 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8317 element
= this.getElementGroup(),
8320 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8321 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8322 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8323 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8333 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8334 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8335 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8336 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8338 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8339 * OO.ui.CheckboxMultiselectInputWidget instead.
8342 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8343 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8346 * label: 'Selected checkbox'
8349 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
8351 * label: 'Unselected checkbox'
8354 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
8355 * items: [ option1, option2 ]
8358 * $( 'body' ).append( multiselect.$element );
8360 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8363 * @extends OO.ui.MultiselectWidget
8366 * @param {Object} [config] Configuration options
8368 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8369 // Parent constructor
8370 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8373 this.$lastClicked
= null;
8376 this.$group
.on( 'click', this.onClick
.bind( this ) );
8380 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8385 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8390 * Get an option by its position relative to the specified item (or to the start of the option array,
8391 * if item is `null`). The direction in which to search through the option array is specified with a
8392 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8393 * `null` if there are no options in the array.
8395 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8396 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8397 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8399 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8400 var currentIndex
, nextIndex
, i
,
8401 increase
= direction
> 0 ? 1 : -1,
8402 len
= this.items
.length
;
8405 currentIndex
= this.items
.indexOf( item
);
8406 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8408 // If no item is selected and moving forward, start at the beginning.
8409 // If moving backward, start at the end.
8410 nextIndex
= direction
> 0 ? 0 : len
- 1;
8413 for ( i
= 0; i
< len
; i
++ ) {
8414 item
= this.items
[ nextIndex
];
8415 if ( item
&& !item
.isDisabled() ) {
8418 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8424 * Handle click events on checkboxes.
8426 * @param {jQuery.Event} e
8428 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8429 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8430 $lastClicked
= this.$lastClicked
,
8431 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8432 .not( '.oo-ui-widget-disabled' );
8434 // Allow selecting multiple options at once by Shift-clicking them
8435 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8436 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8437 lastClickedIndex
= $options
.index( $lastClicked
);
8438 nowClickedIndex
= $options
.index( $nowClicked
);
8439 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8440 // browser. In either case we don't need custom handling.
8441 if ( nowClickedIndex
!== lastClickedIndex
) {
8443 wasSelected
= items
[ nowClickedIndex
].isSelected();
8444 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8446 // This depends on the DOM order of the items and the order of the .items array being the same.
8447 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8448 if ( !items
[ i
].isDisabled() ) {
8449 items
[ i
].setSelected( !wasSelected
);
8452 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8453 // handling first, then set our value. The order in which events happen is different for
8454 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8455 // non-click actions that change the checkboxes.
8457 setTimeout( function () {
8458 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8459 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8465 if ( $nowClicked
.length
) {
8466 this.$lastClicked
= $nowClicked
;
8475 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8477 if ( !this.isDisabled() ) {
8478 item
= this.getRelativeFocusableItem( null, 1 );
8489 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8494 * Progress bars visually display the status of an operation, such as a download,
8495 * and can be either determinate or indeterminate:
8497 * - **determinate** process bars show the percent of an operation that is complete.
8499 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8500 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8501 * not use percentages.
8503 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8506 * // Examples of determinate and indeterminate progress bars.
8507 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8510 * var progressBar2 = new OO.ui.ProgressBarWidget();
8512 * // Create a FieldsetLayout to layout progress bars
8513 * var fieldset = new OO.ui.FieldsetLayout;
8514 * fieldset.addItems( [
8515 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
8516 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
8518 * $( 'body' ).append( fieldset.$element );
8521 * @extends OO.ui.Widget
8524 * @param {Object} [config] Configuration options
8525 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8526 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
8527 * By default, the progress bar is indeterminate.
8529 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8530 // Configuration initialization
8531 config
= config
|| {};
8533 // Parent constructor
8534 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8537 this.$bar
= $( '<div>' );
8538 this.progress
= null;
8541 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8542 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8545 role
: 'progressbar',
8547 'aria-valuemax': 100
8549 .addClass( 'oo-ui-progressBarWidget' )
8550 .append( this.$bar
);
8555 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8557 /* Static Properties */
8563 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8568 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8570 * @return {number|boolean} Progress percent
8572 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8573 return this.progress
;
8577 * Set the percent of the process completed or `false` for an indeterminate process.
8579 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8581 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8582 this.progress
= progress
;
8584 if ( progress
!== false ) {
8585 this.$bar
.css( 'width', this.progress
+ '%' );
8586 this.$element
.attr( 'aria-valuenow', this.progress
);
8588 this.$bar
.css( 'width', '' );
8589 this.$element
.removeAttr( 'aria-valuenow' );
8591 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8595 * InputWidget is the base class for all input widgets, which
8596 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8597 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8598 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
8600 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
8604 * @extends OO.ui.Widget
8605 * @mixins OO.ui.mixin.FlaggedElement
8606 * @mixins OO.ui.mixin.TabIndexedElement
8607 * @mixins OO.ui.mixin.TitledElement
8608 * @mixins OO.ui.mixin.AccessKeyedElement
8611 * @param {Object} [config] Configuration options
8612 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8613 * @cfg {string} [value=''] The value of the input.
8614 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8615 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8616 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8617 * before it is accepted.
8619 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8620 // Configuration initialization
8621 config
= config
|| {};
8623 // Parent constructor
8624 OO
.ui
.InputWidget
.parent
.call( this, config
);
8627 // See #reusePreInfuseDOM about config.$input
8628 this.$input
= config
.$input
|| this.getInputElement( config
);
8630 this.inputFilter
= config
.inputFilter
;
8632 // Mixin constructors
8633 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8634 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8635 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8636 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8639 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8643 .addClass( 'oo-ui-inputWidget-input' )
8644 .attr( 'name', config
.name
)
8645 .prop( 'disabled', this.isDisabled() );
8647 .addClass( 'oo-ui-inputWidget' )
8648 .append( this.$input
);
8649 this.setValue( config
.value
);
8651 this.setDir( config
.dir
);
8653 if ( config
.inputId
!== undefined ) {
8654 this.setInputId( config
.inputId
);
8660 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8661 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8662 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8663 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8664 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8666 /* Static Methods */
8671 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8672 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8673 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8674 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8681 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8682 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8683 if ( config
.$input
&& config
.$input
.length
) {
8684 state
.value
= config
.$input
.val();
8685 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8686 state
.focus
= config
.$input
.is( ':focus' );
8696 * A change event is emitted when the value of the input changes.
8698 * @param {string} value
8704 * Get input element.
8706 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
8707 * different circumstances. The element must have a `value` property (like form elements).
8710 * @param {Object} config Configuration options
8711 * @return {jQuery} Input element
8713 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
8714 return $( '<input>' );
8718 * Handle potentially value-changing events.
8721 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8723 OO
.ui
.InputWidget
.prototype.onEdit = function () {
8725 if ( !this.isDisabled() ) {
8726 // Allow the stack to clear so the value will be updated
8727 setTimeout( function () {
8728 widget
.setValue( widget
.$input
.val() );
8734 * Get the value of the input.
8736 * @return {string} Input value
8738 OO
.ui
.InputWidget
.prototype.getValue = function () {
8739 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8740 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8741 var value
= this.$input
.val();
8742 if ( this.value
!== value
) {
8743 this.setValue( value
);
8749 * Set the directionality of the input.
8751 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
8754 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
8755 this.$input
.prop( 'dir', dir
);
8760 * Set the value of the input.
8762 * @param {string} value New value
8766 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
8767 value
= this.cleanUpValue( value
);
8768 // Update the DOM if it has changed. Note that with cleanUpValue, it
8769 // is possible for the DOM value to change without this.value changing.
8770 if ( this.$input
.val() !== value
) {
8771 this.$input
.val( value
);
8773 if ( this.value
!== value
) {
8775 this.emit( 'change', this.value
);
8777 // The first time that the value is set (probably while constructing the widget),
8778 // remember it in defaultValue. This property can be later used to check whether
8779 // the value of the input has been changed since it was created.
8780 if ( this.defaultValue
=== undefined ) {
8781 this.defaultValue
= this.value
;
8782 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
8788 * Clean up incoming value.
8790 * Ensures value is a string, and converts undefined and null to empty string.
8793 * @param {string} value Original value
8794 * @return {string} Cleaned up value
8796 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
8797 if ( value
=== undefined || value
=== null ) {
8799 } else if ( this.inputFilter
) {
8800 return this.inputFilter( String( value
) );
8802 return String( value
);
8809 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
8810 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8811 if ( this.$input
) {
8812 this.$input
.prop( 'disabled', this.isDisabled() );
8818 * Set the 'id' attribute of the `<input>` element.
8820 * @param {string} id
8823 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
8824 this.$input
.attr( 'id', id
);
8831 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
8832 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8833 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
8834 this.setValue( state
.value
);
8836 if ( state
.focus
) {
8842 * Data widget intended for creating 'hidden'-type inputs.
8845 * @extends OO.ui.Widget
8848 * @param {Object} [config] Configuration options
8849 * @cfg {string} [value=''] The value of the input.
8850 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8852 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
8853 // Configuration initialization
8854 config
= $.extend( { value
: '', name
: '' }, config
);
8856 // Parent constructor
8857 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
8860 this.$element
.attr( {
8862 value
: config
.value
,
8865 this.$element
.removeAttr( 'aria-disabled' );
8870 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
8872 /* Static Properties */
8878 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
8881 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
8882 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
8883 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
8884 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
8885 * [OOUI documentation on MediaWiki] [1] for more information.
8888 * // A ButtonInputWidget rendered as an HTML button, the default.
8889 * var button = new OO.ui.ButtonInputWidget( {
8890 * label: 'Input button',
8894 * $( 'body' ).append( button.$element );
8896 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
8899 * @extends OO.ui.InputWidget
8900 * @mixins OO.ui.mixin.ButtonElement
8901 * @mixins OO.ui.mixin.IconElement
8902 * @mixins OO.ui.mixin.IndicatorElement
8903 * @mixins OO.ui.mixin.LabelElement
8904 * @mixins OO.ui.mixin.TitledElement
8907 * @param {Object} [config] Configuration options
8908 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
8909 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
8910 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
8911 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
8912 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
8914 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
8915 // Configuration initialization
8916 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
8918 // See InputWidget#reusePreInfuseDOM about config.$input
8919 if ( config
.$input
) {
8920 config
.$input
.empty();
8923 // Properties (must be set before parent constructor, which calls #setValue)
8924 this.useInputTag
= config
.useInputTag
;
8926 // Parent constructor
8927 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
8929 // Mixin constructors
8930 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
8931 OO
.ui
.mixin
.IconElement
.call( this, config
);
8932 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8933 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8934 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8937 if ( !config
.useInputTag
) {
8938 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
8940 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
8945 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
8946 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
8947 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
8948 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
8949 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
8950 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
8952 /* Static Properties */
8958 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
8966 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
8968 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
8969 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
8975 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
8977 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
8978 * text, or `null` for no label
8981 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
8982 if ( typeof label
=== 'function' ) {
8983 label
= OO
.ui
.resolveMsg( label
);
8986 if ( this.useInputTag
) {
8987 // Discard non-plaintext labels
8988 if ( typeof label
!== 'string' ) {
8992 this.$input
.val( label
);
8995 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
8999 * Set the value of the input.
9001 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9002 * they do not support {@link #value values}.
9004 * @param {string} value New value
9007 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9008 if ( !this.useInputTag
) {
9009 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9017 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9018 // Disable generating `<label>` elements for buttons. One would very rarely need additional label
9019 // for a button, and it's already a big clickable target, and it causes unexpected rendering.
9024 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9025 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9026 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9027 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9029 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9032 * // An example of selected, unselected, and disabled checkbox inputs
9033 * var checkbox1=new OO.ui.CheckboxInputWidget( {
9037 * var checkbox2=new OO.ui.CheckboxInputWidget( {
9040 * var checkbox3=new OO.ui.CheckboxInputWidget( {
9044 * // Create a fieldset layout with fields for each checkbox.
9045 * var fieldset = new OO.ui.FieldsetLayout( {
9046 * label: 'Checkboxes'
9048 * fieldset.addItems( [
9049 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9050 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9051 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9053 * $( 'body' ).append( fieldset.$element );
9055 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9058 * @extends OO.ui.InputWidget
9061 * @param {Object} [config] Configuration options
9062 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
9064 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9065 // Configuration initialization
9066 config
= config
|| {};
9068 // Parent constructor
9069 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9072 this.checkIcon
= new OO
.ui
.IconWidget( {
9074 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9079 .addClass( 'oo-ui-checkboxInputWidget' )
9080 // Required for pretty styling in WikimediaUI theme
9081 .append( this.checkIcon
.$element
);
9082 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9087 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9089 /* Static Properties */
9095 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9097 /* Static Methods */
9102 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9103 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9104 state
.checked
= config
.$input
.prop( 'checked' );
9114 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9115 return $( '<input>' ).attr( 'type', 'checkbox' );
9121 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9123 if ( !this.isDisabled() ) {
9124 // Allow the stack to clear so the value will be updated
9125 setTimeout( function () {
9126 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9132 * Set selection state of this checkbox.
9134 * @param {boolean} state `true` for selected
9137 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
9139 if ( this.selected
!== state
) {
9140 this.selected
= state
;
9141 this.$input
.prop( 'checked', this.selected
);
9142 this.emit( 'change', this.selected
);
9144 // The first time that the selection state is set (probably while constructing the widget),
9145 // remember it in defaultSelected. This property can be later used to check whether
9146 // the selection state of the input has been changed since it was created.
9147 if ( this.defaultSelected
=== undefined ) {
9148 this.defaultSelected
= this.selected
;
9149 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9155 * Check if this checkbox is selected.
9157 * @return {boolean} Checkbox is selected
9159 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9160 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9161 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9162 var selected
= this.$input
.prop( 'checked' );
9163 if ( this.selected
!== selected
) {
9164 this.setSelected( selected
);
9166 return this.selected
;
9172 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9173 if ( !this.isDisabled() ) {
9174 this.$input
.click();
9182 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9183 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9184 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9185 this.setSelected( state
.checked
);
9190 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9191 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9192 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9193 * more information about input widgets.
9195 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9196 * are no options. If no `value` configuration option is provided, the first option is selected.
9197 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9199 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
9202 * // Example: A DropdownInputWidget with three options
9203 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9205 * { data: 'a', label: 'First' },
9206 * { data: 'b', label: 'Second'},
9207 * { data: 'c', label: 'Third' }
9210 * $( 'body' ).append( dropdownInput.$element );
9212 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9215 * @extends OO.ui.InputWidget
9218 * @param {Object} [config] Configuration options
9219 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9220 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9222 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9223 // Configuration initialization
9224 config
= config
|| {};
9226 // Properties (must be done before parent constructor which calls #setDisabled)
9227 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
9228 // Set up the options before parent constructor, which uses them to validate config.value.
9229 // Use this instead of setOptions() because this.$input is not set up yet.
9230 this.setOptionsData( config
.options
|| [] );
9232 // Parent constructor
9233 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9236 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
9240 .addClass( 'oo-ui-dropdownInputWidget' )
9241 .append( this.dropdownWidget
.$element
);
9242 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9247 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9255 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9256 return $( '<select>' );
9260 * Handles menu select events.
9263 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9265 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9266 this.setValue( item
? item
.getData() : '' );
9272 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9274 value
= this.cleanUpValue( value
);
9275 // Only allow setting values that are actually present in the dropdown
9276 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9277 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9278 this.dropdownWidget
.getMenu().selectItem( selected
);
9279 value
= selected
? selected
.getData() : '';
9280 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9281 if ( this.optionsDirty
) {
9282 // We reached this from the constructor or from #setOptions.
9283 // We have to update the <select> element.
9284 this.updateOptionsInterface();
9292 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9293 this.dropdownWidget
.setDisabled( state
);
9294 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9299 * Set the options available for this input.
9301 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9304 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9305 var value
= this.getValue();
9307 this.setOptionsData( options
);
9309 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9310 // In case the previous value is no longer an available option, select the first valid one.
9311 this.setValue( value
);
9317 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9319 * This method may be called before the parent constructor, so various properties may not be
9322 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9325 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9330 this.optionsDirty
= true;
9332 optionWidgets
= options
.map( function ( opt
) {
9335 if ( opt
.optgroup
!== undefined ) {
9336 return widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9339 optValue
= widget
.cleanUpValue( opt
.data
);
9340 return widget
.createMenuOptionWidget(
9342 opt
.label
!== undefined ? opt
.label
: optValue
9347 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9351 * Create a menu option widget.
9354 * @param {string} data Item data
9355 * @param {string} label Item label
9356 * @return {OO.ui.MenuOptionWidget} Option widget
9358 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9359 return new OO
.ui
.MenuOptionWidget( {
9366 * Create a menu section option widget.
9369 * @param {string} label Section item label
9370 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9372 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9373 return new OO
.ui
.MenuSectionOptionWidget( {
9379 * Update the user-visible interface to match the internal list of options and value.
9381 * This method must only be called after the parent constructor.
9385 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9387 $optionsContainer
= this.$input
,
9388 defaultValue
= this.defaultValue
,
9391 this.$input
.empty();
9393 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9396 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9397 $optionNode
= $( '<option>' )
9398 .attr( 'value', optionWidget
.getData() )
9399 .text( optionWidget
.getLabel() );
9401 // Remember original selection state. This property can be later used to check whether
9402 // the selection state of the input has been changed since it was created.
9403 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9405 $optionsContainer
.append( $optionNode
);
9407 $optionNode
= $( '<optgroup>' )
9408 .attr( 'label', optionWidget
.getLabel() );
9409 widget
.$input
.append( $optionNode
);
9410 $optionsContainer
= $optionNode
;
9414 this.optionsDirty
= false;
9420 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9421 this.dropdownWidget
.focus();
9428 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9429 this.dropdownWidget
.blur();
9434 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9435 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9436 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9437 * please see the [OOUI documentation on MediaWiki][1].
9439 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9442 * // An example of selected, unselected, and disabled radio inputs
9443 * var radio1 = new OO.ui.RadioInputWidget( {
9447 * var radio2 = new OO.ui.RadioInputWidget( {
9450 * var radio3 = new OO.ui.RadioInputWidget( {
9454 * // Create a fieldset layout with fields for each radio button.
9455 * var fieldset = new OO.ui.FieldsetLayout( {
9456 * label: 'Radio inputs'
9458 * fieldset.addItems( [
9459 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9460 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9461 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9463 * $( 'body' ).append( fieldset.$element );
9465 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9468 * @extends OO.ui.InputWidget
9471 * @param {Object} [config] Configuration options
9472 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9474 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9475 // Configuration initialization
9476 config
= config
|| {};
9478 // Parent constructor
9479 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9483 .addClass( 'oo-ui-radioInputWidget' )
9484 // Required for pretty styling in WikimediaUI theme
9485 .append( $( '<span>' ) );
9486 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9491 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9493 /* Static Properties */
9499 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9501 /* Static Methods */
9506 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9507 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9508 state
.checked
= config
.$input
.prop( 'checked' );
9518 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9519 return $( '<input>' ).attr( 'type', 'radio' );
9525 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9526 // RadioInputWidget doesn't track its state.
9530 * Set selection state of this radio button.
9532 * @param {boolean} state `true` for selected
9535 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9536 // RadioInputWidget doesn't track its state.
9537 this.$input
.prop( 'checked', state
);
9538 // The first time that the selection state is set (probably while constructing the widget),
9539 // remember it in defaultSelected. This property can be later used to check whether
9540 // the selection state of the input has been changed since it was created.
9541 if ( this.defaultSelected
=== undefined ) {
9542 this.defaultSelected
= state
;
9543 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9549 * Check if this radio button is selected.
9551 * @return {boolean} Radio is selected
9553 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
9554 return this.$input
.prop( 'checked' );
9560 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
9561 if ( !this.isDisabled() ) {
9562 this.$input
.click();
9570 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9571 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9572 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9573 this.setSelected( state
.checked
);
9578 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9579 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9580 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9581 * more information about input widgets.
9583 * This and OO.ui.DropdownInputWidget support the same configuration options.
9586 * // Example: A RadioSelectInputWidget with three options
9587 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9589 * { data: 'a', label: 'First' },
9590 * { data: 'b', label: 'Second'},
9591 * { data: 'c', label: 'Third' }
9594 * $( 'body' ).append( radioSelectInput.$element );
9596 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9599 * @extends OO.ui.InputWidget
9602 * @param {Object} [config] Configuration options
9603 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9605 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
9606 // Configuration initialization
9607 config
= config
|| {};
9609 // Properties (must be done before parent constructor which calls #setDisabled)
9610 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
9611 // Set up the options before parent constructor, which uses them to validate config.value.
9612 // Use this instead of setOptions() because this.$input is not set up yet
9613 this.setOptionsData( config
.options
|| [] );
9615 // Parent constructor
9616 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
9619 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
9623 .addClass( 'oo-ui-radioSelectInputWidget' )
9624 .append( this.radioSelectWidget
.$element
);
9625 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
9630 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
9632 /* Static Methods */
9637 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9638 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9639 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9646 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9647 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9648 // Cannot reuse the `<input type=radio>` set
9649 delete config
.$input
;
9659 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
9660 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
9661 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
9662 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
9666 * Handles menu select events.
9669 * @param {OO.ui.RadioOptionWidget} item Selected menu item
9671 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
9672 this.setValue( item
.getData() );
9678 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
9680 value
= this.cleanUpValue( value
);
9681 // Only allow setting values that are actually present in the dropdown
9682 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
9683 this.radioSelectWidget
.findFirstSelectableItem();
9684 this.radioSelectWidget
.selectItem( selected
);
9685 value
= selected
? selected
.getData() : '';
9686 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9693 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
9694 this.radioSelectWidget
.setDisabled( state
);
9695 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9700 * Set the options available for this input.
9702 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9705 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
9706 var value
= this.getValue();
9708 this.setOptionsData( options
);
9710 // Re-set the value to update the visible interface (RadioSelectWidget).
9711 // In case the previous value is no longer an available option, select the first valid one.
9712 this.setValue( value
);
9718 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9720 * This method may be called before the parent constructor, so various properties may not be
9723 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9726 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
9729 this.radioSelectWidget
9731 .addItems( options
.map( function ( opt
) {
9732 var optValue
= widget
.cleanUpValue( opt
.data
);
9733 return new OO
.ui
.RadioOptionWidget( {
9735 label
: opt
.label
!== undefined ? opt
.label
: optValue
9743 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
9744 this.radioSelectWidget
.focus();
9751 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
9752 this.radioSelectWidget
.blur();
9757 * CheckboxMultiselectInputWidget is a
9758 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
9759 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
9760 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
9761 * more information about input widgets.
9764 * // Example: A CheckboxMultiselectInputWidget with three options
9765 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
9767 * { data: 'a', label: 'First' },
9768 * { data: 'b', label: 'Second'},
9769 * { data: 'c', label: 'Third' }
9772 * $( 'body' ).append( multiselectInput.$element );
9774 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9777 * @extends OO.ui.InputWidget
9780 * @param {Object} [config] Configuration options
9781 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
9783 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
9784 // Configuration initialization
9785 config
= config
|| {};
9787 // Properties (must be done before parent constructor which calls #setDisabled)
9788 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
9789 // Must be set before the #setOptionsData call below
9790 this.inputName
= config
.name
;
9791 // Set up the options before parent constructor, which uses them to validate config.value.
9792 // Use this instead of setOptions() because this.$input is not set up yet
9793 this.setOptionsData( config
.options
|| [] );
9795 // Parent constructor
9796 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
9799 this.checkboxMultiselectWidget
.connect( this, { select
: 'onCheckboxesSelect' } );
9803 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
9804 .append( this.checkboxMultiselectWidget
.$element
);
9805 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
9806 this.$input
.detach();
9811 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
9813 /* Static Methods */
9818 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9819 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9820 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9821 .toArray().map( function ( el
) { return el
.value
; } );
9828 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9829 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9830 // Cannot reuse the `<input type=checkbox>` set
9831 delete config
.$input
;
9841 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
9843 return $( '<unused>' );
9847 * Handles CheckboxMultiselectWidget select events.
9851 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
9852 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
9858 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
9859 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9860 .toArray().map( function ( el
) { return el
.value
; } );
9861 if ( this.value
!== value
) {
9862 this.setValue( value
);
9870 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
9871 value
= this.cleanUpValue( value
);
9872 this.checkboxMultiselectWidget
.selectItemsByData( value
);
9873 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9874 if ( this.optionsDirty
) {
9875 // We reached this from the constructor or from #setOptions.
9876 // We have to update the <select> element.
9877 this.updateOptionsInterface();
9883 * Clean up incoming value.
9885 * @param {string[]} value Original value
9886 * @return {string[]} Cleaned up value
9888 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
9891 if ( !Array
.isArray( value
) ) {
9894 for ( i
= 0; i
< value
.length
; i
++ ) {
9896 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
9897 // Remove options that we don't have here
9898 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
9901 cleanValue
.push( singleValue
);
9909 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
9910 this.checkboxMultiselectWidget
.setDisabled( state
);
9911 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9916 * Set the options available for this input.
9918 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
9921 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
9922 var value
= this.getValue();
9924 this.setOptionsData( options
);
9926 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
9927 // This will also get rid of any stale options that we just removed.
9928 this.setValue( value
);
9934 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9936 * This method may be called before the parent constructor, so various properties may not be
9939 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9942 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
9945 this.optionsDirty
= true;
9947 this.checkboxMultiselectWidget
9949 .addItems( options
.map( function ( opt
) {
9950 var optValue
, item
, optDisabled
;
9952 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
9953 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
9954 item
= new OO
.ui
.CheckboxMultioptionWidget( {
9956 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
9957 disabled
: optDisabled
9959 // Set the 'name' and 'value' for form submission
9960 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
9961 item
.checkbox
.setValue( optValue
);
9967 * Update the user-visible interface to match the internal list of options and value.
9969 * This method must only be called after the parent constructor.
9973 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
9974 var defaultValue
= this.defaultValue
;
9976 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
9977 // Remember original selection state. This property can be later used to check whether
9978 // the selection state of the input has been changed since it was created.
9979 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
9980 item
.checkbox
.defaultSelected
= isDefault
;
9981 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
9984 this.optionsDirty
= false;
9990 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
9991 this.checkboxMultiselectWidget
.focus();
9996 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
9997 * size of the field as well as its presentation. In addition, these widgets can be configured
9998 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
9999 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
10000 * which modifies incoming values rather than validating them.
10001 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10003 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10006 * // Example of a text input widget
10007 * var textInput = new OO.ui.TextInputWidget( {
10008 * value: 'Text input'
10010 * $( 'body' ).append( textInput.$element );
10012 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10015 * @extends OO.ui.InputWidget
10016 * @mixins OO.ui.mixin.IconElement
10017 * @mixins OO.ui.mixin.IndicatorElement
10018 * @mixins OO.ui.mixin.PendingElement
10019 * @mixins OO.ui.mixin.LabelElement
10022 * @param {Object} [config] Configuration options
10023 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10024 * 'email', 'url' or 'number'.
10025 * @cfg {string} [placeholder] Placeholder text
10026 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10027 * instruct the browser to focus this widget.
10028 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10029 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10031 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10032 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10033 * many emojis) count as 2 characters each.
10034 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10035 * the value or placeholder text: `'before'` or `'after'`
10036 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator: 'required'`.
10037 * Note that `false` & setting `indicator: 'required' will result in no indicator shown.
10038 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10039 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined` means
10040 * leaving it up to the browser).
10041 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10042 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10043 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10044 * value for it to be considered valid; when Function, a function receiving the value as parameter
10045 * that must return true, or promise resolving to true, for it to be considered valid.
10047 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10048 // Configuration initialization
10049 config
= $.extend( {
10051 labelPosition
: 'after'
10054 if ( config
.multiline
) {
10055 OO
.ui
.warnDeprecation( 'TextInputWidget: config.multiline is deprecated. Use the MultilineTextInputWidget instead. See T130434.' );
10056 return new OO
.ui
.MultilineTextInputWidget( config
);
10059 // Parent constructor
10060 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10062 // Mixin constructors
10063 OO
.ui
.mixin
.IconElement
.call( this, config
);
10064 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10065 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
10066 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10069 this.type
= this.getSaneType( config
);
10070 this.readOnly
= false;
10071 this.required
= false;
10072 this.validate
= null;
10073 this.styleHeight
= null;
10074 this.scrollWidth
= null;
10076 this.setValidation( config
.validate
);
10077 this.setLabelPosition( config
.labelPosition
);
10081 keypress
: this.onKeyPress
.bind( this ),
10082 blur
: this.onBlur
.bind( this ),
10083 focus
: this.onFocus
.bind( this )
10085 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10086 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10087 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10088 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10092 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10093 .append( this.$icon
, this.$indicator
);
10094 this.setReadOnly( !!config
.readOnly
);
10095 this.setRequired( !!config
.required
);
10096 if ( config
.placeholder
!== undefined ) {
10097 this.$input
.attr( 'placeholder', config
.placeholder
);
10099 if ( config
.maxLength
!== undefined ) {
10100 this.$input
.attr( 'maxlength', config
.maxLength
);
10102 if ( config
.autofocus
) {
10103 this.$input
.attr( 'autofocus', 'autofocus' );
10105 if ( config
.autocomplete
=== false ) {
10106 this.$input
.attr( 'autocomplete', 'off' );
10107 // Turning off autocompletion also disables "form caching" when the user navigates to a
10108 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
10110 beforeunload: function () {
10111 this.$input
.removeAttr( 'autocomplete' );
10113 pageshow: function () {
10114 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
10115 // whole page... it shouldn't hurt, though.
10116 this.$input
.attr( 'autocomplete', 'off' );
10120 if ( config
.spellcheck
!== undefined ) {
10121 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10123 if ( this.label
) {
10124 this.isWaitingToBeAttached
= true;
10125 this.installParentChangeDetector();
10131 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10132 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10133 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10134 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10135 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10137 /* Static Properties */
10139 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10147 * An `enter` event is emitted when the user presses 'enter' inside the text box.
10155 * Handle icon mouse down events.
10158 * @param {jQuery.Event} e Mouse down event
10160 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10161 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10168 * Handle indicator mouse down events.
10171 * @param {jQuery.Event} e Mouse down event
10173 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10174 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10181 * Handle key press events.
10184 * @param {jQuery.Event} e Key press event
10185 * @fires enter If enter key is pressed
10187 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10188 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10189 this.emit( 'enter', e
);
10194 * Handle blur events.
10197 * @param {jQuery.Event} e Blur event
10199 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10200 this.setValidityFlag();
10204 * Handle focus events.
10207 * @param {jQuery.Event} e Focus event
10209 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10210 if ( this.isWaitingToBeAttached
) {
10211 // If we've received focus, then we must be attached to the document, and if
10212 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10213 this.onElementAttach();
10215 this.setValidityFlag( true );
10219 * Handle element attach events.
10222 * @param {jQuery.Event} e Element attach event
10224 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10225 this.isWaitingToBeAttached
= false;
10226 // Any previously calculated size is now probably invalid if we reattached elsewhere
10227 this.valCache
= null;
10228 this.positionLabel();
10232 * Handle debounced change events.
10234 * @param {string} value
10237 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10238 this.setValidityFlag();
10242 * Check if the input is {@link #readOnly read-only}.
10244 * @return {boolean}
10246 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10247 return this.readOnly
;
10251 * Set the {@link #readOnly read-only} state of the input.
10253 * @param {boolean} state Make input read-only
10256 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10257 this.readOnly
= !!state
;
10258 this.$input
.prop( 'readOnly', this.readOnly
);
10263 * Check if the input is {@link #required required}.
10265 * @return {boolean}
10267 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10268 return this.required
;
10272 * Set the {@link #required required} state of the input.
10274 * @param {boolean} state Make input required
10277 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10278 this.required
= !!state
;
10279 if ( this.required
) {
10281 .prop( 'required', true )
10282 .attr( 'aria-required', 'true' );
10283 if ( this.getIndicator() === null ) {
10284 this.setIndicator( 'required' );
10288 .prop( 'required', false )
10289 .removeAttr( 'aria-required' );
10290 if ( this.getIndicator() === 'required' ) {
10291 this.setIndicator( null );
10298 * Support function for making #onElementAttach work across browsers.
10300 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10301 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10303 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10304 * first time that the element gets attached to the documented.
10306 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10307 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10308 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
10311 if ( MutationObserver
) {
10312 // The new way. If only it wasn't so ugly.
10314 if ( this.isElementAttached() ) {
10315 // Widget is attached already, do nothing. This breaks the functionality of this function when
10316 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
10317 // would require observation of the whole document, which would hurt performance of other,
10318 // more important code.
10322 // Find topmost node in the tree
10323 topmostNode
= this.$element
[ 0 ];
10324 while ( topmostNode
.parentNode
) {
10325 topmostNode
= topmostNode
.parentNode
;
10328 // We have no way to detect the $element being attached somewhere without observing the entire
10329 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
10330 // parent node of $element, and instead detect when $element is removed from it (and thus
10331 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
10332 // doesn't get attached, we end up back here and create the parent.
10334 mutationObserver
= new MutationObserver( function ( mutations
) {
10335 var i
, j
, removedNodes
;
10336 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10337 removedNodes
= mutations
[ i
].removedNodes
;
10338 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10339 if ( removedNodes
[ j
] === topmostNode
) {
10340 setTimeout( onRemove
, 0 );
10347 onRemove = function () {
10348 // If the node was attached somewhere else, report it
10349 if ( widget
.isElementAttached() ) {
10350 widget
.onElementAttach();
10352 mutationObserver
.disconnect();
10353 widget
.installParentChangeDetector();
10356 // Create a fake parent and observe it
10357 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10358 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10360 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10361 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10362 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10370 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10371 if ( this.getSaneType( config
) === 'number' ) {
10372 return $( '<input>' )
10373 .attr( 'step', 'any' )
10374 .attr( 'type', 'number' );
10376 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10381 * Get sanitized value for 'type' for given config.
10383 * @param {Object} config Configuration options
10384 * @return {string|null}
10387 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10388 var allowedTypes
= [
10395 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10399 * Focus the input and select a specified range within the text.
10401 * @param {number} from Select from offset
10402 * @param {number} [to] Select to offset, defaults to from
10405 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10406 var isBackwards
, start
, end
,
10407 input
= this.$input
[ 0 ];
10411 isBackwards
= to
< from;
10412 start
= isBackwards
? to
: from;
10413 end
= isBackwards
? from : to
;
10418 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10420 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10421 // Rather than expensively check if the input is attached every time, just check
10422 // if it was the cause of an error being thrown. If not, rethrow the error.
10423 if ( this.getElementDocument().body
.contains( input
) ) {
10431 * Get an object describing the current selection range in a directional manner
10433 * @return {Object} Object containing 'from' and 'to' offsets
10435 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10436 var input
= this.$input
[ 0 ],
10437 start
= input
.selectionStart
,
10438 end
= input
.selectionEnd
,
10439 isBackwards
= input
.selectionDirection
=== 'backward';
10442 from: isBackwards
? end
: start
,
10443 to
: isBackwards
? start
: end
10448 * Get the length of the text input value.
10450 * This could differ from the length of #getValue if the
10451 * value gets filtered
10453 * @return {number} Input length
10455 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10456 return this.$input
[ 0 ].value
.length
;
10460 * Focus the input and select the entire text.
10464 OO
.ui
.TextInputWidget
.prototype.select = function () {
10465 return this.selectRange( 0, this.getInputLength() );
10469 * Focus the input and move the cursor to the start.
10473 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10474 return this.selectRange( 0 );
10478 * Focus the input and move the cursor to the end.
10482 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10483 return this.selectRange( this.getInputLength() );
10487 * Insert new content into the input.
10489 * @param {string} content Content to be inserted
10492 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10494 range
= this.getRange(),
10495 value
= this.getValue();
10497 start
= Math
.min( range
.from, range
.to
);
10498 end
= Math
.max( range
.from, range
.to
);
10500 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10501 this.selectRange( start
+ content
.length
);
10506 * Insert new content either side of a selection.
10508 * @param {string} pre Content to be inserted before the selection
10509 * @param {string} post Content to be inserted after the selection
10512 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10514 range
= this.getRange(),
10515 offset
= pre
.length
;
10517 start
= Math
.min( range
.from, range
.to
);
10518 end
= Math
.max( range
.from, range
.to
);
10520 this.selectRange( start
).insertContent( pre
);
10521 this.selectRange( offset
+ end
).insertContent( post
);
10523 this.selectRange( offset
+ start
, offset
+ end
);
10528 * Set the validation pattern.
10530 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10531 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10532 * value must contain only numbers).
10534 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10535 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10537 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
10538 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
10539 this.validate
= validate
;
10541 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
10546 * Sets the 'invalid' flag appropriately.
10548 * @param {boolean} [isValid] Optionally override validation result
10550 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
10552 setFlag = function ( valid
) {
10554 widget
.$input
.attr( 'aria-invalid', 'true' );
10556 widget
.$input
.removeAttr( 'aria-invalid' );
10558 widget
.setFlags( { invalid
: !valid
} );
10561 if ( isValid
!== undefined ) {
10562 setFlag( isValid
);
10564 this.getValidity().then( function () {
10573 * Get the validity of current value.
10575 * This method returns a promise that resolves if the value is valid and rejects if
10576 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
10578 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10580 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
10583 function rejectOrResolve( valid
) {
10585 return $.Deferred().resolve().promise();
10587 return $.Deferred().reject().promise();
10591 // Check browser validity and reject if it is invalid
10593 this.$input
[ 0 ].checkValidity
!== undefined &&
10594 this.$input
[ 0 ].checkValidity() === false
10596 return rejectOrResolve( false );
10599 // Run our checks if the browser thinks the field is valid
10600 if ( this.validate
instanceof Function
) {
10601 result
= this.validate( this.getValue() );
10602 if ( result
&& $.isFunction( result
.promise
) ) {
10603 return result
.promise().then( function ( valid
) {
10604 return rejectOrResolve( valid
);
10607 return rejectOrResolve( result
);
10610 return rejectOrResolve( this.getValue().match( this.validate
) );
10615 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10617 * @param {string} labelPosition Label position, 'before' or 'after'
10620 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10621 this.labelPosition
= labelPosition
;
10622 if ( this.label
) {
10623 // If there is no label and we only change the position, #updatePosition is a no-op,
10624 // but it takes really a lot of work to do nothing.
10625 this.updatePosition();
10631 * Update the position of the inline label.
10633 * This method is called by #setLabelPosition, and can also be called on its own if
10634 * something causes the label to be mispositioned.
10638 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10639 var after
= this.labelPosition
=== 'after';
10642 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10643 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10645 this.valCache
= null;
10646 this.scrollWidth
= null;
10647 this.positionLabel();
10653 * Position the label by setting the correct padding on the input.
10658 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
10659 var after
, rtl
, property
, newCss
;
10661 if ( this.isWaitingToBeAttached
) {
10662 // #onElementAttach will be called soon, which calls this method
10667 'padding-right': '',
10671 if ( this.label
) {
10672 this.$element
.append( this.$label
);
10674 this.$label
.detach();
10675 // Clear old values if present
10676 this.$input
.css( newCss
);
10680 after
= this.labelPosition
=== 'after';
10681 rtl
= this.$element
.css( 'direction' ) === 'rtl';
10682 property
= after
=== rtl
? 'padding-left' : 'padding-right';
10684 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
10685 // We have to clear the padding on the other side, in case the element direction changed
10686 this.$input
.css( newCss
);
10693 * @extends OO.ui.TextInputWidget
10696 * @param {Object} [config] Configuration options
10698 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
10699 config
= $.extend( {
10703 // Parent constructor
10704 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
10707 this.connect( this, {
10712 this.updateSearchIndicator();
10713 this.connect( this, {
10714 disable
: 'onDisable'
10720 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
10728 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
10735 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10736 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10737 // Clear the text field
10738 this.setValue( '' );
10745 * Update the 'clear' indicator displayed on type: 'search' text
10746 * fields, hiding it when the field is already empty or when it's not
10749 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
10750 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
10751 this.setIndicator( null );
10753 this.setIndicator( 'clear' );
10758 * Handle change events.
10762 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
10763 this.updateSearchIndicator();
10767 * Handle disable events.
10769 * @param {boolean} disabled Element is disabled
10772 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
10773 this.updateSearchIndicator();
10779 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
10780 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
10781 this.updateSearchIndicator();
10787 * @extends OO.ui.TextInputWidget
10790 * @param {Object} [config] Configuration options
10791 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
10792 * specifies minimum number of rows to display.
10793 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
10794 * Use the #maxRows config to specify a maximum number of displayed rows.
10795 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
10796 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
10798 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
10799 config
= $.extend( {
10802 config
.multiline
= false;
10803 // Parent constructor
10804 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
10807 this.multiline
= true;
10808 this.autosize
= !!config
.autosize
;
10809 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
10810 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
10812 // Clone for resizing
10813 if ( this.autosize
) {
10814 this.$clone
= this.$input
10816 .insertAfter( this.$input
)
10817 .attr( 'aria-hidden', 'true' )
10818 .addClass( 'oo-ui-element-hidden' );
10822 this.connect( this, {
10827 if ( this.multiline
&& config
.rows
) {
10828 this.$input
.attr( 'rows', config
.rows
);
10830 if ( this.autosize
) {
10831 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
10832 this.isWaitingToBeAttached
= true;
10833 this.installParentChangeDetector();
10839 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
10841 /* Static Methods */
10846 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10847 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10848 state
.scrollTop
= config
.$input
.scrollTop();
10857 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
10858 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
10863 * Handle change events.
10867 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
10874 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
10875 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
10882 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
10884 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
10886 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
10887 // Some platforms emit keycode 10 for ctrl+enter in a textarea
10890 this.emit( 'enter', e
);
10895 * Automatically adjust the size of the text input.
10897 * This only affects multiline inputs that are {@link #autosize autosized}.
10902 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
10903 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
10904 idealHeight
, newHeight
, scrollWidth
, property
;
10906 if ( this.$input
.val() !== this.valCache
) {
10907 if ( this.autosize
) {
10909 .val( this.$input
.val() )
10910 .attr( 'rows', this.minRows
)
10911 // Set inline height property to 0 to measure scroll height
10912 .css( 'height', 0 );
10914 this.$clone
.removeClass( 'oo-ui-element-hidden' );
10916 this.valCache
= this.$input
.val();
10918 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
10920 // Remove inline height property to measure natural heights
10921 this.$clone
.css( 'height', '' );
10922 innerHeight
= this.$clone
.innerHeight();
10923 outerHeight
= this.$clone
.outerHeight();
10925 // Measure max rows height
10927 .attr( 'rows', this.maxRows
)
10928 .css( 'height', 'auto' )
10930 maxInnerHeight
= this.$clone
.innerHeight();
10932 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
10933 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
10934 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
10935 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
10937 this.$clone
.addClass( 'oo-ui-element-hidden' );
10939 // Only apply inline height when expansion beyond natural height is needed
10940 // Use the difference between the inner and outer height as a buffer
10941 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
10942 if ( newHeight
!== this.styleHeight
) {
10943 this.$input
.css( 'height', newHeight
);
10944 this.styleHeight
= newHeight
;
10945 this.emit( 'resize' );
10948 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
10949 if ( scrollWidth
!== this.scrollWidth
) {
10950 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
10952 this.$label
.css( { right
: '', left
: '' } );
10953 this.$indicator
.css( { right
: '', left
: '' } );
10955 if ( scrollWidth
) {
10956 this.$indicator
.css( property
, scrollWidth
);
10957 if ( this.labelPosition
=== 'after' ) {
10958 this.$label
.css( property
, scrollWidth
);
10962 this.scrollWidth
= scrollWidth
;
10963 this.positionLabel();
10973 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
10974 return $( '<textarea>' );
10978 * Check if the input supports multiple lines.
10980 * @return {boolean}
10982 OO
.ui
.MultilineTextInputWidget
.prototype.isMultiline = function () {
10983 return !!this.multiline
;
10987 * Check if the input automatically adjusts its size.
10989 * @return {boolean}
10991 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
10992 return !!this.autosize
;
10998 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10999 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11000 if ( state
.scrollTop
!== undefined ) {
11001 this.$input
.scrollTop( state
.scrollTop
);
11006 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11007 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11008 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11010 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11011 * option, that option will appear to be selected.
11012 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11015 * After the user chooses an option, its `data` will be used as a new value for the widget.
11016 * A `label` also can be specified for each option: if given, it will be shown instead of the
11017 * `data` in the dropdown menu.
11019 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11021 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
11024 * // Example: A ComboBoxInputWidget.
11025 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11026 * value: 'Option 1',
11028 * { data: 'Option 1' },
11029 * { data: 'Option 2' },
11030 * { data: 'Option 3' }
11033 * $( 'body' ).append( comboBox.$element );
11036 * // Example: A ComboBoxInputWidget with additional option labels.
11037 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11038 * value: 'Option 1',
11041 * data: 'Option 1',
11042 * label: 'Option One'
11045 * data: 'Option 2',
11046 * label: 'Option Two'
11049 * data: 'Option 3',
11050 * label: 'Option Three'
11054 * $( 'body' ).append( comboBox.$element );
11056 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11059 * @extends OO.ui.TextInputWidget
11062 * @param {Object} [config] Configuration options
11063 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11064 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11065 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11066 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11067 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11068 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11070 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11071 // Configuration initialization
11072 config
= $.extend( {
11073 autocomplete
: false
11076 // ComboBoxInputWidget shouldn't support `multiline`
11077 config
.multiline
= false;
11079 // See InputWidget#reusePreInfuseDOM about `config.$input`
11080 if ( config
.$input
) {
11081 config
.$input
.removeAttr( 'list' );
11084 // Parent constructor
11085 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11088 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11089 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11090 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11092 disabled
: this.disabled
11094 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11098 $floatableContainer
: this.$element
,
11099 disabled
: this.isDisabled()
11105 this.connect( this, {
11106 change
: 'onInputChange',
11107 enter
: 'onInputEnter'
11109 this.dropdownButton
.connect( this, {
11110 click
: 'onDropdownButtonClick'
11112 this.menu
.connect( this, {
11113 choose
: 'onMenuChoose',
11114 add
: 'onMenuItemsChange',
11115 remove
: 'onMenuItemsChange',
11116 toggle
: 'onMenuToggle'
11120 this.$input
.attr( {
11122 'aria-owns': this.menu
.getElementId(),
11123 'aria-autocomplete': 'list'
11125 // Do not override options set via config.menu.items
11126 if ( config
.options
!== undefined ) {
11127 this.setOptions( config
.options
);
11129 this.$field
= $( '<div>' )
11130 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11131 .append( this.$input
, this.dropdownButton
.$element
);
11133 .addClass( 'oo-ui-comboBoxInputWidget' )
11134 .append( this.$field
);
11135 this.$overlay
.append( this.menu
.$element
);
11136 this.onMenuItemsChange();
11141 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11146 * Get the combobox's menu.
11148 * @return {OO.ui.MenuSelectWidget} Menu widget
11150 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11155 * Get the combobox's text input widget.
11157 * @return {OO.ui.TextInputWidget} Text input widget
11159 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11164 * Handle input change events.
11167 * @param {string} value New value
11169 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11170 var match
= this.menu
.findItemFromData( value
);
11172 this.menu
.selectItem( match
);
11173 if ( this.menu
.findHighlightedItem() ) {
11174 this.menu
.highlightItem( match
);
11177 if ( !this.isDisabled() ) {
11178 this.menu
.toggle( true );
11183 * Handle input enter events.
11187 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11188 if ( !this.isDisabled() ) {
11189 this.menu
.toggle( false );
11194 * Handle button click events.
11198 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11199 this.menu
.toggle();
11204 * Handle menu choose events.
11207 * @param {OO.ui.OptionWidget} item Chosen item
11209 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11210 this.setValue( item
.getData() );
11214 * Handle menu item change events.
11218 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11219 var match
= this.menu
.findItemFromData( this.getValue() );
11220 this.menu
.selectItem( match
);
11221 if ( this.menu
.findHighlightedItem() ) {
11222 this.menu
.highlightItem( match
);
11224 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11228 * Handle menu toggle events.
11231 * @param {boolean} isVisible Open state of the menu
11233 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11234 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11240 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
11242 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
11244 if ( this.dropdownButton
) {
11245 this.dropdownButton
.setDisabled( this.isDisabled() );
11248 this.menu
.setDisabled( this.isDisabled() );
11255 * Set the options available for this input.
11257 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11260 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11263 .addItems( options
.map( function ( opt
) {
11264 return new OO
.ui
.MenuOptionWidget( {
11266 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11274 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11275 * which is a widget that is specified by reference before any optional configuration settings.
11277 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
11279 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11280 * A left-alignment is used for forms with many fields.
11281 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11282 * A right-alignment is used for long but familiar forms which users tab through,
11283 * verifying the current field with a quick glance at the label.
11284 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11285 * that users fill out from top to bottom.
11286 * - **inline**: The label is placed after the field-widget and aligned to the left.
11287 * An inline-alignment is best used with checkboxes or radio buttons.
11289 * Help text can either be:
11291 * - accessed via a help icon that appears in the upper right corner of the rendered field layout, or
11292 * - shown as a subtle explanation below the label.
11294 * If the help text is brief, or is essential to always espose it, set `helpInline` to `true`. If it
11295 * is long or not essential, leave `helpInline` to its default, `false`.
11297 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11299 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11302 * @extends OO.ui.Layout
11303 * @mixins OO.ui.mixin.LabelElement
11304 * @mixins OO.ui.mixin.TitledElement
11307 * @param {OO.ui.Widget} fieldWidget Field widget
11308 * @param {Object} [config] Configuration options
11309 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11311 * @cfg {Array} [errors] Error messages about the widget, which will be
11312 * displayed below the widget.
11313 * The array may contain strings or OO.ui.HtmlSnippet instances.
11314 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11315 * below the widget.
11316 * The array may contain strings or OO.ui.HtmlSnippet instances.
11317 * These are more visible than `help` messages when `helpInline` is set, and so
11318 * might be good for transient messages.
11319 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
11320 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
11321 * corner of the rendered field; clicking it will display the text in a popup.
11322 * If `helpInline` is `true`, then a subtle description will be shown after the
11324 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
11325 * or shown when the "help" icon is clicked.
11326 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
11328 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11330 * @throws {Error} An error is thrown if no widget is specified
11332 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11333 // Allow passing positional parameters inside the config object
11334 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11335 config
= fieldWidget
;
11336 fieldWidget
= config
.fieldWidget
;
11339 // Make sure we have required constructor arguments
11340 if ( fieldWidget
=== undefined ) {
11341 throw new Error( 'Widget not found' );
11344 // Configuration initialization
11345 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
11347 // Parent constructor
11348 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11350 // Mixin constructors
11351 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
11352 $label
: $( '<label>' )
11354 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
11357 this.fieldWidget
= fieldWidget
;
11360 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11361 this.$messages
= $( '<ul>' );
11362 this.$header
= $( '<span>' );
11363 this.$body
= $( '<div>' );
11365 this.helpInline
= config
.helpInline
;
11367 if ( config
.help
) {
11368 if ( this.helpInline
) {
11369 this.$help
= new OO
.ui
.LabelWidget( {
11370 label
: config
.help
,
11371 classes
: [ 'oo-ui-inline-help' ]
11374 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
11375 $overlay
: config
.$overlay
,
11379 classes
: [ 'oo-ui-fieldLayout-help' ],
11382 label
: OO
.ui
.msg( 'ooui-field-help' )
11384 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
11385 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
11387 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
11389 this.$help
= this.popupButtonWidget
.$element
;
11392 this.$help
= $( [] );
11396 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
11399 if ( config
.help
&& !config
.helpInline
) {
11400 // Set the 'aria-describedby' attribute on the fieldWidget
11401 // Preference given to an input or a button
11403 this.fieldWidget
.$input
||
11404 this.fieldWidget
.$button
||
11405 this.fieldWidget
.$element
11407 'aria-describedby',
11408 this.popupButtonWidget
.getPopup().getBodyId()
11411 if ( this.fieldWidget
.getInputId() ) {
11412 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11414 this.$label
.on( 'click', function () {
11415 this.fieldWidget
.simulateLabelClick();
11419 .addClass( 'oo-ui-fieldLayout' )
11420 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11421 .append( this.$body
);
11422 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11423 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11424 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11426 .addClass( 'oo-ui-fieldLayout-field' )
11427 .append( this.fieldWidget
.$element
);
11429 this.setErrors( config
.errors
|| [] );
11430 this.setNotices( config
.notices
|| [] );
11431 this.setAlignment( config
.align
);
11432 // Call this again to take into account the widget's accessKey
11433 this.updateTitle();
11438 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11439 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11440 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
11445 * Handle field disable events.
11448 * @param {boolean} value Field is disabled
11450 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
11451 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11455 * Get the widget contained by the field.
11457 * @return {OO.ui.Widget} Field widget
11459 OO
.ui
.FieldLayout
.prototype.getField = function () {
11460 return this.fieldWidget
;
11464 * Return `true` if the given field widget can be used with `'inline'` alignment (see
11465 * #setAlignment). Return `false` if it can't or if this can't be determined.
11467 * @return {boolean}
11469 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11470 // This is very simplistic, but should be good enough.
11471 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11476 * @param {string} kind 'error' or 'notice'
11477 * @param {string|OO.ui.HtmlSnippet} text
11480 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11481 var $listItem
, $icon
, message
;
11482 $listItem
= $( '<li>' );
11483 if ( kind
=== 'error' ) {
11484 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11485 $listItem
.attr( 'role', 'alert' );
11486 } else if ( kind
=== 'notice' ) {
11487 $icon
= new OO
.ui
.IconWidget( { icon
: 'notice' } ).$element
;
11491 message
= new OO
.ui
.LabelWidget( { label
: text
} );
11493 .append( $icon
, message
.$element
)
11494 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
11499 * Set the field alignment mode.
11502 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
11505 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
11506 if ( value
!== this.align
) {
11507 // Default to 'left'
11508 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
11512 if ( value
=== 'inline' && !this.isFieldInline() ) {
11515 // Reorder elements
11517 if ( this.helpInline
) {
11518 if ( value
=== 'inline' ) {
11519 this.$header
.append( this.$label
, this.$help
);
11520 this.$body
.append( this.$field
, this.$header
);
11522 this.$header
.append( this.$label
, this.$help
);
11523 this.$body
.append( this.$header
, this.$field
);
11526 if ( value
=== 'top' ) {
11527 this.$header
.append( this.$help
, this.$label
);
11528 this.$body
.append( this.$header
, this.$field
);
11529 } else if ( value
=== 'inline' ) {
11530 this.$header
.append( this.$help
, this.$label
);
11531 this.$body
.append( this.$field
, this.$header
);
11533 this.$header
.append( this.$label
);
11534 this.$body
.append( this.$header
, this.$help
, this.$field
);
11537 // Set classes. The following classes can be used here:
11538 // * oo-ui-fieldLayout-align-left
11539 // * oo-ui-fieldLayout-align-right
11540 // * oo-ui-fieldLayout-align-top
11541 // * oo-ui-fieldLayout-align-inline
11542 if ( this.align
) {
11543 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
11545 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
11546 this.align
= value
;
11553 * Set the list of error messages.
11555 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11556 * The array may contain strings or OO.ui.HtmlSnippet instances.
11559 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
11560 this.errors
= errors
.slice();
11561 this.updateMessages();
11566 * Set the list of notice messages.
11568 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11569 * The array may contain strings or OO.ui.HtmlSnippet instances.
11572 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
11573 this.notices
= notices
.slice();
11574 this.updateMessages();
11579 * Update the rendering of error and notice messages.
11583 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
11585 this.$messages
.empty();
11587 if ( this.errors
.length
|| this.notices
.length
) {
11588 this.$body
.after( this.$messages
);
11590 this.$messages
.remove();
11594 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
11595 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
11597 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
11598 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
11603 * Include information about the widget's accessKey in our title. TitledElement calls this method.
11604 * (This is a bit of a hack.)
11607 * @param {string} title Tooltip label for 'title' attribute
11610 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
11611 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
11612 return this.fieldWidget
.formatTitleWithAccessKey( title
);
11618 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
11619 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
11620 * is required and is specified before any optional configuration settings.
11622 * Labels can be aligned in one of four ways:
11624 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11625 * A left-alignment is used for forms with many fields.
11626 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11627 * A right-alignment is used for long but familiar forms which users tab through,
11628 * verifying the current field with a quick glance at the label.
11629 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11630 * that users fill out from top to bottom.
11631 * - **inline**: The label is placed after the field-widget and aligned to the left.
11632 * An inline-alignment is best used with checkboxes or radio buttons.
11634 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
11635 * text is specified.
11638 * // Example of an ActionFieldLayout
11639 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
11640 * new OO.ui.TextInputWidget( {
11641 * placeholder: 'Field widget'
11643 * new OO.ui.ButtonWidget( {
11647 * label: 'An ActionFieldLayout. This label is aligned top',
11649 * help: 'This is help text'
11653 * $( 'body' ).append( actionFieldLayout.$element );
11656 * @extends OO.ui.FieldLayout
11659 * @param {OO.ui.Widget} fieldWidget Field widget
11660 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
11661 * @param {Object} config
11663 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
11664 // Allow passing positional parameters inside the config object
11665 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11666 config
= fieldWidget
;
11667 fieldWidget
= config
.fieldWidget
;
11668 buttonWidget
= config
.buttonWidget
;
11671 // Parent constructor
11672 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
11675 this.buttonWidget
= buttonWidget
;
11676 this.$button
= $( '<span>' );
11677 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11681 .addClass( 'oo-ui-actionFieldLayout' );
11683 .addClass( 'oo-ui-actionFieldLayout-button' )
11684 .append( this.buttonWidget
.$element
);
11686 .addClass( 'oo-ui-actionFieldLayout-input' )
11687 .append( this.fieldWidget
.$element
);
11689 .append( this.$input
, this.$button
);
11694 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
11697 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
11698 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
11699 * configured with a label as well. For more information and examples,
11700 * please see the [OOUI documentation on MediaWiki][1].
11703 * // Example of a fieldset layout
11704 * var input1 = new OO.ui.TextInputWidget( {
11705 * placeholder: 'A text input field'
11708 * var input2 = new OO.ui.TextInputWidget( {
11709 * placeholder: 'A text input field'
11712 * var fieldset = new OO.ui.FieldsetLayout( {
11713 * label: 'Example of a fieldset layout'
11716 * fieldset.addItems( [
11717 * new OO.ui.FieldLayout( input1, {
11718 * label: 'Field One'
11720 * new OO.ui.FieldLayout( input2, {
11721 * label: 'Field Two'
11724 * $( 'body' ).append( fieldset.$element );
11726 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11729 * @extends OO.ui.Layout
11730 * @mixins OO.ui.mixin.IconElement
11731 * @mixins OO.ui.mixin.LabelElement
11732 * @mixins OO.ui.mixin.GroupElement
11735 * @param {Object} [config] Configuration options
11736 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
11737 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
11738 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
11739 * For important messages, you are advised to use `notices`, as they are always shown.
11740 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
11741 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11743 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
11744 // Configuration initialization
11745 config
= config
|| {};
11747 // Parent constructor
11748 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
11750 // Mixin constructors
11751 OO
.ui
.mixin
.IconElement
.call( this, config
);
11752 OO
.ui
.mixin
.LabelElement
.call( this, config
);
11753 OO
.ui
.mixin
.GroupElement
.call( this, config
);
11756 this.$header
= $( '<legend>' );
11757 if ( config
.help
) {
11758 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
11759 $overlay
: config
.$overlay
,
11763 classes
: [ 'oo-ui-fieldsetLayout-help' ],
11766 label
: OO
.ui
.msg( 'ooui-field-help' )
11768 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
11769 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
11771 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
11773 this.$help
= this.popupButtonWidget
.$element
;
11775 this.$help
= $( [] );
11780 .addClass( 'oo-ui-fieldsetLayout-header' )
11781 .append( this.$icon
, this.$label
, this.$help
);
11782 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
11784 .addClass( 'oo-ui-fieldsetLayout' )
11785 .prepend( this.$header
, this.$group
);
11786 if ( Array
.isArray( config
.items
) ) {
11787 this.addItems( config
.items
);
11793 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
11794 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
11795 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
11796 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
11798 /* Static Properties */
11804 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
11807 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
11808 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
11809 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
11810 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
11812 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
11813 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
11814 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
11815 * some fancier controls. Some controls have both regular and InputWidget variants, for example
11816 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
11817 * often have simplified APIs to match the capabilities of HTML forms.
11818 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
11820 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
11821 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
11824 * // Example of a form layout that wraps a fieldset layout
11825 * var input1 = new OO.ui.TextInputWidget( {
11826 * placeholder: 'Username'
11828 * var input2 = new OO.ui.TextInputWidget( {
11829 * placeholder: 'Password',
11832 * var submit = new OO.ui.ButtonInputWidget( {
11836 * var fieldset = new OO.ui.FieldsetLayout( {
11837 * label: 'A form layout'
11839 * fieldset.addItems( [
11840 * new OO.ui.FieldLayout( input1, {
11841 * label: 'Username',
11844 * new OO.ui.FieldLayout( input2, {
11845 * label: 'Password',
11848 * new OO.ui.FieldLayout( submit )
11850 * var form = new OO.ui.FormLayout( {
11851 * items: [ fieldset ],
11852 * action: '/api/formhandler',
11855 * $( 'body' ).append( form.$element );
11858 * @extends OO.ui.Layout
11859 * @mixins OO.ui.mixin.GroupElement
11862 * @param {Object} [config] Configuration options
11863 * @cfg {string} [method] HTML form `method` attribute
11864 * @cfg {string} [action] HTML form `action` attribute
11865 * @cfg {string} [enctype] HTML form `enctype` attribute
11866 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
11868 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
11871 // Configuration initialization
11872 config
= config
|| {};
11874 // Parent constructor
11875 OO
.ui
.FormLayout
.parent
.call( this, config
);
11877 // Mixin constructors
11878 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11881 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
11883 // Make sure the action is safe
11884 action
= config
.action
;
11885 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
11886 action
= './' + action
;
11891 .addClass( 'oo-ui-formLayout' )
11893 method
: config
.method
,
11895 enctype
: config
.enctype
11897 if ( Array
.isArray( config
.items
) ) {
11898 this.addItems( config
.items
);
11904 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
11905 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
11910 * A 'submit' event is emitted when the form is submitted.
11915 /* Static Properties */
11921 OO
.ui
.FormLayout
.static.tagName
= 'form';
11926 * Handle form submit events.
11929 * @param {jQuery.Event} e Submit event
11932 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
11933 if ( this.emit( 'submit' ) ) {
11939 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
11940 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
11943 * // Example of a panel layout
11944 * var panel = new OO.ui.PanelLayout( {
11948 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
11950 * $( 'body' ).append( panel.$element );
11953 * @extends OO.ui.Layout
11956 * @param {Object} [config] Configuration options
11957 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
11958 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
11959 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
11960 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
11962 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
11963 // Configuration initialization
11964 config
= $.extend( {
11971 // Parent constructor
11972 OO
.ui
.PanelLayout
.parent
.call( this, config
);
11975 this.$element
.addClass( 'oo-ui-panelLayout' );
11976 if ( config
.scrollable
) {
11977 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
11979 if ( config
.padded
) {
11980 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
11982 if ( config
.expanded
) {
11983 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
11985 if ( config
.framed
) {
11986 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
11992 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
11997 * Focus the panel layout
11999 * The default implementation just focuses the first focusable element in the panel
12001 OO
.ui
.PanelLayout
.prototype.focus = function () {
12002 OO
.ui
.findFocusable( this.$element
).focus();
12006 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12007 * items), with small margins between them. Convenient when you need to put a number of block-level
12008 * widgets on a single line next to each other.
12010 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12013 * // HorizontalLayout with a text input and a label
12014 * var layout = new OO.ui.HorizontalLayout( {
12016 * new OO.ui.LabelWidget( { label: 'Label' } ),
12017 * new OO.ui.TextInputWidget( { value: 'Text' } )
12020 * $( 'body' ).append( layout.$element );
12023 * @extends OO.ui.Layout
12024 * @mixins OO.ui.mixin.GroupElement
12027 * @param {Object} [config] Configuration options
12028 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12030 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12031 // Configuration initialization
12032 config
= config
|| {};
12034 // Parent constructor
12035 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12037 // Mixin constructors
12038 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12041 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12042 if ( Array
.isArray( config
.items
) ) {
12043 this.addItems( config
.items
);
12049 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12050 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12053 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12054 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12055 * (to adjust the value in increments) to allow the user to enter a number.
12058 * // Example: A NumberInputWidget.
12059 * var numberInput = new OO.ui.NumberInputWidget( {
12060 * label: 'NumberInputWidget',
12061 * input: { value: 5 },
12065 * $( 'body' ).append( numberInput.$element );
12068 * @extends OO.ui.TextInputWidget
12071 * @param {Object} [config] Configuration options
12072 * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
12073 * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
12074 * @cfg {boolean} [allowInteger=false] Whether the field accepts only integer values.
12075 * @cfg {number} [min=-Infinity] Minimum allowed value
12076 * @cfg {number} [max=Infinity] Maximum allowed value
12077 * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
12078 * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
12079 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12081 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12082 var $field
= $( '<div>' )
12083 .addClass( 'oo-ui-numberInputWidget-field' );
12085 // Configuration initialization
12086 config
= $.extend( {
12087 allowInteger
: false,
12095 // For backward compatibility
12096 $.extend( config
, config
.input
);
12099 // Parent constructor
12100 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12104 if ( config
.showButtons
) {
12105 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12107 disabled
: this.isDisabled(),
12109 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12114 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12115 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12117 disabled
: this.isDisabled(),
12119 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12124 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12129 keydown
: this.onKeyDown
.bind( this ),
12130 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12132 if ( config
.showButtons
) {
12133 this.plusButton
.connect( this, {
12134 click
: [ 'onButtonClick', +1 ]
12136 this.minusButton
.connect( this, {
12137 click
: [ 'onButtonClick', -1 ]
12142 $field
.append( this.$input
);
12143 if ( config
.showButtons
) {
12145 .prepend( this.minusButton
.$element
)
12146 .append( this.plusButton
.$element
);
12150 this.setAllowInteger( config
.allowInteger
|| config
.isInteger
);
12151 this.setRange( config
.min
, config
.max
);
12152 this.setStep( config
.step
, config
.pageStep
);
12153 // Set the validation method after we set allowInteger and range
12154 // so that it doesn't immediately call setValidityFlag
12155 this.setValidation( this.validateNumber
.bind( this ) );
12158 .addClass( 'oo-ui-numberInputWidget' )
12159 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12165 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12170 * Set whether only integers are allowed
12172 * @param {boolean} flag
12174 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12175 this.allowInteger
= !!flag
;
12176 this.setValidityFlag();
12178 // Backward compatibility
12179 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12182 * Get whether only integers are allowed
12184 * @return {boolean} Flag value
12186 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12187 return this.allowInteger
;
12189 // Backward compatibility
12190 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12193 * Set the range of allowed values
12195 * @param {number} min Minimum allowed value
12196 * @param {number} max Maximum allowed value
12198 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12200 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12204 this.$input
.attr( 'min', this.min
);
12205 this.$input
.attr( 'max', this.max
);
12206 this.setValidityFlag();
12210 * Get the current range
12212 * @return {number[]} Minimum and maximum values
12214 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12215 return [ this.min
, this.max
];
12219 * Set the stepping deltas
12221 * @param {number} step Normal step
12222 * @param {number|null} pageStep Page step. If null, 10 * step will be used.
12224 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( step
, pageStep
) {
12226 throw new Error( 'Step value must be positive' );
12228 if ( pageStep
=== null ) {
12229 pageStep
= step
* 10;
12230 } else if ( pageStep
<= 0 ) {
12231 throw new Error( 'Page step value must be positive' );
12234 this.pageStep
= pageStep
;
12235 this.$input
.attr( 'step', this.step
);
12241 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
12242 if ( value
=== '' ) {
12243 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
12244 // so here we make sure an 'empty' value is actually displayed as such.
12245 this.$input
.val( '' );
12247 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
12251 * Get the current stepping values
12253 * @return {number[]} Step and page step
12255 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
12256 return [ this.step
, this.pageStep
];
12260 * Get the current value of the widget as a number
12262 * @return {number} May be NaN, or an invalid number
12264 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
12265 return +this.getValue();
12269 * Adjust the value of the widget
12271 * @param {number} delta Adjustment amount
12273 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
12274 var n
, v
= this.getNumericValue();
12277 if ( isNaN( delta
) || !isFinite( delta
) ) {
12278 throw new Error( 'Delta must be a finite number' );
12281 if ( isNaN( v
) ) {
12285 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
12286 if ( this.allowInteger
) {
12287 n
= Math
.round( n
);
12292 this.setValue( n
);
12299 * @param {string} value Field value
12300 * @return {boolean}
12302 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
12304 if ( value
=== '' ) {
12305 return !this.isRequired();
12308 if ( isNaN( n
) || !isFinite( n
) ) {
12312 if ( this.allowInteger
&& Math
.floor( n
) !== n
) {
12316 if ( n
< this.min
|| n
> this.max
) {
12324 * Handle mouse click events.
12327 * @param {number} dir +1 or -1
12329 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
12330 this.adjustValue( dir
* this.step
);
12334 * Handle mouse wheel events.
12337 * @param {jQuery.Event} event
12339 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
12342 if ( !this.isDisabled() && this.$input
.is( ':focus' ) ) {
12343 // Standard 'wheel' event
12344 if ( event
.originalEvent
.deltaMode
!== undefined ) {
12345 this.sawWheelEvent
= true;
12347 if ( event
.originalEvent
.deltaY
) {
12348 delta
= -event
.originalEvent
.deltaY
;
12349 } else if ( event
.originalEvent
.deltaX
) {
12350 delta
= event
.originalEvent
.deltaX
;
12353 // Non-standard events
12354 if ( !this.sawWheelEvent
) {
12355 if ( event
.originalEvent
.wheelDeltaX
) {
12356 delta
= -event
.originalEvent
.wheelDeltaX
;
12357 } else if ( event
.originalEvent
.wheelDeltaY
) {
12358 delta
= event
.originalEvent
.wheelDeltaY
;
12359 } else if ( event
.originalEvent
.wheelDelta
) {
12360 delta
= event
.originalEvent
.wheelDelta
;
12361 } else if ( event
.originalEvent
.detail
) {
12362 delta
= -event
.originalEvent
.detail
;
12367 delta
= delta
< 0 ? -1 : 1;
12368 this.adjustValue( delta
* this.step
);
12376 * Handle key down events.
12379 * @param {jQuery.Event} e Key down event
12381 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
12382 if ( !this.isDisabled() ) {
12383 switch ( e
.which
) {
12384 case OO
.ui
.Keys
.UP
:
12385 this.adjustValue( this.step
);
12387 case OO
.ui
.Keys
.DOWN
:
12388 this.adjustValue( -this.step
);
12390 case OO
.ui
.Keys
.PAGEUP
:
12391 this.adjustValue( this.pageStep
);
12393 case OO
.ui
.Keys
.PAGEDOWN
:
12394 this.adjustValue( -this.pageStep
);
12403 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
12405 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
12407 if ( this.minusButton
) {
12408 this.minusButton
.setDisabled( this.isDisabled() );
12410 if ( this.plusButton
) {
12411 this.plusButton
.setDisabled( this.isDisabled() );
12419 //# sourceMappingURL=oojs-ui-core.js.map.json