3 * https://www.mediawiki.org/wiki/OOUI
5 * Copyright 2011–2019 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2019-01-10T07:00:09Z
16 * Namespace for all classes, static methods and static properties.
48 * Constants for MouseEvent.which
52 OO
.ui
.MouseButtons
= {
65 * Generate a unique ID for element
69 OO
.ui
.generateElementId = function () {
71 return 'ooui-' + OO
.ui
.elementId
;
75 * Check if an element is focusable.
76 * Inspired by :focusable in jQueryUI v1.11.4 - 2015-04-14
78 * @param {jQuery} $element Element to test
79 * @return {boolean} Element is focusable
81 OO
.ui
.isFocusableElement = function ( $element
) {
83 element
= $element
[ 0 ];
85 // Anything disabled is not focusable
86 if ( element
.disabled
) {
90 // Check if the element is visible
92 // This is quicker than calling $element.is( ':visible' )
93 $.expr
.pseudos
.visible( element
) &&
94 // Check that all parents are visible
95 !$element
.parents().addBack().filter( function () {
96 return $.css( this, 'visibility' ) === 'hidden';
102 // Check if the element is ContentEditable, which is the string 'true'
103 if ( element
.contentEditable
=== 'true' ) {
107 // Anything with a non-negative numeric tabIndex is focusable.
108 // Use .prop to avoid browser bugs
109 if ( $element
.prop( 'tabIndex' ) >= 0 ) {
113 // Some element types are naturally focusable
114 // (indexOf is much faster than regex in Chrome and about the
115 // same in FF: https://jsperf.com/regex-vs-indexof-array2)
116 nodeName
= element
.nodeName
.toLowerCase();
117 if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName
) !== -1 ) {
121 // Links and areas are focusable if they have an href
122 if ( ( nodeName
=== 'a' || nodeName
=== 'area' ) && $element
.attr( 'href' ) !== undefined ) {
130 * Find a focusable child
132 * @param {jQuery} $container Container to search in
133 * @param {boolean} [backwards] Search backwards
134 * @return {jQuery} Focusable child, or an empty jQuery object if none found
136 OO
.ui
.findFocusable = function ( $container
, backwards
) {
137 var $focusable
= $( [] ),
138 // $focusableCandidates is a superset of things that
139 // could get matched by isFocusableElement
140 $focusableCandidates
= $container
141 .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
144 $focusableCandidates
= Array
.prototype.reverse
.call( $focusableCandidates
);
147 $focusableCandidates
.each( function () {
148 var $this = $( this );
149 if ( OO
.ui
.isFocusableElement( $this ) ) {
158 * Get the user's language and any fallback languages.
160 * These language codes are used to localize user interface elements in the user's language.
162 * In environments that provide a localization system, this function should be overridden to
163 * return the user's language(s). The default implementation returns English (en) only.
165 * @return {string[]} Language codes, in descending order of priority
167 OO
.ui
.getUserLanguages = function () {
172 * Get a value in an object keyed by language code.
174 * @param {Object.<string,Mixed>} obj Object keyed by language code
175 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
176 * @param {string} [fallback] Fallback code, used if no matching language can be found
177 * @return {Mixed} Local value
179 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
182 // Requested language
186 // Known user language
187 langs
= OO
.ui
.getUserLanguages();
188 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
195 if ( obj
[ fallback
] ) {
196 return obj
[ fallback
];
198 // First existing language
199 for ( lang
in obj
) {
207 * Check if a node is contained within another node
209 * Similar to jQuery#contains except a list of containers can be supplied
210 * and a boolean argument allows you to include the container in the match list
212 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
213 * @param {HTMLElement} contained Node to find
214 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
215 * @return {boolean} The node is in the list of target nodes
217 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
219 if ( !Array
.isArray( containers
) ) {
220 containers
= [ containers
];
222 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
223 if ( ( matchContainers
&& contained
=== containers
[ i
] ) || $.contains( containers
[ i
], contained
) ) {
231 * Return a function, that, as long as it continues to be invoked, will not
232 * be triggered. The function will be called after it stops being called for
233 * N milliseconds. If `immediate` is passed, trigger the function on the
234 * leading edge, instead of the trailing.
236 * Ported from: http://underscorejs.org/underscore.js
238 * @param {Function} func Function to debounce
239 * @param {number} [wait=0] Wait period in milliseconds
240 * @param {boolean} [immediate] Trigger on leading edge
241 * @return {Function} Debounced function
243 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
248 later = function () {
251 func
.apply( context
, args
);
254 if ( immediate
&& !timeout
) {
255 func
.apply( context
, args
);
257 if ( !timeout
|| wait
) {
258 clearTimeout( timeout
);
259 timeout
= setTimeout( later
, wait
);
265 * Puts a console warning with provided message.
267 * @param {string} message Message
269 OO
.ui
.warnDeprecation = function ( message
) {
270 if ( OO
.getProp( window
, 'console', 'warn' ) !== undefined ) {
271 // eslint-disable-next-line no-console
272 console
.warn( message
);
277 * Returns a function, that, when invoked, will only be triggered at most once
278 * during a given window of time. If called again during that window, it will
279 * wait until the window ends and then trigger itself again.
281 * As it's not knowable to the caller whether the function will actually run
282 * when the wrapper is called, return values from the function are entirely
285 * @param {Function} func Function to throttle
286 * @param {number} wait Throttle window length, in milliseconds
287 * @return {Function} Throttled function
289 OO
.ui
.throttle = function ( func
, wait
) {
290 var context
, args
, timeout
,
294 previous
= OO
.ui
.now();
295 func
.apply( context
, args
);
298 // Check how long it's been since the last time the function was
299 // called, and whether it's more or less than the requested throttle
300 // period. If it's less, run the function immediately. If it's more,
301 // set a timeout for the remaining time -- but don't replace an
302 // existing timeout, since that'd indefinitely prolong the wait.
303 var remaining
= wait
- ( OO
.ui
.now() - previous
);
306 if ( remaining
<= 0 ) {
307 // Note: unless wait was ridiculously large, this means we'll
308 // automatically run the first time the function was called in a
309 // given period. (If you provide a wait period larger than the
310 // current Unix timestamp, you *deserve* unexpected behavior.)
311 clearTimeout( timeout
);
313 } else if ( !timeout
) {
314 timeout
= setTimeout( run
, remaining
);
320 * A (possibly faster) way to get the current timestamp as an integer
322 * @return {number} Current timestamp, in milliseconds since the Unix epoch
324 OO
.ui
.now
= Date
.now
|| function () {
325 return new Date().getTime();
329 * Reconstitute a JavaScript object corresponding to a widget created by
330 * the PHP implementation.
332 * This is an alias for `OO.ui.Element.static.infuse()`.
334 * @param {string|HTMLElement|jQuery} idOrNode
335 * A DOM id (if a string) or node for the widget to infuse.
336 * @param {Object} [config] Configuration options
337 * @return {OO.ui.Element}
338 * The `OO.ui.Element` corresponding to this (infusable) document node.
340 OO
.ui
.infuse = function ( idOrNode
, config
) {
341 return OO
.ui
.Element
.static.infuse( idOrNode
, config
);
346 * Message store for the default implementation of OO.ui.msg
348 * Environments that provide a localization system should not use this, but should override
349 * OO.ui.msg altogether.
354 // Tool tip for a button that moves items in a list down one place
355 'ooui-outline-control-move-down': 'Move item down',
356 // Tool tip for a button that moves items in a list up one place
357 'ooui-outline-control-move-up': 'Move item up',
358 // Tool tip for a button that removes items from a list
359 'ooui-outline-control-remove': 'Remove item',
360 // Label for the toolbar group that contains a list of all other available tools
361 'ooui-toolbar-more': 'More',
362 // Label for the fake tool that expands the full list of tools in a toolbar group
363 'ooui-toolgroup-expand': 'More',
364 // Label for the fake tool that collapses the full list of tools in a toolbar group
365 'ooui-toolgroup-collapse': 'Fewer',
366 // Default label for the tooltip for the button that removes a tag item
367 'ooui-item-remove': 'Remove',
368 // Default label for the accept button of a confirmation dialog
369 'ooui-dialog-message-accept': 'OK',
370 // Default label for the reject button of a confirmation dialog
371 'ooui-dialog-message-reject': 'Cancel',
372 // Title for process dialog error description
373 'ooui-dialog-process-error': 'Something went wrong',
374 // Label for process dialog dismiss error button, visible when describing errors
375 'ooui-dialog-process-dismiss': 'Dismiss',
376 // Label for process dialog retry action button, visible when describing only recoverable errors
377 'ooui-dialog-process-retry': 'Try again',
378 // Label for process dialog retry action button, visible when describing only warnings
379 'ooui-dialog-process-continue': 'Continue',
380 // Label for button in combobox input that triggers its dropdown
381 'ooui-combobox-button-label': 'Dropdown for combobox',
382 // Label for the file selection widget's select file button
383 'ooui-selectfile-button-select': 'Select a file',
384 // Label for the file selection widget if file selection is not supported
385 'ooui-selectfile-not-supported': 'File selection is not supported',
386 // Label for the file selection widget when no file is currently selected
387 'ooui-selectfile-placeholder': 'No file is selected',
388 // Label for the file selection widget's drop target
389 'ooui-selectfile-dragdrop-placeholder': 'Drop file here',
390 // Label for the help icon attached to a form field
391 'ooui-field-help': 'Help'
395 * Get a localized message.
397 * After the message key, message parameters may optionally be passed. In the default implementation,
398 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
399 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
400 * they support unnamed, ordered message parameters.
402 * In environments that provide a localization system, this function should be overridden to
403 * return the message translated in the user's language. The default implementation always returns
404 * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
408 * var i, iLen, button,
409 * messagePath = 'oojs-ui/dist/i18n/',
410 * languages = [ $.i18n().locale, 'ur', 'en' ],
413 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
414 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
417 * $.i18n().load( languageMap ).done( function() {
418 * // Replace the built-in `msg` only once we've loaded the internationalization.
419 * // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
420 * // you put off creating any widgets until this promise is complete, no English
421 * // will be displayed.
422 * OO.ui.msg = $.i18n;
424 * // A button displaying "OK" in the default locale
425 * button = new OO.ui.ButtonWidget( {
426 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
429 * $( document.body ).append( button.$element );
431 * // A button displaying "OK" in Urdu
432 * $.i18n().locale = 'ur';
433 * button = new OO.ui.ButtonWidget( {
434 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
437 * $( document.body ).append( button.$element );
440 * @param {string} key Message key
441 * @param {...Mixed} [params] Message parameters
442 * @return {string} Translated message with parameters substituted
444 OO
.ui
.msg = function ( key
) {
445 var message
= messages
[ key
],
446 params
= Array
.prototype.slice
.call( arguments
, 1 );
447 if ( typeof message
=== 'string' ) {
448 // Perform $1 substitution
449 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
450 var i
= parseInt( n
, 10 );
451 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
454 // Return placeholder if message not found
455 message
= '[' + key
+ ']';
462 * Package a message and arguments for deferred resolution.
464 * Use this when you are statically specifying a message and the message may not yet be present.
466 * @param {string} key Message key
467 * @param {...Mixed} [params] Message parameters
468 * @return {Function} Function that returns the resolved message when executed
470 OO
.ui
.deferMsg = function () {
471 var args
= arguments
;
473 return OO
.ui
.msg
.apply( OO
.ui
, args
);
480 * If the message is a function it will be executed, otherwise it will pass through directly.
482 * @param {Function|string} msg Deferred message, or message text
483 * @return {string} Resolved message
485 OO
.ui
.resolveMsg = function ( msg
) {
486 if ( typeof msg
=== 'function' ) {
493 * @param {string} url
496 OO
.ui
.isSafeUrl = function ( url
) {
497 // Keep this function in sync with php/Tag.php
498 var i
, protocolWhitelist
;
500 function stringStartsWith( haystack
, needle
) {
501 return haystack
.substr( 0, needle
.length
) === needle
;
504 protocolWhitelist
= [
505 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
506 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
507 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
514 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
515 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
520 // This matches '//' too
521 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
524 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
532 * Check if the user has a 'mobile' device.
534 * For our purposes this means the user is primarily using an
535 * on-screen keyboard, touch input instead of a mouse and may
536 * have a physically small display.
538 * It is left up to implementors to decide how to compute this
539 * so the default implementation always returns false.
541 * @return {boolean} User is on a mobile device
543 OO
.ui
.isMobile = function () {
548 * Get the additional spacing that should be taken into account when displaying elements that are
549 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
550 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
552 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
553 * the extra spacing from that edge of viewport (in pixels)
555 OO
.ui
.getViewportSpacing = function () {
565 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
566 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
568 * @return {jQuery} Default overlay node
570 OO
.ui
.getDefaultOverlay = function () {
571 if ( !OO
.ui
.$defaultOverlay
) {
572 OO
.ui
.$defaultOverlay
= $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
573 $( document
.body
).append( OO
.ui
.$defaultOverlay
);
575 return OO
.ui
.$defaultOverlay
;
583 * Namespace for OOUI mixins.
585 * Mixins are named according to the type of object they are intended to
586 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
587 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
588 * is intended to be mixed in to an instance of OO.ui.Widget.
596 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
597 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
598 * connected to them and can't be interacted with.
604 * @param {Object} [config] Configuration options
605 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
606 * to the top level (e.g., the outermost div) of the element. See the [OOUI documentation on MediaWiki][2]
608 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
609 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
610 * @cfg {string} [text] Text to insert
611 * @cfg {Array} [content] An array of content elements to append (after #text).
612 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
613 * Instances of OO.ui.Element will have their $element appended.
614 * @cfg {jQuery} [$content] Content elements to append (after #text).
615 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
616 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
617 * Data can also be specified with the #setData method.
619 OO
.ui
.Element
= function OoUiElement( config
) {
620 if ( OO
.ui
.isDemo
) {
621 this.initialConfig
= config
;
623 // Configuration initialization
624 config
= config
|| {};
628 this.elementId
= null;
630 this.data
= config
.data
;
631 this.$element
= config
.$element
||
632 $( document
.createElement( this.getTagName() ) );
633 this.elementGroup
= null;
636 if ( Array
.isArray( config
.classes
) ) {
637 this.$element
.addClass( config
.classes
);
640 this.setElementId( config
.id
);
643 this.$element
.text( config
.text
);
645 if ( config
.content
) {
646 // The `content` property treats plain strings as text; use an
647 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
648 // appropriate $element appended.
649 this.$element
.append( config
.content
.map( function ( v
) {
650 if ( typeof v
=== 'string' ) {
651 // Escape string so it is properly represented in HTML.
652 return document
.createTextNode( v
);
653 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
656 } else if ( v
instanceof OO
.ui
.Element
) {
662 if ( config
.$content
) {
663 // The `$content` property treats plain strings as HTML.
664 this.$element
.append( config
.$content
);
670 OO
.initClass( OO
.ui
.Element
);
672 /* Static Properties */
675 * The name of the HTML tag used by the element.
677 * The static value may be ignored if the #getTagName method is overridden.
683 OO
.ui
.Element
.static.tagName
= 'div';
688 * Reconstitute a JavaScript object corresponding to a widget created
689 * by the PHP implementation.
691 * @param {string|HTMLElement|jQuery} idOrNode
692 * A DOM id (if a string) or node for the widget to infuse.
693 * @param {Object} [config] Configuration options
694 * @return {OO.ui.Element}
695 * The `OO.ui.Element` corresponding to this (infusable) document node.
696 * For `Tag` objects emitted on the HTML side (used occasionally for content)
697 * the value returned is a newly-created Element wrapping around the existing
700 OO
.ui
.Element
.static.infuse = function ( idOrNode
, config
) {
701 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, config
, false );
703 if ( typeof idOrNode
=== 'string' ) {
704 // IDs deprecated since 0.29.7
705 OO
.ui
.warnDeprecation(
706 'Passing a string ID to infuse is deprecated. Use an HTMLElement or jQuery collection instead.'
709 // Verify that the type matches up.
710 // FIXME: uncomment after T89721 is fixed, see T90929.
712 if ( !( obj instanceof this['class'] ) ) {
713 throw new Error( 'Infusion type mismatch!' );
720 * Implementation helper for `infuse`; skips the type check and has an
721 * extra property so that only the top-level invocation touches the DOM.
724 * @param {string|HTMLElement|jQuery} idOrNode
725 * @param {Object} [config] Configuration options
726 * @param {jQuery.Promise} [domPromise] A promise that will be resolved
727 * when the top-level widget of this infusion is inserted into DOM,
728 * replacing the original node; only used internally.
729 * @return {OO.ui.Element}
731 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, config
, domPromise
) {
732 // look for a cached result of a previous infusion.
733 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
734 if ( typeof idOrNode
=== 'string' ) {
736 $elem
= $( document
.getElementById( id
) );
738 $elem
= $( idOrNode
);
739 id
= $elem
.attr( 'id' );
741 if ( !$elem
.length
) {
742 if ( typeof idOrNode
=== 'string' ) {
743 error
= 'Widget not found: ' + idOrNode
;
744 } else if ( idOrNode
&& idOrNode
.selector
) {
745 error
= 'Widget not found: ' + idOrNode
.selector
;
747 error
= 'Widget not found';
749 throw new Error( error
);
751 if ( $elem
[ 0 ].oouiInfused
) {
752 $elem
= $elem
[ 0 ].oouiInfused
;
754 data
= $elem
.data( 'ooui-infused' );
757 if ( data
=== true ) {
758 throw new Error( 'Circular dependency! ' + id
);
761 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
762 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
763 // restore dynamic state after the new element is re-inserted into DOM under infused parent
764 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
765 infusedChildren
= $elem
.data( 'ooui-infused-children' );
766 if ( infusedChildren
&& infusedChildren
.length
) {
767 infusedChildren
.forEach( function ( data
) {
768 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
769 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
775 data
= $elem
.attr( 'data-ooui' );
777 throw new Error( 'No infusion data found: ' + id
);
780 data
= JSON
.parse( data
);
784 if ( !( data
&& data
._
) ) {
785 throw new Error( 'No valid infusion data found: ' + id
);
787 if ( data
._
=== 'Tag' ) {
788 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
789 return new OO
.ui
.Element( $.extend( {}, config
, { $element
: $elem
} ) );
791 parts
= data
._
.split( '.' );
792 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
793 if ( cls
=== undefined ) {
794 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
797 // Verify that we're creating an OO.ui.Element instance
800 while ( parent
!== undefined ) {
801 if ( parent
=== OO
.ui
.Element
) {
806 parent
= parent
.parent
;
809 if ( parent
!== OO
.ui
.Element
) {
810 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
815 domPromise
= top
.promise();
817 $elem
.data( 'ooui-infused', true ); // prevent loops
818 data
.id
= id
; // implicit
819 infusedChildren
= [];
820 data
= OO
.copy( data
, null, function deserialize( value
) {
822 if ( OO
.isPlainObject( value
) ) {
824 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, config
, domPromise
);
825 infusedChildren
.push( infused
);
826 // Flatten the structure
827 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
828 infused
.$element
.removeData( 'ooui-infused-children' );
831 if ( value
.html
!== undefined ) {
832 return new OO
.ui
.HtmlSnippet( value
.html
);
836 // allow widgets to reuse parts of the DOM
837 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
838 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
839 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
841 // eslint-disable-next-line new-cap
842 obj
= new cls( $.extend( {}, config
, data
) );
843 // If anyone is holding a reference to the old DOM element,
844 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
845 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
846 $elem
[ 0 ].oouiInfused
= obj
.$element
;
847 // now replace old DOM with this new DOM.
849 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
850 // so only mutate the DOM if we need to.
851 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
852 $elem
.replaceWith( obj
.$element
);
856 obj
.$element
.data( 'ooui-infused', obj
);
857 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
858 // set the 'data-ooui' attribute so we can identify infused widgets
859 obj
.$element
.attr( 'data-ooui', '' );
860 // restore dynamic state after the new element is inserted into DOM
861 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
866 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
868 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
869 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
870 * constructor, which will be given the enhanced config.
873 * @param {HTMLElement} node
874 * @param {Object} config
877 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
882 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
883 * (and its children) that represent an Element of the same class and the given configuration,
884 * generated by the PHP implementation.
886 * This method is called just before `node` is detached from the DOM. The return value of this
887 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
888 * is inserted into DOM to replace `node`.
891 * @param {HTMLElement} node
892 * @param {Object} config
895 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
900 * Get a jQuery function within a specific document.
903 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
904 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
906 * @return {Function} Bound jQuery function
908 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
909 function wrapper( selector
) {
910 return $( selector
, wrapper
.context
);
913 wrapper
.context
= this.getDocument( context
);
916 wrapper
.$iframe
= $iframe
;
923 * Get the document of an element.
926 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
927 * @return {HTMLDocument|null} Document object
929 OO
.ui
.Element
.static.getDocument = function ( obj
) {
930 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
931 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
932 // Empty jQuery selections might have a context
939 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
944 * Get the window of an element or document.
947 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
948 * @return {Window} Window object
950 OO
.ui
.Element
.static.getWindow = function ( obj
) {
951 var doc
= this.getDocument( obj
);
952 return doc
.defaultView
;
956 * Get the direction of an element or document.
959 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
960 * @return {string} Text direction, either 'ltr' or 'rtl'
962 OO
.ui
.Element
.static.getDir = function ( obj
) {
965 if ( obj
instanceof $ ) {
968 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
969 isWin
= obj
.document
!== undefined;
970 if ( isDoc
|| isWin
) {
976 return $( obj
).css( 'direction' );
980 * Get the offset between two frames.
982 * TODO: Make this function not use recursion.
985 * @param {Window} from Window of the child frame
986 * @param {Window} [to=window] Window of the parent frame
987 * @param {Object} [offset] Offset to start with, used internally
988 * @return {Object} Offset object, containing left and top properties
990 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
991 var i
, len
, frames
, frame
, rect
;
997 offset
= { top
: 0, left
: 0 };
999 if ( from.parent
=== from ) {
1003 // Get iframe element
1004 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
1005 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
1006 if ( frames
[ i
].contentWindow
=== from ) {
1007 frame
= frames
[ i
];
1012 // Recursively accumulate offset values
1014 rect
= frame
.getBoundingClientRect();
1015 offset
.left
+= rect
.left
;
1016 offset
.top
+= rect
.top
;
1017 if ( from !== to
) {
1018 this.getFrameOffset( from.parent
, offset
);
1025 * Get the offset between two elements.
1027 * The two elements may be in a different frame, but in that case the frame $element is in must
1028 * be contained in the frame $anchor is in.
1031 * @param {jQuery} $element Element whose position to get
1032 * @param {jQuery} $anchor Element to get $element's position relative to
1033 * @return {Object} Translated position coordinates, containing top and left properties
1035 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1036 var iframe
, iframePos
,
1037 pos
= $element
.offset(),
1038 anchorPos
= $anchor
.offset(),
1039 elementDocument
= this.getDocument( $element
),
1040 anchorDocument
= this.getDocument( $anchor
);
1042 // If $element isn't in the same document as $anchor, traverse up
1043 while ( elementDocument
!== anchorDocument
) {
1044 iframe
= elementDocument
.defaultView
.frameElement
;
1046 throw new Error( '$element frame is not contained in $anchor frame' );
1048 iframePos
= $( iframe
).offset();
1049 pos
.left
+= iframePos
.left
;
1050 pos
.top
+= iframePos
.top
;
1051 elementDocument
= iframe
.ownerDocument
;
1053 pos
.left
-= anchorPos
.left
;
1054 pos
.top
-= anchorPos
.top
;
1059 * Get element border sizes.
1062 * @param {HTMLElement} el Element to measure
1063 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1065 OO
.ui
.Element
.static.getBorders = function ( el
) {
1066 var doc
= el
.ownerDocument
,
1067 win
= doc
.defaultView
,
1068 style
= win
.getComputedStyle( el
, null ),
1070 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1071 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1072 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1073 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1084 * Get dimensions of an element or window.
1087 * @param {HTMLElement|Window} el Element to measure
1088 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1090 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1092 doc
= el
.ownerDocument
|| el
.document
,
1093 win
= doc
.defaultView
;
1095 if ( win
=== el
|| el
=== doc
.documentElement
) {
1098 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1100 top
: $win
.scrollTop(),
1101 left
: $win
.scrollLeft()
1103 scrollbar
: { right
: 0, bottom
: 0 },
1107 bottom
: $win
.innerHeight(),
1108 right
: $win
.innerWidth()
1114 borders
: this.getBorders( el
),
1116 top
: $el
.scrollTop(),
1117 left
: $el
.scrollLeft()
1120 right
: $el
.innerWidth() - el
.clientWidth
,
1121 bottom
: $el
.innerHeight() - el
.clientHeight
1123 rect
: el
.getBoundingClientRect()
1129 * Get the number of pixels that an element's content is scrolled to the left.
1131 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1132 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1134 * This function smooths out browser inconsistencies (nicely described in the README at
1135 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1136 * with Firefox's 'scrollLeft', which seems the sanest.
1140 * @param {HTMLElement|Window} el Element to measure
1141 * @return {number} Scroll position from the left.
1142 * If the element's direction is LTR, this is a positive number between `0` (initial scroll position)
1143 * and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1144 * If the element's direction is RTL, this is a negative number between `0` (initial scroll position)
1145 * and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1147 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1148 var rtlScrollType
= null;
1151 var $definer
= $( '<div>' ).attr( {
1153 style
: 'font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll;'
1155 definer
= $definer
[ 0 ];
1157 $definer
.appendTo( 'body' );
1158 if ( definer
.scrollLeft
> 0 ) {
1160 rtlScrollType
= 'default';
1162 definer
.scrollLeft
= 1;
1163 if ( definer
.scrollLeft
=== 0 ) {
1164 // Firefox, old Opera
1165 rtlScrollType
= 'negative';
1167 // Internet Explorer, Edge
1168 rtlScrollType
= 'reverse';
1174 return function getScrollLeft( el
) {
1175 var isRoot
= el
.window
=== el
||
1176 el
=== el
.ownerDocument
.body
||
1177 el
=== el
.ownerDocument
.documentElement
,
1178 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1179 // All browsers use the correct scroll type ('negative') on the root, so don't
1180 // do any fixups when looking at the root element
1181 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1183 if ( direction
=== 'rtl' ) {
1184 if ( rtlScrollType
=== null ) {
1187 if ( rtlScrollType
=== 'reverse' ) {
1188 scrollLeft
= -scrollLeft
;
1189 } else if ( rtlScrollType
=== 'default' ) {
1190 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1199 * Get the root scrollable element of given element's document.
1201 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1202 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1203 * lets us use 'body' or 'documentElement' based on what is working.
1205 * https://code.google.com/p/chromium/issues/detail?id=303131
1208 * @param {HTMLElement} el Element to find root scrollable parent for
1209 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1210 * depending on browser
1212 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1213 var scrollTop
, body
;
1215 if ( OO
.ui
.scrollableElement
=== undefined ) {
1216 body
= el
.ownerDocument
.body
;
1217 scrollTop
= body
.scrollTop
;
1220 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1221 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1222 if ( Math
.round( body
.scrollTop
) === 1 ) {
1223 body
.scrollTop
= scrollTop
;
1224 OO
.ui
.scrollableElement
= 'body';
1226 OO
.ui
.scrollableElement
= 'documentElement';
1230 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1234 * Get closest scrollable container.
1236 * Traverses up until either a scrollable element or the root is reached, in which case the root
1237 * scrollable element will be returned (see #getRootScrollableElement).
1240 * @param {HTMLElement} el Element to find scrollable container for
1241 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1242 * @return {HTMLElement} Closest scrollable container
1244 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1246 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1247 // 'overflow-y' have different values, so we need to check the separate properties.
1248 props
= [ 'overflow-x', 'overflow-y' ],
1249 $parent
= $( el
).parent();
1251 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1252 props
= [ 'overflow-' + dimension
];
1255 // Special case for the document root (which doesn't really have any scrollable container, since
1256 // it is the ultimate scrollable container, but this is probably saner than null or exception)
1257 if ( $( el
).is( 'html, body' ) ) {
1258 return this.getRootScrollableElement( el
);
1261 while ( $parent
.length
) {
1262 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1263 return $parent
[ 0 ];
1267 val
= $parent
.css( props
[ i
] );
1268 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be
1269 // scrolled in that direction, but they can actually be scrolled programatically. The user can
1270 // unintentionally perform a scroll in such case even if the application doesn't scroll
1271 // programatically, e.g. when jumping to an anchor, or when using built-in find functionality.
1272 // This could cause funny issues...
1273 if ( val
=== 'auto' || val
=== 'scroll' ) {
1274 return $parent
[ 0 ];
1277 $parent
= $parent
.parent();
1279 // The element is unattached... return something mostly sane
1280 return this.getRootScrollableElement( el
);
1284 * Scroll element into view.
1287 * @param {HTMLElement} el Element to scroll into view
1288 * @param {Object} [config] Configuration options
1289 * @param {string} [config.duration='fast'] jQuery animation duration value
1290 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1291 * to scroll in both directions
1292 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1294 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1295 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1296 deferred
= $.Deferred();
1298 // Configuration initialization
1299 config
= config
|| {};
1302 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1303 $container
= $( container
);
1304 elementDimensions
= this.getDimensions( el
);
1305 containerDimensions
= this.getDimensions( container
);
1306 $window
= $( this.getWindow( el
) );
1308 // Compute the element's position relative to the container
1309 if ( $container
.is( 'html, body' ) ) {
1310 // If the scrollable container is the root, this is easy
1312 top
: elementDimensions
.rect
.top
,
1313 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1314 left
: elementDimensions
.rect
.left
,
1315 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1318 // Otherwise, we have to subtract el's coordinates from container's coordinates
1320 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1321 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1322 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1323 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1327 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1328 if ( position
.top
< 0 ) {
1329 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1330 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1331 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1334 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1335 if ( position
.left
< 0 ) {
1336 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1337 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1338 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1341 if ( !$.isEmptyObject( animations
) ) {
1342 // eslint-disable-next-line jquery/no-animate
1343 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1344 $container
.queue( function ( next
) {
1351 return deferred
.promise();
1355 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1356 * and reserve space for them, because it probably doesn't.
1358 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1359 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1360 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1361 * and then reattach (or show) them back.
1364 * @param {HTMLElement} el Element to reconsider the scrollbars on
1366 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1367 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1368 // Save scroll position
1369 scrollLeft
= el
.scrollLeft
;
1370 scrollTop
= el
.scrollTop
;
1371 // Detach all children
1372 while ( el
.firstChild
) {
1373 nodes
.push( el
.firstChild
);
1374 el
.removeChild( el
.firstChild
);
1377 // eslint-disable-next-line no-void
1378 void el
.offsetHeight
;
1379 // Reattach all children
1380 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1381 el
.appendChild( nodes
[ i
] );
1383 // Restore scroll position (no-op if scrollbars disappeared)
1384 el
.scrollLeft
= scrollLeft
;
1385 el
.scrollTop
= scrollTop
;
1391 * Toggle visibility of an element.
1393 * @param {boolean} [show] Make element visible, omit to toggle visibility
1396 * @return {OO.ui.Element} The element, for chaining
1398 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1399 show
= show
=== undefined ? !this.visible
: !!show
;
1401 if ( show
!== this.isVisible() ) {
1402 this.visible
= show
;
1403 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1404 this.emit( 'toggle', show
);
1411 * Check if element is visible.
1413 * @return {boolean} element is visible
1415 OO
.ui
.Element
.prototype.isVisible = function () {
1416 return this.visible
;
1422 * @return {Mixed} Element data
1424 OO
.ui
.Element
.prototype.getData = function () {
1431 * @param {Mixed} data Element data
1433 * @return {OO.ui.Element} The element, for chaining
1435 OO
.ui
.Element
.prototype.setData = function ( data
) {
1441 * Set the element has an 'id' attribute.
1443 * @param {string} id
1445 * @return {OO.ui.Element} The element, for chaining
1447 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1448 this.elementId
= id
;
1449 this.$element
.attr( 'id', id
);
1454 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1455 * and return its value.
1459 OO
.ui
.Element
.prototype.getElementId = function () {
1460 if ( this.elementId
=== null ) {
1461 this.setElementId( OO
.ui
.generateElementId() );
1463 return this.elementId
;
1467 * Check if element supports one or more methods.
1469 * @param {string|string[]} methods Method or list of methods to check
1470 * @return {boolean} All methods are supported
1472 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1476 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1477 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1478 if ( typeof this[ methods
[ i
] ] === 'function' ) {
1483 return methods
.length
=== support
;
1487 * Update the theme-provided classes.
1489 * @localdoc This is called in element mixins and widget classes any time state changes.
1490 * Updating is debounced, minimizing overhead of changing multiple attributes and
1491 * guaranteeing that theme updates do not occur within an element's constructor
1493 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1494 OO
.ui
.theme
.queueUpdateElementClasses( this );
1498 * Get the HTML tag name.
1500 * Override this method to base the result on instance information.
1502 * @return {string} HTML tag name
1504 OO
.ui
.Element
.prototype.getTagName = function () {
1505 return this.constructor.static.tagName
;
1509 * Check if the element is attached to the DOM
1511 * @return {boolean} The element is attached to the DOM
1513 OO
.ui
.Element
.prototype.isElementAttached = function () {
1514 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1518 * Get the DOM document.
1520 * @return {HTMLDocument} Document object
1522 OO
.ui
.Element
.prototype.getElementDocument = function () {
1523 // Don't cache this in other ways either because subclasses could can change this.$element
1524 return OO
.ui
.Element
.static.getDocument( this.$element
);
1528 * Get the DOM window.
1530 * @return {Window} Window object
1532 OO
.ui
.Element
.prototype.getElementWindow = function () {
1533 return OO
.ui
.Element
.static.getWindow( this.$element
);
1537 * Get closest scrollable container.
1539 * @return {HTMLElement} Closest scrollable container
1541 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1542 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1546 * Get group element is in.
1548 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1550 OO
.ui
.Element
.prototype.getElementGroup = function () {
1551 return this.elementGroup
;
1555 * Set group element is in.
1557 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1559 * @return {OO.ui.Element} The element, for chaining
1561 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1562 this.elementGroup
= group
;
1567 * Scroll element into view.
1569 * @param {Object} [config] Configuration options
1570 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1572 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1574 !this.isElementAttached() ||
1575 !this.isVisible() ||
1576 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1578 return $.Deferred().resolve();
1580 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1584 * Restore the pre-infusion dynamic state for this widget.
1586 * This method is called after #$element has been inserted into DOM. The parameter is the return
1587 * value of #gatherPreInfuseState.
1590 * @param {Object} state
1592 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1596 * Wraps an HTML snippet for use with configuration values which default
1597 * to strings. This bypasses the default html-escaping done to string
1603 * @param {string} [content] HTML content
1605 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1607 this.content
= content
;
1612 OO
.initClass( OO
.ui
.HtmlSnippet
);
1619 * @return {string} Unchanged HTML snippet.
1621 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1622 return this.content
;
1626 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1627 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1628 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1629 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1630 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1634 * @extends OO.ui.Element
1635 * @mixins OO.EventEmitter
1638 * @param {Object} [config] Configuration options
1640 OO
.ui
.Layout
= function OoUiLayout( config
) {
1641 // Configuration initialization
1642 config
= config
|| {};
1644 // Parent constructor
1645 OO
.ui
.Layout
.parent
.call( this, config
);
1647 // Mixin constructors
1648 OO
.EventEmitter
.call( this );
1651 this.$element
.addClass( 'oo-ui-layout' );
1656 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1657 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1662 * Reset scroll offsets
1665 * @return {OO.ui.Layout} The layout, for chaining
1667 OO
.ui
.Layout
.prototype.resetScroll = function () {
1668 this.$element
[ 0 ].scrollTop
= 0;
1669 // TODO: Reset scrollLeft in an RTL-aware manner, see OO.ui.Element.static.getScrollLeft.
1675 * Widgets are compositions of one or more OOUI elements that users can both view
1676 * and interact with. All widgets can be configured and modified via a standard API,
1677 * and their state can change dynamically according to a model.
1681 * @extends OO.ui.Element
1682 * @mixins OO.EventEmitter
1685 * @param {Object} [config] Configuration options
1686 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1687 * appearance reflects this state.
1689 OO
.ui
.Widget
= function OoUiWidget( config
) {
1690 // Initialize config
1691 config
= $.extend( { disabled
: false }, config
);
1693 // Parent constructor
1694 OO
.ui
.Widget
.parent
.call( this, config
);
1696 // Mixin constructors
1697 OO
.EventEmitter
.call( this );
1700 this.disabled
= null;
1701 this.wasDisabled
= null;
1704 this.$element
.addClass( 'oo-ui-widget' );
1705 this.setDisabled( !!config
.disabled
);
1710 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1711 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1718 * A 'disable' event is emitted when the disabled state of the widget changes
1719 * (i.e. on disable **and** enable).
1721 * @param {boolean} disabled Widget is disabled
1727 * A 'toggle' event is emitted when the visibility of the widget changes.
1729 * @param {boolean} visible Widget is visible
1735 * Check if the widget is disabled.
1737 * @return {boolean} Widget is disabled
1739 OO
.ui
.Widget
.prototype.isDisabled = function () {
1740 return this.disabled
;
1744 * Set the 'disabled' state of the widget.
1746 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1748 * @param {boolean} disabled Disable widget
1750 * @return {OO.ui.Widget} The widget, for chaining
1752 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1755 this.disabled
= !!disabled
;
1756 isDisabled
= this.isDisabled();
1757 if ( isDisabled
!== this.wasDisabled
) {
1758 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1759 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1760 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1761 this.emit( 'disable', isDisabled
);
1762 this.updateThemeClasses();
1764 this.wasDisabled
= isDisabled
;
1770 * Update the disabled state, in case of changes in parent widget.
1773 * @return {OO.ui.Widget} The widget, for chaining
1775 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1776 this.setDisabled( this.disabled
);
1781 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1784 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1787 * @return {string|null} The ID of the labelable element
1789 OO
.ui
.Widget
.prototype.getInputId = function () {
1794 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1795 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1796 * override this method to provide intuitive, accessible behavior.
1798 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1799 * Individual widgets may override it too.
1801 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1804 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1815 OO
.ui
.Theme
= function OoUiTheme() {
1816 this.elementClassesQueue
= [];
1817 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1822 OO
.initClass( OO
.ui
.Theme
);
1827 * Get a list of classes to be applied to a widget.
1829 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1830 * otherwise state transitions will not work properly.
1832 * @param {OO.ui.Element} element Element for which to get classes
1833 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1835 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1836 return { on
: [], off
: [] };
1840 * Update CSS classes provided by the theme.
1842 * For elements with theme logic hooks, this should be called any time there's a state change.
1844 * @param {OO.ui.Element} element Element for which to update classes
1846 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1847 var $elements
= $( [] ),
1848 classes
= this.getElementClasses( element
);
1850 if ( element
.$icon
) {
1851 $elements
= $elements
.add( element
.$icon
);
1853 if ( element
.$indicator
) {
1854 $elements
= $elements
.add( element
.$indicator
);
1858 .removeClass( classes
.off
)
1859 .addClass( classes
.on
);
1865 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1867 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1868 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1871 this.elementClassesQueue
= [];
1875 * Queue #updateElementClasses to be called for this element.
1877 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1878 * to make them synchronous.
1880 * @param {OO.ui.Element} element Element for which to update classes
1882 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1883 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1884 // the most common case (this method is often called repeatedly for the same element).
1885 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1888 this.elementClassesQueue
.push( element
);
1889 this.debouncedUpdateQueuedElementClasses();
1893 * Get the transition duration in milliseconds for dialogs opening/closing
1895 * The dialog should be fully rendered this many milliseconds after the
1896 * ready process has executed.
1898 * @return {number} Transition duration in milliseconds
1900 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1905 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1906 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1907 * order in which users will navigate through the focusable elements via the “tab” key.
1910 * // TabIndexedElement is mixed into the ButtonWidget class
1911 * // to provide a tabIndex property.
1912 * var button1 = new OO.ui.ButtonWidget( {
1916 * button2 = new OO.ui.ButtonWidget( {
1920 * button3 = new OO.ui.ButtonWidget( {
1924 * button4 = new OO.ui.ButtonWidget( {
1928 * $( document.body ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1934 * @param {Object} [config] Configuration options
1935 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1936 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1937 * functionality will be applied to it instead.
1938 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1939 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1940 * to remove the element from the tab-navigation flow.
1942 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1943 // Configuration initialization
1944 config
= $.extend( { tabIndex
: 0 }, config
);
1947 this.$tabIndexed
= null;
1948 this.tabIndex
= null;
1951 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1954 this.setTabIndex( config
.tabIndex
);
1955 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1960 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1965 * Set the element that should use the tabindex functionality.
1967 * This method is used to retarget a tabindex mixin so that its functionality applies
1968 * to the specified element. If an element is currently using the functionality, the mixin’s
1969 * effect on that element is removed before the new element is set up.
1971 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1973 * @return {OO.ui.Element} The element, for chaining
1975 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1976 var tabIndex
= this.tabIndex
;
1977 // Remove attributes from old $tabIndexed
1978 this.setTabIndex( null );
1979 // Force update of new $tabIndexed
1980 this.$tabIndexed
= $tabIndexed
;
1981 this.tabIndex
= tabIndex
;
1982 return this.updateTabIndex();
1986 * Set the value of the tabindex.
1988 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
1990 * @return {OO.ui.Element} The element, for chaining
1992 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1993 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
1995 if ( this.tabIndex
!== tabIndex
) {
1996 this.tabIndex
= tabIndex
;
1997 this.updateTabIndex();
2004 * Update the `tabindex` attribute, in case of changes to tab index or
2009 * @return {OO.ui.Element} The element, for chaining
2011 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
2012 if ( this.$tabIndexed
) {
2013 if ( this.tabIndex
!== null ) {
2014 // Do not index over disabled elements
2015 this.$tabIndexed
.attr( {
2016 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
2017 // Support: ChromeVox and NVDA
2018 // These do not seem to inherit aria-disabled from parent elements
2019 'aria-disabled': this.isDisabled().toString()
2022 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
2029 * Handle disable events.
2032 * @param {boolean} disabled Element is disabled
2034 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
2035 this.updateTabIndex();
2039 * Get the value of the tabindex.
2041 * @return {number|null} Tabindex value
2043 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2044 return this.tabIndex
;
2048 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2050 * If the element already has an ID then that is returned, otherwise unique ID is
2051 * generated, set on the element, and returned.
2053 * @return {string|null} The ID of the focusable element
2055 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2058 if ( !this.$tabIndexed
) {
2061 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2065 id
= this.$tabIndexed
.attr( 'id' );
2066 if ( id
=== undefined ) {
2067 id
= OO
.ui
.generateElementId();
2068 this.$tabIndexed
.attr( 'id', id
);
2075 * Whether the node is 'labelable' according to the HTML spec
2076 * (i.e., whether it can be interacted with through a `<label for="…">`).
2077 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2080 * @param {jQuery} $node
2083 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2085 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2086 tagName
= $node
.prop( 'tagName' ).toLowerCase();
2088 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2091 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2098 * Focus this element.
2101 * @return {OO.ui.Element} The element, for chaining
2103 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2104 if ( !this.isDisabled() ) {
2105 this.$tabIndexed
.focus();
2111 * Blur this element.
2114 * @return {OO.ui.Element} The element, for chaining
2116 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2117 this.$tabIndexed
.blur();
2122 * @inheritdoc OO.ui.Widget
2124 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2129 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2130 * interface element that can be configured with access keys for keyboard interaction.
2131 * See the [OOUI documentation on MediaWiki] [1] for examples.
2133 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2139 * @param {Object} [config] Configuration options
2140 * @cfg {jQuery} [$button] The button element created by the class.
2141 * If this configuration is omitted, the button element will use a generated `<a>`.
2142 * @cfg {boolean} [framed=true] Render the button with a frame
2144 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2145 // Configuration initialization
2146 config
= config
|| {};
2149 this.$button
= null;
2151 this.active
= config
.active
!== undefined && config
.active
;
2152 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
2153 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2154 this.onDocumentKeyUpHandler
= this.onDocumentKeyUp
.bind( this );
2155 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2156 this.onClickHandler
= this.onClick
.bind( this );
2157 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2160 this.$element
.addClass( 'oo-ui-buttonElement' );
2161 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2162 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2167 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2169 /* Static Properties */
2172 * Cancel mouse down events.
2174 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
2175 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
2176 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
2181 * @property {boolean}
2183 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2188 * A 'click' event is emitted when the button element is clicked.
2196 * Set the button element.
2198 * This method is used to retarget a button mixin so that its functionality applies to
2199 * the specified button element instead of the one created by the class. If a button element
2200 * is already set, the method will remove the mixin’s effect on that element.
2202 * @param {jQuery} $button Element to use as button
2204 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2205 if ( this.$button
) {
2207 .removeClass( 'oo-ui-buttonElement-button' )
2208 .removeAttr( 'role accesskey' )
2210 mousedown
: this.onMouseDownHandler
,
2211 keydown
: this.onKeyDownHandler
,
2212 click
: this.onClickHandler
,
2213 keypress
: this.onKeyPressHandler
2217 this.$button
= $button
2218 .addClass( 'oo-ui-buttonElement-button' )
2220 mousedown
: this.onMouseDownHandler
,
2221 keydown
: this.onKeyDownHandler
,
2222 click
: this.onClickHandler
,
2223 keypress
: this.onKeyPressHandler
2226 // Add `role="button"` on `<a>` elements, where it's needed
2227 // `toUpperCase()` is added for XHTML documents
2228 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2229 this.$button
.attr( 'role', 'button' );
2234 * Handles mouse down events.
2237 * @param {jQuery.Event} e Mouse down event
2238 * @return {undefined/boolean} False to prevent default if event is handled
2240 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2241 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2244 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2245 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2246 // reliably remove the pressed class
2247 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2248 // Prevent change of focus unless specifically configured otherwise
2249 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2255 * Handles document mouse up events.
2258 * @param {MouseEvent} e Mouse up event
2260 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentMouseUp = function ( e
) {
2261 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2264 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2265 // Stop listening for mouseup, since we only needed this once
2266 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2269 // Deprecated alias since 0.28.3
2270 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function () {
2271 OO
.ui
.warnDeprecation( 'onMouseUp is deprecated, use onDocumentMouseUp instead' );
2272 this.onDocumentMouseUp
.apply( this, arguments
);
2276 * Handles mouse click events.
2279 * @param {jQuery.Event} e Mouse click event
2281 * @return {undefined/boolean} False to prevent default if event is handled
2283 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2284 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2285 if ( this.emit( 'click' ) ) {
2292 * Handles key down events.
2295 * @param {jQuery.Event} e Key down event
2297 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2298 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2301 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2302 // Run the keyup handler no matter where the key is when the button is let go, so we can
2303 // reliably remove the pressed class
2304 this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2308 * Handles document key up events.
2311 * @param {KeyboardEvent} e Key up event
2313 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentKeyUp = function ( e
) {
2314 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2317 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2318 // Stop listening for keyup, since we only needed this once
2319 this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2322 // Deprecated alias since 0.28.3
2323 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function () {
2324 OO
.ui
.warnDeprecation( 'onKeyUp is deprecated, use onDocumentKeyUp instead' );
2325 this.onDocumentKeyUp
.apply( this, arguments
);
2329 * Handles key press events.
2332 * @param {jQuery.Event} e Key press event
2334 * @return {undefined/boolean} False to prevent default if event is handled
2336 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2337 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2338 if ( this.emit( 'click' ) ) {
2345 * Check if button has a frame.
2347 * @return {boolean} Button is framed
2349 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2354 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2356 * @param {boolean} [framed] Make button framed, omit to toggle
2358 * @return {OO.ui.Element} The element, for chaining
2360 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2361 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2362 if ( framed
!== this.framed
) {
2363 this.framed
= framed
;
2365 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2366 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2367 this.updateThemeClasses();
2374 * Set the button's active state.
2376 * The active state can be set on:
2378 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2379 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2380 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2383 * @param {boolean} value Make button active
2385 * @return {OO.ui.Element} The element, for chaining
2387 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2388 this.active
= !!value
;
2389 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2390 this.updateThemeClasses();
2395 * Check if the button is active
2398 * @return {boolean} The button is active
2400 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2405 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2406 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2407 * items from the group is done through the interface the class provides.
2408 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2410 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2413 * @mixins OO.EmitterList
2417 * @param {Object} [config] Configuration options
2418 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2419 * is omitted, the group element will use a generated `<div>`.
2421 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2422 // Configuration initialization
2423 config
= config
|| {};
2425 // Mixin constructors
2426 OO
.EmitterList
.call( this, config
);
2432 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2437 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2444 * A change event is emitted when the set of selected items changes.
2446 * @param {OO.ui.Element[]} items Items currently in the group
2452 * Set the group element.
2454 * If an element is already set, items will be moved to the new element.
2456 * @param {jQuery} $group Element to use as group
2458 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2461 this.$group
= $group
;
2462 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2463 this.$group
.append( this.items
[ i
].$element
);
2468 * Find an item by its data.
2470 * Only the first item with matching data will be returned. To return all matching items,
2471 * use the #findItemsFromData method.
2473 * @param {Object} data Item data to search for
2474 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2476 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2478 hash
= OO
.getHash( data
);
2480 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2481 item
= this.items
[ i
];
2482 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2491 * Find items by their data.
2493 * All items with matching data will be returned. To return only the first match, use the #findItemFromData method instead.
2495 * @param {Object} data Item data to search for
2496 * @return {OO.ui.Element[]} Items with equivalent data
2498 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2500 hash
= OO
.getHash( data
),
2503 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2504 item
= this.items
[ i
];
2505 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2514 * Add items to the group.
2516 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2517 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2519 * @param {OO.ui.Element[]} items An array of items to add to the group
2520 * @param {number} [index] Index of the insertion point
2522 * @return {OO.ui.Element} The element, for chaining
2524 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2526 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2528 this.emit( 'change', this.getItems() );
2535 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2536 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2537 this.insertItemElements( items
, newIndex
);
2540 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2548 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2549 item
.setElementGroup( this );
2550 this.insertItemElements( item
, index
);
2553 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2559 * Insert elements into the group
2562 * @param {OO.ui.Element} itemWidget Item to insert
2563 * @param {number} index Insertion index
2565 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2566 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2567 this.$group
.append( itemWidget
.$element
);
2568 } else if ( index
=== 0 ) {
2569 this.$group
.prepend( itemWidget
.$element
);
2571 this.items
[ index
].$element
.before( itemWidget
.$element
);
2576 * Remove the specified items from a group.
2578 * Removed items are detached (not removed) from the DOM so that they may be reused.
2579 * To remove all items from a group, you may wish to use the #clearItems method instead.
2581 * @param {OO.ui.Element[]} items An array of items to remove
2583 * @return {OO.ui.Element} The element, for chaining
2585 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2586 var i
, len
, item
, index
;
2588 // Remove specific items elements
2589 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2591 index
= this.items
.indexOf( item
);
2592 if ( index
!== -1 ) {
2593 item
.setElementGroup( null );
2594 item
.$element
.detach();
2599 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2601 this.emit( 'change', this.getItems() );
2606 * Clear all items from the group.
2608 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2609 * To remove only a subset of items from a group, use the #removeItems method.
2612 * @return {OO.ui.Element} The element, for chaining
2614 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2617 // Remove all item elements
2618 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2619 this.items
[ i
].setElementGroup( null );
2620 this.items
[ i
].$element
.detach();
2624 OO
.EmitterList
.prototype.clearItems
.call( this );
2626 this.emit( 'change', this.getItems() );
2631 * LabelElement is often mixed into other classes to generate a label, which
2632 * helps identify the function of an interface element.
2633 * See the [OOUI documentation on MediaWiki] [1] for more information.
2635 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2641 * @param {Object} [config] Configuration options
2642 * @cfg {jQuery} [$label] The label element created by the class. If this
2643 * configuration is omitted, the label element will use a generated `<span>`.
2644 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2645 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2646 * in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2647 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2648 * @cfg {boolean} [invisibleLabel] Whether the label should be visually hidden (but still accessible
2649 * to screen-readers).
2651 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2652 // Configuration initialization
2653 config
= config
|| {};
2658 this.invisibleLabel
= null;
2661 this.setLabel( config
.label
|| this.constructor.static.label
);
2662 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2663 this.setInvisibleLabel( config
.invisibleLabel
);
2668 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2673 * @event labelChange
2674 * @param {string} value
2677 /* Static Properties */
2680 * The label text. The label can be specified as a plaintext string, a function that will
2681 * produce a string in the future, or `null` for no label. The static value will
2682 * be overridden if a label is specified with the #label config option.
2686 * @property {string|Function|null}
2688 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2690 /* Static methods */
2693 * Highlight the first occurrence of the query in the given text
2695 * @param {string} text Text
2696 * @param {string} query Query to find
2697 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2698 * @return {jQuery} Text with the first match of the query
2699 * sub-string wrapped in highlighted span
2701 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2704 $result
= $( '<span>' );
2708 qLen
= query
.length
;
2709 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2710 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2715 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2718 if ( !query
.length
|| offset
=== -1 ) {
2719 $result
.text( text
);
2722 document
.createTextNode( text
.slice( 0, offset
) ),
2724 .addClass( 'oo-ui-labelElement-label-highlight' )
2725 .text( text
.slice( offset
, offset
+ query
.length
) ),
2726 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2729 return $result
.contents();
2735 * Set the label element.
2737 * If an element is already set, it will be cleaned up before setting up the new element.
2739 * @param {jQuery} $label Element to use as label
2741 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2742 if ( this.$label
) {
2743 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2746 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2747 this.setLabelContent( this.label
);
2753 * An empty string will result in the label being hidden. A string containing only whitespace will
2754 * be converted to a single ` `.
2756 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2757 * text; or null for no label
2759 * @return {OO.ui.Element} The element, for chaining
2761 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2762 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2763 label
= ( ( typeof label
=== 'string' || label
instanceof $ ) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2765 if ( this.label
!== label
) {
2766 if ( this.$label
) {
2767 this.setLabelContent( label
);
2770 this.emit( 'labelChange' );
2773 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2779 * Set whether the label should be visually hidden (but still accessible to screen-readers).
2781 * @param {boolean} invisibleLabel
2783 * @return {OO.ui.Element} The element, for chaining
2785 OO
.ui
.mixin
.LabelElement
.prototype.setInvisibleLabel = function ( invisibleLabel
) {
2786 invisibleLabel
= !!invisibleLabel
;
2788 if ( this.invisibleLabel
!== invisibleLabel
) {
2789 this.invisibleLabel
= invisibleLabel
;
2790 this.emit( 'labelChange' );
2793 this.$label
.toggleClass( 'oo-ui-labelElement-invisible', this.invisibleLabel
);
2794 // Pretend that there is no label, a lot of CSS has been written with this assumption
2795 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2801 * Set the label as plain text with a highlighted query
2803 * @param {string} text Text label to set
2804 * @param {string} query Substring of text to highlight
2805 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2807 * @return {OO.ui.Element} The element, for chaining
2809 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
2810 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
2816 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2817 * text; or null for no label
2819 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2824 * Set the content of the label.
2826 * Do not call this method until after the label element has been set by #setLabelElement.
2829 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2830 * text; or null for no label
2832 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2833 if ( typeof label
=== 'string' ) {
2834 if ( label
.match( /^\s*$/ ) ) {
2835 // Convert whitespace only string to a single non-breaking space
2836 this.$label
.html( ' ' );
2838 this.$label
.text( label
);
2840 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2841 this.$label
.html( label
.toString() );
2842 } else if ( label
instanceof $ ) {
2843 this.$label
.empty().append( label
);
2845 this.$label
.empty();
2850 * IconElement is often mixed into other classes to generate an icon.
2851 * Icons are graphics, about the size of normal text. They are used to aid the user
2852 * in locating a control or to convey information in a space-efficient way. See the
2853 * [OOUI documentation on MediaWiki] [1] for a list of icons
2854 * included in the library.
2856 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2862 * @param {Object} [config] Configuration options
2863 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2864 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2865 * the icon element be set to an existing icon instead of the one generated by this class, set a
2866 * value using a jQuery selection. For example:
2868 * // Use a <div> tag instead of a <span>
2869 * $icon: $( '<div>' )
2870 * // Use an existing icon element instead of the one generated by the class
2871 * $icon: this.$element
2872 * // Use an icon element from a child widget
2873 * $icon: this.childwidget.$element
2874 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2875 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2876 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2877 * by the user's language.
2879 * Example of an i18n map:
2881 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2882 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2883 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2884 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2885 * text. The icon title is displayed when users move the mouse over the icon.
2887 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2888 // Configuration initialization
2889 config
= config
|| {};
2894 this.iconTitle
= null;
2896 // `iconTitle`s are deprecated since 0.30.0
2897 if ( config
.iconTitle
!== undefined ) {
2898 OO
.ui
.warnDeprecation( 'IconElement: Widgets with iconTitle set are deprecated, use title instead. See T76638 for details.' );
2902 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2903 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2904 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2909 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2911 /* Static Properties */
2914 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2915 * for i18n purposes and contains a `default` icon name and additional names keyed by
2916 * language code. The `default` name is used when no icon is keyed by the user's language.
2918 * Example of an i18n map:
2920 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2922 * Note: the static property will be overridden if the #icon configuration is used.
2926 * @property {Object|string}
2928 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2931 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2932 * function that returns title text, or `null` for no title.
2934 * The static property will be overridden if the #iconTitle configuration is used.
2938 * @property {string|Function|null}
2940 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2945 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2946 * applies to the specified icon element instead of the one created by the class. If an icon
2947 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2948 * and mixin methods will no longer affect the element.
2950 * @param {jQuery} $icon Element to use as icon
2952 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2955 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2956 .removeAttr( 'title' );
2960 .addClass( 'oo-ui-iconElement-icon' )
2961 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
)
2962 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2963 if ( this.iconTitle
!== null ) {
2964 this.$icon
.attr( 'title', this.iconTitle
);
2967 this.updateThemeClasses();
2971 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2972 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2975 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2976 * by language code, or `null` to remove the icon.
2978 * @return {OO.ui.Element} The element, for chaining
2980 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2981 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2982 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2984 if ( this.icon
!== icon
) {
2986 if ( this.icon
!== null ) {
2987 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2989 if ( icon
!== null ) {
2990 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2996 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2998 this.$icon
.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
);
3000 this.updateThemeClasses();
3006 * Set the icon title. Use `null` to remove the title.
3008 * @param {string|Function|null} iconTitle A text string used as the icon title,
3009 * a function that returns title text, or `null` for no title.
3011 * @return {OO.ui.Element} The element, for chaining
3014 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
3016 ( typeof iconTitle
=== 'function' || ( typeof iconTitle
=== 'string' && iconTitle
.length
) ) ?
3017 OO
.ui
.resolveMsg( iconTitle
) : null;
3019 if ( this.iconTitle
!== iconTitle
) {
3020 this.iconTitle
= iconTitle
;
3022 if ( this.iconTitle
!== null ) {
3023 this.$icon
.attr( 'title', iconTitle
);
3025 this.$icon
.removeAttr( 'title' );
3030 // `setIconTitle is deprecated since 0.30.0
3031 if ( iconTitle
!== null ) {
3032 // Avoid a warning when this is called from the constructor with no iconTitle set
3033 OO
.ui
.warnDeprecation( 'IconElement: setIconTitle is deprecated, use setTitle of TitledElement instead. See T76638 for details.' );
3040 * Get the symbolic name of the icon.
3042 * @return {string} Icon name
3044 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
3049 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
3051 * @return {string} Icon title text
3053 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
3054 return this.iconTitle
;
3058 * IndicatorElement is often mixed into other classes to generate an indicator.
3059 * Indicators are small graphics that are generally used in two ways:
3061 * - To draw attention to the status of an item. For example, an indicator might be
3062 * used to show that an item in a list has errors that need to be resolved.
3063 * - To clarify the function of a control that acts in an exceptional way (a button
3064 * that opens a menu instead of performing an action directly, for example).
3066 * For a list of indicators included in the library, please see the
3067 * [OOUI documentation on MediaWiki] [1].
3069 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3075 * @param {Object} [config] Configuration options
3076 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
3077 * configuration is omitted, the indicator element will use a generated `<span>`.
3078 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3079 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
3081 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3082 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
3083 * or a function that returns title text. The indicator title is displayed when users move
3084 * the mouse over the indicator.
3086 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
3087 // Configuration initialization
3088 config
= config
|| {};
3091 this.$indicator
= null;
3092 this.indicator
= null;
3093 this.indicatorTitle
= null;
3095 // `indicatorTitle`s are deprecated since 0.30.0
3096 if ( config
.indicatorTitle
!== undefined ) {
3097 OO
.ui
.warnDeprecation( 'IndicatorElement: Widgets with indicatorTitle set are deprecated, use title instead. See T76638 for details.' );
3101 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
3102 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
3103 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
3108 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
3110 /* Static Properties */
3113 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3114 * The static property will be overridden if the #indicator configuration is used.
3118 * @property {string|null}
3120 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
3123 * A text string used as the indicator title, a function that returns title text, or `null`
3124 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
3128 * @property {string|Function|null}
3130 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
3135 * Set the indicator element.
3137 * If an element is already set, it will be cleaned up before setting up the new element.
3139 * @param {jQuery} $indicator Element to use as indicator
3141 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
3142 if ( this.$indicator
) {
3144 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
3145 .removeAttr( 'title' );
3148 this.$indicator
= $indicator
3149 .addClass( 'oo-ui-indicatorElement-indicator' )
3150 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
)
3151 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
3152 if ( this.indicatorTitle
!== null ) {
3153 this.$indicator
.attr( 'title', this.indicatorTitle
);
3156 this.updateThemeClasses();
3160 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null` to remove the indicator.
3162 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
3164 * @return {OO.ui.Element} The element, for chaining
3166 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
3167 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
3169 if ( this.indicator
!== indicator
) {
3170 if ( this.$indicator
) {
3171 if ( this.indicator
!== null ) {
3172 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
3174 if ( indicator
!== null ) {
3175 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
3178 this.indicator
= indicator
;
3181 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
3182 if ( this.$indicator
) {
3183 this.$indicator
.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
);
3185 this.updateThemeClasses();
3191 * Set the indicator title.
3193 * The title is displayed when a user moves the mouse over the indicator.
3195 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
3196 * `null` for no indicator title
3198 * @return {OO.ui.Element} The element, for chaining
3201 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
3203 ( typeof indicatorTitle
=== 'function' || ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ) ?
3204 OO
.ui
.resolveMsg( indicatorTitle
) : null;
3206 if ( this.indicatorTitle
!== indicatorTitle
) {
3207 this.indicatorTitle
= indicatorTitle
;
3208 if ( this.$indicator
) {
3209 if ( this.indicatorTitle
!== null ) {
3210 this.$indicator
.attr( 'title', indicatorTitle
);
3212 this.$indicator
.removeAttr( 'title' );
3217 // `setIndicatorTitle is deprecated since 0.30.0
3218 if ( indicatorTitle
!== null ) {
3219 // Avoid a warning when this is called from the constructor with no indicatorTitle set
3220 OO
.ui
.warnDeprecation( 'IndicatorElement: setIndicatorTitle is deprecated, use setTitle of TitledElement instead. See T76638 for details.' );
3227 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3229 * @return {string} Symbolic name of indicator
3231 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
3232 return this.indicator
;
3236 * Get the indicator title.
3238 * The title is displayed when a user moves the mouse over the indicator.
3240 * @return {string} Indicator title text
3242 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
3243 return this.indicatorTitle
;
3247 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3248 * additional functionality to an element created by another class. The class provides
3249 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3250 * which are used to customize the look and feel of a widget to better describe its
3251 * importance and functionality.
3253 * The library currently contains the following styling flags for general use:
3255 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
3256 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
3258 * The flags affect the appearance of the buttons:
3261 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3262 * var button1 = new OO.ui.ButtonWidget( {
3263 * label: 'Progressive',
3264 * flags: 'progressive'
3266 * button2 = new OO.ui.ButtonWidget( {
3267 * label: 'Destructive',
3268 * flags: 'destructive'
3270 * $( document.body ).append( button1.$element, button2.$element );
3272 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
3273 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3275 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3281 * @param {Object} [config] Configuration options
3282 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary') to apply.
3283 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3284 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3285 * @cfg {jQuery} [$flagged] The flagged element. By default,
3286 * the flagged functionality is applied to the element created by the class ($element).
3287 * If a different element is specified, the flagged functionality will be applied to it instead.
3289 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3290 // Configuration initialization
3291 config
= config
|| {};
3295 this.$flagged
= null;
3298 this.setFlags( config
.flags
);
3299 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3306 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3307 * parameter contains the name of each modified flag and indicates whether it was
3310 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3311 * that the flag was added, `false` that the flag was removed.
3317 * Set the flagged element.
3319 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3320 * If an element is already set, the method will remove the mixin’s effect on that element.
3322 * @param {jQuery} $flagged Element that should be flagged
3324 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3325 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3326 return 'oo-ui-flaggedElement-' + flag
;
3329 if ( this.$flagged
) {
3330 this.$flagged
.removeClass( classNames
);
3333 this.$flagged
= $flagged
.addClass( classNames
);
3337 * Check if the specified flag is set.
3339 * @param {string} flag Name of flag
3340 * @return {boolean} The flag is set
3342 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3343 // This may be called before the constructor, thus before this.flags is set
3344 return this.flags
&& ( flag
in this.flags
);
3348 * Get the names of all flags set.
3350 * @return {string[]} Flag names
3352 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3353 // This may be called before the constructor, thus before this.flags is set
3354 return Object
.keys( this.flags
|| {} );
3361 * @return {OO.ui.Element} The element, for chaining
3364 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3365 var flag
, className
,
3368 classPrefix
= 'oo-ui-flaggedElement-';
3370 for ( flag
in this.flags
) {
3371 className
= classPrefix
+ flag
;
3372 changes
[ flag
] = false;
3373 delete this.flags
[ flag
];
3374 remove
.push( className
);
3377 if ( this.$flagged
) {
3378 this.$flagged
.removeClass( remove
);
3381 this.updateThemeClasses();
3382 this.emit( 'flag', changes
);
3388 * Add one or more flags.
3390 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3391 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3392 * be added (`true`) or removed (`false`).
3394 * @return {OO.ui.Element} The element, for chaining
3397 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3398 var i
, len
, flag
, className
,
3402 classPrefix
= 'oo-ui-flaggedElement-';
3404 if ( typeof flags
=== 'string' ) {
3405 className
= classPrefix
+ flags
;
3407 if ( !this.flags
[ flags
] ) {
3408 this.flags
[ flags
] = true;
3409 add
.push( className
);
3411 } else if ( Array
.isArray( flags
) ) {
3412 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3414 className
= classPrefix
+ flag
;
3416 if ( !this.flags
[ flag
] ) {
3417 changes
[ flag
] = true;
3418 this.flags
[ flag
] = true;
3419 add
.push( className
);
3422 } else if ( OO
.isPlainObject( flags
) ) {
3423 for ( flag
in flags
) {
3424 className
= classPrefix
+ flag
;
3425 if ( flags
[ flag
] ) {
3427 if ( !this.flags
[ flag
] ) {
3428 changes
[ flag
] = true;
3429 this.flags
[ flag
] = true;
3430 add
.push( className
);
3434 if ( this.flags
[ flag
] ) {
3435 changes
[ flag
] = false;
3436 delete this.flags
[ flag
];
3437 remove
.push( className
);
3443 if ( this.$flagged
) {
3446 .removeClass( remove
);
3449 this.updateThemeClasses();
3450 this.emit( 'flag', changes
);
3456 * TitledElement is mixed into other classes to provide a `title` attribute.
3457 * Titles are rendered by the browser and are made visible when the user moves
3458 * the mouse over the element. Titles are not visible on touch devices.
3461 * // TitledElement provides a `title` attribute to the
3462 * // ButtonWidget class.
3463 * var button = new OO.ui.ButtonWidget( {
3464 * label: 'Button with Title',
3465 * title: 'I am a button'
3467 * $( document.body ).append( button.$element );
3473 * @param {Object} [config] Configuration options
3474 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3475 * If this config is omitted, the title functionality is applied to $element, the
3476 * element created by the class.
3477 * @cfg {string|Function} [title] The title text or a function that returns text. If
3478 * this config is omitted, the value of the {@link #static-title static title} property is used.
3480 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3481 // Configuration initialization
3482 config
= config
|| {};
3485 this.$titled
= null;
3489 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3490 this.setTitledElement( config
.$titled
|| this.$element
);
3495 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3497 /* Static Properties */
3500 * The title text, a function that returns text, or `null` for no title. The value of the static property
3501 * is overridden if the #title config option is used.
3505 * @property {string|Function|null}
3507 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3512 * Set the titled element.
3514 * This method is used to retarget a TitledElement mixin so that its functionality applies to the specified element.
3515 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3517 * @param {jQuery} $titled Element that should use the 'titled' functionality
3519 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3520 if ( this.$titled
) {
3521 this.$titled
.removeAttr( 'title' );
3524 this.$titled
= $titled
;
3533 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3535 * @return {OO.ui.Element} The element, for chaining
3537 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3538 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3539 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3541 if ( this.title
!== title
) {
3550 * Update the title attribute, in case of changes to title or accessKey.
3554 * @return {OO.ui.Element} The element, for chaining
3556 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3557 var title
= this.getTitle();
3558 if ( this.$titled
) {
3559 if ( title
!== null ) {
3560 // Only if this is an AccessKeyedElement
3561 if ( this.formatTitleWithAccessKey
) {
3562 title
= this.formatTitleWithAccessKey( title
);
3564 this.$titled
.attr( 'title', title
);
3566 this.$titled
.removeAttr( 'title' );
3575 * @return {string} Title string
3577 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3582 * AccessKeyedElement is mixed into other classes to provide an `accesskey` HTML attribute.
3583 * Accesskeys allow an user to go to a specific element by using
3584 * a shortcut combination of a browser specific keys + the key
3588 * // AccessKeyedElement provides an `accesskey` attribute to the
3589 * // ButtonWidget class.
3590 * var button = new OO.ui.ButtonWidget( {
3591 * label: 'Button with Accesskey',
3594 * $( document.body ).append( button.$element );
3600 * @param {Object} [config] Configuration options
3601 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3602 * If this config is omitted, the accesskey functionality is applied to $element, the
3603 * element created by the class.
3604 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3605 * this config is omitted, no accesskey will be added.
3607 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3608 // Configuration initialization
3609 config
= config
|| {};
3612 this.$accessKeyed
= null;
3613 this.accessKey
= null;
3616 this.setAccessKey( config
.accessKey
|| null );
3617 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3619 // If this is also a TitledElement and it initialized before we did, we may have
3620 // to update the title with the access key
3621 if ( this.updateTitle
) {
3628 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3630 /* Static Properties */
3633 * The access key, a function that returns a key, or `null` for no accesskey.
3637 * @property {string|Function|null}
3639 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3644 * Set the accesskeyed element.
3646 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3647 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3649 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyed' functionality
3651 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3652 if ( this.$accessKeyed
) {
3653 this.$accessKeyed
.removeAttr( 'accesskey' );
3656 this.$accessKeyed
= $accessKeyed
;
3657 if ( this.accessKey
) {
3658 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3665 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3667 * @return {OO.ui.Element} The element, for chaining
3669 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3670 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3672 if ( this.accessKey
!== accessKey
) {
3673 if ( this.$accessKeyed
) {
3674 if ( accessKey
!== null ) {
3675 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3677 this.$accessKeyed
.removeAttr( 'accesskey' );
3680 this.accessKey
= accessKey
;
3682 // Only if this is a TitledElement
3683 if ( this.updateTitle
) {
3694 * @return {string} accessKey string
3696 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3697 return this.accessKey
;
3701 * Add information about the access key to the element's tooltip label.
3702 * (This is only public for hacky usage in FieldLayout.)
3704 * @param {string} title Tooltip label for `title` attribute
3707 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3710 if ( !this.$accessKeyed
) {
3711 // Not initialized yet; the constructor will call updateTitle() which will rerun this function
3714 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the single key
3715 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3716 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3718 accessKey
= this.getAccessKey();
3721 title
+= ' [' + accessKey
+ ']';
3727 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3728 * feels, and functionality can be customized via the class’s configuration options
3729 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3732 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3735 * // A button widget.
3736 * var button = new OO.ui.ButtonWidget( {
3737 * label: 'Button with Icon',
3741 * $( document.body ).append( button.$element );
3743 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3746 * @extends OO.ui.Widget
3747 * @mixins OO.ui.mixin.ButtonElement
3748 * @mixins OO.ui.mixin.IconElement
3749 * @mixins OO.ui.mixin.IndicatorElement
3750 * @mixins OO.ui.mixin.LabelElement
3751 * @mixins OO.ui.mixin.TitledElement
3752 * @mixins OO.ui.mixin.FlaggedElement
3753 * @mixins OO.ui.mixin.TabIndexedElement
3754 * @mixins OO.ui.mixin.AccessKeyedElement
3757 * @param {Object} [config] Configuration options
3758 * @cfg {boolean} [active=false] Whether button should be shown as active
3759 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3760 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3761 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3763 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3764 // Configuration initialization
3765 config
= config
|| {};
3767 // Parent constructor
3768 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3770 // Mixin constructors
3771 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3772 OO
.ui
.mixin
.IconElement
.call( this, config
);
3773 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3774 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3775 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3776 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3777 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3778 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3783 this.noFollow
= false;
3786 this.connect( this, { disable
: 'onDisable' } );
3789 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3791 .addClass( 'oo-ui-buttonWidget' )
3792 .append( this.$button
);
3793 this.setActive( config
.active
);
3794 this.setHref( config
.href
);
3795 this.setTarget( config
.target
);
3796 this.setNoFollow( config
.noFollow
);
3801 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3802 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3803 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3804 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3805 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3806 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3807 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3808 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3809 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3811 /* Static Properties */
3817 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3823 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3828 * Get hyperlink location.
3830 * @return {string} Hyperlink location
3832 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3837 * Get hyperlink target.
3839 * @return {string} Hyperlink target
3841 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3846 * Get search engine traversal hint.
3848 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3850 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3851 return this.noFollow
;
3855 * Set hyperlink location.
3857 * @param {string|null} href Hyperlink location, null to remove
3859 * @return {OO.ui.Widget} The widget, for chaining
3861 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3862 href
= typeof href
=== 'string' ? href
: null;
3863 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3867 if ( href
!== this.href
) {
3876 * Update the `href` attribute, in case of changes to href or
3881 * @return {OO.ui.Widget} The widget, for chaining
3883 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3884 if ( this.href
!== null && !this.isDisabled() ) {
3885 this.$button
.attr( 'href', this.href
);
3887 this.$button
.removeAttr( 'href' );
3894 * Handle disable events.
3897 * @param {boolean} disabled Element is disabled
3899 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3904 * Set hyperlink target.
3906 * @param {string|null} target Hyperlink target, null to remove
3907 * @return {OO.ui.Widget} The widget, for chaining
3909 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3910 target
= typeof target
=== 'string' ? target
: null;
3912 if ( target
!== this.target
) {
3913 this.target
= target
;
3914 if ( target
!== null ) {
3915 this.$button
.attr( 'target', target
);
3917 this.$button
.removeAttr( 'target' );
3925 * Set search engine traversal hint.
3927 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3928 * @return {OO.ui.Widget} The widget, for chaining
3930 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3931 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3933 if ( noFollow
!== this.noFollow
) {
3934 this.noFollow
= noFollow
;
3936 this.$button
.attr( 'rel', 'nofollow' );
3938 this.$button
.removeAttr( 'rel' );
3945 // Override method visibility hints from ButtonElement
3956 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3957 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3958 * removed, and cleared from the group.
3961 * // A ButtonGroupWidget with two buttons.
3962 * var button1 = new OO.ui.PopupButtonWidget( {
3963 * label: 'Select a category',
3966 * $content: $( '<p>List of categories…</p>' ),
3971 * button2 = new OO.ui.ButtonWidget( {
3974 * buttonGroup = new OO.ui.ButtonGroupWidget( {
3975 * items: [ button1, button2 ]
3977 * $( document.body ).append( buttonGroup.$element );
3980 * @extends OO.ui.Widget
3981 * @mixins OO.ui.mixin.GroupElement
3982 * @mixins OO.ui.mixin.TitledElement
3985 * @param {Object} [config] Configuration options
3986 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3988 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3989 // Configuration initialization
3990 config
= config
|| {};
3992 // Parent constructor
3993 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3995 // Mixin constructors
3996 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3997 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4000 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
4001 if ( Array
.isArray( config
.items
) ) {
4002 this.addItems( config
.items
);
4008 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
4009 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
4010 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.TitledElement
);
4012 /* Static Properties */
4018 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
4026 * @return {OO.ui.Widget} The widget, for chaining
4028 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
4029 if ( !this.isDisabled() ) {
4030 if ( this.items
[ 0 ] ) {
4031 this.items
[ 0 ].focus();
4040 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
4045 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
4046 * which creates a label that identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
4047 * for a list of icons included in the library.
4050 * // An IconWidget with a label via LabelWidget.
4051 * var myIcon = new OO.ui.IconWidget( {
4055 * // Create a label.
4056 * iconLabel = new OO.ui.LabelWidget( {
4059 * $( document.body ).append( myIcon.$element, iconLabel.$element );
4061 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
4064 * @extends OO.ui.Widget
4065 * @mixins OO.ui.mixin.IconElement
4066 * @mixins OO.ui.mixin.TitledElement
4067 * @mixins OO.ui.mixin.LabelElement
4068 * @mixins OO.ui.mixin.FlaggedElement
4071 * @param {Object} [config] Configuration options
4073 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
4074 // Configuration initialization
4075 config
= config
|| {};
4077 // Parent constructor
4078 OO
.ui
.IconWidget
.parent
.call( this, config
);
4080 // Mixin constructors
4081 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
4082 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
4083 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
, invisibleLabel
: true } ) );
4084 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
4087 this.$element
.addClass( 'oo-ui-iconWidget' );
4088 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4089 // nested in other widgets, because this widget used to not mix in LabelElement.
4090 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4095 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
4096 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
4097 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
4098 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.LabelElement
);
4099 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
4101 /* Static Properties */
4107 OO
.ui
.IconWidget
.static.tagName
= 'span';
4110 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
4111 * attention to the status of an item or to clarify the function within a control. For a list of
4112 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
4115 * // An indicator widget.
4116 * var indicator1 = new OO.ui.IndicatorWidget( {
4117 * indicator: 'required'
4119 * // Create a fieldset layout to add a label.
4120 * fieldset = new OO.ui.FieldsetLayout();
4121 * fieldset.addItems( [
4122 * new OO.ui.FieldLayout( indicator1, {
4123 * label: 'A required indicator:'
4126 * $( document.body ).append( fieldset.$element );
4128 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4131 * @extends OO.ui.Widget
4132 * @mixins OO.ui.mixin.IndicatorElement
4133 * @mixins OO.ui.mixin.TitledElement
4134 * @mixins OO.ui.mixin.LabelElement
4137 * @param {Object} [config] Configuration options
4139 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4140 // Configuration initialization
4141 config
= config
|| {};
4143 // Parent constructor
4144 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4146 // Mixin constructors
4147 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
4148 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
4149 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
, invisibleLabel
: true } ) );
4152 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4153 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4154 // nested in other widgets, because this widget used to not mix in LabelElement.
4155 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4160 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4161 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4162 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4163 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.LabelElement
);
4165 /* Static Properties */
4171 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4174 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4175 * be configured with a `label` option that is set to a string, a label node, or a function:
4177 * - String: a plaintext string
4178 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4179 * label that includes a link or special styling, such as a gray color or additional graphical elements.
4180 * - Function: a function that will produce a string in the future. Functions are used
4181 * in cases where the value of the label is not currently defined.
4183 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
4184 * will come into focus when the label is clicked.
4187 * // Two LabelWidgets.
4188 * var label1 = new OO.ui.LabelWidget( {
4189 * label: 'plaintext label'
4191 * label2 = new OO.ui.LabelWidget( {
4192 * label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
4194 * // Create a fieldset layout with fields for each example.
4195 * fieldset = new OO.ui.FieldsetLayout();
4196 * fieldset.addItems( [
4197 * new OO.ui.FieldLayout( label1 ),
4198 * new OO.ui.FieldLayout( label2 )
4200 * $( document.body ).append( fieldset.$element );
4203 * @extends OO.ui.Widget
4204 * @mixins OO.ui.mixin.LabelElement
4205 * @mixins OO.ui.mixin.TitledElement
4208 * @param {Object} [config] Configuration options
4209 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4210 * Clicking the label will focus the specified input field.
4212 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4213 // Configuration initialization
4214 config
= config
|| {};
4216 // Parent constructor
4217 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4219 // Mixin constructors
4220 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
4221 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4224 this.input
= config
.input
;
4228 if ( this.input
.getInputId() ) {
4229 this.$element
.attr( 'for', this.input
.getInputId() );
4231 this.$label
.on( 'click', function () {
4232 this.input
.simulateLabelClick();
4236 this.$element
.addClass( 'oo-ui-labelWidget' );
4241 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4242 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4243 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4245 /* Static Properties */
4251 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4254 * PendingElement is a mixin that is used to create elements that notify users that something is happening
4255 * and that they should wait before proceeding. The pending state is visually represented with a pending
4256 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
4257 * field of a {@link OO.ui.TextInputWidget text input widget}.
4259 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
4260 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
4261 * in process dialogs.
4264 * function MessageDialog( config ) {
4265 * MessageDialog.parent.call( this, config );
4267 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4269 * MessageDialog.static.name = 'myMessageDialog';
4270 * MessageDialog.static.actions = [
4271 * { action: 'save', label: 'Done', flags: 'primary' },
4272 * { label: 'Cancel', flags: 'safe' }
4275 * MessageDialog.prototype.initialize = function () {
4276 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4277 * this.content = new OO.ui.PanelLayout( { padded: true } );
4278 * 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>' );
4279 * this.$body.append( this.content.$element );
4281 * MessageDialog.prototype.getBodyHeight = function () {
4284 * MessageDialog.prototype.getActionProcess = function ( action ) {
4285 * var dialog = this;
4286 * if ( action === 'save' ) {
4287 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4288 * return new OO.ui.Process()
4290 * .next( function () {
4291 * dialog.getActions().get({actions: 'save'})[0].popPending();
4294 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4297 * var windowManager = new OO.ui.WindowManager();
4298 * $( document.body ).append( windowManager.$element );
4300 * var dialog = new MessageDialog();
4301 * windowManager.addWindows( [ dialog ] );
4302 * windowManager.openWindow( dialog );
4308 * @param {Object} [config] Configuration options
4309 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4311 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4312 // Configuration initialization
4313 config
= config
|| {};
4317 this.$pending
= null;
4320 this.setPendingElement( config
.$pending
|| this.$element
);
4325 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4330 * Set the pending element (and clean up any existing one).
4332 * @param {jQuery} $pending The element to set to pending.
4334 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4335 if ( this.$pending
) {
4336 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4339 this.$pending
= $pending
;
4340 if ( this.pending
> 0 ) {
4341 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4346 * Check if an element is pending.
4348 * @return {boolean} Element is pending
4350 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4351 return !!this.pending
;
4355 * Increase the pending counter. The pending state will remain active until the counter is zero
4356 * (i.e., the number of calls to #pushPending and #popPending is the same).
4359 * @return {OO.ui.Element} The element, for chaining
4361 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4362 if ( this.pending
=== 0 ) {
4363 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4364 this.updateThemeClasses();
4372 * Decrease the pending counter. The pending state will remain active until the counter is zero
4373 * (i.e., the number of calls to #pushPending and #popPending is the same).
4376 * @return {OO.ui.Element} The element, for chaining
4378 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4379 if ( this.pending
=== 1 ) {
4380 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4381 this.updateThemeClasses();
4383 this.pending
= Math
.max( 0, this.pending
- 1 );
4389 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4390 * in the document (for example, in an OO.ui.Window's $overlay).
4392 * The elements's position is automatically calculated and maintained when window is resized or the
4393 * page is scrolled. If you reposition the container manually, you have to call #position to make
4394 * sure the element is still placed correctly.
4396 * As positioning is only possible when both the element and the container are attached to the DOM
4397 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4398 * the #toggle method to display a floating popup, for example.
4404 * @param {Object} [config] Configuration options
4405 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4406 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4407 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4408 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4409 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4410 * 'top': Align the top edge with $floatableContainer's top edge
4411 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4412 * 'center': Vertically align the center with $floatableContainer's center
4413 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4414 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4415 * 'after': Directly after $floatableContainer, aligning f's start edge with fC's end edge
4416 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4417 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4418 * 'center': Horizontally align the center with $floatableContainer's center
4419 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4422 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4423 // Configuration initialization
4424 config
= config
|| {};
4427 this.$floatable
= null;
4428 this.$floatableContainer
= null;
4429 this.$floatableWindow
= null;
4430 this.$floatableClosestScrollable
= null;
4431 this.floatableOutOfView
= false;
4432 this.onFloatableScrollHandler
= this.position
.bind( this );
4433 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4436 this.setFloatableContainer( config
.$floatableContainer
);
4437 this.setFloatableElement( config
.$floatable
|| this.$element
);
4438 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4439 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4440 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ? true : !!config
.hideWhenOutOfView
;
4446 * Set floatable element.
4448 * If an element is already set, it will be cleaned up before setting up the new element.
4450 * @param {jQuery} $floatable Element to make floatable
4452 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4453 if ( this.$floatable
) {
4454 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4455 this.$floatable
.css( { left
: '', top
: '' } );
4458 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4463 * Set floatable container.
4465 * The element will be positioned relative to the specified container.
4467 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4469 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4470 this.$floatableContainer
= $floatableContainer
;
4471 if ( this.$floatable
) {
4477 * Change how the element is positioned vertically.
4479 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4481 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4482 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4483 throw new Error( 'Invalid value for vertical position: ' + position
);
4485 if ( this.verticalPosition
!== position
) {
4486 this.verticalPosition
= position
;
4487 if ( this.$floatable
) {
4494 * Change how the element is positioned horizontally.
4496 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4498 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4499 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4500 throw new Error( 'Invalid value for horizontal position: ' + position
);
4502 if ( this.horizontalPosition
!== position
) {
4503 this.horizontalPosition
= position
;
4504 if ( this.$floatable
) {
4511 * Toggle positioning.
4513 * Do not turn positioning on until after the element is attached to the DOM and visible.
4515 * @param {boolean} [positioning] Enable positioning, omit to toggle
4517 * @return {OO.ui.Element} The element, for chaining
4519 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4520 var closestScrollableOfContainer
;
4522 if ( !this.$floatable
|| !this.$floatableContainer
) {
4526 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4528 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4529 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4530 this.warnedUnattached
= true;
4533 if ( this.positioning
!== positioning
) {
4534 this.positioning
= positioning
;
4536 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
4537 // If the scrollable is the root, we have to listen to scroll events
4538 // on the window because of browser inconsistencies.
4539 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4540 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
4543 if ( positioning
) {
4544 this.$floatableWindow
= $( this.getElementWindow() );
4545 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4547 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4548 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4550 // Initial position after visible
4553 if ( this.$floatableWindow
) {
4554 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4555 this.$floatableWindow
= null;
4558 if ( this.$floatableClosestScrollable
) {
4559 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4560 this.$floatableClosestScrollable
= null;
4563 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4571 * Check whether the bottom edge of the given element is within the viewport of the given container.
4574 * @param {jQuery} $element
4575 * @param {jQuery} $container
4578 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4579 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
, rightEdgeInBounds
,
4580 startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4581 direction
= $element
.css( 'direction' );
4583 elemRect
= $element
[ 0 ].getBoundingClientRect();
4584 if ( $container
[ 0 ] === window
) {
4585 viewportSpacing
= OO
.ui
.getViewportSpacing();
4589 right
: document
.documentElement
.clientWidth
,
4590 bottom
: document
.documentElement
.clientHeight
4592 contRect
.top
+= viewportSpacing
.top
;
4593 contRect
.left
+= viewportSpacing
.left
;
4594 contRect
.right
-= viewportSpacing
.right
;
4595 contRect
.bottom
-= viewportSpacing
.bottom
;
4597 contRect
= $container
[ 0 ].getBoundingClientRect();
4600 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4601 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4602 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4603 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4604 if ( direction
=== 'rtl' ) {
4605 startEdgeInBounds
= rightEdgeInBounds
;
4606 endEdgeInBounds
= leftEdgeInBounds
;
4608 startEdgeInBounds
= leftEdgeInBounds
;
4609 endEdgeInBounds
= rightEdgeInBounds
;
4612 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4615 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4618 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4621 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4625 // The other positioning values are all about being inside the container,
4626 // so in those cases all we care about is that any part of the container is visible.
4627 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4628 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4632 * Check if the floatable is hidden to the user because it was offscreen.
4634 * @return {boolean} Floatable is out of view
4636 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4637 return this.floatableOutOfView
;
4641 * Position the floatable below its container.
4643 * This should only be done when both of them are attached to the DOM and visible.
4646 * @return {OO.ui.Element} The element, for chaining
4648 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4649 if ( !this.positioning
) {
4654 // To continue, some things need to be true:
4655 // The element must actually be in the DOM
4656 this.isElementAttached() && (
4657 // The closest scrollable is the current window
4658 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4659 // OR is an element in the element's DOM
4660 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4663 // Abort early if important parts of the widget are no longer attached to the DOM
4667 this.floatableOutOfView
= this.hideWhenOutOfView
&& !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4668 if ( this.floatableOutOfView
) {
4669 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4672 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4675 this.$floatable
.css( this.computePosition() );
4677 // We updated the position, so re-evaluate the clipping state.
4678 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4679 // will not notice the need to update itself.)
4680 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4681 // it not listen to the right events in the right places?
4690 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4691 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4692 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4694 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4696 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4697 var isBody
, scrollableX
, scrollableY
, containerPos
,
4698 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4699 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4700 direction
= this.$floatableContainer
.css( 'direction' ),
4701 $offsetParent
= this.$floatable
.offsetParent();
4703 if ( $offsetParent
.is( 'html' ) ) {
4704 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4705 // <html> element, but they do work on the <body>
4706 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4708 isBody
= $offsetParent
.is( 'body' );
4709 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' || $offsetParent
.css( 'overflow-x' ) === 'auto';
4710 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' || $offsetParent
.css( 'overflow-y' ) === 'auto';
4712 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4713 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4714 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
4715 // or if it isn't scrollable
4716 scrollTop
= scrollableY
&& !isBody
? $offsetParent
.scrollTop() : 0;
4717 scrollLeft
= scrollableX
&& !isBody
? OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4719 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4720 // if the <body> has a margin
4721 containerPos
= isBody
?
4722 this.$floatableContainer
.offset() :
4723 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4724 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4725 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4726 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4727 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4729 if ( this.verticalPosition
=== 'below' ) {
4730 newPos
.top
= containerPos
.bottom
;
4731 } else if ( this.verticalPosition
=== 'above' ) {
4732 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4733 } else if ( this.verticalPosition
=== 'top' ) {
4734 newPos
.top
= containerPos
.top
;
4735 } else if ( this.verticalPosition
=== 'bottom' ) {
4736 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4737 } else if ( this.verticalPosition
=== 'center' ) {
4738 newPos
.top
= containerPos
.top
+
4739 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4742 if ( this.horizontalPosition
=== 'before' ) {
4743 newPos
.end
= containerPos
.start
;
4744 } else if ( this.horizontalPosition
=== 'after' ) {
4745 newPos
.start
= containerPos
.end
;
4746 } else if ( this.horizontalPosition
=== 'start' ) {
4747 newPos
.start
= containerPos
.start
;
4748 } else if ( this.horizontalPosition
=== 'end' ) {
4749 newPos
.end
= containerPos
.end
;
4750 } else if ( this.horizontalPosition
=== 'center' ) {
4751 newPos
.left
= containerPos
.left
+
4752 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4755 if ( newPos
.start
!== undefined ) {
4756 if ( direction
=== 'rtl' ) {
4757 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.start
;
4759 newPos
.left
= newPos
.start
;
4761 delete newPos
.start
;
4763 if ( newPos
.end
!== undefined ) {
4764 if ( direction
=== 'rtl' ) {
4765 newPos
.left
= newPos
.end
;
4767 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.end
;
4772 // Account for scroll position
4773 if ( newPos
.top
!== '' ) {
4774 newPos
.top
+= scrollTop
;
4776 if ( newPos
.bottom
!== '' ) {
4777 newPos
.bottom
-= scrollTop
;
4779 if ( newPos
.left
!== '' ) {
4780 newPos
.left
+= scrollLeft
;
4782 if ( newPos
.right
!== '' ) {
4783 newPos
.right
-= scrollLeft
;
4786 // Account for scrollbar gutter
4787 if ( newPos
.bottom
!== '' ) {
4788 newPos
.bottom
-= horizScrollbarHeight
;
4790 if ( direction
=== 'rtl' ) {
4791 if ( newPos
.left
!== '' ) {
4792 newPos
.left
-= vertScrollbarWidth
;
4795 if ( newPos
.right
!== '' ) {
4796 newPos
.right
-= vertScrollbarWidth
;
4804 * Element that can be automatically clipped to visible boundaries.
4806 * Whenever the element's natural height changes, you have to call
4807 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4808 * clipping correctly.
4810 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4811 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4812 * then #$clippable will be given a fixed reduced height and/or width and will be made
4813 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4814 * but you can build a static footer by setting #$clippableContainer to an element that contains
4815 * #$clippable and the footer.
4821 * @param {Object} [config] Configuration options
4822 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4823 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4824 * omit to use #$clippable
4826 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4827 // Configuration initialization
4828 config
= config
|| {};
4831 this.$clippable
= null;
4832 this.$clippableContainer
= null;
4833 this.clipping
= false;
4834 this.clippedHorizontally
= false;
4835 this.clippedVertically
= false;
4836 this.$clippableScrollableContainer
= null;
4837 this.$clippableScroller
= null;
4838 this.$clippableWindow
= null;
4839 this.idealWidth
= null;
4840 this.idealHeight
= null;
4841 this.onClippableScrollHandler
= this.clip
.bind( this );
4842 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4845 if ( config
.$clippableContainer
) {
4846 this.setClippableContainer( config
.$clippableContainer
);
4848 this.setClippableElement( config
.$clippable
|| this.$element
);
4854 * Set clippable element.
4856 * If an element is already set, it will be cleaned up before setting up the new element.
4858 * @param {jQuery} $clippable Element to make clippable
4860 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4861 if ( this.$clippable
) {
4862 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4863 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4864 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4867 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4872 * Set clippable container.
4874 * This is the container that will be measured when deciding whether to clip. When clipping,
4875 * #$clippable will be resized in order to keep the clippable container fully visible.
4877 * If the clippable container is unset, #$clippable will be used.
4879 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4881 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4882 this.$clippableContainer
= $clippableContainer
;
4883 if ( this.$clippable
) {
4891 * Do not turn clipping on until after the element is attached to the DOM and visible.
4893 * @param {boolean} [clipping] Enable clipping, omit to toggle
4895 * @return {OO.ui.Element} The element, for chaining
4897 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4898 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4900 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4901 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4902 this.warnedUnattached
= true;
4905 if ( this.clipping
!== clipping
) {
4906 this.clipping
= clipping
;
4908 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4909 // If the clippable container is the root, we have to listen to scroll events and check
4910 // jQuery.scrollTop on the window because of browser inconsistencies
4911 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4912 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4913 this.$clippableScrollableContainer
;
4914 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4915 this.$clippableWindow
= $( this.getElementWindow() )
4916 .on( 'resize', this.onClippableWindowResizeHandler
);
4917 // Initial clip after visible
4920 this.$clippable
.css( {
4928 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4930 this.$clippableScrollableContainer
= null;
4931 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4932 this.$clippableScroller
= null;
4933 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4934 this.$clippableWindow
= null;
4942 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4944 * @return {boolean} Element will be clipped to the visible area
4946 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4947 return this.clipping
;
4951 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4953 * @return {boolean} Part of the element is being clipped
4955 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4956 return this.clippedHorizontally
|| this.clippedVertically
;
4960 * Check if the right of the element is being clipped by the nearest scrollable container.
4962 * @return {boolean} Part of the element is being clipped
4964 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4965 return this.clippedHorizontally
;
4969 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4971 * @return {boolean} Part of the element is being clipped
4973 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4974 return this.clippedVertically
;
4978 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4980 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4981 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4983 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4984 this.idealWidth
= width
;
4985 this.idealHeight
= height
;
4987 if ( !this.clipping
) {
4988 // Update dimensions
4989 this.$clippable
.css( { width
: width
, height
: height
} );
4991 // While clipping, idealWidth and idealHeight are not considered
4995 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4996 * ClippableElement will clip the opposite side when reducing element's width.
4998 * Classes that mix in ClippableElement should override this to return 'right' if their
4999 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
5000 * If your class also mixes in FloatableElement, this is handled automatically.
5002 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5003 * always in pixels, even if they were unset or set to 'auto'.)
5005 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
5007 * @return {string} 'left' or 'right'
5009 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
5010 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
5017 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5018 * ClippableElement will clip the opposite side when reducing element's width.
5020 * Classes that mix in ClippableElement should override this to return 'bottom' if their
5021 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
5022 * If your class also mixes in FloatableElement, this is handled automatically.
5024 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5025 * always in pixels, even if they were unset or set to 'auto'.)
5027 * When in doubt, 'top' is a sane fallback.
5029 * @return {string} 'top' or 'bottom'
5031 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
5032 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
5039 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
5040 * when the element's natural height changes.
5042 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5043 * overlapped by, the visible area of the nearest scrollable container.
5045 * Because calling clip() when the natural height changes isn't always possible, we also set
5046 * max-height when the element isn't being clipped. This means that if the element tries to grow
5047 * beyond the edge, something reasonable will happen before clip() is called.
5050 * @return {OO.ui.Element} The element, for chaining
5052 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
5053 var extraHeight
, extraWidth
, viewportSpacing
,
5054 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
5055 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
5056 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
5057 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
5058 // Extra tolerance so that the sloppy code below doesn't result in results that are off
5059 // by one or two pixels. (And also so that we have space to display drop shadows.)
5060 // Chosen by fair dice roll.
5063 if ( !this.clipping
) {
5064 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
5068 function rectIntersection( a
, b
) {
5070 out
.top
= Math
.max( a
.top
, b
.top
);
5071 out
.left
= Math
.max( a
.left
, b
.left
);
5072 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
5073 out
.right
= Math
.min( a
.right
, b
.right
);
5077 viewportSpacing
= OO
.ui
.getViewportSpacing();
5079 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
5080 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
5081 // Dimensions of the browser window, rather than the element!
5085 right
: document
.documentElement
.clientWidth
,
5086 bottom
: document
.documentElement
.clientHeight
5088 viewportRect
.top
+= viewportSpacing
.top
;
5089 viewportRect
.left
+= viewportSpacing
.left
;
5090 viewportRect
.right
-= viewportSpacing
.right
;
5091 viewportRect
.bottom
-= viewportSpacing
.bottom
;
5093 $viewport
= this.$clippableScrollableContainer
;
5094 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
5095 // Convert into a plain object
5096 viewportRect
= $.extend( {}, viewportRect
);
5099 // Account for scrollbar gutter
5100 direction
= $viewport
.css( 'direction' );
5101 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
5102 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
5103 viewportRect
.bottom
-= horizScrollbarHeight
;
5104 if ( direction
=== 'rtl' ) {
5105 viewportRect
.left
+= vertScrollbarWidth
;
5107 viewportRect
.right
-= vertScrollbarWidth
;
5110 // Add arbitrary tolerance
5111 viewportRect
.top
+= buffer
;
5112 viewportRect
.left
+= buffer
;
5113 viewportRect
.right
-= buffer
;
5114 viewportRect
.bottom
-= buffer
;
5116 $item
= this.$clippableContainer
|| this.$clippable
;
5118 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
5119 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
5121 itemRect
= $item
[ 0 ].getBoundingClientRect();
5122 // Convert into a plain object
5123 itemRect
= $.extend( {}, itemRect
);
5125 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
5126 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
5127 if ( this.getHorizontalAnchorEdge() === 'right' ) {
5128 itemRect
.left
= viewportRect
.left
;
5130 itemRect
.right
= viewportRect
.right
;
5132 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5133 itemRect
.top
= viewportRect
.top
;
5135 itemRect
.bottom
= viewportRect
.bottom
;
5138 availableRect
= rectIntersection( viewportRect
, itemRect
);
5140 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
5141 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5142 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5143 desiredWidth
= Math
.min( desiredWidth
,
5144 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5145 desiredHeight
= Math
.min( desiredHeight
,
5146 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5147 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5148 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5149 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5150 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5151 clipWidth
= allotedWidth
< naturalWidth
;
5152 clipHeight
= allotedHeight
< naturalHeight
;
5155 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5156 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5157 this.$clippable
.css( 'overflowX', 'scroll' );
5158 // eslint-disable-next-line no-void
5159 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5160 this.$clippable
.css( {
5161 width
: Math
.max( 0, allotedWidth
),
5165 this.$clippable
.css( {
5167 width
: this.idealWidth
|| '',
5168 maxWidth
: Math
.max( 0, allotedWidth
)
5172 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5173 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5174 this.$clippable
.css( 'overflowY', 'scroll' );
5175 // eslint-disable-next-line no-void
5176 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5177 this.$clippable
.css( {
5178 height
: Math
.max( 0, allotedHeight
),
5182 this.$clippable
.css( {
5184 height
: this.idealHeight
|| '',
5185 maxHeight
: Math
.max( 0, allotedHeight
)
5189 // If we stopped clipping in at least one of the dimensions
5190 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5191 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5194 this.clippedHorizontally
= clipWidth
;
5195 this.clippedVertically
= clipHeight
;
5201 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5202 * By default, each popup has an anchor that points toward its origin.
5203 * Please see the [OOUI documentation on MediaWiki.org] [1] for more information and examples.
5205 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5209 * var popup = new OO.ui.PopupWidget( {
5210 * $content: $( '<p>Hi there!</p>' ),
5215 * $( document.body ).append( popup.$element );
5216 * // To display the popup, toggle the visibility to 'true'.
5217 * popup.toggle( true );
5219 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5222 * @extends OO.ui.Widget
5223 * @mixins OO.ui.mixin.LabelElement
5224 * @mixins OO.ui.mixin.ClippableElement
5225 * @mixins OO.ui.mixin.FloatableElement
5228 * @param {Object} [config] Configuration options
5229 * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
5230 * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
5231 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5232 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5233 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5234 * of $floatableContainer
5235 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5236 * of $floatableContainer
5237 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5238 * endwards (right/left) to the vertical center of $floatableContainer
5239 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5240 * startwards (left/right) to the vertical center of $floatableContainer
5241 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5242 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
5243 * as possible while still keeping the anchor within the popup;
5244 * if position is before/after, move the popup as far downwards as possible.
5245 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
5246 * as possible while still keeping the anchor within the popup;
5247 * if position in before/after, move the popup as far upwards as possible.
5248 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center
5249 * of the popup with the center of $floatableContainer.
5250 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5251 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5252 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5253 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5254 * desired direction to display the popup without clipping
5255 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5256 * See the [OOUI docs on MediaWiki][3] for an example.
5257 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5258 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
5259 * @cfg {jQuery} [$content] Content to append to the popup's body
5260 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5261 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5262 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5263 * This config option is only relevant if #autoClose is set to `true`. See the [OOUI documentation on MediaWiki][2]
5265 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5266 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5268 * @cfg {boolean} [padded=false] Add padding to the popup's body
5270 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5271 // Configuration initialization
5272 config
= config
|| {};
5274 // Parent constructor
5275 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5277 // Properties (must be set before ClippableElement constructor call)
5278 this.$body
= $( '<div>' );
5279 this.$popup
= $( '<div>' );
5281 // Mixin constructors
5282 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5283 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
5284 $clippable
: this.$body
,
5285 $clippableContainer
: this.$popup
5287 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5290 this.$anchor
= $( '<div>' );
5291 // If undefined, will be computed lazily in computePosition()
5292 this.$container
= config
.$container
;
5293 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5294 this.autoClose
= !!config
.autoClose
;
5295 this.transitionTimeout
= null;
5296 this.anchored
= false;
5297 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5298 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5301 this.setSize( config
.width
, config
.height
);
5302 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5303 this.setAlignment( config
.align
|| 'center' );
5304 this.setPosition( config
.position
|| 'below' );
5305 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5306 this.setAutoCloseIgnore( config
.$autoCloseIgnore
);
5307 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5308 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5310 .addClass( 'oo-ui-popupWidget-popup' )
5311 .append( this.$body
);
5313 .addClass( 'oo-ui-popupWidget' )
5314 .append( this.$popup
, this.$anchor
);
5315 // Move content, which was added to #$element by OO.ui.Widget, to the body
5316 // FIXME This is gross, we should use '$body' or something for the config
5317 if ( config
.$content
instanceof $ ) {
5318 this.$body
.append( config
.$content
);
5321 if ( config
.padded
) {
5322 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5325 if ( config
.head
) {
5326 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
5327 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
5328 this.$head
= $( '<div>' )
5329 .addClass( 'oo-ui-popupWidget-head' )
5330 .append( this.$label
, this.closeButton
.$element
);
5331 this.$popup
.prepend( this.$head
);
5334 if ( config
.$footer
) {
5335 this.$footer
= $( '<div>' )
5336 .addClass( 'oo-ui-popupWidget-footer' )
5337 .append( config
.$footer
);
5338 this.$popup
.append( this.$footer
);
5341 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5342 // that reference properties not initialized at that time of parent class construction
5343 // TODO: Find a better way to handle post-constructor setup
5344 this.visible
= false;
5345 this.$element
.addClass( 'oo-ui-element-hidden' );
5350 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5351 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5352 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5353 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5360 * The popup is ready: it is visible and has been positioned and clipped.
5366 * Handles document mouse down events.
5369 * @param {MouseEvent} e Mouse down event
5371 OO
.ui
.PopupWidget
.prototype.onDocumentMouseDown = function ( e
) {
5374 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5376 this.toggle( false );
5380 // Deprecated alias since 0.28.3
5381 OO
.ui
.PopupWidget
.prototype.onMouseDown = function () {
5382 OO
.ui
.warnDeprecation( 'onMouseDown is deprecated, use onDocumentMouseDown instead' );
5383 this.onDocumentMouseDown
.apply( this, arguments
);
5387 * Bind document mouse down listener.
5391 OO
.ui
.PopupWidget
.prototype.bindDocumentMouseDownListener = function () {
5392 // Capture clicks outside popup
5393 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5394 // We add 'click' event because iOS safari needs to respond to this event.
5395 // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
5396 // then it will trigger when scrolling. While iOS Safari has some reported behavior
5397 // of occasionally not emitting 'click' properly, that event seems to be the standard
5398 // that it should be emitting, so we add it to this and will operate the event handler
5399 // on whichever of these events was triggered first
5400 this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5403 // Deprecated alias since 0.28.3
5404 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
5405 OO
.ui
.warnDeprecation( 'bindMouseDownListener is deprecated, use bindDocumentMouseDownListener instead' );
5406 this.bindDocumentMouseDownListener
.apply( this, arguments
);
5410 * Handles close button click events.
5414 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5415 if ( this.isVisible() ) {
5416 this.toggle( false );
5421 * Unbind document mouse down listener.
5425 OO
.ui
.PopupWidget
.prototype.unbindDocumentMouseDownListener = function () {
5426 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5427 this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5430 // Deprecated alias since 0.28.3
5431 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
5432 OO
.ui
.warnDeprecation( 'unbindMouseDownListener is deprecated, use unbindDocumentMouseDownListener instead' );
5433 this.unbindDocumentMouseDownListener
.apply( this, arguments
);
5437 * Handles document key down events.
5440 * @param {KeyboardEvent} e Key down event
5442 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5444 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5447 this.toggle( false );
5449 e
.stopPropagation();
5454 * Bind document key down listener.
5458 OO
.ui
.PopupWidget
.prototype.bindDocumentKeyDownListener = function () {
5459 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5462 // Deprecated alias since 0.28.3
5463 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
5464 OO
.ui
.warnDeprecation( 'bindKeyDownListener is deprecated, use bindDocumentKeyDownListener instead' );
5465 this.bindDocumentKeyDownListener
.apply( this, arguments
);
5469 * Unbind document key down listener.
5473 OO
.ui
.PopupWidget
.prototype.unbindDocumentKeyDownListener = function () {
5474 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5477 // Deprecated alias since 0.28.3
5478 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
5479 OO
.ui
.warnDeprecation( 'unbindKeyDownListener is deprecated, use unbindDocumentKeyDownListener instead' );
5480 this.unbindDocumentKeyDownListener
.apply( this, arguments
);
5484 * Show, hide, or toggle the visibility of the anchor.
5486 * @param {boolean} [show] Show anchor, omit to toggle
5488 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5489 show
= show
=== undefined ? !this.anchored
: !!show
;
5491 if ( this.anchored
!== show
) {
5493 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5494 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5496 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5497 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5499 this.anchored
= show
;
5504 * Change which edge the anchor appears on.
5506 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5508 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5509 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5510 throw new Error( 'Invalid value for edge: ' + edge
);
5512 if ( this.anchorEdge
!== null ) {
5513 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5515 this.anchorEdge
= edge
;
5516 if ( this.anchored
) {
5517 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5522 * Check if the anchor is visible.
5524 * @return {boolean} Anchor is visible
5526 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5527 return this.anchored
;
5531 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5532 * `.toggle( true )` after its #$element is attached to the DOM.
5534 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5535 * it in the right place and with the right dimensions only work correctly while it is attached.
5536 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5537 * strictly enforced, so currently it only generates a warning in the browser console.
5542 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5543 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5544 show
= show
=== undefined ? !this.isVisible() : !!show
;
5546 change
= show
!== this.isVisible();
5548 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5549 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5550 this.warnedUnattached
= true;
5552 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5553 // Fall back to the parent node if the floatableContainer is not set
5554 this.setFloatableContainer( this.$element
.parent() );
5557 if ( change
&& show
&& this.autoFlip
) {
5558 // Reset auto-flipping before showing the popup again. It's possible we no longer need to flip
5559 // (e.g. if the user scrolled).
5560 this.isAutoFlipped
= false;
5564 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5567 this.togglePositioning( show
&& !!this.$floatableContainer
);
5570 if ( this.autoClose
) {
5571 this.bindDocumentMouseDownListener();
5572 this.bindDocumentKeyDownListener();
5574 this.updateDimensions();
5575 this.toggleClipping( true );
5577 if ( this.autoFlip
) {
5578 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5579 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5580 // If opening the popup in the normal direction causes it to be clipped, open
5581 // in the opposite one instead
5582 normalHeight
= this.$element
.height();
5583 this.isAutoFlipped
= !this.isAutoFlipped
;
5585 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5586 // If that also causes it to be clipped, open in whichever direction
5587 // we have more space
5588 oppositeHeight
= this.$element
.height();
5589 if ( oppositeHeight
< normalHeight
) {
5590 this.isAutoFlipped
= !this.isAutoFlipped
;
5596 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5597 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5598 // If opening the popup in the normal direction causes it to be clipped, open
5599 // in the opposite one instead
5600 normalWidth
= this.$element
.width();
5601 this.isAutoFlipped
= !this.isAutoFlipped
;
5602 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5603 // which causes positioning to be off. Toggle clipping back and fort to work around.
5604 this.toggleClipping( false );
5606 this.toggleClipping( true );
5607 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5608 // If that also causes it to be clipped, open in whichever direction
5609 // we have more space
5610 oppositeWidth
= this.$element
.width();
5611 if ( oppositeWidth
< normalWidth
) {
5612 this.isAutoFlipped
= !this.isAutoFlipped
;
5613 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5614 // which causes positioning to be off. Toggle clipping back and fort to work around.
5615 this.toggleClipping( false );
5617 this.toggleClipping( true );
5624 this.emit( 'ready' );
5626 this.toggleClipping( false );
5627 if ( this.autoClose
) {
5628 this.unbindDocumentMouseDownListener();
5629 this.unbindDocumentKeyDownListener();
5638 * Set the size of the popup.
5640 * Changing the size may also change the popup's position depending on the alignment.
5642 * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
5643 * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
5644 * @param {boolean} [transition=false] Use a smooth transition
5647 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5648 this.width
= width
!== undefined ? width
: 320;
5649 this.height
= height
!== undefined ? height
: null;
5650 if ( this.isVisible() ) {
5651 this.updateDimensions( transition
);
5656 * Update the size and position.
5658 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5659 * be called automatically.
5661 * @param {boolean} [transition=false] Use a smooth transition
5664 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5667 // Prevent transition from being interrupted
5668 clearTimeout( this.transitionTimeout
);
5670 // Enable transition
5671 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5677 // Prevent transitioning after transition is complete
5678 this.transitionTimeout
= setTimeout( function () {
5679 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5682 // Prevent transitioning immediately
5683 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5690 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5691 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
, anchorPos
,
5692 anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
, floatablePos
,
5693 offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5695 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5696 popupPositionOppositeMap
= {
5704 'force-left': 'backwards',
5705 'force-right': 'forwards'
5708 'force-left': 'forwards',
5709 'force-right': 'backwards'
5721 backwards
: this.anchored
? 'before' : 'end'
5729 if ( !this.$container
) {
5730 // Lazy-initialize $container if not specified in constructor
5731 this.$container
= $( this.getClosestScrollableElementContainer() );
5733 direction
= this.$container
.css( 'direction' );
5735 // Set height and width before we do anything else, since it might cause our measurements
5736 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5738 width
: this.width
!== null ? this.width
: 'auto',
5739 height
: this.height
!== null ? this.height
: 'auto'
5742 align
= alignMap
[ direction
][ this.align
] || this.align
;
5743 popupPosition
= this.popupPosition
;
5744 if ( this.isAutoFlipped
) {
5745 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5748 // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
5749 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5750 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5751 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5752 near
= vertical
? 'top' : 'left';
5753 far
= vertical
? 'bottom' : 'right';
5754 sizeProp
= vertical
? 'Height' : 'Width';
5755 popupSize
= vertical
? ( this.height
|| this.$popup
.height() ) : ( this.width
|| this.$popup
.width() );
5757 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5758 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5759 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5762 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5763 // Find out which property FloatableElement used for positioning, and adjust that value
5764 positionProp
= vertical
?
5765 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5766 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5768 // Figure out where the near and far edges of the popup and $floatableContainer are
5769 floatablePos
= this.$floatableContainer
.offset();
5770 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5771 // Measure where the offsetParent is and compute our position based on that and parentPosition
5772 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5773 { top
: 0, left
: 0 } :
5774 this.$element
.offsetParent().offset();
5776 if ( positionProp
=== near
) {
5777 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5778 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5780 popupPos
[ far
] = offsetParentPos
[ near
] +
5781 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5782 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5785 if ( this.anchored
) {
5786 // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
5787 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5788 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5790 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
5791 // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
5792 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5793 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5794 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5795 // Not enough space for the anchor on the start side; pull the popup startwards
5796 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5797 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5798 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5799 // Not enough space for the anchor on the end side; pull the popup endwards
5800 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5801 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5803 positionAdjustment
= 0;
5806 positionAdjustment
= 0;
5809 // Check if the popup will go beyond the edge of this.$container
5810 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5811 { top
: 0, left
: 0 } :
5812 this.$container
.offset();
5813 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5814 if ( this.$container
[ 0 ] === document
.documentElement
) {
5815 viewportSpacing
= OO
.ui
.getViewportSpacing();
5816 containerPos
[ near
] += viewportSpacing
[ near
];
5817 containerPos
[ far
] -= viewportSpacing
[ far
];
5819 // Take into account how much the popup will move because of the adjustments we're going to make
5820 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5821 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5822 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5823 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5824 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5825 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5826 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5827 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5828 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5829 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5832 if ( this.anchored
) {
5833 // Adjust anchorOffset for positionAdjustment
5834 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5836 // Position the anchor
5837 anchorCss
[ start
] = anchorOffset
;
5838 this.$anchor
.css( anchorCss
);
5841 // Move the popup if needed
5842 parentPosition
[ positionProp
] += positionAdjustment
;
5844 return parentPosition
;
5848 * Set popup alignment
5850 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5851 * `backwards` or `forwards`.
5853 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5854 // Validate alignment
5855 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5858 this.align
= 'center';
5864 * Get popup alignment
5866 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5867 * `backwards` or `forwards`.
5869 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5874 * Change the positioning of the popup.
5876 * @param {string} position 'above', 'below', 'before' or 'after'
5878 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5879 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5882 this.popupPosition
= position
;
5887 * Get popup positioning.
5889 * @return {string} 'above', 'below', 'before' or 'after'
5891 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5892 return this.popupPosition
;
5896 * Set popup auto-flipping.
5898 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
5899 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5900 * desired direction to display the popup without clipping
5902 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
5903 autoFlip
= !!autoFlip
;
5905 if ( this.autoFlip
!== autoFlip
) {
5906 this.autoFlip
= autoFlip
;
5911 * Set which elements will not close the popup when clicked.
5913 * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
5915 * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
5917 OO
.ui
.PopupWidget
.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore
) {
5918 this.$autoCloseIgnore
= $autoCloseIgnore
;
5922 * Get an ID of the body element, this can be used as the
5923 * `aria-describedby` attribute for an input field.
5925 * @return {string} The ID of the body element
5927 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
5928 var id
= this.$body
.attr( 'id' );
5929 if ( id
=== undefined ) {
5930 id
= OO
.ui
.generateElementId();
5931 this.$body
.attr( 'id', id
);
5937 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5938 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5939 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5940 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5946 * @param {Object} [config] Configuration options
5947 * @cfg {Object} [popup] Configuration to pass to popup
5948 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5950 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5951 // Configuration initialization
5952 config
= config
|| {};
5955 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5958 $floatableContainer
: this.$element
5962 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5972 * @return {OO.ui.PopupWidget} Popup widget
5974 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5979 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5980 * which is used to display additional information or options.
5983 * // A PopupButtonWidget.
5984 * var popupButton = new OO.ui.PopupButtonWidget( {
5985 * label: 'Popup button with options',
5988 * $content: $( '<p>Additional options here.</p>' ),
5990 * align: 'force-left'
5993 * // Append the button to the DOM.
5994 * $( document.body ).append( popupButton.$element );
5997 * @extends OO.ui.ButtonWidget
5998 * @mixins OO.ui.mixin.PopupElement
6001 * @param {Object} [config] Configuration options
6002 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
6003 * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6004 * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
6005 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
6007 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
6008 // Configuration initialization
6009 config
= config
|| {};
6011 // Parent constructor
6012 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
6014 // Mixin constructors
6015 OO
.ui
.mixin
.PopupElement
.call( this, config
);
6018 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
6021 this.connect( this, { click
: 'onAction' } );
6025 .addClass( 'oo-ui-popupButtonWidget' );
6027 .addClass( 'oo-ui-popupButtonWidget-popup' )
6028 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
6029 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
6030 this.$overlay
.append( this.popup
.$element
);
6035 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
6036 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
6041 * Handle the button action being triggered.
6045 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
6046 this.popup
.toggle();
6050 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
6052 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
6057 * @mixins OO.ui.mixin.GroupElement
6060 * @param {Object} [config] Configuration options
6062 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
6063 // Mixin constructors
6064 OO
.ui
.mixin
.GroupElement
.call( this, config
);
6069 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
6074 * Set the disabled state of the widget.
6076 * This will also update the disabled state of child widgets.
6078 * @param {boolean} disabled Disable widget
6080 * @return {OO.ui.Widget} The widget, for chaining
6082 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
6086 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
6087 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
6089 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
6091 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6092 this.items
[ i
].updateDisabled();
6100 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
6102 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
6103 * allows bidirectional communication.
6105 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
6113 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
6120 * Check if widget is disabled.
6122 * Checks parent if present, making disabled state inheritable.
6124 * @return {boolean} Widget is disabled
6126 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
6127 return this.disabled
||
6128 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
6132 * Set group element is in.
6134 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
6136 * @return {OO.ui.Widget} The widget, for chaining
6138 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
6140 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
6141 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
6143 // Initialize item disabled states
6144 this.updateDisabled();
6150 * OptionWidgets are special elements that can be selected and configured with data. The
6151 * data is often unique for each option, but it does not have to be. OptionWidgets are used
6152 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6153 * and examples, please see the [OOUI documentation on MediaWiki][1].
6155 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6158 * @extends OO.ui.Widget
6159 * @mixins OO.ui.mixin.ItemWidget
6160 * @mixins OO.ui.mixin.LabelElement
6161 * @mixins OO.ui.mixin.FlaggedElement
6162 * @mixins OO.ui.mixin.AccessKeyedElement
6163 * @mixins OO.ui.mixin.TitledElement
6166 * @param {Object} [config] Configuration options
6168 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
6169 // Configuration initialization
6170 config
= config
|| {};
6172 // Parent constructor
6173 OO
.ui
.OptionWidget
.parent
.call( this, config
);
6175 // Mixin constructors
6176 OO
.ui
.mixin
.ItemWidget
.call( this );
6177 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6178 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6179 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6180 OO
.ui
.mixin
.TitledElement
.call( this, config
);
6183 this.selected
= false;
6184 this.highlighted
= false;
6185 this.pressed
= false;
6189 .data( 'oo-ui-optionWidget', this )
6190 // Allow programmatic focussing (and by accesskey), but not tabbing
6191 .attr( 'tabindex', '-1' )
6192 .attr( 'role', 'option' )
6193 .attr( 'aria-selected', 'false' )
6194 .addClass( 'oo-ui-optionWidget' )
6195 .append( this.$label
);
6200 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6201 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6202 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6203 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6204 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6205 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.TitledElement
);
6207 /* Static Properties */
6210 * Whether this option can be selected. See #setSelected.
6214 * @property {boolean}
6216 OO
.ui
.OptionWidget
.static.selectable
= true;
6219 * Whether this option can be highlighted. See #setHighlighted.
6223 * @property {boolean}
6225 OO
.ui
.OptionWidget
.static.highlightable
= true;
6228 * Whether this option can be pressed. See #setPressed.
6232 * @property {boolean}
6234 OO
.ui
.OptionWidget
.static.pressable
= true;
6237 * Whether this option will be scrolled into view when it is selected.
6241 * @property {boolean}
6243 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6248 * Check if the option can be selected.
6250 * @return {boolean} Item is selectable
6252 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6253 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6257 * Check if the option can be highlighted. A highlight indicates that the option
6258 * may be selected when a user presses enter or clicks. Disabled items cannot
6261 * @return {boolean} Item is highlightable
6263 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6264 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6268 * Check if the option can be pressed. The pressed state occurs when a user mouses
6269 * down on an item, but has not yet let go of the mouse.
6271 * @return {boolean} Item is pressable
6273 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6274 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6278 * Check if the option is selected.
6280 * @return {boolean} Item is selected
6282 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6283 return this.selected
;
6287 * Check if the option is highlighted. A highlight indicates that the
6288 * item may be selected when a user presses enter or clicks.
6290 * @return {boolean} Item is highlighted
6292 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6293 return this.highlighted
;
6297 * Check if the option is pressed. The pressed state occurs when a user mouses
6298 * down on an item, but has not yet let go of the mouse. The item may appear
6299 * selected, but it will not be selected until the user releases the mouse.
6301 * @return {boolean} Item is pressed
6303 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6304 return this.pressed
;
6308 * Set the option’s selected state. In general, all modifications to the selection
6309 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6310 * method instead of this method.
6312 * @param {boolean} [state=false] Select option
6314 * @return {OO.ui.Widget} The widget, for chaining
6316 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6317 if ( this.constructor.static.selectable
) {
6318 this.selected
= !!state
;
6320 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6321 .attr( 'aria-selected', state
.toString() );
6322 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6323 this.scrollElementIntoView();
6325 this.updateThemeClasses();
6331 * Set the option’s highlighted state. In general, all programmatic
6332 * modifications to the highlight should be handled by the
6333 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6334 * method instead of this method.
6336 * @param {boolean} [state=false] Highlight option
6338 * @return {OO.ui.Widget} The widget, for chaining
6340 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6341 if ( this.constructor.static.highlightable
) {
6342 this.highlighted
= !!state
;
6343 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6344 this.updateThemeClasses();
6350 * Set the option’s pressed state. In general, all
6351 * programmatic modifications to the pressed state should be handled by the
6352 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6353 * method instead of this method.
6355 * @param {boolean} [state=false] Press option
6357 * @return {OO.ui.Widget} The widget, for chaining
6359 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6360 if ( this.constructor.static.pressable
) {
6361 this.pressed
= !!state
;
6362 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6363 this.updateThemeClasses();
6369 * Get text to match search strings against.
6371 * The default implementation returns the label text, but subclasses
6372 * can override this to provide more complex behavior.
6374 * @return {string|boolean} String to match search string against
6376 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6377 var label
= this.getLabel();
6378 return typeof label
=== 'string' ? label
: this.$label
.text();
6382 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6383 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6384 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6387 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
6388 * information, please see the [OOUI documentation on MediaWiki][1].
6391 * // A select widget with three options.
6392 * var select = new OO.ui.SelectWidget( {
6394 * new OO.ui.OptionWidget( {
6396 * label: 'Option One',
6398 * new OO.ui.OptionWidget( {
6400 * label: 'Option Two',
6402 * new OO.ui.OptionWidget( {
6404 * label: 'Option Three',
6408 * $( document.body ).append( select.$element );
6410 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6414 * @extends OO.ui.Widget
6415 * @mixins OO.ui.mixin.GroupWidget
6418 * @param {Object} [config] Configuration options
6419 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6420 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6421 * the [OOUI documentation on MediaWiki] [2] for examples.
6422 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6424 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6425 // Configuration initialization
6426 config
= config
|| {};
6428 // Parent constructor
6429 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6431 // Mixin constructors
6432 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
6435 this.pressed
= false;
6436 this.selecting
= null;
6437 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
6438 this.onDocumentMouseMoveHandler
= this.onDocumentMouseMove
.bind( this );
6439 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
6440 this.onDocumentKeyPressHandler
= this.onDocumentKeyPress
.bind( this );
6441 this.keyPressBuffer
= '';
6442 this.keyPressBufferTimer
= null;
6443 this.blockMouseOverEvents
= 0;
6446 this.connect( this, {
6450 focusin
: this.onFocus
.bind( this ),
6451 mousedown
: this.onMouseDown
.bind( this ),
6452 mouseover
: this.onMouseOver
.bind( this ),
6453 mouseleave
: this.onMouseLeave
.bind( this )
6458 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
6459 .attr( 'role', 'listbox' );
6460 this.setFocusOwner( this.$element
);
6461 if ( Array
.isArray( config
.items
) ) {
6462 this.addItems( config
.items
);
6468 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6469 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6476 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6478 * @param {OO.ui.OptionWidget|null} item Highlighted item
6484 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6485 * pressed state of an option.
6487 * @param {OO.ui.OptionWidget|null} item Pressed item
6493 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
6495 * @param {OO.ui.OptionWidget|null} item Selected item
6500 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6501 * @param {OO.ui.OptionWidget} item Chosen item
6507 * An `add` event is emitted when options are added to the select with the #addItems method.
6509 * @param {OO.ui.OptionWidget[]} items Added items
6510 * @param {number} index Index of insertion point
6516 * A `remove` event is emitted when options are removed from the select with the #clearItems
6517 * or #removeItems methods.
6519 * @param {OO.ui.OptionWidget[]} items Removed items
6525 * Handle focus events
6528 * @param {jQuery.Event} event
6530 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6532 if ( event
.target
=== this.$element
[ 0 ] ) {
6533 // This widget was focussed, e.g. by the user tabbing to it.
6534 // The styles for focus state depend on one of the items being selected.
6535 if ( !this.findSelectedItem() ) {
6536 item
= this.findFirstSelectableItem();
6539 if ( event
.target
.tabIndex
=== -1 ) {
6540 // One of the options got focussed (and the event bubbled up here).
6541 // They can't be tabbed to, but they can be activated using accesskeys.
6542 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6543 item
= this.findTargetItem( event
);
6545 // There is something actually user-focusable in one of the labels of the options, and the
6546 // user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change the focus).
6552 if ( item
.constructor.static.highlightable
) {
6553 this.highlightItem( item
);
6555 this.selectItem( item
);
6559 if ( event
.target
!== this.$element
[ 0 ] ) {
6560 this.$focusOwner
.focus();
6565 * Handle mouse down events.
6568 * @param {jQuery.Event} e Mouse down event
6569 * @return {undefined/boolean} False to prevent default if event is handled
6571 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6574 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6575 this.togglePressed( true );
6576 item
= this.findTargetItem( e
);
6577 if ( item
&& item
.isSelectable() ) {
6578 this.pressItem( item
);
6579 this.selecting
= item
;
6580 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6581 this.getElementDocument().addEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6588 * Handle document mouse up events.
6591 * @param {MouseEvent} e Mouse up event
6592 * @return {undefined/boolean} False to prevent default if event is handled
6594 OO
.ui
.SelectWidget
.prototype.onDocumentMouseUp = function ( e
) {
6597 this.togglePressed( false );
6598 if ( !this.selecting
) {
6599 item
= this.findTargetItem( e
);
6600 if ( item
&& item
.isSelectable() ) {
6601 this.selecting
= item
;
6604 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6605 this.pressItem( null );
6606 this.chooseItem( this.selecting
);
6607 this.selecting
= null;
6610 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6611 this.getElementDocument().removeEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6616 // Deprecated alias since 0.28.3
6617 OO
.ui
.SelectWidget
.prototype.onMouseUp = function () {
6618 OO
.ui
.warnDeprecation( 'onMouseUp is deprecated, use onDocumentMouseUp instead' );
6619 this.onDocumentMouseUp
.apply( this, arguments
);
6623 * Handle document mouse move events.
6626 * @param {MouseEvent} e Mouse move event
6628 OO
.ui
.SelectWidget
.prototype.onDocumentMouseMove = function ( e
) {
6631 if ( !this.isDisabled() && this.pressed
) {
6632 item
= this.findTargetItem( e
);
6633 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6634 this.pressItem( item
);
6635 this.selecting
= item
;
6640 // Deprecated alias since 0.28.3
6641 OO
.ui
.SelectWidget
.prototype.onMouseMove = function () {
6642 OO
.ui
.warnDeprecation( 'onMouseMove is deprecated, use onDocumentMouseMove instead' );
6643 this.onDocumentMouseMove
.apply( this, arguments
);
6647 * Handle mouse over events.
6650 * @param {jQuery.Event} e Mouse over event
6651 * @return {undefined/boolean} False to prevent default if event is handled
6653 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6655 if ( this.blockMouseOverEvents
) {
6658 if ( !this.isDisabled() ) {
6659 item
= this.findTargetItem( e
);
6660 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6666 * Handle mouse leave events.
6669 * @param {jQuery.Event} e Mouse over event
6670 * @return {undefined/boolean} False to prevent default if event is handled
6672 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6673 if ( !this.isDisabled() ) {
6674 this.highlightItem( null );
6680 * Handle document key down events.
6683 * @param {KeyboardEvent} e Key down event
6685 OO
.ui
.SelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
6688 currentItem
= this.findHighlightedItem() || this.findSelectedItem();
6690 if ( !this.isDisabled() && this.isVisible() ) {
6691 switch ( e
.keyCode
) {
6692 case OO
.ui
.Keys
.ENTER
:
6693 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6694 // Was only highlighted, now let's select it. No-op if already selected.
6695 this.chooseItem( currentItem
);
6700 case OO
.ui
.Keys
.LEFT
:
6701 this.clearKeyPressBuffer();
6702 nextItem
= this.findRelativeSelectableItem( currentItem
, -1 );
6705 case OO
.ui
.Keys
.DOWN
:
6706 case OO
.ui
.Keys
.RIGHT
:
6707 this.clearKeyPressBuffer();
6708 nextItem
= this.findRelativeSelectableItem( currentItem
, 1 );
6711 case OO
.ui
.Keys
.ESCAPE
:
6712 case OO
.ui
.Keys
.TAB
:
6713 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6714 currentItem
.setHighlighted( false );
6716 this.unbindDocumentKeyDownListener();
6717 this.unbindDocumentKeyPressListener();
6718 // Don't prevent tabbing away / defocusing
6724 if ( nextItem
.constructor.static.highlightable
) {
6725 this.highlightItem( nextItem
);
6727 this.chooseItem( nextItem
);
6729 this.scrollItemIntoView( nextItem
);
6734 e
.stopPropagation();
6739 // Deprecated alias since 0.28.3
6740 OO
.ui
.SelectWidget
.prototype.onKeyDown = function () {
6741 OO
.ui
.warnDeprecation( 'onKeyDown is deprecated, use onDocumentKeyDown instead' );
6742 this.onDocumentKeyDown
.apply( this, arguments
);
6746 * Bind document key down listener.
6750 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyDownListener = function () {
6751 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6754 // Deprecated alias since 0.28.3
6755 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
6756 OO
.ui
.warnDeprecation( 'bindKeyDownListener is deprecated, use bindDocumentKeyDownListener instead' );
6757 this.bindDocumentKeyDownListener
.apply( this, arguments
);
6761 * Unbind document key down listener.
6765 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
6766 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6769 // Deprecated alias since 0.28.3
6770 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
6771 OO
.ui
.warnDeprecation( 'unbindKeyDownListener is deprecated, use unbindDocumentKeyDownListener instead' );
6772 this.unbindDocumentKeyDownListener
.apply( this, arguments
);
6776 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6778 * @param {OO.ui.OptionWidget} item Item to scroll into view
6780 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6782 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
6783 // and around 100-150 ms after it is finished.
6784 this.blockMouseOverEvents
++;
6785 item
.scrollElementIntoView().done( function () {
6786 setTimeout( function () {
6787 widget
.blockMouseOverEvents
--;
6793 * Clear the key-press buffer
6797 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6798 if ( this.keyPressBufferTimer
) {
6799 clearTimeout( this.keyPressBufferTimer
);
6800 this.keyPressBufferTimer
= null;
6802 this.keyPressBuffer
= '';
6806 * Handle key press events.
6809 * @param {KeyboardEvent} e Key press event
6810 * @return {undefined/boolean} False to prevent default if event is handled
6812 OO
.ui
.SelectWidget
.prototype.onDocumentKeyPress = function ( e
) {
6813 var c
, filter
, item
;
6815 if ( !e
.charCode
) {
6816 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6817 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6822 // eslint-disable-next-line no-restricted-properties
6823 if ( String
.fromCodePoint
) {
6824 // eslint-disable-next-line no-restricted-properties
6825 c
= String
.fromCodePoint( e
.charCode
);
6827 c
= String
.fromCharCode( e
.charCode
);
6830 if ( this.keyPressBufferTimer
) {
6831 clearTimeout( this.keyPressBufferTimer
);
6833 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6835 item
= this.findHighlightedItem() || this.findSelectedItem();
6837 if ( this.keyPressBuffer
=== c
) {
6838 // Common (if weird) special case: typing "xxxx" will cycle through all
6839 // the items beginning with "x".
6841 item
= this.findRelativeSelectableItem( item
, 1 );
6844 this.keyPressBuffer
+= c
;
6847 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6848 if ( !item
|| !filter( item
) ) {
6849 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6852 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6853 this.highlightItem( item
);
6855 this.chooseItem( item
);
6857 this.scrollItemIntoView( item
);
6861 e
.stopPropagation();
6864 // Deprecated alias since 0.28.3
6865 OO
.ui
.SelectWidget
.prototype.onKeyPress = function () {
6866 OO
.ui
.warnDeprecation( 'onKeyPress is deprecated, use onDocumentKeyPress instead' );
6867 this.onDocumentKeyPress
.apply( this, arguments
);
6871 * Get a matcher for the specific string
6874 * @param {string} s String to match against items
6875 * @param {boolean} [exact=false] Only accept exact matches
6876 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6878 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
6881 // eslint-disable-next-line no-restricted-properties
6882 if ( s
.normalize
) {
6883 // eslint-disable-next-line no-restricted-properties
6886 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
6887 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
6891 re
= new RegExp( re
, 'i' );
6892 return function ( item
) {
6893 var matchText
= item
.getMatchText();
6894 // eslint-disable-next-line no-restricted-properties
6895 if ( matchText
.normalize
) {
6896 // eslint-disable-next-line no-restricted-properties
6897 matchText
= matchText
.normalize();
6899 return re
.test( matchText
);
6904 * Bind document key press listener.
6908 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyPressListener = function () {
6909 this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
6912 // Deprecated alias since 0.28.3
6913 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
6914 OO
.ui
.warnDeprecation( 'bindKeyPressListener is deprecated, use bindDocumentKeyPressListener instead' );
6915 this.bindDocumentKeyPressListener
.apply( this, arguments
);
6919 * Unbind document key down listener.
6921 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6926 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
6927 this.getElementDocument().removeEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
6928 this.clearKeyPressBuffer();
6931 // Deprecated alias since 0.28.3
6932 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
6933 OO
.ui
.warnDeprecation( 'unbindKeyPressListener is deprecated, use unbindDocumentKeyPressListener instead' );
6934 this.unbindDocumentKeyPressListener
.apply( this, arguments
);
6938 * Visibility change handler
6941 * @param {boolean} visible
6943 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6945 this.clearKeyPressBuffer();
6950 * Get the closest item to a jQuery.Event.
6953 * @param {jQuery.Event} e
6954 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6956 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
6957 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
6958 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
6961 return $option
.data( 'oo-ui-optionWidget' ) || null;
6965 * Find selected item.
6967 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6969 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
6972 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6973 if ( this.items
[ i
].isSelected() ) {
6974 return this.items
[ i
];
6981 * Find highlighted item.
6983 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6985 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
6988 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6989 if ( this.items
[ i
].isHighlighted() ) {
6990 return this.items
[ i
];
6997 * Toggle pressed state.
6999 * Press is a state that occurs when a user mouses down on an item, but
7000 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
7001 * until the user releases the mouse.
7003 * @param {boolean} pressed An option is being pressed
7005 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
7006 if ( pressed
=== undefined ) {
7007 pressed
= !this.pressed
;
7009 if ( pressed
!== this.pressed
) {
7011 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
7012 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
7013 this.pressed
= pressed
;
7018 * Highlight an option. If the `item` param is omitted, no options will be highlighted
7019 * and any existing highlight will be removed. The highlight is mutually exclusive.
7021 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
7024 * @return {OO.ui.Widget} The widget, for chaining
7026 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
7027 var i
, len
, highlighted
,
7030 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7031 highlighted
= this.items
[ i
] === item
;
7032 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
7033 this.items
[ i
].setHighlighted( highlighted
);
7039 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7041 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7043 this.emit( 'highlight', item
);
7050 * Fetch an item by its label.
7052 * @param {string} label Label of the item to select.
7053 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7054 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
7056 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
7058 len
= this.items
.length
,
7059 filter
= this.getItemMatcher( label
, true );
7061 for ( i
= 0; i
< len
; i
++ ) {
7062 item
= this.items
[ i
];
7063 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7070 filter
= this.getItemMatcher( label
, false );
7071 for ( i
= 0; i
< len
; i
++ ) {
7072 item
= this.items
[ i
];
7073 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7089 * Programmatically select an option by its label. If the item does not exist,
7090 * all options will be deselected.
7092 * @param {string} [label] Label of the item to select.
7093 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7096 * @return {OO.ui.Widget} The widget, for chaining
7098 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
7099 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
7100 if ( label
=== undefined || !itemFromLabel
) {
7101 return this.selectItem();
7103 return this.selectItem( itemFromLabel
);
7107 * Programmatically select an option by its data. If the `data` parameter is omitted,
7108 * or if the item does not exist, all options will be deselected.
7110 * @param {Object|string} [data] Value of the item to select, omit to deselect all
7113 * @return {OO.ui.Widget} The widget, for chaining
7115 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
7116 var itemFromData
= this.findItemFromData( data
);
7117 if ( data
=== undefined || !itemFromData
) {
7118 return this.selectItem();
7120 return this.selectItem( itemFromData
);
7124 * Programmatically select an option by its reference. If the `item` parameter is omitted,
7125 * all options will be deselected.
7127 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
7130 * @return {OO.ui.Widget} The widget, for chaining
7132 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
7133 var i
, len
, selected
,
7136 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7137 selected
= this.items
[ i
] === item
;
7138 if ( this.items
[ i
].isSelected() !== selected
) {
7139 this.items
[ i
].setSelected( selected
);
7144 if ( item
&& !item
.constructor.static.highlightable
) {
7146 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7148 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7151 this.emit( 'select', item
);
7160 * Press is a state that occurs when a user mouses down on an item, but has not
7161 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
7162 * releases the mouse.
7164 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
7167 * @return {OO.ui.Widget} The widget, for chaining
7169 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
7170 var i
, len
, pressed
,
7173 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7174 pressed
= this.items
[ i
] === item
;
7175 if ( this.items
[ i
].isPressed() !== pressed
) {
7176 this.items
[ i
].setPressed( pressed
);
7181 this.emit( 'press', item
);
7190 * Note that ‘choose’ should never be modified programmatically. A user can choose
7191 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
7192 * use the #selectItem method.
7194 * This method is identical to #selectItem, but may vary in subclasses that take additional action
7195 * when users choose an item with the keyboard or mouse.
7197 * @param {OO.ui.OptionWidget} item Item to choose
7200 * @return {OO.ui.Widget} The widget, for chaining
7202 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
7204 this.selectItem( item
);
7205 this.emit( 'choose', item
);
7212 * Find an option by its position relative to the specified item (or to the start of the option array,
7213 * if item is `null`). The direction in which to search through the option array is specified with a
7214 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
7215 * `null` if there are no options in the array.
7217 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
7218 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7219 * @param {Function} [filter] Only consider items for which this function returns
7220 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
7221 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
7223 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
7224 var currentIndex
, nextIndex
, i
,
7225 increase
= direction
> 0 ? 1 : -1,
7226 len
= this.items
.length
;
7228 if ( item
instanceof OO
.ui
.OptionWidget
) {
7229 currentIndex
= this.items
.indexOf( item
);
7230 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7232 // If no item is selected and moving forward, start at the beginning.
7233 // If moving backward, start at the end.
7234 nextIndex
= direction
> 0 ? 0 : len
- 1;
7237 for ( i
= 0; i
< len
; i
++ ) {
7238 item
= this.items
[ nextIndex
];
7240 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
7241 ( !filter
|| filter( item
) )
7245 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7251 * Find the next selectable item or `null` if there are no selectable items.
7252 * Disabled options and menu-section markers and breaks are not selectable.
7254 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7256 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7257 return this.findRelativeSelectableItem( null, 1 );
7261 * Add an array of options to the select. Optionally, an index number can be used to
7262 * specify an insertion point.
7264 * @param {OO.ui.OptionWidget[]} items Items to add
7265 * @param {number} [index] Index to insert items after
7268 * @return {OO.ui.Widget} The widget, for chaining
7270 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7272 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7274 // Always provide an index, even if it was omitted
7275 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7281 * Remove the specified array of options from the select. Options will be detached
7282 * from the DOM, not removed, so they can be reused later. To remove all options from
7283 * the select, you may wish to use the #clearItems method instead.
7285 * @param {OO.ui.OptionWidget[]} items Items to remove
7288 * @return {OO.ui.Widget} The widget, for chaining
7290 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7293 // Deselect items being removed
7294 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7296 if ( item
.isSelected() ) {
7297 this.selectItem( null );
7302 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7304 this.emit( 'remove', items
);
7310 * Clear all options from the select. Options will be detached from the DOM, not removed,
7311 * so that they can be reused later. To remove a subset of options from the select, use
7312 * the #removeItems method.
7316 * @return {OO.ui.Widget} The widget, for chaining
7318 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7319 var items
= this.items
.slice();
7322 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7325 this.selectItem( null );
7327 this.emit( 'remove', items
);
7333 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7335 * This is used to set `aria-activedescendant` and `aria-expanded` on it.
7338 * @param {jQuery} $focusOwner
7340 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7341 this.$focusOwner
= $focusOwner
;
7345 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7346 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
7347 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7348 * options. For more information about options and selects, please see the
7349 * [OOUI documentation on MediaWiki][1].
7352 * // Decorated options in a select widget.
7353 * var select = new OO.ui.SelectWidget( {
7355 * new OO.ui.DecoratedOptionWidget( {
7357 * label: 'Option with icon',
7360 * new OO.ui.DecoratedOptionWidget( {
7362 * label: 'Option with indicator',
7367 * $( document.body ).append( select.$element );
7369 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7372 * @extends OO.ui.OptionWidget
7373 * @mixins OO.ui.mixin.IconElement
7374 * @mixins OO.ui.mixin.IndicatorElement
7377 * @param {Object} [config] Configuration options
7379 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7380 // Parent constructor
7381 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7383 // Mixin constructors
7384 OO
.ui
.mixin
.IconElement
.call( this, config
);
7385 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7389 .addClass( 'oo-ui-decoratedOptionWidget' )
7390 .prepend( this.$icon
)
7391 .append( this.$indicator
);
7396 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7397 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7398 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7401 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7402 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7403 * the [OOUI documentation on MediaWiki] [1] for more information.
7405 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7408 * @extends OO.ui.DecoratedOptionWidget
7411 * @param {Object} [config] Configuration options
7413 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7414 // Parent constructor
7415 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7418 this.checkIcon
= new OO
.ui
.IconWidget( {
7420 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7425 .prepend( this.checkIcon
.$element
)
7426 .addClass( 'oo-ui-menuOptionWidget' );
7431 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7433 /* Static Properties */
7439 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7442 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
7443 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
7446 * var dropdown = new OO.ui.DropdownWidget( {
7449 * new OO.ui.MenuSectionOptionWidget( {
7452 * new OO.ui.MenuOptionWidget( {
7454 * label: 'Welsh Corgi'
7456 * new OO.ui.MenuOptionWidget( {
7458 * label: 'Standard Poodle'
7460 * new OO.ui.MenuSectionOptionWidget( {
7463 * new OO.ui.MenuOptionWidget( {
7470 * $( document.body ).append( dropdown.$element );
7473 * @extends OO.ui.DecoratedOptionWidget
7476 * @param {Object} [config] Configuration options
7478 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7479 // Parent constructor
7480 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7483 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' )
7484 .removeAttr( 'role aria-selected' );
7489 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7491 /* Static Properties */
7497 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7503 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7506 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7507 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7508 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
7509 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7510 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7511 * and customized to be opened, closed, and displayed as needed.
7513 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7514 * mouse outside the menu.
7516 * Menus also have support for keyboard interaction:
7518 * - Enter/Return key: choose and select a menu option
7519 * - Up-arrow key: highlight the previous menu option
7520 * - Down-arrow key: highlight the next menu option
7521 * - Esc key: hide the menu
7523 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7525 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7526 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7529 * @extends OO.ui.SelectWidget
7530 * @mixins OO.ui.mixin.ClippableElement
7531 * @mixins OO.ui.mixin.FloatableElement
7534 * @param {Object} [config] Configuration options
7535 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
7536 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
7537 * and {@link OO.ui.mixin.LookupElement LookupElement}
7538 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7539 * the text the user types. This config is used by {@link OO.ui.TagMultiselectWidget TagMultiselectWidget}
7540 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
7541 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
7542 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
7543 * that button, unless the button (or its parent widget) is passed in here.
7544 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7545 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7546 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7547 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7548 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7549 * @cfg {number} [width] Width of the menu
7551 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7552 // Configuration initialization
7553 config
= config
|| {};
7555 // Parent constructor
7556 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7558 // Mixin constructors
7559 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
7560 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7562 // Initial vertical positions other than 'center' will result in
7563 // the menu being flipped if there is not enough space in the container.
7564 // Store the original position so we know what to reset to.
7565 this.originalVerticalPosition
= this.verticalPosition
;
7568 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7569 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7570 this.filterFromInput
= !!config
.filterFromInput
;
7571 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7572 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7573 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7574 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7575 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7576 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7577 this.width
= config
.width
;
7580 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7581 if ( config
.widget
) {
7582 this.setFocusOwner( config
.widget
.$tabIndexed
);
7585 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7586 // that reference properties not initialized at that time of parent class construction
7587 // TODO: Find a better way to handle post-constructor setup
7588 this.visible
= false;
7589 this.$element
.addClass( 'oo-ui-element-hidden' );
7590 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7595 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7596 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7597 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7604 * The menu is ready: it is visible and has been positioned and clipped.
7607 /* Static properties */
7610 * Positions to flip to if there isn't room in the container for the
7611 * menu in a specific direction.
7613 * @property {Object.<string,string>}
7615 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7625 * Handles document mouse down events.
7628 * @param {MouseEvent} e Mouse down event
7630 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7634 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7639 this.toggle( false );
7646 OO
.ui
.MenuSelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
7647 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7649 if ( !this.isDisabled() && this.isVisible() ) {
7650 switch ( e
.keyCode
) {
7651 case OO
.ui
.Keys
.LEFT
:
7652 case OO
.ui
.Keys
.RIGHT
:
7653 // Do nothing if a text field is associated, arrow keys will be handled natively
7654 if ( !this.$input
) {
7655 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7658 case OO
.ui
.Keys
.ESCAPE
:
7659 case OO
.ui
.Keys
.TAB
:
7660 if ( currentItem
) {
7661 currentItem
.setHighlighted( false );
7663 this.toggle( false );
7664 // Don't prevent tabbing away, prevent defocusing
7665 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7667 e
.stopPropagation();
7671 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7678 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7679 * or after items were added/removed (always).
7683 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7684 var i
, item
, items
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7686 len
= this.items
.length
,
7687 showAll
= !this.isVisible(),
7690 if ( this.$input
&& this.filterFromInput
) {
7691 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
7692 exactFilter
= this.getItemMatcher( this.$input
.val(), true );
7693 // Hide non-matching options, and also hide section headers if all options
7694 // in their section are hidden.
7695 for ( i
= 0; i
< len
; i
++ ) {
7696 item
= this.items
[ i
];
7697 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7699 // If the previous section was empty, hide its header
7700 section
.toggle( showAll
|| !sectionEmpty
);
7703 sectionEmpty
= true;
7704 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7705 visible
= showAll
|| filter( item
);
7706 exactMatch
= exactMatch
|| exactFilter( item
);
7707 anyVisible
= anyVisible
|| visible
;
7708 sectionEmpty
= sectionEmpty
&& !visible
;
7709 item
.toggle( visible
);
7712 // Process the final section
7714 section
.toggle( showAll
|| !sectionEmpty
);
7717 if ( anyVisible
&& this.items
.length
&& !exactMatch
) {
7718 this.scrollItemIntoView( this.items
[ 0 ] );
7721 if ( !anyVisible
) {
7722 this.highlightItem( null );
7725 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7727 if ( this.highlightOnFilter
) {
7728 // Highlight the first item on the list
7730 items
= this.getItems();
7731 for ( i
= 0; i
< items
.length
; i
++ ) {
7732 if ( items
[ i
].isVisible() ) {
7737 this.highlightItem( item
);
7742 // Reevaluate clipping
7749 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyDownListener = function () {
7750 if ( this.$input
) {
7751 this.$input
.on( 'keydown', this.onDocumentKeyDownHandler
);
7753 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyDownListener
.call( this );
7760 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
7761 if ( this.$input
) {
7762 this.$input
.off( 'keydown', this.onDocumentKeyDownHandler
);
7764 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyDownListener
.call( this );
7771 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7772 if ( this.$input
) {
7773 if ( this.filterFromInput
) {
7774 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7775 this.updateItemVisibility();
7778 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyPressListener
.call( this );
7785 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7786 if ( this.$input
) {
7787 if ( this.filterFromInput
) {
7788 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7789 this.updateItemVisibility();
7792 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyPressListener
.call( this );
7799 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
7801 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
7802 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
7804 * @param {OO.ui.OptionWidget} item Item to choose
7806 * @return {OO.ui.Widget} The widget, for chaining
7808 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7809 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7810 if ( this.hideOnChoose
) {
7811 this.toggle( false );
7819 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7821 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7823 this.updateItemVisibility();
7831 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7833 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7835 this.updateItemVisibility();
7843 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7845 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
7847 this.updateItemVisibility();
7853 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7854 * `.toggle( true )` after its #$element is attached to the DOM.
7856 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7857 * it in the right place and with the right dimensions only work correctly while it is attached.
7858 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7859 * strictly enforced, so currently it only generates a warning in the browser console.
7864 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7865 var change
, originalHeight
, flippedHeight
;
7867 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7868 change
= visible
!== this.isVisible();
7870 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7871 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7872 this.warnedUnattached
= true;
7875 if ( change
&& visible
) {
7876 // Reset position before showing the popup again. It's possible we no longer need to flip
7877 // (e.g. if the user scrolled).
7878 this.setVerticalPosition( this.originalVerticalPosition
);
7882 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7888 this.setIdealSize( this.width
);
7889 } else if ( this.$floatableContainer
) {
7890 this.$clippable
.css( 'width', 'auto' );
7892 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
7893 // Dropdown is smaller than handle so expand to width
7894 this.$floatableContainer
[ 0 ].offsetWidth
:
7895 // Dropdown is larger than handle so auto size
7898 this.$clippable
.css( 'width', '' );
7901 this.togglePositioning( !!this.$floatableContainer
);
7902 this.toggleClipping( true );
7904 this.bindDocumentKeyDownListener();
7905 this.bindDocumentKeyPressListener();
7908 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
7909 this.originalVerticalPosition
!== 'center'
7911 // If opening the menu in one direction causes it to be clipped, flip it
7912 originalHeight
= this.$element
.height();
7913 this.setVerticalPosition(
7914 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
7916 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
7917 // If flipping also causes it to be clipped, open in whichever direction
7918 // we have more space
7919 flippedHeight
= this.$element
.height();
7920 if ( originalHeight
> flippedHeight
) {
7921 this.setVerticalPosition( this.originalVerticalPosition
);
7925 // Note that we do not flip the menu's opening direction if the clipping changes
7926 // later (e.g. after the user scrolls), that seems like it would be annoying
7928 this.$focusOwner
.attr( 'aria-expanded', 'true' );
7930 if ( this.findSelectedItem() ) {
7931 this.$focusOwner
.attr( 'aria-activedescendant', this.findSelectedItem().getElementId() );
7932 this.findSelectedItem().scrollElementIntoView( { duration
: 0 } );
7936 if ( this.autoHide
) {
7937 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7940 this.emit( 'ready' );
7942 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7943 this.unbindDocumentKeyDownListener();
7944 this.unbindDocumentKeyPressListener();
7945 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7946 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7947 this.togglePositioning( false );
7948 this.toggleClipping( false );
7956 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
7957 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
7958 * users can interact with it.
7960 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7961 * OO.ui.DropdownInputWidget instead.
7964 * // A DropdownWidget with a menu that contains three options.
7965 * var dropDown = new OO.ui.DropdownWidget( {
7966 * label: 'Dropdown menu: Select a menu option',
7969 * new OO.ui.MenuOptionWidget( {
7973 * new OO.ui.MenuOptionWidget( {
7977 * new OO.ui.MenuOptionWidget( {
7985 * $( document.body ).append( dropDown.$element );
7987 * dropDown.getMenu().selectItemByData( 'b' );
7989 * dropDown.getMenu().findSelectedItem().getData(); // Returns 'b'.
7991 * For more information, please see the [OOUI documentation on MediaWiki] [1].
7993 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7996 * @extends OO.ui.Widget
7997 * @mixins OO.ui.mixin.IconElement
7998 * @mixins OO.ui.mixin.IndicatorElement
7999 * @mixins OO.ui.mixin.LabelElement
8000 * @mixins OO.ui.mixin.TitledElement
8001 * @mixins OO.ui.mixin.TabIndexedElement
8004 * @param {Object} [config] Configuration options
8005 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.MenuSelectWidget menu select widget}
8006 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
8007 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
8008 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
8009 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
8011 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
8012 // Configuration initialization
8013 config
= $.extend( { indicator
: 'down' }, config
);
8015 // Parent constructor
8016 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
8018 // Properties (must be set before TabIndexedElement constructor call)
8019 this.$handle
= $( '<button>' );
8020 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
8022 // Mixin constructors
8023 OO
.ui
.mixin
.IconElement
.call( this, config
);
8024 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8025 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8026 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
8027 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
8030 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
8032 $floatableContainer
: this.$element
8037 click
: this.onClick
.bind( this ),
8038 keydown
: this.onKeyDown
.bind( this ),
8039 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
8040 keypress
: this.menu
.onDocumentKeyPressHandler
,
8041 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
8043 this.menu
.connect( this, {
8044 select
: 'onMenuSelect',
8045 toggle
: 'onMenuToggle'
8050 .addClass( 'oo-ui-dropdownWidget-handle' )
8053 'aria-owns': this.menu
.getElementId(),
8054 'aria-haspopup': 'listbox'
8056 .append( this.$icon
, this.$label
, this.$indicator
);
8058 .addClass( 'oo-ui-dropdownWidget' )
8059 .append( this.$handle
);
8060 this.$overlay
.append( this.menu
.$element
);
8065 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
8066 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
8067 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
8068 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
8069 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
8070 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8077 * @return {OO.ui.MenuSelectWidget} Menu of widget
8079 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
8084 * Handles menu select events.
8087 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8089 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
8093 this.setLabel( null );
8097 selectedLabel
= item
.getLabel();
8099 // If the label is a DOM element, clone it, because setLabel will append() it
8100 if ( selectedLabel
instanceof $ ) {
8101 selectedLabel
= selectedLabel
.clone();
8104 this.setLabel( selectedLabel
);
8108 * Handle menu toggle events.
8111 * @param {boolean} isVisible Open state of the menu
8113 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
8114 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
8118 * Handle mouse click events.
8121 * @param {jQuery.Event} e Mouse click event
8122 * @return {undefined/boolean} False to prevent default if event is handled
8124 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
8125 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8132 * Handle key down events.
8135 * @param {jQuery.Event} e Key down event
8136 * @return {undefined/boolean} False to prevent default if event is handled
8138 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
8140 !this.isDisabled() &&
8142 e
.which
=== OO
.ui
.Keys
.ENTER
||
8144 e
.which
=== OO
.ui
.Keys
.SPACE
&&
8145 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
8146 // Space only closes the menu is the user is not typing to search.
8147 this.menu
.keyPressBuffer
=== ''
8150 !this.menu
.isVisible() &&
8152 e
.which
=== OO
.ui
.Keys
.UP
||
8153 e
.which
=== OO
.ui
.Keys
.DOWN
8164 * RadioOptionWidget is an option widget that looks like a radio button.
8165 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
8166 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8168 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8171 * @extends OO.ui.OptionWidget
8174 * @param {Object} [config] Configuration options
8176 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
8177 // Configuration initialization
8178 config
= config
|| {};
8180 // Properties (must be done before parent constructor which calls #setDisabled)
8181 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
8183 // Parent constructor
8184 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
8187 // Remove implicit role, we're handling it ourselves
8188 this.radio
.$input
.attr( 'role', 'presentation' );
8190 .addClass( 'oo-ui-radioOptionWidget' )
8191 .attr( 'role', 'radio' )
8192 .attr( 'aria-checked', 'false' )
8193 .removeAttr( 'aria-selected' )
8194 .prepend( this.radio
.$element
);
8199 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
8201 /* Static Properties */
8207 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
8213 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
8219 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
8225 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
8232 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
8233 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8235 this.radio
.setSelected( state
);
8237 .attr( 'aria-checked', state
.toString() )
8238 .removeAttr( 'aria-selected' );
8246 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
8247 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8249 this.radio
.setDisabled( this.isDisabled() );
8255 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8256 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8257 * an interface for adding, removing and selecting options.
8258 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8260 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8261 * OO.ui.RadioSelectInputWidget instead.
8264 * // A RadioSelectWidget with RadioOptions.
8265 * var option1 = new OO.ui.RadioOptionWidget( {
8267 * label: 'Selected radio option'
8269 * option2 = new OO.ui.RadioOptionWidget( {
8271 * label: 'Unselected radio option'
8273 * radioSelect = new OO.ui.RadioSelectWidget( {
8274 * items: [ option1, option2 ]
8277 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8278 * radioSelect.selectItem( option1 );
8280 * $( document.body ).append( radioSelect.$element );
8282 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8286 * @extends OO.ui.SelectWidget
8287 * @mixins OO.ui.mixin.TabIndexedElement
8290 * @param {Object} [config] Configuration options
8292 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8293 // Parent constructor
8294 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8296 // Mixin constructors
8297 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8301 focus
: this.bindDocumentKeyDownListener
.bind( this ),
8302 blur
: this.unbindDocumentKeyDownListener
.bind( this )
8307 .addClass( 'oo-ui-radioSelectWidget' )
8308 .attr( 'role', 'radiogroup' );
8313 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8314 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8317 * MultioptionWidgets are special elements that can be selected and configured with data. The
8318 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8319 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8320 * and examples, please see the [OOUI documentation on MediaWiki][1].
8322 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Multioptions
8325 * @extends OO.ui.Widget
8326 * @mixins OO.ui.mixin.ItemWidget
8327 * @mixins OO.ui.mixin.LabelElement
8328 * @mixins OO.ui.mixin.TitledElement
8331 * @param {Object} [config] Configuration options
8332 * @cfg {boolean} [selected=false] Whether the option is initially selected
8334 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8335 // Configuration initialization
8336 config
= config
|| {};
8338 // Parent constructor
8339 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8341 // Mixin constructors
8342 OO
.ui
.mixin
.ItemWidget
.call( this );
8343 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8344 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8347 this.selected
= null;
8351 .addClass( 'oo-ui-multioptionWidget' )
8352 .append( this.$label
);
8353 this.setSelected( config
.selected
);
8358 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8359 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8360 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8361 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.TitledElement
);
8368 * A change event is emitted when the selected state of the option changes.
8370 * @param {boolean} selected Whether the option is now selected
8376 * Check if the option is selected.
8378 * @return {boolean} Item is selected
8380 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8381 return this.selected
;
8385 * Set the option’s selected state. In general, all modifications to the selection
8386 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
8387 * method instead of this method.
8389 * @param {boolean} [state=false] Select option
8391 * @return {OO.ui.Widget} The widget, for chaining
8393 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8395 if ( this.selected
!== state
) {
8396 this.selected
= state
;
8397 this.emit( 'change', state
);
8398 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8404 * MultiselectWidget allows selecting multiple options from a list.
8406 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
8408 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8412 * @extends OO.ui.Widget
8413 * @mixins OO.ui.mixin.GroupWidget
8414 * @mixins OO.ui.mixin.TitledElement
8417 * @param {Object} [config] Configuration options
8418 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8420 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8421 // Parent constructor
8422 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8424 // Configuration initialization
8425 config
= config
|| {};
8427 // Mixin constructors
8428 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8429 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8432 this.aggregate( { change
: 'select' } );
8433 // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
8434 // by GroupElement only when items are added/removed
8435 this.connect( this, { select
: [ 'emit', 'change' ] } );
8438 if ( config
.items
) {
8439 this.addItems( config
.items
);
8441 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8442 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8443 .append( this.$group
);
8448 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8449 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8450 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.TitledElement
);
8457 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8463 * A select event is emitted when an item is selected or deselected.
8469 * Find options that are selected.
8471 * @return {OO.ui.MultioptionWidget[]} Selected options
8473 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8474 return this.items
.filter( function ( item
) {
8475 return item
.isSelected();
8480 * Find the data of options that are selected.
8482 * @return {Object[]|string[]} Values of selected options
8484 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8485 return this.findSelectedItems().map( function ( item
) {
8491 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8493 * @param {OO.ui.MultioptionWidget[]} items Items to select
8495 * @return {OO.ui.Widget} The widget, for chaining
8497 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8498 this.items
.forEach( function ( item
) {
8499 var selected
= items
.indexOf( item
) !== -1;
8500 item
.setSelected( selected
);
8506 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8508 * @param {Object[]|string[]} datas Values of items to select
8510 * @return {OO.ui.Widget} The widget, for chaining
8512 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8515 items
= datas
.map( function ( data
) {
8516 return widget
.findItemFromData( data
);
8518 this.selectItems( items
);
8523 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8524 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8525 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8527 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8530 * @extends OO.ui.MultioptionWidget
8533 * @param {Object} [config] Configuration options
8535 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8536 // Configuration initialization
8537 config
= config
|| {};
8539 // Properties (must be done before parent constructor which calls #setDisabled)
8540 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8542 // Parent constructor
8543 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8546 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8547 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8551 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8552 .prepend( this.checkbox
.$element
);
8557 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8559 /* Static Properties */
8565 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8570 * Handle checkbox selected state change.
8574 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8575 this.setSelected( this.checkbox
.isSelected() );
8581 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8582 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8583 this.checkbox
.setSelected( state
);
8590 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8591 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8592 this.checkbox
.setDisabled( this.isDisabled() );
8599 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8600 this.checkbox
.focus();
8604 * Handle key down events.
8607 * @param {jQuery.Event} e
8609 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8611 element
= this.getElementGroup(),
8614 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8615 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8616 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8617 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8627 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8628 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8629 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8630 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8632 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8633 * OO.ui.CheckboxMultiselectInputWidget instead.
8636 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8637 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8640 * label: 'Selected checkbox'
8642 * option2 = new OO.ui.CheckboxMultioptionWidget( {
8644 * label: 'Unselected checkbox'
8646 * multiselect = new OO.ui.CheckboxMultiselectWidget( {
8647 * items: [ option1, option2 ]
8649 * $( document.body ).append( multiselect.$element );
8651 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8654 * @extends OO.ui.MultiselectWidget
8657 * @param {Object} [config] Configuration options
8659 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8660 // Parent constructor
8661 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8664 this.$lastClicked
= null;
8667 this.$group
.on( 'click', this.onClick
.bind( this ) );
8671 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8676 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8681 * Get an option by its position relative to the specified item (or to the start of the option array,
8682 * if item is `null`). The direction in which to search through the option array is specified with a
8683 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8684 * `null` if there are no options in the array.
8686 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8687 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8688 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8690 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8691 var currentIndex
, nextIndex
, i
,
8692 increase
= direction
> 0 ? 1 : -1,
8693 len
= this.items
.length
;
8696 currentIndex
= this.items
.indexOf( item
);
8697 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8699 // If no item is selected and moving forward, start at the beginning.
8700 // If moving backward, start at the end.
8701 nextIndex
= direction
> 0 ? 0 : len
- 1;
8704 for ( i
= 0; i
< len
; i
++ ) {
8705 item
= this.items
[ nextIndex
];
8706 if ( item
&& !item
.isDisabled() ) {
8709 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8715 * Handle click events on checkboxes.
8717 * @param {jQuery.Event} e
8719 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8720 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8721 $lastClicked
= this.$lastClicked
,
8722 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8723 .not( '.oo-ui-widget-disabled' );
8725 // Allow selecting multiple options at once by Shift-clicking them
8726 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8727 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8728 lastClickedIndex
= $options
.index( $lastClicked
);
8729 nowClickedIndex
= $options
.index( $nowClicked
);
8730 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8731 // browser. In either case we don't need custom handling.
8732 if ( nowClickedIndex
!== lastClickedIndex
) {
8734 wasSelected
= items
[ nowClickedIndex
].isSelected();
8735 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8737 // This depends on the DOM order of the items and the order of the .items array being the same.
8738 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8739 if ( !items
[ i
].isDisabled() ) {
8740 items
[ i
].setSelected( !wasSelected
);
8743 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8744 // handling first, then set our value. The order in which events happen is different for
8745 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8746 // non-click actions that change the checkboxes.
8748 setTimeout( function () {
8749 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8750 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8756 if ( $nowClicked
.length
) {
8757 this.$lastClicked
= $nowClicked
;
8765 * @return {OO.ui.Widget} The widget, for chaining
8767 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8769 if ( !this.isDisabled() ) {
8770 item
= this.getRelativeFocusableItem( null, 1 );
8781 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8786 * Progress bars visually display the status of an operation, such as a download,
8787 * and can be either determinate or indeterminate:
8789 * - **determinate** process bars show the percent of an operation that is complete.
8791 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8792 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8793 * not use percentages.
8795 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8798 * // Examples of determinate and indeterminate progress bars.
8799 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8802 * var progressBar2 = new OO.ui.ProgressBarWidget();
8804 * // Create a FieldsetLayout to layout progress bars.
8805 * var fieldset = new OO.ui.FieldsetLayout;
8806 * fieldset.addItems( [
8807 * new OO.ui.FieldLayout( progressBar1, {
8808 * label: 'Determinate',
8811 * new OO.ui.FieldLayout( progressBar2, {
8812 * label: 'Indeterminate',
8816 * $( document.body ).append( fieldset.$element );
8819 * @extends OO.ui.Widget
8822 * @param {Object} [config] Configuration options
8823 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8824 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
8825 * By default, the progress bar is indeterminate.
8827 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8828 // Configuration initialization
8829 config
= config
|| {};
8831 // Parent constructor
8832 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8835 this.$bar
= $( '<div>' );
8836 this.progress
= null;
8839 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8840 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8843 role
: 'progressbar',
8845 'aria-valuemax': 100
8847 .addClass( 'oo-ui-progressBarWidget' )
8848 .append( this.$bar
);
8853 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8855 /* Static Properties */
8861 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8866 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8868 * @return {number|boolean} Progress percent
8870 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8871 return this.progress
;
8875 * Set the percent of the process completed or `false` for an indeterminate process.
8877 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8879 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8880 this.progress
= progress
;
8882 if ( progress
!== false ) {
8883 this.$bar
.css( 'width', this.progress
+ '%' );
8884 this.$element
.attr( 'aria-valuenow', this.progress
);
8886 this.$bar
.css( 'width', '' );
8887 this.$element
.removeAttr( 'aria-valuenow' );
8889 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8893 * InputWidget is the base class for all input widgets, which
8894 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8895 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8896 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
8898 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
8902 * @extends OO.ui.Widget
8903 * @mixins OO.ui.mixin.FlaggedElement
8904 * @mixins OO.ui.mixin.TabIndexedElement
8905 * @mixins OO.ui.mixin.TitledElement
8906 * @mixins OO.ui.mixin.AccessKeyedElement
8909 * @param {Object} [config] Configuration options
8910 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8911 * @cfg {string} [value=''] The value of the input.
8912 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8913 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8914 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8915 * before it is accepted.
8917 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8918 // Configuration initialization
8919 config
= config
|| {};
8921 // Parent constructor
8922 OO
.ui
.InputWidget
.parent
.call( this, config
);
8925 // See #reusePreInfuseDOM about config.$input
8926 this.$input
= config
.$input
|| this.getInputElement( config
);
8928 this.inputFilter
= config
.inputFilter
;
8930 // Mixin constructors
8931 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8932 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8933 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8934 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8937 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8941 .addClass( 'oo-ui-inputWidget-input' )
8942 .attr( 'name', config
.name
)
8943 .prop( 'disabled', this.isDisabled() );
8945 .addClass( 'oo-ui-inputWidget' )
8946 .append( this.$input
);
8947 this.setValue( config
.value
);
8949 this.setDir( config
.dir
);
8951 if ( config
.inputId
!== undefined ) {
8952 this.setInputId( config
.inputId
);
8958 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8959 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8960 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8961 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8962 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8964 /* Static Methods */
8969 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8970 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8971 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8972 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8979 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8980 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8981 if ( config
.$input
&& config
.$input
.length
) {
8982 state
.value
= config
.$input
.val();
8983 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8984 state
.focus
= config
.$input
.is( ':focus' );
8994 * A change event is emitted when the value of the input changes.
8996 * @param {string} value
9002 * Get input element.
9004 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
9005 * different circumstances. The element must have a `value` property (like form elements).
9008 * @param {Object} config Configuration options
9009 * @return {jQuery} Input element
9011 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
9012 return $( '<input>' );
9016 * Handle potentially value-changing events.
9019 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
9021 OO
.ui
.InputWidget
.prototype.onEdit = function () {
9023 if ( !this.isDisabled() ) {
9024 // Allow the stack to clear so the value will be updated
9025 setTimeout( function () {
9026 widget
.setValue( widget
.$input
.val() );
9032 * Get the value of the input.
9034 * @return {string} Input value
9036 OO
.ui
.InputWidget
.prototype.getValue = function () {
9037 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9038 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9039 var value
= this.$input
.val();
9040 if ( this.value
!== value
) {
9041 this.setValue( value
);
9047 * Set the directionality of the input.
9049 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
9051 * @return {OO.ui.Widget} The widget, for chaining
9053 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
9054 this.$input
.prop( 'dir', dir
);
9059 * Set the value of the input.
9061 * @param {string} value New value
9064 * @return {OO.ui.Widget} The widget, for chaining
9066 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
9067 value
= this.cleanUpValue( value
);
9068 // Update the DOM if it has changed. Note that with cleanUpValue, it
9069 // is possible for the DOM value to change without this.value changing.
9070 if ( this.$input
.val() !== value
) {
9071 this.$input
.val( value
);
9073 if ( this.value
!== value
) {
9075 this.emit( 'change', this.value
);
9077 // The first time that the value is set (probably while constructing the widget),
9078 // remember it in defaultValue. This property can be later used to check whether
9079 // the value of the input has been changed since it was created.
9080 if ( this.defaultValue
=== undefined ) {
9081 this.defaultValue
= this.value
;
9082 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
9088 * Clean up incoming value.
9090 * Ensures value is a string, and converts undefined and null to empty string.
9093 * @param {string} value Original value
9094 * @return {string} Cleaned up value
9096 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
9097 if ( value
=== undefined || value
=== null ) {
9099 } else if ( this.inputFilter
) {
9100 return this.inputFilter( String( value
) );
9102 return String( value
);
9109 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
9110 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9111 if ( this.$input
) {
9112 this.$input
.prop( 'disabled', this.isDisabled() );
9118 * Set the 'id' attribute of the `<input>` element.
9120 * @param {string} id
9122 * @return {OO.ui.Widget} The widget, for chaining
9124 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
9125 this.$input
.attr( 'id', id
);
9132 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
9133 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9134 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
9135 this.setValue( state
.value
);
9137 if ( state
.focus
) {
9143 * Data widget intended for creating `<input type="hidden">` inputs.
9146 * @extends OO.ui.Widget
9149 * @param {Object} [config] Configuration options
9150 * @cfg {string} [value=''] The value of the input.
9151 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9153 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
9154 // Configuration initialization
9155 config
= $.extend( { value
: '', name
: '' }, config
);
9157 // Parent constructor
9158 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
9161 this.$element
.attr( {
9163 value
: config
.value
,
9166 this.$element
.removeAttr( 'aria-disabled' );
9171 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
9173 /* Static Properties */
9179 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
9182 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
9183 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
9184 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
9185 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
9186 * [OOUI documentation on MediaWiki] [1] for more information.
9189 * // A ButtonInputWidget rendered as an HTML button, the default.
9190 * var button = new OO.ui.ButtonInputWidget( {
9191 * label: 'Input button',
9195 * $( document.body ).append( button.$element );
9197 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
9200 * @extends OO.ui.InputWidget
9201 * @mixins OO.ui.mixin.ButtonElement
9202 * @mixins OO.ui.mixin.IconElement
9203 * @mixins OO.ui.mixin.IndicatorElement
9204 * @mixins OO.ui.mixin.LabelElement
9207 * @param {Object} [config] Configuration options
9208 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
9209 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
9210 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
9211 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
9212 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
9214 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
9215 // Configuration initialization
9216 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
9218 // See InputWidget#reusePreInfuseDOM about config.$input
9219 if ( config
.$input
) {
9220 config
.$input
.empty();
9223 // Properties (must be set before parent constructor, which calls #setValue)
9224 this.useInputTag
= config
.useInputTag
;
9226 // Parent constructor
9227 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
9229 // Mixin constructors
9230 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
9231 OO
.ui
.mixin
.IconElement
.call( this, config
);
9232 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9233 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9236 if ( !config
.useInputTag
) {
9237 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
9239 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
9244 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
9245 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
9246 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
9247 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9248 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
9250 /* Static Properties */
9256 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
9264 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9266 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9267 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9273 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9275 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9276 * text, or `null` for no label
9278 * @return {OO.ui.Widget} The widget, for chaining
9280 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9281 if ( typeof label
=== 'function' ) {
9282 label
= OO
.ui
.resolveMsg( label
);
9285 if ( this.useInputTag
) {
9286 // Discard non-plaintext labels
9287 if ( typeof label
!== 'string' ) {
9291 this.$input
.val( label
);
9294 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9298 * Set the value of the input.
9300 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9301 * they do not support {@link #value values}.
9303 * @param {string} value New value
9305 * @return {OO.ui.Widget} The widget, for chaining
9307 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9308 if ( !this.useInputTag
) {
9309 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9317 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9318 // Disable generating `<label>` elements for buttons. One would very rarely need additional label
9319 // for a button, and it's already a big clickable target, and it causes unexpected rendering.
9324 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9325 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9326 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9327 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9329 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9332 * // An example of selected, unselected, and disabled checkbox inputs.
9333 * var checkbox1 = new OO.ui.CheckboxInputWidget( {
9337 * checkbox2 = new OO.ui.CheckboxInputWidget( {
9340 * checkbox3 = new OO.ui.CheckboxInputWidget( {
9344 * // Create a fieldset layout with fields for each checkbox.
9345 * fieldset = new OO.ui.FieldsetLayout( {
9346 * label: 'Checkboxes'
9348 * fieldset.addItems( [
9349 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9350 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9351 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9353 * $( document.body ).append( fieldset.$element );
9355 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9358 * @extends OO.ui.InputWidget
9361 * @param {Object} [config] Configuration options
9362 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
9364 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9365 // Configuration initialization
9366 config
= config
|| {};
9368 // Parent constructor
9369 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9372 this.checkIcon
= new OO
.ui
.IconWidget( {
9374 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9379 .addClass( 'oo-ui-checkboxInputWidget' )
9380 // Required for pretty styling in WikimediaUI theme
9381 .append( this.checkIcon
.$element
);
9382 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9387 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9389 /* Static Properties */
9395 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9397 /* Static Methods */
9402 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9403 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9404 state
.checked
= config
.$input
.prop( 'checked' );
9414 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9415 return $( '<input>' ).attr( 'type', 'checkbox' );
9421 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9423 if ( !this.isDisabled() ) {
9424 // Allow the stack to clear so the value will be updated
9425 setTimeout( function () {
9426 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9432 * Set selection state of this checkbox.
9434 * @param {boolean} state `true` for selected
9436 * @return {OO.ui.Widget} The widget, for chaining
9438 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
9440 if ( this.selected
!== state
) {
9441 this.selected
= state
;
9442 this.$input
.prop( 'checked', this.selected
);
9443 this.emit( 'change', this.selected
);
9445 // The first time that the selection state is set (probably while constructing the widget),
9446 // remember it in defaultSelected. This property can be later used to check whether
9447 // the selection state of the input has been changed since it was created.
9448 if ( this.defaultSelected
=== undefined ) {
9449 this.defaultSelected
= this.selected
;
9450 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9456 * Check if this checkbox is selected.
9458 * @return {boolean} Checkbox is selected
9460 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9461 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9462 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9463 var selected
= this.$input
.prop( 'checked' );
9464 if ( this.selected
!== selected
) {
9465 this.setSelected( selected
);
9467 return this.selected
;
9473 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9474 if ( !this.isDisabled() ) {
9475 this.$input
.click();
9483 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9484 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9485 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9486 this.setSelected( state
.checked
);
9491 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9492 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9493 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9494 * more information about input widgets.
9496 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9497 * are no options. If no `value` configuration option is provided, the first option is selected.
9498 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9500 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
9503 * // A DropdownInputWidget with three options.
9504 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9506 * { data: 'a', label: 'First' },
9507 * { data: 'b', label: 'Second'},
9508 * { data: 'c', label: 'Third' }
9511 * $( document.body ).append( dropdownInput.$element );
9513 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9516 * @extends OO.ui.InputWidget
9519 * @param {Object} [config] Configuration options
9520 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9521 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9522 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
9523 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
9524 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
9525 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9527 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9528 // Configuration initialization
9529 config
= config
|| {};
9531 // Properties (must be done before parent constructor which calls #setDisabled)
9532 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9534 $overlay
: config
.$overlay
9538 // Set up the options before parent constructor, which uses them to validate config.value.
9539 // Use this instead of setOptions() because this.$input is not set up yet.
9540 this.setOptionsData( config
.options
|| [] );
9542 // Parent constructor
9543 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9546 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
9550 .addClass( 'oo-ui-dropdownInputWidget' )
9551 .append( this.dropdownWidget
.$element
);
9552 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9553 this.setTitledElement( this.dropdownWidget
.$handle
);
9558 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9566 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9567 return $( '<select>' );
9571 * Handles menu select events.
9574 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9576 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9577 this.setValue( item
? item
.getData() : '' );
9583 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9585 value
= this.cleanUpValue( value
);
9586 // Only allow setting values that are actually present in the dropdown
9587 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9588 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9589 this.dropdownWidget
.getMenu().selectItem( selected
);
9590 value
= selected
? selected
.getData() : '';
9591 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9592 if ( this.optionsDirty
) {
9593 // We reached this from the constructor or from #setOptions.
9594 // We have to update the <select> element.
9595 this.updateOptionsInterface();
9603 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9604 this.dropdownWidget
.setDisabled( state
);
9605 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9610 * Set the options available for this input.
9612 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9614 * @return {OO.ui.Widget} The widget, for chaining
9616 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9617 var value
= this.getValue();
9619 this.setOptionsData( options
);
9621 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9622 // In case the previous value is no longer an available option, select the first valid one.
9623 this.setValue( value
);
9629 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9631 * This method may be called before the parent constructor, so various properties may not be
9634 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9637 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9642 this.optionsDirty
= true;
9644 optionWidgets
= options
.map( function ( opt
) {
9647 if ( opt
.optgroup
!== undefined ) {
9648 return widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9651 optValue
= widget
.cleanUpValue( opt
.data
);
9652 return widget
.createMenuOptionWidget(
9654 opt
.label
!== undefined ? opt
.label
: optValue
9659 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9663 * Create a menu option widget.
9666 * @param {string} data Item data
9667 * @param {string} label Item label
9668 * @return {OO.ui.MenuOptionWidget} Option widget
9670 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9671 return new OO
.ui
.MenuOptionWidget( {
9678 * Create a menu section option widget.
9681 * @param {string} label Section item label
9682 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9684 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9685 return new OO
.ui
.MenuSectionOptionWidget( {
9691 * Update the user-visible interface to match the internal list of options and value.
9693 * This method must only be called after the parent constructor.
9697 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9699 $optionsContainer
= this.$input
,
9700 defaultValue
= this.defaultValue
,
9703 this.$input
.empty();
9705 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9708 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9709 $optionNode
= $( '<option>' )
9710 .attr( 'value', optionWidget
.getData() )
9711 .text( optionWidget
.getLabel() );
9713 // Remember original selection state. This property can be later used to check whether
9714 // the selection state of the input has been changed since it was created.
9715 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9717 $optionsContainer
.append( $optionNode
);
9719 $optionNode
= $( '<optgroup>' )
9720 .attr( 'label', optionWidget
.getLabel() );
9721 widget
.$input
.append( $optionNode
);
9722 $optionsContainer
= $optionNode
;
9726 this.optionsDirty
= false;
9732 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9733 this.dropdownWidget
.focus();
9740 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9741 this.dropdownWidget
.blur();
9746 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9747 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9748 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9749 * please see the [OOUI documentation on MediaWiki][1].
9751 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9754 * // An example of selected, unselected, and disabled radio inputs
9755 * var radio1 = new OO.ui.RadioInputWidget( {
9759 * var radio2 = new OO.ui.RadioInputWidget( {
9762 * var radio3 = new OO.ui.RadioInputWidget( {
9766 * // Create a fieldset layout with fields for each radio button.
9767 * var fieldset = new OO.ui.FieldsetLayout( {
9768 * label: 'Radio inputs'
9770 * fieldset.addItems( [
9771 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9772 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9773 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9775 * $( document.body ).append( fieldset.$element );
9777 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9780 * @extends OO.ui.InputWidget
9783 * @param {Object} [config] Configuration options
9784 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9786 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9787 // Configuration initialization
9788 config
= config
|| {};
9790 // Parent constructor
9791 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9795 .addClass( 'oo-ui-radioInputWidget' )
9796 // Required for pretty styling in WikimediaUI theme
9797 .append( $( '<span>' ) );
9798 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9803 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9805 /* Static Properties */
9811 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9813 /* Static Methods */
9818 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9819 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9820 state
.checked
= config
.$input
.prop( 'checked' );
9830 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9831 return $( '<input>' ).attr( 'type', 'radio' );
9837 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9838 // RadioInputWidget doesn't track its state.
9842 * Set selection state of this radio button.
9844 * @param {boolean} state `true` for selected
9846 * @return {OO.ui.Widget} The widget, for chaining
9848 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9849 // RadioInputWidget doesn't track its state.
9850 this.$input
.prop( 'checked', state
);
9851 // The first time that the selection state is set (probably while constructing the widget),
9852 // remember it in defaultSelected. This property can be later used to check whether
9853 // the selection state of the input has been changed since it was created.
9854 if ( this.defaultSelected
=== undefined ) {
9855 this.defaultSelected
= state
;
9856 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9862 * Check if this radio button is selected.
9864 * @return {boolean} Radio is selected
9866 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
9867 return this.$input
.prop( 'checked' );
9873 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
9874 if ( !this.isDisabled() ) {
9875 this.$input
.click();
9883 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9884 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9885 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9886 this.setSelected( state
.checked
);
9891 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9892 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9893 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9894 * more information about input widgets.
9896 * This and OO.ui.DropdownInputWidget support the same configuration options.
9899 * // A RadioSelectInputWidget with three options
9900 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9902 * { data: 'a', label: 'First' },
9903 * { data: 'b', label: 'Second'},
9904 * { data: 'c', label: 'Third' }
9907 * $( document.body ).append( radioSelectInput.$element );
9909 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9912 * @extends OO.ui.InputWidget
9915 * @param {Object} [config] Configuration options
9916 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9918 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
9919 // Configuration initialization
9920 config
= config
|| {};
9922 // Properties (must be done before parent constructor which calls #setDisabled)
9923 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
9924 // Set up the options before parent constructor, which uses them to validate config.value.
9925 // Use this instead of setOptions() because this.$input is not set up yet
9926 this.setOptionsData( config
.options
|| [] );
9928 // Parent constructor
9929 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
9932 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
9936 .addClass( 'oo-ui-radioSelectInputWidget' )
9937 .append( this.radioSelectWidget
.$element
);
9938 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
9943 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
9945 /* Static Methods */
9950 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9951 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9952 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9959 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9960 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9961 // Cannot reuse the `<input type=radio>` set
9962 delete config
.$input
;
9972 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
9973 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
9974 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
9975 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
9979 * Handles menu select events.
9982 * @param {OO.ui.RadioOptionWidget} item Selected menu item
9984 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
9985 this.setValue( item
.getData() );
9991 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
9993 value
= this.cleanUpValue( value
);
9994 // Only allow setting values that are actually present in the dropdown
9995 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
9996 this.radioSelectWidget
.findFirstSelectableItem();
9997 this.radioSelectWidget
.selectItem( selected
);
9998 value
= selected
? selected
.getData() : '';
9999 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10006 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
10007 this.radioSelectWidget
.setDisabled( state
);
10008 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10013 * Set the options available for this input.
10015 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10017 * @return {OO.ui.Widget} The widget, for chaining
10019 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
10020 var value
= this.getValue();
10022 this.setOptionsData( options
);
10024 // Re-set the value to update the visible interface (RadioSelectWidget).
10025 // In case the previous value is no longer an available option, select the first valid one.
10026 this.setValue( value
);
10032 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10034 * This method may be called before the parent constructor, so various properties may not be
10037 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10040 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10043 this.radioSelectWidget
10045 .addItems( options
.map( function ( opt
) {
10046 var optValue
= widget
.cleanUpValue( opt
.data
);
10047 return new OO
.ui
.RadioOptionWidget( {
10049 label
: opt
.label
!== undefined ? opt
.label
: optValue
10057 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10058 this.radioSelectWidget
.focus();
10065 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10066 this.radioSelectWidget
.blur();
10071 * CheckboxMultiselectInputWidget is a
10072 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10073 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10074 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10075 * more information about input widgets.
10078 * // A CheckboxMultiselectInputWidget with three options.
10079 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10081 * { data: 'a', label: 'First' },
10082 * { data: 'b', label: 'Second' },
10083 * { data: 'c', label: 'Third' }
10086 * $( document.body ).append( multiselectInput.$element );
10088 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10091 * @extends OO.ui.InputWidget
10094 * @param {Object} [config] Configuration options
10095 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
10097 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10098 // Configuration initialization
10099 config
= config
|| {};
10101 // Properties (must be done before parent constructor which calls #setDisabled)
10102 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10103 // Must be set before the #setOptionsData call below
10104 this.inputName
= config
.name
;
10105 // Set up the options before parent constructor, which uses them to validate config.value.
10106 // Use this instead of setOptions() because this.$input is not set up yet
10107 this.setOptionsData( config
.options
|| [] );
10109 // Parent constructor
10110 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10113 this.checkboxMultiselectWidget
.connect( this, { select
: 'onCheckboxesSelect' } );
10117 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10118 .append( this.checkboxMultiselectWidget
.$element
);
10119 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10120 this.$input
.detach();
10125 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10127 /* Static Methods */
10132 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10133 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10134 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10135 .toArray().map( function ( el
) { return el
.value
; } );
10142 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10143 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10144 // Cannot reuse the `<input type=checkbox>` set
10145 delete config
.$input
;
10155 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10157 return $( '<unused>' );
10161 * Handles CheckboxMultiselectWidget select events.
10165 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10166 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10172 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10173 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10174 .toArray().map( function ( el
) { return el
.value
; } );
10175 if ( this.value
!== value
) {
10176 this.setValue( value
);
10184 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10185 value
= this.cleanUpValue( value
);
10186 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10187 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10188 if ( this.optionsDirty
) {
10189 // We reached this from the constructor or from #setOptions.
10190 // We have to update the <select> element.
10191 this.updateOptionsInterface();
10197 * Clean up incoming value.
10199 * @param {string[]} value Original value
10200 * @return {string[]} Cleaned up value
10202 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10203 var i
, singleValue
,
10205 if ( !Array
.isArray( value
) ) {
10208 for ( i
= 0; i
< value
.length
; i
++ ) {
10210 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
10211 // Remove options that we don't have here
10212 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10215 cleanValue
.push( singleValue
);
10223 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10224 this.checkboxMultiselectWidget
.setDisabled( state
);
10225 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10230 * Set the options available for this input.
10232 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
10234 * @return {OO.ui.Widget} The widget, for chaining
10236 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10237 var value
= this.getValue();
10239 this.setOptionsData( options
);
10241 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10242 // This will also get rid of any stale options that we just removed.
10243 this.setValue( value
);
10249 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10251 * This method may be called before the parent constructor, so various properties may not be
10254 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10257 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10260 this.optionsDirty
= true;
10262 this.checkboxMultiselectWidget
10264 .addItems( options
.map( function ( opt
) {
10265 var optValue
, item
, optDisabled
;
10267 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
10268 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10269 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10271 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10272 disabled
: optDisabled
10274 // Set the 'name' and 'value' for form submission
10275 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10276 item
.checkbox
.setValue( optValue
);
10282 * Update the user-visible interface to match the internal list of options and value.
10284 * This method must only be called after the parent constructor.
10288 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10289 var defaultValue
= this.defaultValue
;
10291 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10292 // Remember original selection state. This property can be later used to check whether
10293 // the selection state of the input has been changed since it was created.
10294 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10295 item
.checkbox
.defaultSelected
= isDefault
;
10296 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10299 this.optionsDirty
= false;
10305 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10306 this.checkboxMultiselectWidget
.focus();
10311 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10312 * size of the field as well as its presentation. In addition, these widgets can be configured
10313 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
10314 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
10315 * which modifies incoming values rather than validating them.
10316 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10318 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10321 * // A TextInputWidget.
10322 * var textInput = new OO.ui.TextInputWidget( {
10323 * value: 'Text input'
10325 * $( document.body ).append( textInput.$element );
10327 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10330 * @extends OO.ui.InputWidget
10331 * @mixins OO.ui.mixin.IconElement
10332 * @mixins OO.ui.mixin.IndicatorElement
10333 * @mixins OO.ui.mixin.PendingElement
10334 * @mixins OO.ui.mixin.LabelElement
10337 * @param {Object} [config] Configuration options
10338 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10339 * 'email', 'url' or 'number'.
10340 * @cfg {string} [placeholder] Placeholder text
10341 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10342 * instruct the browser to focus this widget.
10343 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10344 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10346 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10347 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10348 * many emojis) count as 2 characters each.
10349 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10350 * the value or placeholder text: `'before'` or `'after'`
10351 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator: 'required'`.
10352 * Note that `false` & setting `indicator: 'required' will result in no indicator shown.
10353 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10354 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined` means
10355 * leaving it up to the browser).
10356 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10357 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10358 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10359 * value for it to be considered valid; when Function, a function receiving the value as parameter
10360 * that must return true, or promise resolving to true, for it to be considered valid.
10362 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10363 // Configuration initialization
10364 config
= $.extend( {
10366 labelPosition
: 'after'
10369 // Parent constructor
10370 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10372 // Mixin constructors
10373 OO
.ui
.mixin
.IconElement
.call( this, config
);
10374 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10375 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
10376 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10379 this.type
= this.getSaneType( config
);
10380 this.readOnly
= false;
10381 this.required
= false;
10382 this.validate
= null;
10383 this.scrollWidth
= null;
10385 this.setValidation( config
.validate
);
10386 this.setLabelPosition( config
.labelPosition
);
10390 keypress
: this.onKeyPress
.bind( this ),
10391 blur
: this.onBlur
.bind( this ),
10392 focus
: this.onFocus
.bind( this )
10394 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10395 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10396 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10397 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10401 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10402 .append( this.$icon
, this.$indicator
);
10403 this.setReadOnly( !!config
.readOnly
);
10404 this.setRequired( !!config
.required
);
10405 if ( config
.placeholder
!== undefined ) {
10406 this.$input
.attr( 'placeholder', config
.placeholder
);
10408 if ( config
.maxLength
!== undefined ) {
10409 this.$input
.attr( 'maxlength', config
.maxLength
);
10411 if ( config
.autofocus
) {
10412 this.$input
.attr( 'autofocus', 'autofocus' );
10414 if ( config
.autocomplete
=== false ) {
10415 this.$input
.attr( 'autocomplete', 'off' );
10416 // Turning off autocompletion also disables "form caching" when the user navigates to a
10417 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
10419 beforeunload: function () {
10420 this.$input
.removeAttr( 'autocomplete' );
10422 pageshow: function () {
10423 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
10424 // whole page... it shouldn't hurt, though.
10425 this.$input
.attr( 'autocomplete', 'off' );
10429 if ( config
.spellcheck
!== undefined ) {
10430 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10432 if ( this.label
) {
10433 this.isWaitingToBeAttached
= true;
10434 this.installParentChangeDetector();
10440 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10441 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10442 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10443 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10444 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10446 /* Static Properties */
10448 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10456 * An `enter` event is emitted when the user presses 'enter' inside the text box.
10464 * Handle icon mouse down events.
10467 * @param {jQuery.Event} e Mouse down event
10468 * @return {undefined/boolean} False to prevent default if event is handled
10470 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10471 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10478 * Handle indicator mouse down events.
10481 * @param {jQuery.Event} e Mouse down event
10482 * @return {undefined/boolean} False to prevent default if event is handled
10484 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10485 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10492 * Handle key press events.
10495 * @param {jQuery.Event} e Key press event
10496 * @fires enter If enter key is pressed
10498 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10499 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10500 this.emit( 'enter', e
);
10505 * Handle blur events.
10508 * @param {jQuery.Event} e Blur event
10510 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10511 this.setValidityFlag();
10515 * Handle focus events.
10518 * @param {jQuery.Event} e Focus event
10520 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10521 if ( this.isWaitingToBeAttached
) {
10522 // If we've received focus, then we must be attached to the document, and if
10523 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10524 this.onElementAttach();
10526 this.setValidityFlag( true );
10530 * Handle element attach events.
10533 * @param {jQuery.Event} e Element attach event
10535 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10536 this.isWaitingToBeAttached
= false;
10537 // Any previously calculated size is now probably invalid if we reattached elsewhere
10538 this.valCache
= null;
10539 this.positionLabel();
10543 * Handle debounced change events.
10545 * @param {string} value
10548 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10549 this.setValidityFlag();
10553 * Check if the input is {@link #readOnly read-only}.
10555 * @return {boolean}
10557 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10558 return this.readOnly
;
10562 * Set the {@link #readOnly read-only} state of the input.
10564 * @param {boolean} state Make input read-only
10566 * @return {OO.ui.Widget} The widget, for chaining
10568 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10569 this.readOnly
= !!state
;
10570 this.$input
.prop( 'readOnly', this.readOnly
);
10575 * Check if the input is {@link #required required}.
10577 * @return {boolean}
10579 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10580 return this.required
;
10584 * Set the {@link #required required} state of the input.
10586 * @param {boolean} state Make input required
10588 * @return {OO.ui.Widget} The widget, for chaining
10590 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10591 this.required
= !!state
;
10592 if ( this.required
) {
10594 .prop( 'required', true )
10595 .attr( 'aria-required', 'true' );
10596 if ( this.getIndicator() === null ) {
10597 this.setIndicator( 'required' );
10601 .prop( 'required', false )
10602 .removeAttr( 'aria-required' );
10603 if ( this.getIndicator() === 'required' ) {
10604 this.setIndicator( null );
10611 * Support function for making #onElementAttach work across browsers.
10613 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10614 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10616 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10617 * first time that the element gets attached to the documented.
10619 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10620 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10621 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
10624 if ( MutationObserver
) {
10625 // The new way. If only it wasn't so ugly.
10627 if ( this.isElementAttached() ) {
10628 // Widget is attached already, do nothing. This breaks the functionality of this function when
10629 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
10630 // would require observation of the whole document, which would hurt performance of other,
10631 // more important code.
10635 // Find topmost node in the tree
10636 topmostNode
= this.$element
[ 0 ];
10637 while ( topmostNode
.parentNode
) {
10638 topmostNode
= topmostNode
.parentNode
;
10641 // We have no way to detect the $element being attached somewhere without observing the entire
10642 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
10643 // parent node of $element, and instead detect when $element is removed from it (and thus
10644 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
10645 // doesn't get attached, we end up back here and create the parent.
10647 mutationObserver
= new MutationObserver( function ( mutations
) {
10648 var i
, j
, removedNodes
;
10649 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10650 removedNodes
= mutations
[ i
].removedNodes
;
10651 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10652 if ( removedNodes
[ j
] === topmostNode
) {
10653 setTimeout( onRemove
, 0 );
10660 onRemove = function () {
10661 // If the node was attached somewhere else, report it
10662 if ( widget
.isElementAttached() ) {
10663 widget
.onElementAttach();
10665 mutationObserver
.disconnect();
10666 widget
.installParentChangeDetector();
10669 // Create a fake parent and observe it
10670 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10671 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10673 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10674 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10675 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10683 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10684 if ( this.getSaneType( config
) === 'number' ) {
10685 return $( '<input>' )
10686 .attr( 'step', 'any' )
10687 .attr( 'type', 'number' );
10689 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10694 * Get sanitized value for 'type' for given config.
10696 * @param {Object} config Configuration options
10697 * @return {string|null}
10700 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10701 var allowedTypes
= [
10708 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10712 * Focus the input and select a specified range within the text.
10714 * @param {number} from Select from offset
10715 * @param {number} [to] Select to offset, defaults to from
10717 * @return {OO.ui.Widget} The widget, for chaining
10719 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10720 var isBackwards
, start
, end
,
10721 input
= this.$input
[ 0 ];
10725 isBackwards
= to
< from;
10726 start
= isBackwards
? to
: from;
10727 end
= isBackwards
? from : to
;
10732 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10734 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10735 // Rather than expensively check if the input is attached every time, just check
10736 // if it was the cause of an error being thrown. If not, rethrow the error.
10737 if ( this.getElementDocument().body
.contains( input
) ) {
10745 * Get an object describing the current selection range in a directional manner
10747 * @return {Object} Object containing 'from' and 'to' offsets
10749 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10750 var input
= this.$input
[ 0 ],
10751 start
= input
.selectionStart
,
10752 end
= input
.selectionEnd
,
10753 isBackwards
= input
.selectionDirection
=== 'backward';
10756 from: isBackwards
? end
: start
,
10757 to
: isBackwards
? start
: end
10762 * Get the length of the text input value.
10764 * This could differ from the length of #getValue if the
10765 * value gets filtered
10767 * @return {number} Input length
10769 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10770 return this.$input
[ 0 ].value
.length
;
10774 * Focus the input and select the entire text.
10777 * @return {OO.ui.Widget} The widget, for chaining
10779 OO
.ui
.TextInputWidget
.prototype.select = function () {
10780 return this.selectRange( 0, this.getInputLength() );
10784 * Focus the input and move the cursor to the start.
10787 * @return {OO.ui.Widget} The widget, for chaining
10789 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10790 return this.selectRange( 0 );
10794 * Focus the input and move the cursor to the end.
10797 * @return {OO.ui.Widget} The widget, for chaining
10799 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10800 return this.selectRange( this.getInputLength() );
10804 * Insert new content into the input.
10806 * @param {string} content Content to be inserted
10808 * @return {OO.ui.Widget} The widget, for chaining
10810 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10812 range
= this.getRange(),
10813 value
= this.getValue();
10815 start
= Math
.min( range
.from, range
.to
);
10816 end
= Math
.max( range
.from, range
.to
);
10818 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10819 this.selectRange( start
+ content
.length
);
10824 * Insert new content either side of a selection.
10826 * @param {string} pre Content to be inserted before the selection
10827 * @param {string} post Content to be inserted after the selection
10829 * @return {OO.ui.Widget} The widget, for chaining
10831 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10833 range
= this.getRange(),
10834 offset
= pre
.length
;
10836 start
= Math
.min( range
.from, range
.to
);
10837 end
= Math
.max( range
.from, range
.to
);
10839 this.selectRange( start
).insertContent( pre
);
10840 this.selectRange( offset
+ end
).insertContent( post
);
10842 this.selectRange( offset
+ start
, offset
+ end
);
10847 * Set the validation pattern.
10849 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10850 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10851 * value must contain only numbers).
10853 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10854 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10856 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
10857 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
10858 this.validate
= validate
;
10860 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
10865 * Sets the 'invalid' flag appropriately.
10867 * @param {boolean} [isValid] Optionally override validation result
10869 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
10871 setFlag = function ( valid
) {
10873 widget
.$input
.attr( 'aria-invalid', 'true' );
10875 widget
.$input
.removeAttr( 'aria-invalid' );
10877 widget
.setFlags( { invalid
: !valid
} );
10880 if ( isValid
!== undefined ) {
10881 setFlag( isValid
);
10883 this.getValidity().then( function () {
10892 * Get the validity of current value.
10894 * This method returns a promise that resolves if the value is valid and rejects if
10895 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
10897 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10899 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
10902 function rejectOrResolve( valid
) {
10904 return $.Deferred().resolve().promise();
10906 return $.Deferred().reject().promise();
10910 // Check browser validity and reject if it is invalid
10912 this.$input
[ 0 ].checkValidity
!== undefined &&
10913 this.$input
[ 0 ].checkValidity() === false
10915 return rejectOrResolve( false );
10918 // Run our checks if the browser thinks the field is valid
10919 if ( this.validate
instanceof Function
) {
10920 result
= this.validate( this.getValue() );
10921 if ( result
&& typeof result
.promise
=== 'function' ) {
10922 return result
.promise().then( function ( valid
) {
10923 return rejectOrResolve( valid
);
10926 return rejectOrResolve( result
);
10929 return rejectOrResolve( this.getValue().match( this.validate
) );
10934 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10936 * @param {string} labelPosition Label position, 'before' or 'after'
10938 * @return {OO.ui.Widget} The widget, for chaining
10940 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10941 this.labelPosition
= labelPosition
;
10942 if ( this.label
) {
10943 // If there is no label and we only change the position, #updatePosition is a no-op,
10944 // but it takes really a lot of work to do nothing.
10945 this.updatePosition();
10951 * Update the position of the inline label.
10953 * This method is called by #setLabelPosition, and can also be called on its own if
10954 * something causes the label to be mispositioned.
10957 * @return {OO.ui.Widget} The widget, for chaining
10959 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10960 var after
= this.labelPosition
=== 'after';
10963 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10964 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10966 this.valCache
= null;
10967 this.scrollWidth
= null;
10968 this.positionLabel();
10974 * Position the label by setting the correct padding on the input.
10978 * @return {OO.ui.Widget} The widget, for chaining
10980 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
10981 var after
, rtl
, property
, newCss
;
10983 if ( this.isWaitingToBeAttached
) {
10984 // #onElementAttach will be called soon, which calls this method
10989 'padding-right': '',
10993 if ( this.label
) {
10994 this.$element
.append( this.$label
);
10996 this.$label
.detach();
10997 // Clear old values if present
10998 this.$input
.css( newCss
);
11002 after
= this.labelPosition
=== 'after';
11003 rtl
= this.$element
.css( 'direction' ) === 'rtl';
11004 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11006 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
11007 // We have to clear the padding on the other side, in case the element direction changed
11008 this.$input
.css( newCss
);
11014 * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
11015 * {@link OO.ui.mixin.IconElement search icon} by default.
11016 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11018 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
11021 * @extends OO.ui.TextInputWidget
11024 * @param {Object} [config] Configuration options
11026 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
11027 config
= $.extend( {
11031 // Parent constructor
11032 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11035 this.connect( this, {
11040 this.updateSearchIndicator();
11041 this.connect( this, {
11042 disable
: 'onDisable'
11048 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11056 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11063 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
11064 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11065 // Clear the text field
11066 this.setValue( '' );
11073 * Update the 'clear' indicator displayed on type: 'search' text
11074 * fields, hiding it when the field is already empty or when it's not
11077 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11078 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11079 this.setIndicator( null );
11081 this.setIndicator( 'clear' );
11086 * Handle change events.
11090 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11091 this.updateSearchIndicator();
11095 * Handle disable events.
11097 * @param {boolean} disabled Element is disabled
11100 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11101 this.updateSearchIndicator();
11107 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11108 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11109 this.updateSearchIndicator();
11114 * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
11115 * configure number of rows visible. In addition, these widgets can be autosized to fit user
11116 * inputs and can show {@link OO.ui.mixin.IconElement icons} and
11117 * {@link OO.ui.mixin.IndicatorElement indicators}.
11118 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11120 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11123 * // A MultilineTextInputWidget.
11124 * var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
11125 * value: 'Text input on multiple lines'
11127 * $( 'body' ).append( multilineTextInput.$element );
11129 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
11132 * @extends OO.ui.TextInputWidget
11135 * @param {Object} [config] Configuration options
11136 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11137 * specifies minimum number of rows to display.
11138 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11139 * Use the #maxRows config to specify a maximum number of displayed rows.
11140 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11141 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11143 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11144 config
= $.extend( {
11147 // Parent constructor
11148 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11151 this.autosize
= !!config
.autosize
;
11152 this.styleHeight
= null;
11153 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11154 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11156 // Clone for resizing
11157 if ( this.autosize
) {
11158 this.$clone
= this.$input
11160 .removeAttr( 'id' )
11161 .removeAttr( 'name' )
11162 .insertAfter( this.$input
)
11163 .attr( 'aria-hidden', 'true' )
11164 .addClass( 'oo-ui-element-hidden' );
11168 this.connect( this, {
11173 if ( config
.rows
) {
11174 this.$input
.attr( 'rows', config
.rows
);
11176 if ( this.autosize
) {
11177 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11178 this.isWaitingToBeAttached
= true;
11179 this.installParentChangeDetector();
11185 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11187 /* Static Methods */
11192 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11193 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11194 state
.scrollTop
= config
.$input
.scrollTop();
11203 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11204 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11209 * Handle change events.
11213 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11220 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11221 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11228 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11230 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11232 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11233 // Some platforms emit keycode 10 for ctrl+enter in a textarea
11236 this.emit( 'enter', e
);
11241 * Automatically adjust the size of the text input.
11243 * This only affects multiline inputs that are {@link #autosize autosized}.
11246 * @return {OO.ui.Widget} The widget, for chaining
11249 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11250 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11251 idealHeight
, newHeight
, scrollWidth
, property
;
11253 if ( this.$input
.val() !== this.valCache
) {
11254 if ( this.autosize
) {
11256 .val( this.$input
.val() )
11257 .attr( 'rows', this.minRows
)
11258 // Set inline height property to 0 to measure scroll height
11259 .css( 'height', 0 );
11261 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11263 this.valCache
= this.$input
.val();
11265 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11267 // Remove inline height property to measure natural heights
11268 this.$clone
.css( 'height', '' );
11269 innerHeight
= this.$clone
.innerHeight();
11270 outerHeight
= this.$clone
.outerHeight();
11272 // Measure max rows height
11274 .attr( 'rows', this.maxRows
)
11275 .css( 'height', 'auto' )
11277 maxInnerHeight
= this.$clone
.innerHeight();
11279 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11280 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11281 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11282 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11284 this.$clone
.addClass( 'oo-ui-element-hidden' );
11286 // Only apply inline height when expansion beyond natural height is needed
11287 // Use the difference between the inner and outer height as a buffer
11288 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11289 if ( newHeight
!== this.styleHeight
) {
11290 this.$input
.css( 'height', newHeight
);
11291 this.styleHeight
= newHeight
;
11292 this.emit( 'resize' );
11295 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11296 if ( scrollWidth
!== this.scrollWidth
) {
11297 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11299 this.$label
.css( { right
: '', left
: '' } );
11300 this.$indicator
.css( { right
: '', left
: '' } );
11302 if ( scrollWidth
) {
11303 this.$indicator
.css( property
, scrollWidth
);
11304 if ( this.labelPosition
=== 'after' ) {
11305 this.$label
.css( property
, scrollWidth
);
11309 this.scrollWidth
= scrollWidth
;
11310 this.positionLabel();
11320 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11321 return $( '<textarea>' );
11325 * Check if the input automatically adjusts its size.
11327 * @return {boolean}
11329 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11330 return !!this.autosize
;
11336 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11337 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11338 if ( state
.scrollTop
!== undefined ) {
11339 this.$input
.scrollTop( state
.scrollTop
);
11344 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11345 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11346 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11348 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11349 * option, that option will appear to be selected.
11350 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11353 * After the user chooses an option, its `data` will be used as a new value for the widget.
11354 * A `label` also can be specified for each option: if given, it will be shown instead of the
11355 * `data` in the dropdown menu.
11357 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11359 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
11362 * // A ComboBoxInputWidget.
11363 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11364 * value: 'Option 1',
11366 * { data: 'Option 1' },
11367 * { data: 'Option 2' },
11368 * { data: 'Option 3' }
11371 * $( document.body ).append( comboBox.$element );
11374 * // Example: A ComboBoxInputWidget with additional option labels.
11375 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11376 * value: 'Option 1',
11379 * data: 'Option 1',
11380 * label: 'Option One'
11383 * data: 'Option 2',
11384 * label: 'Option Two'
11387 * data: 'Option 3',
11388 * label: 'Option Three'
11392 * $( document.body ).append( comboBox.$element );
11394 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11397 * @extends OO.ui.TextInputWidget
11400 * @param {Object} [config] Configuration options
11401 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11402 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11403 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11404 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11405 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11406 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11408 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11409 // Configuration initialization
11410 config
= $.extend( {
11411 autocomplete
: false
11414 // ComboBoxInputWidget shouldn't support `multiline`
11415 config
.multiline
= false;
11417 // See InputWidget#reusePreInfuseDOM about `config.$input`
11418 if ( config
.$input
) {
11419 config
.$input
.removeAttr( 'list' );
11422 // Parent constructor
11423 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11426 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11427 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11428 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11429 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11431 invisibleLabel
: true,
11432 disabled
: this.disabled
11434 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11438 $floatableContainer
: this.$element
,
11439 disabled
: this.isDisabled()
11445 this.connect( this, {
11446 change
: 'onInputChange',
11447 enter
: 'onInputEnter'
11449 this.dropdownButton
.connect( this, {
11450 click
: 'onDropdownButtonClick'
11452 this.menu
.connect( this, {
11453 choose
: 'onMenuChoose',
11454 add
: 'onMenuItemsChange',
11455 remove
: 'onMenuItemsChange',
11456 toggle
: 'onMenuToggle'
11460 this.$input
.attr( {
11462 'aria-owns': this.menu
.getElementId(),
11463 'aria-autocomplete': 'list'
11465 this.dropdownButton
.$button
.attr( {
11466 'aria-controls': this.menu
.getElementId()
11468 // Do not override options set via config.menu.items
11469 if ( config
.options
!== undefined ) {
11470 this.setOptions( config
.options
);
11472 this.$field
= $( '<div>' )
11473 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11474 .append( this.$input
, this.dropdownButton
.$element
);
11476 .addClass( 'oo-ui-comboBoxInputWidget' )
11477 .append( this.$field
);
11478 this.$overlay
.append( this.menu
.$element
);
11479 this.onMenuItemsChange();
11484 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11489 * Get the combobox's menu.
11491 * @return {OO.ui.MenuSelectWidget} Menu widget
11493 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11498 * Get the combobox's text input widget.
11500 * @return {OO.ui.TextInputWidget} Text input widget
11502 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11507 * Handle input change events.
11510 * @param {string} value New value
11512 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11513 var match
= this.menu
.findItemFromData( value
);
11515 this.menu
.selectItem( match
);
11516 if ( this.menu
.findHighlightedItem() ) {
11517 this.menu
.highlightItem( match
);
11520 if ( !this.isDisabled() ) {
11521 this.menu
.toggle( true );
11526 * Handle input enter events.
11530 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11531 if ( !this.isDisabled() ) {
11532 this.menu
.toggle( false );
11537 * Handle button click events.
11541 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11542 this.menu
.toggle();
11547 * Handle menu choose events.
11550 * @param {OO.ui.OptionWidget} item Chosen item
11552 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11553 this.setValue( item
.getData() );
11557 * Handle menu item change events.
11561 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11562 var match
= this.menu
.findItemFromData( this.getValue() );
11563 this.menu
.selectItem( match
);
11564 if ( this.menu
.findHighlightedItem() ) {
11565 this.menu
.highlightItem( match
);
11567 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11571 * Handle menu toggle events.
11574 * @param {boolean} isVisible Open state of the menu
11576 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11577 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11583 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
11585 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
11587 if ( this.dropdownButton
) {
11588 this.dropdownButton
.setDisabled( this.isDisabled() );
11591 this.menu
.setDisabled( this.isDisabled() );
11598 * Set the options available for this input.
11600 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11602 * @return {OO.ui.Widget} The widget, for chaining
11604 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11607 .addItems( options
.map( function ( opt
) {
11608 return new OO
.ui
.MenuOptionWidget( {
11610 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11618 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11619 * which is a widget that is specified by reference before any optional configuration settings.
11621 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
11623 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11624 * A left-alignment is used for forms with many fields.
11625 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11626 * A right-alignment is used for long but familiar forms which users tab through,
11627 * verifying the current field with a quick glance at the label.
11628 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11629 * that users fill out from top to bottom.
11630 * - **inline**: The label is placed after the field-widget and aligned to the left.
11631 * An inline-alignment is best used with checkboxes or radio buttons.
11633 * Help text can either be:
11635 * - accessed via a help icon that appears in the upper right corner of the rendered field layout, or
11636 * - shown as a subtle explanation below the label.
11638 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`. If it
11639 * is long or not essential, leave `helpInline` to its default, `false`.
11641 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11643 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11646 * @extends OO.ui.Layout
11647 * @mixins OO.ui.mixin.LabelElement
11648 * @mixins OO.ui.mixin.TitledElement
11651 * @param {OO.ui.Widget} fieldWidget Field widget
11652 * @param {Object} [config] Configuration options
11653 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11655 * @cfg {Array} [errors] Error messages about the widget, which will be
11656 * displayed below the widget.
11657 * The array may contain strings or OO.ui.HtmlSnippet instances.
11658 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11659 * below the widget.
11660 * The array may contain strings or OO.ui.HtmlSnippet instances.
11661 * These are more visible than `help` messages when `helpInline` is set, and so
11662 * might be good for transient messages.
11663 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
11664 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
11665 * corner of the rendered field; clicking it will display the text in a popup.
11666 * If `helpInline` is `true`, then a subtle description will be shown after the
11668 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
11669 * or shown when the "help" icon is clicked.
11670 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
11672 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11674 * @throws {Error} An error is thrown if no widget is specified
11676 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11677 // Allow passing positional parameters inside the config object
11678 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11679 config
= fieldWidget
;
11680 fieldWidget
= config
.fieldWidget
;
11683 // Make sure we have required constructor arguments
11684 if ( fieldWidget
=== undefined ) {
11685 throw new Error( 'Widget not found' );
11688 // Configuration initialization
11689 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
11691 // Parent constructor
11692 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11694 // Mixin constructors
11695 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
11696 $label
: $( '<label>' )
11698 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
11701 this.fieldWidget
= fieldWidget
;
11704 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11705 this.$messages
= $( '<ul>' );
11706 this.$header
= $( '<span>' );
11707 this.$body
= $( '<div>' );
11709 this.helpInline
= config
.helpInline
;
11712 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
11715 this.$help
= config
.help
?
11716 this.createHelpElement( config
.help
, config
.$overlay
) :
11718 if ( this.fieldWidget
.getInputId() ) {
11719 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11720 if ( this.helpInline
) {
11721 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
11724 this.$label
.on( 'click', function () {
11725 this.fieldWidget
.simulateLabelClick();
11727 if ( this.helpInline
) {
11728 this.$help
.on( 'click', function () {
11729 this.fieldWidget
.simulateLabelClick();
11734 .addClass( 'oo-ui-fieldLayout' )
11735 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11736 .append( this.$body
);
11737 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11738 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11739 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11741 .addClass( 'oo-ui-fieldLayout-field' )
11742 .append( this.fieldWidget
.$element
);
11744 this.setErrors( config
.errors
|| [] );
11745 this.setNotices( config
.notices
|| [] );
11746 this.setAlignment( config
.align
);
11747 // Call this again to take into account the widget's accessKey
11748 this.updateTitle();
11753 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11754 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11755 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
11760 * Handle field disable events.
11763 * @param {boolean} value Field is disabled
11765 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
11766 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11770 * Get the widget contained by the field.
11772 * @return {OO.ui.Widget} Field widget
11774 OO
.ui
.FieldLayout
.prototype.getField = function () {
11775 return this.fieldWidget
;
11779 * Return `true` if the given field widget can be used with `'inline'` alignment (see
11780 * #setAlignment). Return `false` if it can't or if this can't be determined.
11782 * @return {boolean}
11784 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11785 // This is very simplistic, but should be good enough.
11786 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11791 * @param {string} kind 'error' or 'notice'
11792 * @param {string|OO.ui.HtmlSnippet} text
11795 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11796 var $listItem
, $icon
, message
;
11797 $listItem
= $( '<li>' );
11798 if ( kind
=== 'error' ) {
11799 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11800 $listItem
.attr( 'role', 'alert' );
11801 } else if ( kind
=== 'notice' ) {
11802 $icon
= new OO
.ui
.IconWidget( { icon
: 'notice' } ).$element
;
11806 message
= new OO
.ui
.LabelWidget( { label
: text
} );
11808 .append( $icon
, message
.$element
)
11809 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
11814 * Set the field alignment mode.
11817 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
11819 * @return {OO.ui.BookletLayout} The layout, for chaining
11821 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
11822 if ( value
!== this.align
) {
11823 // Default to 'left'
11824 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
11828 if ( value
=== 'inline' && !this.isFieldInline() ) {
11831 // Reorder elements
11833 if ( this.helpInline
) {
11834 if ( value
=== 'top' ) {
11835 this.$header
.append( this.$label
);
11836 this.$body
.append( this.$header
, this.$field
, this.$help
);
11837 } else if ( value
=== 'inline' ) {
11838 this.$header
.append( this.$label
, this.$help
);
11839 this.$body
.append( this.$field
, this.$header
);
11841 this.$header
.append( this.$label
, this.$help
);
11842 this.$body
.append( this.$header
, this.$field
);
11845 if ( value
=== 'top' ) {
11846 this.$header
.append( this.$help
, this.$label
);
11847 this.$body
.append( this.$header
, this.$field
);
11848 } else if ( value
=== 'inline' ) {
11849 this.$header
.append( this.$help
, this.$label
);
11850 this.$body
.append( this.$field
, this.$header
);
11852 this.$header
.append( this.$label
);
11853 this.$body
.append( this.$header
, this.$help
, this.$field
);
11856 // Set classes. The following classes can be used here:
11857 // * oo-ui-fieldLayout-align-left
11858 // * oo-ui-fieldLayout-align-right
11859 // * oo-ui-fieldLayout-align-top
11860 // * oo-ui-fieldLayout-align-inline
11861 if ( this.align
) {
11862 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
11864 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
11865 this.align
= value
;
11872 * Set the list of error messages.
11874 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11875 * The array may contain strings or OO.ui.HtmlSnippet instances.
11877 * @return {OO.ui.BookletLayout} The layout, for chaining
11879 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
11880 this.errors
= errors
.slice();
11881 this.updateMessages();
11886 * Set the list of notice messages.
11888 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11889 * The array may contain strings or OO.ui.HtmlSnippet instances.
11891 * @return {OO.ui.BookletLayout} The layout, for chaining
11893 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
11894 this.notices
= notices
.slice();
11895 this.updateMessages();
11900 * Update the rendering of error and notice messages.
11904 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
11906 this.$messages
.empty();
11908 if ( this.errors
.length
|| this.notices
.length
) {
11909 this.$body
.after( this.$messages
);
11911 this.$messages
.remove();
11915 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
11916 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
11918 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
11919 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
11924 * Include information about the widget's accessKey in our title. TitledElement calls this method.
11925 * (This is a bit of a hack.)
11928 * @param {string} title Tooltip label for 'title' attribute
11931 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
11932 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
11933 return this.fieldWidget
.formatTitleWithAccessKey( title
);
11939 * Creates and returns the help element. Also sets the `aria-describedby`
11940 * attribute on the main element of the `fieldWidget`.
11943 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
11944 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
11945 * @return {jQuery} The element that should become `this.$help`.
11947 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
11948 var helpId
, helpWidget
;
11950 if ( this.helpInline
) {
11951 helpWidget
= new OO
.ui
.LabelWidget( {
11953 classes
: [ 'oo-ui-inline-help' ]
11956 helpId
= helpWidget
.getElementId();
11958 helpWidget
= new OO
.ui
.PopupButtonWidget( {
11959 $overlay
: $overlay
,
11963 classes
: [ 'oo-ui-fieldLayout-help' ],
11966 label
: OO
.ui
.msg( 'ooui-field-help' ),
11967 invisibleLabel
: true
11969 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
11970 helpWidget
.getPopup().$body
.html( help
.toString() );
11972 helpWidget
.getPopup().$body
.text( help
);
11975 helpId
= helpWidget
.getPopup().getBodyId();
11978 // Set the 'aria-describedby' attribute on the fieldWidget
11979 // Preference given to an input or a button
11981 this.fieldWidget
.$input
||
11982 this.fieldWidget
.$button
||
11983 this.fieldWidget
.$element
11984 ).attr( 'aria-describedby', helpId
);
11986 return helpWidget
.$element
;
11990 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
11991 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
11992 * is required and is specified before any optional configuration settings.
11994 * Labels can be aligned in one of four ways:
11996 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11997 * A left-alignment is used for forms with many fields.
11998 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11999 * A right-alignment is used for long but familiar forms which users tab through,
12000 * verifying the current field with a quick glance at the label.
12001 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12002 * that users fill out from top to bottom.
12003 * - **inline**: The label is placed after the field-widget and aligned to the left.
12004 * An inline-alignment is best used with checkboxes or radio buttons.
12006 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
12007 * text is specified.
12010 * // Example of an ActionFieldLayout
12011 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
12012 * new OO.ui.TextInputWidget( {
12013 * placeholder: 'Field widget'
12015 * new OO.ui.ButtonWidget( {
12019 * label: 'An ActionFieldLayout. This label is aligned top',
12021 * help: 'This is help text'
12025 * $( document.body ).append( actionFieldLayout.$element );
12028 * @extends OO.ui.FieldLayout
12031 * @param {OO.ui.Widget} fieldWidget Field widget
12032 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
12033 * @param {Object} config
12035 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
12036 // Allow passing positional parameters inside the config object
12037 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12038 config
= fieldWidget
;
12039 fieldWidget
= config
.fieldWidget
;
12040 buttonWidget
= config
.buttonWidget
;
12043 // Parent constructor
12044 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
12047 this.buttonWidget
= buttonWidget
;
12048 this.$button
= $( '<span>' );
12049 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12053 .addClass( 'oo-ui-actionFieldLayout' );
12055 .addClass( 'oo-ui-actionFieldLayout-button' )
12056 .append( this.buttonWidget
.$element
);
12058 .addClass( 'oo-ui-actionFieldLayout-input' )
12059 .append( this.fieldWidget
.$element
);
12061 .append( this.$input
, this.$button
);
12066 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12069 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12070 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12071 * configured with a label as well. For more information and examples,
12072 * please see the [OOUI documentation on MediaWiki][1].
12075 * // Example of a fieldset layout
12076 * var input1 = new OO.ui.TextInputWidget( {
12077 * placeholder: 'A text input field'
12080 * var input2 = new OO.ui.TextInputWidget( {
12081 * placeholder: 'A text input field'
12084 * var fieldset = new OO.ui.FieldsetLayout( {
12085 * label: 'Example of a fieldset layout'
12088 * fieldset.addItems( [
12089 * new OO.ui.FieldLayout( input1, {
12090 * label: 'Field One'
12092 * new OO.ui.FieldLayout( input2, {
12093 * label: 'Field Two'
12096 * $( document.body ).append( fieldset.$element );
12098 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12101 * @extends OO.ui.Layout
12102 * @mixins OO.ui.mixin.IconElement
12103 * @mixins OO.ui.mixin.LabelElement
12104 * @mixins OO.ui.mixin.GroupElement
12107 * @param {Object} [config] Configuration options
12108 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
12109 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
12110 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
12111 * For important messages, you are advised to use `notices`, as they are always shown.
12112 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12113 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12115 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12116 // Configuration initialization
12117 config
= config
|| {};
12119 // Parent constructor
12120 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12122 // Mixin constructors
12123 OO
.ui
.mixin
.IconElement
.call( this, config
);
12124 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12125 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12128 this.$header
= $( '<legend>' );
12129 if ( config
.help
) {
12130 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
12131 $overlay
: config
.$overlay
,
12135 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12138 label
: OO
.ui
.msg( 'ooui-field-help' ),
12139 invisibleLabel
: true
12141 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12142 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
12144 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
12146 this.$help
= this.popupButtonWidget
.$element
;
12148 this.$help
= $( [] );
12153 .addClass( 'oo-ui-fieldsetLayout-header' )
12154 .append( this.$icon
, this.$label
, this.$help
);
12155 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12157 .addClass( 'oo-ui-fieldsetLayout' )
12158 .prepend( this.$header
, this.$group
);
12159 if ( Array
.isArray( config
.items
) ) {
12160 this.addItems( config
.items
);
12166 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12167 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12168 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12169 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12171 /* Static Properties */
12177 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12180 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
12181 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
12182 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
12183 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12185 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12186 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12187 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12188 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12189 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12190 * often have simplified APIs to match the capabilities of HTML forms.
12191 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12193 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12194 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12197 * // Example of a form layout that wraps a fieldset layout
12198 * var input1 = new OO.ui.TextInputWidget( {
12199 * placeholder: 'Username'
12201 * var input2 = new OO.ui.TextInputWidget( {
12202 * placeholder: 'Password',
12205 * var submit = new OO.ui.ButtonInputWidget( {
12209 * var fieldset = new OO.ui.FieldsetLayout( {
12210 * label: 'A form layout'
12212 * fieldset.addItems( [
12213 * new OO.ui.FieldLayout( input1, {
12214 * label: 'Username',
12217 * new OO.ui.FieldLayout( input2, {
12218 * label: 'Password',
12221 * new OO.ui.FieldLayout( submit )
12223 * var form = new OO.ui.FormLayout( {
12224 * items: [ fieldset ],
12225 * action: '/api/formhandler',
12228 * $( document.body ).append( form.$element );
12231 * @extends OO.ui.Layout
12232 * @mixins OO.ui.mixin.GroupElement
12235 * @param {Object} [config] Configuration options
12236 * @cfg {string} [method] HTML form `method` attribute
12237 * @cfg {string} [action] HTML form `action` attribute
12238 * @cfg {string} [enctype] HTML form `enctype` attribute
12239 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12241 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12244 // Configuration initialization
12245 config
= config
|| {};
12247 // Parent constructor
12248 OO
.ui
.FormLayout
.parent
.call( this, config
);
12250 // Mixin constructors
12251 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12254 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12256 // Make sure the action is safe
12257 action
= config
.action
;
12258 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12259 action
= './' + action
;
12264 .addClass( 'oo-ui-formLayout' )
12266 method
: config
.method
,
12268 enctype
: config
.enctype
12270 if ( Array
.isArray( config
.items
) ) {
12271 this.addItems( config
.items
);
12277 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12278 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12283 * A 'submit' event is emitted when the form is submitted.
12288 /* Static Properties */
12294 OO
.ui
.FormLayout
.static.tagName
= 'form';
12299 * Handle form submit events.
12302 * @param {jQuery.Event} e Submit event
12304 * @return {OO.ui.FormLayout} The layout, for chaining
12306 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12307 if ( this.emit( 'submit' ) ) {
12313 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
12314 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
12317 * // Example of a panel layout
12318 * var panel = new OO.ui.PanelLayout( {
12322 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12324 * $( document.body ).append( panel.$element );
12327 * @extends OO.ui.Layout
12330 * @param {Object} [config] Configuration options
12331 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12332 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12333 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12334 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
12336 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12337 // Configuration initialization
12338 config
= $.extend( {
12345 // Parent constructor
12346 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12349 this.$element
.addClass( 'oo-ui-panelLayout' );
12350 if ( config
.scrollable
) {
12351 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12353 if ( config
.padded
) {
12354 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12356 if ( config
.expanded
) {
12357 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12359 if ( config
.framed
) {
12360 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12366 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12371 * Focus the panel layout
12373 * The default implementation just focuses the first focusable element in the panel
12375 OO
.ui
.PanelLayout
.prototype.focus = function () {
12376 OO
.ui
.findFocusable( this.$element
).focus();
12380 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12381 * items), with small margins between them. Convenient when you need to put a number of block-level
12382 * widgets on a single line next to each other.
12384 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12387 * // HorizontalLayout with a text input and a label
12388 * var layout = new OO.ui.HorizontalLayout( {
12390 * new OO.ui.LabelWidget( { label: 'Label' } ),
12391 * new OO.ui.TextInputWidget( { value: 'Text' } )
12394 * $( document.body ).append( layout.$element );
12397 * @extends OO.ui.Layout
12398 * @mixins OO.ui.mixin.GroupElement
12401 * @param {Object} [config] Configuration options
12402 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12404 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12405 // Configuration initialization
12406 config
= config
|| {};
12408 // Parent constructor
12409 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12411 // Mixin constructors
12412 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12415 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12416 if ( Array
.isArray( config
.items
) ) {
12417 this.addItems( config
.items
);
12423 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12424 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12427 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12428 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12429 * (to adjust the value in increments) to allow the user to enter a number.
12432 * // A NumberInputWidget.
12433 * var numberInput = new OO.ui.NumberInputWidget( {
12434 * label: 'NumberInputWidget',
12435 * input: { value: 5 },
12439 * $( document.body ).append( numberInput.$element );
12442 * @extends OO.ui.TextInputWidget
12445 * @param {Object} [config] Configuration options
12446 * @cfg {Object} [minusButton] Configuration options to pass to the
12447 * {@link OO.ui.ButtonWidget decrementing button widget}.
12448 * @cfg {Object} [plusButton] Configuration options to pass to the
12449 * {@link OO.ui.ButtonWidget incrementing button widget}.
12450 * @cfg {number} [min=-Infinity] Minimum allowed value
12451 * @cfg {number} [max=Infinity] Maximum allowed value
12452 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12453 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12454 * Defaults to `step` if specified, otherwise `1`.
12455 * @cfg {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12456 * Defaults to 10 times `buttonStep`.
12457 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12459 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12460 var $field
= $( '<div>' )
12461 .addClass( 'oo-ui-numberInputWidget-field' );
12463 // Configuration initialization
12464 config
= $.extend( {
12470 // For backward compatibility
12471 $.extend( config
, config
.input
);
12474 // Parent constructor
12475 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12479 if ( config
.showButtons
) {
12480 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12482 disabled
: this.isDisabled(),
12484 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12489 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12490 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12492 disabled
: this.isDisabled(),
12494 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12499 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12504 keydown
: this.onKeyDown
.bind( this ),
12505 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12507 if ( config
.showButtons
) {
12508 this.plusButton
.connect( this, {
12509 click
: [ 'onButtonClick', +1 ]
12511 this.minusButton
.connect( this, {
12512 click
: [ 'onButtonClick', -1 ]
12517 $field
.append( this.$input
);
12518 if ( config
.showButtons
) {
12520 .prepend( this.minusButton
.$element
)
12521 .append( this.plusButton
.$element
);
12525 if ( config
.allowInteger
|| config
.isInteger
) {
12526 // Backward compatibility
12529 this.setRange( config
.min
, config
.max
);
12530 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
12531 // Set the validation method after we set step and range
12532 // so that it doesn't immediately call setValidityFlag
12533 this.setValidation( this.validateNumber
.bind( this ) );
12536 .addClass( 'oo-ui-numberInputWidget' )
12537 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12543 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12547 // Backward compatibility
12548 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12549 this.setStep( flag
? 1 : null );
12551 // Backward compatibility
12552 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12554 // Backward compatibility
12555 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12556 return this.step
=== 1;
12558 // Backward compatibility
12559 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12562 * Set the range of allowed values
12564 * @param {number} min Minimum allowed value
12565 * @param {number} max Maximum allowed value
12567 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12569 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12573 this.$input
.attr( 'min', this.min
);
12574 this.$input
.attr( 'max', this.max
);
12575 this.setValidityFlag();
12579 * Get the current range
12581 * @return {number[]} Minimum and maximum values
12583 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12584 return [ this.min
, this.max
];
12588 * Set the stepping deltas
12590 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12591 * Defaults to `step` if specified, otherwise `1`.
12592 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12593 * Defaults to 10 times `buttonStep`.
12594 * @param {number|null} [step] If specified, the field only accepts values that are multiples of this.
12596 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
12597 if ( buttonStep
=== undefined ) {
12598 buttonStep
= step
|| 1;
12600 if ( pageStep
=== undefined ) {
12601 pageStep
= 10 * buttonStep
;
12603 if ( step
!== null && step
<= 0 ) {
12604 throw new Error( 'Step value, if given, must be positive' );
12606 if ( buttonStep
<= 0 ) {
12607 throw new Error( 'Button step value must be positive' );
12609 if ( pageStep
<= 0 ) {
12610 throw new Error( 'Page step value must be positive' );
12613 this.buttonStep
= buttonStep
;
12614 this.pageStep
= pageStep
;
12615 this.$input
.attr( 'step', this.step
|| 'any' );
12616 this.setValidityFlag();
12622 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
12623 if ( value
=== '' ) {
12624 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
12625 // so here we make sure an 'empty' value is actually displayed as such.
12626 this.$input
.val( '' );
12628 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
12632 * Get the current stepping values
12634 * @return {number[]} Button step, page step, and validity step
12636 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
12637 return [ this.buttonStep
, this.pageStep
, this.step
];
12641 * Get the current value of the widget as a number
12643 * @return {number} May be NaN, or an invalid number
12645 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
12646 return +this.getValue();
12650 * Adjust the value of the widget
12652 * @param {number} delta Adjustment amount
12654 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
12655 var n
, v
= this.getNumericValue();
12658 if ( isNaN( delta
) || !isFinite( delta
) ) {
12659 throw new Error( 'Delta must be a finite number' );
12662 if ( isNaN( v
) ) {
12666 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
12668 n
= Math
.round( n
/ this.step
) * this.step
;
12673 this.setValue( n
);
12680 * @param {string} value Field value
12681 * @return {boolean}
12683 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
12685 if ( value
=== '' ) {
12686 return !this.isRequired();
12689 if ( isNaN( n
) || !isFinite( n
) ) {
12693 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
12697 if ( n
< this.min
|| n
> this.max
) {
12705 * Handle mouse click events.
12708 * @param {number} dir +1 or -1
12710 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
12711 this.adjustValue( dir
* this.buttonStep
);
12715 * Handle mouse wheel events.
12718 * @param {jQuery.Event} event
12719 * @return {undefined/boolean} False to prevent default if event is handled
12721 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
12724 if ( !this.isDisabled() && this.$input
.is( ':focus' ) ) {
12725 // Standard 'wheel' event
12726 if ( event
.originalEvent
.deltaMode
!== undefined ) {
12727 this.sawWheelEvent
= true;
12729 if ( event
.originalEvent
.deltaY
) {
12730 delta
= -event
.originalEvent
.deltaY
;
12731 } else if ( event
.originalEvent
.deltaX
) {
12732 delta
= event
.originalEvent
.deltaX
;
12735 // Non-standard events
12736 if ( !this.sawWheelEvent
) {
12737 if ( event
.originalEvent
.wheelDeltaX
) {
12738 delta
= -event
.originalEvent
.wheelDeltaX
;
12739 } else if ( event
.originalEvent
.wheelDeltaY
) {
12740 delta
= event
.originalEvent
.wheelDeltaY
;
12741 } else if ( event
.originalEvent
.wheelDelta
) {
12742 delta
= event
.originalEvent
.wheelDelta
;
12743 } else if ( event
.originalEvent
.detail
) {
12744 delta
= -event
.originalEvent
.detail
;
12749 delta
= delta
< 0 ? -1 : 1;
12750 this.adjustValue( delta
* this.buttonStep
);
12758 * Handle key down events.
12761 * @param {jQuery.Event} e Key down event
12762 * @return {undefined/boolean} False to prevent default if event is handled
12764 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
12765 if ( !this.isDisabled() ) {
12766 switch ( e
.which
) {
12767 case OO
.ui
.Keys
.UP
:
12768 this.adjustValue( this.buttonStep
);
12770 case OO
.ui
.Keys
.DOWN
:
12771 this.adjustValue( -this.buttonStep
);
12773 case OO
.ui
.Keys
.PAGEUP
:
12774 this.adjustValue( this.pageStep
);
12776 case OO
.ui
.Keys
.PAGEDOWN
:
12777 this.adjustValue( -this.pageStep
);
12786 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
12788 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
12790 if ( this.minusButton
) {
12791 this.minusButton
.setDisabled( this.isDisabled() );
12793 if ( this.plusButton
) {
12794 this.plusButton
.setDisabled( this.isDisabled() );
12802 //# sourceMappingURL=oojs-ui-core.js.map.json