3 * https://www.mediawiki.org/wiki/OOUI
5 * Copyright 2011–2018 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2018-12-20T06:10:28Z
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 * $( '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 * $( '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 $( '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 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1343 $container
.queue( function ( next
) {
1350 return deferred
.promise();
1354 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1355 * and reserve space for them, because it probably doesn't.
1357 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1358 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1359 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1360 * and then reattach (or show) them back.
1363 * @param {HTMLElement} el Element to reconsider the scrollbars on
1365 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1366 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1367 // Save scroll position
1368 scrollLeft
= el
.scrollLeft
;
1369 scrollTop
= el
.scrollTop
;
1370 // Detach all children
1371 while ( el
.firstChild
) {
1372 nodes
.push( el
.firstChild
);
1373 el
.removeChild( el
.firstChild
);
1376 // eslint-disable-next-line no-void
1377 void el
.offsetHeight
;
1378 // Reattach all children
1379 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1380 el
.appendChild( nodes
[ i
] );
1382 // Restore scroll position (no-op if scrollbars disappeared)
1383 el
.scrollLeft
= scrollLeft
;
1384 el
.scrollTop
= scrollTop
;
1390 * Toggle visibility of an element.
1392 * @param {boolean} [show] Make element visible, omit to toggle visibility
1395 * @return {OO.ui.Element} The element, for chaining
1397 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1398 show
= show
=== undefined ? !this.visible
: !!show
;
1400 if ( show
!== this.isVisible() ) {
1401 this.visible
= show
;
1402 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1403 this.emit( 'toggle', show
);
1410 * Check if element is visible.
1412 * @return {boolean} element is visible
1414 OO
.ui
.Element
.prototype.isVisible = function () {
1415 return this.visible
;
1421 * @return {Mixed} Element data
1423 OO
.ui
.Element
.prototype.getData = function () {
1430 * @param {Mixed} data Element data
1432 * @return {OO.ui.Element} The element, for chaining
1434 OO
.ui
.Element
.prototype.setData = function ( data
) {
1440 * Set the element has an 'id' attribute.
1442 * @param {string} id
1444 * @return {OO.ui.Element} The element, for chaining
1446 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1447 this.elementId
= id
;
1448 this.$element
.attr( 'id', id
);
1453 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1454 * and return its value.
1458 OO
.ui
.Element
.prototype.getElementId = function () {
1459 if ( this.elementId
=== null ) {
1460 this.setElementId( OO
.ui
.generateElementId() );
1462 return this.elementId
;
1466 * Check if element supports one or more methods.
1468 * @param {string|string[]} methods Method or list of methods to check
1469 * @return {boolean} All methods are supported
1471 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1475 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1476 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1477 if ( typeof this[ methods
[ i
] ] === 'function' ) {
1482 return methods
.length
=== support
;
1486 * Update the theme-provided classes.
1488 * @localdoc This is called in element mixins and widget classes any time state changes.
1489 * Updating is debounced, minimizing overhead of changing multiple attributes and
1490 * guaranteeing that theme updates do not occur within an element's constructor
1492 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1493 OO
.ui
.theme
.queueUpdateElementClasses( this );
1497 * Get the HTML tag name.
1499 * Override this method to base the result on instance information.
1501 * @return {string} HTML tag name
1503 OO
.ui
.Element
.prototype.getTagName = function () {
1504 return this.constructor.static.tagName
;
1508 * Check if the element is attached to the DOM
1510 * @return {boolean} The element is attached to the DOM
1512 OO
.ui
.Element
.prototype.isElementAttached = function () {
1513 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1517 * Get the DOM document.
1519 * @return {HTMLDocument} Document object
1521 OO
.ui
.Element
.prototype.getElementDocument = function () {
1522 // Don't cache this in other ways either because subclasses could can change this.$element
1523 return OO
.ui
.Element
.static.getDocument( this.$element
);
1527 * Get the DOM window.
1529 * @return {Window} Window object
1531 OO
.ui
.Element
.prototype.getElementWindow = function () {
1532 return OO
.ui
.Element
.static.getWindow( this.$element
);
1536 * Get closest scrollable container.
1538 * @return {HTMLElement} Closest scrollable container
1540 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1541 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1545 * Get group element is in.
1547 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1549 OO
.ui
.Element
.prototype.getElementGroup = function () {
1550 return this.elementGroup
;
1554 * Set group element is in.
1556 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1558 * @return {OO.ui.Element} The element, for chaining
1560 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1561 this.elementGroup
= group
;
1566 * Scroll element into view.
1568 * @param {Object} [config] Configuration options
1569 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1571 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1573 !this.isElementAttached() ||
1574 !this.isVisible() ||
1575 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1577 return $.Deferred().resolve();
1579 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1583 * Restore the pre-infusion dynamic state for this widget.
1585 * This method is called after #$element has been inserted into DOM. The parameter is the return
1586 * value of #gatherPreInfuseState.
1589 * @param {Object} state
1591 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1595 * Wraps an HTML snippet for use with configuration values which default
1596 * to strings. This bypasses the default html-escaping done to string
1602 * @param {string} [content] HTML content
1604 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1606 this.content
= content
;
1611 OO
.initClass( OO
.ui
.HtmlSnippet
);
1618 * @return {string} Unchanged HTML snippet.
1620 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1621 return this.content
;
1625 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1626 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1627 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1628 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1629 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1633 * @extends OO.ui.Element
1634 * @mixins OO.EventEmitter
1637 * @param {Object} [config] Configuration options
1639 OO
.ui
.Layout
= function OoUiLayout( config
) {
1640 // Configuration initialization
1641 config
= config
|| {};
1643 // Parent constructor
1644 OO
.ui
.Layout
.parent
.call( this, config
);
1646 // Mixin constructors
1647 OO
.EventEmitter
.call( this );
1650 this.$element
.addClass( 'oo-ui-layout' );
1655 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1656 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1661 * Reset scroll offsets
1664 * @return {OO.ui.Layout} The layout, for chaining
1666 OO
.ui
.Layout
.prototype.resetScroll = function () {
1667 this.$element
[ 0 ].scrollTop
= 0;
1668 // TODO: Reset scrollLeft in an RTL-aware manner, see OO.ui.Element.static.getScrollLeft.
1674 * Widgets are compositions of one or more OOUI elements that users can both view
1675 * and interact with. All widgets can be configured and modified via a standard API,
1676 * and their state can change dynamically according to a model.
1680 * @extends OO.ui.Element
1681 * @mixins OO.EventEmitter
1684 * @param {Object} [config] Configuration options
1685 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1686 * appearance reflects this state.
1688 OO
.ui
.Widget
= function OoUiWidget( config
) {
1689 // Initialize config
1690 config
= $.extend( { disabled
: false }, config
);
1692 // Parent constructor
1693 OO
.ui
.Widget
.parent
.call( this, config
);
1695 // Mixin constructors
1696 OO
.EventEmitter
.call( this );
1699 this.disabled
= null;
1700 this.wasDisabled
= null;
1703 this.$element
.addClass( 'oo-ui-widget' );
1704 this.setDisabled( !!config
.disabled
);
1709 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1710 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1717 * A 'disable' event is emitted when the disabled state of the widget changes
1718 * (i.e. on disable **and** enable).
1720 * @param {boolean} disabled Widget is disabled
1726 * A 'toggle' event is emitted when the visibility of the widget changes.
1728 * @param {boolean} visible Widget is visible
1734 * Check if the widget is disabled.
1736 * @return {boolean} Widget is disabled
1738 OO
.ui
.Widget
.prototype.isDisabled = function () {
1739 return this.disabled
;
1743 * Set the 'disabled' state of the widget.
1745 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1747 * @param {boolean} disabled Disable widget
1749 * @return {OO.ui.Widget} The widget, for chaining
1751 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1754 this.disabled
= !!disabled
;
1755 isDisabled
= this.isDisabled();
1756 if ( isDisabled
!== this.wasDisabled
) {
1757 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1758 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1759 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1760 this.emit( 'disable', isDisabled
);
1761 this.updateThemeClasses();
1763 this.wasDisabled
= isDisabled
;
1769 * Update the disabled state, in case of changes in parent widget.
1772 * @return {OO.ui.Widget} The widget, for chaining
1774 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1775 this.setDisabled( this.disabled
);
1780 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1783 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1786 * @return {string|null} The ID of the labelable element
1788 OO
.ui
.Widget
.prototype.getInputId = function () {
1793 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1794 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1795 * override this method to provide intuitive, accessible behavior.
1797 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1798 * Individual widgets may override it too.
1800 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1803 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1814 OO
.ui
.Theme
= function OoUiTheme() {
1815 this.elementClassesQueue
= [];
1816 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1821 OO
.initClass( OO
.ui
.Theme
);
1826 * Get a list of classes to be applied to a widget.
1828 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1829 * otherwise state transitions will not work properly.
1831 * @param {OO.ui.Element} element Element for which to get classes
1832 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1834 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1835 return { on
: [], off
: [] };
1839 * Update CSS classes provided by the theme.
1841 * For elements with theme logic hooks, this should be called any time there's a state change.
1843 * @param {OO.ui.Element} element Element for which to update classes
1845 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1846 var $elements
= $( [] ),
1847 classes
= this.getElementClasses( element
);
1849 if ( element
.$icon
) {
1850 $elements
= $elements
.add( element
.$icon
);
1852 if ( element
.$indicator
) {
1853 $elements
= $elements
.add( element
.$indicator
);
1857 .removeClass( classes
.off
)
1858 .addClass( classes
.on
);
1864 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1866 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1867 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1870 this.elementClassesQueue
= [];
1874 * Queue #updateElementClasses to be called for this element.
1876 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1877 * to make them synchronous.
1879 * @param {OO.ui.Element} element Element for which to update classes
1881 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1882 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1883 // the most common case (this method is often called repeatedly for the same element).
1884 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1887 this.elementClassesQueue
.push( element
);
1888 this.debouncedUpdateQueuedElementClasses();
1892 * Get the transition duration in milliseconds for dialogs opening/closing
1894 * The dialog should be fully rendered this many milliseconds after the
1895 * ready process has executed.
1897 * @return {number} Transition duration in milliseconds
1899 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1904 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1905 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1906 * order in which users will navigate through the focusable elements via the "tab" key.
1909 * // TabIndexedElement is mixed into the ButtonWidget class
1910 * // to provide a tabIndex property.
1911 * var button1 = new OO.ui.ButtonWidget( {
1915 * var button2 = new OO.ui.ButtonWidget( {
1919 * var button3 = new OO.ui.ButtonWidget( {
1923 * var button4 = new OO.ui.ButtonWidget( {
1927 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1933 * @param {Object} [config] Configuration options
1934 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1935 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1936 * functionality will be applied to it instead.
1937 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1938 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1939 * to remove the element from the tab-navigation flow.
1941 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1942 // Configuration initialization
1943 config
= $.extend( { tabIndex
: 0 }, config
);
1946 this.$tabIndexed
= null;
1947 this.tabIndex
= null;
1950 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1953 this.setTabIndex( config
.tabIndex
);
1954 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1959 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1964 * Set the element that should use the tabindex functionality.
1966 * This method is used to retarget a tabindex mixin so that its functionality applies
1967 * to the specified element. If an element is currently using the functionality, the mixin’s
1968 * effect on that element is removed before the new element is set up.
1970 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1972 * @return {OO.ui.Element} The element, for chaining
1974 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1975 var tabIndex
= this.tabIndex
;
1976 // Remove attributes from old $tabIndexed
1977 this.setTabIndex( null );
1978 // Force update of new $tabIndexed
1979 this.$tabIndexed
= $tabIndexed
;
1980 this.tabIndex
= tabIndex
;
1981 return this.updateTabIndex();
1985 * Set the value of the tabindex.
1987 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
1989 * @return {OO.ui.Element} The element, for chaining
1991 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1992 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
1994 if ( this.tabIndex
!== tabIndex
) {
1995 this.tabIndex
= tabIndex
;
1996 this.updateTabIndex();
2003 * Update the `tabindex` attribute, in case of changes to tab index or
2008 * @return {OO.ui.Element} The element, for chaining
2010 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
2011 if ( this.$tabIndexed
) {
2012 if ( this.tabIndex
!== null ) {
2013 // Do not index over disabled elements
2014 this.$tabIndexed
.attr( {
2015 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
2016 // Support: ChromeVox and NVDA
2017 // These do not seem to inherit aria-disabled from parent elements
2018 'aria-disabled': this.isDisabled().toString()
2021 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
2028 * Handle disable events.
2031 * @param {boolean} disabled Element is disabled
2033 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
2034 this.updateTabIndex();
2038 * Get the value of the tabindex.
2040 * @return {number|null} Tabindex value
2042 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2043 return this.tabIndex
;
2047 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2049 * If the element already has an ID then that is returned, otherwise unique ID is
2050 * generated, set on the element, and returned.
2052 * @return {string|null} The ID of the focusable element
2054 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2057 if ( !this.$tabIndexed
) {
2060 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2064 id
= this.$tabIndexed
.attr( 'id' );
2065 if ( id
=== undefined ) {
2066 id
= OO
.ui
.generateElementId();
2067 this.$tabIndexed
.attr( 'id', id
);
2074 * Whether the node is 'labelable' according to the HTML spec
2075 * (i.e., whether it can be interacted with through a `<label for="…">`).
2076 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2079 * @param {jQuery} $node
2082 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2084 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2085 tagName
= $node
.prop( 'tagName' ).toLowerCase();
2087 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2090 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2097 * Focus this element.
2100 * @return {OO.ui.Element} The element, for chaining
2102 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2103 if ( !this.isDisabled() ) {
2104 this.$tabIndexed
.focus();
2110 * Blur this element.
2113 * @return {OO.ui.Element} The element, for chaining
2115 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2116 this.$tabIndexed
.blur();
2121 * @inheritdoc OO.ui.Widget
2123 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2128 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2129 * interface element that can be configured with access keys for accessibility.
2130 * See the [OOUI documentation on MediaWiki] [1] for examples.
2132 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2138 * @param {Object} [config] Configuration options
2139 * @cfg {jQuery} [$button] The button element created by the class.
2140 * If this configuration is omitted, the button element will use a generated `<a>`.
2141 * @cfg {boolean} [framed=true] Render the button with a frame
2143 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2144 // Configuration initialization
2145 config
= config
|| {};
2148 this.$button
= null;
2150 this.active
= config
.active
!== undefined && config
.active
;
2151 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
2152 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2153 this.onDocumentKeyUpHandler
= this.onDocumentKeyUp
.bind( this );
2154 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2155 this.onClickHandler
= this.onClick
.bind( this );
2156 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2159 this.$element
.addClass( 'oo-ui-buttonElement' );
2160 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2161 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2166 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2168 /* Static Properties */
2171 * Cancel mouse down events.
2173 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
2174 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
2175 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
2180 * @property {boolean}
2182 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2187 * A 'click' event is emitted when the button element is clicked.
2195 * Set the button element.
2197 * This method is used to retarget a button mixin so that its functionality applies to
2198 * the specified button element instead of the one created by the class. If a button element
2199 * is already set, the method will remove the mixin’s effect on that element.
2201 * @param {jQuery} $button Element to use as button
2203 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2204 if ( this.$button
) {
2206 .removeClass( 'oo-ui-buttonElement-button' )
2207 .removeAttr( 'role accesskey' )
2209 mousedown
: this.onMouseDownHandler
,
2210 keydown
: this.onKeyDownHandler
,
2211 click
: this.onClickHandler
,
2212 keypress
: this.onKeyPressHandler
2216 this.$button
= $button
2217 .addClass( 'oo-ui-buttonElement-button' )
2219 mousedown
: this.onMouseDownHandler
,
2220 keydown
: this.onKeyDownHandler
,
2221 click
: this.onClickHandler
,
2222 keypress
: this.onKeyPressHandler
2225 // Add `role="button"` on `<a>` elements, where it's needed
2226 // `toUpperCase()` is added for XHTML documents
2227 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2228 this.$button
.attr( 'role', 'button' );
2233 * Handles mouse down events.
2236 * @param {jQuery.Event} e Mouse down event
2237 * @return {undefined/boolean} False to prevent default if event is handled
2239 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2240 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2243 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2244 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2245 // reliably remove the pressed class
2246 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2247 // Prevent change of focus unless specifically configured otherwise
2248 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2254 * Handles document mouse up events.
2257 * @param {MouseEvent} e Mouse up event
2259 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentMouseUp = function ( e
) {
2260 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2263 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2264 // Stop listening for mouseup, since we only needed this once
2265 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2268 // Deprecated alias since 0.28.3
2269 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function () {
2270 OO
.ui
.warnDeprecation( 'onMouseUp is deprecated, use onDocumentMouseUp instead' );
2271 this.onDocumentMouseUp
.apply( this, arguments
);
2275 * Handles mouse click events.
2278 * @param {jQuery.Event} e Mouse click event
2280 * @return {undefined/boolean} False to prevent default if event is handled
2282 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2283 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2284 if ( this.emit( 'click' ) ) {
2291 * Handles key down events.
2294 * @param {jQuery.Event} e Key down event
2296 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2297 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2300 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2301 // Run the keyup handler no matter where the key is when the button is let go, so we can
2302 // reliably remove the pressed class
2303 this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2307 * Handles document key up events.
2310 * @param {KeyboardEvent} e Key up event
2312 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentKeyUp = function ( e
) {
2313 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2316 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2317 // Stop listening for keyup, since we only needed this once
2318 this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2321 // Deprecated alias since 0.28.3
2322 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function () {
2323 OO
.ui
.warnDeprecation( 'onKeyUp is deprecated, use onDocumentKeyUp instead' );
2324 this.onDocumentKeyUp
.apply( this, arguments
);
2328 * Handles key press events.
2331 * @param {jQuery.Event} e Key press event
2333 * @return {undefined/boolean} False to prevent default if event is handled
2335 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2336 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2337 if ( this.emit( 'click' ) ) {
2344 * Check if button has a frame.
2346 * @return {boolean} Button is framed
2348 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2353 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2355 * @param {boolean} [framed] Make button framed, omit to toggle
2357 * @return {OO.ui.Element} The element, for chaining
2359 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2360 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2361 if ( framed
!== this.framed
) {
2362 this.framed
= framed
;
2364 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2365 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2366 this.updateThemeClasses();
2373 * Set the button's active state.
2375 * The active state can be set on:
2377 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2378 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2379 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2382 * @param {boolean} value Make button active
2384 * @return {OO.ui.Element} The element, for chaining
2386 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2387 this.active
= !!value
;
2388 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2389 this.updateThemeClasses();
2394 * Check if the button is active
2397 * @return {boolean} The button is active
2399 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2404 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2405 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2406 * items from the group is done through the interface the class provides.
2407 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2409 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2412 * @mixins OO.EmitterList
2416 * @param {Object} [config] Configuration options
2417 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2418 * is omitted, the group element will use a generated `<div>`.
2420 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2421 // Configuration initialization
2422 config
= config
|| {};
2424 // Mixin constructors
2425 OO
.EmitterList
.call( this, config
);
2431 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2436 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2443 * A change event is emitted when the set of selected items changes.
2445 * @param {OO.ui.Element[]} items Items currently in the group
2451 * Set the group element.
2453 * If an element is already set, items will be moved to the new element.
2455 * @param {jQuery} $group Element to use as group
2457 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2460 this.$group
= $group
;
2461 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2462 this.$group
.append( this.items
[ i
].$element
);
2467 * Find an item by its data.
2469 * Only the first item with matching data will be returned. To return all matching items,
2470 * use the #findItemsFromData method.
2472 * @param {Object} data Item data to search for
2473 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2475 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2477 hash
= OO
.getHash( data
);
2479 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2480 item
= this.items
[ i
];
2481 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2490 * Find items by their data.
2492 * All items with matching data will be returned. To return only the first match, use the #findItemFromData method instead.
2494 * @param {Object} data Item data to search for
2495 * @return {OO.ui.Element[]} Items with equivalent data
2497 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2499 hash
= OO
.getHash( data
),
2502 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2503 item
= this.items
[ i
];
2504 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2513 * Add items to the group.
2515 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2516 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2518 * @param {OO.ui.Element[]} items An array of items to add to the group
2519 * @param {number} [index] Index of the insertion point
2521 * @return {OO.ui.Element} The element, for chaining
2523 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2525 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2527 this.emit( 'change', this.getItems() );
2534 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2535 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2536 this.insertItemElements( items
, newIndex
);
2539 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2547 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2548 item
.setElementGroup( this );
2549 this.insertItemElements( item
, index
);
2552 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2558 * Insert elements into the group
2561 * @param {OO.ui.Element} itemWidget Item to insert
2562 * @param {number} index Insertion index
2564 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2565 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2566 this.$group
.append( itemWidget
.$element
);
2567 } else if ( index
=== 0 ) {
2568 this.$group
.prepend( itemWidget
.$element
);
2570 this.items
[ index
].$element
.before( itemWidget
.$element
);
2575 * Remove the specified items from a group.
2577 * Removed items are detached (not removed) from the DOM so that they may be reused.
2578 * To remove all items from a group, you may wish to use the #clearItems method instead.
2580 * @param {OO.ui.Element[]} items An array of items to remove
2582 * @return {OO.ui.Element} The element, for chaining
2584 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2585 var i
, len
, item
, index
;
2587 // Remove specific items elements
2588 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2590 index
= this.items
.indexOf( item
);
2591 if ( index
!== -1 ) {
2592 item
.setElementGroup( null );
2593 item
.$element
.detach();
2598 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2600 this.emit( 'change', this.getItems() );
2605 * Clear all items from the group.
2607 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2608 * To remove only a subset of items from a group, use the #removeItems method.
2611 * @return {OO.ui.Element} The element, for chaining
2613 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2616 // Remove all item elements
2617 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2618 this.items
[ i
].setElementGroup( null );
2619 this.items
[ i
].$element
.detach();
2623 OO
.EmitterList
.prototype.clearItems
.call( this );
2625 this.emit( 'change', this.getItems() );
2630 * LabelElement is often mixed into other classes to generate a label, which
2631 * helps identify the function of an interface element.
2632 * See the [OOUI documentation on MediaWiki] [1] for more information.
2634 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2640 * @param {Object} [config] Configuration options
2641 * @cfg {jQuery} [$label] The label element created by the class. If this
2642 * configuration is omitted, the label element will use a generated `<span>`.
2643 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2644 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2645 * in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2646 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2647 * @cfg {boolean} [invisibleLabel] Whether the label should be visually hidden (but still accessible
2648 * to screen-readers).
2650 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2651 // Configuration initialization
2652 config
= config
|| {};
2657 this.invisibleLabel
= null;
2660 this.setLabel( config
.label
|| this.constructor.static.label
);
2661 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2662 this.setInvisibleLabel( config
.invisibleLabel
);
2667 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2672 * @event labelChange
2673 * @param {string} value
2676 /* Static Properties */
2679 * The label text. The label can be specified as a plaintext string, a function that will
2680 * produce a string in the future, or `null` for no label. The static value will
2681 * be overridden if a label is specified with the #label config option.
2685 * @property {string|Function|null}
2687 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2689 /* Static methods */
2692 * Highlight the first occurrence of the query in the given text
2694 * @param {string} text Text
2695 * @param {string} query Query to find
2696 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2697 * @return {jQuery} Text with the first match of the query
2698 * sub-string wrapped in highlighted span
2700 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2703 $result
= $( '<span>' );
2707 qLen
= query
.length
;
2708 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2709 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2714 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2717 if ( !query
.length
|| offset
=== -1 ) {
2718 $result
.text( text
);
2721 document
.createTextNode( text
.slice( 0, offset
) ),
2723 .addClass( 'oo-ui-labelElement-label-highlight' )
2724 .text( text
.slice( offset
, offset
+ query
.length
) ),
2725 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2728 return $result
.contents();
2734 * Set the label element.
2736 * If an element is already set, it will be cleaned up before setting up the new element.
2738 * @param {jQuery} $label Element to use as label
2740 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2741 if ( this.$label
) {
2742 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2745 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2746 this.setLabelContent( this.label
);
2752 * An empty string will result in the label being hidden. A string containing only whitespace will
2753 * be converted to a single ` `.
2755 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2756 * text; or null for no label
2758 * @return {OO.ui.Element} The element, for chaining
2760 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2761 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2762 label
= ( ( typeof label
=== 'string' || label
instanceof $ ) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2764 if ( this.label
!== label
) {
2765 if ( this.$label
) {
2766 this.setLabelContent( label
);
2769 this.emit( 'labelChange' );
2772 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2778 * Set whether the label should be visually hidden (but still accessible to screen-readers).
2780 * @param {boolean} invisibleLabel
2782 * @return {OO.ui.Element} The element, for chaining
2784 OO
.ui
.mixin
.LabelElement
.prototype.setInvisibleLabel = function ( invisibleLabel
) {
2785 invisibleLabel
= !!invisibleLabel
;
2787 if ( this.invisibleLabel
!== invisibleLabel
) {
2788 this.invisibleLabel
= invisibleLabel
;
2789 this.emit( 'labelChange' );
2792 this.$label
.toggleClass( 'oo-ui-labelElement-invisible', this.invisibleLabel
);
2793 // Pretend that there is no label, a lot of CSS has been written with this assumption
2794 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2800 * Set the label as plain text with a highlighted query
2802 * @param {string} text Text label to set
2803 * @param {string} query Substring of text to highlight
2804 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2806 * @return {OO.ui.Element} The element, for chaining
2808 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
2809 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
2815 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2816 * text; or null for no label
2818 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2823 * Set the content of the label.
2825 * Do not call this method until after the label element has been set by #setLabelElement.
2828 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2829 * text; or null for no label
2831 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2832 if ( typeof label
=== 'string' ) {
2833 if ( label
.match( /^\s*$/ ) ) {
2834 // Convert whitespace only string to a single non-breaking space
2835 this.$label
.html( ' ' );
2837 this.$label
.text( label
);
2839 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2840 this.$label
.html( label
.toString() );
2841 } else if ( label
instanceof $ ) {
2842 this.$label
.empty().append( label
);
2844 this.$label
.empty();
2849 * IconElement is often mixed into other classes to generate an icon.
2850 * Icons are graphics, about the size of normal text. They are used to aid the user
2851 * in locating a control or to convey information in a space-efficient way. See the
2852 * [OOUI documentation on MediaWiki] [1] for a list of icons
2853 * included in the library.
2855 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2861 * @param {Object} [config] Configuration options
2862 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2863 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2864 * the icon element be set to an existing icon instead of the one generated by this class, set a
2865 * value using a jQuery selection. For example:
2867 * // Use a <div> tag instead of a <span>
2869 * // Use an existing icon element instead of the one generated by the class
2870 * $icon: this.$element
2871 * // Use an icon element from a child widget
2872 * $icon: this.childwidget.$element
2873 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2874 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2875 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2876 * by the user's language.
2878 * Example of an i18n map:
2880 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2881 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2882 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2883 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2884 * text. The icon title is displayed when users move the mouse over the icon.
2886 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2887 // Configuration initialization
2888 config
= config
|| {};
2893 this.iconTitle
= null;
2896 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2897 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2898 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2903 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2905 /* Static Properties */
2908 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2909 * for i18n purposes and contains a `default` icon name and additional names keyed by
2910 * language code. The `default` name is used when no icon is keyed by the user's language.
2912 * Example of an i18n map:
2914 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2916 * Note: the static property will be overridden if the #icon configuration is used.
2920 * @property {Object|string}
2922 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2925 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2926 * function that returns title text, or `null` for no title.
2928 * The static property will be overridden if the #iconTitle configuration is used.
2932 * @property {string|Function|null}
2934 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2939 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2940 * applies to the specified icon element instead of the one created by the class. If an icon
2941 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2942 * and mixin methods will no longer affect the element.
2944 * @param {jQuery} $icon Element to use as icon
2946 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2949 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2950 .removeAttr( 'title' );
2954 .addClass( 'oo-ui-iconElement-icon' )
2955 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
)
2956 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2957 if ( this.iconTitle
!== null ) {
2958 this.$icon
.attr( 'title', this.iconTitle
);
2961 this.updateThemeClasses();
2965 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2966 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2969 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2970 * by language code, or `null` to remove the icon.
2972 * @return {OO.ui.Element} The element, for chaining
2974 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2975 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2976 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2978 if ( this.icon
!== icon
) {
2980 if ( this.icon
!== null ) {
2981 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2983 if ( icon
!== null ) {
2984 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2990 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2992 this.$icon
.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
);
2994 this.updateThemeClasses();
3000 * Set the icon title. Use `null` to remove the title.
3002 * @param {string|Function|null} iconTitle A text string used as the icon title,
3003 * a function that returns title text, or `null` for no title.
3005 * @return {OO.ui.Element} The element, for chaining
3007 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
3009 ( typeof iconTitle
=== 'function' || ( typeof iconTitle
=== 'string' && iconTitle
.length
) ) ?
3010 OO
.ui
.resolveMsg( iconTitle
) : null;
3012 if ( this.iconTitle
!== iconTitle
) {
3013 this.iconTitle
= iconTitle
;
3015 if ( this.iconTitle
!== null ) {
3016 this.$icon
.attr( 'title', iconTitle
);
3018 this.$icon
.removeAttr( 'title' );
3027 * Get the symbolic name of the icon.
3029 * @return {string} Icon name
3031 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
3036 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
3038 * @return {string} Icon title text
3040 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
3041 return this.iconTitle
;
3045 * IndicatorElement is often mixed into other classes to generate an indicator.
3046 * Indicators are small graphics that are generally used in two ways:
3048 * - To draw attention to the status of an item. For example, an indicator might be
3049 * used to show that an item in a list has errors that need to be resolved.
3050 * - To clarify the function of a control that acts in an exceptional way (a button
3051 * that opens a menu instead of performing an action directly, for example).
3053 * For a list of indicators included in the library, please see the
3054 * [OOUI documentation on MediaWiki] [1].
3056 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3062 * @param {Object} [config] Configuration options
3063 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
3064 * configuration is omitted, the indicator element will use a generated `<span>`.
3065 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3066 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
3068 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3069 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
3070 * or a function that returns title text. The indicator title is displayed when users move
3071 * the mouse over the indicator.
3073 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
3074 // Configuration initialization
3075 config
= config
|| {};
3078 this.$indicator
= null;
3079 this.indicator
= null;
3080 this.indicatorTitle
= null;
3083 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
3084 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
3085 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
3090 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
3092 /* Static Properties */
3095 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3096 * The static property will be overridden if the #indicator configuration is used.
3100 * @property {string|null}
3102 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
3105 * A text string used as the indicator title, a function that returns title text, or `null`
3106 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
3110 * @property {string|Function|null}
3112 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
3117 * Set the indicator element.
3119 * If an element is already set, it will be cleaned up before setting up the new element.
3121 * @param {jQuery} $indicator Element to use as indicator
3123 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
3124 if ( this.$indicator
) {
3126 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
3127 .removeAttr( 'title' );
3130 this.$indicator
= $indicator
3131 .addClass( 'oo-ui-indicatorElement-indicator' )
3132 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
)
3133 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
3134 if ( this.indicatorTitle
!== null ) {
3135 this.$indicator
.attr( 'title', this.indicatorTitle
);
3138 this.updateThemeClasses();
3142 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null` to remove the indicator.
3144 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
3146 * @return {OO.ui.Element} The element, for chaining
3148 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
3149 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
3151 if ( this.indicator
!== indicator
) {
3152 if ( this.$indicator
) {
3153 if ( this.indicator
!== null ) {
3154 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
3156 if ( indicator
!== null ) {
3157 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
3160 this.indicator
= indicator
;
3163 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
3164 if ( this.$indicator
) {
3165 this.$indicator
.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
);
3167 this.updateThemeClasses();
3173 * Set the indicator title.
3175 * The title is displayed when a user moves the mouse over the indicator.
3177 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
3178 * `null` for no indicator title
3180 * @return {OO.ui.Element} The element, for chaining
3182 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
3184 ( typeof indicatorTitle
=== 'function' || ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ) ?
3185 OO
.ui
.resolveMsg( indicatorTitle
) : null;
3187 if ( this.indicatorTitle
!== indicatorTitle
) {
3188 this.indicatorTitle
= indicatorTitle
;
3189 if ( this.$indicator
) {
3190 if ( this.indicatorTitle
!== null ) {
3191 this.$indicator
.attr( 'title', indicatorTitle
);
3193 this.$indicator
.removeAttr( 'title' );
3202 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3204 * @return {string} Symbolic name of indicator
3206 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
3207 return this.indicator
;
3211 * Get the indicator title.
3213 * The title is displayed when a user moves the mouse over the indicator.
3215 * @return {string} Indicator title text
3217 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
3218 return this.indicatorTitle
;
3222 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3223 * additional functionality to an element created by another class. The class provides
3224 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3225 * which are used to customize the look and feel of a widget to better describe its
3226 * importance and functionality.
3228 * The library currently contains the following styling flags for general use:
3230 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
3231 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
3233 * The flags affect the appearance of the buttons:
3236 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3237 * var button1 = new OO.ui.ButtonWidget( {
3238 * label: 'Progressive',
3239 * flags: 'progressive'
3241 * var button2 = new OO.ui.ButtonWidget( {
3242 * label: 'Destructive',
3243 * flags: 'destructive'
3245 * $( 'body' ).append( button1.$element, button2.$element );
3247 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
3248 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3250 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3256 * @param {Object} [config] Configuration options
3257 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary') to apply.
3258 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3259 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3260 * @cfg {jQuery} [$flagged] The flagged element. By default,
3261 * the flagged functionality is applied to the element created by the class ($element).
3262 * If a different element is specified, the flagged functionality will be applied to it instead.
3264 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3265 // Configuration initialization
3266 config
= config
|| {};
3270 this.$flagged
= null;
3273 this.setFlags( config
.flags
);
3274 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3281 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3282 * parameter contains the name of each modified flag and indicates whether it was
3285 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3286 * that the flag was added, `false` that the flag was removed.
3292 * Set the flagged element.
3294 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3295 * If an element is already set, the method will remove the mixin’s effect on that element.
3297 * @param {jQuery} $flagged Element that should be flagged
3299 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3300 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3301 return 'oo-ui-flaggedElement-' + flag
;
3304 if ( this.$flagged
) {
3305 this.$flagged
.removeClass( classNames
);
3308 this.$flagged
= $flagged
.addClass( classNames
);
3312 * Check if the specified flag is set.
3314 * @param {string} flag Name of flag
3315 * @return {boolean} The flag is set
3317 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3318 // This may be called before the constructor, thus before this.flags is set
3319 return this.flags
&& ( flag
in this.flags
);
3323 * Get the names of all flags set.
3325 * @return {string[]} Flag names
3327 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3328 // This may be called before the constructor, thus before this.flags is set
3329 return Object
.keys( this.flags
|| {} );
3336 * @return {OO.ui.Element} The element, for chaining
3339 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3340 var flag
, className
,
3343 classPrefix
= 'oo-ui-flaggedElement-';
3345 for ( flag
in this.flags
) {
3346 className
= classPrefix
+ flag
;
3347 changes
[ flag
] = false;
3348 delete this.flags
[ flag
];
3349 remove
.push( className
);
3352 if ( this.$flagged
) {
3353 this.$flagged
.removeClass( remove
);
3356 this.updateThemeClasses();
3357 this.emit( 'flag', changes
);
3363 * Add one or more flags.
3365 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3366 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3367 * be added (`true`) or removed (`false`).
3369 * @return {OO.ui.Element} The element, for chaining
3372 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3373 var i
, len
, flag
, className
,
3377 classPrefix
= 'oo-ui-flaggedElement-';
3379 if ( typeof flags
=== 'string' ) {
3380 className
= classPrefix
+ flags
;
3382 if ( !this.flags
[ flags
] ) {
3383 this.flags
[ flags
] = true;
3384 add
.push( className
);
3386 } else if ( Array
.isArray( flags
) ) {
3387 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3389 className
= classPrefix
+ flag
;
3391 if ( !this.flags
[ flag
] ) {
3392 changes
[ flag
] = true;
3393 this.flags
[ flag
] = true;
3394 add
.push( className
);
3397 } else if ( OO
.isPlainObject( flags
) ) {
3398 for ( flag
in flags
) {
3399 className
= classPrefix
+ flag
;
3400 if ( flags
[ flag
] ) {
3402 if ( !this.flags
[ flag
] ) {
3403 changes
[ flag
] = true;
3404 this.flags
[ flag
] = true;
3405 add
.push( className
);
3409 if ( this.flags
[ flag
] ) {
3410 changes
[ flag
] = false;
3411 delete this.flags
[ flag
];
3412 remove
.push( className
);
3418 if ( this.$flagged
) {
3421 .removeClass( remove
);
3424 this.updateThemeClasses();
3425 this.emit( 'flag', changes
);
3431 * TitledElement is mixed into other classes to provide a `title` attribute.
3432 * Titles are rendered by the browser and are made visible when the user moves
3433 * the mouse over the element. Titles are not visible on touch devices.
3436 * // TitledElement provides a 'title' attribute to the
3437 * // ButtonWidget class
3438 * var button = new OO.ui.ButtonWidget( {
3439 * label: 'Button with Title',
3440 * title: 'I am a button'
3442 * $( 'body' ).append( button.$element );
3448 * @param {Object} [config] Configuration options
3449 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3450 * If this config is omitted, the title functionality is applied to $element, the
3451 * element created by the class.
3452 * @cfg {string|Function} [title] The title text or a function that returns text. If
3453 * this config is omitted, the value of the {@link #static-title static title} property is used.
3455 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3456 // Configuration initialization
3457 config
= config
|| {};
3460 this.$titled
= null;
3464 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3465 this.setTitledElement( config
.$titled
|| this.$element
);
3470 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3472 /* Static Properties */
3475 * The title text, a function that returns text, or `null` for no title. The value of the static property
3476 * is overridden if the #title config option is used.
3480 * @property {string|Function|null}
3482 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3487 * Set the titled element.
3489 * This method is used to retarget a TitledElement mixin so that its functionality applies to the specified element.
3490 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3492 * @param {jQuery} $titled Element that should use the 'titled' functionality
3494 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3495 if ( this.$titled
) {
3496 this.$titled
.removeAttr( 'title' );
3499 this.$titled
= $titled
;
3508 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3510 * @return {OO.ui.Element} The element, for chaining
3512 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3513 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3514 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3516 if ( this.title
!== title
) {
3525 * Update the title attribute, in case of changes to title or accessKey.
3529 * @return {OO.ui.Element} The element, for chaining
3531 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3532 var title
= this.getTitle();
3533 if ( this.$titled
) {
3534 if ( title
!== null ) {
3535 // Only if this is an AccessKeyedElement
3536 if ( this.formatTitleWithAccessKey
) {
3537 title
= this.formatTitleWithAccessKey( title
);
3539 this.$titled
.attr( 'title', title
);
3541 this.$titled
.removeAttr( 'title' );
3550 * @return {string} Title string
3552 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3557 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3558 * Accesskeys allow an user to go to a specific element by using
3559 * a shortcut combination of a browser specific keys + the key
3563 * // AccessKeyedElement provides an 'accesskey' attribute to the
3564 * // ButtonWidget class
3565 * var button = new OO.ui.ButtonWidget( {
3566 * label: 'Button with Accesskey',
3569 * $( 'body' ).append( button.$element );
3575 * @param {Object} [config] Configuration options
3576 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3577 * If this config is omitted, the accesskey functionality is applied to $element, the
3578 * element created by the class.
3579 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3580 * this config is omitted, no accesskey will be added.
3582 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3583 // Configuration initialization
3584 config
= config
|| {};
3587 this.$accessKeyed
= null;
3588 this.accessKey
= null;
3591 this.setAccessKey( config
.accessKey
|| null );
3592 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3594 // If this is also a TitledElement and it initialized before we did, we may have
3595 // to update the title with the access key
3596 if ( this.updateTitle
) {
3603 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3605 /* Static Properties */
3608 * The access key, a function that returns a key, or `null` for no accesskey.
3612 * @property {string|Function|null}
3614 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3619 * Set the accesskeyed element.
3621 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3622 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3624 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyed' functionality
3626 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3627 if ( this.$accessKeyed
) {
3628 this.$accessKeyed
.removeAttr( 'accesskey' );
3631 this.$accessKeyed
= $accessKeyed
;
3632 if ( this.accessKey
) {
3633 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3640 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3642 * @return {OO.ui.Element} The element, for chaining
3644 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3645 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3647 if ( this.accessKey
!== accessKey
) {
3648 if ( this.$accessKeyed
) {
3649 if ( accessKey
!== null ) {
3650 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3652 this.$accessKeyed
.removeAttr( 'accesskey' );
3655 this.accessKey
= accessKey
;
3657 // Only if this is a TitledElement
3658 if ( this.updateTitle
) {
3669 * @return {string} accessKey string
3671 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3672 return this.accessKey
;
3676 * Add information about the access key to the element's tooltip label.
3677 * (This is only public for hacky usage in FieldLayout.)
3679 * @param {string} title Tooltip label for `title` attribute
3682 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3685 if ( !this.$accessKeyed
) {
3686 // Not initialized yet; the constructor will call updateTitle() which will rerun this function
3689 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the single key
3690 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3691 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3693 accessKey
= this.getAccessKey();
3696 title
+= ' [' + accessKey
+ ']';
3702 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3703 * feels, and functionality can be customized via the class’s configuration options
3704 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3707 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3710 * // A button widget
3711 * var button = new OO.ui.ButtonWidget( {
3712 * label: 'Button with Icon',
3716 * $( 'body' ).append( button.$element );
3718 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3721 * @extends OO.ui.Widget
3722 * @mixins OO.ui.mixin.ButtonElement
3723 * @mixins OO.ui.mixin.IconElement
3724 * @mixins OO.ui.mixin.IndicatorElement
3725 * @mixins OO.ui.mixin.LabelElement
3726 * @mixins OO.ui.mixin.TitledElement
3727 * @mixins OO.ui.mixin.FlaggedElement
3728 * @mixins OO.ui.mixin.TabIndexedElement
3729 * @mixins OO.ui.mixin.AccessKeyedElement
3732 * @param {Object} [config] Configuration options
3733 * @cfg {boolean} [active=false] Whether button should be shown as active
3734 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3735 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3736 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3738 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3739 // Configuration initialization
3740 config
= config
|| {};
3742 // Parent constructor
3743 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3745 // Mixin constructors
3746 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3747 OO
.ui
.mixin
.IconElement
.call( this, config
);
3748 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3749 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3750 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3751 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3752 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3753 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3758 this.noFollow
= false;
3761 this.connect( this, { disable
: 'onDisable' } );
3764 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3766 .addClass( 'oo-ui-buttonWidget' )
3767 .append( this.$button
);
3768 this.setActive( config
.active
);
3769 this.setHref( config
.href
);
3770 this.setTarget( config
.target
);
3771 this.setNoFollow( config
.noFollow
);
3776 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3777 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3778 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3779 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3780 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3781 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3782 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3783 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3784 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3786 /* Static Properties */
3792 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3798 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3803 * Get hyperlink location.
3805 * @return {string} Hyperlink location
3807 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3812 * Get hyperlink target.
3814 * @return {string} Hyperlink target
3816 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3821 * Get search engine traversal hint.
3823 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3825 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3826 return this.noFollow
;
3830 * Set hyperlink location.
3832 * @param {string|null} href Hyperlink location, null to remove
3834 * @return {OO.ui.Widget} The widget, for chaining
3836 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3837 href
= typeof href
=== 'string' ? href
: null;
3838 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3842 if ( href
!== this.href
) {
3851 * Update the `href` attribute, in case of changes to href or
3856 * @return {OO.ui.Widget} The widget, for chaining
3858 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3859 if ( this.href
!== null && !this.isDisabled() ) {
3860 this.$button
.attr( 'href', this.href
);
3862 this.$button
.removeAttr( 'href' );
3869 * Handle disable events.
3872 * @param {boolean} disabled Element is disabled
3874 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3879 * Set hyperlink target.
3881 * @param {string|null} target Hyperlink target, null to remove
3882 * @return {OO.ui.Widget} The widget, for chaining
3884 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3885 target
= typeof target
=== 'string' ? target
: null;
3887 if ( target
!== this.target
) {
3888 this.target
= target
;
3889 if ( target
!== null ) {
3890 this.$button
.attr( 'target', target
);
3892 this.$button
.removeAttr( 'target' );
3900 * Set search engine traversal hint.
3902 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3903 * @return {OO.ui.Widget} The widget, for chaining
3905 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3906 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3908 if ( noFollow
!== this.noFollow
) {
3909 this.noFollow
= noFollow
;
3911 this.$button
.attr( 'rel', 'nofollow' );
3913 this.$button
.removeAttr( 'rel' );
3920 // Override method visibility hints from ButtonElement
3931 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3932 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3933 * removed, and cleared from the group.
3936 * // Example: A ButtonGroupWidget with two buttons
3937 * var button1 = new OO.ui.PopupButtonWidget( {
3938 * label: 'Select a category',
3941 * $content: $( '<p>List of categories...</p>' ),
3946 * var button2 = new OO.ui.ButtonWidget( {
3949 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3950 * items: [button1, button2]
3952 * $( 'body' ).append( buttonGroup.$element );
3955 * @extends OO.ui.Widget
3956 * @mixins OO.ui.mixin.GroupElement
3959 * @param {Object} [config] Configuration options
3960 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3962 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3963 // Configuration initialization
3964 config
= config
|| {};
3966 // Parent constructor
3967 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3969 // Mixin constructors
3970 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3973 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3974 if ( Array
.isArray( config
.items
) ) {
3975 this.addItems( config
.items
);
3981 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3982 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3984 /* Static Properties */
3990 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
3998 * @return {OO.ui.Widget} The widget, for chaining
4000 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
4001 if ( !this.isDisabled() ) {
4002 if ( this.items
[ 0 ] ) {
4003 this.items
[ 0 ].focus();
4012 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
4017 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
4018 * which creates a label that identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
4019 * for a list of icons included in the library.
4022 * // An icon widget with a label
4023 * var myIcon = new OO.ui.IconWidget( {
4027 * // Create a label.
4028 * var iconLabel = new OO.ui.LabelWidget( {
4031 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
4033 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
4036 * @extends OO.ui.Widget
4037 * @mixins OO.ui.mixin.IconElement
4038 * @mixins OO.ui.mixin.TitledElement
4039 * @mixins OO.ui.mixin.LabelElement
4040 * @mixins OO.ui.mixin.FlaggedElement
4043 * @param {Object} [config] Configuration options
4045 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
4046 // Configuration initialization
4047 config
= config
|| {};
4049 // Parent constructor
4050 OO
.ui
.IconWidget
.parent
.call( this, config
);
4052 // Mixin constructors
4053 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
4054 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
4055 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
, invisibleLabel
: true } ) );
4056 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
4059 this.$element
.addClass( 'oo-ui-iconWidget' );
4060 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4061 // nested in other widgets, because this widget used to not mix in LabelElement.
4062 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4067 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
4068 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
4069 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
4070 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.LabelElement
);
4071 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
4073 /* Static Properties */
4079 OO
.ui
.IconWidget
.static.tagName
= 'span';
4082 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
4083 * attention to the status of an item or to clarify the function within a control. For a list of
4084 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
4087 * // Example of an indicator widget
4088 * var indicator1 = new OO.ui.IndicatorWidget( {
4089 * indicator: 'required'
4092 * // Create a fieldset layout to add a label
4093 * var fieldset = new OO.ui.FieldsetLayout();
4094 * fieldset.addItems( [
4095 * new OO.ui.FieldLayout( indicator1, { label: 'A required indicator:' } )
4097 * $( 'body' ).append( fieldset.$element );
4099 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4102 * @extends OO.ui.Widget
4103 * @mixins OO.ui.mixin.IndicatorElement
4104 * @mixins OO.ui.mixin.TitledElement
4105 * @mixins OO.ui.mixin.LabelElement
4108 * @param {Object} [config] Configuration options
4110 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4111 // Configuration initialization
4112 config
= config
|| {};
4114 // Parent constructor
4115 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4117 // Mixin constructors
4118 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
4119 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
4120 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
, invisibleLabel
: true } ) );
4123 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4124 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4125 // nested in other widgets, because this widget used to not mix in LabelElement.
4126 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4131 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4132 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4133 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4134 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.LabelElement
);
4136 /* Static Properties */
4142 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4145 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4146 * be configured with a `label` option that is set to a string, a label node, or a function:
4148 * - String: a plaintext string
4149 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4150 * label that includes a link or special styling, such as a gray color or additional graphical elements.
4151 * - Function: a function that will produce a string in the future. Functions are used
4152 * in cases where the value of the label is not currently defined.
4154 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
4155 * will come into focus when the label is clicked.
4158 * // Examples of LabelWidgets
4159 * var label1 = new OO.ui.LabelWidget( {
4160 * label: 'plaintext label'
4162 * var label2 = new OO.ui.LabelWidget( {
4163 * label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
4165 * // Create a fieldset layout with fields for each example
4166 * var fieldset = new OO.ui.FieldsetLayout();
4167 * fieldset.addItems( [
4168 * new OO.ui.FieldLayout( label1 ),
4169 * new OO.ui.FieldLayout( label2 )
4171 * $( 'body' ).append( fieldset.$element );
4174 * @extends OO.ui.Widget
4175 * @mixins OO.ui.mixin.LabelElement
4176 * @mixins OO.ui.mixin.TitledElement
4179 * @param {Object} [config] Configuration options
4180 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4181 * Clicking the label will focus the specified input field.
4183 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4184 // Configuration initialization
4185 config
= config
|| {};
4187 // Parent constructor
4188 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4190 // Mixin constructors
4191 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
4192 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4195 this.input
= config
.input
;
4199 if ( this.input
.getInputId() ) {
4200 this.$element
.attr( 'for', this.input
.getInputId() );
4202 this.$label
.on( 'click', function () {
4203 this.input
.simulateLabelClick();
4207 this.$element
.addClass( 'oo-ui-labelWidget' );
4212 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4213 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4214 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4216 /* Static Properties */
4222 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4225 * PendingElement is a mixin that is used to create elements that notify users that something is happening
4226 * and that they should wait before proceeding. The pending state is visually represented with a pending
4227 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
4228 * field of a {@link OO.ui.TextInputWidget text input widget}.
4230 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
4231 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
4232 * in process dialogs.
4235 * function MessageDialog( config ) {
4236 * MessageDialog.parent.call( this, config );
4238 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4240 * MessageDialog.static.name = 'myMessageDialog';
4241 * MessageDialog.static.actions = [
4242 * { action: 'save', label: 'Done', flags: 'primary' },
4243 * { label: 'Cancel', flags: 'safe' }
4246 * MessageDialog.prototype.initialize = function () {
4247 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4248 * this.content = new OO.ui.PanelLayout( { padded: true } );
4249 * 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>' );
4250 * this.$body.append( this.content.$element );
4252 * MessageDialog.prototype.getBodyHeight = function () {
4255 * MessageDialog.prototype.getActionProcess = function ( action ) {
4256 * var dialog = this;
4257 * if ( action === 'save' ) {
4258 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4259 * return new OO.ui.Process()
4261 * .next( function () {
4262 * dialog.getActions().get({actions: 'save'})[0].popPending();
4265 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4268 * var windowManager = new OO.ui.WindowManager();
4269 * $( 'body' ).append( windowManager.$element );
4271 * var dialog = new MessageDialog();
4272 * windowManager.addWindows( [ dialog ] );
4273 * windowManager.openWindow( dialog );
4279 * @param {Object} [config] Configuration options
4280 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4282 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4283 // Configuration initialization
4284 config
= config
|| {};
4288 this.$pending
= null;
4291 this.setPendingElement( config
.$pending
|| this.$element
);
4296 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4301 * Set the pending element (and clean up any existing one).
4303 * @param {jQuery} $pending The element to set to pending.
4305 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4306 if ( this.$pending
) {
4307 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4310 this.$pending
= $pending
;
4311 if ( this.pending
> 0 ) {
4312 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4317 * Check if an element is pending.
4319 * @return {boolean} Element is pending
4321 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4322 return !!this.pending
;
4326 * Increase the pending counter. The pending state will remain active until the counter is zero
4327 * (i.e., the number of calls to #pushPending and #popPending is the same).
4330 * @return {OO.ui.Element} The element, for chaining
4332 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4333 if ( this.pending
=== 0 ) {
4334 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4335 this.updateThemeClasses();
4343 * Decrease the pending counter. The pending state will remain active until the counter is zero
4344 * (i.e., the number of calls to #pushPending and #popPending is the same).
4347 * @return {OO.ui.Element} The element, for chaining
4349 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4350 if ( this.pending
=== 1 ) {
4351 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4352 this.updateThemeClasses();
4354 this.pending
= Math
.max( 0, this.pending
- 1 );
4360 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4361 * in the document (for example, in an OO.ui.Window's $overlay).
4363 * The elements's position is automatically calculated and maintained when window is resized or the
4364 * page is scrolled. If you reposition the container manually, you have to call #position to make
4365 * sure the element is still placed correctly.
4367 * As positioning is only possible when both the element and the container are attached to the DOM
4368 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4369 * the #toggle method to display a floating popup, for example.
4375 * @param {Object} [config] Configuration options
4376 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4377 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4378 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4379 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4380 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4381 * 'top': Align the top edge with $floatableContainer's top edge
4382 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4383 * 'center': Vertically align the center with $floatableContainer's center
4384 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4385 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4386 * 'after': Directly after $floatableContainer, aligning f's start edge with fC's end edge
4387 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4388 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4389 * 'center': Horizontally align the center with $floatableContainer's center
4390 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4393 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4394 // Configuration initialization
4395 config
= config
|| {};
4398 this.$floatable
= null;
4399 this.$floatableContainer
= null;
4400 this.$floatableWindow
= null;
4401 this.$floatableClosestScrollable
= null;
4402 this.floatableOutOfView
= false;
4403 this.onFloatableScrollHandler
= this.position
.bind( this );
4404 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4407 this.setFloatableContainer( config
.$floatableContainer
);
4408 this.setFloatableElement( config
.$floatable
|| this.$element
);
4409 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4410 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4411 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ? true : !!config
.hideWhenOutOfView
;
4417 * Set floatable element.
4419 * If an element is already set, it will be cleaned up before setting up the new element.
4421 * @param {jQuery} $floatable Element to make floatable
4423 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4424 if ( this.$floatable
) {
4425 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4426 this.$floatable
.css( { left
: '', top
: '' } );
4429 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4434 * Set floatable container.
4436 * The element will be positioned relative to the specified container.
4438 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4440 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4441 this.$floatableContainer
= $floatableContainer
;
4442 if ( this.$floatable
) {
4448 * Change how the element is positioned vertically.
4450 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4452 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4453 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4454 throw new Error( 'Invalid value for vertical position: ' + position
);
4456 if ( this.verticalPosition
!== position
) {
4457 this.verticalPosition
= position
;
4458 if ( this.$floatable
) {
4465 * Change how the element is positioned horizontally.
4467 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4469 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4470 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4471 throw new Error( 'Invalid value for horizontal position: ' + position
);
4473 if ( this.horizontalPosition
!== position
) {
4474 this.horizontalPosition
= position
;
4475 if ( this.$floatable
) {
4482 * Toggle positioning.
4484 * Do not turn positioning on until after the element is attached to the DOM and visible.
4486 * @param {boolean} [positioning] Enable positioning, omit to toggle
4488 * @return {OO.ui.Element} The element, for chaining
4490 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4491 var closestScrollableOfContainer
;
4493 if ( !this.$floatable
|| !this.$floatableContainer
) {
4497 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4499 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4500 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4501 this.warnedUnattached
= true;
4504 if ( this.positioning
!== positioning
) {
4505 this.positioning
= positioning
;
4507 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
4508 // If the scrollable is the root, we have to listen to scroll events
4509 // on the window because of browser inconsistencies.
4510 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4511 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
4514 if ( positioning
) {
4515 this.$floatableWindow
= $( this.getElementWindow() );
4516 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4518 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4519 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4521 // Initial position after visible
4524 if ( this.$floatableWindow
) {
4525 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4526 this.$floatableWindow
= null;
4529 if ( this.$floatableClosestScrollable
) {
4530 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4531 this.$floatableClosestScrollable
= null;
4534 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4542 * Check whether the bottom edge of the given element is within the viewport of the given container.
4545 * @param {jQuery} $element
4546 * @param {jQuery} $container
4549 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4550 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
, rightEdgeInBounds
,
4551 startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4552 direction
= $element
.css( 'direction' );
4554 elemRect
= $element
[ 0 ].getBoundingClientRect();
4555 if ( $container
[ 0 ] === window
) {
4556 viewportSpacing
= OO
.ui
.getViewportSpacing();
4560 right
: document
.documentElement
.clientWidth
,
4561 bottom
: document
.documentElement
.clientHeight
4563 contRect
.top
+= viewportSpacing
.top
;
4564 contRect
.left
+= viewportSpacing
.left
;
4565 contRect
.right
-= viewportSpacing
.right
;
4566 contRect
.bottom
-= viewportSpacing
.bottom
;
4568 contRect
= $container
[ 0 ].getBoundingClientRect();
4571 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4572 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4573 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4574 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4575 if ( direction
=== 'rtl' ) {
4576 startEdgeInBounds
= rightEdgeInBounds
;
4577 endEdgeInBounds
= leftEdgeInBounds
;
4579 startEdgeInBounds
= leftEdgeInBounds
;
4580 endEdgeInBounds
= rightEdgeInBounds
;
4583 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4586 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4589 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4592 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4596 // The other positioning values are all about being inside the container,
4597 // so in those cases all we care about is that any part of the container is visible.
4598 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4599 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4603 * Check if the floatable is hidden to the user because it was offscreen.
4605 * @return {boolean} Floatable is out of view
4607 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4608 return this.floatableOutOfView
;
4612 * Position the floatable below its container.
4614 * This should only be done when both of them are attached to the DOM and visible.
4617 * @return {OO.ui.Element} The element, for chaining
4619 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4620 if ( !this.positioning
) {
4625 // To continue, some things need to be true:
4626 // The element must actually be in the DOM
4627 this.isElementAttached() && (
4628 // The closest scrollable is the current window
4629 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4630 // OR is an element in the element's DOM
4631 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4634 // Abort early if important parts of the widget are no longer attached to the DOM
4638 this.floatableOutOfView
= this.hideWhenOutOfView
&& !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4639 if ( this.floatableOutOfView
) {
4640 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4643 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4646 this.$floatable
.css( this.computePosition() );
4648 // We updated the position, so re-evaluate the clipping state.
4649 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4650 // will not notice the need to update itself.)
4651 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4652 // it not listen to the right events in the right places?
4661 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4662 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4663 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4665 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4667 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4668 var isBody
, scrollableX
, scrollableY
, containerPos
,
4669 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4670 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4671 direction
= this.$floatableContainer
.css( 'direction' ),
4672 $offsetParent
= this.$floatable
.offsetParent();
4674 if ( $offsetParent
.is( 'html' ) ) {
4675 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4676 // <html> element, but they do work on the <body>
4677 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4679 isBody
= $offsetParent
.is( 'body' );
4680 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' || $offsetParent
.css( 'overflow-x' ) === 'auto';
4681 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' || $offsetParent
.css( 'overflow-y' ) === 'auto';
4683 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4684 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4685 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
4686 // or if it isn't scrollable
4687 scrollTop
= scrollableY
&& !isBody
? $offsetParent
.scrollTop() : 0;
4688 scrollLeft
= scrollableX
&& !isBody
? OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4690 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4691 // if the <body> has a margin
4692 containerPos
= isBody
?
4693 this.$floatableContainer
.offset() :
4694 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4695 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4696 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4697 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4698 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4700 if ( this.verticalPosition
=== 'below' ) {
4701 newPos
.top
= containerPos
.bottom
;
4702 } else if ( this.verticalPosition
=== 'above' ) {
4703 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4704 } else if ( this.verticalPosition
=== 'top' ) {
4705 newPos
.top
= containerPos
.top
;
4706 } else if ( this.verticalPosition
=== 'bottom' ) {
4707 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4708 } else if ( this.verticalPosition
=== 'center' ) {
4709 newPos
.top
= containerPos
.top
+
4710 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4713 if ( this.horizontalPosition
=== 'before' ) {
4714 newPos
.end
= containerPos
.start
;
4715 } else if ( this.horizontalPosition
=== 'after' ) {
4716 newPos
.start
= containerPos
.end
;
4717 } else if ( this.horizontalPosition
=== 'start' ) {
4718 newPos
.start
= containerPos
.start
;
4719 } else if ( this.horizontalPosition
=== 'end' ) {
4720 newPos
.end
= containerPos
.end
;
4721 } else if ( this.horizontalPosition
=== 'center' ) {
4722 newPos
.left
= containerPos
.left
+
4723 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4726 if ( newPos
.start
!== undefined ) {
4727 if ( direction
=== 'rtl' ) {
4728 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.start
;
4730 newPos
.left
= newPos
.start
;
4732 delete newPos
.start
;
4734 if ( newPos
.end
!== undefined ) {
4735 if ( direction
=== 'rtl' ) {
4736 newPos
.left
= newPos
.end
;
4738 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.end
;
4743 // Account for scroll position
4744 if ( newPos
.top
!== '' ) {
4745 newPos
.top
+= scrollTop
;
4747 if ( newPos
.bottom
!== '' ) {
4748 newPos
.bottom
-= scrollTop
;
4750 if ( newPos
.left
!== '' ) {
4751 newPos
.left
+= scrollLeft
;
4753 if ( newPos
.right
!== '' ) {
4754 newPos
.right
-= scrollLeft
;
4757 // Account for scrollbar gutter
4758 if ( newPos
.bottom
!== '' ) {
4759 newPos
.bottom
-= horizScrollbarHeight
;
4761 if ( direction
=== 'rtl' ) {
4762 if ( newPos
.left
!== '' ) {
4763 newPos
.left
-= vertScrollbarWidth
;
4766 if ( newPos
.right
!== '' ) {
4767 newPos
.right
-= vertScrollbarWidth
;
4775 * Element that can be automatically clipped to visible boundaries.
4777 * Whenever the element's natural height changes, you have to call
4778 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4779 * clipping correctly.
4781 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4782 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4783 * then #$clippable will be given a fixed reduced height and/or width and will be made
4784 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4785 * but you can build a static footer by setting #$clippableContainer to an element that contains
4786 * #$clippable and the footer.
4792 * @param {Object} [config] Configuration options
4793 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4794 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4795 * omit to use #$clippable
4797 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4798 // Configuration initialization
4799 config
= config
|| {};
4802 this.$clippable
= null;
4803 this.$clippableContainer
= null;
4804 this.clipping
= false;
4805 this.clippedHorizontally
= false;
4806 this.clippedVertically
= false;
4807 this.$clippableScrollableContainer
= null;
4808 this.$clippableScroller
= null;
4809 this.$clippableWindow
= null;
4810 this.idealWidth
= null;
4811 this.idealHeight
= null;
4812 this.onClippableScrollHandler
= this.clip
.bind( this );
4813 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4816 if ( config
.$clippableContainer
) {
4817 this.setClippableContainer( config
.$clippableContainer
);
4819 this.setClippableElement( config
.$clippable
|| this.$element
);
4825 * Set clippable element.
4827 * If an element is already set, it will be cleaned up before setting up the new element.
4829 * @param {jQuery} $clippable Element to make clippable
4831 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4832 if ( this.$clippable
) {
4833 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4834 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4835 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4838 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4843 * Set clippable container.
4845 * This is the container that will be measured when deciding whether to clip. When clipping,
4846 * #$clippable will be resized in order to keep the clippable container fully visible.
4848 * If the clippable container is unset, #$clippable will be used.
4850 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4852 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4853 this.$clippableContainer
= $clippableContainer
;
4854 if ( this.$clippable
) {
4862 * Do not turn clipping on until after the element is attached to the DOM and visible.
4864 * @param {boolean} [clipping] Enable clipping, omit to toggle
4866 * @return {OO.ui.Element} The element, for chaining
4868 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4869 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4871 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4872 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4873 this.warnedUnattached
= true;
4876 if ( this.clipping
!== clipping
) {
4877 this.clipping
= clipping
;
4879 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4880 // If the clippable container is the root, we have to listen to scroll events and check
4881 // jQuery.scrollTop on the window because of browser inconsistencies
4882 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4883 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4884 this.$clippableScrollableContainer
;
4885 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4886 this.$clippableWindow
= $( this.getElementWindow() )
4887 .on( 'resize', this.onClippableWindowResizeHandler
);
4888 // Initial clip after visible
4891 this.$clippable
.css( {
4899 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4901 this.$clippableScrollableContainer
= null;
4902 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4903 this.$clippableScroller
= null;
4904 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4905 this.$clippableWindow
= null;
4913 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4915 * @return {boolean} Element will be clipped to the visible area
4917 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4918 return this.clipping
;
4922 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4924 * @return {boolean} Part of the element is being clipped
4926 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4927 return this.clippedHorizontally
|| this.clippedVertically
;
4931 * Check if the right of the element is being clipped by the nearest scrollable container.
4933 * @return {boolean} Part of the element is being clipped
4935 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4936 return this.clippedHorizontally
;
4940 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4942 * @return {boolean} Part of the element is being clipped
4944 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4945 return this.clippedVertically
;
4949 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4951 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4952 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4954 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4955 this.idealWidth
= width
;
4956 this.idealHeight
= height
;
4958 if ( !this.clipping
) {
4959 // Update dimensions
4960 this.$clippable
.css( { width
: width
, height
: height
} );
4962 // While clipping, idealWidth and idealHeight are not considered
4966 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4967 * ClippableElement will clip the opposite side when reducing element's width.
4969 * Classes that mix in ClippableElement should override this to return 'right' if their
4970 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
4971 * If your class also mixes in FloatableElement, this is handled automatically.
4973 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4974 * always in pixels, even if they were unset or set to 'auto'.)
4976 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
4978 * @return {string} 'left' or 'right'
4980 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
4981 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
4988 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4989 * ClippableElement will clip the opposite side when reducing element's width.
4991 * Classes that mix in ClippableElement should override this to return 'bottom' if their
4992 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
4993 * If your class also mixes in FloatableElement, this is handled automatically.
4995 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4996 * always in pixels, even if they were unset or set to 'auto'.)
4998 * When in doubt, 'top' is a sane fallback.
5000 * @return {string} 'top' or 'bottom'
5002 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
5003 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
5010 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
5011 * when the element's natural height changes.
5013 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5014 * overlapped by, the visible area of the nearest scrollable container.
5016 * Because calling clip() when the natural height changes isn't always possible, we also set
5017 * max-height when the element isn't being clipped. This means that if the element tries to grow
5018 * beyond the edge, something reasonable will happen before clip() is called.
5021 * @return {OO.ui.Element} The element, for chaining
5023 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
5024 var extraHeight
, extraWidth
, viewportSpacing
,
5025 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
5026 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
5027 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
5028 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
5029 // Extra tolerance so that the sloppy code below doesn't result in results that are off
5030 // by one or two pixels. (And also so that we have space to display drop shadows.)
5031 // Chosen by fair dice roll.
5034 if ( !this.clipping
) {
5035 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
5039 function rectIntersection( a
, b
) {
5041 out
.top
= Math
.max( a
.top
, b
.top
);
5042 out
.left
= Math
.max( a
.left
, b
.left
);
5043 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
5044 out
.right
= Math
.min( a
.right
, b
.right
);
5048 viewportSpacing
= OO
.ui
.getViewportSpacing();
5050 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
5051 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
5052 // Dimensions of the browser window, rather than the element!
5056 right
: document
.documentElement
.clientWidth
,
5057 bottom
: document
.documentElement
.clientHeight
5059 viewportRect
.top
+= viewportSpacing
.top
;
5060 viewportRect
.left
+= viewportSpacing
.left
;
5061 viewportRect
.right
-= viewportSpacing
.right
;
5062 viewportRect
.bottom
-= viewportSpacing
.bottom
;
5064 $viewport
= this.$clippableScrollableContainer
;
5065 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
5066 // Convert into a plain object
5067 viewportRect
= $.extend( {}, viewportRect
);
5070 // Account for scrollbar gutter
5071 direction
= $viewport
.css( 'direction' );
5072 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
5073 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
5074 viewportRect
.bottom
-= horizScrollbarHeight
;
5075 if ( direction
=== 'rtl' ) {
5076 viewportRect
.left
+= vertScrollbarWidth
;
5078 viewportRect
.right
-= vertScrollbarWidth
;
5081 // Add arbitrary tolerance
5082 viewportRect
.top
+= buffer
;
5083 viewportRect
.left
+= buffer
;
5084 viewportRect
.right
-= buffer
;
5085 viewportRect
.bottom
-= buffer
;
5087 $item
= this.$clippableContainer
|| this.$clippable
;
5089 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
5090 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
5092 itemRect
= $item
[ 0 ].getBoundingClientRect();
5093 // Convert into a plain object
5094 itemRect
= $.extend( {}, itemRect
);
5096 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
5097 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
5098 if ( this.getHorizontalAnchorEdge() === 'right' ) {
5099 itemRect
.left
= viewportRect
.left
;
5101 itemRect
.right
= viewportRect
.right
;
5103 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5104 itemRect
.top
= viewportRect
.top
;
5106 itemRect
.bottom
= viewportRect
.bottom
;
5109 availableRect
= rectIntersection( viewportRect
, itemRect
);
5111 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
5112 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5113 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5114 desiredWidth
= Math
.min( desiredWidth
,
5115 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5116 desiredHeight
= Math
.min( desiredHeight
,
5117 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5118 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5119 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5120 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5121 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5122 clipWidth
= allotedWidth
< naturalWidth
;
5123 clipHeight
= allotedHeight
< naturalHeight
;
5126 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5127 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5128 this.$clippable
.css( 'overflowX', 'scroll' );
5129 // eslint-disable-next-line no-void
5130 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5131 this.$clippable
.css( {
5132 width
: Math
.max( 0, allotedWidth
),
5136 this.$clippable
.css( {
5138 width
: this.idealWidth
|| '',
5139 maxWidth
: Math
.max( 0, allotedWidth
)
5143 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5144 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5145 this.$clippable
.css( 'overflowY', 'scroll' );
5146 // eslint-disable-next-line no-void
5147 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5148 this.$clippable
.css( {
5149 height
: Math
.max( 0, allotedHeight
),
5153 this.$clippable
.css( {
5155 height
: this.idealHeight
|| '',
5156 maxHeight
: Math
.max( 0, allotedHeight
)
5160 // If we stopped clipping in at least one of the dimensions
5161 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5162 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5165 this.clippedHorizontally
= clipWidth
;
5166 this.clippedVertically
= clipHeight
;
5172 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5173 * By default, each popup has an anchor that points toward its origin.
5174 * Please see the [OOUI documentation on MediaWiki.org] [1] for more information and examples.
5176 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5179 * // A popup widget.
5180 * var popup = new OO.ui.PopupWidget( {
5181 * $content: $( '<p>Hi there!</p>' ),
5186 * $( 'body' ).append( popup.$element );
5187 * // To display the popup, toggle the visibility to 'true'.
5188 * popup.toggle( true );
5190 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5193 * @extends OO.ui.Widget
5194 * @mixins OO.ui.mixin.LabelElement
5195 * @mixins OO.ui.mixin.ClippableElement
5196 * @mixins OO.ui.mixin.FloatableElement
5199 * @param {Object} [config] Configuration options
5200 * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
5201 * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
5202 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5203 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5204 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5205 * of $floatableContainer
5206 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5207 * of $floatableContainer
5208 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5209 * endwards (right/left) to the vertical center of $floatableContainer
5210 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5211 * startwards (left/right) to the vertical center of $floatableContainer
5212 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5213 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
5214 * as possible while still keeping the anchor within the popup;
5215 * if position is before/after, move the popup as far downwards as possible.
5216 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
5217 * as possible while still keeping the anchor within the popup;
5218 * if position in before/after, move the popup as far upwards as possible.
5219 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center
5220 * of the popup with the center of $floatableContainer.
5221 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5222 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5223 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5224 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5225 * desired direction to display the popup without clipping
5226 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5227 * See the [OOUI docs on MediaWiki][3] for an example.
5228 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5229 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
5230 * @cfg {jQuery} [$content] Content to append to the popup's body
5231 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5232 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5233 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5234 * This config option is only relevant if #autoClose is set to `true`. See the [OOUI documentation on MediaWiki][2]
5236 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5237 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5239 * @cfg {boolean} [padded=false] Add padding to the popup's body
5241 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5242 // Configuration initialization
5243 config
= config
|| {};
5245 // Parent constructor
5246 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5248 // Properties (must be set before ClippableElement constructor call)
5249 this.$body
= $( '<div>' );
5250 this.$popup
= $( '<div>' );
5252 // Mixin constructors
5253 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5254 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
5255 $clippable
: this.$body
,
5256 $clippableContainer
: this.$popup
5258 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5261 this.$anchor
= $( '<div>' );
5262 // If undefined, will be computed lazily in computePosition()
5263 this.$container
= config
.$container
;
5264 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5265 this.autoClose
= !!config
.autoClose
;
5266 this.transitionTimeout
= null;
5267 this.anchored
= false;
5268 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5269 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5272 this.setSize( config
.width
, config
.height
);
5273 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5274 this.setAlignment( config
.align
|| 'center' );
5275 this.setPosition( config
.position
|| 'below' );
5276 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5277 this.setAutoCloseIgnore( config
.$autoCloseIgnore
);
5278 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5279 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5281 .addClass( 'oo-ui-popupWidget-popup' )
5282 .append( this.$body
);
5284 .addClass( 'oo-ui-popupWidget' )
5285 .append( this.$popup
, this.$anchor
);
5286 // Move content, which was added to #$element by OO.ui.Widget, to the body
5287 // FIXME This is gross, we should use '$body' or something for the config
5288 if ( config
.$content
instanceof $ ) {
5289 this.$body
.append( config
.$content
);
5292 if ( config
.padded
) {
5293 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5296 if ( config
.head
) {
5297 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
5298 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
5299 this.$head
= $( '<div>' )
5300 .addClass( 'oo-ui-popupWidget-head' )
5301 .append( this.$label
, this.closeButton
.$element
);
5302 this.$popup
.prepend( this.$head
);
5305 if ( config
.$footer
) {
5306 this.$footer
= $( '<div>' )
5307 .addClass( 'oo-ui-popupWidget-footer' )
5308 .append( config
.$footer
);
5309 this.$popup
.append( this.$footer
);
5312 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5313 // that reference properties not initialized at that time of parent class construction
5314 // TODO: Find a better way to handle post-constructor setup
5315 this.visible
= false;
5316 this.$element
.addClass( 'oo-ui-element-hidden' );
5321 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5322 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5323 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5324 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5331 * The popup is ready: it is visible and has been positioned and clipped.
5337 * Handles document mouse down events.
5340 * @param {MouseEvent} e Mouse down event
5342 OO
.ui
.PopupWidget
.prototype.onDocumentMouseDown = function ( e
) {
5345 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5347 this.toggle( false );
5351 // Deprecated alias since 0.28.3
5352 OO
.ui
.PopupWidget
.prototype.onMouseDown = function () {
5353 OO
.ui
.warnDeprecation( 'onMouseDown is deprecated, use onDocumentMouseDown instead' );
5354 this.onDocumentMouseDown
.apply( this, arguments
);
5358 * Bind document mouse down listener.
5362 OO
.ui
.PopupWidget
.prototype.bindDocumentMouseDownListener = function () {
5363 // Capture clicks outside popup
5364 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5365 // We add 'click' event because iOS safari needs to respond to this event.
5366 // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
5367 // then it will trigger when scrolling. While iOS Safari has some reported behavior
5368 // of occasionally not emitting 'click' properly, that event seems to be the standard
5369 // that it should be emitting, so we add it to this and will operate the event handler
5370 // on whichever of these events was triggered first
5371 this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5374 // Deprecated alias since 0.28.3
5375 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
5376 OO
.ui
.warnDeprecation( 'bindMouseDownListener is deprecated, use bindDocumentMouseDownListener instead' );
5377 this.bindDocumentMouseDownListener
.apply( this, arguments
);
5381 * Handles close button click events.
5385 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5386 if ( this.isVisible() ) {
5387 this.toggle( false );
5392 * Unbind document mouse down listener.
5396 OO
.ui
.PopupWidget
.prototype.unbindDocumentMouseDownListener = function () {
5397 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5398 this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5401 // Deprecated alias since 0.28.3
5402 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
5403 OO
.ui
.warnDeprecation( 'unbindMouseDownListener is deprecated, use unbindDocumentMouseDownListener instead' );
5404 this.unbindDocumentMouseDownListener
.apply( this, arguments
);
5408 * Handles document key down events.
5411 * @param {KeyboardEvent} e Key down event
5413 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5415 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5418 this.toggle( false );
5420 e
.stopPropagation();
5425 * Bind document key down listener.
5429 OO
.ui
.PopupWidget
.prototype.bindDocumentKeyDownListener = function () {
5430 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5433 // Deprecated alias since 0.28.3
5434 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
5435 OO
.ui
.warnDeprecation( 'bindKeyDownListener is deprecated, use bindDocumentKeyDownListener instead' );
5436 this.bindDocumentKeyDownListener
.apply( this, arguments
);
5440 * Unbind document key down listener.
5444 OO
.ui
.PopupWidget
.prototype.unbindDocumentKeyDownListener = function () {
5445 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5448 // Deprecated alias since 0.28.3
5449 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
5450 OO
.ui
.warnDeprecation( 'unbindKeyDownListener is deprecated, use unbindDocumentKeyDownListener instead' );
5451 this.unbindDocumentKeyDownListener
.apply( this, arguments
);
5455 * Show, hide, or toggle the visibility of the anchor.
5457 * @param {boolean} [show] Show anchor, omit to toggle
5459 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5460 show
= show
=== undefined ? !this.anchored
: !!show
;
5462 if ( this.anchored
!== show
) {
5464 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5465 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5467 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5468 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5470 this.anchored
= show
;
5475 * Change which edge the anchor appears on.
5477 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5479 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5480 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5481 throw new Error( 'Invalid value for edge: ' + edge
);
5483 if ( this.anchorEdge
!== null ) {
5484 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5486 this.anchorEdge
= edge
;
5487 if ( this.anchored
) {
5488 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5493 * Check if the anchor is visible.
5495 * @return {boolean} Anchor is visible
5497 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5498 return this.anchored
;
5502 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5503 * `.toggle( true )` after its #$element is attached to the DOM.
5505 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5506 * it in the right place and with the right dimensions only work correctly while it is attached.
5507 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5508 * strictly enforced, so currently it only generates a warning in the browser console.
5513 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5514 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5515 show
= show
=== undefined ? !this.isVisible() : !!show
;
5517 change
= show
!== this.isVisible();
5519 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5520 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5521 this.warnedUnattached
= true;
5523 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5524 // Fall back to the parent node if the floatableContainer is not set
5525 this.setFloatableContainer( this.$element
.parent() );
5528 if ( change
&& show
&& this.autoFlip
) {
5529 // Reset auto-flipping before showing the popup again. It's possible we no longer need to flip
5530 // (e.g. if the user scrolled).
5531 this.isAutoFlipped
= false;
5535 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5538 this.togglePositioning( show
&& !!this.$floatableContainer
);
5541 if ( this.autoClose
) {
5542 this.bindDocumentMouseDownListener();
5543 this.bindDocumentKeyDownListener();
5545 this.updateDimensions();
5546 this.toggleClipping( true );
5548 if ( this.autoFlip
) {
5549 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5550 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5551 // If opening the popup in the normal direction causes it to be clipped, open
5552 // in the opposite one instead
5553 normalHeight
= this.$element
.height();
5554 this.isAutoFlipped
= !this.isAutoFlipped
;
5556 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5557 // If that also causes it to be clipped, open in whichever direction
5558 // we have more space
5559 oppositeHeight
= this.$element
.height();
5560 if ( oppositeHeight
< normalHeight
) {
5561 this.isAutoFlipped
= !this.isAutoFlipped
;
5567 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5568 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5569 // If opening the popup in the normal direction causes it to be clipped, open
5570 // in the opposite one instead
5571 normalWidth
= this.$element
.width();
5572 this.isAutoFlipped
= !this.isAutoFlipped
;
5573 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5574 // which causes positioning to be off. Toggle clipping back and fort to work around.
5575 this.toggleClipping( false );
5577 this.toggleClipping( true );
5578 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5579 // If that also causes it to be clipped, open in whichever direction
5580 // we have more space
5581 oppositeWidth
= this.$element
.width();
5582 if ( oppositeWidth
< normalWidth
) {
5583 this.isAutoFlipped
= !this.isAutoFlipped
;
5584 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5585 // which causes positioning to be off. Toggle clipping back and fort to work around.
5586 this.toggleClipping( false );
5588 this.toggleClipping( true );
5595 this.emit( 'ready' );
5597 this.toggleClipping( false );
5598 if ( this.autoClose
) {
5599 this.unbindDocumentMouseDownListener();
5600 this.unbindDocumentKeyDownListener();
5609 * Set the size of the popup.
5611 * Changing the size may also change the popup's position depending on the alignment.
5613 * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
5614 * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
5615 * @param {boolean} [transition=false] Use a smooth transition
5618 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5619 this.width
= width
!== undefined ? width
: 320;
5620 this.height
= height
!== undefined ? height
: null;
5621 if ( this.isVisible() ) {
5622 this.updateDimensions( transition
);
5627 * Update the size and position.
5629 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5630 * be called automatically.
5632 * @param {boolean} [transition=false] Use a smooth transition
5635 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5638 // Prevent transition from being interrupted
5639 clearTimeout( this.transitionTimeout
);
5641 // Enable transition
5642 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5648 // Prevent transitioning after transition is complete
5649 this.transitionTimeout
= setTimeout( function () {
5650 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5653 // Prevent transitioning immediately
5654 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5661 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5662 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
, anchorPos
,
5663 anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
, floatablePos
,
5664 offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5666 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5667 popupPositionOppositeMap
= {
5675 'force-left': 'backwards',
5676 'force-right': 'forwards'
5679 'force-left': 'forwards',
5680 'force-right': 'backwards'
5692 backwards
: this.anchored
? 'before' : 'end'
5700 if ( !this.$container
) {
5701 // Lazy-initialize $container if not specified in constructor
5702 this.$container
= $( this.getClosestScrollableElementContainer() );
5704 direction
= this.$container
.css( 'direction' );
5706 // Set height and width before we do anything else, since it might cause our measurements
5707 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5709 width
: this.width
!== null ? this.width
: 'auto',
5710 height
: this.height
!== null ? this.height
: 'auto'
5713 align
= alignMap
[ direction
][ this.align
] || this.align
;
5714 popupPosition
= this.popupPosition
;
5715 if ( this.isAutoFlipped
) {
5716 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5719 // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
5720 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5721 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5722 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5723 near
= vertical
? 'top' : 'left';
5724 far
= vertical
? 'bottom' : 'right';
5725 sizeProp
= vertical
? 'Height' : 'Width';
5726 popupSize
= vertical
? ( this.height
|| this.$popup
.height() ) : ( this.width
|| this.$popup
.width() );
5728 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5729 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5730 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5733 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5734 // Find out which property FloatableElement used for positioning, and adjust that value
5735 positionProp
= vertical
?
5736 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5737 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5739 // Figure out where the near and far edges of the popup and $floatableContainer are
5740 floatablePos
= this.$floatableContainer
.offset();
5741 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5742 // Measure where the offsetParent is and compute our position based on that and parentPosition
5743 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5744 { top
: 0, left
: 0 } :
5745 this.$element
.offsetParent().offset();
5747 if ( positionProp
=== near
) {
5748 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5749 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5751 popupPos
[ far
] = offsetParentPos
[ near
] +
5752 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5753 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5756 if ( this.anchored
) {
5757 // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
5758 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5759 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5761 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
5762 // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
5763 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5764 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5765 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5766 // Not enough space for the anchor on the start side; pull the popup startwards
5767 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5768 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5769 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5770 // Not enough space for the anchor on the end side; pull the popup endwards
5771 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5772 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5774 positionAdjustment
= 0;
5777 positionAdjustment
= 0;
5780 // Check if the popup will go beyond the edge of this.$container
5781 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5782 { top
: 0, left
: 0 } :
5783 this.$container
.offset();
5784 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5785 if ( this.$container
[ 0 ] === document
.documentElement
) {
5786 viewportSpacing
= OO
.ui
.getViewportSpacing();
5787 containerPos
[ near
] += viewportSpacing
[ near
];
5788 containerPos
[ far
] -= viewportSpacing
[ far
];
5790 // Take into account how much the popup will move because of the adjustments we're going to make
5791 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5792 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5793 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5794 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5795 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5796 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5797 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5798 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5799 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5800 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5803 if ( this.anchored
) {
5804 // Adjust anchorOffset for positionAdjustment
5805 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5807 // Position the anchor
5808 anchorCss
[ start
] = anchorOffset
;
5809 this.$anchor
.css( anchorCss
);
5812 // Move the popup if needed
5813 parentPosition
[ positionProp
] += positionAdjustment
;
5815 return parentPosition
;
5819 * Set popup alignment
5821 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5822 * `backwards` or `forwards`.
5824 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5825 // Validate alignment
5826 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5829 this.align
= 'center';
5835 * Get popup alignment
5837 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5838 * `backwards` or `forwards`.
5840 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5845 * Change the positioning of the popup.
5847 * @param {string} position 'above', 'below', 'before' or 'after'
5849 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5850 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5853 this.popupPosition
= position
;
5858 * Get popup positioning.
5860 * @return {string} 'above', 'below', 'before' or 'after'
5862 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5863 return this.popupPosition
;
5867 * Set popup auto-flipping.
5869 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
5870 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5871 * desired direction to display the popup without clipping
5873 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
5874 autoFlip
= !!autoFlip
;
5876 if ( this.autoFlip
!== autoFlip
) {
5877 this.autoFlip
= autoFlip
;
5882 * Set which elements will not close the popup when clicked.
5884 * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
5886 * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
5888 OO
.ui
.PopupWidget
.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore
) {
5889 this.$autoCloseIgnore
= $autoCloseIgnore
;
5893 * Get an ID of the body element, this can be used as the
5894 * `aria-describedby` attribute for an input field.
5896 * @return {string} The ID of the body element
5898 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
5899 var id
= this.$body
.attr( 'id' );
5900 if ( id
=== undefined ) {
5901 id
= OO
.ui
.generateElementId();
5902 this.$body
.attr( 'id', id
);
5908 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5909 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5910 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5911 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5917 * @param {Object} [config] Configuration options
5918 * @cfg {Object} [popup] Configuration to pass to popup
5919 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5921 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5922 // Configuration initialization
5923 config
= config
|| {};
5926 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5929 $floatableContainer
: this.$element
5933 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5943 * @return {OO.ui.PopupWidget} Popup widget
5945 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5950 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5951 * which is used to display additional information or options.
5954 * // Example of a popup button.
5955 * var popupButton = new OO.ui.PopupButtonWidget( {
5956 * label: 'Popup button with options',
5959 * $content: $( '<p>Additional options here.</p>' ),
5961 * align: 'force-left'
5964 * // Append the button to the DOM.
5965 * $( 'body' ).append( popupButton.$element );
5968 * @extends OO.ui.ButtonWidget
5969 * @mixins OO.ui.mixin.PopupElement
5972 * @param {Object} [config] Configuration options
5973 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
5974 * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
5975 * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
5976 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
5978 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
5979 // Configuration initialization
5980 config
= config
|| {};
5982 // Parent constructor
5983 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
5985 // Mixin constructors
5986 OO
.ui
.mixin
.PopupElement
.call( this, config
);
5989 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
5992 this.connect( this, { click
: 'onAction' } );
5996 .addClass( 'oo-ui-popupButtonWidget' );
5998 .addClass( 'oo-ui-popupButtonWidget-popup' )
5999 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
6000 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
6001 this.$overlay
.append( this.popup
.$element
);
6006 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
6007 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
6012 * Handle the button action being triggered.
6016 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
6017 this.popup
.toggle();
6021 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
6023 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
6028 * @mixins OO.ui.mixin.GroupElement
6031 * @param {Object} [config] Configuration options
6033 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
6034 // Mixin constructors
6035 OO
.ui
.mixin
.GroupElement
.call( this, config
);
6040 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
6045 * Set the disabled state of the widget.
6047 * This will also update the disabled state of child widgets.
6049 * @param {boolean} disabled Disable widget
6051 * @return {OO.ui.Widget} The widget, for chaining
6053 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
6057 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
6058 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
6060 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
6062 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6063 this.items
[ i
].updateDisabled();
6071 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
6073 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
6074 * allows bidirectional communication.
6076 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
6084 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
6091 * Check if widget is disabled.
6093 * Checks parent if present, making disabled state inheritable.
6095 * @return {boolean} Widget is disabled
6097 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
6098 return this.disabled
||
6099 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
6103 * Set group element is in.
6105 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
6107 * @return {OO.ui.Widget} The widget, for chaining
6109 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
6111 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
6112 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
6114 // Initialize item disabled states
6115 this.updateDisabled();
6121 * OptionWidgets are special elements that can be selected and configured with data. The
6122 * data is often unique for each option, but it does not have to be. OptionWidgets are used
6123 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6124 * and examples, please see the [OOUI documentation on MediaWiki][1].
6126 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6129 * @extends OO.ui.Widget
6130 * @mixins OO.ui.mixin.ItemWidget
6131 * @mixins OO.ui.mixin.LabelElement
6132 * @mixins OO.ui.mixin.FlaggedElement
6133 * @mixins OO.ui.mixin.AccessKeyedElement
6136 * @param {Object} [config] Configuration options
6138 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
6139 // Configuration initialization
6140 config
= config
|| {};
6142 // Parent constructor
6143 OO
.ui
.OptionWidget
.parent
.call( this, config
);
6145 // Mixin constructors
6146 OO
.ui
.mixin
.ItemWidget
.call( this );
6147 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6148 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6149 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6152 this.selected
= false;
6153 this.highlighted
= false;
6154 this.pressed
= false;
6158 .data( 'oo-ui-optionWidget', this )
6159 // Allow programmatic focussing (and by accesskey), but not tabbing
6160 .attr( 'tabindex', '-1' )
6161 .attr( 'role', 'option' )
6162 .attr( 'aria-selected', 'false' )
6163 .addClass( 'oo-ui-optionWidget' )
6164 .append( this.$label
);
6169 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6170 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6171 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6172 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6173 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6175 /* Static Properties */
6178 * Whether this option can be selected. See #setSelected.
6182 * @property {boolean}
6184 OO
.ui
.OptionWidget
.static.selectable
= true;
6187 * Whether this option can be highlighted. See #setHighlighted.
6191 * @property {boolean}
6193 OO
.ui
.OptionWidget
.static.highlightable
= true;
6196 * Whether this option can be pressed. See #setPressed.
6200 * @property {boolean}
6202 OO
.ui
.OptionWidget
.static.pressable
= true;
6205 * Whether this option will be scrolled into view when it is selected.
6209 * @property {boolean}
6211 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6216 * Check if the option can be selected.
6218 * @return {boolean} Item is selectable
6220 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6221 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6225 * Check if the option can be highlighted. A highlight indicates that the option
6226 * may be selected when a user presses enter or clicks. Disabled items cannot
6229 * @return {boolean} Item is highlightable
6231 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6232 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6236 * Check if the option can be pressed. The pressed state occurs when a user mouses
6237 * down on an item, but has not yet let go of the mouse.
6239 * @return {boolean} Item is pressable
6241 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6242 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6246 * Check if the option is selected.
6248 * @return {boolean} Item is selected
6250 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6251 return this.selected
;
6255 * Check if the option is highlighted. A highlight indicates that the
6256 * item may be selected when a user presses enter or clicks.
6258 * @return {boolean} Item is highlighted
6260 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6261 return this.highlighted
;
6265 * Check if the option is pressed. The pressed state occurs when a user mouses
6266 * down on an item, but has not yet let go of the mouse. The item may appear
6267 * selected, but it will not be selected until the user releases the mouse.
6269 * @return {boolean} Item is pressed
6271 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6272 return this.pressed
;
6276 * Set the option’s selected state. In general, all modifications to the selection
6277 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6278 * method instead of this method.
6280 * @param {boolean} [state=false] Select option
6282 * @return {OO.ui.Widget} The widget, for chaining
6284 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6285 if ( this.constructor.static.selectable
) {
6286 this.selected
= !!state
;
6288 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6289 .attr( 'aria-selected', state
.toString() );
6290 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6291 this.scrollElementIntoView();
6293 this.updateThemeClasses();
6299 * Set the option’s highlighted state. In general, all programmatic
6300 * modifications to the highlight should be handled by the
6301 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6302 * method instead of this method.
6304 * @param {boolean} [state=false] Highlight option
6306 * @return {OO.ui.Widget} The widget, for chaining
6308 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6309 if ( this.constructor.static.highlightable
) {
6310 this.highlighted
= !!state
;
6311 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6312 this.updateThemeClasses();
6318 * Set the option’s pressed state. In general, all
6319 * programmatic modifications to the pressed state should be handled by the
6320 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6321 * method instead of this method.
6323 * @param {boolean} [state=false] Press option
6325 * @return {OO.ui.Widget} The widget, for chaining
6327 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6328 if ( this.constructor.static.pressable
) {
6329 this.pressed
= !!state
;
6330 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6331 this.updateThemeClasses();
6337 * Get text to match search strings against.
6339 * The default implementation returns the label text, but subclasses
6340 * can override this to provide more complex behavior.
6342 * @return {string|boolean} String to match search string against
6344 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6345 var label
= this.getLabel();
6346 return typeof label
=== 'string' ? label
: this.$label
.text();
6350 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6351 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6352 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6355 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
6356 * information, please see the [OOUI documentation on MediaWiki][1].
6359 * // Example of a select widget with three options
6360 * var select = new OO.ui.SelectWidget( {
6362 * new OO.ui.OptionWidget( {
6364 * label: 'Option One',
6366 * new OO.ui.OptionWidget( {
6368 * label: 'Option Two',
6370 * new OO.ui.OptionWidget( {
6372 * label: 'Option Three',
6376 * $( 'body' ).append( select.$element );
6378 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6382 * @extends OO.ui.Widget
6383 * @mixins OO.ui.mixin.GroupWidget
6386 * @param {Object} [config] Configuration options
6387 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6388 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6389 * the [OOUI documentation on MediaWiki] [2] for examples.
6390 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6392 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6393 // Configuration initialization
6394 config
= config
|| {};
6396 // Parent constructor
6397 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6399 // Mixin constructors
6400 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
6403 this.pressed
= false;
6404 this.selecting
= null;
6405 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
6406 this.onDocumentMouseMoveHandler
= this.onDocumentMouseMove
.bind( this );
6407 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
6408 this.onDocumentKeyPressHandler
= this.onDocumentKeyPress
.bind( this );
6409 this.keyPressBuffer
= '';
6410 this.keyPressBufferTimer
= null;
6411 this.blockMouseOverEvents
= 0;
6414 this.connect( this, {
6418 focusin
: this.onFocus
.bind( this ),
6419 mousedown
: this.onMouseDown
.bind( this ),
6420 mouseover
: this.onMouseOver
.bind( this ),
6421 mouseleave
: this.onMouseLeave
.bind( this )
6426 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
6427 .attr( 'role', 'listbox' );
6428 this.setFocusOwner( this.$element
);
6429 if ( Array
.isArray( config
.items
) ) {
6430 this.addItems( config
.items
);
6436 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6437 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6444 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6446 * @param {OO.ui.OptionWidget|null} item Highlighted item
6452 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6453 * pressed state of an option.
6455 * @param {OO.ui.OptionWidget|null} item Pressed item
6461 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
6463 * @param {OO.ui.OptionWidget|null} item Selected item
6468 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6469 * @param {OO.ui.OptionWidget} item Chosen item
6475 * An `add` event is emitted when options are added to the select with the #addItems method.
6477 * @param {OO.ui.OptionWidget[]} items Added items
6478 * @param {number} index Index of insertion point
6484 * A `remove` event is emitted when options are removed from the select with the #clearItems
6485 * or #removeItems methods.
6487 * @param {OO.ui.OptionWidget[]} items Removed items
6493 * Handle focus events
6496 * @param {jQuery.Event} event
6498 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6500 if ( event
.target
=== this.$element
[ 0 ] ) {
6501 // This widget was focussed, e.g. by the user tabbing to it.
6502 // The styles for focus state depend on one of the items being selected.
6503 if ( !this.findSelectedItem() ) {
6504 item
= this.findFirstSelectableItem();
6507 if ( event
.target
.tabIndex
=== -1 ) {
6508 // One of the options got focussed (and the event bubbled up here).
6509 // They can't be tabbed to, but they can be activated using accesskeys.
6510 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6511 item
= this.findTargetItem( event
);
6513 // There is something actually user-focusable in one of the labels of the options, and the
6514 // user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change the focus).
6520 if ( item
.constructor.static.highlightable
) {
6521 this.highlightItem( item
);
6523 this.selectItem( item
);
6527 if ( event
.target
!== this.$element
[ 0 ] ) {
6528 this.$focusOwner
.focus();
6533 * Handle mouse down events.
6536 * @param {jQuery.Event} e Mouse down event
6537 * @return {undefined/boolean} False to prevent default if event is handled
6539 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6542 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6543 this.togglePressed( true );
6544 item
= this.findTargetItem( e
);
6545 if ( item
&& item
.isSelectable() ) {
6546 this.pressItem( item
);
6547 this.selecting
= item
;
6548 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6549 this.getElementDocument().addEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6556 * Handle document mouse up events.
6559 * @param {MouseEvent} e Mouse up event
6560 * @return {undefined/boolean} False to prevent default if event is handled
6562 OO
.ui
.SelectWidget
.prototype.onDocumentMouseUp = function ( e
) {
6565 this.togglePressed( false );
6566 if ( !this.selecting
) {
6567 item
= this.findTargetItem( e
);
6568 if ( item
&& item
.isSelectable() ) {
6569 this.selecting
= item
;
6572 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6573 this.pressItem( null );
6574 this.chooseItem( this.selecting
);
6575 this.selecting
= null;
6578 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6579 this.getElementDocument().removeEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6584 // Deprecated alias since 0.28.3
6585 OO
.ui
.SelectWidget
.prototype.onMouseUp = function () {
6586 OO
.ui
.warnDeprecation( 'onMouseUp is deprecated, use onDocumentMouseUp instead' );
6587 this.onDocumentMouseUp
.apply( this, arguments
);
6591 * Handle document mouse move events.
6594 * @param {MouseEvent} e Mouse move event
6596 OO
.ui
.SelectWidget
.prototype.onDocumentMouseMove = function ( e
) {
6599 if ( !this.isDisabled() && this.pressed
) {
6600 item
= this.findTargetItem( e
);
6601 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6602 this.pressItem( item
);
6603 this.selecting
= item
;
6608 // Deprecated alias since 0.28.3
6609 OO
.ui
.SelectWidget
.prototype.onMouseMove = function () {
6610 OO
.ui
.warnDeprecation( 'onMouseMove is deprecated, use onDocumentMouseMove instead' );
6611 this.onDocumentMouseMove
.apply( this, arguments
);
6615 * Handle mouse over events.
6618 * @param {jQuery.Event} e Mouse over event
6619 * @return {undefined/boolean} False to prevent default if event is handled
6621 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6623 if ( this.blockMouseOverEvents
) {
6626 if ( !this.isDisabled() ) {
6627 item
= this.findTargetItem( e
);
6628 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6634 * Handle mouse leave events.
6637 * @param {jQuery.Event} e Mouse over event
6638 * @return {undefined/boolean} False to prevent default if event is handled
6640 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6641 if ( !this.isDisabled() ) {
6642 this.highlightItem( null );
6648 * Handle document key down events.
6651 * @param {KeyboardEvent} e Key down event
6653 OO
.ui
.SelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
6656 currentItem
= this.findHighlightedItem() || this.findSelectedItem();
6658 if ( !this.isDisabled() && this.isVisible() ) {
6659 switch ( e
.keyCode
) {
6660 case OO
.ui
.Keys
.ENTER
:
6661 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6662 // Was only highlighted, now let's select it. No-op if already selected.
6663 this.chooseItem( currentItem
);
6668 case OO
.ui
.Keys
.LEFT
:
6669 this.clearKeyPressBuffer();
6670 nextItem
= this.findRelativeSelectableItem( currentItem
, -1 );
6673 case OO
.ui
.Keys
.DOWN
:
6674 case OO
.ui
.Keys
.RIGHT
:
6675 this.clearKeyPressBuffer();
6676 nextItem
= this.findRelativeSelectableItem( currentItem
, 1 );
6679 case OO
.ui
.Keys
.ESCAPE
:
6680 case OO
.ui
.Keys
.TAB
:
6681 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6682 currentItem
.setHighlighted( false );
6684 this.unbindDocumentKeyDownListener();
6685 this.unbindDocumentKeyPressListener();
6686 // Don't prevent tabbing away / defocusing
6692 if ( nextItem
.constructor.static.highlightable
) {
6693 this.highlightItem( nextItem
);
6695 this.chooseItem( nextItem
);
6697 this.scrollItemIntoView( nextItem
);
6702 e
.stopPropagation();
6707 // Deprecated alias since 0.28.3
6708 OO
.ui
.SelectWidget
.prototype.onKeyDown = function () {
6709 OO
.ui
.warnDeprecation( 'onKeyDown is deprecated, use onDocumentKeyDown instead' );
6710 this.onDocumentKeyDown
.apply( this, arguments
);
6714 * Bind document key down listener.
6718 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyDownListener = function () {
6719 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6722 // Deprecated alias since 0.28.3
6723 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
6724 OO
.ui
.warnDeprecation( 'bindKeyDownListener is deprecated, use bindDocumentKeyDownListener instead' );
6725 this.bindDocumentKeyDownListener
.apply( this, arguments
);
6729 * Unbind document key down listener.
6733 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
6734 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6737 // Deprecated alias since 0.28.3
6738 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
6739 OO
.ui
.warnDeprecation( 'unbindKeyDownListener is deprecated, use unbindDocumentKeyDownListener instead' );
6740 this.unbindDocumentKeyDownListener
.apply( this, arguments
);
6744 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6746 * @param {OO.ui.OptionWidget} item Item to scroll into view
6748 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6750 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
6751 // and around 100-150 ms after it is finished.
6752 this.blockMouseOverEvents
++;
6753 item
.scrollElementIntoView().done( function () {
6754 setTimeout( function () {
6755 widget
.blockMouseOverEvents
--;
6761 * Clear the key-press buffer
6765 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6766 if ( this.keyPressBufferTimer
) {
6767 clearTimeout( this.keyPressBufferTimer
);
6768 this.keyPressBufferTimer
= null;
6770 this.keyPressBuffer
= '';
6774 * Handle key press events.
6777 * @param {KeyboardEvent} e Key press event
6778 * @return {undefined/boolean} False to prevent default if event is handled
6780 OO
.ui
.SelectWidget
.prototype.onDocumentKeyPress = function ( e
) {
6781 var c
, filter
, item
;
6783 if ( !e
.charCode
) {
6784 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6785 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6790 // eslint-disable-next-line no-restricted-properties
6791 if ( String
.fromCodePoint
) {
6792 // eslint-disable-next-line no-restricted-properties
6793 c
= String
.fromCodePoint( e
.charCode
);
6795 c
= String
.fromCharCode( e
.charCode
);
6798 if ( this.keyPressBufferTimer
) {
6799 clearTimeout( this.keyPressBufferTimer
);
6801 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6803 item
= this.findHighlightedItem() || this.findSelectedItem();
6805 if ( this.keyPressBuffer
=== c
) {
6806 // Common (if weird) special case: typing "xxxx" will cycle through all
6807 // the items beginning with "x".
6809 item
= this.findRelativeSelectableItem( item
, 1 );
6812 this.keyPressBuffer
+= c
;
6815 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6816 if ( !item
|| !filter( item
) ) {
6817 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6820 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6821 this.highlightItem( item
);
6823 this.chooseItem( item
);
6825 this.scrollItemIntoView( item
);
6829 e
.stopPropagation();
6832 // Deprecated alias since 0.28.3
6833 OO
.ui
.SelectWidget
.prototype.onKeyPress = function () {
6834 OO
.ui
.warnDeprecation( 'onKeyPress is deprecated, use onDocumentKeyPress instead' );
6835 this.onDocumentKeyPress
.apply( this, arguments
);
6839 * Get a matcher for the specific string
6842 * @param {string} s String to match against items
6843 * @param {boolean} [exact=false] Only accept exact matches
6844 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6846 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
6849 // eslint-disable-next-line no-restricted-properties
6850 if ( s
.normalize
) {
6851 // eslint-disable-next-line no-restricted-properties
6854 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
6855 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
6859 re
= new RegExp( re
, 'i' );
6860 return function ( item
) {
6861 var matchText
= item
.getMatchText();
6862 // eslint-disable-next-line no-restricted-properties
6863 if ( matchText
.normalize
) {
6864 // eslint-disable-next-line no-restricted-properties
6865 matchText
= matchText
.normalize();
6867 return re
.test( matchText
);
6872 * Bind document key press listener.
6876 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyPressListener = function () {
6877 this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
6880 // Deprecated alias since 0.28.3
6881 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
6882 OO
.ui
.warnDeprecation( 'bindKeyPressListener is deprecated, use bindDocumentKeyPressListener instead' );
6883 this.bindDocumentKeyPressListener
.apply( this, arguments
);
6887 * Unbind document key down listener.
6889 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6894 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
6895 this.getElementDocument().removeEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
6896 this.clearKeyPressBuffer();
6899 // Deprecated alias since 0.28.3
6900 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
6901 OO
.ui
.warnDeprecation( 'unbindKeyPressListener is deprecated, use unbindDocumentKeyPressListener instead' );
6902 this.unbindDocumentKeyPressListener
.apply( this, arguments
);
6906 * Visibility change handler
6909 * @param {boolean} visible
6911 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6913 this.clearKeyPressBuffer();
6918 * Get the closest item to a jQuery.Event.
6921 * @param {jQuery.Event} e
6922 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6924 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
6925 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
6926 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
6929 return $option
.data( 'oo-ui-optionWidget' ) || null;
6933 * Find selected item.
6935 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6937 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
6940 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6941 if ( this.items
[ i
].isSelected() ) {
6942 return this.items
[ i
];
6949 * Find highlighted item.
6951 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6953 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
6956 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6957 if ( this.items
[ i
].isHighlighted() ) {
6958 return this.items
[ i
];
6965 * Toggle pressed state.
6967 * Press is a state that occurs when a user mouses down on an item, but
6968 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
6969 * until the user releases the mouse.
6971 * @param {boolean} pressed An option is being pressed
6973 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
6974 if ( pressed
=== undefined ) {
6975 pressed
= !this.pressed
;
6977 if ( pressed
!== this.pressed
) {
6979 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
6980 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
6981 this.pressed
= pressed
;
6986 * Highlight an option. If the `item` param is omitted, no options will be highlighted
6987 * and any existing highlight will be removed. The highlight is mutually exclusive.
6989 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
6992 * @return {OO.ui.Widget} The widget, for chaining
6994 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
6995 var i
, len
, highlighted
,
6998 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6999 highlighted
= this.items
[ i
] === item
;
7000 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
7001 this.items
[ i
].setHighlighted( highlighted
);
7007 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7009 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7011 this.emit( 'highlight', item
);
7018 * Fetch an item by its label.
7020 * @param {string} label Label of the item to select.
7021 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7022 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
7024 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
7026 len
= this.items
.length
,
7027 filter
= this.getItemMatcher( label
, true );
7029 for ( i
= 0; i
< len
; i
++ ) {
7030 item
= this.items
[ i
];
7031 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7038 filter
= this.getItemMatcher( label
, false );
7039 for ( i
= 0; i
< len
; i
++ ) {
7040 item
= this.items
[ i
];
7041 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7057 * Programmatically select an option by its label. If the item does not exist,
7058 * all options will be deselected.
7060 * @param {string} [label] Label of the item to select.
7061 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7064 * @return {OO.ui.Widget} The widget, for chaining
7066 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
7067 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
7068 if ( label
=== undefined || !itemFromLabel
) {
7069 return this.selectItem();
7071 return this.selectItem( itemFromLabel
);
7075 * Programmatically select an option by its data. If the `data` parameter is omitted,
7076 * or if the item does not exist, all options will be deselected.
7078 * @param {Object|string} [data] Value of the item to select, omit to deselect all
7081 * @return {OO.ui.Widget} The widget, for chaining
7083 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
7084 var itemFromData
= this.findItemFromData( data
);
7085 if ( data
=== undefined || !itemFromData
) {
7086 return this.selectItem();
7088 return this.selectItem( itemFromData
);
7092 * Programmatically select an option by its reference. If the `item` parameter is omitted,
7093 * all options will be deselected.
7095 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
7098 * @return {OO.ui.Widget} The widget, for chaining
7100 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
7101 var i
, len
, selected
,
7104 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7105 selected
= this.items
[ i
] === item
;
7106 if ( this.items
[ i
].isSelected() !== selected
) {
7107 this.items
[ i
].setSelected( selected
);
7112 if ( item
&& !item
.constructor.static.highlightable
) {
7114 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7116 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7119 this.emit( 'select', item
);
7128 * Press is a state that occurs when a user mouses down on an item, but has not
7129 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
7130 * releases the mouse.
7132 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
7135 * @return {OO.ui.Widget} The widget, for chaining
7137 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
7138 var i
, len
, pressed
,
7141 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7142 pressed
= this.items
[ i
] === item
;
7143 if ( this.items
[ i
].isPressed() !== pressed
) {
7144 this.items
[ i
].setPressed( pressed
);
7149 this.emit( 'press', item
);
7158 * Note that ‘choose’ should never be modified programmatically. A user can choose
7159 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
7160 * use the #selectItem method.
7162 * This method is identical to #selectItem, but may vary in subclasses that take additional action
7163 * when users choose an item with the keyboard or mouse.
7165 * @param {OO.ui.OptionWidget} item Item to choose
7168 * @return {OO.ui.Widget} The widget, for chaining
7170 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
7172 this.selectItem( item
);
7173 this.emit( 'choose', item
);
7180 * Find an option by its position relative to the specified item (or to the start of the option array,
7181 * if item is `null`). The direction in which to search through the option array is specified with a
7182 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
7183 * `null` if there are no options in the array.
7185 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
7186 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7187 * @param {Function} [filter] Only consider items for which this function returns
7188 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
7189 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
7191 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
7192 var currentIndex
, nextIndex
, i
,
7193 increase
= direction
> 0 ? 1 : -1,
7194 len
= this.items
.length
;
7196 if ( item
instanceof OO
.ui
.OptionWidget
) {
7197 currentIndex
= this.items
.indexOf( item
);
7198 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7200 // If no item is selected and moving forward, start at the beginning.
7201 // If moving backward, start at the end.
7202 nextIndex
= direction
> 0 ? 0 : len
- 1;
7205 for ( i
= 0; i
< len
; i
++ ) {
7206 item
= this.items
[ nextIndex
];
7208 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
7209 ( !filter
|| filter( item
) )
7213 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7219 * Find the next selectable item or `null` if there are no selectable items.
7220 * Disabled options and menu-section markers and breaks are not selectable.
7222 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7224 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7225 return this.findRelativeSelectableItem( null, 1 );
7229 * Add an array of options to the select. Optionally, an index number can be used to
7230 * specify an insertion point.
7232 * @param {OO.ui.OptionWidget[]} items Items to add
7233 * @param {number} [index] Index to insert items after
7236 * @return {OO.ui.Widget} The widget, for chaining
7238 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7240 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7242 // Always provide an index, even if it was omitted
7243 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7249 * Remove the specified array of options from the select. Options will be detached
7250 * from the DOM, not removed, so they can be reused later. To remove all options from
7251 * the select, you may wish to use the #clearItems method instead.
7253 * @param {OO.ui.OptionWidget[]} items Items to remove
7256 * @return {OO.ui.Widget} The widget, for chaining
7258 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7261 // Deselect items being removed
7262 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7264 if ( item
.isSelected() ) {
7265 this.selectItem( null );
7270 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7272 this.emit( 'remove', items
);
7278 * Clear all options from the select. Options will be detached from the DOM, not removed,
7279 * so that they can be reused later. To remove a subset of options from the select, use
7280 * the #removeItems method.
7284 * @return {OO.ui.Widget} The widget, for chaining
7286 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7287 var items
= this.items
.slice();
7290 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7293 this.selectItem( null );
7295 this.emit( 'remove', items
);
7301 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7303 * Currently this is just used to set `aria-activedescendant` on it.
7306 * @param {jQuery} $focusOwner
7308 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7309 this.$focusOwner
= $focusOwner
;
7313 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7314 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
7315 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7316 * options. For more information about options and selects, please see the
7317 * [OOUI documentation on MediaWiki][1].
7320 * // Decorated options in a select widget
7321 * var select = new OO.ui.SelectWidget( {
7323 * new OO.ui.DecoratedOptionWidget( {
7325 * label: 'Option with icon',
7328 * new OO.ui.DecoratedOptionWidget( {
7330 * label: 'Option with indicator',
7335 * $( 'body' ).append( select.$element );
7337 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7340 * @extends OO.ui.OptionWidget
7341 * @mixins OO.ui.mixin.IconElement
7342 * @mixins OO.ui.mixin.IndicatorElement
7345 * @param {Object} [config] Configuration options
7347 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7348 // Parent constructor
7349 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7351 // Mixin constructors
7352 OO
.ui
.mixin
.IconElement
.call( this, config
);
7353 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7357 .addClass( 'oo-ui-decoratedOptionWidget' )
7358 .prepend( this.$icon
)
7359 .append( this.$indicator
);
7364 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7365 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7366 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7369 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7370 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7371 * the [OOUI documentation on MediaWiki] [1] for more information.
7373 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7376 * @extends OO.ui.DecoratedOptionWidget
7379 * @param {Object} [config] Configuration options
7381 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7382 // Parent constructor
7383 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7386 this.checkIcon
= new OO
.ui
.IconWidget( {
7388 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7393 .prepend( this.checkIcon
.$element
)
7394 .addClass( 'oo-ui-menuOptionWidget' );
7399 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7401 /* Static Properties */
7407 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7410 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
7411 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
7414 * var myDropdown = new OO.ui.DropdownWidget( {
7417 * new OO.ui.MenuSectionOptionWidget( {
7420 * new OO.ui.MenuOptionWidget( {
7422 * label: 'Welsh Corgi'
7424 * new OO.ui.MenuOptionWidget( {
7426 * label: 'Standard Poodle'
7428 * new OO.ui.MenuSectionOptionWidget( {
7431 * new OO.ui.MenuOptionWidget( {
7438 * $( 'body' ).append( myDropdown.$element );
7441 * @extends OO.ui.DecoratedOptionWidget
7444 * @param {Object} [config] Configuration options
7446 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7447 // Parent constructor
7448 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7451 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' )
7452 .removeAttr( 'role aria-selected' );
7457 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7459 /* Static Properties */
7465 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7471 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7474 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7475 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7476 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
7477 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7478 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7479 * and customized to be opened, closed, and displayed as needed.
7481 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7482 * mouse outside the menu.
7484 * Menus also have support for keyboard interaction:
7486 * - Enter/Return key: choose and select a menu option
7487 * - Up-arrow key: highlight the previous menu option
7488 * - Down-arrow key: highlight the next menu option
7489 * - Esc key: hide the menu
7491 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7493 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7494 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7497 * @extends OO.ui.SelectWidget
7498 * @mixins OO.ui.mixin.ClippableElement
7499 * @mixins OO.ui.mixin.FloatableElement
7502 * @param {Object} [config] Configuration options
7503 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
7504 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
7505 * and {@link OO.ui.mixin.LookupElement LookupElement}
7506 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7507 * the text the user types. This config is used by {@link OO.ui.TagMultiselectWidget TagMultiselectWidget}
7508 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
7509 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
7510 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
7511 * that button, unless the button (or its parent widget) is passed in here.
7512 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7513 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7514 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7515 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7516 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7517 * @cfg {number} [width] Width of the menu
7519 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7520 // Configuration initialization
7521 config
= config
|| {};
7523 // Parent constructor
7524 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7526 // Mixin constructors
7527 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
7528 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7530 // Initial vertical positions other than 'center' will result in
7531 // the menu being flipped if there is not enough space in the container.
7532 // Store the original position so we know what to reset to.
7533 this.originalVerticalPosition
= this.verticalPosition
;
7536 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7537 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7538 this.filterFromInput
= !!config
.filterFromInput
;
7539 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7540 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7541 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7542 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7543 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7544 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7545 this.width
= config
.width
;
7548 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7549 if ( config
.widget
) {
7550 this.setFocusOwner( config
.widget
.$tabIndexed
);
7553 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7554 // that reference properties not initialized at that time of parent class construction
7555 // TODO: Find a better way to handle post-constructor setup
7556 this.visible
= false;
7557 this.$element
.addClass( 'oo-ui-element-hidden' );
7562 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7563 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7564 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7571 * The menu is ready: it is visible and has been positioned and clipped.
7574 /* Static properties */
7577 * Positions to flip to if there isn't room in the container for the
7578 * menu in a specific direction.
7580 * @property {Object.<string,string>}
7582 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7592 * Handles document mouse down events.
7595 * @param {MouseEvent} e Mouse down event
7597 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7601 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7606 this.toggle( false );
7613 OO
.ui
.MenuSelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
7614 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7616 if ( !this.isDisabled() && this.isVisible() ) {
7617 switch ( e
.keyCode
) {
7618 case OO
.ui
.Keys
.LEFT
:
7619 case OO
.ui
.Keys
.RIGHT
:
7620 // Do nothing if a text field is associated, arrow keys will be handled natively
7621 if ( !this.$input
) {
7622 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7625 case OO
.ui
.Keys
.ESCAPE
:
7626 case OO
.ui
.Keys
.TAB
:
7627 if ( currentItem
) {
7628 currentItem
.setHighlighted( false );
7630 this.toggle( false );
7631 // Don't prevent tabbing away, prevent defocusing
7632 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7634 e
.stopPropagation();
7638 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7645 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7646 * or after items were added/removed (always).
7650 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7651 var i
, item
, items
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7653 len
= this.items
.length
,
7654 showAll
= !this.isVisible(),
7657 if ( this.$input
&& this.filterFromInput
) {
7658 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
7659 exactFilter
= this.getItemMatcher( this.$input
.val(), true );
7660 // Hide non-matching options, and also hide section headers if all options
7661 // in their section are hidden.
7662 for ( i
= 0; i
< len
; i
++ ) {
7663 item
= this.items
[ i
];
7664 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7666 // If the previous section was empty, hide its header
7667 section
.toggle( showAll
|| !sectionEmpty
);
7670 sectionEmpty
= true;
7671 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7672 visible
= showAll
|| filter( item
);
7673 exactMatch
= exactMatch
|| exactFilter( item
);
7674 anyVisible
= anyVisible
|| visible
;
7675 sectionEmpty
= sectionEmpty
&& !visible
;
7676 item
.toggle( visible
);
7679 // Process the final section
7681 section
.toggle( showAll
|| !sectionEmpty
);
7684 if ( anyVisible
&& this.items
.length
&& !exactMatch
) {
7685 this.scrollItemIntoView( this.items
[ 0 ] );
7688 if ( !anyVisible
) {
7689 this.highlightItem( null );
7692 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7694 if ( this.highlightOnFilter
) {
7695 // Highlight the first item on the list
7697 items
= this.getItems();
7698 for ( i
= 0; i
< items
.length
; i
++ ) {
7699 if ( items
[ i
].isVisible() ) {
7704 this.highlightItem( item
);
7709 // Reevaluate clipping
7716 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyDownListener = function () {
7717 if ( this.$input
) {
7718 this.$input
.on( 'keydown', this.onDocumentKeyDownHandler
);
7720 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyDownListener
.call( this );
7727 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
7728 if ( this.$input
) {
7729 this.$input
.off( 'keydown', this.onDocumentKeyDownHandler
);
7731 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyDownListener
.call( this );
7738 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7739 if ( this.$input
) {
7740 if ( this.filterFromInput
) {
7741 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7742 this.updateItemVisibility();
7745 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyPressListener
.call( this );
7752 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7753 if ( this.$input
) {
7754 if ( this.filterFromInput
) {
7755 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7756 this.updateItemVisibility();
7759 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyPressListener
.call( this );
7766 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
7768 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
7769 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
7771 * @param {OO.ui.OptionWidget} item Item to choose
7773 * @return {OO.ui.Widget} The widget, for chaining
7775 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7776 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7777 if ( this.hideOnChoose
) {
7778 this.toggle( false );
7786 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7788 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7790 this.updateItemVisibility();
7798 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7800 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7802 this.updateItemVisibility();
7810 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7812 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
7814 this.updateItemVisibility();
7820 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7821 * `.toggle( true )` after its #$element is attached to the DOM.
7823 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7824 * it in the right place and with the right dimensions only work correctly while it is attached.
7825 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7826 * strictly enforced, so currently it only generates a warning in the browser console.
7831 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7832 var change
, originalHeight
, flippedHeight
;
7834 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7835 change
= visible
!== this.isVisible();
7837 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7838 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7839 this.warnedUnattached
= true;
7842 if ( change
&& visible
) {
7843 // Reset position before showing the popup again. It's possible we no longer need to flip
7844 // (e.g. if the user scrolled).
7845 this.setVerticalPosition( this.originalVerticalPosition
);
7849 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7855 this.setIdealSize( this.width
);
7856 } else if ( this.$floatableContainer
) {
7857 this.$clippable
.css( 'width', 'auto' );
7859 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
7860 // Dropdown is smaller than handle so expand to width
7861 this.$floatableContainer
[ 0 ].offsetWidth
:
7862 // Dropdown is larger than handle so auto size
7865 this.$clippable
.css( 'width', '' );
7868 this.togglePositioning( !!this.$floatableContainer
);
7869 this.toggleClipping( true );
7871 this.bindDocumentKeyDownListener();
7872 this.bindDocumentKeyPressListener();
7875 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
7876 this.originalVerticalPosition
!== 'center'
7878 // If opening the menu in one direction causes it to be clipped, flip it
7879 originalHeight
= this.$element
.height();
7880 this.setVerticalPosition(
7881 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
7883 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
7884 // If flipping also causes it to be clipped, open in whichever direction
7885 // we have more space
7886 flippedHeight
= this.$element
.height();
7887 if ( originalHeight
> flippedHeight
) {
7888 this.setVerticalPosition( this.originalVerticalPosition
);
7892 // Note that we do not flip the menu's opening direction if the clipping changes
7893 // later (e.g. after the user scrolls), that seems like it would be annoying
7895 this.$focusOwner
.attr( 'aria-expanded', 'true' );
7897 if ( this.findSelectedItem() ) {
7898 this.$focusOwner
.attr( 'aria-activedescendant', this.findSelectedItem().getElementId() );
7899 this.findSelectedItem().scrollElementIntoView( { duration
: 0 } );
7903 if ( this.autoHide
) {
7904 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7907 this.emit( 'ready' );
7909 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7910 this.unbindDocumentKeyDownListener();
7911 this.unbindDocumentKeyPressListener();
7912 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7913 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7914 this.togglePositioning( false );
7915 this.toggleClipping( false );
7923 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
7924 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
7925 * users can interact with it.
7927 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7928 * OO.ui.DropdownInputWidget instead.
7931 * // Example: A DropdownWidget with a menu that contains three options
7932 * var dropDown = new OO.ui.DropdownWidget( {
7933 * label: 'Dropdown menu: Select a menu option',
7936 * new OO.ui.MenuOptionWidget( {
7940 * new OO.ui.MenuOptionWidget( {
7944 * new OO.ui.MenuOptionWidget( {
7952 * $( 'body' ).append( dropDown.$element );
7954 * dropDown.getMenu().selectItemByData( 'b' );
7956 * dropDown.getMenu().findSelectedItem().getData(); // returns 'b'
7958 * For more information, please see the [OOUI documentation on MediaWiki] [1].
7960 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7963 * @extends OO.ui.Widget
7964 * @mixins OO.ui.mixin.IconElement
7965 * @mixins OO.ui.mixin.IndicatorElement
7966 * @mixins OO.ui.mixin.LabelElement
7967 * @mixins OO.ui.mixin.TitledElement
7968 * @mixins OO.ui.mixin.TabIndexedElement
7971 * @param {Object} [config] Configuration options
7972 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.MenuSelectWidget menu select widget}
7973 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
7974 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
7975 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
7976 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
7978 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
7979 // Configuration initialization
7980 config
= $.extend( { indicator
: 'down' }, config
);
7982 // Parent constructor
7983 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
7985 // Properties (must be set before TabIndexedElement constructor call)
7986 this.$handle
= $( '<button>' );
7987 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
7989 // Mixin constructors
7990 OO
.ui
.mixin
.IconElement
.call( this, config
);
7991 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7992 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7993 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
7994 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
7997 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
7999 $floatableContainer
: this.$element
8004 click
: this.onClick
.bind( this ),
8005 keydown
: this.onKeyDown
.bind( this ),
8006 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
8007 keypress
: this.menu
.onDocumentKeyPressHandler
,
8008 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
8010 this.menu
.connect( this, {
8011 select
: 'onMenuSelect',
8012 toggle
: 'onMenuToggle'
8017 .addClass( 'oo-ui-dropdownWidget-handle' )
8019 'aria-owns': this.menu
.getElementId(),
8020 'aria-haspopup': 'listbox'
8022 .append( this.$icon
, this.$label
, this.$indicator
);
8024 .addClass( 'oo-ui-dropdownWidget' )
8025 .append( this.$handle
);
8026 this.$overlay
.append( this.menu
.$element
);
8031 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
8032 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
8033 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
8034 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
8035 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
8036 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8043 * @return {OO.ui.MenuSelectWidget} Menu of widget
8045 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
8050 * Handles menu select events.
8053 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8055 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
8059 this.setLabel( null );
8063 selectedLabel
= item
.getLabel();
8065 // If the label is a DOM element, clone it, because setLabel will append() it
8066 if ( selectedLabel
instanceof $ ) {
8067 selectedLabel
= selectedLabel
.clone();
8070 this.setLabel( selectedLabel
);
8074 * Handle menu toggle events.
8077 * @param {boolean} isVisible Open state of the menu
8079 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
8080 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
8083 this.$element
.hasClass( 'oo-ui-dropdownWidget-open' ).toString()
8088 * Handle mouse click events.
8091 * @param {jQuery.Event} e Mouse click event
8092 * @return {undefined/boolean} False to prevent default if event is handled
8094 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
8095 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8102 * Handle key down events.
8105 * @param {jQuery.Event} e Key down event
8106 * @return {undefined/boolean} False to prevent default if event is handled
8108 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
8110 !this.isDisabled() &&
8112 e
.which
=== OO
.ui
.Keys
.ENTER
||
8114 e
.which
=== OO
.ui
.Keys
.SPACE
&&
8115 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
8116 // Space only closes the menu is the user is not typing to search.
8117 this.menu
.keyPressBuffer
=== ''
8120 !this.menu
.isVisible() &&
8122 e
.which
=== OO
.ui
.Keys
.UP
||
8123 e
.which
=== OO
.ui
.Keys
.DOWN
8134 * RadioOptionWidget is an option widget that looks like a radio button.
8135 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
8136 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8138 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8141 * @extends OO.ui.OptionWidget
8144 * @param {Object} [config] Configuration options
8146 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
8147 // Configuration initialization
8148 config
= config
|| {};
8150 // Properties (must be done before parent constructor which calls #setDisabled)
8151 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
8153 // Parent constructor
8154 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
8157 // Remove implicit role, we're handling it ourselves
8158 this.radio
.$input
.attr( 'role', 'presentation' );
8160 .addClass( 'oo-ui-radioOptionWidget' )
8161 .attr( 'role', 'radio' )
8162 .attr( 'aria-checked', 'false' )
8163 .removeAttr( 'aria-selected' )
8164 .prepend( this.radio
.$element
);
8169 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
8171 /* Static Properties */
8177 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
8183 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
8189 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
8195 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
8202 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
8203 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8205 this.radio
.setSelected( state
);
8207 .attr( 'aria-checked', state
.toString() )
8208 .removeAttr( 'aria-selected' );
8216 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
8217 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8219 this.radio
.setDisabled( this.isDisabled() );
8225 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8226 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8227 * an interface for adding, removing and selecting options.
8228 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8230 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8231 * OO.ui.RadioSelectInputWidget instead.
8234 * // A RadioSelectWidget with RadioOptions.
8235 * var option1 = new OO.ui.RadioOptionWidget( {
8237 * label: 'Selected radio option'
8240 * var option2 = new OO.ui.RadioOptionWidget( {
8242 * label: 'Unselected radio option'
8245 * var radioSelect=new OO.ui.RadioSelectWidget( {
8246 * items: [ option1, option2 ]
8249 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8250 * radioSelect.selectItem( option1 );
8252 * $( 'body' ).append( radioSelect.$element );
8254 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8258 * @extends OO.ui.SelectWidget
8259 * @mixins OO.ui.mixin.TabIndexedElement
8262 * @param {Object} [config] Configuration options
8264 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8265 // Parent constructor
8266 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8268 // Mixin constructors
8269 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8273 focus
: this.bindDocumentKeyDownListener
.bind( this ),
8274 blur
: this.unbindDocumentKeyDownListener
.bind( this )
8279 .addClass( 'oo-ui-radioSelectWidget' )
8280 .attr( 'role', 'radiogroup' );
8285 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8286 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8289 * MultioptionWidgets are special elements that can be selected and configured with data. The
8290 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8291 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8292 * and examples, please see the [OOUI documentation on MediaWiki][1].
8294 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Multioptions
8297 * @extends OO.ui.Widget
8298 * @mixins OO.ui.mixin.ItemWidget
8299 * @mixins OO.ui.mixin.LabelElement
8302 * @param {Object} [config] Configuration options
8303 * @cfg {boolean} [selected=false] Whether the option is initially selected
8305 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8306 // Configuration initialization
8307 config
= config
|| {};
8309 // Parent constructor
8310 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8312 // Mixin constructors
8313 OO
.ui
.mixin
.ItemWidget
.call( this );
8314 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8317 this.selected
= null;
8321 .addClass( 'oo-ui-multioptionWidget' )
8322 .append( this.$label
);
8323 this.setSelected( config
.selected
);
8328 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8329 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8330 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8337 * A change event is emitted when the selected state of the option changes.
8339 * @param {boolean} selected Whether the option is now selected
8345 * Check if the option is selected.
8347 * @return {boolean} Item is selected
8349 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8350 return this.selected
;
8354 * Set the option’s selected state. In general, all modifications to the selection
8355 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
8356 * method instead of this method.
8358 * @param {boolean} [state=false] Select option
8360 * @return {OO.ui.Widget} The widget, for chaining
8362 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8364 if ( this.selected
!== state
) {
8365 this.selected
= state
;
8366 this.emit( 'change', state
);
8367 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8373 * MultiselectWidget allows selecting multiple options from a list.
8375 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
8377 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8381 * @extends OO.ui.Widget
8382 * @mixins OO.ui.mixin.GroupWidget
8385 * @param {Object} [config] Configuration options
8386 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8388 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8389 // Parent constructor
8390 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8392 // Configuration initialization
8393 config
= config
|| {};
8395 // Mixin constructors
8396 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8399 this.aggregate( { change
: 'select' } );
8400 // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
8401 // by GroupElement only when items are added/removed
8402 this.connect( this, { select
: [ 'emit', 'change' ] } );
8405 if ( config
.items
) {
8406 this.addItems( config
.items
);
8408 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8409 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8410 .append( this.$group
);
8415 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8416 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8423 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8429 * A select event is emitted when an item is selected or deselected.
8435 * Find options that are selected.
8437 * @return {OO.ui.MultioptionWidget[]} Selected options
8439 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8440 return this.items
.filter( function ( item
) {
8441 return item
.isSelected();
8446 * Find the data of options that are selected.
8448 * @return {Object[]|string[]} Values of selected options
8450 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8451 return this.findSelectedItems().map( function ( item
) {
8457 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8459 * @param {OO.ui.MultioptionWidget[]} items Items to select
8461 * @return {OO.ui.Widget} The widget, for chaining
8463 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8464 this.items
.forEach( function ( item
) {
8465 var selected
= items
.indexOf( item
) !== -1;
8466 item
.setSelected( selected
);
8472 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8474 * @param {Object[]|string[]} datas Values of items to select
8476 * @return {OO.ui.Widget} The widget, for chaining
8478 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8481 items
= datas
.map( function ( data
) {
8482 return widget
.findItemFromData( data
);
8484 this.selectItems( items
);
8489 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8490 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8491 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8493 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8496 * @extends OO.ui.MultioptionWidget
8499 * @param {Object} [config] Configuration options
8501 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8502 // Configuration initialization
8503 config
= config
|| {};
8505 // Properties (must be done before parent constructor which calls #setDisabled)
8506 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8508 // Parent constructor
8509 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8512 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8513 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8517 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8518 .prepend( this.checkbox
.$element
);
8523 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8525 /* Static Properties */
8531 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8536 * Handle checkbox selected state change.
8540 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8541 this.setSelected( this.checkbox
.isSelected() );
8547 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8548 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8549 this.checkbox
.setSelected( state
);
8556 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8557 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8558 this.checkbox
.setDisabled( this.isDisabled() );
8565 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8566 this.checkbox
.focus();
8570 * Handle key down events.
8573 * @param {jQuery.Event} e
8575 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8577 element
= this.getElementGroup(),
8580 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8581 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8582 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8583 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8593 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8594 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8595 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8596 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8598 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8599 * OO.ui.CheckboxMultiselectInputWidget instead.
8602 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8603 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8606 * label: 'Selected checkbox'
8609 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
8611 * label: 'Unselected checkbox'
8614 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
8615 * items: [ option1, option2 ]
8618 * $( 'body' ).append( multiselect.$element );
8620 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8623 * @extends OO.ui.MultiselectWidget
8626 * @param {Object} [config] Configuration options
8628 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8629 // Parent constructor
8630 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8633 this.$lastClicked
= null;
8636 this.$group
.on( 'click', this.onClick
.bind( this ) );
8640 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8645 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8650 * Get an option by its position relative to the specified item (or to the start of the option array,
8651 * if item is `null`). The direction in which to search through the option array is specified with a
8652 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8653 * `null` if there are no options in the array.
8655 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8656 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8657 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8659 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8660 var currentIndex
, nextIndex
, i
,
8661 increase
= direction
> 0 ? 1 : -1,
8662 len
= this.items
.length
;
8665 currentIndex
= this.items
.indexOf( item
);
8666 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8668 // If no item is selected and moving forward, start at the beginning.
8669 // If moving backward, start at the end.
8670 nextIndex
= direction
> 0 ? 0 : len
- 1;
8673 for ( i
= 0; i
< len
; i
++ ) {
8674 item
= this.items
[ nextIndex
];
8675 if ( item
&& !item
.isDisabled() ) {
8678 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8684 * Handle click events on checkboxes.
8686 * @param {jQuery.Event} e
8688 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8689 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8690 $lastClicked
= this.$lastClicked
,
8691 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8692 .not( '.oo-ui-widget-disabled' );
8694 // Allow selecting multiple options at once by Shift-clicking them
8695 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8696 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8697 lastClickedIndex
= $options
.index( $lastClicked
);
8698 nowClickedIndex
= $options
.index( $nowClicked
);
8699 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8700 // browser. In either case we don't need custom handling.
8701 if ( nowClickedIndex
!== lastClickedIndex
) {
8703 wasSelected
= items
[ nowClickedIndex
].isSelected();
8704 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8706 // This depends on the DOM order of the items and the order of the .items array being the same.
8707 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8708 if ( !items
[ i
].isDisabled() ) {
8709 items
[ i
].setSelected( !wasSelected
);
8712 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8713 // handling first, then set our value. The order in which events happen is different for
8714 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8715 // non-click actions that change the checkboxes.
8717 setTimeout( function () {
8718 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8719 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8725 if ( $nowClicked
.length
) {
8726 this.$lastClicked
= $nowClicked
;
8734 * @return {OO.ui.Widget} The widget, for chaining
8736 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8738 if ( !this.isDisabled() ) {
8739 item
= this.getRelativeFocusableItem( null, 1 );
8750 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8755 * Progress bars visually display the status of an operation, such as a download,
8756 * and can be either determinate or indeterminate:
8758 * - **determinate** process bars show the percent of an operation that is complete.
8760 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8761 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8762 * not use percentages.
8764 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8767 * // Examples of determinate and indeterminate progress bars.
8768 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8771 * var progressBar2 = new OO.ui.ProgressBarWidget();
8773 * // Create a FieldsetLayout to layout progress bars
8774 * var fieldset = new OO.ui.FieldsetLayout;
8775 * fieldset.addItems( [
8776 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
8777 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
8779 * $( 'body' ).append( fieldset.$element );
8782 * @extends OO.ui.Widget
8785 * @param {Object} [config] Configuration options
8786 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8787 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
8788 * By default, the progress bar is indeterminate.
8790 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8791 // Configuration initialization
8792 config
= config
|| {};
8794 // Parent constructor
8795 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8798 this.$bar
= $( '<div>' );
8799 this.progress
= null;
8802 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8803 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8806 role
: 'progressbar',
8808 'aria-valuemax': 100
8810 .addClass( 'oo-ui-progressBarWidget' )
8811 .append( this.$bar
);
8816 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8818 /* Static Properties */
8824 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8829 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8831 * @return {number|boolean} Progress percent
8833 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8834 return this.progress
;
8838 * Set the percent of the process completed or `false` for an indeterminate process.
8840 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8842 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8843 this.progress
= progress
;
8845 if ( progress
!== false ) {
8846 this.$bar
.css( 'width', this.progress
+ '%' );
8847 this.$element
.attr( 'aria-valuenow', this.progress
);
8849 this.$bar
.css( 'width', '' );
8850 this.$element
.removeAttr( 'aria-valuenow' );
8852 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8856 * InputWidget is the base class for all input widgets, which
8857 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8858 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8859 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
8861 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
8865 * @extends OO.ui.Widget
8866 * @mixins OO.ui.mixin.FlaggedElement
8867 * @mixins OO.ui.mixin.TabIndexedElement
8868 * @mixins OO.ui.mixin.TitledElement
8869 * @mixins OO.ui.mixin.AccessKeyedElement
8872 * @param {Object} [config] Configuration options
8873 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8874 * @cfg {string} [value=''] The value of the input.
8875 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8876 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8877 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8878 * before it is accepted.
8880 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8881 // Configuration initialization
8882 config
= config
|| {};
8884 // Parent constructor
8885 OO
.ui
.InputWidget
.parent
.call( this, config
);
8888 // See #reusePreInfuseDOM about config.$input
8889 this.$input
= config
.$input
|| this.getInputElement( config
);
8891 this.inputFilter
= config
.inputFilter
;
8893 // Mixin constructors
8894 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8895 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8896 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8897 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8900 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8904 .addClass( 'oo-ui-inputWidget-input' )
8905 .attr( 'name', config
.name
)
8906 .prop( 'disabled', this.isDisabled() );
8908 .addClass( 'oo-ui-inputWidget' )
8909 .append( this.$input
);
8910 this.setValue( config
.value
);
8912 this.setDir( config
.dir
);
8914 if ( config
.inputId
!== undefined ) {
8915 this.setInputId( config
.inputId
);
8921 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8922 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8923 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8924 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8925 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8927 /* Static Methods */
8932 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8933 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8934 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8935 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8942 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8943 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8944 if ( config
.$input
&& config
.$input
.length
) {
8945 state
.value
= config
.$input
.val();
8946 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8947 state
.focus
= config
.$input
.is( ':focus' );
8957 * A change event is emitted when the value of the input changes.
8959 * @param {string} value
8965 * Get input element.
8967 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
8968 * different circumstances. The element must have a `value` property (like form elements).
8971 * @param {Object} config Configuration options
8972 * @return {jQuery} Input element
8974 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
8975 return $( '<input>' );
8979 * Handle potentially value-changing events.
8982 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8984 OO
.ui
.InputWidget
.prototype.onEdit = function () {
8986 if ( !this.isDisabled() ) {
8987 // Allow the stack to clear so the value will be updated
8988 setTimeout( function () {
8989 widget
.setValue( widget
.$input
.val() );
8995 * Get the value of the input.
8997 * @return {string} Input value
8999 OO
.ui
.InputWidget
.prototype.getValue = function () {
9000 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9001 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9002 var value
= this.$input
.val();
9003 if ( this.value
!== value
) {
9004 this.setValue( value
);
9010 * Set the directionality of the input.
9012 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
9014 * @return {OO.ui.Widget} The widget, for chaining
9016 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
9017 this.$input
.prop( 'dir', dir
);
9022 * Set the value of the input.
9024 * @param {string} value New value
9027 * @return {OO.ui.Widget} The widget, for chaining
9029 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
9030 value
= this.cleanUpValue( value
);
9031 // Update the DOM if it has changed. Note that with cleanUpValue, it
9032 // is possible for the DOM value to change without this.value changing.
9033 if ( this.$input
.val() !== value
) {
9034 this.$input
.val( value
);
9036 if ( this.value
!== value
) {
9038 this.emit( 'change', this.value
);
9040 // The first time that the value is set (probably while constructing the widget),
9041 // remember it in defaultValue. This property can be later used to check whether
9042 // the value of the input has been changed since it was created.
9043 if ( this.defaultValue
=== undefined ) {
9044 this.defaultValue
= this.value
;
9045 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
9051 * Clean up incoming value.
9053 * Ensures value is a string, and converts undefined and null to empty string.
9056 * @param {string} value Original value
9057 * @return {string} Cleaned up value
9059 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
9060 if ( value
=== undefined || value
=== null ) {
9062 } else if ( this.inputFilter
) {
9063 return this.inputFilter( String( value
) );
9065 return String( value
);
9072 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
9073 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9074 if ( this.$input
) {
9075 this.$input
.prop( 'disabled', this.isDisabled() );
9081 * Set the 'id' attribute of the `<input>` element.
9083 * @param {string} id
9085 * @return {OO.ui.Widget} The widget, for chaining
9087 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
9088 this.$input
.attr( 'id', id
);
9095 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
9096 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9097 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
9098 this.setValue( state
.value
);
9100 if ( state
.focus
) {
9106 * Data widget intended for creating 'hidden'-type inputs.
9109 * @extends OO.ui.Widget
9112 * @param {Object} [config] Configuration options
9113 * @cfg {string} [value=''] The value of the input.
9114 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9116 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
9117 // Configuration initialization
9118 config
= $.extend( { value
: '', name
: '' }, config
);
9120 // Parent constructor
9121 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
9124 this.$element
.attr( {
9126 value
: config
.value
,
9129 this.$element
.removeAttr( 'aria-disabled' );
9134 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
9136 /* Static Properties */
9142 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
9145 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
9146 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
9147 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
9148 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
9149 * [OOUI documentation on MediaWiki] [1] for more information.
9152 * // A ButtonInputWidget rendered as an HTML button, the default.
9153 * var button = new OO.ui.ButtonInputWidget( {
9154 * label: 'Input button',
9158 * $( 'body' ).append( button.$element );
9160 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
9163 * @extends OO.ui.InputWidget
9164 * @mixins OO.ui.mixin.ButtonElement
9165 * @mixins OO.ui.mixin.IconElement
9166 * @mixins OO.ui.mixin.IndicatorElement
9167 * @mixins OO.ui.mixin.LabelElement
9168 * @mixins OO.ui.mixin.TitledElement
9171 * @param {Object} [config] Configuration options
9172 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
9173 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
9174 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
9175 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
9176 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
9178 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
9179 // Configuration initialization
9180 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
9182 // See InputWidget#reusePreInfuseDOM about config.$input
9183 if ( config
.$input
) {
9184 config
.$input
.empty();
9187 // Properties (must be set before parent constructor, which calls #setValue)
9188 this.useInputTag
= config
.useInputTag
;
9190 // Parent constructor
9191 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
9193 // Mixin constructors
9194 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
9195 OO
.ui
.mixin
.IconElement
.call( this, config
);
9196 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9197 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9198 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
9201 if ( !config
.useInputTag
) {
9202 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
9204 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
9209 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
9210 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
9211 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
9212 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9213 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
9214 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
9216 /* Static Properties */
9222 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
9230 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9232 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9233 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9239 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9241 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9242 * text, or `null` for no label
9244 * @return {OO.ui.Widget} The widget, for chaining
9246 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9247 if ( typeof label
=== 'function' ) {
9248 label
= OO
.ui
.resolveMsg( label
);
9251 if ( this.useInputTag
) {
9252 // Discard non-plaintext labels
9253 if ( typeof label
!== 'string' ) {
9257 this.$input
.val( label
);
9260 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9264 * Set the value of the input.
9266 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9267 * they do not support {@link #value values}.
9269 * @param {string} value New value
9271 * @return {OO.ui.Widget} The widget, for chaining
9273 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9274 if ( !this.useInputTag
) {
9275 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9283 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9284 // Disable generating `<label>` elements for buttons. One would very rarely need additional label
9285 // for a button, and it's already a big clickable target, and it causes unexpected rendering.
9290 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9291 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9292 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9293 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9295 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9298 * // An example of selected, unselected, and disabled checkbox inputs
9299 * var checkbox1=new OO.ui.CheckboxInputWidget( {
9303 * var checkbox2=new OO.ui.CheckboxInputWidget( {
9306 * var checkbox3=new OO.ui.CheckboxInputWidget( {
9310 * // Create a fieldset layout with fields for each checkbox.
9311 * var fieldset = new OO.ui.FieldsetLayout( {
9312 * label: 'Checkboxes'
9314 * fieldset.addItems( [
9315 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9316 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9317 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9319 * $( 'body' ).append( fieldset.$element );
9321 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9324 * @extends OO.ui.InputWidget
9327 * @param {Object} [config] Configuration options
9328 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
9330 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9331 // Configuration initialization
9332 config
= config
|| {};
9334 // Parent constructor
9335 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9338 this.checkIcon
= new OO
.ui
.IconWidget( {
9340 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9345 .addClass( 'oo-ui-checkboxInputWidget' )
9346 // Required for pretty styling in WikimediaUI theme
9347 .append( this.checkIcon
.$element
);
9348 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9353 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9355 /* Static Properties */
9361 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9363 /* Static Methods */
9368 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9369 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9370 state
.checked
= config
.$input
.prop( 'checked' );
9380 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9381 return $( '<input>' ).attr( 'type', 'checkbox' );
9387 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9389 if ( !this.isDisabled() ) {
9390 // Allow the stack to clear so the value will be updated
9391 setTimeout( function () {
9392 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9398 * Set selection state of this checkbox.
9400 * @param {boolean} state `true` for selected
9402 * @return {OO.ui.Widget} The widget, for chaining
9404 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
9406 if ( this.selected
!== state
) {
9407 this.selected
= state
;
9408 this.$input
.prop( 'checked', this.selected
);
9409 this.emit( 'change', this.selected
);
9411 // The first time that the selection state is set (probably while constructing the widget),
9412 // remember it in defaultSelected. This property can be later used to check whether
9413 // the selection state of the input has been changed since it was created.
9414 if ( this.defaultSelected
=== undefined ) {
9415 this.defaultSelected
= this.selected
;
9416 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9422 * Check if this checkbox is selected.
9424 * @return {boolean} Checkbox is selected
9426 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9427 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9428 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9429 var selected
= this.$input
.prop( 'checked' );
9430 if ( this.selected
!== selected
) {
9431 this.setSelected( selected
);
9433 return this.selected
;
9439 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9440 if ( !this.isDisabled() ) {
9441 this.$input
.click();
9449 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9450 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9451 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9452 this.setSelected( state
.checked
);
9457 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9458 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9459 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9460 * more information about input widgets.
9462 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9463 * are no options. If no `value` configuration option is provided, the first option is selected.
9464 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9466 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
9469 * // Example: A DropdownInputWidget with three options
9470 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9472 * { data: 'a', label: 'First' },
9473 * { data: 'b', label: 'Second'},
9474 * { data: 'c', label: 'Third' }
9477 * $( 'body' ).append( dropdownInput.$element );
9479 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9482 * @extends OO.ui.InputWidget
9483 * @mixins OO.ui.mixin.TitledElement
9486 * @param {Object} [config] Configuration options
9487 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9488 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9489 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
9490 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
9491 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
9492 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9494 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9495 // Configuration initialization
9496 config
= config
|| {};
9498 // Properties (must be done before parent constructor which calls #setDisabled)
9499 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9501 $overlay
: config
.$overlay
9505 // Set up the options before parent constructor, which uses them to validate config.value.
9506 // Use this instead of setOptions() because this.$input is not set up yet.
9507 this.setOptionsData( config
.options
|| [] );
9509 // Mixin constructors
9510 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.dropdownWidget
.$handle
} ) );
9512 // Parent constructor
9513 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9516 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
9520 .addClass( 'oo-ui-dropdownInputWidget' )
9521 .append( this.dropdownWidget
.$element
);
9522 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9527 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9528 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
9536 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9537 return $( '<select>' );
9541 * Handles menu select events.
9544 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9546 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9547 this.setValue( item
? item
.getData() : '' );
9553 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9555 value
= this.cleanUpValue( value
);
9556 // Only allow setting values that are actually present in the dropdown
9557 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9558 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9559 this.dropdownWidget
.getMenu().selectItem( selected
);
9560 value
= selected
? selected
.getData() : '';
9561 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9562 if ( this.optionsDirty
) {
9563 // We reached this from the constructor or from #setOptions.
9564 // We have to update the <select> element.
9565 this.updateOptionsInterface();
9573 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9574 this.dropdownWidget
.setDisabled( state
);
9575 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9580 * Set the options available for this input.
9582 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9584 * @return {OO.ui.Widget} The widget, for chaining
9586 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9587 var value
= this.getValue();
9589 this.setOptionsData( options
);
9591 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9592 // In case the previous value is no longer an available option, select the first valid one.
9593 this.setValue( value
);
9599 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9601 * This method may be called before the parent constructor, so various properties may not be
9604 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9607 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9612 this.optionsDirty
= true;
9614 optionWidgets
= options
.map( function ( opt
) {
9617 if ( opt
.optgroup
!== undefined ) {
9618 return widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9621 optValue
= widget
.cleanUpValue( opt
.data
);
9622 return widget
.createMenuOptionWidget(
9624 opt
.label
!== undefined ? opt
.label
: optValue
9629 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9633 * Create a menu option widget.
9636 * @param {string} data Item data
9637 * @param {string} label Item label
9638 * @return {OO.ui.MenuOptionWidget} Option widget
9640 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9641 return new OO
.ui
.MenuOptionWidget( {
9648 * Create a menu section option widget.
9651 * @param {string} label Section item label
9652 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9654 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9655 return new OO
.ui
.MenuSectionOptionWidget( {
9661 * Update the user-visible interface to match the internal list of options and value.
9663 * This method must only be called after the parent constructor.
9667 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9669 $optionsContainer
= this.$input
,
9670 defaultValue
= this.defaultValue
,
9673 this.$input
.empty();
9675 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9678 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9679 $optionNode
= $( '<option>' )
9680 .attr( 'value', optionWidget
.getData() )
9681 .text( optionWidget
.getLabel() );
9683 // Remember original selection state. This property can be later used to check whether
9684 // the selection state of the input has been changed since it was created.
9685 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9687 $optionsContainer
.append( $optionNode
);
9689 $optionNode
= $( '<optgroup>' )
9690 .attr( 'label', optionWidget
.getLabel() );
9691 widget
.$input
.append( $optionNode
);
9692 $optionsContainer
= $optionNode
;
9696 this.optionsDirty
= false;
9702 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9703 this.dropdownWidget
.focus();
9710 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9711 this.dropdownWidget
.blur();
9716 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9717 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9718 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9719 * please see the [OOUI documentation on MediaWiki][1].
9721 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9724 * // An example of selected, unselected, and disabled radio inputs
9725 * var radio1 = new OO.ui.RadioInputWidget( {
9729 * var radio2 = new OO.ui.RadioInputWidget( {
9732 * var radio3 = new OO.ui.RadioInputWidget( {
9736 * // Create a fieldset layout with fields for each radio button.
9737 * var fieldset = new OO.ui.FieldsetLayout( {
9738 * label: 'Radio inputs'
9740 * fieldset.addItems( [
9741 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9742 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9743 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9745 * $( 'body' ).append( fieldset.$element );
9747 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9750 * @extends OO.ui.InputWidget
9753 * @param {Object} [config] Configuration options
9754 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9756 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9757 // Configuration initialization
9758 config
= config
|| {};
9760 // Parent constructor
9761 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9765 .addClass( 'oo-ui-radioInputWidget' )
9766 // Required for pretty styling in WikimediaUI theme
9767 .append( $( '<span>' ) );
9768 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9773 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9775 /* Static Properties */
9781 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9783 /* Static Methods */
9788 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9789 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9790 state
.checked
= config
.$input
.prop( 'checked' );
9800 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9801 return $( '<input>' ).attr( 'type', 'radio' );
9807 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9808 // RadioInputWidget doesn't track its state.
9812 * Set selection state of this radio button.
9814 * @param {boolean} state `true` for selected
9816 * @return {OO.ui.Widget} The widget, for chaining
9818 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9819 // RadioInputWidget doesn't track its state.
9820 this.$input
.prop( 'checked', state
);
9821 // The first time that the selection state is set (probably while constructing the widget),
9822 // remember it in defaultSelected. This property can be later used to check whether
9823 // the selection state of the input has been changed since it was created.
9824 if ( this.defaultSelected
=== undefined ) {
9825 this.defaultSelected
= state
;
9826 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9832 * Check if this radio button is selected.
9834 * @return {boolean} Radio is selected
9836 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
9837 return this.$input
.prop( 'checked' );
9843 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
9844 if ( !this.isDisabled() ) {
9845 this.$input
.click();
9853 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9854 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9855 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9856 this.setSelected( state
.checked
);
9861 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9862 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9863 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9864 * more information about input widgets.
9866 * This and OO.ui.DropdownInputWidget support the same configuration options.
9869 * // Example: A RadioSelectInputWidget with three options
9870 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9872 * { data: 'a', label: 'First' },
9873 * { data: 'b', label: 'Second'},
9874 * { data: 'c', label: 'Third' }
9877 * $( 'body' ).append( radioSelectInput.$element );
9879 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9882 * @extends OO.ui.InputWidget
9885 * @param {Object} [config] Configuration options
9886 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9888 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
9889 // Configuration initialization
9890 config
= config
|| {};
9892 // Properties (must be done before parent constructor which calls #setDisabled)
9893 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
9894 // Set up the options before parent constructor, which uses them to validate config.value.
9895 // Use this instead of setOptions() because this.$input is not set up yet
9896 this.setOptionsData( config
.options
|| [] );
9898 // Parent constructor
9899 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
9902 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
9906 .addClass( 'oo-ui-radioSelectInputWidget' )
9907 .append( this.radioSelectWidget
.$element
);
9908 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
9913 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
9915 /* Static Methods */
9920 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9921 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9922 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9929 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9930 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9931 // Cannot reuse the `<input type=radio>` set
9932 delete config
.$input
;
9942 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
9943 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
9944 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
9945 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
9949 * Handles menu select events.
9952 * @param {OO.ui.RadioOptionWidget} item Selected menu item
9954 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
9955 this.setValue( item
.getData() );
9961 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
9963 value
= this.cleanUpValue( value
);
9964 // Only allow setting values that are actually present in the dropdown
9965 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
9966 this.radioSelectWidget
.findFirstSelectableItem();
9967 this.radioSelectWidget
.selectItem( selected
);
9968 value
= selected
? selected
.getData() : '';
9969 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9976 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
9977 this.radioSelectWidget
.setDisabled( state
);
9978 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9983 * Set the options available for this input.
9985 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9987 * @return {OO.ui.Widget} The widget, for chaining
9989 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
9990 var value
= this.getValue();
9992 this.setOptionsData( options
);
9994 // Re-set the value to update the visible interface (RadioSelectWidget).
9995 // In case the previous value is no longer an available option, select the first valid one.
9996 this.setValue( value
);
10002 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10004 * This method may be called before the parent constructor, so various properties may not be
10007 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10010 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10013 this.radioSelectWidget
10015 .addItems( options
.map( function ( opt
) {
10016 var optValue
= widget
.cleanUpValue( opt
.data
);
10017 return new OO
.ui
.RadioOptionWidget( {
10019 label
: opt
.label
!== undefined ? opt
.label
: optValue
10027 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10028 this.radioSelectWidget
.focus();
10035 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10036 this.radioSelectWidget
.blur();
10041 * CheckboxMultiselectInputWidget is a
10042 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10043 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10044 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10045 * more information about input widgets.
10048 * // Example: A CheckboxMultiselectInputWidget with three options
10049 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10051 * { data: 'a', label: 'First' },
10052 * { data: 'b', label: 'Second'},
10053 * { data: 'c', label: 'Third' }
10056 * $( 'body' ).append( multiselectInput.$element );
10058 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10061 * @extends OO.ui.InputWidget
10064 * @param {Object} [config] Configuration options
10065 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
10067 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10068 // Configuration initialization
10069 config
= config
|| {};
10071 // Properties (must be done before parent constructor which calls #setDisabled)
10072 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10073 // Must be set before the #setOptionsData call below
10074 this.inputName
= config
.name
;
10075 // Set up the options before parent constructor, which uses them to validate config.value.
10076 // Use this instead of setOptions() because this.$input is not set up yet
10077 this.setOptionsData( config
.options
|| [] );
10079 // Parent constructor
10080 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10083 this.checkboxMultiselectWidget
.connect( this, { select
: 'onCheckboxesSelect' } );
10087 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10088 .append( this.checkboxMultiselectWidget
.$element
);
10089 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10090 this.$input
.detach();
10095 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10097 /* Static Methods */
10102 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10103 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10104 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10105 .toArray().map( function ( el
) { return el
.value
; } );
10112 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10113 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10114 // Cannot reuse the `<input type=checkbox>` set
10115 delete config
.$input
;
10125 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10127 return $( '<unused>' );
10131 * Handles CheckboxMultiselectWidget select events.
10135 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10136 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10142 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10143 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10144 .toArray().map( function ( el
) { return el
.value
; } );
10145 if ( this.value
!== value
) {
10146 this.setValue( value
);
10154 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10155 value
= this.cleanUpValue( value
);
10156 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10157 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10158 if ( this.optionsDirty
) {
10159 // We reached this from the constructor or from #setOptions.
10160 // We have to update the <select> element.
10161 this.updateOptionsInterface();
10167 * Clean up incoming value.
10169 * @param {string[]} value Original value
10170 * @return {string[]} Cleaned up value
10172 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10173 var i
, singleValue
,
10175 if ( !Array
.isArray( value
) ) {
10178 for ( i
= 0; i
< value
.length
; i
++ ) {
10180 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
10181 // Remove options that we don't have here
10182 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10185 cleanValue
.push( singleValue
);
10193 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10194 this.checkboxMultiselectWidget
.setDisabled( state
);
10195 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10200 * Set the options available for this input.
10202 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
10204 * @return {OO.ui.Widget} The widget, for chaining
10206 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10207 var value
= this.getValue();
10209 this.setOptionsData( options
);
10211 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10212 // This will also get rid of any stale options that we just removed.
10213 this.setValue( value
);
10219 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10221 * This method may be called before the parent constructor, so various properties may not be
10224 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10227 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10230 this.optionsDirty
= true;
10232 this.checkboxMultiselectWidget
10234 .addItems( options
.map( function ( opt
) {
10235 var optValue
, item
, optDisabled
;
10237 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
10238 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10239 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10241 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10242 disabled
: optDisabled
10244 // Set the 'name' and 'value' for form submission
10245 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10246 item
.checkbox
.setValue( optValue
);
10252 * Update the user-visible interface to match the internal list of options and value.
10254 * This method must only be called after the parent constructor.
10258 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10259 var defaultValue
= this.defaultValue
;
10261 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10262 // Remember original selection state. This property can be later used to check whether
10263 // the selection state of the input has been changed since it was created.
10264 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10265 item
.checkbox
.defaultSelected
= isDefault
;
10266 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10269 this.optionsDirty
= false;
10275 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10276 this.checkboxMultiselectWidget
.focus();
10281 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10282 * size of the field as well as its presentation. In addition, these widgets can be configured
10283 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
10284 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
10285 * which modifies incoming values rather than validating them.
10286 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10288 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10291 * // Example of a text input widget
10292 * var textInput = new OO.ui.TextInputWidget( {
10293 * value: 'Text input'
10295 * $( 'body' ).append( textInput.$element );
10297 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10300 * @extends OO.ui.InputWidget
10301 * @mixins OO.ui.mixin.IconElement
10302 * @mixins OO.ui.mixin.IndicatorElement
10303 * @mixins OO.ui.mixin.PendingElement
10304 * @mixins OO.ui.mixin.LabelElement
10307 * @param {Object} [config] Configuration options
10308 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10309 * 'email', 'url' or 'number'.
10310 * @cfg {string} [placeholder] Placeholder text
10311 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10312 * instruct the browser to focus this widget.
10313 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10314 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10316 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10317 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10318 * many emojis) count as 2 characters each.
10319 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10320 * the value or placeholder text: `'before'` or `'after'`
10321 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator: 'required'`.
10322 * Note that `false` & setting `indicator: 'required' will result in no indicator shown.
10323 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10324 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined` means
10325 * leaving it up to the browser).
10326 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10327 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10328 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10329 * value for it to be considered valid; when Function, a function receiving the value as parameter
10330 * that must return true, or promise resolving to true, for it to be considered valid.
10332 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10333 // Configuration initialization
10334 config
= $.extend( {
10336 labelPosition
: 'after'
10339 // Parent constructor
10340 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10342 // Mixin constructors
10343 OO
.ui
.mixin
.IconElement
.call( this, config
);
10344 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10345 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
10346 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10349 this.type
= this.getSaneType( config
);
10350 this.readOnly
= false;
10351 this.required
= false;
10352 this.validate
= null;
10353 this.styleHeight
= null;
10354 this.scrollWidth
= null;
10356 this.setValidation( config
.validate
);
10357 this.setLabelPosition( config
.labelPosition
);
10361 keypress
: this.onKeyPress
.bind( this ),
10362 blur
: this.onBlur
.bind( this ),
10363 focus
: this.onFocus
.bind( this )
10365 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10366 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10367 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10368 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10372 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10373 .append( this.$icon
, this.$indicator
);
10374 this.setReadOnly( !!config
.readOnly
);
10375 this.setRequired( !!config
.required
);
10376 if ( config
.placeholder
!== undefined ) {
10377 this.$input
.attr( 'placeholder', config
.placeholder
);
10379 if ( config
.maxLength
!== undefined ) {
10380 this.$input
.attr( 'maxlength', config
.maxLength
);
10382 if ( config
.autofocus
) {
10383 this.$input
.attr( 'autofocus', 'autofocus' );
10385 if ( config
.autocomplete
=== false ) {
10386 this.$input
.attr( 'autocomplete', 'off' );
10387 // Turning off autocompletion also disables "form caching" when the user navigates to a
10388 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
10390 beforeunload: function () {
10391 this.$input
.removeAttr( 'autocomplete' );
10393 pageshow: function () {
10394 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
10395 // whole page... it shouldn't hurt, though.
10396 this.$input
.attr( 'autocomplete', 'off' );
10400 if ( config
.spellcheck
!== undefined ) {
10401 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10403 if ( this.label
) {
10404 this.isWaitingToBeAttached
= true;
10405 this.installParentChangeDetector();
10411 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10412 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10413 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10414 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10415 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10417 /* Static Properties */
10419 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10427 * An `enter` event is emitted when the user presses 'enter' inside the text box.
10435 * Handle icon mouse down events.
10438 * @param {jQuery.Event} e Mouse down event
10439 * @return {undefined/boolean} False to prevent default if event is handled
10441 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10442 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10449 * Handle indicator mouse down events.
10452 * @param {jQuery.Event} e Mouse down event
10453 * @return {undefined/boolean} False to prevent default if event is handled
10455 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10456 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10463 * Handle key press events.
10466 * @param {jQuery.Event} e Key press event
10467 * @fires enter If enter key is pressed
10469 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10470 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10471 this.emit( 'enter', e
);
10476 * Handle blur events.
10479 * @param {jQuery.Event} e Blur event
10481 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10482 this.setValidityFlag();
10486 * Handle focus events.
10489 * @param {jQuery.Event} e Focus event
10491 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10492 if ( this.isWaitingToBeAttached
) {
10493 // If we've received focus, then we must be attached to the document, and if
10494 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10495 this.onElementAttach();
10497 this.setValidityFlag( true );
10501 * Handle element attach events.
10504 * @param {jQuery.Event} e Element attach event
10506 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10507 this.isWaitingToBeAttached
= false;
10508 // Any previously calculated size is now probably invalid if we reattached elsewhere
10509 this.valCache
= null;
10510 this.positionLabel();
10514 * Handle debounced change events.
10516 * @param {string} value
10519 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10520 this.setValidityFlag();
10524 * Check if the input is {@link #readOnly read-only}.
10526 * @return {boolean}
10528 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10529 return this.readOnly
;
10533 * Set the {@link #readOnly read-only} state of the input.
10535 * @param {boolean} state Make input read-only
10537 * @return {OO.ui.Widget} The widget, for chaining
10539 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10540 this.readOnly
= !!state
;
10541 this.$input
.prop( 'readOnly', this.readOnly
);
10546 * Check if the input is {@link #required required}.
10548 * @return {boolean}
10550 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10551 return this.required
;
10555 * Set the {@link #required required} state of the input.
10557 * @param {boolean} state Make input required
10559 * @return {OO.ui.Widget} The widget, for chaining
10561 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10562 this.required
= !!state
;
10563 if ( this.required
) {
10565 .prop( 'required', true )
10566 .attr( 'aria-required', 'true' );
10567 if ( this.getIndicator() === null ) {
10568 this.setIndicator( 'required' );
10572 .prop( 'required', false )
10573 .removeAttr( 'aria-required' );
10574 if ( this.getIndicator() === 'required' ) {
10575 this.setIndicator( null );
10582 * Support function for making #onElementAttach work across browsers.
10584 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10585 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10587 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10588 * first time that the element gets attached to the documented.
10590 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10591 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10592 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
10595 if ( MutationObserver
) {
10596 // The new way. If only it wasn't so ugly.
10598 if ( this.isElementAttached() ) {
10599 // Widget is attached already, do nothing. This breaks the functionality of this function when
10600 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
10601 // would require observation of the whole document, which would hurt performance of other,
10602 // more important code.
10606 // Find topmost node in the tree
10607 topmostNode
= this.$element
[ 0 ];
10608 while ( topmostNode
.parentNode
) {
10609 topmostNode
= topmostNode
.parentNode
;
10612 // We have no way to detect the $element being attached somewhere without observing the entire
10613 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
10614 // parent node of $element, and instead detect when $element is removed from it (and thus
10615 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
10616 // doesn't get attached, we end up back here and create the parent.
10618 mutationObserver
= new MutationObserver( function ( mutations
) {
10619 var i
, j
, removedNodes
;
10620 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10621 removedNodes
= mutations
[ i
].removedNodes
;
10622 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10623 if ( removedNodes
[ j
] === topmostNode
) {
10624 setTimeout( onRemove
, 0 );
10631 onRemove = function () {
10632 // If the node was attached somewhere else, report it
10633 if ( widget
.isElementAttached() ) {
10634 widget
.onElementAttach();
10636 mutationObserver
.disconnect();
10637 widget
.installParentChangeDetector();
10640 // Create a fake parent and observe it
10641 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10642 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10644 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10645 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10646 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10654 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10655 if ( this.getSaneType( config
) === 'number' ) {
10656 return $( '<input>' )
10657 .attr( 'step', 'any' )
10658 .attr( 'type', 'number' );
10660 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10665 * Get sanitized value for 'type' for given config.
10667 * @param {Object} config Configuration options
10668 * @return {string|null}
10671 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10672 var allowedTypes
= [
10679 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10683 * Focus the input and select a specified range within the text.
10685 * @param {number} from Select from offset
10686 * @param {number} [to] Select to offset, defaults to from
10688 * @return {OO.ui.Widget} The widget, for chaining
10690 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10691 var isBackwards
, start
, end
,
10692 input
= this.$input
[ 0 ];
10696 isBackwards
= to
< from;
10697 start
= isBackwards
? to
: from;
10698 end
= isBackwards
? from : to
;
10703 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10705 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10706 // Rather than expensively check if the input is attached every time, just check
10707 // if it was the cause of an error being thrown. If not, rethrow the error.
10708 if ( this.getElementDocument().body
.contains( input
) ) {
10716 * Get an object describing the current selection range in a directional manner
10718 * @return {Object} Object containing 'from' and 'to' offsets
10720 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10721 var input
= this.$input
[ 0 ],
10722 start
= input
.selectionStart
,
10723 end
= input
.selectionEnd
,
10724 isBackwards
= input
.selectionDirection
=== 'backward';
10727 from: isBackwards
? end
: start
,
10728 to
: isBackwards
? start
: end
10733 * Get the length of the text input value.
10735 * This could differ from the length of #getValue if the
10736 * value gets filtered
10738 * @return {number} Input length
10740 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10741 return this.$input
[ 0 ].value
.length
;
10745 * Focus the input and select the entire text.
10748 * @return {OO.ui.Widget} The widget, for chaining
10750 OO
.ui
.TextInputWidget
.prototype.select = function () {
10751 return this.selectRange( 0, this.getInputLength() );
10755 * Focus the input and move the cursor to the start.
10758 * @return {OO.ui.Widget} The widget, for chaining
10760 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10761 return this.selectRange( 0 );
10765 * Focus the input and move the cursor to the end.
10768 * @return {OO.ui.Widget} The widget, for chaining
10770 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10771 return this.selectRange( this.getInputLength() );
10775 * Insert new content into the input.
10777 * @param {string} content Content to be inserted
10779 * @return {OO.ui.Widget} The widget, for chaining
10781 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10783 range
= this.getRange(),
10784 value
= this.getValue();
10786 start
= Math
.min( range
.from, range
.to
);
10787 end
= Math
.max( range
.from, range
.to
);
10789 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10790 this.selectRange( start
+ content
.length
);
10795 * Insert new content either side of a selection.
10797 * @param {string} pre Content to be inserted before the selection
10798 * @param {string} post Content to be inserted after the selection
10800 * @return {OO.ui.Widget} The widget, for chaining
10802 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10804 range
= this.getRange(),
10805 offset
= pre
.length
;
10807 start
= Math
.min( range
.from, range
.to
);
10808 end
= Math
.max( range
.from, range
.to
);
10810 this.selectRange( start
).insertContent( pre
);
10811 this.selectRange( offset
+ end
).insertContent( post
);
10813 this.selectRange( offset
+ start
, offset
+ end
);
10818 * Set the validation pattern.
10820 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10821 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10822 * value must contain only numbers).
10824 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10825 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10827 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
10828 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
10829 this.validate
= validate
;
10831 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
10836 * Sets the 'invalid' flag appropriately.
10838 * @param {boolean} [isValid] Optionally override validation result
10840 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
10842 setFlag = function ( valid
) {
10844 widget
.$input
.attr( 'aria-invalid', 'true' );
10846 widget
.$input
.removeAttr( 'aria-invalid' );
10848 widget
.setFlags( { invalid
: !valid
} );
10851 if ( isValid
!== undefined ) {
10852 setFlag( isValid
);
10854 this.getValidity().then( function () {
10863 * Get the validity of current value.
10865 * This method returns a promise that resolves if the value is valid and rejects if
10866 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
10868 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10870 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
10873 function rejectOrResolve( valid
) {
10875 return $.Deferred().resolve().promise();
10877 return $.Deferred().reject().promise();
10881 // Check browser validity and reject if it is invalid
10883 this.$input
[ 0 ].checkValidity
!== undefined &&
10884 this.$input
[ 0 ].checkValidity() === false
10886 return rejectOrResolve( false );
10889 // Run our checks if the browser thinks the field is valid
10890 if ( this.validate
instanceof Function
) {
10891 result
= this.validate( this.getValue() );
10892 if ( result
&& typeof result
.promise
=== 'function' ) {
10893 return result
.promise().then( function ( valid
) {
10894 return rejectOrResolve( valid
);
10897 return rejectOrResolve( result
);
10900 return rejectOrResolve( this.getValue().match( this.validate
) );
10905 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10907 * @param {string} labelPosition Label position, 'before' or 'after'
10909 * @return {OO.ui.Widget} The widget, for chaining
10911 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10912 this.labelPosition
= labelPosition
;
10913 if ( this.label
) {
10914 // If there is no label and we only change the position, #updatePosition is a no-op,
10915 // but it takes really a lot of work to do nothing.
10916 this.updatePosition();
10922 * Update the position of the inline label.
10924 * This method is called by #setLabelPosition, and can also be called on its own if
10925 * something causes the label to be mispositioned.
10928 * @return {OO.ui.Widget} The widget, for chaining
10930 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10931 var after
= this.labelPosition
=== 'after';
10934 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10935 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10937 this.valCache
= null;
10938 this.scrollWidth
= null;
10939 this.positionLabel();
10945 * Position the label by setting the correct padding on the input.
10949 * @return {OO.ui.Widget} The widget, for chaining
10951 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
10952 var after
, rtl
, property
, newCss
;
10954 if ( this.isWaitingToBeAttached
) {
10955 // #onElementAttach will be called soon, which calls this method
10960 'padding-right': '',
10964 if ( this.label
) {
10965 this.$element
.append( this.$label
);
10967 this.$label
.detach();
10968 // Clear old values if present
10969 this.$input
.css( newCss
);
10973 after
= this.labelPosition
=== 'after';
10974 rtl
= this.$element
.css( 'direction' ) === 'rtl';
10975 property
= after
=== rtl
? 'padding-left' : 'padding-right';
10977 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
10978 // We have to clear the padding on the other side, in case the element direction changed
10979 this.$input
.css( newCss
);
10986 * @extends OO.ui.TextInputWidget
10989 * @param {Object} [config] Configuration options
10991 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
10992 config
= $.extend( {
10996 // Parent constructor
10997 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11000 this.connect( this, {
11005 this.updateSearchIndicator();
11006 this.connect( this, {
11007 disable
: 'onDisable'
11013 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11021 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11028 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
11029 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11030 // Clear the text field
11031 this.setValue( '' );
11038 * Update the 'clear' indicator displayed on type: 'search' text
11039 * fields, hiding it when the field is already empty or when it's not
11042 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11043 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11044 this.setIndicator( null );
11046 this.setIndicator( 'clear' );
11051 * Handle change events.
11055 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11056 this.updateSearchIndicator();
11060 * Handle disable events.
11062 * @param {boolean} disabled Element is disabled
11065 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11066 this.updateSearchIndicator();
11072 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11073 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11074 this.updateSearchIndicator();
11080 * @extends OO.ui.TextInputWidget
11083 * @param {Object} [config] Configuration options
11084 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11085 * specifies minimum number of rows to display.
11086 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11087 * Use the #maxRows config to specify a maximum number of displayed rows.
11088 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11089 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11091 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11092 config
= $.extend( {
11095 // Parent constructor
11096 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11099 this.autosize
= !!config
.autosize
;
11100 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11101 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11103 // Clone for resizing
11104 if ( this.autosize
) {
11105 this.$clone
= this.$input
11107 .removeAttr( 'id' )
11108 .removeAttr( 'name' )
11109 .insertAfter( this.$input
)
11110 .attr( 'aria-hidden', 'true' )
11111 .addClass( 'oo-ui-element-hidden' );
11115 this.connect( this, {
11120 if ( config
.rows
) {
11121 this.$input
.attr( 'rows', config
.rows
);
11123 if ( this.autosize
) {
11124 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11125 this.isWaitingToBeAttached
= true;
11126 this.installParentChangeDetector();
11132 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11134 /* Static Methods */
11139 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11140 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11141 state
.scrollTop
= config
.$input
.scrollTop();
11150 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11151 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11156 * Handle change events.
11160 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11167 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11168 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11175 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11177 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11179 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11180 // Some platforms emit keycode 10 for ctrl+enter in a textarea
11183 this.emit( 'enter', e
);
11188 * Automatically adjust the size of the text input.
11190 * This only affects multiline inputs that are {@link #autosize autosized}.
11193 * @return {OO.ui.Widget} The widget, for chaining
11196 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11197 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11198 idealHeight
, newHeight
, scrollWidth
, property
;
11200 if ( this.$input
.val() !== this.valCache
) {
11201 if ( this.autosize
) {
11203 .val( this.$input
.val() )
11204 .attr( 'rows', this.minRows
)
11205 // Set inline height property to 0 to measure scroll height
11206 .css( 'height', 0 );
11208 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11210 this.valCache
= this.$input
.val();
11212 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11214 // Remove inline height property to measure natural heights
11215 this.$clone
.css( 'height', '' );
11216 innerHeight
= this.$clone
.innerHeight();
11217 outerHeight
= this.$clone
.outerHeight();
11219 // Measure max rows height
11221 .attr( 'rows', this.maxRows
)
11222 .css( 'height', 'auto' )
11224 maxInnerHeight
= this.$clone
.innerHeight();
11226 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11227 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11228 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11229 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11231 this.$clone
.addClass( 'oo-ui-element-hidden' );
11233 // Only apply inline height when expansion beyond natural height is needed
11234 // Use the difference between the inner and outer height as a buffer
11235 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11236 if ( newHeight
!== this.styleHeight
) {
11237 this.$input
.css( 'height', newHeight
);
11238 this.styleHeight
= newHeight
;
11239 this.emit( 'resize' );
11242 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11243 if ( scrollWidth
!== this.scrollWidth
) {
11244 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11246 this.$label
.css( { right
: '', left
: '' } );
11247 this.$indicator
.css( { right
: '', left
: '' } );
11249 if ( scrollWidth
) {
11250 this.$indicator
.css( property
, scrollWidth
);
11251 if ( this.labelPosition
=== 'after' ) {
11252 this.$label
.css( property
, scrollWidth
);
11256 this.scrollWidth
= scrollWidth
;
11257 this.positionLabel();
11267 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11268 return $( '<textarea>' );
11272 * Check if the input automatically adjusts its size.
11274 * @return {boolean}
11276 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11277 return !!this.autosize
;
11283 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11284 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11285 if ( state
.scrollTop
!== undefined ) {
11286 this.$input
.scrollTop( state
.scrollTop
);
11291 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11292 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11293 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11295 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11296 * option, that option will appear to be selected.
11297 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11300 * After the user chooses an option, its `data` will be used as a new value for the widget.
11301 * A `label` also can be specified for each option: if given, it will be shown instead of the
11302 * `data` in the dropdown menu.
11304 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11306 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
11309 * // Example: A ComboBoxInputWidget.
11310 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11311 * value: 'Option 1',
11313 * { data: 'Option 1' },
11314 * { data: 'Option 2' },
11315 * { data: 'Option 3' }
11318 * $( 'body' ).append( comboBox.$element );
11321 * // Example: A ComboBoxInputWidget with additional option labels.
11322 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11323 * value: 'Option 1',
11326 * data: 'Option 1',
11327 * label: 'Option One'
11330 * data: 'Option 2',
11331 * label: 'Option Two'
11334 * data: 'Option 3',
11335 * label: 'Option Three'
11339 * $( 'body' ).append( comboBox.$element );
11341 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11344 * @extends OO.ui.TextInputWidget
11347 * @param {Object} [config] Configuration options
11348 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11349 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11350 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11351 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11352 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11353 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11355 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11356 // Configuration initialization
11357 config
= $.extend( {
11358 autocomplete
: false
11361 // ComboBoxInputWidget shouldn't support `multiline`
11362 config
.multiline
= false;
11364 // See InputWidget#reusePreInfuseDOM about `config.$input`
11365 if ( config
.$input
) {
11366 config
.$input
.removeAttr( 'list' );
11369 // Parent constructor
11370 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11373 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11374 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11375 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11376 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11378 invisibleLabel
: true,
11379 disabled
: this.disabled
11381 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11385 $floatableContainer
: this.$element
,
11386 disabled
: this.isDisabled()
11392 this.connect( this, {
11393 change
: 'onInputChange',
11394 enter
: 'onInputEnter'
11396 this.dropdownButton
.connect( this, {
11397 click
: 'onDropdownButtonClick'
11399 this.menu
.connect( this, {
11400 choose
: 'onMenuChoose',
11401 add
: 'onMenuItemsChange',
11402 remove
: 'onMenuItemsChange',
11403 toggle
: 'onMenuToggle'
11407 this.$input
.attr( {
11409 'aria-expanded': 'false',
11410 'aria-owns': this.menu
.getElementId(),
11411 'aria-autocomplete': 'list'
11413 this.dropdownButton
.$button
.attr( {
11414 'aria-controls': this.menu
.getElementId()
11416 // Do not override options set via config.menu.items
11417 if ( config
.options
!== undefined ) {
11418 this.setOptions( config
.options
);
11420 this.$field
= $( '<div>' )
11421 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11422 .append( this.$input
, this.dropdownButton
.$element
);
11424 .addClass( 'oo-ui-comboBoxInputWidget' )
11425 .append( this.$field
);
11426 this.$overlay
.append( this.menu
.$element
);
11427 this.onMenuItemsChange();
11432 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11437 * Get the combobox's menu.
11439 * @return {OO.ui.MenuSelectWidget} Menu widget
11441 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11446 * Get the combobox's text input widget.
11448 * @return {OO.ui.TextInputWidget} Text input widget
11450 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11455 * Handle input change events.
11458 * @param {string} value New value
11460 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11461 var match
= this.menu
.findItemFromData( value
);
11463 this.menu
.selectItem( match
);
11464 if ( this.menu
.findHighlightedItem() ) {
11465 this.menu
.highlightItem( match
);
11468 if ( !this.isDisabled() ) {
11469 this.menu
.toggle( true );
11474 * Handle input enter events.
11478 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11479 if ( !this.isDisabled() ) {
11480 this.menu
.toggle( false );
11485 * Handle button click events.
11489 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11490 this.menu
.toggle();
11495 * Handle menu choose events.
11498 * @param {OO.ui.OptionWidget} item Chosen item
11500 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11501 this.setValue( item
.getData() );
11505 * Handle menu item change events.
11509 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11510 var match
= this.menu
.findItemFromData( this.getValue() );
11511 this.menu
.selectItem( match
);
11512 if ( this.menu
.findHighlightedItem() ) {
11513 this.menu
.highlightItem( match
);
11515 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11519 * Handle menu toggle events.
11522 * @param {boolean} isVisible Open state of the menu
11524 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11525 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11531 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
11533 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
11535 if ( this.dropdownButton
) {
11536 this.dropdownButton
.setDisabled( this.isDisabled() );
11539 this.menu
.setDisabled( this.isDisabled() );
11546 * Set the options available for this input.
11548 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11550 * @return {OO.ui.Widget} The widget, for chaining
11552 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11555 .addItems( options
.map( function ( opt
) {
11556 return new OO
.ui
.MenuOptionWidget( {
11558 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11566 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11567 * which is a widget that is specified by reference before any optional configuration settings.
11569 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
11571 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11572 * A left-alignment is used for forms with many fields.
11573 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11574 * A right-alignment is used for long but familiar forms which users tab through,
11575 * verifying the current field with a quick glance at the label.
11576 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11577 * that users fill out from top to bottom.
11578 * - **inline**: The label is placed after the field-widget and aligned to the left.
11579 * An inline-alignment is best used with checkboxes or radio buttons.
11581 * Help text can either be:
11583 * - accessed via a help icon that appears in the upper right corner of the rendered field layout, or
11584 * - shown as a subtle explanation below the label.
11586 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`. If it
11587 * is long or not essential, leave `helpInline` to its default, `false`.
11589 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11591 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11594 * @extends OO.ui.Layout
11595 * @mixins OO.ui.mixin.LabelElement
11596 * @mixins OO.ui.mixin.TitledElement
11599 * @param {OO.ui.Widget} fieldWidget Field widget
11600 * @param {Object} [config] Configuration options
11601 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11603 * @cfg {Array} [errors] Error messages about the widget, which will be
11604 * displayed below the widget.
11605 * The array may contain strings or OO.ui.HtmlSnippet instances.
11606 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11607 * below the widget.
11608 * The array may contain strings or OO.ui.HtmlSnippet instances.
11609 * These are more visible than `help` messages when `helpInline` is set, and so
11610 * might be good for transient messages.
11611 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
11612 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
11613 * corner of the rendered field; clicking it will display the text in a popup.
11614 * If `helpInline` is `true`, then a subtle description will be shown after the
11616 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
11617 * or shown when the "help" icon is clicked.
11618 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
11620 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11622 * @throws {Error} An error is thrown if no widget is specified
11624 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11625 // Allow passing positional parameters inside the config object
11626 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11627 config
= fieldWidget
;
11628 fieldWidget
= config
.fieldWidget
;
11631 // Make sure we have required constructor arguments
11632 if ( fieldWidget
=== undefined ) {
11633 throw new Error( 'Widget not found' );
11636 // Configuration initialization
11637 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
11639 // Parent constructor
11640 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11642 // Mixin constructors
11643 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
11644 $label
: $( '<label>' )
11646 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
11649 this.fieldWidget
= fieldWidget
;
11652 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11653 this.$messages
= $( '<ul>' );
11654 this.$header
= $( '<span>' );
11655 this.$body
= $( '<div>' );
11657 this.helpInline
= config
.helpInline
;
11660 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
11663 this.$help
= config
.help
?
11664 this.createHelpElement( config
.help
, config
.$overlay
) :
11666 if ( this.fieldWidget
.getInputId() ) {
11667 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11668 if ( this.helpInline
) {
11669 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
11672 this.$label
.on( 'click', function () {
11673 this.fieldWidget
.simulateLabelClick();
11675 if ( this.helpInline
) {
11676 this.$help
.on( 'click', function () {
11677 this.fieldWidget
.simulateLabelClick();
11682 .addClass( 'oo-ui-fieldLayout' )
11683 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11684 .append( this.$body
);
11685 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11686 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11687 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11689 .addClass( 'oo-ui-fieldLayout-field' )
11690 .append( this.fieldWidget
.$element
);
11692 this.setErrors( config
.errors
|| [] );
11693 this.setNotices( config
.notices
|| [] );
11694 this.setAlignment( config
.align
);
11695 // Call this again to take into account the widget's accessKey
11696 this.updateTitle();
11701 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11702 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11703 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
11708 * Handle field disable events.
11711 * @param {boolean} value Field is disabled
11713 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
11714 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11718 * Get the widget contained by the field.
11720 * @return {OO.ui.Widget} Field widget
11722 OO
.ui
.FieldLayout
.prototype.getField = function () {
11723 return this.fieldWidget
;
11727 * Return `true` if the given field widget can be used with `'inline'` alignment (see
11728 * #setAlignment). Return `false` if it can't or if this can't be determined.
11730 * @return {boolean}
11732 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11733 // This is very simplistic, but should be good enough.
11734 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11739 * @param {string} kind 'error' or 'notice'
11740 * @param {string|OO.ui.HtmlSnippet} text
11743 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11744 var $listItem
, $icon
, message
;
11745 $listItem
= $( '<li>' );
11746 if ( kind
=== 'error' ) {
11747 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11748 $listItem
.attr( 'role', 'alert' );
11749 } else if ( kind
=== 'notice' ) {
11750 $icon
= new OO
.ui
.IconWidget( { icon
: 'notice' } ).$element
;
11754 message
= new OO
.ui
.LabelWidget( { label
: text
} );
11756 .append( $icon
, message
.$element
)
11757 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
11762 * Set the field alignment mode.
11765 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
11767 * @return {OO.ui.BookletLayout} The layout, for chaining
11769 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
11770 if ( value
!== this.align
) {
11771 // Default to 'left'
11772 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
11776 if ( value
=== 'inline' && !this.isFieldInline() ) {
11779 // Reorder elements
11781 if ( this.helpInline
) {
11782 if ( value
=== 'top' ) {
11783 this.$header
.append( this.$label
);
11784 this.$body
.append( this.$header
, this.$field
, this.$help
);
11785 } else if ( value
=== 'inline' ) {
11786 this.$header
.append( this.$label
, this.$help
);
11787 this.$body
.append( this.$field
, this.$header
);
11789 this.$header
.append( this.$label
, this.$help
);
11790 this.$body
.append( this.$header
, this.$field
);
11793 if ( value
=== 'top' ) {
11794 this.$header
.append( this.$help
, this.$label
);
11795 this.$body
.append( this.$header
, this.$field
);
11796 } else if ( value
=== 'inline' ) {
11797 this.$header
.append( this.$help
, this.$label
);
11798 this.$body
.append( this.$field
, this.$header
);
11800 this.$header
.append( this.$label
);
11801 this.$body
.append( this.$header
, this.$help
, this.$field
);
11804 // Set classes. The following classes can be used here:
11805 // * oo-ui-fieldLayout-align-left
11806 // * oo-ui-fieldLayout-align-right
11807 // * oo-ui-fieldLayout-align-top
11808 // * oo-ui-fieldLayout-align-inline
11809 if ( this.align
) {
11810 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
11812 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
11813 this.align
= value
;
11820 * Set the list of error messages.
11822 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11823 * The array may contain strings or OO.ui.HtmlSnippet instances.
11825 * @return {OO.ui.BookletLayout} The layout, for chaining
11827 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
11828 this.errors
= errors
.slice();
11829 this.updateMessages();
11834 * Set the list of notice messages.
11836 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11837 * The array may contain strings or OO.ui.HtmlSnippet instances.
11839 * @return {OO.ui.BookletLayout} The layout, for chaining
11841 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
11842 this.notices
= notices
.slice();
11843 this.updateMessages();
11848 * Update the rendering of error and notice messages.
11852 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
11854 this.$messages
.empty();
11856 if ( this.errors
.length
|| this.notices
.length
) {
11857 this.$body
.after( this.$messages
);
11859 this.$messages
.remove();
11863 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
11864 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
11866 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
11867 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
11872 * Include information about the widget's accessKey in our title. TitledElement calls this method.
11873 * (This is a bit of a hack.)
11876 * @param {string} title Tooltip label for 'title' attribute
11879 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
11880 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
11881 return this.fieldWidget
.formatTitleWithAccessKey( title
);
11887 * Creates and returns the help element. Also sets the `aria-describedby`
11888 * attribute on the main element of the `fieldWidget`.
11891 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
11892 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
11893 * @return {jQuery} The element that should become `this.$help`.
11895 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
11896 var helpId
, helpWidget
;
11898 if ( this.helpInline
) {
11899 helpWidget
= new OO
.ui
.LabelWidget( {
11901 classes
: [ 'oo-ui-inline-help' ]
11904 helpId
= helpWidget
.getElementId();
11906 helpWidget
= new OO
.ui
.PopupButtonWidget( {
11907 $overlay
: $overlay
,
11911 classes
: [ 'oo-ui-fieldLayout-help' ],
11914 label
: OO
.ui
.msg( 'ooui-field-help' ),
11915 invisibleLabel
: true
11917 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
11918 helpWidget
.getPopup().$body
.html( help
.toString() );
11920 helpWidget
.getPopup().$body
.text( help
);
11923 helpId
= helpWidget
.getPopup().getBodyId();
11926 // Set the 'aria-describedby' attribute on the fieldWidget
11927 // Preference given to an input or a button
11929 this.fieldWidget
.$input
||
11930 this.fieldWidget
.$button
||
11931 this.fieldWidget
.$element
11932 ).attr( 'aria-describedby', helpId
);
11934 return helpWidget
.$element
;
11938 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
11939 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
11940 * is required and is specified before any optional configuration settings.
11942 * Labels can be aligned in one of four ways:
11944 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11945 * A left-alignment is used for forms with many fields.
11946 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11947 * A right-alignment is used for long but familiar forms which users tab through,
11948 * verifying the current field with a quick glance at the label.
11949 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11950 * that users fill out from top to bottom.
11951 * - **inline**: The label is placed after the field-widget and aligned to the left.
11952 * An inline-alignment is best used with checkboxes or radio buttons.
11954 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
11955 * text is specified.
11958 * // Example of an ActionFieldLayout
11959 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
11960 * new OO.ui.TextInputWidget( {
11961 * placeholder: 'Field widget'
11963 * new OO.ui.ButtonWidget( {
11967 * label: 'An ActionFieldLayout. This label is aligned top',
11969 * help: 'This is help text'
11973 * $( 'body' ).append( actionFieldLayout.$element );
11976 * @extends OO.ui.FieldLayout
11979 * @param {OO.ui.Widget} fieldWidget Field widget
11980 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
11981 * @param {Object} config
11983 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
11984 // Allow passing positional parameters inside the config object
11985 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11986 config
= fieldWidget
;
11987 fieldWidget
= config
.fieldWidget
;
11988 buttonWidget
= config
.buttonWidget
;
11991 // Parent constructor
11992 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
11995 this.buttonWidget
= buttonWidget
;
11996 this.$button
= $( '<span>' );
11997 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12001 .addClass( 'oo-ui-actionFieldLayout' );
12003 .addClass( 'oo-ui-actionFieldLayout-button' )
12004 .append( this.buttonWidget
.$element
);
12006 .addClass( 'oo-ui-actionFieldLayout-input' )
12007 .append( this.fieldWidget
.$element
);
12009 .append( this.$input
, this.$button
);
12014 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12017 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12018 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12019 * configured with a label as well. For more information and examples,
12020 * please see the [OOUI documentation on MediaWiki][1].
12023 * // Example of a fieldset layout
12024 * var input1 = new OO.ui.TextInputWidget( {
12025 * placeholder: 'A text input field'
12028 * var input2 = new OO.ui.TextInputWidget( {
12029 * placeholder: 'A text input field'
12032 * var fieldset = new OO.ui.FieldsetLayout( {
12033 * label: 'Example of a fieldset layout'
12036 * fieldset.addItems( [
12037 * new OO.ui.FieldLayout( input1, {
12038 * label: 'Field One'
12040 * new OO.ui.FieldLayout( input2, {
12041 * label: 'Field Two'
12044 * $( 'body' ).append( fieldset.$element );
12046 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12049 * @extends OO.ui.Layout
12050 * @mixins OO.ui.mixin.IconElement
12051 * @mixins OO.ui.mixin.LabelElement
12052 * @mixins OO.ui.mixin.GroupElement
12055 * @param {Object} [config] Configuration options
12056 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
12057 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
12058 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
12059 * For important messages, you are advised to use `notices`, as they are always shown.
12060 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12061 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12063 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12064 // Configuration initialization
12065 config
= config
|| {};
12067 // Parent constructor
12068 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12070 // Mixin constructors
12071 OO
.ui
.mixin
.IconElement
.call( this, config
);
12072 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12073 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12076 this.$header
= $( '<legend>' );
12077 if ( config
.help
) {
12078 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
12079 $overlay
: config
.$overlay
,
12083 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12086 label
: OO
.ui
.msg( 'ooui-field-help' ),
12087 invisibleLabel
: true
12089 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12090 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
12092 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
12094 this.$help
= this.popupButtonWidget
.$element
;
12096 this.$help
= $( [] );
12101 .addClass( 'oo-ui-fieldsetLayout-header' )
12102 .append( this.$icon
, this.$label
, this.$help
);
12103 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12105 .addClass( 'oo-ui-fieldsetLayout' )
12106 .prepend( this.$header
, this.$group
);
12107 if ( Array
.isArray( config
.items
) ) {
12108 this.addItems( config
.items
);
12114 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12115 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12116 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12117 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12119 /* Static Properties */
12125 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12128 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
12129 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
12130 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
12131 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12133 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12134 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12135 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12136 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12137 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12138 * often have simplified APIs to match the capabilities of HTML forms.
12139 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12141 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12142 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12145 * // Example of a form layout that wraps a fieldset layout
12146 * var input1 = new OO.ui.TextInputWidget( {
12147 * placeholder: 'Username'
12149 * var input2 = new OO.ui.TextInputWidget( {
12150 * placeholder: 'Password',
12153 * var submit = new OO.ui.ButtonInputWidget( {
12157 * var fieldset = new OO.ui.FieldsetLayout( {
12158 * label: 'A form layout'
12160 * fieldset.addItems( [
12161 * new OO.ui.FieldLayout( input1, {
12162 * label: 'Username',
12165 * new OO.ui.FieldLayout( input2, {
12166 * label: 'Password',
12169 * new OO.ui.FieldLayout( submit )
12171 * var form = new OO.ui.FormLayout( {
12172 * items: [ fieldset ],
12173 * action: '/api/formhandler',
12176 * $( 'body' ).append( form.$element );
12179 * @extends OO.ui.Layout
12180 * @mixins OO.ui.mixin.GroupElement
12183 * @param {Object} [config] Configuration options
12184 * @cfg {string} [method] HTML form `method` attribute
12185 * @cfg {string} [action] HTML form `action` attribute
12186 * @cfg {string} [enctype] HTML form `enctype` attribute
12187 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12189 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12192 // Configuration initialization
12193 config
= config
|| {};
12195 // Parent constructor
12196 OO
.ui
.FormLayout
.parent
.call( this, config
);
12198 // Mixin constructors
12199 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12202 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12204 // Make sure the action is safe
12205 action
= config
.action
;
12206 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12207 action
= './' + action
;
12212 .addClass( 'oo-ui-formLayout' )
12214 method
: config
.method
,
12216 enctype
: config
.enctype
12218 if ( Array
.isArray( config
.items
) ) {
12219 this.addItems( config
.items
);
12225 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12226 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12231 * A 'submit' event is emitted when the form is submitted.
12236 /* Static Properties */
12242 OO
.ui
.FormLayout
.static.tagName
= 'form';
12247 * Handle form submit events.
12250 * @param {jQuery.Event} e Submit event
12252 * @return {OO.ui.FormLayout} The layout, for chaining
12254 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12255 if ( this.emit( 'submit' ) ) {
12261 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
12262 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
12265 * // Example of a panel layout
12266 * var panel = new OO.ui.PanelLayout( {
12270 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12272 * $( 'body' ).append( panel.$element );
12275 * @extends OO.ui.Layout
12278 * @param {Object} [config] Configuration options
12279 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12280 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12281 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12282 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
12284 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12285 // Configuration initialization
12286 config
= $.extend( {
12293 // Parent constructor
12294 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12297 this.$element
.addClass( 'oo-ui-panelLayout' );
12298 if ( config
.scrollable
) {
12299 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12301 if ( config
.padded
) {
12302 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12304 if ( config
.expanded
) {
12305 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12307 if ( config
.framed
) {
12308 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12314 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12319 * Focus the panel layout
12321 * The default implementation just focuses the first focusable element in the panel
12323 OO
.ui
.PanelLayout
.prototype.focus = function () {
12324 OO
.ui
.findFocusable( this.$element
).focus();
12328 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12329 * items), with small margins between them. Convenient when you need to put a number of block-level
12330 * widgets on a single line next to each other.
12332 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12335 * // HorizontalLayout with a text input and a label
12336 * var layout = new OO.ui.HorizontalLayout( {
12338 * new OO.ui.LabelWidget( { label: 'Label' } ),
12339 * new OO.ui.TextInputWidget( { value: 'Text' } )
12342 * $( 'body' ).append( layout.$element );
12345 * @extends OO.ui.Layout
12346 * @mixins OO.ui.mixin.GroupElement
12349 * @param {Object} [config] Configuration options
12350 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12352 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12353 // Configuration initialization
12354 config
= config
|| {};
12356 // Parent constructor
12357 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12359 // Mixin constructors
12360 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12363 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12364 if ( Array
.isArray( config
.items
) ) {
12365 this.addItems( config
.items
);
12371 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12372 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12375 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12376 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12377 * (to adjust the value in increments) to allow the user to enter a number.
12380 * // Example: A NumberInputWidget.
12381 * var numberInput = new OO.ui.NumberInputWidget( {
12382 * label: 'NumberInputWidget',
12383 * input: { value: 5 },
12387 * $( 'body' ).append( numberInput.$element );
12390 * @extends OO.ui.TextInputWidget
12393 * @param {Object} [config] Configuration options
12394 * @cfg {Object} [minusButton] Configuration options to pass to the
12395 * {@link OO.ui.ButtonWidget decrementing button widget}.
12396 * @cfg {Object} [plusButton] Configuration options to pass to the
12397 * {@link OO.ui.ButtonWidget incrementing button widget}.
12398 * @cfg {number} [min=-Infinity] Minimum allowed value
12399 * @cfg {number} [max=Infinity] Maximum allowed value
12400 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12401 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12402 * Defaults to `step` if specified, otherwise `1`.
12403 * @cfg {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12404 * Defaults to 10 times `buttonStep`.
12405 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12407 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12408 var $field
= $( '<div>' )
12409 .addClass( 'oo-ui-numberInputWidget-field' );
12411 // Configuration initialization
12412 config
= $.extend( {
12418 // For backward compatibility
12419 $.extend( config
, config
.input
);
12422 // Parent constructor
12423 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12427 if ( config
.showButtons
) {
12428 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12430 disabled
: this.isDisabled(),
12432 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12437 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12438 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12440 disabled
: this.isDisabled(),
12442 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12447 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12452 keydown
: this.onKeyDown
.bind( this ),
12453 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12455 if ( config
.showButtons
) {
12456 this.plusButton
.connect( this, {
12457 click
: [ 'onButtonClick', +1 ]
12459 this.minusButton
.connect( this, {
12460 click
: [ 'onButtonClick', -1 ]
12465 $field
.append( this.$input
);
12466 if ( config
.showButtons
) {
12468 .prepend( this.minusButton
.$element
)
12469 .append( this.plusButton
.$element
);
12473 if ( config
.allowInteger
|| config
.isInteger
) {
12474 // Backward compatibility
12477 this.setRange( config
.min
, config
.max
);
12478 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
12479 // Set the validation method after we set step and range
12480 // so that it doesn't immediately call setValidityFlag
12481 this.setValidation( this.validateNumber
.bind( this ) );
12484 .addClass( 'oo-ui-numberInputWidget' )
12485 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12491 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12495 // Backward compatibility
12496 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12497 this.setStep( flag
? 1 : null );
12499 // Backward compatibility
12500 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12502 // Backward compatibility
12503 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12504 return this.step
=== 1;
12506 // Backward compatibility
12507 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12510 * Set the range of allowed values
12512 * @param {number} min Minimum allowed value
12513 * @param {number} max Maximum allowed value
12515 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12517 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12521 this.$input
.attr( 'min', this.min
);
12522 this.$input
.attr( 'max', this.max
);
12523 this.setValidityFlag();
12527 * Get the current range
12529 * @return {number[]} Minimum and maximum values
12531 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12532 return [ this.min
, this.max
];
12536 * Set the stepping deltas
12538 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12539 * Defaults to `step` if specified, otherwise `1`.
12540 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12541 * Defaults to 10 times `buttonStep`.
12542 * @param {number|null} [step] If specified, the field only accepts values that are multiples of this.
12544 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
12545 if ( buttonStep
=== undefined ) {
12546 buttonStep
= step
|| 1;
12548 if ( pageStep
=== undefined ) {
12549 pageStep
= 10 * buttonStep
;
12551 if ( step
!== null && step
<= 0 ) {
12552 throw new Error( 'Step value, if given, must be positive' );
12554 if ( buttonStep
<= 0 ) {
12555 throw new Error( 'Button step value must be positive' );
12557 if ( pageStep
<= 0 ) {
12558 throw new Error( 'Page step value must be positive' );
12561 this.buttonStep
= buttonStep
;
12562 this.pageStep
= pageStep
;
12563 this.$input
.attr( 'step', this.step
|| 'any' );
12564 this.setValidityFlag();
12570 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
12571 if ( value
=== '' ) {
12572 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
12573 // so here we make sure an 'empty' value is actually displayed as such.
12574 this.$input
.val( '' );
12576 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
12580 * Get the current stepping values
12582 * @return {number[]} Button step, page step, and validity step
12584 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
12585 return [ this.buttonStep
, this.pageStep
, this.step
];
12589 * Get the current value of the widget as a number
12591 * @return {number} May be NaN, or an invalid number
12593 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
12594 return +this.getValue();
12598 * Adjust the value of the widget
12600 * @param {number} delta Adjustment amount
12602 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
12603 var n
, v
= this.getNumericValue();
12606 if ( isNaN( delta
) || !isFinite( delta
) ) {
12607 throw new Error( 'Delta must be a finite number' );
12610 if ( isNaN( v
) ) {
12614 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
12616 n
= Math
.round( n
/ this.step
) * this.step
;
12621 this.setValue( n
);
12628 * @param {string} value Field value
12629 * @return {boolean}
12631 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
12633 if ( value
=== '' ) {
12634 return !this.isRequired();
12637 if ( isNaN( n
) || !isFinite( n
) ) {
12641 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
12645 if ( n
< this.min
|| n
> this.max
) {
12653 * Handle mouse click events.
12656 * @param {number} dir +1 or -1
12658 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
12659 this.adjustValue( dir
* this.buttonStep
);
12663 * Handle mouse wheel events.
12666 * @param {jQuery.Event} event
12667 * @return {undefined/boolean} False to prevent default if event is handled
12669 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
12672 if ( !this.isDisabled() && this.$input
.is( ':focus' ) ) {
12673 // Standard 'wheel' event
12674 if ( event
.originalEvent
.deltaMode
!== undefined ) {
12675 this.sawWheelEvent
= true;
12677 if ( event
.originalEvent
.deltaY
) {
12678 delta
= -event
.originalEvent
.deltaY
;
12679 } else if ( event
.originalEvent
.deltaX
) {
12680 delta
= event
.originalEvent
.deltaX
;
12683 // Non-standard events
12684 if ( !this.sawWheelEvent
) {
12685 if ( event
.originalEvent
.wheelDeltaX
) {
12686 delta
= -event
.originalEvent
.wheelDeltaX
;
12687 } else if ( event
.originalEvent
.wheelDeltaY
) {
12688 delta
= event
.originalEvent
.wheelDeltaY
;
12689 } else if ( event
.originalEvent
.wheelDelta
) {
12690 delta
= event
.originalEvent
.wheelDelta
;
12691 } else if ( event
.originalEvent
.detail
) {
12692 delta
= -event
.originalEvent
.detail
;
12697 delta
= delta
< 0 ? -1 : 1;
12698 this.adjustValue( delta
* this.buttonStep
);
12706 * Handle key down events.
12709 * @param {jQuery.Event} e Key down event
12710 * @return {undefined/boolean} False to prevent default if event is handled
12712 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
12713 if ( !this.isDisabled() ) {
12714 switch ( e
.which
) {
12715 case OO
.ui
.Keys
.UP
:
12716 this.adjustValue( this.buttonStep
);
12718 case OO
.ui
.Keys
.DOWN
:
12719 this.adjustValue( -this.buttonStep
);
12721 case OO
.ui
.Keys
.PAGEUP
:
12722 this.adjustValue( this.pageStep
);
12724 case OO
.ui
.Keys
.PAGEDOWN
:
12725 this.adjustValue( -this.pageStep
);
12734 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
12736 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
12738 if ( this.minusButton
) {
12739 this.minusButton
.setDisabled( this.isDisabled() );
12741 if ( this.plusButton
) {
12742 this.plusButton
.setDisabled( this.isDisabled() );
12750 //# sourceMappingURL=oojs-ui-core.js.map.json