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' )
8052 'aria-owns': this.menu
.getElementId(),
8053 'aria-haspopup': 'listbox'
8055 .append( this.$icon
, this.$label
, this.$indicator
);
8057 .addClass( 'oo-ui-dropdownWidget' )
8058 .append( this.$handle
);
8059 this.$overlay
.append( this.menu
.$element
);
8064 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
8065 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
8066 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
8067 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
8068 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
8069 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8076 * @return {OO.ui.MenuSelectWidget} Menu of widget
8078 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
8083 * Handles menu select events.
8086 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8088 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
8092 this.setLabel( null );
8096 selectedLabel
= item
.getLabel();
8098 // If the label is a DOM element, clone it, because setLabel will append() it
8099 if ( selectedLabel
instanceof $ ) {
8100 selectedLabel
= selectedLabel
.clone();
8103 this.setLabel( selectedLabel
);
8107 * Handle menu toggle events.
8110 * @param {boolean} isVisible Open state of the menu
8112 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
8113 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
8117 * Handle mouse click events.
8120 * @param {jQuery.Event} e Mouse click event
8121 * @return {undefined/boolean} False to prevent default if event is handled
8123 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
8124 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8131 * Handle key down events.
8134 * @param {jQuery.Event} e Key down event
8135 * @return {undefined/boolean} False to prevent default if event is handled
8137 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
8139 !this.isDisabled() &&
8141 e
.which
=== OO
.ui
.Keys
.ENTER
||
8143 e
.which
=== OO
.ui
.Keys
.SPACE
&&
8144 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
8145 // Space only closes the menu is the user is not typing to search.
8146 this.menu
.keyPressBuffer
=== ''
8149 !this.menu
.isVisible() &&
8151 e
.which
=== OO
.ui
.Keys
.UP
||
8152 e
.which
=== OO
.ui
.Keys
.DOWN
8163 * RadioOptionWidget is an option widget that looks like a radio button.
8164 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
8165 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8167 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8170 * @extends OO.ui.OptionWidget
8173 * @param {Object} [config] Configuration options
8175 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
8176 // Configuration initialization
8177 config
= config
|| {};
8179 // Properties (must be done before parent constructor which calls #setDisabled)
8180 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
8182 // Parent constructor
8183 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
8186 // Remove implicit role, we're handling it ourselves
8187 this.radio
.$input
.attr( 'role', 'presentation' );
8189 .addClass( 'oo-ui-radioOptionWidget' )
8190 .attr( 'role', 'radio' )
8191 .attr( 'aria-checked', 'false' )
8192 .removeAttr( 'aria-selected' )
8193 .prepend( this.radio
.$element
);
8198 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
8200 /* Static Properties */
8206 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
8212 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
8218 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
8224 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
8231 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
8232 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8234 this.radio
.setSelected( state
);
8236 .attr( 'aria-checked', state
.toString() )
8237 .removeAttr( 'aria-selected' );
8245 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
8246 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8248 this.radio
.setDisabled( this.isDisabled() );
8254 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8255 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8256 * an interface for adding, removing and selecting options.
8257 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8259 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8260 * OO.ui.RadioSelectInputWidget instead.
8263 * // A RadioSelectWidget with RadioOptions.
8264 * var option1 = new OO.ui.RadioOptionWidget( {
8266 * label: 'Selected radio option'
8268 * option2 = new OO.ui.RadioOptionWidget( {
8270 * label: 'Unselected radio option'
8272 * radioSelect = new OO.ui.RadioSelectWidget( {
8273 * items: [ option1, option2 ]
8276 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8277 * radioSelect.selectItem( option1 );
8279 * $( document.body ).append( radioSelect.$element );
8281 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8285 * @extends OO.ui.SelectWidget
8286 * @mixins OO.ui.mixin.TabIndexedElement
8289 * @param {Object} [config] Configuration options
8291 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8292 // Parent constructor
8293 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8295 // Mixin constructors
8296 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8300 focus
: this.bindDocumentKeyDownListener
.bind( this ),
8301 blur
: this.unbindDocumentKeyDownListener
.bind( this )
8306 .addClass( 'oo-ui-radioSelectWidget' )
8307 .attr( 'role', 'radiogroup' );
8312 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8313 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8316 * MultioptionWidgets are special elements that can be selected and configured with data. The
8317 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8318 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8319 * and examples, please see the [OOUI documentation on MediaWiki][1].
8321 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Multioptions
8324 * @extends OO.ui.Widget
8325 * @mixins OO.ui.mixin.ItemWidget
8326 * @mixins OO.ui.mixin.LabelElement
8327 * @mixins OO.ui.mixin.TitledElement
8330 * @param {Object} [config] Configuration options
8331 * @cfg {boolean} [selected=false] Whether the option is initially selected
8333 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8334 // Configuration initialization
8335 config
= config
|| {};
8337 // Parent constructor
8338 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8340 // Mixin constructors
8341 OO
.ui
.mixin
.ItemWidget
.call( this );
8342 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8343 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8346 this.selected
= null;
8350 .addClass( 'oo-ui-multioptionWidget' )
8351 .append( this.$label
);
8352 this.setSelected( config
.selected
);
8357 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8358 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8359 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8360 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.TitledElement
);
8367 * A change event is emitted when the selected state of the option changes.
8369 * @param {boolean} selected Whether the option is now selected
8375 * Check if the option is selected.
8377 * @return {boolean} Item is selected
8379 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8380 return this.selected
;
8384 * Set the option’s selected state. In general, all modifications to the selection
8385 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
8386 * method instead of this method.
8388 * @param {boolean} [state=false] Select option
8390 * @return {OO.ui.Widget} The widget, for chaining
8392 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8394 if ( this.selected
!== state
) {
8395 this.selected
= state
;
8396 this.emit( 'change', state
);
8397 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8403 * MultiselectWidget allows selecting multiple options from a list.
8405 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
8407 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8411 * @extends OO.ui.Widget
8412 * @mixins OO.ui.mixin.GroupWidget
8413 * @mixins OO.ui.mixin.TitledElement
8416 * @param {Object} [config] Configuration options
8417 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8419 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8420 // Parent constructor
8421 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8423 // Configuration initialization
8424 config
= config
|| {};
8426 // Mixin constructors
8427 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8428 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8431 this.aggregate( { change
: 'select' } );
8432 // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
8433 // by GroupElement only when items are added/removed
8434 this.connect( this, { select
: [ 'emit', 'change' ] } );
8437 if ( config
.items
) {
8438 this.addItems( config
.items
);
8440 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8441 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8442 .append( this.$group
);
8447 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8448 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8449 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.TitledElement
);
8456 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8462 * A select event is emitted when an item is selected or deselected.
8468 * Find options that are selected.
8470 * @return {OO.ui.MultioptionWidget[]} Selected options
8472 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8473 return this.items
.filter( function ( item
) {
8474 return item
.isSelected();
8479 * Find the data of options that are selected.
8481 * @return {Object[]|string[]} Values of selected options
8483 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8484 return this.findSelectedItems().map( function ( item
) {
8490 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8492 * @param {OO.ui.MultioptionWidget[]} items Items to select
8494 * @return {OO.ui.Widget} The widget, for chaining
8496 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8497 this.items
.forEach( function ( item
) {
8498 var selected
= items
.indexOf( item
) !== -1;
8499 item
.setSelected( selected
);
8505 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8507 * @param {Object[]|string[]} datas Values of items to select
8509 * @return {OO.ui.Widget} The widget, for chaining
8511 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8514 items
= datas
.map( function ( data
) {
8515 return widget
.findItemFromData( data
);
8517 this.selectItems( items
);
8522 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8523 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8524 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8526 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8529 * @extends OO.ui.MultioptionWidget
8532 * @param {Object} [config] Configuration options
8534 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8535 // Configuration initialization
8536 config
= config
|| {};
8538 // Properties (must be done before parent constructor which calls #setDisabled)
8539 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8541 // Parent constructor
8542 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8545 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8546 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8550 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8551 .prepend( this.checkbox
.$element
);
8556 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8558 /* Static Properties */
8564 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8569 * Handle checkbox selected state change.
8573 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8574 this.setSelected( this.checkbox
.isSelected() );
8580 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8581 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8582 this.checkbox
.setSelected( state
);
8589 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8590 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8591 this.checkbox
.setDisabled( this.isDisabled() );
8598 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8599 this.checkbox
.focus();
8603 * Handle key down events.
8606 * @param {jQuery.Event} e
8608 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8610 element
= this.getElementGroup(),
8613 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8614 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8615 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8616 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8626 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8627 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8628 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8629 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8631 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8632 * OO.ui.CheckboxMultiselectInputWidget instead.
8635 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8636 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8639 * label: 'Selected checkbox'
8641 * option2 = new OO.ui.CheckboxMultioptionWidget( {
8643 * label: 'Unselected checkbox'
8645 * multiselect = new OO.ui.CheckboxMultiselectWidget( {
8646 * items: [ option1, option2 ]
8648 * $( document.body ).append( multiselect.$element );
8650 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8653 * @extends OO.ui.MultiselectWidget
8656 * @param {Object} [config] Configuration options
8658 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8659 // Parent constructor
8660 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8663 this.$lastClicked
= null;
8666 this.$group
.on( 'click', this.onClick
.bind( this ) );
8670 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8675 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8680 * Get an option by its position relative to the specified item (or to the start of the option array,
8681 * if item is `null`). The direction in which to search through the option array is specified with a
8682 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8683 * `null` if there are no options in the array.
8685 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8686 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8687 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8689 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8690 var currentIndex
, nextIndex
, i
,
8691 increase
= direction
> 0 ? 1 : -1,
8692 len
= this.items
.length
;
8695 currentIndex
= this.items
.indexOf( item
);
8696 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8698 // If no item is selected and moving forward, start at the beginning.
8699 // If moving backward, start at the end.
8700 nextIndex
= direction
> 0 ? 0 : len
- 1;
8703 for ( i
= 0; i
< len
; i
++ ) {
8704 item
= this.items
[ nextIndex
];
8705 if ( item
&& !item
.isDisabled() ) {
8708 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8714 * Handle click events on checkboxes.
8716 * @param {jQuery.Event} e
8718 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8719 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8720 $lastClicked
= this.$lastClicked
,
8721 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8722 .not( '.oo-ui-widget-disabled' );
8724 // Allow selecting multiple options at once by Shift-clicking them
8725 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8726 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8727 lastClickedIndex
= $options
.index( $lastClicked
);
8728 nowClickedIndex
= $options
.index( $nowClicked
);
8729 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8730 // browser. In either case we don't need custom handling.
8731 if ( nowClickedIndex
!== lastClickedIndex
) {
8733 wasSelected
= items
[ nowClickedIndex
].isSelected();
8734 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8736 // This depends on the DOM order of the items and the order of the .items array being the same.
8737 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8738 if ( !items
[ i
].isDisabled() ) {
8739 items
[ i
].setSelected( !wasSelected
);
8742 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8743 // handling first, then set our value. The order in which events happen is different for
8744 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8745 // non-click actions that change the checkboxes.
8747 setTimeout( function () {
8748 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8749 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8755 if ( $nowClicked
.length
) {
8756 this.$lastClicked
= $nowClicked
;
8764 * @return {OO.ui.Widget} The widget, for chaining
8766 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8768 if ( !this.isDisabled() ) {
8769 item
= this.getRelativeFocusableItem( null, 1 );
8780 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8785 * Progress bars visually display the status of an operation, such as a download,
8786 * and can be either determinate or indeterminate:
8788 * - **determinate** process bars show the percent of an operation that is complete.
8790 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8791 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8792 * not use percentages.
8794 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8797 * // Examples of determinate and indeterminate progress bars.
8798 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8801 * var progressBar2 = new OO.ui.ProgressBarWidget();
8803 * // Create a FieldsetLayout to layout progress bars.
8804 * var fieldset = new OO.ui.FieldsetLayout;
8805 * fieldset.addItems( [
8806 * new OO.ui.FieldLayout( progressBar1, {
8807 * label: 'Determinate',
8810 * new OO.ui.FieldLayout( progressBar2, {
8811 * label: 'Indeterminate',
8815 * $( document.body ).append( fieldset.$element );
8818 * @extends OO.ui.Widget
8821 * @param {Object} [config] Configuration options
8822 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8823 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
8824 * By default, the progress bar is indeterminate.
8826 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8827 // Configuration initialization
8828 config
= config
|| {};
8830 // Parent constructor
8831 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8834 this.$bar
= $( '<div>' );
8835 this.progress
= null;
8838 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8839 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8842 role
: 'progressbar',
8844 'aria-valuemax': 100
8846 .addClass( 'oo-ui-progressBarWidget' )
8847 .append( this.$bar
);
8852 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8854 /* Static Properties */
8860 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8865 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8867 * @return {number|boolean} Progress percent
8869 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8870 return this.progress
;
8874 * Set the percent of the process completed or `false` for an indeterminate process.
8876 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8878 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8879 this.progress
= progress
;
8881 if ( progress
!== false ) {
8882 this.$bar
.css( 'width', this.progress
+ '%' );
8883 this.$element
.attr( 'aria-valuenow', this.progress
);
8885 this.$bar
.css( 'width', '' );
8886 this.$element
.removeAttr( 'aria-valuenow' );
8888 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8892 * InputWidget is the base class for all input widgets, which
8893 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8894 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8895 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
8897 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
8901 * @extends OO.ui.Widget
8902 * @mixins OO.ui.mixin.FlaggedElement
8903 * @mixins OO.ui.mixin.TabIndexedElement
8904 * @mixins OO.ui.mixin.TitledElement
8905 * @mixins OO.ui.mixin.AccessKeyedElement
8908 * @param {Object} [config] Configuration options
8909 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8910 * @cfg {string} [value=''] The value of the input.
8911 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8912 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8913 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8914 * before it is accepted.
8916 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8917 // Configuration initialization
8918 config
= config
|| {};
8920 // Parent constructor
8921 OO
.ui
.InputWidget
.parent
.call( this, config
);
8924 // See #reusePreInfuseDOM about config.$input
8925 this.$input
= config
.$input
|| this.getInputElement( config
);
8927 this.inputFilter
= config
.inputFilter
;
8929 // Mixin constructors
8930 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8931 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8932 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8933 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8936 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8940 .addClass( 'oo-ui-inputWidget-input' )
8941 .attr( 'name', config
.name
)
8942 .prop( 'disabled', this.isDisabled() );
8944 .addClass( 'oo-ui-inputWidget' )
8945 .append( this.$input
);
8946 this.setValue( config
.value
);
8948 this.setDir( config
.dir
);
8950 if ( config
.inputId
!== undefined ) {
8951 this.setInputId( config
.inputId
);
8957 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8958 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8959 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8960 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8961 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8963 /* Static Methods */
8968 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8969 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8970 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8971 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8978 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8979 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8980 if ( config
.$input
&& config
.$input
.length
) {
8981 state
.value
= config
.$input
.val();
8982 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8983 state
.focus
= config
.$input
.is( ':focus' );
8993 * A change event is emitted when the value of the input changes.
8995 * @param {string} value
9001 * Get input element.
9003 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
9004 * different circumstances. The element must have a `value` property (like form elements).
9007 * @param {Object} config Configuration options
9008 * @return {jQuery} Input element
9010 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
9011 return $( '<input>' );
9015 * Handle potentially value-changing events.
9018 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
9020 OO
.ui
.InputWidget
.prototype.onEdit = function () {
9022 if ( !this.isDisabled() ) {
9023 // Allow the stack to clear so the value will be updated
9024 setTimeout( function () {
9025 widget
.setValue( widget
.$input
.val() );
9031 * Get the value of the input.
9033 * @return {string} Input value
9035 OO
.ui
.InputWidget
.prototype.getValue = function () {
9036 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9037 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9038 var value
= this.$input
.val();
9039 if ( this.value
!== value
) {
9040 this.setValue( value
);
9046 * Set the directionality of the input.
9048 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
9050 * @return {OO.ui.Widget} The widget, for chaining
9052 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
9053 this.$input
.prop( 'dir', dir
);
9058 * Set the value of the input.
9060 * @param {string} value New value
9063 * @return {OO.ui.Widget} The widget, for chaining
9065 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
9066 value
= this.cleanUpValue( value
);
9067 // Update the DOM if it has changed. Note that with cleanUpValue, it
9068 // is possible for the DOM value to change without this.value changing.
9069 if ( this.$input
.val() !== value
) {
9070 this.$input
.val( value
);
9072 if ( this.value
!== value
) {
9074 this.emit( 'change', this.value
);
9076 // The first time that the value is set (probably while constructing the widget),
9077 // remember it in defaultValue. This property can be later used to check whether
9078 // the value of the input has been changed since it was created.
9079 if ( this.defaultValue
=== undefined ) {
9080 this.defaultValue
= this.value
;
9081 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
9087 * Clean up incoming value.
9089 * Ensures value is a string, and converts undefined and null to empty string.
9092 * @param {string} value Original value
9093 * @return {string} Cleaned up value
9095 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
9096 if ( value
=== undefined || value
=== null ) {
9098 } else if ( this.inputFilter
) {
9099 return this.inputFilter( String( value
) );
9101 return String( value
);
9108 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
9109 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9110 if ( this.$input
) {
9111 this.$input
.prop( 'disabled', this.isDisabled() );
9117 * Set the 'id' attribute of the `<input>` element.
9119 * @param {string} id
9121 * @return {OO.ui.Widget} The widget, for chaining
9123 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
9124 this.$input
.attr( 'id', id
);
9131 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
9132 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9133 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
9134 this.setValue( state
.value
);
9136 if ( state
.focus
) {
9142 * Data widget intended for creating `<input type="hidden">` inputs.
9145 * @extends OO.ui.Widget
9148 * @param {Object} [config] Configuration options
9149 * @cfg {string} [value=''] The value of the input.
9150 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9152 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
9153 // Configuration initialization
9154 config
= $.extend( { value
: '', name
: '' }, config
);
9156 // Parent constructor
9157 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
9160 this.$element
.attr( {
9162 value
: config
.value
,
9165 this.$element
.removeAttr( 'aria-disabled' );
9170 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
9172 /* Static Properties */
9178 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
9181 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
9182 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
9183 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
9184 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
9185 * [OOUI documentation on MediaWiki] [1] for more information.
9188 * // A ButtonInputWidget rendered as an HTML button, the default.
9189 * var button = new OO.ui.ButtonInputWidget( {
9190 * label: 'Input button',
9194 * $( document.body ).append( button.$element );
9196 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
9199 * @extends OO.ui.InputWidget
9200 * @mixins OO.ui.mixin.ButtonElement
9201 * @mixins OO.ui.mixin.IconElement
9202 * @mixins OO.ui.mixin.IndicatorElement
9203 * @mixins OO.ui.mixin.LabelElement
9206 * @param {Object} [config] Configuration options
9207 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
9208 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
9209 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
9210 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
9211 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
9213 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
9214 // Configuration initialization
9215 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
9217 // See InputWidget#reusePreInfuseDOM about config.$input
9218 if ( config
.$input
) {
9219 config
.$input
.empty();
9222 // Properties (must be set before parent constructor, which calls #setValue)
9223 this.useInputTag
= config
.useInputTag
;
9225 // Parent constructor
9226 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
9228 // Mixin constructors
9229 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
9230 OO
.ui
.mixin
.IconElement
.call( this, config
);
9231 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9232 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9235 if ( !config
.useInputTag
) {
9236 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
9238 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
9243 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
9244 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
9245 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
9246 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9247 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
9249 /* Static Properties */
9255 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
9263 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9265 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9266 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9272 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9274 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9275 * text, or `null` for no label
9277 * @return {OO.ui.Widget} The widget, for chaining
9279 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9280 if ( typeof label
=== 'function' ) {
9281 label
= OO
.ui
.resolveMsg( label
);
9284 if ( this.useInputTag
) {
9285 // Discard non-plaintext labels
9286 if ( typeof label
!== 'string' ) {
9290 this.$input
.val( label
);
9293 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9297 * Set the value of the input.
9299 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9300 * they do not support {@link #value values}.
9302 * @param {string} value New value
9304 * @return {OO.ui.Widget} The widget, for chaining
9306 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9307 if ( !this.useInputTag
) {
9308 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9316 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9317 // Disable generating `<label>` elements for buttons. One would very rarely need additional label
9318 // for a button, and it's already a big clickable target, and it causes unexpected rendering.
9323 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9324 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9325 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9326 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9328 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9331 * // An example of selected, unselected, and disabled checkbox inputs.
9332 * var checkbox1 = new OO.ui.CheckboxInputWidget( {
9336 * checkbox2 = new OO.ui.CheckboxInputWidget( {
9339 * checkbox3 = new OO.ui.CheckboxInputWidget( {
9343 * // Create a fieldset layout with fields for each checkbox.
9344 * fieldset = new OO.ui.FieldsetLayout( {
9345 * label: 'Checkboxes'
9347 * fieldset.addItems( [
9348 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9349 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9350 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9352 * $( document.body ).append( fieldset.$element );
9354 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9357 * @extends OO.ui.InputWidget
9360 * @param {Object} [config] Configuration options
9361 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
9363 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9364 // Configuration initialization
9365 config
= config
|| {};
9367 // Parent constructor
9368 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9371 this.checkIcon
= new OO
.ui
.IconWidget( {
9373 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9378 .addClass( 'oo-ui-checkboxInputWidget' )
9379 // Required for pretty styling in WikimediaUI theme
9380 .append( this.checkIcon
.$element
);
9381 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9386 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9388 /* Static Properties */
9394 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9396 /* Static Methods */
9401 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9402 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9403 state
.checked
= config
.$input
.prop( 'checked' );
9413 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9414 return $( '<input>' ).attr( 'type', 'checkbox' );
9420 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9422 if ( !this.isDisabled() ) {
9423 // Allow the stack to clear so the value will be updated
9424 setTimeout( function () {
9425 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9431 * Set selection state of this checkbox.
9433 * @param {boolean} state `true` for selected
9435 * @return {OO.ui.Widget} The widget, for chaining
9437 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
9439 if ( this.selected
!== state
) {
9440 this.selected
= state
;
9441 this.$input
.prop( 'checked', this.selected
);
9442 this.emit( 'change', this.selected
);
9444 // The first time that the selection state is set (probably while constructing the widget),
9445 // remember it in defaultSelected. This property can be later used to check whether
9446 // the selection state of the input has been changed since it was created.
9447 if ( this.defaultSelected
=== undefined ) {
9448 this.defaultSelected
= this.selected
;
9449 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9455 * Check if this checkbox is selected.
9457 * @return {boolean} Checkbox is selected
9459 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9460 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9461 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9462 var selected
= this.$input
.prop( 'checked' );
9463 if ( this.selected
!== selected
) {
9464 this.setSelected( selected
);
9466 return this.selected
;
9472 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9473 if ( !this.isDisabled() ) {
9474 this.$input
.click();
9482 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9483 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9484 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9485 this.setSelected( state
.checked
);
9490 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9491 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9492 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9493 * more information about input widgets.
9495 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9496 * are no options. If no `value` configuration option is provided, the first option is selected.
9497 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9499 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
9502 * // A DropdownInputWidget with three options.
9503 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9505 * { data: 'a', label: 'First' },
9506 * { data: 'b', label: 'Second'},
9507 * { data: 'c', label: 'Third' }
9510 * $( document.body ).append( dropdownInput.$element );
9512 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9515 * @extends OO.ui.InputWidget
9518 * @param {Object} [config] Configuration options
9519 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9520 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9521 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
9522 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
9523 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
9524 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9526 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9527 // Configuration initialization
9528 config
= config
|| {};
9530 // Properties (must be done before parent constructor which calls #setDisabled)
9531 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9533 $overlay
: config
.$overlay
9537 // Set up the options before parent constructor, which uses them to validate config.value.
9538 // Use this instead of setOptions() because this.$input is not set up yet.
9539 this.setOptionsData( config
.options
|| [] );
9541 // Parent constructor
9542 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9545 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
9549 .addClass( 'oo-ui-dropdownInputWidget' )
9550 .append( this.dropdownWidget
.$element
);
9551 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9552 this.setTitledElement( this.dropdownWidget
.$handle
);
9557 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9565 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9566 return $( '<select>' );
9570 * Handles menu select events.
9573 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9575 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9576 this.setValue( item
? item
.getData() : '' );
9582 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9584 value
= this.cleanUpValue( value
);
9585 // Only allow setting values that are actually present in the dropdown
9586 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9587 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9588 this.dropdownWidget
.getMenu().selectItem( selected
);
9589 value
= selected
? selected
.getData() : '';
9590 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9591 if ( this.optionsDirty
) {
9592 // We reached this from the constructor or from #setOptions.
9593 // We have to update the <select> element.
9594 this.updateOptionsInterface();
9602 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9603 this.dropdownWidget
.setDisabled( state
);
9604 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9609 * Set the options available for this input.
9611 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9613 * @return {OO.ui.Widget} The widget, for chaining
9615 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9616 var value
= this.getValue();
9618 this.setOptionsData( options
);
9620 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9621 // In case the previous value is no longer an available option, select the first valid one.
9622 this.setValue( value
);
9628 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9630 * This method may be called before the parent constructor, so various properties may not be
9633 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9636 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9641 this.optionsDirty
= true;
9643 optionWidgets
= options
.map( function ( opt
) {
9646 if ( opt
.optgroup
!== undefined ) {
9647 return widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9650 optValue
= widget
.cleanUpValue( opt
.data
);
9651 return widget
.createMenuOptionWidget(
9653 opt
.label
!== undefined ? opt
.label
: optValue
9658 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9662 * Create a menu option widget.
9665 * @param {string} data Item data
9666 * @param {string} label Item label
9667 * @return {OO.ui.MenuOptionWidget} Option widget
9669 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9670 return new OO
.ui
.MenuOptionWidget( {
9677 * Create a menu section option widget.
9680 * @param {string} label Section item label
9681 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9683 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9684 return new OO
.ui
.MenuSectionOptionWidget( {
9690 * Update the user-visible interface to match the internal list of options and value.
9692 * This method must only be called after the parent constructor.
9696 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9698 $optionsContainer
= this.$input
,
9699 defaultValue
= this.defaultValue
,
9702 this.$input
.empty();
9704 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9707 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9708 $optionNode
= $( '<option>' )
9709 .attr( 'value', optionWidget
.getData() )
9710 .text( optionWidget
.getLabel() );
9712 // Remember original selection state. This property can be later used to check whether
9713 // the selection state of the input has been changed since it was created.
9714 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9716 $optionsContainer
.append( $optionNode
);
9718 $optionNode
= $( '<optgroup>' )
9719 .attr( 'label', optionWidget
.getLabel() );
9720 widget
.$input
.append( $optionNode
);
9721 $optionsContainer
= $optionNode
;
9725 this.optionsDirty
= false;
9731 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9732 this.dropdownWidget
.focus();
9739 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9740 this.dropdownWidget
.blur();
9745 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9746 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9747 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9748 * please see the [OOUI documentation on MediaWiki][1].
9750 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9753 * // An example of selected, unselected, and disabled radio inputs
9754 * var radio1 = new OO.ui.RadioInputWidget( {
9758 * var radio2 = new OO.ui.RadioInputWidget( {
9761 * var radio3 = new OO.ui.RadioInputWidget( {
9765 * // Create a fieldset layout with fields for each radio button.
9766 * var fieldset = new OO.ui.FieldsetLayout( {
9767 * label: 'Radio inputs'
9769 * fieldset.addItems( [
9770 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9771 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9772 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9774 * $( document.body ).append( fieldset.$element );
9776 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9779 * @extends OO.ui.InputWidget
9782 * @param {Object} [config] Configuration options
9783 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9785 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9786 // Configuration initialization
9787 config
= config
|| {};
9789 // Parent constructor
9790 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9794 .addClass( 'oo-ui-radioInputWidget' )
9795 // Required for pretty styling in WikimediaUI theme
9796 .append( $( '<span>' ) );
9797 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9802 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9804 /* Static Properties */
9810 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9812 /* Static Methods */
9817 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9818 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9819 state
.checked
= config
.$input
.prop( 'checked' );
9829 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9830 return $( '<input>' ).attr( 'type', 'radio' );
9836 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9837 // RadioInputWidget doesn't track its state.
9841 * Set selection state of this radio button.
9843 * @param {boolean} state `true` for selected
9845 * @return {OO.ui.Widget} The widget, for chaining
9847 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9848 // RadioInputWidget doesn't track its state.
9849 this.$input
.prop( 'checked', state
);
9850 // The first time that the selection state is set (probably while constructing the widget),
9851 // remember it in defaultSelected. This property can be later used to check whether
9852 // the selection state of the input has been changed since it was created.
9853 if ( this.defaultSelected
=== undefined ) {
9854 this.defaultSelected
= state
;
9855 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9861 * Check if this radio button is selected.
9863 * @return {boolean} Radio is selected
9865 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
9866 return this.$input
.prop( 'checked' );
9872 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
9873 if ( !this.isDisabled() ) {
9874 this.$input
.click();
9882 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9883 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9884 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9885 this.setSelected( state
.checked
);
9890 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9891 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9892 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9893 * more information about input widgets.
9895 * This and OO.ui.DropdownInputWidget support the same configuration options.
9898 * // A RadioSelectInputWidget with three options
9899 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9901 * { data: 'a', label: 'First' },
9902 * { data: 'b', label: 'Second'},
9903 * { data: 'c', label: 'Third' }
9906 * $( document.body ).append( radioSelectInput.$element );
9908 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9911 * @extends OO.ui.InputWidget
9914 * @param {Object} [config] Configuration options
9915 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9917 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
9918 // Configuration initialization
9919 config
= config
|| {};
9921 // Properties (must be done before parent constructor which calls #setDisabled)
9922 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
9923 // Set up the options before parent constructor, which uses them to validate config.value.
9924 // Use this instead of setOptions() because this.$input is not set up yet
9925 this.setOptionsData( config
.options
|| [] );
9927 // Parent constructor
9928 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
9931 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
9935 .addClass( 'oo-ui-radioSelectInputWidget' )
9936 .append( this.radioSelectWidget
.$element
);
9937 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
9942 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
9944 /* Static Methods */
9949 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9950 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9951 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9958 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9959 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9960 // Cannot reuse the `<input type=radio>` set
9961 delete config
.$input
;
9971 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
9972 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
9973 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
9974 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
9978 * Handles menu select events.
9981 * @param {OO.ui.RadioOptionWidget} item Selected menu item
9983 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
9984 this.setValue( item
.getData() );
9990 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
9992 value
= this.cleanUpValue( value
);
9993 // Only allow setting values that are actually present in the dropdown
9994 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
9995 this.radioSelectWidget
.findFirstSelectableItem();
9996 this.radioSelectWidget
.selectItem( selected
);
9997 value
= selected
? selected
.getData() : '';
9998 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10005 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
10006 this.radioSelectWidget
.setDisabled( state
);
10007 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10012 * Set the options available for this input.
10014 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10016 * @return {OO.ui.Widget} The widget, for chaining
10018 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
10019 var value
= this.getValue();
10021 this.setOptionsData( options
);
10023 // Re-set the value to update the visible interface (RadioSelectWidget).
10024 // In case the previous value is no longer an available option, select the first valid one.
10025 this.setValue( value
);
10031 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10033 * This method may be called before the parent constructor, so various properties may not be
10036 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10039 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10042 this.radioSelectWidget
10044 .addItems( options
.map( function ( opt
) {
10045 var optValue
= widget
.cleanUpValue( opt
.data
);
10046 return new OO
.ui
.RadioOptionWidget( {
10048 label
: opt
.label
!== undefined ? opt
.label
: optValue
10056 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10057 this.radioSelectWidget
.focus();
10064 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10065 this.radioSelectWidget
.blur();
10070 * CheckboxMultiselectInputWidget is a
10071 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10072 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10073 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10074 * more information about input widgets.
10077 * // A CheckboxMultiselectInputWidget with three options.
10078 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10080 * { data: 'a', label: 'First' },
10081 * { data: 'b', label: 'Second' },
10082 * { data: 'c', label: 'Third' }
10085 * $( document.body ).append( multiselectInput.$element );
10087 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10090 * @extends OO.ui.InputWidget
10093 * @param {Object} [config] Configuration options
10094 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
10096 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10097 // Configuration initialization
10098 config
= config
|| {};
10100 // Properties (must be done before parent constructor which calls #setDisabled)
10101 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10102 // Must be set before the #setOptionsData call below
10103 this.inputName
= config
.name
;
10104 // Set up the options before parent constructor, which uses them to validate config.value.
10105 // Use this instead of setOptions() because this.$input is not set up yet
10106 this.setOptionsData( config
.options
|| [] );
10108 // Parent constructor
10109 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10112 this.checkboxMultiselectWidget
.connect( this, { select
: 'onCheckboxesSelect' } );
10116 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10117 .append( this.checkboxMultiselectWidget
.$element
);
10118 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10119 this.$input
.detach();
10124 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10126 /* Static Methods */
10131 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10132 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10133 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10134 .toArray().map( function ( el
) { return el
.value
; } );
10141 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10142 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10143 // Cannot reuse the `<input type=checkbox>` set
10144 delete config
.$input
;
10154 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10156 return $( '<unused>' );
10160 * Handles CheckboxMultiselectWidget select events.
10164 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10165 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10171 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10172 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10173 .toArray().map( function ( el
) { return el
.value
; } );
10174 if ( this.value
!== value
) {
10175 this.setValue( value
);
10183 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10184 value
= this.cleanUpValue( value
);
10185 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10186 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10187 if ( this.optionsDirty
) {
10188 // We reached this from the constructor or from #setOptions.
10189 // We have to update the <select> element.
10190 this.updateOptionsInterface();
10196 * Clean up incoming value.
10198 * @param {string[]} value Original value
10199 * @return {string[]} Cleaned up value
10201 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10202 var i
, singleValue
,
10204 if ( !Array
.isArray( value
) ) {
10207 for ( i
= 0; i
< value
.length
; i
++ ) {
10209 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
10210 // Remove options that we don't have here
10211 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10214 cleanValue
.push( singleValue
);
10222 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10223 this.checkboxMultiselectWidget
.setDisabled( state
);
10224 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10229 * Set the options available for this input.
10231 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
10233 * @return {OO.ui.Widget} The widget, for chaining
10235 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10236 var value
= this.getValue();
10238 this.setOptionsData( options
);
10240 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10241 // This will also get rid of any stale options that we just removed.
10242 this.setValue( value
);
10248 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10250 * This method may be called before the parent constructor, so various properties may not be
10253 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10256 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10259 this.optionsDirty
= true;
10261 this.checkboxMultiselectWidget
10263 .addItems( options
.map( function ( opt
) {
10264 var optValue
, item
, optDisabled
;
10266 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
10267 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10268 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10270 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10271 disabled
: optDisabled
10273 // Set the 'name' and 'value' for form submission
10274 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10275 item
.checkbox
.setValue( optValue
);
10281 * Update the user-visible interface to match the internal list of options and value.
10283 * This method must only be called after the parent constructor.
10287 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10288 var defaultValue
= this.defaultValue
;
10290 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10291 // Remember original selection state. This property can be later used to check whether
10292 // the selection state of the input has been changed since it was created.
10293 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10294 item
.checkbox
.defaultSelected
= isDefault
;
10295 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10298 this.optionsDirty
= false;
10304 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10305 this.checkboxMultiselectWidget
.focus();
10310 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10311 * size of the field as well as its presentation. In addition, these widgets can be configured
10312 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
10313 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
10314 * which modifies incoming values rather than validating them.
10315 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10317 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10320 * // A TextInputWidget.
10321 * var textInput = new OO.ui.TextInputWidget( {
10322 * value: 'Text input'
10324 * $( document.body ).append( textInput.$element );
10326 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10329 * @extends OO.ui.InputWidget
10330 * @mixins OO.ui.mixin.IconElement
10331 * @mixins OO.ui.mixin.IndicatorElement
10332 * @mixins OO.ui.mixin.PendingElement
10333 * @mixins OO.ui.mixin.LabelElement
10336 * @param {Object} [config] Configuration options
10337 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10338 * 'email', 'url' or 'number'.
10339 * @cfg {string} [placeholder] Placeholder text
10340 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10341 * instruct the browser to focus this widget.
10342 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10343 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10345 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10346 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10347 * many emojis) count as 2 characters each.
10348 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10349 * the value or placeholder text: `'before'` or `'after'`
10350 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator: 'required'`.
10351 * Note that `false` & setting `indicator: 'required' will result in no indicator shown.
10352 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10353 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined` means
10354 * leaving it up to the browser).
10355 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10356 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10357 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10358 * value for it to be considered valid; when Function, a function receiving the value as parameter
10359 * that must return true, or promise resolving to true, for it to be considered valid.
10361 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10362 // Configuration initialization
10363 config
= $.extend( {
10365 labelPosition
: 'after'
10368 // Parent constructor
10369 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10371 // Mixin constructors
10372 OO
.ui
.mixin
.IconElement
.call( this, config
);
10373 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10374 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
10375 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10378 this.type
= this.getSaneType( config
);
10379 this.readOnly
= false;
10380 this.required
= false;
10381 this.validate
= null;
10382 this.scrollWidth
= null;
10384 this.setValidation( config
.validate
);
10385 this.setLabelPosition( config
.labelPosition
);
10389 keypress
: this.onKeyPress
.bind( this ),
10390 blur
: this.onBlur
.bind( this ),
10391 focus
: this.onFocus
.bind( this )
10393 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10394 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10395 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10396 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10400 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10401 .append( this.$icon
, this.$indicator
);
10402 this.setReadOnly( !!config
.readOnly
);
10403 this.setRequired( !!config
.required
);
10404 if ( config
.placeholder
!== undefined ) {
10405 this.$input
.attr( 'placeholder', config
.placeholder
);
10407 if ( config
.maxLength
!== undefined ) {
10408 this.$input
.attr( 'maxlength', config
.maxLength
);
10410 if ( config
.autofocus
) {
10411 this.$input
.attr( 'autofocus', 'autofocus' );
10413 if ( config
.autocomplete
=== false ) {
10414 this.$input
.attr( 'autocomplete', 'off' );
10415 // Turning off autocompletion also disables "form caching" when the user navigates to a
10416 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
10418 beforeunload: function () {
10419 this.$input
.removeAttr( 'autocomplete' );
10421 pageshow: function () {
10422 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
10423 // whole page... it shouldn't hurt, though.
10424 this.$input
.attr( 'autocomplete', 'off' );
10428 if ( config
.spellcheck
!== undefined ) {
10429 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10431 if ( this.label
) {
10432 this.isWaitingToBeAttached
= true;
10433 this.installParentChangeDetector();
10439 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10440 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10441 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10442 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10443 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10445 /* Static Properties */
10447 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10455 * An `enter` event is emitted when the user presses 'enter' inside the text box.
10463 * Handle icon mouse down events.
10466 * @param {jQuery.Event} e Mouse down event
10467 * @return {undefined/boolean} False to prevent default if event is handled
10469 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10470 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10477 * Handle indicator mouse down events.
10480 * @param {jQuery.Event} e Mouse down event
10481 * @return {undefined/boolean} False to prevent default if event is handled
10483 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10484 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10491 * Handle key press events.
10494 * @param {jQuery.Event} e Key press event
10495 * @fires enter If enter key is pressed
10497 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10498 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10499 this.emit( 'enter', e
);
10504 * Handle blur events.
10507 * @param {jQuery.Event} e Blur event
10509 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10510 this.setValidityFlag();
10514 * Handle focus events.
10517 * @param {jQuery.Event} e Focus event
10519 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10520 if ( this.isWaitingToBeAttached
) {
10521 // If we've received focus, then we must be attached to the document, and if
10522 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10523 this.onElementAttach();
10525 this.setValidityFlag( true );
10529 * Handle element attach events.
10532 * @param {jQuery.Event} e Element attach event
10534 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10535 this.isWaitingToBeAttached
= false;
10536 // Any previously calculated size is now probably invalid if we reattached elsewhere
10537 this.valCache
= null;
10538 this.positionLabel();
10542 * Handle debounced change events.
10544 * @param {string} value
10547 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10548 this.setValidityFlag();
10552 * Check if the input is {@link #readOnly read-only}.
10554 * @return {boolean}
10556 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10557 return this.readOnly
;
10561 * Set the {@link #readOnly read-only} state of the input.
10563 * @param {boolean} state Make input read-only
10565 * @return {OO.ui.Widget} The widget, for chaining
10567 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10568 this.readOnly
= !!state
;
10569 this.$input
.prop( 'readOnly', this.readOnly
);
10574 * Check if the input is {@link #required required}.
10576 * @return {boolean}
10578 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10579 return this.required
;
10583 * Set the {@link #required required} state of the input.
10585 * @param {boolean} state Make input required
10587 * @return {OO.ui.Widget} The widget, for chaining
10589 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10590 this.required
= !!state
;
10591 if ( this.required
) {
10593 .prop( 'required', true )
10594 .attr( 'aria-required', 'true' );
10595 if ( this.getIndicator() === null ) {
10596 this.setIndicator( 'required' );
10600 .prop( 'required', false )
10601 .removeAttr( 'aria-required' );
10602 if ( this.getIndicator() === 'required' ) {
10603 this.setIndicator( null );
10610 * Support function for making #onElementAttach work across browsers.
10612 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10613 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10615 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10616 * first time that the element gets attached to the documented.
10618 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10619 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10620 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
10623 if ( MutationObserver
) {
10624 // The new way. If only it wasn't so ugly.
10626 if ( this.isElementAttached() ) {
10627 // Widget is attached already, do nothing. This breaks the functionality of this function when
10628 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
10629 // would require observation of the whole document, which would hurt performance of other,
10630 // more important code.
10634 // Find topmost node in the tree
10635 topmostNode
= this.$element
[ 0 ];
10636 while ( topmostNode
.parentNode
) {
10637 topmostNode
= topmostNode
.parentNode
;
10640 // We have no way to detect the $element being attached somewhere without observing the entire
10641 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
10642 // parent node of $element, and instead detect when $element is removed from it (and thus
10643 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
10644 // doesn't get attached, we end up back here and create the parent.
10646 mutationObserver
= new MutationObserver( function ( mutations
) {
10647 var i
, j
, removedNodes
;
10648 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10649 removedNodes
= mutations
[ i
].removedNodes
;
10650 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10651 if ( removedNodes
[ j
] === topmostNode
) {
10652 setTimeout( onRemove
, 0 );
10659 onRemove = function () {
10660 // If the node was attached somewhere else, report it
10661 if ( widget
.isElementAttached() ) {
10662 widget
.onElementAttach();
10664 mutationObserver
.disconnect();
10665 widget
.installParentChangeDetector();
10668 // Create a fake parent and observe it
10669 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10670 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10672 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10673 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10674 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10682 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10683 if ( this.getSaneType( config
) === 'number' ) {
10684 return $( '<input>' )
10685 .attr( 'step', 'any' )
10686 .attr( 'type', 'number' );
10688 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10693 * Get sanitized value for 'type' for given config.
10695 * @param {Object} config Configuration options
10696 * @return {string|null}
10699 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10700 var allowedTypes
= [
10707 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10711 * Focus the input and select a specified range within the text.
10713 * @param {number} from Select from offset
10714 * @param {number} [to] Select to offset, defaults to from
10716 * @return {OO.ui.Widget} The widget, for chaining
10718 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10719 var isBackwards
, start
, end
,
10720 input
= this.$input
[ 0 ];
10724 isBackwards
= to
< from;
10725 start
= isBackwards
? to
: from;
10726 end
= isBackwards
? from : to
;
10731 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10733 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10734 // Rather than expensively check if the input is attached every time, just check
10735 // if it was the cause of an error being thrown. If not, rethrow the error.
10736 if ( this.getElementDocument().body
.contains( input
) ) {
10744 * Get an object describing the current selection range in a directional manner
10746 * @return {Object} Object containing 'from' and 'to' offsets
10748 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10749 var input
= this.$input
[ 0 ],
10750 start
= input
.selectionStart
,
10751 end
= input
.selectionEnd
,
10752 isBackwards
= input
.selectionDirection
=== 'backward';
10755 from: isBackwards
? end
: start
,
10756 to
: isBackwards
? start
: end
10761 * Get the length of the text input value.
10763 * This could differ from the length of #getValue if the
10764 * value gets filtered
10766 * @return {number} Input length
10768 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10769 return this.$input
[ 0 ].value
.length
;
10773 * Focus the input and select the entire text.
10776 * @return {OO.ui.Widget} The widget, for chaining
10778 OO
.ui
.TextInputWidget
.prototype.select = function () {
10779 return this.selectRange( 0, this.getInputLength() );
10783 * Focus the input and move the cursor to the start.
10786 * @return {OO.ui.Widget} The widget, for chaining
10788 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10789 return this.selectRange( 0 );
10793 * Focus the input and move the cursor to the end.
10796 * @return {OO.ui.Widget} The widget, for chaining
10798 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10799 return this.selectRange( this.getInputLength() );
10803 * Insert new content into the input.
10805 * @param {string} content Content to be inserted
10807 * @return {OO.ui.Widget} The widget, for chaining
10809 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10811 range
= this.getRange(),
10812 value
= this.getValue();
10814 start
= Math
.min( range
.from, range
.to
);
10815 end
= Math
.max( range
.from, range
.to
);
10817 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10818 this.selectRange( start
+ content
.length
);
10823 * Insert new content either side of a selection.
10825 * @param {string} pre Content to be inserted before the selection
10826 * @param {string} post Content to be inserted after the selection
10828 * @return {OO.ui.Widget} The widget, for chaining
10830 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10832 range
= this.getRange(),
10833 offset
= pre
.length
;
10835 start
= Math
.min( range
.from, range
.to
);
10836 end
= Math
.max( range
.from, range
.to
);
10838 this.selectRange( start
).insertContent( pre
);
10839 this.selectRange( offset
+ end
).insertContent( post
);
10841 this.selectRange( offset
+ start
, offset
+ end
);
10846 * Set the validation pattern.
10848 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10849 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10850 * value must contain only numbers).
10852 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10853 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10855 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
10856 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
10857 this.validate
= validate
;
10859 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
10864 * Sets the 'invalid' flag appropriately.
10866 * @param {boolean} [isValid] Optionally override validation result
10868 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
10870 setFlag = function ( valid
) {
10872 widget
.$input
.attr( 'aria-invalid', 'true' );
10874 widget
.$input
.removeAttr( 'aria-invalid' );
10876 widget
.setFlags( { invalid
: !valid
} );
10879 if ( isValid
!== undefined ) {
10880 setFlag( isValid
);
10882 this.getValidity().then( function () {
10891 * Get the validity of current value.
10893 * This method returns a promise that resolves if the value is valid and rejects if
10894 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
10896 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10898 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
10901 function rejectOrResolve( valid
) {
10903 return $.Deferred().resolve().promise();
10905 return $.Deferred().reject().promise();
10909 // Check browser validity and reject if it is invalid
10911 this.$input
[ 0 ].checkValidity
!== undefined &&
10912 this.$input
[ 0 ].checkValidity() === false
10914 return rejectOrResolve( false );
10917 // Run our checks if the browser thinks the field is valid
10918 if ( this.validate
instanceof Function
) {
10919 result
= this.validate( this.getValue() );
10920 if ( result
&& typeof result
.promise
=== 'function' ) {
10921 return result
.promise().then( function ( valid
) {
10922 return rejectOrResolve( valid
);
10925 return rejectOrResolve( result
);
10928 return rejectOrResolve( this.getValue().match( this.validate
) );
10933 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10935 * @param {string} labelPosition Label position, 'before' or 'after'
10937 * @return {OO.ui.Widget} The widget, for chaining
10939 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10940 this.labelPosition
= labelPosition
;
10941 if ( this.label
) {
10942 // If there is no label and we only change the position, #updatePosition is a no-op,
10943 // but it takes really a lot of work to do nothing.
10944 this.updatePosition();
10950 * Update the position of the inline label.
10952 * This method is called by #setLabelPosition, and can also be called on its own if
10953 * something causes the label to be mispositioned.
10956 * @return {OO.ui.Widget} The widget, for chaining
10958 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10959 var after
= this.labelPosition
=== 'after';
10962 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10963 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10965 this.valCache
= null;
10966 this.scrollWidth
= null;
10967 this.positionLabel();
10973 * Position the label by setting the correct padding on the input.
10977 * @return {OO.ui.Widget} The widget, for chaining
10979 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
10980 var after
, rtl
, property
, newCss
;
10982 if ( this.isWaitingToBeAttached
) {
10983 // #onElementAttach will be called soon, which calls this method
10988 'padding-right': '',
10992 if ( this.label
) {
10993 this.$element
.append( this.$label
);
10995 this.$label
.detach();
10996 // Clear old values if present
10997 this.$input
.css( newCss
);
11001 after
= this.labelPosition
=== 'after';
11002 rtl
= this.$element
.css( 'direction' ) === 'rtl';
11003 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11005 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
11006 // We have to clear the padding on the other side, in case the element direction changed
11007 this.$input
.css( newCss
);
11013 * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
11014 * {@link OO.ui.mixin.IconElement search icon} by default.
11015 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11017 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
11020 * @extends OO.ui.TextInputWidget
11023 * @param {Object} [config] Configuration options
11025 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
11026 config
= $.extend( {
11030 // Parent constructor
11031 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11034 this.connect( this, {
11039 this.updateSearchIndicator();
11040 this.connect( this, {
11041 disable
: 'onDisable'
11047 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11055 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11062 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
11063 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11064 // Clear the text field
11065 this.setValue( '' );
11072 * Update the 'clear' indicator displayed on type: 'search' text
11073 * fields, hiding it when the field is already empty or when it's not
11076 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11077 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11078 this.setIndicator( null );
11080 this.setIndicator( 'clear' );
11085 * Handle change events.
11089 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11090 this.updateSearchIndicator();
11094 * Handle disable events.
11096 * @param {boolean} disabled Element is disabled
11099 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11100 this.updateSearchIndicator();
11106 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11107 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11108 this.updateSearchIndicator();
11113 * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
11114 * configure number of rows visible. In addition, these widgets can be autosized to fit user
11115 * inputs and can show {@link OO.ui.mixin.IconElement icons} and
11116 * {@link OO.ui.mixin.IndicatorElement indicators}.
11117 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11119 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11122 * // A MultilineTextInputWidget.
11123 * var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
11124 * value: 'Text input on multiple lines'
11126 * $( 'body' ).append( multilineTextInput.$element );
11128 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
11131 * @extends OO.ui.TextInputWidget
11134 * @param {Object} [config] Configuration options
11135 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11136 * specifies minimum number of rows to display.
11137 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11138 * Use the #maxRows config to specify a maximum number of displayed rows.
11139 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11140 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11142 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11143 config
= $.extend( {
11146 // Parent constructor
11147 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11150 this.autosize
= !!config
.autosize
;
11151 this.styleHeight
= null;
11152 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11153 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11155 // Clone for resizing
11156 if ( this.autosize
) {
11157 this.$clone
= this.$input
11159 .removeAttr( 'id' )
11160 .removeAttr( 'name' )
11161 .insertAfter( this.$input
)
11162 .attr( 'aria-hidden', 'true' )
11163 .addClass( 'oo-ui-element-hidden' );
11167 this.connect( this, {
11172 if ( config
.rows
) {
11173 this.$input
.attr( 'rows', config
.rows
);
11175 if ( this.autosize
) {
11176 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11177 this.isWaitingToBeAttached
= true;
11178 this.installParentChangeDetector();
11184 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11186 /* Static Methods */
11191 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11192 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11193 state
.scrollTop
= config
.$input
.scrollTop();
11202 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11203 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11208 * Handle change events.
11212 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11219 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11220 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11227 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11229 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11231 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11232 // Some platforms emit keycode 10 for ctrl+enter in a textarea
11235 this.emit( 'enter', e
);
11240 * Automatically adjust the size of the text input.
11242 * This only affects multiline inputs that are {@link #autosize autosized}.
11245 * @return {OO.ui.Widget} The widget, for chaining
11248 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11249 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11250 idealHeight
, newHeight
, scrollWidth
, property
;
11252 if ( this.$input
.val() !== this.valCache
) {
11253 if ( this.autosize
) {
11255 .val( this.$input
.val() )
11256 .attr( 'rows', this.minRows
)
11257 // Set inline height property to 0 to measure scroll height
11258 .css( 'height', 0 );
11260 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11262 this.valCache
= this.$input
.val();
11264 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11266 // Remove inline height property to measure natural heights
11267 this.$clone
.css( 'height', '' );
11268 innerHeight
= this.$clone
.innerHeight();
11269 outerHeight
= this.$clone
.outerHeight();
11271 // Measure max rows height
11273 .attr( 'rows', this.maxRows
)
11274 .css( 'height', 'auto' )
11276 maxInnerHeight
= this.$clone
.innerHeight();
11278 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11279 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11280 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11281 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11283 this.$clone
.addClass( 'oo-ui-element-hidden' );
11285 // Only apply inline height when expansion beyond natural height is needed
11286 // Use the difference between the inner and outer height as a buffer
11287 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11288 if ( newHeight
!== this.styleHeight
) {
11289 this.$input
.css( 'height', newHeight
);
11290 this.styleHeight
= newHeight
;
11291 this.emit( 'resize' );
11294 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11295 if ( scrollWidth
!== this.scrollWidth
) {
11296 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11298 this.$label
.css( { right
: '', left
: '' } );
11299 this.$indicator
.css( { right
: '', left
: '' } );
11301 if ( scrollWidth
) {
11302 this.$indicator
.css( property
, scrollWidth
);
11303 if ( this.labelPosition
=== 'after' ) {
11304 this.$label
.css( property
, scrollWidth
);
11308 this.scrollWidth
= scrollWidth
;
11309 this.positionLabel();
11319 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11320 return $( '<textarea>' );
11324 * Check if the input automatically adjusts its size.
11326 * @return {boolean}
11328 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11329 return !!this.autosize
;
11335 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11336 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11337 if ( state
.scrollTop
!== undefined ) {
11338 this.$input
.scrollTop( state
.scrollTop
);
11343 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11344 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11345 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11347 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11348 * option, that option will appear to be selected.
11349 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11352 * After the user chooses an option, its `data` will be used as a new value for the widget.
11353 * A `label` also can be specified for each option: if given, it will be shown instead of the
11354 * `data` in the dropdown menu.
11356 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11358 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
11361 * // A ComboBoxInputWidget.
11362 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11363 * value: 'Option 1',
11365 * { data: 'Option 1' },
11366 * { data: 'Option 2' },
11367 * { data: 'Option 3' }
11370 * $( document.body ).append( comboBox.$element );
11373 * // Example: A ComboBoxInputWidget with additional option labels.
11374 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11375 * value: 'Option 1',
11378 * data: 'Option 1',
11379 * label: 'Option One'
11382 * data: 'Option 2',
11383 * label: 'Option Two'
11386 * data: 'Option 3',
11387 * label: 'Option Three'
11391 * $( document.body ).append( comboBox.$element );
11393 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11396 * @extends OO.ui.TextInputWidget
11399 * @param {Object} [config] Configuration options
11400 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11401 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11402 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11403 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11404 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11405 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11407 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11408 // Configuration initialization
11409 config
= $.extend( {
11410 autocomplete
: false
11413 // ComboBoxInputWidget shouldn't support `multiline`
11414 config
.multiline
= false;
11416 // See InputWidget#reusePreInfuseDOM about `config.$input`
11417 if ( config
.$input
) {
11418 config
.$input
.removeAttr( 'list' );
11421 // Parent constructor
11422 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11425 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11426 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11427 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11428 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11430 invisibleLabel
: true,
11431 disabled
: this.disabled
11433 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11437 $floatableContainer
: this.$element
,
11438 disabled
: this.isDisabled()
11444 this.connect( this, {
11445 change
: 'onInputChange',
11446 enter
: 'onInputEnter'
11448 this.dropdownButton
.connect( this, {
11449 click
: 'onDropdownButtonClick'
11451 this.menu
.connect( this, {
11452 choose
: 'onMenuChoose',
11453 add
: 'onMenuItemsChange',
11454 remove
: 'onMenuItemsChange',
11455 toggle
: 'onMenuToggle'
11459 this.$input
.attr( {
11461 'aria-owns': this.menu
.getElementId(),
11462 'aria-autocomplete': 'list'
11464 this.dropdownButton
.$button
.attr( {
11465 'aria-controls': this.menu
.getElementId()
11467 // Do not override options set via config.menu.items
11468 if ( config
.options
!== undefined ) {
11469 this.setOptions( config
.options
);
11471 this.$field
= $( '<div>' )
11472 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11473 .append( this.$input
, this.dropdownButton
.$element
);
11475 .addClass( 'oo-ui-comboBoxInputWidget' )
11476 .append( this.$field
);
11477 this.$overlay
.append( this.menu
.$element
);
11478 this.onMenuItemsChange();
11483 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11488 * Get the combobox's menu.
11490 * @return {OO.ui.MenuSelectWidget} Menu widget
11492 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11497 * Get the combobox's text input widget.
11499 * @return {OO.ui.TextInputWidget} Text input widget
11501 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11506 * Handle input change events.
11509 * @param {string} value New value
11511 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11512 var match
= this.menu
.findItemFromData( value
);
11514 this.menu
.selectItem( match
);
11515 if ( this.menu
.findHighlightedItem() ) {
11516 this.menu
.highlightItem( match
);
11519 if ( !this.isDisabled() ) {
11520 this.menu
.toggle( true );
11525 * Handle input enter events.
11529 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11530 if ( !this.isDisabled() ) {
11531 this.menu
.toggle( false );
11536 * Handle button click events.
11540 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11541 this.menu
.toggle();
11546 * Handle menu choose events.
11549 * @param {OO.ui.OptionWidget} item Chosen item
11551 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11552 this.setValue( item
.getData() );
11556 * Handle menu item change events.
11560 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11561 var match
= this.menu
.findItemFromData( this.getValue() );
11562 this.menu
.selectItem( match
);
11563 if ( this.menu
.findHighlightedItem() ) {
11564 this.menu
.highlightItem( match
);
11566 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11570 * Handle menu toggle events.
11573 * @param {boolean} isVisible Open state of the menu
11575 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11576 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11582 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
11584 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
11586 if ( this.dropdownButton
) {
11587 this.dropdownButton
.setDisabled( this.isDisabled() );
11590 this.menu
.setDisabled( this.isDisabled() );
11597 * Set the options available for this input.
11599 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11601 * @return {OO.ui.Widget} The widget, for chaining
11603 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11606 .addItems( options
.map( function ( opt
) {
11607 return new OO
.ui
.MenuOptionWidget( {
11609 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11617 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11618 * which is a widget that is specified by reference before any optional configuration settings.
11620 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
11622 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11623 * A left-alignment is used for forms with many fields.
11624 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11625 * A right-alignment is used for long but familiar forms which users tab through,
11626 * verifying the current field with a quick glance at the label.
11627 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11628 * that users fill out from top to bottom.
11629 * - **inline**: The label is placed after the field-widget and aligned to the left.
11630 * An inline-alignment is best used with checkboxes or radio buttons.
11632 * Help text can either be:
11634 * - accessed via a help icon that appears in the upper right corner of the rendered field layout, or
11635 * - shown as a subtle explanation below the label.
11637 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`. If it
11638 * is long or not essential, leave `helpInline` to its default, `false`.
11640 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11642 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11645 * @extends OO.ui.Layout
11646 * @mixins OO.ui.mixin.LabelElement
11647 * @mixins OO.ui.mixin.TitledElement
11650 * @param {OO.ui.Widget} fieldWidget Field widget
11651 * @param {Object} [config] Configuration options
11652 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11654 * @cfg {Array} [errors] Error messages about the widget, which will be
11655 * displayed below the widget.
11656 * The array may contain strings or OO.ui.HtmlSnippet instances.
11657 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11658 * below the widget.
11659 * The array may contain strings or OO.ui.HtmlSnippet instances.
11660 * These are more visible than `help` messages when `helpInline` is set, and so
11661 * might be good for transient messages.
11662 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
11663 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
11664 * corner of the rendered field; clicking it will display the text in a popup.
11665 * If `helpInline` is `true`, then a subtle description will be shown after the
11667 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
11668 * or shown when the "help" icon is clicked.
11669 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
11671 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11673 * @throws {Error} An error is thrown if no widget is specified
11675 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11676 // Allow passing positional parameters inside the config object
11677 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11678 config
= fieldWidget
;
11679 fieldWidget
= config
.fieldWidget
;
11682 // Make sure we have required constructor arguments
11683 if ( fieldWidget
=== undefined ) {
11684 throw new Error( 'Widget not found' );
11687 // Configuration initialization
11688 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
11690 // Parent constructor
11691 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11693 // Mixin constructors
11694 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
11695 $label
: $( '<label>' )
11697 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
11700 this.fieldWidget
= fieldWidget
;
11703 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11704 this.$messages
= $( '<ul>' );
11705 this.$header
= $( '<span>' );
11706 this.$body
= $( '<div>' );
11708 this.helpInline
= config
.helpInline
;
11711 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
11714 this.$help
= config
.help
?
11715 this.createHelpElement( config
.help
, config
.$overlay
) :
11717 if ( this.fieldWidget
.getInputId() ) {
11718 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11719 if ( this.helpInline
) {
11720 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
11723 this.$label
.on( 'click', function () {
11724 this.fieldWidget
.simulateLabelClick();
11726 if ( this.helpInline
) {
11727 this.$help
.on( 'click', function () {
11728 this.fieldWidget
.simulateLabelClick();
11733 .addClass( 'oo-ui-fieldLayout' )
11734 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11735 .append( this.$body
);
11736 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11737 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11738 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11740 .addClass( 'oo-ui-fieldLayout-field' )
11741 .append( this.fieldWidget
.$element
);
11743 this.setErrors( config
.errors
|| [] );
11744 this.setNotices( config
.notices
|| [] );
11745 this.setAlignment( config
.align
);
11746 // Call this again to take into account the widget's accessKey
11747 this.updateTitle();
11752 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11753 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11754 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
11759 * Handle field disable events.
11762 * @param {boolean} value Field is disabled
11764 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
11765 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11769 * Get the widget contained by the field.
11771 * @return {OO.ui.Widget} Field widget
11773 OO
.ui
.FieldLayout
.prototype.getField = function () {
11774 return this.fieldWidget
;
11778 * Return `true` if the given field widget can be used with `'inline'` alignment (see
11779 * #setAlignment). Return `false` if it can't or if this can't be determined.
11781 * @return {boolean}
11783 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11784 // This is very simplistic, but should be good enough.
11785 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11790 * @param {string} kind 'error' or 'notice'
11791 * @param {string|OO.ui.HtmlSnippet} text
11794 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11795 var $listItem
, $icon
, message
;
11796 $listItem
= $( '<li>' );
11797 if ( kind
=== 'error' ) {
11798 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11799 $listItem
.attr( 'role', 'alert' );
11800 } else if ( kind
=== 'notice' ) {
11801 $icon
= new OO
.ui
.IconWidget( { icon
: 'notice' } ).$element
;
11805 message
= new OO
.ui
.LabelWidget( { label
: text
} );
11807 .append( $icon
, message
.$element
)
11808 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
11813 * Set the field alignment mode.
11816 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
11818 * @return {OO.ui.BookletLayout} The layout, for chaining
11820 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
11821 if ( value
!== this.align
) {
11822 // Default to 'left'
11823 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
11827 if ( value
=== 'inline' && !this.isFieldInline() ) {
11830 // Reorder elements
11832 if ( this.helpInline
) {
11833 if ( value
=== 'top' ) {
11834 this.$header
.append( this.$label
);
11835 this.$body
.append( this.$header
, this.$field
, this.$help
);
11836 } else if ( value
=== 'inline' ) {
11837 this.$header
.append( this.$label
, this.$help
);
11838 this.$body
.append( this.$field
, this.$header
);
11840 this.$header
.append( this.$label
, this.$help
);
11841 this.$body
.append( this.$header
, this.$field
);
11844 if ( value
=== 'top' ) {
11845 this.$header
.append( this.$help
, this.$label
);
11846 this.$body
.append( this.$header
, this.$field
);
11847 } else if ( value
=== 'inline' ) {
11848 this.$header
.append( this.$help
, this.$label
);
11849 this.$body
.append( this.$field
, this.$header
);
11851 this.$header
.append( this.$label
);
11852 this.$body
.append( this.$header
, this.$help
, this.$field
);
11855 // Set classes. The following classes can be used here:
11856 // * oo-ui-fieldLayout-align-left
11857 // * oo-ui-fieldLayout-align-right
11858 // * oo-ui-fieldLayout-align-top
11859 // * oo-ui-fieldLayout-align-inline
11860 if ( this.align
) {
11861 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
11863 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
11864 this.align
= value
;
11871 * Set the list of error messages.
11873 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11874 * The array may contain strings or OO.ui.HtmlSnippet instances.
11876 * @return {OO.ui.BookletLayout} The layout, for chaining
11878 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
11879 this.errors
= errors
.slice();
11880 this.updateMessages();
11885 * Set the list of notice messages.
11887 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11888 * The array may contain strings or OO.ui.HtmlSnippet instances.
11890 * @return {OO.ui.BookletLayout} The layout, for chaining
11892 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
11893 this.notices
= notices
.slice();
11894 this.updateMessages();
11899 * Update the rendering of error and notice messages.
11903 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
11905 this.$messages
.empty();
11907 if ( this.errors
.length
|| this.notices
.length
) {
11908 this.$body
.after( this.$messages
);
11910 this.$messages
.remove();
11914 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
11915 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
11917 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
11918 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
11923 * Include information about the widget's accessKey in our title. TitledElement calls this method.
11924 * (This is a bit of a hack.)
11927 * @param {string} title Tooltip label for 'title' attribute
11930 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
11931 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
11932 return this.fieldWidget
.formatTitleWithAccessKey( title
);
11938 * Creates and returns the help element. Also sets the `aria-describedby`
11939 * attribute on the main element of the `fieldWidget`.
11942 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
11943 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
11944 * @return {jQuery} The element that should become `this.$help`.
11946 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
11947 var helpId
, helpWidget
;
11949 if ( this.helpInline
) {
11950 helpWidget
= new OO
.ui
.LabelWidget( {
11952 classes
: [ 'oo-ui-inline-help' ]
11955 helpId
= helpWidget
.getElementId();
11957 helpWidget
= new OO
.ui
.PopupButtonWidget( {
11958 $overlay
: $overlay
,
11962 classes
: [ 'oo-ui-fieldLayout-help' ],
11965 label
: OO
.ui
.msg( 'ooui-field-help' ),
11966 invisibleLabel
: true
11968 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
11969 helpWidget
.getPopup().$body
.html( help
.toString() );
11971 helpWidget
.getPopup().$body
.text( help
);
11974 helpId
= helpWidget
.getPopup().getBodyId();
11977 // Set the 'aria-describedby' attribute on the fieldWidget
11978 // Preference given to an input or a button
11980 this.fieldWidget
.$input
||
11981 this.fieldWidget
.$button
||
11982 this.fieldWidget
.$element
11983 ).attr( 'aria-describedby', helpId
);
11985 return helpWidget
.$element
;
11989 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
11990 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
11991 * is required and is specified before any optional configuration settings.
11993 * Labels can be aligned in one of four ways:
11995 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11996 * A left-alignment is used for forms with many fields.
11997 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11998 * A right-alignment is used for long but familiar forms which users tab through,
11999 * verifying the current field with a quick glance at the label.
12000 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12001 * that users fill out from top to bottom.
12002 * - **inline**: The label is placed after the field-widget and aligned to the left.
12003 * An inline-alignment is best used with checkboxes or radio buttons.
12005 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
12006 * text is specified.
12009 * // Example of an ActionFieldLayout
12010 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
12011 * new OO.ui.TextInputWidget( {
12012 * placeholder: 'Field widget'
12014 * new OO.ui.ButtonWidget( {
12018 * label: 'An ActionFieldLayout. This label is aligned top',
12020 * help: 'This is help text'
12024 * $( document.body ).append( actionFieldLayout.$element );
12027 * @extends OO.ui.FieldLayout
12030 * @param {OO.ui.Widget} fieldWidget Field widget
12031 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
12032 * @param {Object} config
12034 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
12035 // Allow passing positional parameters inside the config object
12036 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12037 config
= fieldWidget
;
12038 fieldWidget
= config
.fieldWidget
;
12039 buttonWidget
= config
.buttonWidget
;
12042 // Parent constructor
12043 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
12046 this.buttonWidget
= buttonWidget
;
12047 this.$button
= $( '<span>' );
12048 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12052 .addClass( 'oo-ui-actionFieldLayout' );
12054 .addClass( 'oo-ui-actionFieldLayout-button' )
12055 .append( this.buttonWidget
.$element
);
12057 .addClass( 'oo-ui-actionFieldLayout-input' )
12058 .append( this.fieldWidget
.$element
);
12060 .append( this.$input
, this.$button
);
12065 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12068 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12069 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12070 * configured with a label as well. For more information and examples,
12071 * please see the [OOUI documentation on MediaWiki][1].
12074 * // Example of a fieldset layout
12075 * var input1 = new OO.ui.TextInputWidget( {
12076 * placeholder: 'A text input field'
12079 * var input2 = new OO.ui.TextInputWidget( {
12080 * placeholder: 'A text input field'
12083 * var fieldset = new OO.ui.FieldsetLayout( {
12084 * label: 'Example of a fieldset layout'
12087 * fieldset.addItems( [
12088 * new OO.ui.FieldLayout( input1, {
12089 * label: 'Field One'
12091 * new OO.ui.FieldLayout( input2, {
12092 * label: 'Field Two'
12095 * $( document.body ).append( fieldset.$element );
12097 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12100 * @extends OO.ui.Layout
12101 * @mixins OO.ui.mixin.IconElement
12102 * @mixins OO.ui.mixin.LabelElement
12103 * @mixins OO.ui.mixin.GroupElement
12106 * @param {Object} [config] Configuration options
12107 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
12108 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
12109 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
12110 * For important messages, you are advised to use `notices`, as they are always shown.
12111 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12112 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12114 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12115 // Configuration initialization
12116 config
= config
|| {};
12118 // Parent constructor
12119 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12121 // Mixin constructors
12122 OO
.ui
.mixin
.IconElement
.call( this, config
);
12123 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12124 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12127 this.$header
= $( '<legend>' );
12128 if ( config
.help
) {
12129 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
12130 $overlay
: config
.$overlay
,
12134 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12137 label
: OO
.ui
.msg( 'ooui-field-help' ),
12138 invisibleLabel
: true
12140 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12141 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
12143 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
12145 this.$help
= this.popupButtonWidget
.$element
;
12147 this.$help
= $( [] );
12152 .addClass( 'oo-ui-fieldsetLayout-header' )
12153 .append( this.$icon
, this.$label
, this.$help
);
12154 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12156 .addClass( 'oo-ui-fieldsetLayout' )
12157 .prepend( this.$header
, this.$group
);
12158 if ( Array
.isArray( config
.items
) ) {
12159 this.addItems( config
.items
);
12165 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12166 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12167 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12168 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12170 /* Static Properties */
12176 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12179 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
12180 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
12181 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
12182 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12184 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12185 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12186 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12187 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12188 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12189 * often have simplified APIs to match the capabilities of HTML forms.
12190 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12192 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12193 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12196 * // Example of a form layout that wraps a fieldset layout
12197 * var input1 = new OO.ui.TextInputWidget( {
12198 * placeholder: 'Username'
12200 * var input2 = new OO.ui.TextInputWidget( {
12201 * placeholder: 'Password',
12204 * var submit = new OO.ui.ButtonInputWidget( {
12208 * var fieldset = new OO.ui.FieldsetLayout( {
12209 * label: 'A form layout'
12211 * fieldset.addItems( [
12212 * new OO.ui.FieldLayout( input1, {
12213 * label: 'Username',
12216 * new OO.ui.FieldLayout( input2, {
12217 * label: 'Password',
12220 * new OO.ui.FieldLayout( submit )
12222 * var form = new OO.ui.FormLayout( {
12223 * items: [ fieldset ],
12224 * action: '/api/formhandler',
12227 * $( document.body ).append( form.$element );
12230 * @extends OO.ui.Layout
12231 * @mixins OO.ui.mixin.GroupElement
12234 * @param {Object} [config] Configuration options
12235 * @cfg {string} [method] HTML form `method` attribute
12236 * @cfg {string} [action] HTML form `action` attribute
12237 * @cfg {string} [enctype] HTML form `enctype` attribute
12238 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12240 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12243 // Configuration initialization
12244 config
= config
|| {};
12246 // Parent constructor
12247 OO
.ui
.FormLayout
.parent
.call( this, config
);
12249 // Mixin constructors
12250 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12253 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12255 // Make sure the action is safe
12256 action
= config
.action
;
12257 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12258 action
= './' + action
;
12263 .addClass( 'oo-ui-formLayout' )
12265 method
: config
.method
,
12267 enctype
: config
.enctype
12269 if ( Array
.isArray( config
.items
) ) {
12270 this.addItems( config
.items
);
12276 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12277 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12282 * A 'submit' event is emitted when the form is submitted.
12287 /* Static Properties */
12293 OO
.ui
.FormLayout
.static.tagName
= 'form';
12298 * Handle form submit events.
12301 * @param {jQuery.Event} e Submit event
12303 * @return {OO.ui.FormLayout} The layout, for chaining
12305 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12306 if ( this.emit( 'submit' ) ) {
12312 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
12313 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
12316 * // Example of a panel layout
12317 * var panel = new OO.ui.PanelLayout( {
12321 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12323 * $( document.body ).append( panel.$element );
12326 * @extends OO.ui.Layout
12329 * @param {Object} [config] Configuration options
12330 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12331 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12332 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12333 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
12335 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12336 // Configuration initialization
12337 config
= $.extend( {
12344 // Parent constructor
12345 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12348 this.$element
.addClass( 'oo-ui-panelLayout' );
12349 if ( config
.scrollable
) {
12350 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12352 if ( config
.padded
) {
12353 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12355 if ( config
.expanded
) {
12356 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12358 if ( config
.framed
) {
12359 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12365 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12370 * Focus the panel layout
12372 * The default implementation just focuses the first focusable element in the panel
12374 OO
.ui
.PanelLayout
.prototype.focus = function () {
12375 OO
.ui
.findFocusable( this.$element
).focus();
12379 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12380 * items), with small margins between them. Convenient when you need to put a number of block-level
12381 * widgets on a single line next to each other.
12383 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12386 * // HorizontalLayout with a text input and a label
12387 * var layout = new OO.ui.HorizontalLayout( {
12389 * new OO.ui.LabelWidget( { label: 'Label' } ),
12390 * new OO.ui.TextInputWidget( { value: 'Text' } )
12393 * $( document.body ).append( layout.$element );
12396 * @extends OO.ui.Layout
12397 * @mixins OO.ui.mixin.GroupElement
12400 * @param {Object} [config] Configuration options
12401 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12403 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12404 // Configuration initialization
12405 config
= config
|| {};
12407 // Parent constructor
12408 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12410 // Mixin constructors
12411 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12414 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12415 if ( Array
.isArray( config
.items
) ) {
12416 this.addItems( config
.items
);
12422 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12423 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12426 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12427 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12428 * (to adjust the value in increments) to allow the user to enter a number.
12431 * // A NumberInputWidget.
12432 * var numberInput = new OO.ui.NumberInputWidget( {
12433 * label: 'NumberInputWidget',
12434 * input: { value: 5 },
12438 * $( document.body ).append( numberInput.$element );
12441 * @extends OO.ui.TextInputWidget
12444 * @param {Object} [config] Configuration options
12445 * @cfg {Object} [minusButton] Configuration options to pass to the
12446 * {@link OO.ui.ButtonWidget decrementing button widget}.
12447 * @cfg {Object} [plusButton] Configuration options to pass to the
12448 * {@link OO.ui.ButtonWidget incrementing button widget}.
12449 * @cfg {number} [min=-Infinity] Minimum allowed value
12450 * @cfg {number} [max=Infinity] Maximum allowed value
12451 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12452 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12453 * Defaults to `step` if specified, otherwise `1`.
12454 * @cfg {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12455 * Defaults to 10 times `buttonStep`.
12456 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12458 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12459 var $field
= $( '<div>' )
12460 .addClass( 'oo-ui-numberInputWidget-field' );
12462 // Configuration initialization
12463 config
= $.extend( {
12469 // For backward compatibility
12470 $.extend( config
, config
.input
);
12473 // Parent constructor
12474 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12478 if ( config
.showButtons
) {
12479 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12481 disabled
: this.isDisabled(),
12483 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12488 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12489 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12491 disabled
: this.isDisabled(),
12493 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12498 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12503 keydown
: this.onKeyDown
.bind( this ),
12504 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12506 if ( config
.showButtons
) {
12507 this.plusButton
.connect( this, {
12508 click
: [ 'onButtonClick', +1 ]
12510 this.minusButton
.connect( this, {
12511 click
: [ 'onButtonClick', -1 ]
12516 $field
.append( this.$input
);
12517 if ( config
.showButtons
) {
12519 .prepend( this.minusButton
.$element
)
12520 .append( this.plusButton
.$element
);
12524 if ( config
.allowInteger
|| config
.isInteger
) {
12525 // Backward compatibility
12528 this.setRange( config
.min
, config
.max
);
12529 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
12530 // Set the validation method after we set step and range
12531 // so that it doesn't immediately call setValidityFlag
12532 this.setValidation( this.validateNumber
.bind( this ) );
12535 .addClass( 'oo-ui-numberInputWidget' )
12536 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12542 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12546 // Backward compatibility
12547 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12548 this.setStep( flag
? 1 : null );
12550 // Backward compatibility
12551 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12553 // Backward compatibility
12554 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12555 return this.step
=== 1;
12557 // Backward compatibility
12558 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12561 * Set the range of allowed values
12563 * @param {number} min Minimum allowed value
12564 * @param {number} max Maximum allowed value
12566 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12568 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12572 this.$input
.attr( 'min', this.min
);
12573 this.$input
.attr( 'max', this.max
);
12574 this.setValidityFlag();
12578 * Get the current range
12580 * @return {number[]} Minimum and maximum values
12582 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12583 return [ this.min
, this.max
];
12587 * Set the stepping deltas
12589 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12590 * Defaults to `step` if specified, otherwise `1`.
12591 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12592 * Defaults to 10 times `buttonStep`.
12593 * @param {number|null} [step] If specified, the field only accepts values that are multiples of this.
12595 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
12596 if ( buttonStep
=== undefined ) {
12597 buttonStep
= step
|| 1;
12599 if ( pageStep
=== undefined ) {
12600 pageStep
= 10 * buttonStep
;
12602 if ( step
!== null && step
<= 0 ) {
12603 throw new Error( 'Step value, if given, must be positive' );
12605 if ( buttonStep
<= 0 ) {
12606 throw new Error( 'Button step value must be positive' );
12608 if ( pageStep
<= 0 ) {
12609 throw new Error( 'Page step value must be positive' );
12612 this.buttonStep
= buttonStep
;
12613 this.pageStep
= pageStep
;
12614 this.$input
.attr( 'step', this.step
|| 'any' );
12615 this.setValidityFlag();
12621 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
12622 if ( value
=== '' ) {
12623 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
12624 // so here we make sure an 'empty' value is actually displayed as such.
12625 this.$input
.val( '' );
12627 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
12631 * Get the current stepping values
12633 * @return {number[]} Button step, page step, and validity step
12635 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
12636 return [ this.buttonStep
, this.pageStep
, this.step
];
12640 * Get the current value of the widget as a number
12642 * @return {number} May be NaN, or an invalid number
12644 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
12645 return +this.getValue();
12649 * Adjust the value of the widget
12651 * @param {number} delta Adjustment amount
12653 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
12654 var n
, v
= this.getNumericValue();
12657 if ( isNaN( delta
) || !isFinite( delta
) ) {
12658 throw new Error( 'Delta must be a finite number' );
12661 if ( isNaN( v
) ) {
12665 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
12667 n
= Math
.round( n
/ this.step
) * this.step
;
12672 this.setValue( n
);
12679 * @param {string} value Field value
12680 * @return {boolean}
12682 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
12684 if ( value
=== '' ) {
12685 return !this.isRequired();
12688 if ( isNaN( n
) || !isFinite( n
) ) {
12692 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
12696 if ( n
< this.min
|| n
> this.max
) {
12704 * Handle mouse click events.
12707 * @param {number} dir +1 or -1
12709 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
12710 this.adjustValue( dir
* this.buttonStep
);
12714 * Handle mouse wheel events.
12717 * @param {jQuery.Event} event
12718 * @return {undefined/boolean} False to prevent default if event is handled
12720 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
12723 if ( !this.isDisabled() && this.$input
.is( ':focus' ) ) {
12724 // Standard 'wheel' event
12725 if ( event
.originalEvent
.deltaMode
!== undefined ) {
12726 this.sawWheelEvent
= true;
12728 if ( event
.originalEvent
.deltaY
) {
12729 delta
= -event
.originalEvent
.deltaY
;
12730 } else if ( event
.originalEvent
.deltaX
) {
12731 delta
= event
.originalEvent
.deltaX
;
12734 // Non-standard events
12735 if ( !this.sawWheelEvent
) {
12736 if ( event
.originalEvent
.wheelDeltaX
) {
12737 delta
= -event
.originalEvent
.wheelDeltaX
;
12738 } else if ( event
.originalEvent
.wheelDeltaY
) {
12739 delta
= event
.originalEvent
.wheelDeltaY
;
12740 } else if ( event
.originalEvent
.wheelDelta
) {
12741 delta
= event
.originalEvent
.wheelDelta
;
12742 } else if ( event
.originalEvent
.detail
) {
12743 delta
= -event
.originalEvent
.detail
;
12748 delta
= delta
< 0 ? -1 : 1;
12749 this.adjustValue( delta
* this.buttonStep
);
12757 * Handle key down events.
12760 * @param {jQuery.Event} e Key down event
12761 * @return {undefined/boolean} False to prevent default if event is handled
12763 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
12764 if ( !this.isDisabled() ) {
12765 switch ( e
.which
) {
12766 case OO
.ui
.Keys
.UP
:
12767 this.adjustValue( this.buttonStep
);
12769 case OO
.ui
.Keys
.DOWN
:
12770 this.adjustValue( -this.buttonStep
);
12772 case OO
.ui
.Keys
.PAGEUP
:
12773 this.adjustValue( this.pageStep
);
12775 case OO
.ui
.Keys
.PAGEDOWN
:
12776 this.adjustValue( -this.pageStep
);
12785 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
12787 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
12789 if ( this.minusButton
) {
12790 this.minusButton
.setDisabled( this.isDisabled() );
12792 if ( this.plusButton
) {
12793 this.plusButton
.setDisabled( this.isDisabled() );
12801 //# sourceMappingURL=oojs-ui-core.js.map.json