3 * https://www.mediawiki.org/wiki/OOjs_UI
5 * Copyright 2011–2017 OOjs UI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2017-10-31T22:46:35Z
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 'oojsui-' + 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 * @return {OO.ui.Element}
337 * The `OO.ui.Element` corresponding to this (infusable) document node.
339 OO
.ui
.infuse = function ( idOrNode
) {
340 return OO
.ui
.Element
.static.infuse( idOrNode
);
345 * Message store for the default implementation of OO.ui.msg
347 * Environments that provide a localization system should not use this, but should override
348 * OO.ui.msg altogether.
353 // Tool tip for a button that moves items in a list down one place
354 'ooui-outline-control-move-down': 'Move item down',
355 // Tool tip for a button that moves items in a list up one place
356 'ooui-outline-control-move-up': 'Move item up',
357 // Tool tip for a button that removes items from a list
358 'ooui-outline-control-remove': 'Remove item',
359 // Label for the toolbar group that contains a list of all other available tools
360 'ooui-toolbar-more': 'More',
361 // Label for the fake tool that expands the full list of tools in a toolbar group
362 'ooui-toolgroup-expand': 'More',
363 // Label for the fake tool that collapses the full list of tools in a toolbar group
364 'ooui-toolgroup-collapse': 'Fewer',
365 // Default label for the tooltip for the button that removes a tag item
366 'ooui-item-remove': 'Remove',
367 // Default label for the accept button of a confirmation dialog
368 'ooui-dialog-message-accept': 'OK',
369 // Default label for the reject button of a confirmation dialog
370 'ooui-dialog-message-reject': 'Cancel',
371 // Title for process dialog error description
372 'ooui-dialog-process-error': 'Something went wrong',
373 // Label for process dialog dismiss error button, visible when describing errors
374 'ooui-dialog-process-dismiss': 'Dismiss',
375 // Label for process dialog retry action button, visible when describing only recoverable errors
376 'ooui-dialog-process-retry': 'Try again',
377 // Label for process dialog retry action button, visible when describing only warnings
378 'ooui-dialog-process-continue': 'Continue',
379 // Label for the file selection widget's select file button
380 'ooui-selectfile-button-select': 'Select a file',
381 // Label for the file selection widget if file selection is not supported
382 'ooui-selectfile-not-supported': 'File selection is not supported',
383 // Label for the file selection widget when no file is currently selected
384 'ooui-selectfile-placeholder': 'No file is selected',
385 // Label for the file selection widget's drop target
386 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
390 * Get a localized message.
392 * After the message key, message parameters may optionally be passed. In the default implementation,
393 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
394 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
395 * they support unnamed, ordered message parameters.
397 * In environments that provide a localization system, this function should be overridden to
398 * return the message translated in the user's language. The default implementation always returns
399 * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
403 * var i, iLen, button,
404 * messagePath = 'oojs-ui/dist/i18n/',
405 * languages = [ $.i18n().locale, 'ur', 'en' ],
408 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
409 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
412 * $.i18n().load( languageMap ).done( function() {
413 * // Replace the built-in `msg` only once we've loaded the internationalization.
414 * // OOjs UI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
415 * // you put off creating any widgets until this promise is complete, no English
416 * // will be displayed.
417 * OO.ui.msg = $.i18n;
419 * // A button displaying "OK" in the default locale
420 * button = new OO.ui.ButtonWidget( {
421 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
424 * $( 'body' ).append( button.$element );
426 * // A button displaying "OK" in Urdu
427 * $.i18n().locale = 'ur';
428 * button = new OO.ui.ButtonWidget( {
429 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
432 * $( 'body' ).append( button.$element );
435 * @param {string} key Message key
436 * @param {...Mixed} [params] Message parameters
437 * @return {string} Translated message with parameters substituted
439 OO
.ui
.msg = function ( key
) {
440 var message
= messages
[ key
],
441 params
= Array
.prototype.slice
.call( arguments
, 1 );
442 if ( typeof message
=== 'string' ) {
443 // Perform $1 substitution
444 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
445 var i
= parseInt( n
, 10 );
446 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
449 // Return placeholder if message not found
450 message
= '[' + key
+ ']';
457 * Package a message and arguments for deferred resolution.
459 * Use this when you are statically specifying a message and the message may not yet be present.
461 * @param {string} key Message key
462 * @param {...Mixed} [params] Message parameters
463 * @return {Function} Function that returns the resolved message when executed
465 OO
.ui
.deferMsg = function () {
466 var args
= arguments
;
468 return OO
.ui
.msg
.apply( OO
.ui
, args
);
475 * If the message is a function it will be executed, otherwise it will pass through directly.
477 * @param {Function|string} msg Deferred message, or message text
478 * @return {string} Resolved message
480 OO
.ui
.resolveMsg = function ( msg
) {
481 if ( $.isFunction( msg
) ) {
488 * @param {string} url
491 OO
.ui
.isSafeUrl = function ( url
) {
492 // Keep this function in sync with php/Tag.php
493 var i
, protocolWhitelist
;
495 function stringStartsWith( haystack
, needle
) {
496 return haystack
.substr( 0, needle
.length
) === needle
;
499 protocolWhitelist
= [
500 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
501 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
502 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
509 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
510 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
515 // This matches '//' too
516 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
519 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
527 * Check if the user has a 'mobile' device.
529 * For our purposes this means the user is primarily using an
530 * on-screen keyboard, touch input instead of a mouse and may
531 * have a physically small display.
533 * It is left up to implementors to decide how to compute this
534 * so the default implementation always returns false.
536 * @return {boolean} Use is on a mobile device
538 OO
.ui
.isMobile = function () {
547 * Namespace for OOjs UI mixins.
549 * Mixins are named according to the type of object they are intended to
550 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
551 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
552 * is intended to be mixed in to an instance of OO.ui.Widget.
560 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
561 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
562 * connected to them and can't be interacted with.
568 * @param {Object} [config] Configuration options
569 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
570 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
572 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
573 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
574 * @cfg {string} [text] Text to insert
575 * @cfg {Array} [content] An array of content elements to append (after #text).
576 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
577 * Instances of OO.ui.Element will have their $element appended.
578 * @cfg {jQuery} [$content] Content elements to append (after #text).
579 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
580 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
581 * Data can also be specified with the #setData method.
583 OO
.ui
.Element
= function OoUiElement( config
) {
584 if ( OO
.ui
.isDemo
) {
585 this.initialConfig
= config
;
587 // Configuration initialization
588 config
= config
|| {};
592 this.elementId
= null;
594 this.data
= config
.data
;
595 this.$element
= config
.$element
||
596 $( document
.createElement( this.getTagName() ) );
597 this.elementGroup
= null;
600 if ( Array
.isArray( config
.classes
) ) {
601 this.$element
.addClass( config
.classes
.join( ' ' ) );
604 this.setElementId( config
.id
);
607 this.$element
.text( config
.text
);
609 if ( config
.content
) {
610 // The `content` property treats plain strings as text; use an
611 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
612 // appropriate $element appended.
613 this.$element
.append( config
.content
.map( function ( v
) {
614 if ( typeof v
=== 'string' ) {
615 // Escape string so it is properly represented in HTML.
616 return document
.createTextNode( v
);
617 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
620 } else if ( v
instanceof OO
.ui
.Element
) {
626 if ( config
.$content
) {
627 // The `$content` property treats plain strings as HTML.
628 this.$element
.append( config
.$content
);
634 OO
.initClass( OO
.ui
.Element
);
636 /* Static Properties */
639 * The name of the HTML tag used by the element.
641 * The static value may be ignored if the #getTagName method is overridden.
647 OO
.ui
.Element
.static.tagName
= 'div';
652 * Reconstitute a JavaScript object corresponding to a widget created
653 * by the PHP implementation.
655 * @param {string|HTMLElement|jQuery} idOrNode
656 * A DOM id (if a string) or node for the widget to infuse.
657 * @return {OO.ui.Element}
658 * The `OO.ui.Element` corresponding to this (infusable) document node.
659 * For `Tag` objects emitted on the HTML side (used occasionally for content)
660 * the value returned is a newly-created Element wrapping around the existing
663 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
664 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, false );
665 // Verify that the type matches up.
666 // FIXME: uncomment after T89721 is fixed, see T90929.
668 if ( !( obj instanceof this['class'] ) ) {
669 throw new Error( 'Infusion type mismatch!' );
676 * Implementation helper for `infuse`; skips the type check and has an
677 * extra property so that only the top-level invocation touches the DOM.
680 * @param {string|HTMLElement|jQuery} idOrNode
681 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
682 * when the top-level widget of this infusion is inserted into DOM,
683 * replacing the original node; or false for top-level invocation.
684 * @return {OO.ui.Element}
686 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, domPromise
) {
687 // look for a cached result of a previous infusion.
688 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
689 if ( typeof idOrNode
=== 'string' ) {
691 $elem
= $( document
.getElementById( id
) );
693 $elem
= $( idOrNode
);
694 id
= $elem
.attr( 'id' );
696 if ( !$elem
.length
) {
697 if ( typeof idOrNode
=== 'string' ) {
698 error
= 'Widget not found: ' + idOrNode
;
699 } else if ( idOrNode
&& idOrNode
.selector
) {
700 error
= 'Widget not found: ' + idOrNode
.selector
;
702 error
= 'Widget not found';
704 throw new Error( error
);
706 if ( $elem
[ 0 ].oouiInfused
) {
707 $elem
= $elem
[ 0 ].oouiInfused
;
709 data
= $elem
.data( 'ooui-infused' );
712 if ( data
=== true ) {
713 throw new Error( 'Circular dependency! ' + id
);
716 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
717 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
718 // restore dynamic state after the new element is re-inserted into DOM under infused parent
719 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
720 infusedChildren
= $elem
.data( 'ooui-infused-children' );
721 if ( infusedChildren
&& infusedChildren
.length
) {
722 infusedChildren
.forEach( function ( data
) {
723 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
724 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
730 data
= $elem
.attr( 'data-ooui' );
732 throw new Error( 'No infusion data found: ' + id
);
735 data
= JSON
.parse( data
);
739 if ( !( data
&& data
._
) ) {
740 throw new Error( 'No valid infusion data found: ' + id
);
742 if ( data
._
=== 'Tag' ) {
743 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
744 return new OO
.ui
.Element( { $element
: $elem
} );
746 parts
= data
._
.split( '.' );
747 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
748 if ( cls
=== undefined ) {
749 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
752 // Verify that we're creating an OO.ui.Element instance
755 while ( parent
!== undefined ) {
756 if ( parent
=== OO
.ui
.Element
) {
761 parent
= parent
.parent
;
764 if ( parent
!== OO
.ui
.Element
) {
765 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
768 if ( domPromise
=== false ) {
770 domPromise
= top
.promise();
772 $elem
.data( 'ooui-infused', true ); // prevent loops
773 data
.id
= id
; // implicit
774 infusedChildren
= [];
775 data
= OO
.copy( data
, null, function deserialize( value
) {
777 if ( OO
.isPlainObject( value
) ) {
779 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, domPromise
);
780 infusedChildren
.push( infused
);
781 // Flatten the structure
782 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
783 infused
.$element
.removeData( 'ooui-infused-children' );
786 if ( value
.html
!== undefined ) {
787 return new OO
.ui
.HtmlSnippet( value
.html
);
791 // allow widgets to reuse parts of the DOM
792 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
793 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
794 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
796 // eslint-disable-next-line new-cap
797 obj
= new cls( data
);
798 // now replace old DOM with this new DOM.
800 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
801 // so only mutate the DOM if we need to.
802 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
803 $elem
.replaceWith( obj
.$element
);
804 // This element is now gone from the DOM, but if anyone is holding a reference to it,
805 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
806 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
807 $elem
[ 0 ].oouiInfused
= obj
.$element
;
811 obj
.$element
.data( 'ooui-infused', obj
);
812 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
813 // set the 'data-ooui' attribute so we can identify infused widgets
814 obj
.$element
.attr( 'data-ooui', '' );
815 // restore dynamic state after the new element is inserted into DOM
816 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
821 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
823 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
824 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
825 * constructor, which will be given the enhanced config.
828 * @param {HTMLElement} node
829 * @param {Object} config
832 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
837 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
838 * (and its children) that represent an Element of the same class and the given configuration,
839 * generated by the PHP implementation.
841 * This method is called just before `node` is detached from the DOM. The return value of this
842 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
843 * is inserted into DOM to replace `node`.
846 * @param {HTMLElement} node
847 * @param {Object} config
850 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
855 * Get a jQuery function within a specific document.
858 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
859 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
861 * @return {Function} Bound jQuery function
863 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
864 function wrapper( selector
) {
865 return $( selector
, wrapper
.context
);
868 wrapper
.context
= this.getDocument( context
);
871 wrapper
.$iframe
= $iframe
;
878 * Get the document of an element.
881 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
882 * @return {HTMLDocument|null} Document object
884 OO
.ui
.Element
.static.getDocument = function ( obj
) {
885 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
886 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
887 // Empty jQuery selections might have a context
894 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
899 * Get the window of an element or document.
902 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
903 * @return {Window} Window object
905 OO
.ui
.Element
.static.getWindow = function ( obj
) {
906 var doc
= this.getDocument( obj
);
907 return doc
.defaultView
;
911 * Get the direction of an element or document.
914 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
915 * @return {string} Text direction, either 'ltr' or 'rtl'
917 OO
.ui
.Element
.static.getDir = function ( obj
) {
920 if ( obj
instanceof jQuery
) {
923 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
924 isWin
= obj
.document
!== undefined;
925 if ( isDoc
|| isWin
) {
931 return $( obj
).css( 'direction' );
935 * Get the offset between two frames.
937 * TODO: Make this function not use recursion.
940 * @param {Window} from Window of the child frame
941 * @param {Window} [to=window] Window of the parent frame
942 * @param {Object} [offset] Offset to start with, used internally
943 * @return {Object} Offset object, containing left and top properties
945 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
946 var i
, len
, frames
, frame
, rect
;
952 offset
= { top
: 0, left
: 0 };
954 if ( from.parent
=== from ) {
958 // Get iframe element
959 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
960 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
961 if ( frames
[ i
].contentWindow
=== from ) {
967 // Recursively accumulate offset values
969 rect
= frame
.getBoundingClientRect();
970 offset
.left
+= rect
.left
;
971 offset
.top
+= rect
.top
;
973 this.getFrameOffset( from.parent
, offset
);
980 * Get the offset between two elements.
982 * The two elements may be in a different frame, but in that case the frame $element is in must
983 * be contained in the frame $anchor is in.
986 * @param {jQuery} $element Element whose position to get
987 * @param {jQuery} $anchor Element to get $element's position relative to
988 * @return {Object} Translated position coordinates, containing top and left properties
990 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
991 var iframe
, iframePos
,
992 pos
= $element
.offset(),
993 anchorPos
= $anchor
.offset(),
994 elementDocument
= this.getDocument( $element
),
995 anchorDocument
= this.getDocument( $anchor
);
997 // If $element isn't in the same document as $anchor, traverse up
998 while ( elementDocument
!== anchorDocument
) {
999 iframe
= elementDocument
.defaultView
.frameElement
;
1001 throw new Error( '$element frame is not contained in $anchor frame' );
1003 iframePos
= $( iframe
).offset();
1004 pos
.left
+= iframePos
.left
;
1005 pos
.top
+= iframePos
.top
;
1006 elementDocument
= iframe
.ownerDocument
;
1008 pos
.left
-= anchorPos
.left
;
1009 pos
.top
-= anchorPos
.top
;
1014 * Get element border sizes.
1017 * @param {HTMLElement} el Element to measure
1018 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1020 OO
.ui
.Element
.static.getBorders = function ( el
) {
1021 var doc
= el
.ownerDocument
,
1022 win
= doc
.defaultView
,
1023 style
= win
.getComputedStyle( el
, null ),
1025 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1026 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1027 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1028 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1039 * Get dimensions of an element or window.
1042 * @param {HTMLElement|Window} el Element to measure
1043 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1045 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1047 doc
= el
.ownerDocument
|| el
.document
,
1048 win
= doc
.defaultView
;
1050 if ( win
=== el
|| el
=== doc
.documentElement
) {
1053 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1055 top
: $win
.scrollTop(),
1056 left
: $win
.scrollLeft()
1058 scrollbar
: { right
: 0, bottom
: 0 },
1062 bottom
: $win
.innerHeight(),
1063 right
: $win
.innerWidth()
1069 borders
: this.getBorders( el
),
1071 top
: $el
.scrollTop(),
1072 left
: $el
.scrollLeft()
1075 right
: $el
.innerWidth() - el
.clientWidth
,
1076 bottom
: $el
.innerHeight() - el
.clientHeight
1078 rect
: el
.getBoundingClientRect()
1084 * Get the number of pixels that an element's content is scrolled to the left.
1086 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1087 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1089 * This function smooths out browser inconsistencies (nicely described in the README at
1090 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1091 * with Firefox's 'scrollLeft', which seems the sanest.
1095 * @param {HTMLElement|Window} el Element to measure
1096 * @return {number} Scroll position from the left.
1097 * If the element's direction is LTR, this is a positive number between `0` (initial scroll position)
1098 * and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1099 * If the element's direction is RTL, this is a negative number between `0` (initial scroll position)
1100 * and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1102 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1103 var rtlScrollType
= null;
1106 var $definer
= $( '<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>' ),
1107 definer
= $definer
[ 0 ];
1109 $definer
.appendTo( 'body' );
1110 if ( definer
.scrollLeft
> 0 ) {
1112 rtlScrollType
= 'default';
1114 definer
.scrollLeft
= 1;
1115 if ( definer
.scrollLeft
=== 0 ) {
1116 // Firefox, old Opera
1117 rtlScrollType
= 'negative';
1119 // Internet Explorer, Edge
1120 rtlScrollType
= 'reverse';
1126 return function getScrollLeft( el
) {
1127 var isRoot
= el
.window
=== el
||
1128 el
=== el
.ownerDocument
.body
||
1129 el
=== el
.ownerDocument
.documentElement
,
1130 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1131 // All browsers use the correct scroll type ('negative') on the root, so don't
1132 // do any fixups when looking at the root element
1133 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1135 if ( direction
=== 'rtl' ) {
1136 if ( rtlScrollType
=== null ) {
1139 if ( rtlScrollType
=== 'reverse' ) {
1140 scrollLeft
= -scrollLeft
;
1141 } else if ( rtlScrollType
=== 'default' ) {
1142 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1151 * Get the root scrollable element of given element's document.
1153 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1154 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1155 * lets us use 'body' or 'documentElement' based on what is working.
1157 * https://code.google.com/p/chromium/issues/detail?id=303131
1160 * @param {HTMLElement} el Element to find root scrollable parent for
1161 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1162 * depending on browser
1164 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1165 var scrollTop
, body
;
1167 if ( OO
.ui
.scrollableElement
=== undefined ) {
1168 body
= el
.ownerDocument
.body
;
1169 scrollTop
= body
.scrollTop
;
1172 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1173 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1174 if ( Math
.round( body
.scrollTop
) === 1 ) {
1175 body
.scrollTop
= scrollTop
;
1176 OO
.ui
.scrollableElement
= 'body';
1178 OO
.ui
.scrollableElement
= 'documentElement';
1182 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1186 * Get closest scrollable container.
1188 * Traverses up until either a scrollable element or the root is reached, in which case the root
1189 * scrollable element will be returned (see #getRootScrollableElement).
1192 * @param {HTMLElement} el Element to find scrollable container for
1193 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1194 * @return {HTMLElement} Closest scrollable container
1196 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1198 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1199 // 'overflow-y' have different values, so we need to check the separate properties.
1200 props
= [ 'overflow-x', 'overflow-y' ],
1201 $parent
= $( el
).parent();
1203 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1204 props
= [ 'overflow-' + dimension
];
1207 // Special case for the document root (which doesn't really have any scrollable container, since
1208 // it is the ultimate scrollable container, but this is probably saner than null or exception)
1209 if ( $( el
).is( 'html, body' ) ) {
1210 return this.getRootScrollableElement( el
);
1213 while ( $parent
.length
) {
1214 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1215 return $parent
[ 0 ];
1219 val
= $parent
.css( props
[ i
] );
1220 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be
1221 // scrolled in that direction, but they can actually be scrolled programatically. The user can
1222 // unintentionally perform a scroll in such case even if the application doesn't scroll
1223 // programatically, e.g. when jumping to an anchor, or when using built-in find functionality.
1224 // This could cause funny issues...
1225 if ( val
=== 'auto' || val
=== 'scroll' ) {
1226 return $parent
[ 0 ];
1229 $parent
= $parent
.parent();
1231 // The element is unattached... return something mostly sane
1232 return this.getRootScrollableElement( el
);
1236 * Scroll element into view.
1239 * @param {HTMLElement} el Element to scroll into view
1240 * @param {Object} [config] Configuration options
1241 * @param {string} [config.duration='fast'] jQuery animation duration value
1242 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1243 * to scroll in both directions
1244 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1246 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1247 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1248 deferred
= $.Deferred();
1250 // Configuration initialization
1251 config
= config
|| {};
1254 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1255 $container
= $( container
);
1256 elementDimensions
= this.getDimensions( el
);
1257 containerDimensions
= this.getDimensions( container
);
1258 $window
= $( this.getWindow( el
) );
1260 // Compute the element's position relative to the container
1261 if ( $container
.is( 'html, body' ) ) {
1262 // If the scrollable container is the root, this is easy
1264 top
: elementDimensions
.rect
.top
,
1265 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1266 left
: elementDimensions
.rect
.left
,
1267 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1270 // Otherwise, we have to subtract el's coordinates from container's coordinates
1272 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1273 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1274 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1275 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1279 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1280 if ( position
.top
< 0 ) {
1281 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1282 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1283 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1286 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1287 if ( position
.left
< 0 ) {
1288 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1289 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1290 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1293 if ( !$.isEmptyObject( animations
) ) {
1294 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1295 $container
.queue( function ( next
) {
1302 return deferred
.promise();
1306 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1307 * and reserve space for them, because it probably doesn't.
1309 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1310 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1311 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1312 * and then reattach (or show) them back.
1315 * @param {HTMLElement} el Element to reconsider the scrollbars on
1317 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1318 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1319 // Save scroll position
1320 scrollLeft
= el
.scrollLeft
;
1321 scrollTop
= el
.scrollTop
;
1322 // Detach all children
1323 while ( el
.firstChild
) {
1324 nodes
.push( el
.firstChild
);
1325 el
.removeChild( el
.firstChild
);
1328 void el
.offsetHeight
;
1329 // Reattach all children
1330 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1331 el
.appendChild( nodes
[ i
] );
1333 // Restore scroll position (no-op if scrollbars disappeared)
1334 el
.scrollLeft
= scrollLeft
;
1335 el
.scrollTop
= scrollTop
;
1341 * Toggle visibility of an element.
1343 * @param {boolean} [show] Make element visible, omit to toggle visibility
1347 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1348 show
= show
=== undefined ? !this.visible
: !!show
;
1350 if ( show
!== this.isVisible() ) {
1351 this.visible
= show
;
1352 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1353 this.emit( 'toggle', show
);
1360 * Check if element is visible.
1362 * @return {boolean} element is visible
1364 OO
.ui
.Element
.prototype.isVisible = function () {
1365 return this.visible
;
1371 * @return {Mixed} Element data
1373 OO
.ui
.Element
.prototype.getData = function () {
1380 * @param {Mixed} data Element data
1383 OO
.ui
.Element
.prototype.setData = function ( data
) {
1389 * Set the element has an 'id' attribute.
1391 * @param {string} id
1394 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1395 this.elementId
= id
;
1396 this.$element
.attr( 'id', id
);
1401 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1402 * and return its value.
1406 OO
.ui
.Element
.prototype.getElementId = function () {
1407 if ( this.elementId
=== null ) {
1408 this.setElementId( OO
.ui
.generateElementId() );
1410 return this.elementId
;
1414 * Check if element supports one or more methods.
1416 * @param {string|string[]} methods Method or list of methods to check
1417 * @return {boolean} All methods are supported
1419 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1423 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1424 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1425 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1430 return methods
.length
=== support
;
1434 * Update the theme-provided classes.
1436 * @localdoc This is called in element mixins and widget classes any time state changes.
1437 * Updating is debounced, minimizing overhead of changing multiple attributes and
1438 * guaranteeing that theme updates do not occur within an element's constructor
1440 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1441 OO
.ui
.theme
.queueUpdateElementClasses( this );
1445 * Get the HTML tag name.
1447 * Override this method to base the result on instance information.
1449 * @return {string} HTML tag name
1451 OO
.ui
.Element
.prototype.getTagName = function () {
1452 return this.constructor.static.tagName
;
1456 * Check if the element is attached to the DOM
1458 * @return {boolean} The element is attached to the DOM
1460 OO
.ui
.Element
.prototype.isElementAttached = function () {
1461 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1465 * Get the DOM document.
1467 * @return {HTMLDocument} Document object
1469 OO
.ui
.Element
.prototype.getElementDocument = function () {
1470 // Don't cache this in other ways either because subclasses could can change this.$element
1471 return OO
.ui
.Element
.static.getDocument( this.$element
);
1475 * Get the DOM window.
1477 * @return {Window} Window object
1479 OO
.ui
.Element
.prototype.getElementWindow = function () {
1480 return OO
.ui
.Element
.static.getWindow( this.$element
);
1484 * Get closest scrollable container.
1486 * @return {HTMLElement} Closest scrollable container
1488 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1489 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1493 * Get group element is in.
1495 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1497 OO
.ui
.Element
.prototype.getElementGroup = function () {
1498 return this.elementGroup
;
1502 * Set group element is in.
1504 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1507 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1508 this.elementGroup
= group
;
1513 * Scroll element into view.
1515 * @param {Object} [config] Configuration options
1516 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1518 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1520 !this.isElementAttached() ||
1521 !this.isVisible() ||
1522 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1524 return $.Deferred().resolve();
1526 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1530 * Restore the pre-infusion dynamic state for this widget.
1532 * This method is called after #$element has been inserted into DOM. The parameter is the return
1533 * value of #gatherPreInfuseState.
1536 * @param {Object} state
1538 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1542 * Wraps an HTML snippet for use with configuration values which default
1543 * to strings. This bypasses the default html-escaping done to string
1549 * @param {string} [content] HTML content
1551 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1553 this.content
= content
;
1558 OO
.initClass( OO
.ui
.HtmlSnippet
);
1565 * @return {string} Unchanged HTML snippet.
1567 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1568 return this.content
;
1572 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1573 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1574 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1575 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1576 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1580 * @extends OO.ui.Element
1581 * @mixins OO.EventEmitter
1584 * @param {Object} [config] Configuration options
1586 OO
.ui
.Layout
= function OoUiLayout( config
) {
1587 // Configuration initialization
1588 config
= config
|| {};
1590 // Parent constructor
1591 OO
.ui
.Layout
.parent
.call( this, config
);
1593 // Mixin constructors
1594 OO
.EventEmitter
.call( this );
1597 this.$element
.addClass( 'oo-ui-layout' );
1602 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1603 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1606 * Widgets are compositions of one or more OOjs UI elements that users can both view
1607 * and interact with. All widgets can be configured and modified via a standard API,
1608 * and their state can change dynamically according to a model.
1612 * @extends OO.ui.Element
1613 * @mixins OO.EventEmitter
1616 * @param {Object} [config] Configuration options
1617 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1618 * appearance reflects this state.
1620 OO
.ui
.Widget
= function OoUiWidget( config
) {
1621 // Initialize config
1622 config
= $.extend( { disabled
: false }, config
);
1624 // Parent constructor
1625 OO
.ui
.Widget
.parent
.call( this, config
);
1627 // Mixin constructors
1628 OO
.EventEmitter
.call( this );
1631 this.disabled
= null;
1632 this.wasDisabled
= null;
1635 this.$element
.addClass( 'oo-ui-widget' );
1636 this.setDisabled( !!config
.disabled
);
1641 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1642 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1649 * A 'disable' event is emitted when the disabled state of the widget changes
1650 * (i.e. on disable **and** enable).
1652 * @param {boolean} disabled Widget is disabled
1658 * A 'toggle' event is emitted when the visibility of the widget changes.
1660 * @param {boolean} visible Widget is visible
1666 * Check if the widget is disabled.
1668 * @return {boolean} Widget is disabled
1670 OO
.ui
.Widget
.prototype.isDisabled = function () {
1671 return this.disabled
;
1675 * Set the 'disabled' state of the widget.
1677 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1679 * @param {boolean} disabled Disable widget
1682 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1685 this.disabled
= !!disabled
;
1686 isDisabled
= this.isDisabled();
1687 if ( isDisabled
!== this.wasDisabled
) {
1688 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1689 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1690 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1691 this.emit( 'disable', isDisabled
);
1692 this.updateThemeClasses();
1694 this.wasDisabled
= isDisabled
;
1700 * Update the disabled state, in case of changes in parent widget.
1704 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1705 this.setDisabled( this.disabled
);
1710 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1713 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1716 * @return {string|null} The ID of the labelable element
1718 OO
.ui
.Widget
.prototype.getInputId = function () {
1723 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1724 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1725 * override this method to provide intuitive, accessible behavior.
1727 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1728 * Individual widgets may override it too.
1730 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1733 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1744 OO
.ui
.Theme
= function OoUiTheme() {
1745 this.elementClassesQueue
= [];
1746 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1751 OO
.initClass( OO
.ui
.Theme
);
1756 * Get a list of classes to be applied to a widget.
1758 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1759 * otherwise state transitions will not work properly.
1761 * @param {OO.ui.Element} element Element for which to get classes
1762 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1764 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1765 return { on
: [], off
: [] };
1769 * Update CSS classes provided by the theme.
1771 * For elements with theme logic hooks, this should be called any time there's a state change.
1773 * @param {OO.ui.Element} element Element for which to update classes
1775 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1776 var $elements
= $( [] ),
1777 classes
= this.getElementClasses( element
);
1779 if ( element
.$icon
) {
1780 $elements
= $elements
.add( element
.$icon
);
1782 if ( element
.$indicator
) {
1783 $elements
= $elements
.add( element
.$indicator
);
1787 .removeClass( classes
.off
.join( ' ' ) )
1788 .addClass( classes
.on
.join( ' ' ) );
1794 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1796 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1797 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1800 this.elementClassesQueue
= [];
1804 * Queue #updateElementClasses to be called for this element.
1806 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1807 * to make them synchronous.
1809 * @param {OO.ui.Element} element Element for which to update classes
1811 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1812 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1813 // the most common case (this method is often called repeatedly for the same element).
1814 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1817 this.elementClassesQueue
.push( element
);
1818 this.debouncedUpdateQueuedElementClasses();
1822 * Get the transition duration in milliseconds for dialogs opening/closing
1824 * The dialog should be fully rendered this many milliseconds after the
1825 * ready process has executed.
1827 * @return {number} Transition duration in milliseconds
1829 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1834 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1835 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1836 * order in which users will navigate through the focusable elements via the "tab" key.
1839 * // TabIndexedElement is mixed into the ButtonWidget class
1840 * // to provide a tabIndex property.
1841 * var button1 = new OO.ui.ButtonWidget( {
1845 * var button2 = new OO.ui.ButtonWidget( {
1849 * var button3 = new OO.ui.ButtonWidget( {
1853 * var button4 = new OO.ui.ButtonWidget( {
1857 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1863 * @param {Object} [config] Configuration options
1864 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1865 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1866 * functionality will be applied to it instead.
1867 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1868 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1869 * to remove the element from the tab-navigation flow.
1871 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1872 // Configuration initialization
1873 config
= $.extend( { tabIndex
: 0 }, config
);
1876 this.$tabIndexed
= null;
1877 this.tabIndex
= null;
1880 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1883 this.setTabIndex( config
.tabIndex
);
1884 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1889 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1894 * Set the element that should use the tabindex functionality.
1896 * This method is used to retarget a tabindex mixin so that its functionality applies
1897 * to the specified element. If an element is currently using the functionality, the mixin’s
1898 * effect on that element is removed before the new element is set up.
1900 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1903 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1904 var tabIndex
= this.tabIndex
;
1905 // Remove attributes from old $tabIndexed
1906 this.setTabIndex( null );
1907 // Force update of new $tabIndexed
1908 this.$tabIndexed
= $tabIndexed
;
1909 this.tabIndex
= tabIndex
;
1910 return this.updateTabIndex();
1914 * Set the value of the tabindex.
1916 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
1919 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1920 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
1922 if ( this.tabIndex
!== tabIndex
) {
1923 this.tabIndex
= tabIndex
;
1924 this.updateTabIndex();
1931 * Update the `tabindex` attribute, in case of changes to tab index or
1937 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1938 if ( this.$tabIndexed
) {
1939 if ( this.tabIndex
!== null ) {
1940 // Do not index over disabled elements
1941 this.$tabIndexed
.attr( {
1942 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1943 // Support: ChromeVox and NVDA
1944 // These do not seem to inherit aria-disabled from parent elements
1945 'aria-disabled': this.isDisabled().toString()
1948 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1955 * Handle disable events.
1958 * @param {boolean} disabled Element is disabled
1960 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1961 this.updateTabIndex();
1965 * Get the value of the tabindex.
1967 * @return {number|null} Tabindex value
1969 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
1970 return this.tabIndex
;
1974 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
1976 * If the element already has an ID then that is returned, otherwise unique ID is
1977 * generated, set on the element, and returned.
1979 * @return {string|null} The ID of the focusable element
1981 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
1984 if ( !this.$tabIndexed
) {
1987 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
1991 id
= this.$tabIndexed
.attr( 'id' );
1992 if ( id
=== undefined ) {
1993 id
= OO
.ui
.generateElementId();
1994 this.$tabIndexed
.attr( 'id', id
);
2001 * Whether the node is 'labelable' according to the HTML spec
2002 * (i.e., whether it can be interacted with through a `<label for="…">`).
2003 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2006 * @param {jQuery} $node
2009 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2011 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2012 tagName
= $node
.prop( 'tagName' ).toLowerCase();
2014 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2017 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2024 * Focus this element.
2028 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2029 if ( !this.isDisabled() ) {
2030 this.$tabIndexed
.focus();
2036 * Blur this element.
2040 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2041 this.$tabIndexed
.blur();
2046 * @inheritdoc OO.ui.Widget
2048 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2053 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2054 * interface element that can be configured with access keys for accessibility.
2055 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
2057 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
2063 * @param {Object} [config] Configuration options
2064 * @cfg {jQuery} [$button] The button element created by the class.
2065 * If this configuration is omitted, the button element will use a generated `<a>`.
2066 * @cfg {boolean} [framed=true] Render the button with a frame
2068 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2069 // Configuration initialization
2070 config
= config
|| {};
2073 this.$button
= null;
2075 this.active
= config
.active
!== undefined && config
.active
;
2076 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
2077 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2078 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2079 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
2080 this.onClickHandler
= this.onClick
.bind( this );
2081 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2084 this.$element
.addClass( 'oo-ui-buttonElement' );
2085 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2086 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2091 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2093 /* Static Properties */
2096 * Cancel mouse down events.
2098 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
2099 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
2100 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
2105 * @property {boolean}
2107 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2112 * A 'click' event is emitted when the button element is clicked.
2120 * Set the button element.
2122 * This method is used to retarget a button mixin so that its functionality applies to
2123 * the specified button element instead of the one created by the class. If a button element
2124 * is already set, the method will remove the mixin’s effect on that element.
2126 * @param {jQuery} $button Element to use as button
2128 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2129 if ( this.$button
) {
2131 .removeClass( 'oo-ui-buttonElement-button' )
2132 .removeAttr( 'role accesskey' )
2134 mousedown
: this.onMouseDownHandler
,
2135 keydown
: this.onKeyDownHandler
,
2136 click
: this.onClickHandler
,
2137 keypress
: this.onKeyPressHandler
2141 this.$button
= $button
2142 .addClass( 'oo-ui-buttonElement-button' )
2144 mousedown
: this.onMouseDownHandler
,
2145 keydown
: this.onKeyDownHandler
,
2146 click
: this.onClickHandler
,
2147 keypress
: this.onKeyPressHandler
2150 // Add `role="button"` on `<a>` elements, where it's needed
2151 // `toUppercase()` is added for XHTML documents
2152 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2153 this.$button
.attr( 'role', 'button' );
2158 * Handles mouse down events.
2161 * @param {jQuery.Event} e Mouse down event
2163 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2164 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2167 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2168 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2169 // reliably remove the pressed class
2170 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
2171 // Prevent change of focus unless specifically configured otherwise
2172 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2178 * Handles mouse up events.
2181 * @param {MouseEvent} e Mouse up event
2183 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
2184 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2187 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2188 // Stop listening for mouseup, since we only needed this once
2189 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
2193 * Handles mouse click events.
2196 * @param {jQuery.Event} e Mouse click event
2199 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2200 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2201 if ( this.emit( 'click' ) ) {
2208 * Handles key down events.
2211 * @param {jQuery.Event} e Key down event
2213 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2214 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2217 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2218 // Run the keyup handler no matter where the key is when the button is let go, so we can
2219 // reliably remove the pressed class
2220 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
2224 * Handles key up events.
2227 * @param {KeyboardEvent} e Key up event
2229 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
2230 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2233 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2234 // Stop listening for keyup, since we only needed this once
2235 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
2239 * Handles key press events.
2242 * @param {jQuery.Event} e Key press event
2245 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2246 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2247 if ( this.emit( 'click' ) ) {
2254 * Check if button has a frame.
2256 * @return {boolean} Button is framed
2258 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2263 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2265 * @param {boolean} [framed] Make button framed, omit to toggle
2268 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2269 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2270 if ( framed
!== this.framed
) {
2271 this.framed
= framed
;
2273 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2274 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2275 this.updateThemeClasses();
2282 * Set the button's active state.
2284 * The active state can be set on:
2286 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2287 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2288 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2291 * @param {boolean} value Make button active
2294 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2295 this.active
= !!value
;
2296 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2297 this.updateThemeClasses();
2302 * Check if the button is active
2305 * @return {boolean} The button is active
2307 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2312 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2313 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2314 * items from the group is done through the interface the class provides.
2315 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2317 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
2320 * @mixins OO.EmitterList
2324 * @param {Object} [config] Configuration options
2325 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2326 * is omitted, the group element will use a generated `<div>`.
2328 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2329 // Configuration initialization
2330 config
= config
|| {};
2332 // Mixin constructors
2333 OO
.EmitterList
.call( this, config
);
2339 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2344 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2351 * A change event is emitted when the set of selected items changes.
2353 * @param {OO.ui.Element[]} items Items currently in the group
2359 * Set the group element.
2361 * If an element is already set, items will be moved to the new element.
2363 * @param {jQuery} $group Element to use as group
2365 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2368 this.$group
= $group
;
2369 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2370 this.$group
.append( this.items
[ i
].$element
);
2375 * Get an item by its data.
2377 * Only the first item with matching data will be returned. To return all matching items,
2378 * use the #getItemsFromData method.
2380 * @param {Object} data Item data to search for
2381 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2383 OO
.ui
.mixin
.GroupElement
.prototype.getItemFromData = function ( data
) {
2385 hash
= OO
.getHash( data
);
2387 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2388 item
= this.items
[ i
];
2389 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2398 * Get items by their data.
2400 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2402 * @param {Object} data Item data to search for
2403 * @return {OO.ui.Element[]} Items with equivalent data
2405 OO
.ui
.mixin
.GroupElement
.prototype.getItemsFromData = function ( data
) {
2407 hash
= OO
.getHash( data
),
2410 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2411 item
= this.items
[ i
];
2412 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2421 * Add items to the group.
2423 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2424 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2426 * @param {OO.ui.Element[]} items An array of items to add to the group
2427 * @param {number} [index] Index of the insertion point
2430 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2432 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2434 this.emit( 'change', this.getItems() );
2441 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2442 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2443 this.insertItemElements( items
, newIndex
);
2446 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2454 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2455 item
.setElementGroup( this );
2456 this.insertItemElements( item
, index
);
2459 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2465 * Insert elements into the group
2468 * @param {OO.ui.Element} itemWidget Item to insert
2469 * @param {number} index Insertion index
2471 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2472 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2473 this.$group
.append( itemWidget
.$element
);
2474 } else if ( index
=== 0 ) {
2475 this.$group
.prepend( itemWidget
.$element
);
2477 this.items
[ index
].$element
.before( itemWidget
.$element
);
2482 * Remove the specified items from a group.
2484 * Removed items are detached (not removed) from the DOM so that they may be reused.
2485 * To remove all items from a group, you may wish to use the #clearItems method instead.
2487 * @param {OO.ui.Element[]} items An array of items to remove
2490 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2491 var i
, len
, item
, index
;
2493 // Remove specific items elements
2494 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2496 index
= this.items
.indexOf( item
);
2497 if ( index
!== -1 ) {
2498 item
.setElementGroup( null );
2499 item
.$element
.detach();
2504 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2506 this.emit( 'change', this.getItems() );
2511 * Clear all items from the group.
2513 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2514 * To remove only a subset of items from a group, use the #removeItems method.
2518 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2521 // Remove all item elements
2522 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2523 this.items
[ i
].setElementGroup( null );
2524 this.items
[ i
].$element
.detach();
2528 OO
.EmitterList
.prototype.clearItems
.call( this );
2530 this.emit( 'change', this.getItems() );
2535 * IconElement is often mixed into other classes to generate an icon.
2536 * Icons are graphics, about the size of normal text. They are used to aid the user
2537 * in locating a control or to convey information in a space-efficient way. See the
2538 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2539 * included in the library.
2541 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2547 * @param {Object} [config] Configuration options
2548 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2549 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2550 * the icon element be set to an existing icon instead of the one generated by this class, set a
2551 * value using a jQuery selection. For example:
2553 * // Use a <div> tag instead of a <span>
2555 * // Use an existing icon element instead of the one generated by the class
2556 * $icon: this.$element
2557 * // Use an icon element from a child widget
2558 * $icon: this.childwidget.$element
2559 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2560 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2561 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2562 * by the user's language.
2564 * Example of an i18n map:
2566 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2567 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2568 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2569 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2570 * text. The icon title is displayed when users move the mouse over the icon.
2572 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2573 // Configuration initialization
2574 config
= config
|| {};
2579 this.iconTitle
= null;
2582 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2583 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2584 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2589 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2591 /* Static Properties */
2594 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2595 * for i18n purposes and contains a `default` icon name and additional names keyed by
2596 * language code. The `default` name is used when no icon is keyed by the user's language.
2598 * Example of an i18n map:
2600 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2602 * Note: the static property will be overridden if the #icon configuration is used.
2606 * @property {Object|string}
2608 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2611 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2612 * function that returns title text, or `null` for no title.
2614 * The static property will be overridden if the #iconTitle configuration is used.
2618 * @property {string|Function|null}
2620 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2625 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2626 * applies to the specified icon element instead of the one created by the class. If an icon
2627 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2628 * and mixin methods will no longer affect the element.
2630 * @param {jQuery} $icon Element to use as icon
2632 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2635 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2636 .removeAttr( 'title' );
2640 .addClass( 'oo-ui-iconElement-icon' )
2641 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2642 if ( this.iconTitle
!== null ) {
2643 this.$icon
.attr( 'title', this.iconTitle
);
2646 this.updateThemeClasses();
2650 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2651 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2654 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2655 * by language code, or `null` to remove the icon.
2658 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2659 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2660 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2662 if ( this.icon
!== icon
) {
2664 if ( this.icon
!== null ) {
2665 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2667 if ( icon
!== null ) {
2668 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2674 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2675 this.updateThemeClasses();
2681 * Set the icon title. Use `null` to remove the title.
2683 * @param {string|Function|null} iconTitle A text string used as the icon title,
2684 * a function that returns title text, or `null` for no title.
2687 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2689 ( typeof iconTitle
=== 'function' || ( typeof iconTitle
=== 'string' && iconTitle
.length
) ) ?
2690 OO
.ui
.resolveMsg( iconTitle
) : null;
2692 if ( this.iconTitle
!== iconTitle
) {
2693 this.iconTitle
= iconTitle
;
2695 if ( this.iconTitle
!== null ) {
2696 this.$icon
.attr( 'title', iconTitle
);
2698 this.$icon
.removeAttr( 'title' );
2707 * Get the symbolic name of the icon.
2709 * @return {string} Icon name
2711 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2716 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2718 * @return {string} Icon title text
2720 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2721 return this.iconTitle
;
2725 * IndicatorElement is often mixed into other classes to generate an indicator.
2726 * Indicators are small graphics that are generally used in two ways:
2728 * - To draw attention to the status of an item. For example, an indicator might be
2729 * used to show that an item in a list has errors that need to be resolved.
2730 * - To clarify the function of a control that acts in an exceptional way (a button
2731 * that opens a menu instead of performing an action directly, for example).
2733 * For a list of indicators included in the library, please see the
2734 * [OOjs UI documentation on MediaWiki] [1].
2736 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2742 * @param {Object} [config] Configuration options
2743 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2744 * configuration is omitted, the indicator element will use a generated `<span>`.
2745 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2746 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2748 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2749 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2750 * or a function that returns title text. The indicator title is displayed when users move
2751 * the mouse over the indicator.
2753 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2754 // Configuration initialization
2755 config
= config
|| {};
2758 this.$indicator
= null;
2759 this.indicator
= null;
2760 this.indicatorTitle
= null;
2763 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2764 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2765 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2770 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2772 /* Static Properties */
2775 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2776 * The static property will be overridden if the #indicator configuration is used.
2780 * @property {string|null}
2782 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2785 * A text string used as the indicator title, a function that returns title text, or `null`
2786 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2790 * @property {string|Function|null}
2792 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2797 * Set the indicator element.
2799 * If an element is already set, it will be cleaned up before setting up the new element.
2801 * @param {jQuery} $indicator Element to use as indicator
2803 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2804 if ( this.$indicator
) {
2806 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2807 .removeAttr( 'title' );
2810 this.$indicator
= $indicator
2811 .addClass( 'oo-ui-indicatorElement-indicator' )
2812 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2813 if ( this.indicatorTitle
!== null ) {
2814 this.$indicator
.attr( 'title', this.indicatorTitle
);
2817 this.updateThemeClasses();
2821 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2823 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2826 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2827 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2829 if ( this.indicator
!== indicator
) {
2830 if ( this.$indicator
) {
2831 if ( this.indicator
!== null ) {
2832 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2834 if ( indicator
!== null ) {
2835 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2838 this.indicator
= indicator
;
2841 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2842 this.updateThemeClasses();
2848 * Set the indicator title.
2850 * The title is displayed when a user moves the mouse over the indicator.
2852 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2853 * `null` for no indicator title
2856 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2858 ( typeof indicatorTitle
=== 'function' || ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ) ?
2859 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2861 if ( this.indicatorTitle
!== indicatorTitle
) {
2862 this.indicatorTitle
= indicatorTitle
;
2863 if ( this.$indicator
) {
2864 if ( this.indicatorTitle
!== null ) {
2865 this.$indicator
.attr( 'title', indicatorTitle
);
2867 this.$indicator
.removeAttr( 'title' );
2876 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2878 * @return {string} Symbolic name of indicator
2880 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2881 return this.indicator
;
2885 * Get the indicator title.
2887 * The title is displayed when a user moves the mouse over the indicator.
2889 * @return {string} Indicator title text
2891 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2892 return this.indicatorTitle
;
2896 * LabelElement is often mixed into other classes to generate a label, which
2897 * helps identify the function of an interface element.
2898 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2900 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2906 * @param {Object} [config] Configuration options
2907 * @cfg {jQuery} [$label] The label element created by the class. If this
2908 * configuration is omitted, the label element will use a generated `<span>`.
2909 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2910 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2911 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2912 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2914 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2915 // Configuration initialization
2916 config
= config
|| {};
2923 this.setLabel( config
.label
|| this.constructor.static.label
);
2924 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2929 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2934 * @event labelChange
2935 * @param {string} value
2938 /* Static Properties */
2941 * The label text. The label can be specified as a plaintext string, a function that will
2942 * produce a string in the future, or `null` for no label. The static value will
2943 * be overridden if a label is specified with the #label config option.
2947 * @property {string|Function|null}
2949 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2951 /* Static methods */
2954 * Highlight the first occurrence of the query in the given text
2956 * @param {string} text Text
2957 * @param {string} query Query to find
2958 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2959 * @return {jQuery} Text with the first match of the query
2960 * sub-string wrapped in highlighted span
2962 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2965 $result
= $( '<span>' );
2969 qLen
= query
.length
;
2970 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2971 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2976 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2979 if ( !query
.length
|| offset
=== -1 ) {
2980 $result
.text( text
);
2983 document
.createTextNode( text
.slice( 0, offset
) ),
2985 .addClass( 'oo-ui-labelElement-label-highlight' )
2986 .text( text
.slice( offset
, offset
+ query
.length
) ),
2987 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2990 return $result
.contents();
2996 * Set the label element.
2998 * If an element is already set, it will be cleaned up before setting up the new element.
3000 * @param {jQuery} $label Element to use as label
3002 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
3003 if ( this.$label
) {
3004 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
3007 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
3008 this.setLabelContent( this.label
);
3014 * An empty string will result in the label being hidden. A string containing only whitespace will
3015 * be converted to a single ` `.
3017 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
3018 * text; or null for no label
3021 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
3022 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
3023 label
= ( ( typeof label
=== 'string' || label
instanceof jQuery
) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
3025 if ( this.label
!== label
) {
3026 if ( this.$label
) {
3027 this.setLabelContent( label
);
3030 this.emit( 'labelChange' );
3033 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
);
3039 * Set the label as plain text with a highlighted query
3041 * @param {string} text Text label to set
3042 * @param {string} query Substring of text to highlight
3043 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
3046 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
3047 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
3053 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
3054 * text; or null for no label
3056 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
3061 * Set the content of the label.
3063 * Do not call this method until after the label element has been set by #setLabelElement.
3066 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
3067 * text; or null for no label
3069 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
3070 if ( typeof label
=== 'string' ) {
3071 if ( label
.match( /^\s*$/ ) ) {
3072 // Convert whitespace only string to a single non-breaking space
3073 this.$label
.html( ' ' );
3075 this.$label
.text( label
);
3077 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
3078 this.$label
.html( label
.toString() );
3079 } else if ( label
instanceof jQuery
) {
3080 this.$label
.empty().append( label
);
3082 this.$label
.empty();
3087 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3088 * additional functionality to an element created by another class. The class provides
3089 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3090 * which are used to customize the look and feel of a widget to better describe its
3091 * importance and functionality.
3093 * The library currently contains the following styling flags for general use:
3095 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
3096 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
3097 * - **constructive**: Constructive styling is deprecated since v0.23.2 and equivalent to progressive.
3099 * The flags affect the appearance of the buttons:
3102 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3103 * var button1 = new OO.ui.ButtonWidget( {
3104 * label: 'Progressive',
3105 * flags: 'progressive'
3107 * var button2 = new OO.ui.ButtonWidget( {
3108 * label: 'Destructive',
3109 * flags: 'destructive'
3111 * $( 'body' ).append( button1.$element, button2.$element );
3113 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
3114 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
3116 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
3122 * @param {Object} [config] Configuration options
3123 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary') to apply.
3124 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
3125 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
3126 * @cfg {jQuery} [$flagged] The flagged element. By default,
3127 * the flagged functionality is applied to the element created by the class ($element).
3128 * If a different element is specified, the flagged functionality will be applied to it instead.
3130 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3131 // Configuration initialization
3132 config
= config
|| {};
3136 this.$flagged
= null;
3139 this.setFlags( config
.flags
);
3140 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3147 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3148 * parameter contains the name of each modified flag and indicates whether it was
3151 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3152 * that the flag was added, `false` that the flag was removed.
3158 * Set the flagged element.
3160 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3161 * If an element is already set, the method will remove the mixin’s effect on that element.
3163 * @param {jQuery} $flagged Element that should be flagged
3165 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3166 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3167 return 'oo-ui-flaggedElement-' + flag
;
3170 if ( this.$flagged
) {
3171 this.$flagged
.removeClass( classNames
);
3174 this.$flagged
= $flagged
.addClass( classNames
);
3178 * Check if the specified flag is set.
3180 * @param {string} flag Name of flag
3181 * @return {boolean} The flag is set
3183 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3184 // This may be called before the constructor, thus before this.flags is set
3185 return this.flags
&& ( flag
in this.flags
);
3189 * Get the names of all flags set.
3191 * @return {string[]} Flag names
3193 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3194 // This may be called before the constructor, thus before this.flags is set
3195 return Object
.keys( this.flags
|| {} );
3204 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3205 var flag
, className
,
3208 classPrefix
= 'oo-ui-flaggedElement-';
3210 for ( flag
in this.flags
) {
3211 className
= classPrefix
+ flag
;
3212 changes
[ flag
] = false;
3213 delete this.flags
[ flag
];
3214 remove
.push( className
);
3217 if ( this.$flagged
) {
3218 this.$flagged
.removeClass( remove
.join( ' ' ) );
3221 this.updateThemeClasses();
3222 this.emit( 'flag', changes
);
3228 * Add one or more flags.
3230 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3231 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3232 * be added (`true`) or removed (`false`).
3236 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3237 var i
, len
, flag
, className
,
3241 classPrefix
= 'oo-ui-flaggedElement-';
3243 if ( typeof flags
=== 'string' ) {
3244 className
= classPrefix
+ flags
;
3246 if ( !this.flags
[ flags
] ) {
3247 this.flags
[ flags
] = true;
3248 add
.push( className
);
3250 } else if ( Array
.isArray( flags
) ) {
3251 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3253 className
= classPrefix
+ flag
;
3255 if ( !this.flags
[ flag
] ) {
3256 changes
[ flag
] = true;
3257 this.flags
[ flag
] = true;
3258 add
.push( className
);
3261 } else if ( OO
.isPlainObject( flags
) ) {
3262 for ( flag
in flags
) {
3263 className
= classPrefix
+ flag
;
3264 if ( flags
[ flag
] ) {
3266 if ( !this.flags
[ flag
] ) {
3267 changes
[ flag
] = true;
3268 this.flags
[ flag
] = true;
3269 add
.push( className
);
3273 if ( this.flags
[ flag
] ) {
3274 changes
[ flag
] = false;
3275 delete this.flags
[ flag
];
3276 remove
.push( className
);
3282 if ( this.$flagged
) {
3284 .addClass( add
.join( ' ' ) )
3285 .removeClass( remove
.join( ' ' ) );
3288 this.updateThemeClasses();
3289 this.emit( 'flag', changes
);
3295 * TitledElement is mixed into other classes to provide a `title` attribute.
3296 * Titles are rendered by the browser and are made visible when the user moves
3297 * the mouse over the element. Titles are not visible on touch devices.
3300 * // TitledElement provides a 'title' attribute to the
3301 * // ButtonWidget class
3302 * var button = new OO.ui.ButtonWidget( {
3303 * label: 'Button with Title',
3304 * title: 'I am a button'
3306 * $( 'body' ).append( button.$element );
3312 * @param {Object} [config] Configuration options
3313 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3314 * If this config is omitted, the title functionality is applied to $element, the
3315 * element created by the class.
3316 * @cfg {string|Function} [title] The title text or a function that returns text. If
3317 * this config is omitted, the value of the {@link #static-title static title} property is used.
3319 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3320 // Configuration initialization
3321 config
= config
|| {};
3324 this.$titled
= null;
3328 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3329 this.setTitledElement( config
.$titled
|| this.$element
);
3334 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3336 /* Static Properties */
3339 * The title text, a function that returns text, or `null` for no title. The value of the static property
3340 * is overridden if the #title config option is used.
3344 * @property {string|Function|null}
3346 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3351 * Set the titled element.
3353 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3354 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3356 * @param {jQuery} $titled Element that should use the 'titled' functionality
3358 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3359 if ( this.$titled
) {
3360 this.$titled
.removeAttr( 'title' );
3363 this.$titled
= $titled
;
3372 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3375 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3376 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3377 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3379 if ( this.title
!== title
) {
3388 * Update the title attribute, in case of changes to title or accessKey.
3393 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3394 var title
= this.getTitle();
3395 if ( this.$titled
) {
3396 if ( title
!== null ) {
3397 // Only if this is an AccessKeyedElement
3398 if ( this.formatTitleWithAccessKey
) {
3399 title
= this.formatTitleWithAccessKey( title
);
3401 this.$titled
.attr( 'title', title
);
3403 this.$titled
.removeAttr( 'title' );
3412 * @return {string} Title string
3414 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3419 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3420 * Accesskeys allow an user to go to a specific element by using
3421 * a shortcut combination of a browser specific keys + the key
3425 * // AccessKeyedElement provides an 'accesskey' attribute to the
3426 * // ButtonWidget class
3427 * var button = new OO.ui.ButtonWidget( {
3428 * label: 'Button with Accesskey',
3431 * $( 'body' ).append( button.$element );
3437 * @param {Object} [config] Configuration options
3438 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3439 * If this config is omitted, the accesskey functionality is applied to $element, the
3440 * element created by the class.
3441 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3442 * this config is omitted, no accesskey will be added.
3444 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3445 // Configuration initialization
3446 config
= config
|| {};
3449 this.$accessKeyed
= null;
3450 this.accessKey
= null;
3453 this.setAccessKey( config
.accessKey
|| null );
3454 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3456 // If this is also a TitledElement and it initialized before we did, we may have
3457 // to update the title with the access key
3458 if ( this.updateTitle
) {
3465 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3467 /* Static Properties */
3470 * The access key, a function that returns a key, or `null` for no accesskey.
3474 * @property {string|Function|null}
3476 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3481 * Set the accesskeyed element.
3483 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3484 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3486 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3488 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3489 if ( this.$accessKeyed
) {
3490 this.$accessKeyed
.removeAttr( 'accesskey' );
3493 this.$accessKeyed
= $accessKeyed
;
3494 if ( this.accessKey
) {
3495 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3502 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3505 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3506 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3508 if ( this.accessKey
!== accessKey
) {
3509 if ( this.$accessKeyed
) {
3510 if ( accessKey
!== null ) {
3511 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3513 this.$accessKeyed
.removeAttr( 'accesskey' );
3516 this.accessKey
= accessKey
;
3518 // Only if this is a TitledElement
3519 if ( this.updateTitle
) {
3530 * @return {string} accessKey string
3532 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3533 return this.accessKey
;
3537 * Add information about the access key to the element's tooltip label.
3538 * (This is only public for hacky usage in FieldLayout.)
3540 * @param {string} title Tooltip label for `title` attribute
3543 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3546 if ( !this.$accessKeyed
) {
3547 // Not initialized yet; the constructor will call updateTitle() which will rerun this function
3550 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the single key
3551 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3552 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3554 accessKey
= this.getAccessKey();
3557 title
+= ' [' + accessKey
+ ']';
3563 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3564 * feels, and functionality can be customized via the class’s configuration options
3565 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3568 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3571 * // A button widget
3572 * var button = new OO.ui.ButtonWidget( {
3573 * label: 'Button with Icon',
3575 * iconTitle: 'Remove'
3577 * $( 'body' ).append( button.$element );
3579 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3582 * @extends OO.ui.Widget
3583 * @mixins OO.ui.mixin.ButtonElement
3584 * @mixins OO.ui.mixin.IconElement
3585 * @mixins OO.ui.mixin.IndicatorElement
3586 * @mixins OO.ui.mixin.LabelElement
3587 * @mixins OO.ui.mixin.TitledElement
3588 * @mixins OO.ui.mixin.FlaggedElement
3589 * @mixins OO.ui.mixin.TabIndexedElement
3590 * @mixins OO.ui.mixin.AccessKeyedElement
3593 * @param {Object} [config] Configuration options
3594 * @cfg {boolean} [active=false] Whether button should be shown as active
3595 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3596 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3597 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3599 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3600 // Configuration initialization
3601 config
= config
|| {};
3603 // Parent constructor
3604 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3606 // Mixin constructors
3607 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3608 OO
.ui
.mixin
.IconElement
.call( this, config
);
3609 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3610 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3611 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3612 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3613 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3614 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3619 this.noFollow
= false;
3622 this.connect( this, { disable
: 'onDisable' } );
3625 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3627 .addClass( 'oo-ui-buttonWidget' )
3628 .append( this.$button
);
3629 this.setActive( config
.active
);
3630 this.setHref( config
.href
);
3631 this.setTarget( config
.target
);
3632 this.setNoFollow( config
.noFollow
);
3637 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3638 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3639 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3640 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3641 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3642 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3643 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3644 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3645 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3647 /* Static Properties */
3653 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3659 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3664 * Get hyperlink location.
3666 * @return {string} Hyperlink location
3668 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3673 * Get hyperlink target.
3675 * @return {string} Hyperlink target
3677 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3682 * Get search engine traversal hint.
3684 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3686 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3687 return this.noFollow
;
3691 * Set hyperlink location.
3693 * @param {string|null} href Hyperlink location, null to remove
3695 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3696 href
= typeof href
=== 'string' ? href
: null;
3697 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3701 if ( href
!== this.href
) {
3710 * Update the `href` attribute, in case of changes to href or
3716 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3717 if ( this.href
!== null && !this.isDisabled() ) {
3718 this.$button
.attr( 'href', this.href
);
3720 this.$button
.removeAttr( 'href' );
3727 * Handle disable events.
3730 * @param {boolean} disabled Element is disabled
3732 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3737 * Set hyperlink target.
3739 * @param {string|null} target Hyperlink target, null to remove
3741 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3742 target
= typeof target
=== 'string' ? target
: null;
3744 if ( target
!== this.target
) {
3745 this.target
= target
;
3746 if ( target
!== null ) {
3747 this.$button
.attr( 'target', target
);
3749 this.$button
.removeAttr( 'target' );
3757 * Set search engine traversal hint.
3759 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3761 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3762 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3764 if ( noFollow
!== this.noFollow
) {
3765 this.noFollow
= noFollow
;
3767 this.$button
.attr( 'rel', 'nofollow' );
3769 this.$button
.removeAttr( 'rel' );
3776 // Override method visibility hints from ButtonElement
3787 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3788 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3789 * removed, and cleared from the group.
3792 * // Example: A ButtonGroupWidget with two buttons
3793 * var button1 = new OO.ui.PopupButtonWidget( {
3794 * label: 'Select a category',
3797 * $content: $( '<p>List of categories...</p>' ),
3802 * var button2 = new OO.ui.ButtonWidget( {
3805 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3806 * items: [button1, button2]
3808 * $( 'body' ).append( buttonGroup.$element );
3811 * @extends OO.ui.Widget
3812 * @mixins OO.ui.mixin.GroupElement
3815 * @param {Object} [config] Configuration options
3816 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3818 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3819 // Configuration initialization
3820 config
= config
|| {};
3822 // Parent constructor
3823 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3825 // Mixin constructors
3826 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3829 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3830 if ( Array
.isArray( config
.items
) ) {
3831 this.addItems( config
.items
);
3837 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3838 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3840 /* Static Properties */
3846 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
3855 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
3856 if ( !this.isDisabled() ) {
3857 if ( this.items
[ 0 ] ) {
3858 this.items
[ 0 ].focus();
3867 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
3872 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3873 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3874 * for a list of icons included in the library.
3877 * // An icon widget with a label
3878 * var myIcon = new OO.ui.IconWidget( {
3882 * // Create a label.
3883 * var iconLabel = new OO.ui.LabelWidget( {
3886 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3888 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3891 * @extends OO.ui.Widget
3892 * @mixins OO.ui.mixin.IconElement
3893 * @mixins OO.ui.mixin.TitledElement
3894 * @mixins OO.ui.mixin.FlaggedElement
3897 * @param {Object} [config] Configuration options
3899 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3900 // Configuration initialization
3901 config
= config
|| {};
3903 // Parent constructor
3904 OO
.ui
.IconWidget
.parent
.call( this, config
);
3906 // Mixin constructors
3907 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3908 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3909 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3912 this.$element
.addClass( 'oo-ui-iconWidget' );
3917 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3918 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3919 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3920 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3922 /* Static Properties */
3928 OO
.ui
.IconWidget
.static.tagName
= 'span';
3931 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3932 * attention to the status of an item or to clarify the function of a control. For a list of
3933 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3936 * // Example of an indicator widget
3937 * var indicator1 = new OO.ui.IndicatorWidget( {
3938 * indicator: 'alert'
3941 * // Create a fieldset layout to add a label
3942 * var fieldset = new OO.ui.FieldsetLayout();
3943 * fieldset.addItems( [
3944 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3946 * $( 'body' ).append( fieldset.$element );
3948 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3951 * @extends OO.ui.Widget
3952 * @mixins OO.ui.mixin.IndicatorElement
3953 * @mixins OO.ui.mixin.TitledElement
3956 * @param {Object} [config] Configuration options
3958 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
3959 // Configuration initialization
3960 config
= config
|| {};
3962 // Parent constructor
3963 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
3965 // Mixin constructors
3966 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
3967 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3970 this.$element
.addClass( 'oo-ui-indicatorWidget' );
3975 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
3976 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
3977 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
3979 /* Static Properties */
3985 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
3988 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3989 * be configured with a `label` option that is set to a string, a label node, or a function:
3991 * - String: a plaintext string
3992 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3993 * label that includes a link or special styling, such as a gray color or additional graphical elements.
3994 * - Function: a function that will produce a string in the future. Functions are used
3995 * in cases where the value of the label is not currently defined.
3997 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3998 * will come into focus when the label is clicked.
4001 * // Examples of LabelWidgets
4002 * var label1 = new OO.ui.LabelWidget( {
4003 * label: 'plaintext label'
4005 * var label2 = new OO.ui.LabelWidget( {
4006 * label: $( '<a href="default.html">jQuery label</a>' )
4008 * // Create a fieldset layout with fields for each example
4009 * var fieldset = new OO.ui.FieldsetLayout();
4010 * fieldset.addItems( [
4011 * new OO.ui.FieldLayout( label1 ),
4012 * new OO.ui.FieldLayout( label2 )
4014 * $( 'body' ).append( fieldset.$element );
4017 * @extends OO.ui.Widget
4018 * @mixins OO.ui.mixin.LabelElement
4019 * @mixins OO.ui.mixin.TitledElement
4022 * @param {Object} [config] Configuration options
4023 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4024 * Clicking the label will focus the specified input field.
4026 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4027 // Configuration initialization
4028 config
= config
|| {};
4030 // Parent constructor
4031 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4033 // Mixin constructors
4034 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
4035 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4038 this.input
= config
.input
;
4042 if ( this.input
.getInputId() ) {
4043 this.$element
.attr( 'for', this.input
.getInputId() );
4045 this.$label
.on( 'click', function () {
4046 this.input
.simulateLabelClick();
4051 this.$element
.addClass( 'oo-ui-labelWidget' );
4056 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4057 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4058 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4060 /* Static Properties */
4066 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4069 * PendingElement is a mixin that is used to create elements that notify users that something is happening
4070 * and that they should wait before proceeding. The pending state is visually represented with a pending
4071 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
4072 * field of a {@link OO.ui.TextInputWidget text input widget}.
4074 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
4075 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
4076 * in process dialogs.
4079 * function MessageDialog( config ) {
4080 * MessageDialog.parent.call( this, config );
4082 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4084 * MessageDialog.static.name = 'myMessageDialog';
4085 * MessageDialog.static.actions = [
4086 * { action: 'save', label: 'Done', flags: 'primary' },
4087 * { label: 'Cancel', flags: 'safe' }
4090 * MessageDialog.prototype.initialize = function () {
4091 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4092 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
4093 * 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>' );
4094 * this.$body.append( this.content.$element );
4096 * MessageDialog.prototype.getBodyHeight = function () {
4099 * MessageDialog.prototype.getActionProcess = function ( action ) {
4100 * var dialog = this;
4101 * if ( action === 'save' ) {
4102 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4103 * return new OO.ui.Process()
4105 * .next( function () {
4106 * dialog.getActions().get({actions: 'save'})[0].popPending();
4109 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4112 * var windowManager = new OO.ui.WindowManager();
4113 * $( 'body' ).append( windowManager.$element );
4115 * var dialog = new MessageDialog();
4116 * windowManager.addWindows( [ dialog ] );
4117 * windowManager.openWindow( dialog );
4123 * @param {Object} [config] Configuration options
4124 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4126 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4127 // Configuration initialization
4128 config
= config
|| {};
4132 this.$pending
= null;
4135 this.setPendingElement( config
.$pending
|| this.$element
);
4140 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4145 * Set the pending element (and clean up any existing one).
4147 * @param {jQuery} $pending The element to set to pending.
4149 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4150 if ( this.$pending
) {
4151 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4154 this.$pending
= $pending
;
4155 if ( this.pending
> 0 ) {
4156 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4161 * Check if an element is pending.
4163 * @return {boolean} Element is pending
4165 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4166 return !!this.pending
;
4170 * Increase the pending counter. The pending state will remain active until the counter is zero
4171 * (i.e., the number of calls to #pushPending and #popPending is the same).
4175 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4176 if ( this.pending
=== 0 ) {
4177 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4178 this.updateThemeClasses();
4186 * Decrease the pending counter. The pending state will remain active until the counter is zero
4187 * (i.e., the number of calls to #pushPending and #popPending is the same).
4191 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4192 if ( this.pending
=== 1 ) {
4193 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4194 this.updateThemeClasses();
4196 this.pending
= Math
.max( 0, this.pending
- 1 );
4202 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4203 * in the document (for example, in an OO.ui.Window's $overlay).
4205 * The elements's position is automatically calculated and maintained when window is resized or the
4206 * page is scrolled. If you reposition the container manually, you have to call #position to make
4207 * sure the element is still placed correctly.
4209 * As positioning is only possible when both the element and the container are attached to the DOM
4210 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4211 * the #toggle method to display a floating popup, for example.
4217 * @param {Object} [config] Configuration options
4218 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4219 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4220 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4221 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4222 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4223 * 'top': Align the top edge with $floatableContainer's top edge
4224 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4225 * 'center': Vertically align the center with $floatableContainer's center
4226 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4227 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4228 * 'after': Directly after $floatableContainer, algining f's start edge with fC's end edge
4229 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4230 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4231 * 'center': Horizontally align the center with $floatableContainer's center
4232 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4235 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4236 // Configuration initialization
4237 config
= config
|| {};
4240 this.$floatable
= null;
4241 this.$floatableContainer
= null;
4242 this.$floatableWindow
= null;
4243 this.$floatableClosestScrollable
= null;
4244 this.onFloatableScrollHandler
= this.position
.bind( this );
4245 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4248 this.setFloatableContainer( config
.$floatableContainer
);
4249 this.setFloatableElement( config
.$floatable
|| this.$element
);
4250 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4251 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4252 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ? true : !!config
.hideWhenOutOfView
;
4258 * Set floatable element.
4260 * If an element is already set, it will be cleaned up before setting up the new element.
4262 * @param {jQuery} $floatable Element to make floatable
4264 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4265 if ( this.$floatable
) {
4266 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4267 this.$floatable
.css( { left
: '', top
: '' } );
4270 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4275 * Set floatable container.
4277 * The element will be positioned relative to the specified container.
4279 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4281 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4282 this.$floatableContainer
= $floatableContainer
;
4283 if ( this.$floatable
) {
4289 * Change how the element is positioned vertically.
4291 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4293 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4294 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4295 throw new Error( 'Invalid value for vertical position: ' + position
);
4297 if ( this.verticalPosition
!== position
) {
4298 this.verticalPosition
= position
;
4299 if ( this.$floatable
) {
4306 * Change how the element is positioned horizontally.
4308 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4310 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4311 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4312 throw new Error( 'Invalid value for horizontal position: ' + position
);
4314 if ( this.horizontalPosition
!== position
) {
4315 this.horizontalPosition
= position
;
4316 if ( this.$floatable
) {
4323 * Toggle positioning.
4325 * Do not turn positioning on until after the element is attached to the DOM and visible.
4327 * @param {boolean} [positioning] Enable positioning, omit to toggle
4330 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4331 var closestScrollableOfContainer
;
4333 if ( !this.$floatable
|| !this.$floatableContainer
) {
4337 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4339 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4340 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4341 this.warnedUnattached
= true;
4344 if ( this.positioning
!== positioning
) {
4345 this.positioning
= positioning
;
4347 this.needsCustomPosition
=
4348 this.verticalPostion
!== 'below' ||
4349 this.horizontalPosition
!== 'start' ||
4350 !OO
.ui
.contains( this.$floatableContainer
[ 0 ], this.$floatable
[ 0 ] );
4352 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
4353 // If the scrollable is the root, we have to listen to scroll events
4354 // on the window because of browser inconsistencies.
4355 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4356 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
4359 if ( positioning
) {
4360 this.$floatableWindow
= $( this.getElementWindow() );
4361 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4363 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4364 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4366 // Initial position after visible
4369 if ( this.$floatableWindow
) {
4370 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4371 this.$floatableWindow
= null;
4374 if ( this.$floatableClosestScrollable
) {
4375 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4376 this.$floatableClosestScrollable
= null;
4379 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4387 * Check whether the bottom edge of the given element is within the viewport of the given container.
4390 * @param {jQuery} $element
4391 * @param {jQuery} $container
4394 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4395 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
, rightEdgeInBounds
,
4396 startEdgeInBounds
, endEdgeInBounds
,
4397 direction
= $element
.css( 'direction' );
4399 elemRect
= $element
[ 0 ].getBoundingClientRect();
4400 if ( $container
[ 0 ] === window
) {
4404 right
: document
.documentElement
.clientWidth
,
4405 bottom
: document
.documentElement
.clientHeight
4408 contRect
= $container
[ 0 ].getBoundingClientRect();
4411 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4412 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4413 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4414 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4415 if ( direction
=== 'rtl' ) {
4416 startEdgeInBounds
= rightEdgeInBounds
;
4417 endEdgeInBounds
= leftEdgeInBounds
;
4419 startEdgeInBounds
= leftEdgeInBounds
;
4420 endEdgeInBounds
= rightEdgeInBounds
;
4423 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4426 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4429 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4432 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4436 // The other positioning values are all about being inside the container,
4437 // so in those cases all we care about is that any part of the container is visible.
4438 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4439 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4443 * Position the floatable below its container.
4445 * This should only be done when both of them are attached to the DOM and visible.
4449 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4450 if ( !this.positioning
) {
4455 // To continue, some things need to be true:
4456 // The element must actually be in the DOM
4457 this.isElementAttached() && (
4458 // The closest scrollable is the current window
4459 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4460 // OR is an element in the element's DOM
4461 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4464 // Abort early if important parts of the widget are no longer attached to the DOM
4468 if ( this.hideWhenOutOfView
&& !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
) ) {
4469 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4472 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4475 if ( !this.needsCustomPosition
) {
4479 this.$floatable
.css( this.computePosition() );
4481 // We updated the position, so re-evaluate the clipping state.
4482 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4483 // will not notice the need to update itself.)
4484 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4485 // it not listen to the right events in the right places?
4494 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4495 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4496 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4498 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4500 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4501 var isBody
, scrollableX
, scrollableY
, containerPos
,
4502 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4503 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4504 direction
= this.$floatableContainer
.css( 'direction' ),
4505 $offsetParent
= this.$floatable
.offsetParent();
4507 if ( $offsetParent
.is( 'html' ) ) {
4508 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4509 // <html> element, but they do work on the <body>
4510 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4512 isBody
= $offsetParent
.is( 'body' );
4513 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' || $offsetParent
.css( 'overflow-x' ) === 'auto';
4514 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' || $offsetParent
.css( 'overflow-y' ) === 'auto';
4516 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4517 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4518 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
4519 // or if it isn't scrollable
4520 scrollTop
= scrollableY
&& !isBody
? $offsetParent
.scrollTop() : 0;
4521 scrollLeft
= scrollableX
&& !isBody
? OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4523 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4524 // if the <body> has a margin
4525 containerPos
= isBody
?
4526 this.$floatableContainer
.offset() :
4527 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4528 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4529 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4530 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4531 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4533 if ( this.verticalPosition
=== 'below' ) {
4534 newPos
.top
= containerPos
.bottom
;
4535 } else if ( this.verticalPosition
=== 'above' ) {
4536 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4537 } else if ( this.verticalPosition
=== 'top' ) {
4538 newPos
.top
= containerPos
.top
;
4539 } else if ( this.verticalPosition
=== 'bottom' ) {
4540 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4541 } else if ( this.verticalPosition
=== 'center' ) {
4542 newPos
.top
= containerPos
.top
+
4543 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4546 if ( this.horizontalPosition
=== 'before' ) {
4547 newPos
.end
= containerPos
.start
;
4548 } else if ( this.horizontalPosition
=== 'after' ) {
4549 newPos
.start
= containerPos
.end
;
4550 } else if ( this.horizontalPosition
=== 'start' ) {
4551 newPos
.start
= containerPos
.start
;
4552 } else if ( this.horizontalPosition
=== 'end' ) {
4553 newPos
.end
= containerPos
.end
;
4554 } else if ( this.horizontalPosition
=== 'center' ) {
4555 newPos
.left
= containerPos
.left
+
4556 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4559 if ( newPos
.start
!== undefined ) {
4560 if ( direction
=== 'rtl' ) {
4561 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.start
;
4563 newPos
.left
= newPos
.start
;
4565 delete newPos
.start
;
4567 if ( newPos
.end
!== undefined ) {
4568 if ( direction
=== 'rtl' ) {
4569 newPos
.left
= newPos
.end
;
4571 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.end
;
4576 // Account for scroll position
4577 if ( newPos
.top
!== '' ) {
4578 newPos
.top
+= scrollTop
;
4580 if ( newPos
.bottom
!== '' ) {
4581 newPos
.bottom
-= scrollTop
;
4583 if ( newPos
.left
!== '' ) {
4584 newPos
.left
+= scrollLeft
;
4586 if ( newPos
.right
!== '' ) {
4587 newPos
.right
-= scrollLeft
;
4590 // Account for scrollbar gutter
4591 if ( newPos
.bottom
!== '' ) {
4592 newPos
.bottom
-= horizScrollbarHeight
;
4594 if ( direction
=== 'rtl' ) {
4595 if ( newPos
.left
!== '' ) {
4596 newPos
.left
-= vertScrollbarWidth
;
4599 if ( newPos
.right
!== '' ) {
4600 newPos
.right
-= vertScrollbarWidth
;
4608 * Element that can be automatically clipped to visible boundaries.
4610 * Whenever the element's natural height changes, you have to call
4611 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4612 * clipping correctly.
4614 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4615 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4616 * then #$clippable will be given a fixed reduced height and/or width and will be made
4617 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4618 * but you can build a static footer by setting #$clippableContainer to an element that contains
4619 * #$clippable and the footer.
4625 * @param {Object} [config] Configuration options
4626 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4627 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4628 * omit to use #$clippable
4630 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4631 // Configuration initialization
4632 config
= config
|| {};
4635 this.$clippable
= null;
4636 this.$clippableContainer
= null;
4637 this.clipping
= false;
4638 this.clippedHorizontally
= false;
4639 this.clippedVertically
= false;
4640 this.$clippableScrollableContainer
= null;
4641 this.$clippableScroller
= null;
4642 this.$clippableWindow
= null;
4643 this.idealWidth
= null;
4644 this.idealHeight
= null;
4645 this.onClippableScrollHandler
= this.clip
.bind( this );
4646 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4649 if ( config
.$clippableContainer
) {
4650 this.setClippableContainer( config
.$clippableContainer
);
4652 this.setClippableElement( config
.$clippable
|| this.$element
);
4658 * Set clippable element.
4660 * If an element is already set, it will be cleaned up before setting up the new element.
4662 * @param {jQuery} $clippable Element to make clippable
4664 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4665 if ( this.$clippable
) {
4666 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4667 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4668 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4671 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4676 * Set clippable container.
4678 * This is the container that will be measured when deciding whether to clip. When clipping,
4679 * #$clippable will be resized in order to keep the clippable container fully visible.
4681 * If the clippable container is unset, #$clippable will be used.
4683 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4685 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4686 this.$clippableContainer
= $clippableContainer
;
4687 if ( this.$clippable
) {
4695 * Do not turn clipping on until after the element is attached to the DOM and visible.
4697 * @param {boolean} [clipping] Enable clipping, omit to toggle
4700 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4701 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4703 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4704 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4705 this.warnedUnattached
= true;
4708 if ( this.clipping
!== clipping
) {
4709 this.clipping
= clipping
;
4711 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4712 // If the clippable container is the root, we have to listen to scroll events and check
4713 // jQuery.scrollTop on the window because of browser inconsistencies
4714 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4715 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4716 this.$clippableScrollableContainer
;
4717 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4718 this.$clippableWindow
= $( this.getElementWindow() )
4719 .on( 'resize', this.onClippableWindowResizeHandler
);
4720 // Initial clip after visible
4723 this.$clippable
.css( {
4731 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4733 this.$clippableScrollableContainer
= null;
4734 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4735 this.$clippableScroller
= null;
4736 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4737 this.$clippableWindow
= null;
4745 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4747 * @return {boolean} Element will be clipped to the visible area
4749 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4750 return this.clipping
;
4754 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4756 * @return {boolean} Part of the element is being clipped
4758 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4759 return this.clippedHorizontally
|| this.clippedVertically
;
4763 * Check if the right of the element is being clipped by the nearest scrollable container.
4765 * @return {boolean} Part of the element is being clipped
4767 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4768 return this.clippedHorizontally
;
4772 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4774 * @return {boolean} Part of the element is being clipped
4776 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4777 return this.clippedVertically
;
4781 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4783 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4784 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4786 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4787 this.idealWidth
= width
;
4788 this.idealHeight
= height
;
4790 if ( !this.clipping
) {
4791 // Update dimensions
4792 this.$clippable
.css( { width
: width
, height
: height
} );
4794 // While clipping, idealWidth and idealHeight are not considered
4798 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4799 * when the element's natural height changes.
4801 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4802 * overlapped by, the visible area of the nearest scrollable container.
4804 * Because calling clip() when the natural height changes isn't always possible, we also set
4805 * max-height when the element isn't being clipped. This means that if the element tries to grow
4806 * beyond the edge, something reasonable will happen before clip() is called.
4810 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
4811 var $container
, extraHeight
, extraWidth
, ccOffset
,
4812 $scrollableContainer
, scOffset
, scHeight
, scWidth
,
4813 ccWidth
, scrollerIsWindow
, scrollTop
, scrollLeft
,
4814 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
4815 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
4816 buffer
= 7; // Chosen by fair dice roll
4818 if ( !this.clipping
) {
4819 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4823 $container
= this.$clippableContainer
|| this.$clippable
;
4824 extraHeight
= $container
.outerHeight() - this.$clippable
.outerHeight();
4825 extraWidth
= $container
.outerWidth() - this.$clippable
.outerWidth();
4826 ccOffset
= $container
.offset();
4827 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
4828 $scrollableContainer
= this.$clippableWindow
;
4829 scOffset
= { top
: 0, left
: 0 };
4831 $scrollableContainer
= this.$clippableScrollableContainer
;
4832 scOffset
= $scrollableContainer
.offset();
4834 scHeight
= $scrollableContainer
.innerHeight() - buffer
;
4835 scWidth
= $scrollableContainer
.innerWidth() - buffer
;
4836 ccWidth
= $container
.outerWidth() + buffer
;
4837 scrollerIsWindow
= this.$clippableScroller
[ 0 ] === this.$clippableWindow
[ 0 ];
4838 scrollTop
= scrollerIsWindow
? this.$clippableScroller
.scrollTop() : 0;
4839 scrollLeft
= scrollerIsWindow
? this.$clippableScroller
.scrollLeft() : 0;
4840 desiredWidth
= ccOffset
.left
< 0 ?
4841 ccWidth
+ ccOffset
.left
:
4842 ( scOffset
.left
+ scrollLeft
+ scWidth
) - ccOffset
.left
;
4843 desiredHeight
= ( scOffset
.top
+ scrollTop
+ scHeight
) - ccOffset
.top
;
4844 // It should never be desirable to exceed the dimensions of the browser viewport... right?
4845 desiredWidth
= Math
.min( desiredWidth
, document
.documentElement
.clientWidth
);
4846 desiredHeight
= Math
.min( desiredHeight
, document
.documentElement
.clientHeight
);
4847 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
4848 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
4849 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
4850 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
4851 clipWidth
= allotedWidth
< naturalWidth
;
4852 clipHeight
= allotedHeight
< naturalHeight
;
4855 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
4856 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
4857 this.$clippable
.css( 'overflowX', 'scroll' );
4858 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
4859 this.$clippable
.css( {
4860 width
: Math
.max( 0, allotedWidth
),
4864 this.$clippable
.css( {
4866 width
: this.idealWidth
|| '',
4867 maxWidth
: Math
.max( 0, allotedWidth
)
4871 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
4872 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
4873 this.$clippable
.css( 'overflowY', 'scroll' );
4874 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
4875 this.$clippable
.css( {
4876 height
: Math
.max( 0, allotedHeight
),
4880 this.$clippable
.css( {
4882 height
: this.idealHeight
|| '',
4883 maxHeight
: Math
.max( 0, allotedHeight
)
4887 // If we stopped clipping in at least one of the dimensions
4888 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
4889 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4892 this.clippedHorizontally
= clipWidth
;
4893 this.clippedVertically
= clipHeight
;
4899 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4900 * By default, each popup has an anchor that points toward its origin.
4901 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4903 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
4906 * // A popup widget.
4907 * var popup = new OO.ui.PopupWidget( {
4908 * $content: $( '<p>Hi there!</p>' ),
4913 * $( 'body' ).append( popup.$element );
4914 * // To display the popup, toggle the visibility to 'true'.
4915 * popup.toggle( true );
4917 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4920 * @extends OO.ui.Widget
4921 * @mixins OO.ui.mixin.LabelElement
4922 * @mixins OO.ui.mixin.ClippableElement
4923 * @mixins OO.ui.mixin.FloatableElement
4926 * @param {Object} [config] Configuration options
4927 * @cfg {number} [width=320] Width of popup in pixels
4928 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4929 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4930 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
4931 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
4932 * of $floatableContainer
4933 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
4934 * of $floatableContainer
4935 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
4936 * endwards (right/left) to the vertical center of $floatableContainer
4937 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
4938 * startwards (left/right) to the vertical center of $floatableContainer
4939 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
4940 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
4941 * as possible while still keeping the anchor within the popup;
4942 * if position is before/after, move the popup as far downwards as possible.
4943 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
4944 * as possible while still keeping the anchor within the popup;
4945 * if position in before/after, move the popup as far upwards as possible.
4946 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center
4947 * of the popup with the center of $floatableContainer.
4948 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
4949 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
4950 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4951 * See the [OOjs UI docs on MediaWiki][3] for an example.
4952 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4953 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4954 * @cfg {jQuery} [$content] Content to append to the popup's body
4955 * @cfg {jQuery} [$footer] Content to append to the popup's footer
4956 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4957 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4958 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4960 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4961 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
4963 * @cfg {boolean} [padded=false] Add padding to the popup's body
4965 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
4966 // Configuration initialization
4967 config
= config
|| {};
4969 // Parent constructor
4970 OO
.ui
.PopupWidget
.parent
.call( this, config
);
4972 // Properties (must be set before ClippableElement constructor call)
4973 this.$body
= $( '<div>' );
4974 this.$popup
= $( '<div>' );
4976 // Mixin constructors
4977 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4978 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
4979 $clippable
: this.$body
,
4980 $clippableContainer
: this.$popup
4982 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
4985 this.$anchor
= $( '<div>' );
4986 // If undefined, will be computed lazily in computePosition()
4987 this.$container
= config
.$container
;
4988 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
4989 this.autoClose
= !!config
.autoClose
;
4990 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
4991 this.transitionTimeout
= null;
4992 this.anchored
= false;
4993 this.width
= config
.width
!== undefined ? config
.width
: 320;
4994 this.height
= config
.height
!== undefined ? config
.height
: null;
4995 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
4996 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
4999 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5000 this.setAlignment( config
.align
|| 'center' );
5001 this.setPosition( config
.position
|| 'below' );
5002 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5003 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5005 .addClass( 'oo-ui-popupWidget-popup' )
5006 .append( this.$body
);
5008 .addClass( 'oo-ui-popupWidget' )
5009 .append( this.$popup
, this.$anchor
);
5010 // Move content, which was added to #$element by OO.ui.Widget, to the body
5011 // FIXME This is gross, we should use '$body' or something for the config
5012 if ( config
.$content
instanceof jQuery
) {
5013 this.$body
.append( config
.$content
);
5016 if ( config
.padded
) {
5017 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5020 if ( config
.head
) {
5021 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
5022 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
5023 this.$head
= $( '<div>' )
5024 .addClass( 'oo-ui-popupWidget-head' )
5025 .append( this.$label
, this.closeButton
.$element
);
5026 this.$popup
.prepend( this.$head
);
5029 if ( config
.$footer
) {
5030 this.$footer
= $( '<div>' )
5031 .addClass( 'oo-ui-popupWidget-footer' )
5032 .append( config
.$footer
);
5033 this.$popup
.append( this.$footer
);
5036 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5037 // that reference properties not initialized at that time of parent class construction
5038 // TODO: Find a better way to handle post-constructor setup
5039 this.visible
= false;
5040 this.$element
.addClass( 'oo-ui-element-hidden' );
5045 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5046 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5047 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5048 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5055 * The popup is ready: it is visible and has been positioned and clipped.
5061 * Handles mouse down events.
5064 * @param {MouseEvent} e Mouse down event
5066 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
5069 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5071 this.toggle( false );
5076 * Bind mouse down listener.
5080 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
5081 // Capture clicks outside popup
5082 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
5086 * Handles close button click events.
5090 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5091 if ( this.isVisible() ) {
5092 this.toggle( false );
5097 * Unbind mouse down listener.
5101 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
5102 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
5106 * Handles key down events.
5109 * @param {KeyboardEvent} e Key down event
5111 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5113 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5116 this.toggle( false );
5118 e
.stopPropagation();
5123 * Bind key down listener.
5127 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
5128 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5132 * Unbind key down listener.
5136 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
5137 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5141 * Show, hide, or toggle the visibility of the anchor.
5143 * @param {boolean} [show] Show anchor, omit to toggle
5145 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5146 show
= show
=== undefined ? !this.anchored
: !!show
;
5148 if ( this.anchored
!== show
) {
5150 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5151 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5153 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5154 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5156 this.anchored
= show
;
5160 * Change which edge the anchor appears on.
5162 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5164 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5165 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5166 throw new Error( 'Invalid value for edge: ' + edge
);
5168 if ( this.anchorEdge
!== null ) {
5169 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5171 this.anchorEdge
= edge
;
5172 if ( this.anchored
) {
5173 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5178 * Check if the anchor is visible.
5180 * @return {boolean} Anchor is visible
5182 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5183 return this.anchored
;
5187 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5188 * `.toggle( true )` after its #$element is attached to the DOM.
5190 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5191 * it in the right place and with the right dimensions only work correctly while it is attached.
5192 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5193 * strictly enforced, so currently it only generates a warning in the browser console.
5198 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5200 show
= show
=== undefined ? !this.isVisible() : !!show
;
5202 change
= show
!== this.isVisible();
5204 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5205 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5206 this.warnedUnattached
= true;
5208 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5209 // Fall back to the parent node if the floatableContainer is not set
5210 this.setFloatableContainer( this.$element
.parent() );
5214 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5217 this.togglePositioning( show
&& !!this.$floatableContainer
);
5220 if ( this.autoClose
) {
5221 this.bindMouseDownListener();
5222 this.bindKeyDownListener();
5224 this.updateDimensions();
5225 this.toggleClipping( true );
5226 this.emit( 'ready' );
5228 this.toggleClipping( false );
5229 if ( this.autoClose
) {
5230 this.unbindMouseDownListener();
5231 this.unbindKeyDownListener();
5240 * Set the size of the popup.
5242 * Changing the size may also change the popup's position depending on the alignment.
5244 * @param {number} width Width in pixels
5245 * @param {number} height Height in pixels
5246 * @param {boolean} [transition=false] Use a smooth transition
5249 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5251 this.height
= height
!== undefined ? height
: null;
5252 if ( this.isVisible() ) {
5253 this.updateDimensions( transition
);
5258 * Update the size and position.
5260 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5261 * be called automatically.
5263 * @param {boolean} [transition=false] Use a smooth transition
5266 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5269 // Prevent transition from being interrupted
5270 clearTimeout( this.transitionTimeout
);
5272 // Enable transition
5273 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5279 // Prevent transitioning after transition is complete
5280 this.transitionTimeout
= setTimeout( function () {
5281 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5284 // Prevent transitioning immediately
5285 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5292 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5293 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
, anchorPos
,
5294 anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
, floatablePos
,
5295 offsetParentPos
, containerPos
,
5297 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5300 'force-left': 'backwards',
5301 'force-right': 'forwards'
5304 'force-left': 'forwards',
5305 'force-right': 'backwards'
5317 backwards
: this.anchored
? 'before' : 'end'
5325 if ( !this.$container
) {
5326 // Lazy-initialize $container if not specified in constructor
5327 this.$container
= $( this.getClosestScrollableElementContainer() );
5329 direction
= this.$container
.css( 'direction' );
5331 // Set height and width before we do anything else, since it might cause our measurements
5332 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5335 height
: this.height
!== null ? this.height
: 'auto'
5338 align
= alignMap
[ direction
][ this.align
] || this.align
;
5339 // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
5340 vertical
= this.popupPosition
=== 'before' || this.popupPosition
=== 'after';
5341 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5342 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5343 near
= vertical
? 'top' : 'left';
5344 far
= vertical
? 'bottom' : 'right';
5345 sizeProp
= vertical
? 'Height' : 'Width';
5346 popupSize
= vertical
? ( this.height
|| this.$popup
.height() ) : this.width
;
5348 this.setAnchorEdge( anchorEdgeMap
[ this.popupPosition
] );
5349 this.horizontalPosition
= vertical
? this.popupPosition
: hPosMap
[ align
];
5350 this.verticalPosition
= vertical
? vPosMap
[ align
] : this.popupPosition
;
5353 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5354 // Find out which property FloatableElement used for positioning, and adjust that value
5355 positionProp
= vertical
?
5356 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5357 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5359 // Figure out where the near and far edges of the popup and $floatableContainer are
5360 floatablePos
= this.$floatableContainer
.offset();
5361 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5362 // Measure where the offsetParent is and compute our position based on that and parentPosition
5363 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5364 { top
: 0, left
: 0 } :
5365 this.$element
.offsetParent().offset();
5367 if ( positionProp
=== near
) {
5368 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5369 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5371 popupPos
[ far
] = offsetParentPos
[ near
] +
5372 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5373 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5376 if ( this.anchored
) {
5377 // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
5378 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5379 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5381 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
5382 // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
5383 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5384 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5385 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5386 // Not enough space for the anchor on the start side; pull the popup startwards
5387 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5388 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5389 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5390 // Not enough space for the anchor on the end side; pull the popup endwards
5391 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5392 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5394 positionAdjustment
= 0;
5397 positionAdjustment
= 0;
5400 // Check if the popup will go beyond the edge of this.$container
5401 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5402 { top
: 0, left
: 0 } :
5403 this.$container
.offset();
5404 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5405 // Take into account how much the popup will move because of the adjustments we're going to make
5406 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5407 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5408 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5409 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5410 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5411 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5412 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5413 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5414 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5415 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5418 if ( this.anchored
) {
5419 // Adjust anchorOffset for positionAdjustment
5420 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5422 // Position the anchor
5423 anchorCss
[ start
] = anchorOffset
;
5424 this.$anchor
.css( anchorCss
);
5427 // Move the popup if needed
5428 parentPosition
[ positionProp
] += positionAdjustment
;
5430 return parentPosition
;
5434 * Set popup alignment
5436 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5437 * `backwards` or `forwards`.
5439 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5440 // Validate alignment
5441 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5444 this.align
= 'center';
5450 * Get popup alignment
5452 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5453 * `backwards` or `forwards`.
5455 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5460 * Change the positioning of the popup.
5462 * @param {string} position 'above', 'below', 'before' or 'after'
5464 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5465 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5468 this.popupPosition
= position
;
5473 * Get popup positioning.
5475 * @return {string} 'above', 'below', 'before' or 'after'
5477 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5478 return this.popupPosition
;
5482 * Get an ID of the body element, this can be used as the
5483 * `aria-describedby` attribute for an input field.
5485 * @return {string} The ID of the body element
5487 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
5488 var id
= this.$body
.attr( 'id' );
5489 if ( id
=== undefined ) {
5490 id
= OO
.ui
.generateElementId();
5491 this.$body
.attr( 'id', id
);
5497 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5498 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5499 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5500 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5506 * @param {Object} [config] Configuration options
5507 * @cfg {Object} [popup] Configuration to pass to popup
5508 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5510 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5511 // Configuration initialization
5512 config
= config
|| {};
5515 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5518 $floatableContainer
: this.$element
5522 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5532 * @return {OO.ui.PopupWidget} Popup widget
5534 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5539 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5540 * which is used to display additional information or options.
5543 * // Example of a popup button.
5544 * var popupButton = new OO.ui.PopupButtonWidget( {
5545 * label: 'Popup button with options',
5548 * $content: $( '<p>Additional options here.</p>' ),
5550 * align: 'force-left'
5553 * // Append the button to the DOM.
5554 * $( 'body' ).append( popupButton.$element );
5557 * @extends OO.ui.ButtonWidget
5558 * @mixins OO.ui.mixin.PopupElement
5561 * @param {Object} [config] Configuration options
5562 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
5563 * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
5564 * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
5565 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
5567 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
5568 // Configuration initialization
5569 config
= config
|| {};
5571 // Parent constructor
5572 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
5574 // Mixin constructors
5575 OO
.ui
.mixin
.PopupElement
.call( this, config
);
5578 this.$overlay
= config
.$overlay
|| this.$element
;
5581 this.connect( this, { click
: 'onAction' } );
5585 .addClass( 'oo-ui-popupButtonWidget' )
5586 .attr( 'aria-haspopup', 'true' );
5588 .addClass( 'oo-ui-popupButtonWidget-popup' )
5589 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
5590 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
5591 this.$overlay
.append( this.popup
.$element
);
5596 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
5597 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
5602 * Handle the button action being triggered.
5606 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
5607 this.popup
.toggle();
5611 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
5613 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
5618 * @mixins OO.ui.mixin.GroupElement
5621 * @param {Object} [config] Configuration options
5623 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
5624 // Mixin constructors
5625 OO
.ui
.mixin
.GroupElement
.call( this, config
);
5630 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
5635 * Set the disabled state of the widget.
5637 * This will also update the disabled state of child widgets.
5639 * @param {boolean} disabled Disable widget
5642 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
5646 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
5647 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
5649 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
5651 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5652 this.items
[ i
].updateDisabled();
5660 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
5662 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
5663 * allows bidirectional communication.
5665 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
5673 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
5680 * Check if widget is disabled.
5682 * Checks parent if present, making disabled state inheritable.
5684 * @return {boolean} Widget is disabled
5686 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
5687 return this.disabled
||
5688 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
5692 * Set group element is in.
5694 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
5697 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
5699 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
5700 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
5702 // Initialize item disabled states
5703 this.updateDisabled();
5709 * OptionWidgets are special elements that can be selected and configured with data. The
5710 * data is often unique for each option, but it does not have to be. OptionWidgets are used
5711 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
5712 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
5714 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5717 * @extends OO.ui.Widget
5718 * @mixins OO.ui.mixin.ItemWidget
5719 * @mixins OO.ui.mixin.LabelElement
5720 * @mixins OO.ui.mixin.FlaggedElement
5721 * @mixins OO.ui.mixin.AccessKeyedElement
5724 * @param {Object} [config] Configuration options
5726 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
5727 // Configuration initialization
5728 config
= config
|| {};
5730 // Parent constructor
5731 OO
.ui
.OptionWidget
.parent
.call( this, config
);
5733 // Mixin constructors
5734 OO
.ui
.mixin
.ItemWidget
.call( this );
5735 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5736 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
5737 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
5740 this.selected
= false;
5741 this.highlighted
= false;
5742 this.pressed
= false;
5746 .data( 'oo-ui-optionWidget', this )
5747 // Allow programmatic focussing (and by accesskey), but not tabbing
5748 .attr( 'tabindex', '-1' )
5749 .attr( 'role', 'option' )
5750 .attr( 'aria-selected', 'false' )
5751 .addClass( 'oo-ui-optionWidget' )
5752 .append( this.$label
);
5757 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
5758 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
5759 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
5760 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
5761 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
5763 /* Static Properties */
5766 * Whether this option can be selected. See #setSelected.
5770 * @property {boolean}
5772 OO
.ui
.OptionWidget
.static.selectable
= true;
5775 * Whether this option can be highlighted. See #setHighlighted.
5779 * @property {boolean}
5781 OO
.ui
.OptionWidget
.static.highlightable
= true;
5784 * Whether this option can be pressed. See #setPressed.
5788 * @property {boolean}
5790 OO
.ui
.OptionWidget
.static.pressable
= true;
5793 * Whether this option will be scrolled into view when it is selected.
5797 * @property {boolean}
5799 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
5804 * Check if the option can be selected.
5806 * @return {boolean} Item is selectable
5808 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
5809 return this.constructor.static.selectable
&& !this.isDisabled() && this.isVisible();
5813 * Check if the option can be highlighted. A highlight indicates that the option
5814 * may be selected when a user presses enter or clicks. Disabled items cannot
5817 * @return {boolean} Item is highlightable
5819 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
5820 return this.constructor.static.highlightable
&& !this.isDisabled() && this.isVisible();
5824 * Check if the option can be pressed. The pressed state occurs when a user mouses
5825 * down on an item, but has not yet let go of the mouse.
5827 * @return {boolean} Item is pressable
5829 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
5830 return this.constructor.static.pressable
&& !this.isDisabled() && this.isVisible();
5834 * Check if the option is selected.
5836 * @return {boolean} Item is selected
5838 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
5839 return this.selected
;
5843 * Check if the option is highlighted. A highlight indicates that the
5844 * item may be selected when a user presses enter or clicks.
5846 * @return {boolean} Item is highlighted
5848 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
5849 return this.highlighted
;
5853 * Check if the option is pressed. The pressed state occurs when a user mouses
5854 * down on an item, but has not yet let go of the mouse. The item may appear
5855 * selected, but it will not be selected until the user releases the mouse.
5857 * @return {boolean} Item is pressed
5859 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
5860 return this.pressed
;
5864 * Set the option’s selected state. In general, all modifications to the selection
5865 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
5866 * method instead of this method.
5868 * @param {boolean} [state=false] Select option
5871 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
5872 if ( this.constructor.static.selectable
) {
5873 this.selected
= !!state
;
5875 .toggleClass( 'oo-ui-optionWidget-selected', state
)
5876 .attr( 'aria-selected', state
.toString() );
5877 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
5878 this.scrollElementIntoView();
5880 this.updateThemeClasses();
5886 * Set the option’s highlighted state. In general, all programmatic
5887 * modifications to the highlight should be handled by the
5888 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
5889 * method instead of this method.
5891 * @param {boolean} [state=false] Highlight option
5894 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
5895 if ( this.constructor.static.highlightable
) {
5896 this.highlighted
= !!state
;
5897 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
5898 this.updateThemeClasses();
5904 * Set the option’s pressed state. In general, all
5905 * programmatic modifications to the pressed state should be handled by the
5906 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
5907 * method instead of this method.
5909 * @param {boolean} [state=false] Press option
5912 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
5913 if ( this.constructor.static.pressable
) {
5914 this.pressed
= !!state
;
5915 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
5916 this.updateThemeClasses();
5922 * Get text to match search strings against.
5924 * The default implementation returns the label text, but subclasses
5925 * can override this to provide more complex behavior.
5927 * @return {string|boolean} String to match search string against
5929 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
5930 var label
= this.getLabel();
5931 return typeof label
=== 'string' ? label
: this.$label
.text();
5935 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
5936 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
5937 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
5940 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
5941 * information, please see the [OOjs UI documentation on MediaWiki][1].
5944 * // Example of a select widget with three options
5945 * var select = new OO.ui.SelectWidget( {
5947 * new OO.ui.OptionWidget( {
5949 * label: 'Option One',
5951 * new OO.ui.OptionWidget( {
5953 * label: 'Option Two',
5955 * new OO.ui.OptionWidget( {
5957 * label: 'Option Three',
5961 * $( 'body' ).append( select.$element );
5963 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5967 * @extends OO.ui.Widget
5968 * @mixins OO.ui.mixin.GroupWidget
5971 * @param {Object} [config] Configuration options
5972 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
5973 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
5974 * the [OOjs UI documentation on MediaWiki] [2] for examples.
5975 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5977 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
5978 // Configuration initialization
5979 config
= config
|| {};
5981 // Parent constructor
5982 OO
.ui
.SelectWidget
.parent
.call( this, config
);
5984 // Mixin constructors
5985 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
5988 this.pressed
= false;
5989 this.selecting
= null;
5990 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
5991 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
5992 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
5993 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
5994 this.keyPressBuffer
= '';
5995 this.keyPressBufferTimer
= null;
5996 this.blockMouseOverEvents
= 0;
5999 this.connect( this, {
6003 focusin
: this.onFocus
.bind( this ),
6004 mousedown
: this.onMouseDown
.bind( this ),
6005 mouseover
: this.onMouseOver
.bind( this ),
6006 mouseleave
: this.onMouseLeave
.bind( this )
6011 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
6012 .attr( 'role', 'listbox' );
6013 this.setFocusOwner( this.$element
);
6014 if ( Array
.isArray( config
.items
) ) {
6015 this.addItems( config
.items
);
6021 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6022 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6029 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6031 * @param {OO.ui.OptionWidget|null} item Highlighted item
6037 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6038 * pressed state of an option.
6040 * @param {OO.ui.OptionWidget|null} item Pressed item
6046 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
6048 * @param {OO.ui.OptionWidget|null} item Selected item
6053 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6054 * @param {OO.ui.OptionWidget} item Chosen item
6060 * An `add` event is emitted when options are added to the select with the #addItems method.
6062 * @param {OO.ui.OptionWidget[]} items Added items
6063 * @param {number} index Index of insertion point
6069 * A `remove` event is emitted when options are removed from the select with the #clearItems
6070 * or #removeItems methods.
6072 * @param {OO.ui.OptionWidget[]} items Removed items
6078 * Handle focus events
6081 * @param {jQuery.Event} event
6083 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6085 if ( event
.target
=== this.$element
[ 0 ] ) {
6086 // This widget was focussed, e.g. by the user tabbing to it.
6087 // The styles for focus state depend on one of the items being selected.
6088 if ( !this.getSelectedItem() ) {
6089 item
= this.findFirstSelectableItem();
6092 if ( event
.target
.tabIndex
=== -1 ) {
6093 // One of the options got focussed (and the event bubbled up here).
6094 // They can't be tabbed to, but they can be activated using accesskeys.
6095 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6096 item
= this.findTargetItem( event
);
6098 // There is something actually user-focusable in one of the labels of the options, and the
6099 // user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change the focus).
6105 if ( item
.constructor.static.highlightable
) {
6106 this.highlightItem( item
);
6108 this.selectItem( item
);
6112 if ( event
.target
!== this.$element
[ 0 ] ) {
6113 this.$focusOwner
.focus();
6118 * Handle mouse down events.
6121 * @param {jQuery.Event} e Mouse down event
6123 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6126 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6127 this.togglePressed( true );
6128 item
= this.findTargetItem( e
);
6129 if ( item
&& item
.isSelectable() ) {
6130 this.pressItem( item
);
6131 this.selecting
= item
;
6132 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
6133 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
6140 * Handle mouse up events.
6143 * @param {MouseEvent} e Mouse up event
6145 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
6148 this.togglePressed( false );
6149 if ( !this.selecting
) {
6150 item
= this.findTargetItem( e
);
6151 if ( item
&& item
.isSelectable() ) {
6152 this.selecting
= item
;
6155 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6156 this.pressItem( null );
6157 this.chooseItem( this.selecting
);
6158 this.selecting
= null;
6161 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
6162 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
6168 * Handle mouse move events.
6171 * @param {MouseEvent} e Mouse move event
6173 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
6176 if ( !this.isDisabled() && this.pressed
) {
6177 item
= this.findTargetItem( e
);
6178 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6179 this.pressItem( item
);
6180 this.selecting
= item
;
6186 * Handle mouse over events.
6189 * @param {jQuery.Event} e Mouse over event
6191 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6193 if ( this.blockMouseOverEvents
) {
6196 if ( !this.isDisabled() ) {
6197 item
= this.findTargetItem( e
);
6198 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6204 * Handle mouse leave events.
6207 * @param {jQuery.Event} e Mouse over event
6209 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6210 if ( !this.isDisabled() ) {
6211 this.highlightItem( null );
6217 * Handle key down events.
6220 * @param {KeyboardEvent} e Key down event
6222 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
6225 currentItem
= this.findHighlightedItem() || this.getSelectedItem();
6227 if ( !this.isDisabled() && this.isVisible() ) {
6228 switch ( e
.keyCode
) {
6229 case OO
.ui
.Keys
.ENTER
:
6230 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6231 // Was only highlighted, now let's select it. No-op if already selected.
6232 this.chooseItem( currentItem
);
6237 case OO
.ui
.Keys
.LEFT
:
6238 this.clearKeyPressBuffer();
6239 nextItem
= this.findRelativeSelectableItem( currentItem
, -1 );
6242 case OO
.ui
.Keys
.DOWN
:
6243 case OO
.ui
.Keys
.RIGHT
:
6244 this.clearKeyPressBuffer();
6245 nextItem
= this.findRelativeSelectableItem( currentItem
, 1 );
6248 case OO
.ui
.Keys
.ESCAPE
:
6249 case OO
.ui
.Keys
.TAB
:
6250 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6251 currentItem
.setHighlighted( false );
6253 this.unbindKeyDownListener();
6254 this.unbindKeyPressListener();
6255 // Don't prevent tabbing away / defocusing
6261 if ( nextItem
.constructor.static.highlightable
) {
6262 this.highlightItem( nextItem
);
6264 this.chooseItem( nextItem
);
6266 this.scrollItemIntoView( nextItem
);
6271 e
.stopPropagation();
6277 * Bind key down listener.
6281 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
6282 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
6286 * Unbind key down listener.
6290 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
6291 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
6295 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6297 * @param {OO.ui.OptionWidget} item Item to scroll into view
6299 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6301 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
6302 // and around 100-150 ms after it is finished.
6303 this.blockMouseOverEvents
++;
6304 item
.scrollElementIntoView().done( function () {
6305 setTimeout( function () {
6306 widget
.blockMouseOverEvents
--;
6312 * Clear the key-press buffer
6316 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6317 if ( this.keyPressBufferTimer
) {
6318 clearTimeout( this.keyPressBufferTimer
);
6319 this.keyPressBufferTimer
= null;
6321 this.keyPressBuffer
= '';
6325 * Handle key press events.
6328 * @param {KeyboardEvent} e Key press event
6330 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
6331 var c
, filter
, item
;
6333 if ( !e
.charCode
) {
6334 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6335 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6340 if ( String
.fromCodePoint
) {
6341 c
= String
.fromCodePoint( e
.charCode
);
6343 c
= String
.fromCharCode( e
.charCode
);
6346 if ( this.keyPressBufferTimer
) {
6347 clearTimeout( this.keyPressBufferTimer
);
6349 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6351 item
= this.findHighlightedItem() || this.getSelectedItem();
6353 if ( this.keyPressBuffer
=== c
) {
6354 // Common (if weird) special case: typing "xxxx" will cycle through all
6355 // the items beginning with "x".
6357 item
= this.findRelativeSelectableItem( item
, 1 );
6360 this.keyPressBuffer
+= c
;
6363 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6364 if ( !item
|| !filter( item
) ) {
6365 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6368 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6369 this.highlightItem( item
);
6371 this.chooseItem( item
);
6373 this.scrollItemIntoView( item
);
6377 e
.stopPropagation();
6381 * Get a matcher for the specific string
6384 * @param {string} s String to match against items
6385 * @param {boolean} [exact=false] Only accept exact matches
6386 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6388 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
6391 if ( s
.normalize
) {
6394 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
6395 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
6399 re
= new RegExp( re
, 'i' );
6400 return function ( item
) {
6401 var matchText
= item
.getMatchText();
6402 if ( matchText
.normalize
) {
6403 matchText
= matchText
.normalize();
6405 return re
.test( matchText
);
6410 * Bind key press listener.
6414 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
6415 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
6419 * Unbind key down listener.
6421 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6426 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
6427 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
6428 this.clearKeyPressBuffer();
6432 * Visibility change handler
6435 * @param {boolean} visible
6437 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6439 this.clearKeyPressBuffer();
6444 * Get the closest item to a jQuery.Event.
6447 * @param {jQuery.Event} e
6448 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6450 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
6451 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
6452 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
6455 return $option
.data( 'oo-ui-optionWidget' ) || null;
6459 * Get selected item.
6461 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6463 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
6466 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6467 if ( this.items
[ i
].isSelected() ) {
6468 return this.items
[ i
];
6475 * Find highlighted item.
6477 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6479 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
6482 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6483 if ( this.items
[ i
].isHighlighted() ) {
6484 return this.items
[ i
];
6491 * Get highlighted item.
6493 * @deprecated 0.23.1 Use {@link #findHighlightedItem} instead.
6494 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6496 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
6497 OO
.ui
.warnDeprecation( 'SelectWidget#getHighlightedItem: Deprecated function. Use findHighlightedItem instead. See T76630.' );
6498 return this.findHighlightedItem();
6502 * Toggle pressed state.
6504 * Press is a state that occurs when a user mouses down on an item, but
6505 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
6506 * until the user releases the mouse.
6508 * @param {boolean} pressed An option is being pressed
6510 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
6511 if ( pressed
=== undefined ) {
6512 pressed
= !this.pressed
;
6514 if ( pressed
!== this.pressed
) {
6516 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
6517 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
6518 this.pressed
= pressed
;
6523 * Highlight an option. If the `item` param is omitted, no options will be highlighted
6524 * and any existing highlight will be removed. The highlight is mutually exclusive.
6526 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
6530 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
6531 var i
, len
, highlighted
,
6534 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6535 highlighted
= this.items
[ i
] === item
;
6536 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
6537 this.items
[ i
].setHighlighted( highlighted
);
6543 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
6545 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
6547 this.emit( 'highlight', item
);
6554 * Fetch an item by its label.
6556 * @param {string} label Label of the item to select.
6557 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6558 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
6560 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
6562 len
= this.items
.length
,
6563 filter
= this.getItemMatcher( label
, true );
6565 for ( i
= 0; i
< len
; i
++ ) {
6566 item
= this.items
[ i
];
6567 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6574 filter
= this.getItemMatcher( label
, false );
6575 for ( i
= 0; i
< len
; i
++ ) {
6576 item
= this.items
[ i
];
6577 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6593 * Programmatically select an option by its label. If the item does not exist,
6594 * all options will be deselected.
6596 * @param {string} [label] Label of the item to select.
6597 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6601 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
6602 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
6603 if ( label
=== undefined || !itemFromLabel
) {
6604 return this.selectItem();
6606 return this.selectItem( itemFromLabel
);
6610 * Programmatically select an option by its data. If the `data` parameter is omitted,
6611 * or if the item does not exist, all options will be deselected.
6613 * @param {Object|string} [data] Value of the item to select, omit to deselect all
6617 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
6618 var itemFromData
= this.getItemFromData( data
);
6619 if ( data
=== undefined || !itemFromData
) {
6620 return this.selectItem();
6622 return this.selectItem( itemFromData
);
6626 * Programmatically select an option by its reference. If the `item` parameter is omitted,
6627 * all options will be deselected.
6629 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
6633 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
6634 var i
, len
, selected
,
6637 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6638 selected
= this.items
[ i
] === item
;
6639 if ( this.items
[ i
].isSelected() !== selected
) {
6640 this.items
[ i
].setSelected( selected
);
6645 if ( item
&& !item
.constructor.static.highlightable
) {
6647 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
6649 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
6652 this.emit( 'select', item
);
6661 * Press is a state that occurs when a user mouses down on an item, but has not
6662 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
6663 * releases the mouse.
6665 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
6669 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
6670 var i
, len
, pressed
,
6673 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6674 pressed
= this.items
[ i
] === item
;
6675 if ( this.items
[ i
].isPressed() !== pressed
) {
6676 this.items
[ i
].setPressed( pressed
);
6681 this.emit( 'press', item
);
6690 * Note that ‘choose’ should never be modified programmatically. A user can choose
6691 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
6692 * use the #selectItem method.
6694 * This method is identical to #selectItem, but may vary in subclasses that take additional action
6695 * when users choose an item with the keyboard or mouse.
6697 * @param {OO.ui.OptionWidget} item Item to choose
6701 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
6703 this.selectItem( item
);
6704 this.emit( 'choose', item
);
6711 * Find an option by its position relative to the specified item (or to the start of the option array,
6712 * if item is `null`). The direction in which to search through the option array is specified with a
6713 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6714 * `null` if there are no options in the array.
6716 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6717 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6718 * @param {Function} [filter] Only consider items for which this function returns
6719 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
6720 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
6722 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
6723 var currentIndex
, nextIndex
, i
,
6724 increase
= direction
> 0 ? 1 : -1,
6725 len
= this.items
.length
;
6727 if ( item
instanceof OO
.ui
.OptionWidget
) {
6728 currentIndex
= this.items
.indexOf( item
);
6729 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
6731 // If no item is selected and moving forward, start at the beginning.
6732 // If moving backward, start at the end.
6733 nextIndex
= direction
> 0 ? 0 : len
- 1;
6736 for ( i
= 0; i
< len
; i
++ ) {
6737 item
= this.items
[ nextIndex
];
6739 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
6740 ( !filter
|| filter( item
) )
6744 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
6750 * Get an option by its position relative to the specified item (or to the start of the option array,
6751 * if item is `null`). The direction in which to search through the option array is specified with a
6752 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6753 * `null` if there are no options in the array.
6755 * @deprecated 0.23.1 Use {@link #findRelativeSelectableItem} instead
6756 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6757 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6758 * @param {Function} [filter] Only consider items for which this function returns
6759 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
6760 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
6762 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
, filter
) {
6763 OO
.ui
.warnDeprecation( 'SelectWidget#getRelativeSelectableItem: Deprecated function. Use findRelativeSelectableItem instead. See T76630.' );
6764 return this.findRelativeSelectableItem( item
, direction
, filter
);
6768 * Find the next selectable item or `null` if there are no selectable items.
6769 * Disabled options and menu-section markers and breaks are not selectable.
6771 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
6773 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
6774 return this.findRelativeSelectableItem( null, 1 );
6778 * Get the next selectable item or `null` if there are no selectable items.
6779 * Disabled options and menu-section markers and breaks are not selectable.
6781 * @deprecated 0.23.1 Use {@link OO.ui.SelectWidget#findFirstSelectableItem} instead.
6782 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
6784 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
6785 OO
.ui
.warnDeprecation( 'SelectWidget#getFirstSelectableItem: Deprecated function. Use findFirstSelectableItem instead. See T76630.' );
6786 return this.findFirstSelectableItem();
6790 * Add an array of options to the select. Optionally, an index number can be used to
6791 * specify an insertion point.
6793 * @param {OO.ui.OptionWidget[]} items Items to add
6794 * @param {number} [index] Index to insert items after
6798 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
6800 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
6802 // Always provide an index, even if it was omitted
6803 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
6809 * Remove the specified array of options from the select. Options will be detached
6810 * from the DOM, not removed, so they can be reused later. To remove all options from
6811 * the select, you may wish to use the #clearItems method instead.
6813 * @param {OO.ui.OptionWidget[]} items Items to remove
6817 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
6820 // Deselect items being removed
6821 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
6823 if ( item
.isSelected() ) {
6824 this.selectItem( null );
6829 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
6831 this.emit( 'remove', items
);
6837 * Clear all options from the select. Options will be detached from the DOM, not removed,
6838 * so that they can be reused later. To remove a subset of options from the select, use
6839 * the #removeItems method.
6844 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
6845 var items
= this.items
.slice();
6848 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
6851 this.selectItem( null );
6853 this.emit( 'remove', items
);
6859 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
6861 * Currently this is just used to set `aria-activedescendant` on it.
6864 * @param {jQuery} $focusOwner
6866 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
6867 this.$focusOwner
= $focusOwner
;
6871 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
6872 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
6873 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
6874 * options. For more information about options and selects, please see the
6875 * [OOjs UI documentation on MediaWiki][1].
6878 * // Decorated options in a select widget
6879 * var select = new OO.ui.SelectWidget( {
6881 * new OO.ui.DecoratedOptionWidget( {
6883 * label: 'Option with icon',
6886 * new OO.ui.DecoratedOptionWidget( {
6888 * label: 'Option with indicator',
6893 * $( 'body' ).append( select.$element );
6895 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6898 * @extends OO.ui.OptionWidget
6899 * @mixins OO.ui.mixin.IconElement
6900 * @mixins OO.ui.mixin.IndicatorElement
6903 * @param {Object} [config] Configuration options
6905 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
6906 // Parent constructor
6907 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
6909 // Mixin constructors
6910 OO
.ui
.mixin
.IconElement
.call( this, config
);
6911 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
6915 .addClass( 'oo-ui-decoratedOptionWidget' )
6916 .prepend( this.$icon
)
6917 .append( this.$indicator
);
6922 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
6923 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
6924 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
6927 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
6928 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
6929 * the [OOjs UI documentation on MediaWiki] [1] for more information.
6931 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6934 * @extends OO.ui.DecoratedOptionWidget
6937 * @param {Object} [config] Configuration options
6939 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
6940 // Parent constructor
6941 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
6944 this.$element
.addClass( 'oo-ui-menuOptionWidget' );
6949 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
6951 /* Static Properties */
6957 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
6960 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
6961 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
6964 * var myDropdown = new OO.ui.DropdownWidget( {
6967 * new OO.ui.MenuSectionOptionWidget( {
6970 * new OO.ui.MenuOptionWidget( {
6972 * label: 'Welsh Corgi'
6974 * new OO.ui.MenuOptionWidget( {
6976 * label: 'Standard Poodle'
6978 * new OO.ui.MenuSectionOptionWidget( {
6981 * new OO.ui.MenuOptionWidget( {
6988 * $( 'body' ).append( myDropdown.$element );
6991 * @extends OO.ui.DecoratedOptionWidget
6994 * @param {Object} [config] Configuration options
6996 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
6997 // Parent constructor
6998 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7001 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' )
7002 .removeAttr( 'role aria-selected' );
7007 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7009 /* Static Properties */
7015 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7021 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7024 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7025 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7026 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
7027 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7028 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7029 * and customized to be opened, closed, and displayed as needed.
7031 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7032 * mouse outside the menu.
7034 * Menus also have support for keyboard interaction:
7036 * - Enter/Return key: choose and select a menu option
7037 * - Up-arrow key: highlight the previous menu option
7038 * - Down-arrow key: highlight the next menu option
7039 * - Esc key: hide the menu
7041 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7043 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
7044 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
7047 * @extends OO.ui.SelectWidget
7048 * @mixins OO.ui.mixin.ClippableElement
7049 * @mixins OO.ui.mixin.FloatableElement
7052 * @param {Object} [config] Configuration options
7053 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
7054 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
7055 * and {@link OO.ui.mixin.LookupElement LookupElement}
7056 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7057 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
7058 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
7059 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
7060 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
7061 * that button, unless the button (or its parent widget) is passed in here.
7062 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7063 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7064 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7065 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7066 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7067 * @cfg {number} [width] Width of the menu
7069 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7070 // Configuration initialization
7071 config
= config
|| {};
7073 // Parent constructor
7074 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7076 // Mixin constructors
7077 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
7078 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7081 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7082 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7083 this.filterFromInput
= !!config
.filterFromInput
;
7084 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7085 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7086 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7087 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7088 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7089 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7090 this.width
= config
.width
;
7093 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7094 if ( config
.widget
) {
7095 this.setFocusOwner( config
.widget
.$tabIndexed
);
7098 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7099 // that reference properties not initialized at that time of parent class construction
7100 // TODO: Find a better way to handle post-constructor setup
7101 this.visible
= false;
7102 this.$element
.addClass( 'oo-ui-element-hidden' );
7107 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7108 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7109 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7116 * The menu is ready: it is visible and has been positioned and clipped.
7122 * Handles document mouse down events.
7125 * @param {MouseEvent} e Mouse down event
7127 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7131 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7136 this.toggle( false );
7143 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
7144 var currentItem
= this.findHighlightedItem() || this.getSelectedItem();
7146 if ( !this.isDisabled() && this.isVisible() ) {
7147 switch ( e
.keyCode
) {
7148 case OO
.ui
.Keys
.LEFT
:
7149 case OO
.ui
.Keys
.RIGHT
:
7150 // Do nothing if a text field is associated, arrow keys will be handled natively
7151 if ( !this.$input
) {
7152 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
7155 case OO
.ui
.Keys
.ESCAPE
:
7156 case OO
.ui
.Keys
.TAB
:
7157 if ( currentItem
) {
7158 currentItem
.setHighlighted( false );
7160 this.toggle( false );
7161 // Don't prevent tabbing away, prevent defocusing
7162 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7164 e
.stopPropagation();
7168 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
7175 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7176 * or after items were added/removed (always).
7180 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7181 var i
, item
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7182 firstItemFound
= false,
7184 len
= this.items
.length
,
7185 showAll
= !this.isVisible(),
7188 if ( this.$input
&& this.filterFromInput
) {
7189 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
7190 exactFilter
= this.getItemMatcher( this.$input
.val(), true );
7192 // Hide non-matching options, and also hide section headers if all options
7193 // in their section are hidden.
7194 for ( i
= 0; i
< len
; i
++ ) {
7195 item
= this.items
[ i
];
7196 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7198 // If the previous section was empty, hide its header
7199 section
.toggle( showAll
|| !sectionEmpty
);
7202 sectionEmpty
= true;
7203 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7204 visible
= showAll
|| filter( item
);
7205 exactMatch
= exactMatch
|| exactFilter( item
);
7206 anyVisible
= anyVisible
|| visible
;
7207 sectionEmpty
= sectionEmpty
&& !visible
;
7208 item
.toggle( visible
);
7209 if ( this.highlightOnFilter
&& visible
&& !firstItemFound
) {
7210 // Highlight the first item in the list
7211 this.highlightItem( item
);
7212 firstItemFound
= true;
7216 // Process the final section
7218 section
.toggle( showAll
|| !sectionEmpty
);
7221 if ( anyVisible
&& this.items
.length
&& !exactMatch
) {
7222 this.scrollItemIntoView( this.items
[ 0 ] );
7225 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7228 // Reevaluate clipping
7235 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
7236 if ( this.$input
) {
7237 this.$input
.on( 'keydown', this.onKeyDownHandler
);
7239 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
7246 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
7247 if ( this.$input
) {
7248 this.$input
.off( 'keydown', this.onKeyDownHandler
);
7250 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
7257 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
7258 if ( this.$input
) {
7259 if ( this.filterFromInput
) {
7260 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7261 this.updateItemVisibility();
7264 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
7271 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
7272 if ( this.$input
) {
7273 if ( this.filterFromInput
) {
7274 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7275 this.updateItemVisibility();
7278 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
7285 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
7287 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
7288 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
7290 * @param {OO.ui.OptionWidget} item Item to choose
7293 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7294 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7295 if ( this.hideOnChoose
) {
7296 this.toggle( false );
7304 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7306 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7308 this.updateItemVisibility();
7316 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7318 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7320 this.updateItemVisibility();
7328 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7330 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
7332 this.updateItemVisibility();
7338 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7339 * `.toggle( true )` after its #$element is attached to the DOM.
7341 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7342 * it in the right place and with the right dimensions only work correctly while it is attached.
7343 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7344 * strictly enforced, so currently it only generates a warning in the browser console.
7349 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7352 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7353 change
= visible
!== this.isVisible();
7355 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7356 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7357 this.warnedUnattached
= true;
7360 if ( change
&& visible
&& ( this.width
|| this.$floatableContainer
) ) {
7361 this.setIdealSize( this.width
|| this.$floatableContainer
.width() );
7365 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7369 this.bindKeyDownListener();
7370 this.bindKeyPressListener();
7372 this.togglePositioning( !!this.$floatableContainer
);
7373 this.toggleClipping( true );
7375 this.$focusOwner
.attr( 'aria-expanded', 'true' );
7377 if ( this.getSelectedItem() ) {
7378 this.$focusOwner
.attr( 'aria-activedescendant', this.getSelectedItem().getElementId() );
7379 this.getSelectedItem().scrollElementIntoView( { duration
: 0 } );
7383 if ( this.autoHide
) {
7384 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7387 this.emit( 'ready' );
7389 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7390 this.unbindKeyDownListener();
7391 this.unbindKeyPressListener();
7392 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7393 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7394 this.togglePositioning( false );
7395 this.toggleClipping( false );
7403 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
7404 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
7405 * users can interact with it.
7407 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7408 * OO.ui.DropdownInputWidget instead.
7411 * // Example: A DropdownWidget with a menu that contains three options
7412 * var dropDown = new OO.ui.DropdownWidget( {
7413 * label: 'Dropdown menu: Select a menu option',
7416 * new OO.ui.MenuOptionWidget( {
7420 * new OO.ui.MenuOptionWidget( {
7424 * new OO.ui.MenuOptionWidget( {
7432 * $( 'body' ).append( dropDown.$element );
7434 * dropDown.getMenu().selectItemByData( 'b' );
7436 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
7438 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
7440 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
7443 * @extends OO.ui.Widget
7444 * @mixins OO.ui.mixin.IconElement
7445 * @mixins OO.ui.mixin.IndicatorElement
7446 * @mixins OO.ui.mixin.LabelElement
7447 * @mixins OO.ui.mixin.TitledElement
7448 * @mixins OO.ui.mixin.TabIndexedElement
7451 * @param {Object} [config] Configuration options
7452 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.MenuSelectWidget menu select widget}
7453 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
7454 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
7455 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
7456 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
7458 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
7459 // Configuration initialization
7460 config
= $.extend( { indicator
: 'down' }, config
);
7462 // Parent constructor
7463 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
7465 // Properties (must be set before TabIndexedElement constructor call)
7466 this.$handle
= this.$( '<span>' );
7467 this.$overlay
= config
.$overlay
|| this.$element
;
7469 // Mixin constructors
7470 OO
.ui
.mixin
.IconElement
.call( this, config
);
7471 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7472 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7473 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
7474 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
7477 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
7479 $floatableContainer
: this.$element
7484 click
: this.onClick
.bind( this ),
7485 keydown
: this.onKeyDown
.bind( this ),
7486 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
7487 keypress
: this.menu
.onKeyPressHandler
,
7488 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
7490 this.menu
.connect( this, {
7491 select
: 'onMenuSelect',
7492 toggle
: 'onMenuToggle'
7497 .addClass( 'oo-ui-dropdownWidget-handle' )
7500 'aria-owns': this.menu
.getElementId(),
7501 'aria-autocomplete': 'list'
7503 .append( this.$icon
, this.$label
, this.$indicator
);
7505 .addClass( 'oo-ui-dropdownWidget' )
7506 .append( this.$handle
);
7507 this.$overlay
.append( this.menu
.$element
);
7512 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
7513 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
7514 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
7515 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
7516 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
7517 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7524 * @return {OO.ui.MenuSelectWidget} Menu of widget
7526 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
7531 * Handles menu select events.
7534 * @param {OO.ui.MenuOptionWidget} item Selected menu item
7536 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
7540 this.setLabel( null );
7544 selectedLabel
= item
.getLabel();
7546 // If the label is a DOM element, clone it, because setLabel will append() it
7547 if ( selectedLabel
instanceof jQuery
) {
7548 selectedLabel
= selectedLabel
.clone();
7551 this.setLabel( selectedLabel
);
7555 * Handle menu toggle events.
7558 * @param {boolean} isVisible Menu toggle event
7560 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
7561 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
7564 this.$element
.hasClass( 'oo-ui-dropdownWidget-open' ).toString()
7569 * Handle mouse click events.
7572 * @param {jQuery.Event} e Mouse click event
7574 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
7575 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7582 * Handle key down events.
7585 * @param {jQuery.Event} e Key down event
7587 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
7589 !this.isDisabled() &&
7591 e
.which
=== OO
.ui
.Keys
.ENTER
||
7593 e
.which
=== OO
.ui
.Keys
.SPACE
&&
7594 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
7595 // Space only closes the menu is the user is not typing to search.
7596 this.menu
.keyPressBuffer
=== ''
7599 !this.menu
.isVisible() &&
7601 e
.which
=== OO
.ui
.Keys
.UP
||
7602 e
.which
=== OO
.ui
.Keys
.DOWN
7613 * RadioOptionWidget is an option widget that looks like a radio button.
7614 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
7615 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
7617 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
7620 * @extends OO.ui.OptionWidget
7623 * @param {Object} [config] Configuration options
7625 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
7626 // Configuration initialization
7627 config
= config
|| {};
7629 // Properties (must be done before parent constructor which calls #setDisabled)
7630 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
7632 // Parent constructor
7633 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
7636 // Remove implicit role, we're handling it ourselves
7637 this.radio
.$input
.attr( 'role', 'presentation' );
7639 .addClass( 'oo-ui-radioOptionWidget' )
7640 .attr( 'role', 'radio' )
7641 .attr( 'aria-checked', 'false' )
7642 .removeAttr( 'aria-selected' )
7643 .prepend( this.radio
.$element
);
7648 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
7650 /* Static Properties */
7656 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
7662 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
7668 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
7674 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
7681 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
7682 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
7684 this.radio
.setSelected( state
);
7686 .attr( 'aria-checked', state
.toString() )
7687 .removeAttr( 'aria-selected' );
7695 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
7696 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
7698 this.radio
.setDisabled( this.isDisabled() );
7704 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
7705 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
7706 * an interface for adding, removing and selecting options.
7707 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
7709 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7710 * OO.ui.RadioSelectInputWidget instead.
7713 * // A RadioSelectWidget with RadioOptions.
7714 * var option1 = new OO.ui.RadioOptionWidget( {
7716 * label: 'Selected radio option'
7719 * var option2 = new OO.ui.RadioOptionWidget( {
7721 * label: 'Unselected radio option'
7724 * var radioSelect=new OO.ui.RadioSelectWidget( {
7725 * items: [ option1, option2 ]
7728 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
7729 * radioSelect.selectItem( option1 );
7731 * $( 'body' ).append( radioSelect.$element );
7733 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
7737 * @extends OO.ui.SelectWidget
7738 * @mixins OO.ui.mixin.TabIndexedElement
7741 * @param {Object} [config] Configuration options
7743 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
7744 // Parent constructor
7745 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
7747 // Mixin constructors
7748 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
7752 focus
: this.bindKeyDownListener
.bind( this ),
7753 blur
: this.unbindKeyDownListener
.bind( this )
7758 .addClass( 'oo-ui-radioSelectWidget' )
7759 .attr( 'role', 'radiogroup' );
7764 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
7765 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7768 * MultioptionWidgets are special elements that can be selected and configured with data. The
7769 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
7770 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
7771 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
7773 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Multioptions
7776 * @extends OO.ui.Widget
7777 * @mixins OO.ui.mixin.ItemWidget
7778 * @mixins OO.ui.mixin.LabelElement
7781 * @param {Object} [config] Configuration options
7782 * @cfg {boolean} [selected=false] Whether the option is initially selected
7784 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
7785 // Configuration initialization
7786 config
= config
|| {};
7788 // Parent constructor
7789 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
7791 // Mixin constructors
7792 OO
.ui
.mixin
.ItemWidget
.call( this );
7793 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7796 this.selected
= null;
7800 .addClass( 'oo-ui-multioptionWidget' )
7801 .append( this.$label
);
7802 this.setSelected( config
.selected
);
7807 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
7808 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
7809 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
7816 * A change event is emitted when the selected state of the option changes.
7818 * @param {boolean} selected Whether the option is now selected
7824 * Check if the option is selected.
7826 * @return {boolean} Item is selected
7828 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
7829 return this.selected
;
7833 * Set the option’s selected state. In general, all modifications to the selection
7834 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
7835 * method instead of this method.
7837 * @param {boolean} [state=false] Select option
7840 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
7842 if ( this.selected
!== state
) {
7843 this.selected
= state
;
7844 this.emit( 'change', state
);
7845 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
7851 * MultiselectWidget allows selecting multiple options from a list.
7853 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
7855 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
7859 * @extends OO.ui.Widget
7860 * @mixins OO.ui.mixin.GroupWidget
7863 * @param {Object} [config] Configuration options
7864 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
7866 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
7867 // Parent constructor
7868 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
7870 // Configuration initialization
7871 config
= config
|| {};
7873 // Mixin constructors
7874 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
7877 this.aggregate( { change
: 'select' } );
7878 // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
7879 // by GroupElement only when items are added/removed
7880 this.connect( this, { select
: [ 'emit', 'change' ] } );
7883 if ( config
.items
) {
7884 this.addItems( config
.items
);
7886 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
7887 this.$element
.addClass( 'oo-ui-multiselectWidget' )
7888 .append( this.$group
);
7893 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
7894 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
7901 * A change event is emitted when the set of items changes, or an item is selected or deselected.
7907 * A select event is emitted when an item is selected or deselected.
7913 * Get options that are selected.
7915 * @return {OO.ui.MultioptionWidget[]} Selected options
7917 OO
.ui
.MultiselectWidget
.prototype.getSelectedItems = function () {
7918 return this.items
.filter( function ( item
) {
7919 return item
.isSelected();
7924 * Get the data of options that are selected.
7926 * @return {Object[]|string[]} Values of selected options
7928 OO
.ui
.MultiselectWidget
.prototype.getSelectedItemsData = function () {
7929 return this.getSelectedItems().map( function ( item
) {
7935 * Select options by reference. Options not mentioned in the `items` array will be deselected.
7937 * @param {OO.ui.MultioptionWidget[]} items Items to select
7940 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
7941 this.items
.forEach( function ( item
) {
7942 var selected
= items
.indexOf( item
) !== -1;
7943 item
.setSelected( selected
);
7949 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
7951 * @param {Object[]|string[]} datas Values of items to select
7954 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
7957 items
= datas
.map( function ( data
) {
7958 return widget
.getItemFromData( data
);
7960 this.selectItems( items
);
7965 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
7966 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
7967 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
7969 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
7972 * @extends OO.ui.MultioptionWidget
7975 * @param {Object} [config] Configuration options
7977 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
7978 // Configuration initialization
7979 config
= config
|| {};
7981 // Properties (must be done before parent constructor which calls #setDisabled)
7982 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
7984 // Parent constructor
7985 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
7988 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
7989 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
7993 .addClass( 'oo-ui-checkboxMultioptionWidget' )
7994 .prepend( this.checkbox
.$element
);
7999 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8001 /* Static Properties */
8007 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8012 * Handle checkbox selected state change.
8016 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8017 this.setSelected( this.checkbox
.isSelected() );
8023 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8024 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8025 this.checkbox
.setSelected( state
);
8032 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8033 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8034 this.checkbox
.setDisabled( this.isDisabled() );
8041 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8042 this.checkbox
.focus();
8046 * Handle key down events.
8049 * @param {jQuery.Event} e
8051 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8053 element
= this.getElementGroup(),
8056 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8057 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8058 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8059 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8069 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8070 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8071 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8072 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
8074 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8075 * OO.ui.CheckboxMultiselectInputWidget instead.
8078 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8079 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8082 * label: 'Selected checkbox'
8085 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
8087 * label: 'Unselected checkbox'
8090 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
8091 * items: [ option1, option2 ]
8094 * $( 'body' ).append( multiselect.$element );
8096 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
8099 * @extends OO.ui.MultiselectWidget
8102 * @param {Object} [config] Configuration options
8104 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8105 // Parent constructor
8106 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8109 this.$lastClicked
= null;
8112 this.$group
.on( 'click', this.onClick
.bind( this ) );
8116 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8121 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8126 * Get an option by its position relative to the specified item (or to the start of the option array,
8127 * if item is `null`). The direction in which to search through the option array is specified with a
8128 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8129 * `null` if there are no options in the array.
8131 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8132 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8133 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8135 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8136 var currentIndex
, nextIndex
, i
,
8137 increase
= direction
> 0 ? 1 : -1,
8138 len
= this.items
.length
;
8141 currentIndex
= this.items
.indexOf( item
);
8142 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8144 // If no item is selected and moving forward, start at the beginning.
8145 // If moving backward, start at the end.
8146 nextIndex
= direction
> 0 ? 0 : len
- 1;
8149 for ( i
= 0; i
< len
; i
++ ) {
8150 item
= this.items
[ nextIndex
];
8151 if ( item
&& !item
.isDisabled() ) {
8154 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8160 * Handle click events on checkboxes.
8162 * @param {jQuery.Event} e
8164 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8165 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8166 $lastClicked
= this.$lastClicked
,
8167 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8168 .not( '.oo-ui-widget-disabled' );
8170 // Allow selecting multiple options at once by Shift-clicking them
8171 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8172 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8173 lastClickedIndex
= $options
.index( $lastClicked
);
8174 nowClickedIndex
= $options
.index( $nowClicked
);
8175 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8176 // browser. In either case we don't need custom handling.
8177 if ( nowClickedIndex
!== lastClickedIndex
) {
8179 wasSelected
= items
[ nowClickedIndex
].isSelected();
8180 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8182 // This depends on the DOM order of the items and the order of the .items array being the same.
8183 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8184 if ( !items
[ i
].isDisabled() ) {
8185 items
[ i
].setSelected( !wasSelected
);
8188 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8189 // handling first, then set our value. The order in which events happen is different for
8190 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8191 // non-click actions that change the checkboxes.
8193 setTimeout( function () {
8194 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8195 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8201 if ( $nowClicked
.length
) {
8202 this.$lastClicked
= $nowClicked
;
8211 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8213 if ( !this.isDisabled() ) {
8214 item
= this.getRelativeFocusableItem( null, 1 );
8225 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8230 * Progress bars visually display the status of an operation, such as a download,
8231 * and can be either determinate or indeterminate:
8233 * - **determinate** process bars show the percent of an operation that is complete.
8235 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8236 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8237 * not use percentages.
8239 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8242 * // Examples of determinate and indeterminate progress bars.
8243 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8246 * var progressBar2 = new OO.ui.ProgressBarWidget();
8248 * // Create a FieldsetLayout to layout progress bars
8249 * var fieldset = new OO.ui.FieldsetLayout;
8250 * fieldset.addItems( [
8251 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
8252 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
8254 * $( 'body' ).append( fieldset.$element );
8257 * @extends OO.ui.Widget
8260 * @param {Object} [config] Configuration options
8261 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8262 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
8263 * By default, the progress bar is indeterminate.
8265 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8266 // Configuration initialization
8267 config
= config
|| {};
8269 // Parent constructor
8270 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8273 this.$bar
= $( '<div>' );
8274 this.progress
= null;
8277 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8278 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8281 role
: 'progressbar',
8283 'aria-valuemax': 100
8285 .addClass( 'oo-ui-progressBarWidget' )
8286 .append( this.$bar
);
8291 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8293 /* Static Properties */
8299 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8304 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8306 * @return {number|boolean} Progress percent
8308 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8309 return this.progress
;
8313 * Set the percent of the process completed or `false` for an indeterminate process.
8315 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8317 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8318 this.progress
= progress
;
8320 if ( progress
!== false ) {
8321 this.$bar
.css( 'width', this.progress
+ '%' );
8322 this.$element
.attr( 'aria-valuenow', this.progress
);
8324 this.$bar
.css( 'width', '' );
8325 this.$element
.removeAttr( 'aria-valuenow' );
8327 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8331 * InputWidget is the base class for all input widgets, which
8332 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8333 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8334 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8336 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8340 * @extends OO.ui.Widget
8341 * @mixins OO.ui.mixin.FlaggedElement
8342 * @mixins OO.ui.mixin.TabIndexedElement
8343 * @mixins OO.ui.mixin.TitledElement
8344 * @mixins OO.ui.mixin.AccessKeyedElement
8347 * @param {Object} [config] Configuration options
8348 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8349 * @cfg {string} [value=''] The value of the input.
8350 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8351 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8352 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8353 * before it is accepted.
8355 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8356 // Configuration initialization
8357 config
= config
|| {};
8359 // Parent constructor
8360 OO
.ui
.InputWidget
.parent
.call( this, config
);
8363 // See #reusePreInfuseDOM about config.$input
8364 this.$input
= config
.$input
|| this.getInputElement( config
);
8366 this.inputFilter
= config
.inputFilter
;
8368 // Mixin constructors
8369 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8370 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8371 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8372 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8375 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8379 .addClass( 'oo-ui-inputWidget-input' )
8380 .attr( 'name', config
.name
)
8381 .prop( 'disabled', this.isDisabled() );
8383 .addClass( 'oo-ui-inputWidget' )
8384 .append( this.$input
);
8385 this.setValue( config
.value
);
8387 this.setDir( config
.dir
);
8389 if ( config
.inputId
!== undefined ) {
8390 this.setInputId( config
.inputId
);
8396 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8397 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8398 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8399 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8400 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8402 /* Static Methods */
8407 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8408 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8409 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8410 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8417 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8418 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8419 if ( config
.$input
&& config
.$input
.length
) {
8420 state
.value
= config
.$input
.val();
8421 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8422 state
.focus
= config
.$input
.is( ':focus' );
8432 * A change event is emitted when the value of the input changes.
8434 * @param {string} value
8440 * Get input element.
8442 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
8443 * different circumstances. The element must have a `value` property (like form elements).
8446 * @param {Object} config Configuration options
8447 * @return {jQuery} Input element
8449 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
8450 return $( '<input>' );
8454 * Handle potentially value-changing events.
8457 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8459 OO
.ui
.InputWidget
.prototype.onEdit = function () {
8461 if ( !this.isDisabled() ) {
8462 // Allow the stack to clear so the value will be updated
8463 setTimeout( function () {
8464 widget
.setValue( widget
.$input
.val() );
8470 * Get the value of the input.
8472 * @return {string} Input value
8474 OO
.ui
.InputWidget
.prototype.getValue = function () {
8475 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8476 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8477 var value
= this.$input
.val();
8478 if ( this.value
!== value
) {
8479 this.setValue( value
);
8485 * Set the directionality of the input.
8487 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
8490 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
8491 this.$input
.prop( 'dir', dir
);
8496 * Set the value of the input.
8498 * @param {string} value New value
8502 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
8503 value
= this.cleanUpValue( value
);
8504 // Update the DOM if it has changed. Note that with cleanUpValue, it
8505 // is possible for the DOM value to change without this.value changing.
8506 if ( this.$input
.val() !== value
) {
8507 this.$input
.val( value
);
8509 if ( this.value
!== value
) {
8511 this.emit( 'change', this.value
);
8517 * Clean up incoming value.
8519 * Ensures value is a string, and converts undefined and null to empty string.
8522 * @param {string} value Original value
8523 * @return {string} Cleaned up value
8525 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
8526 if ( value
=== undefined || value
=== null ) {
8528 } else if ( this.inputFilter
) {
8529 return this.inputFilter( String( value
) );
8531 return String( value
);
8538 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
8539 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8540 if ( this.$input
) {
8541 this.$input
.prop( 'disabled', this.isDisabled() );
8547 * Set the 'id' attribute of the `<input>` element.
8549 * @param {string} id
8552 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
8553 this.$input
.attr( 'id', id
);
8560 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
8561 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8562 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
8563 this.setValue( state
.value
);
8565 if ( state
.focus
) {
8571 * Data widget intended for creating 'hidden'-type inputs.
8574 * @extends OO.ui.Widget
8577 * @param {Object} [config] Configuration options
8578 * @cfg {string} [value=''] The value of the input.
8579 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8581 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
8582 // Configuration initialization
8583 config
= $.extend( { value
: '', name
: '' }, config
);
8585 // Parent constructor
8586 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
8589 this.$element
.attr( {
8591 value
: config
.value
,
8594 this.$element
.removeAttr( 'aria-disabled' );
8599 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
8601 /* Static Properties */
8607 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
8610 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
8611 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
8612 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
8613 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
8614 * [OOjs UI documentation on MediaWiki] [1] for more information.
8617 * // A ButtonInputWidget rendered as an HTML button, the default.
8618 * var button = new OO.ui.ButtonInputWidget( {
8619 * label: 'Input button',
8623 * $( 'body' ).append( button.$element );
8625 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
8628 * @extends OO.ui.InputWidget
8629 * @mixins OO.ui.mixin.ButtonElement
8630 * @mixins OO.ui.mixin.IconElement
8631 * @mixins OO.ui.mixin.IndicatorElement
8632 * @mixins OO.ui.mixin.LabelElement
8633 * @mixins OO.ui.mixin.TitledElement
8636 * @param {Object} [config] Configuration options
8637 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
8638 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
8639 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
8640 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
8641 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
8643 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
8644 // Configuration initialization
8645 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
8647 // See InputWidget#reusePreInfuseDOM about config.$input
8648 if ( config
.$input
) {
8649 config
.$input
.empty();
8652 // Properties (must be set before parent constructor, which calls #setValue)
8653 this.useInputTag
= config
.useInputTag
;
8655 // Parent constructor
8656 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
8658 // Mixin constructors
8659 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
8660 OO
.ui
.mixin
.IconElement
.call( this, config
);
8661 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8662 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8663 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8666 if ( !config
.useInputTag
) {
8667 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
8669 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
8674 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
8675 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
8676 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
8677 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
8678 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
8679 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
8681 /* Static Properties */
8687 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
8695 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
8697 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
8698 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
8704 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
8706 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
8707 * text, or `null` for no label
8710 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
8711 if ( typeof label
=== 'function' ) {
8712 label
= OO
.ui
.resolveMsg( label
);
8715 if ( this.useInputTag
) {
8716 // Discard non-plaintext labels
8717 if ( typeof label
!== 'string' ) {
8721 this.$input
.val( label
);
8724 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
8728 * Set the value of the input.
8730 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
8731 * they do not support {@link #value values}.
8733 * @param {string} value New value
8736 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
8737 if ( !this.useInputTag
) {
8738 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
8746 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
8747 // Disable generating `<label>` elements for buttons. One would very rarely need additional label
8748 // for a button, and it's already a big clickable target, and it causes unexpected rendering.
8753 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
8754 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
8755 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
8756 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
8758 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
8761 * // An example of selected, unselected, and disabled checkbox inputs
8762 * var checkbox1=new OO.ui.CheckboxInputWidget( {
8766 * var checkbox2=new OO.ui.CheckboxInputWidget( {
8769 * var checkbox3=new OO.ui.CheckboxInputWidget( {
8773 * // Create a fieldset layout with fields for each checkbox.
8774 * var fieldset = new OO.ui.FieldsetLayout( {
8775 * label: 'Checkboxes'
8777 * fieldset.addItems( [
8778 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
8779 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
8780 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
8782 * $( 'body' ).append( fieldset.$element );
8784 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8787 * @extends OO.ui.InputWidget
8790 * @param {Object} [config] Configuration options
8791 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
8793 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
8794 // Configuration initialization
8795 config
= config
|| {};
8797 // Parent constructor
8798 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
8802 .addClass( 'oo-ui-checkboxInputWidget' )
8803 // Required for pretty styling in WikimediaUI theme
8804 .append( $( '<span>' ) );
8805 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
8810 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
8812 /* Static Properties */
8818 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
8820 /* Static Methods */
8825 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8826 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8827 state
.checked
= config
.$input
.prop( 'checked' );
8837 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
8838 return $( '<input>' ).attr( 'type', 'checkbox' );
8844 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
8846 if ( !this.isDisabled() ) {
8847 // Allow the stack to clear so the value will be updated
8848 setTimeout( function () {
8849 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
8855 * Set selection state of this checkbox.
8857 * @param {boolean} state `true` for selected
8860 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
8862 if ( this.selected
!== state
) {
8863 this.selected
= state
;
8864 this.$input
.prop( 'checked', this.selected
);
8865 this.emit( 'change', this.selected
);
8871 * Check if this checkbox is selected.
8873 * @return {boolean} Checkbox is selected
8875 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
8876 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8877 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8878 var selected
= this.$input
.prop( 'checked' );
8879 if ( this.selected
!== selected
) {
8880 this.setSelected( selected
);
8882 return this.selected
;
8888 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
8889 if ( !this.isDisabled() ) {
8890 this.$input
.click();
8898 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8899 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8900 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
8901 this.setSelected( state
.checked
);
8906 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
8907 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8908 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8909 * more information about input widgets.
8911 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
8912 * are no options. If no `value` configuration option is provided, the first option is selected.
8913 * If you need a state representing no value (no option being selected), use a DropdownWidget.
8915 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
8918 * // Example: A DropdownInputWidget with three options
8919 * var dropdownInput = new OO.ui.DropdownInputWidget( {
8921 * { data: 'a', label: 'First' },
8922 * { data: 'b', label: 'Second'},
8923 * { data: 'c', label: 'Third' }
8926 * $( 'body' ).append( dropdownInput.$element );
8928 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8931 * @extends OO.ui.InputWidget
8932 * @mixins OO.ui.mixin.TitledElement
8935 * @param {Object} [config] Configuration options
8936 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8937 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
8939 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
8940 // Configuration initialization
8941 config
= config
|| {};
8943 // See InputWidget#reusePreInfuseDOM about config.$input
8944 if ( config
.$input
) {
8945 config
.$input
.addClass( 'oo-ui-element-hidden' );
8948 // Properties (must be done before parent constructor which calls #setDisabled)
8949 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
8951 // Parent constructor
8952 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
8954 // Mixin constructors
8955 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8958 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
8961 this.setOptions( config
.options
|| [] );
8962 // Set the value again, after we did setOptions(). The call from parent doesn't work because the
8963 // widget has no valid options when it happens.
8964 this.setValue( config
.value
);
8966 .addClass( 'oo-ui-dropdownInputWidget' )
8967 .append( this.dropdownWidget
.$element
);
8968 this.setTabIndexedElement( null );
8973 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
8974 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
8982 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
8983 return $( '<input>' ).attr( 'type', 'hidden' );
8987 * Handles menu select events.
8990 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
8992 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
8993 this.setValue( item
? item
.getData() : '' );
8999 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9001 value
= this.cleanUpValue( value
);
9002 // Only allow setting values that are actually present in the dropdown
9003 selected
= this.dropdownWidget
.getMenu().getItemFromData( value
) ||
9004 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9005 this.dropdownWidget
.getMenu().selectItem( selected
);
9006 value
= selected
? selected
.getData() : '';
9007 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9014 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9015 this.dropdownWidget
.setDisabled( state
);
9016 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9021 * Set the options available for this input.
9023 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9026 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9028 value
= this.getValue(),
9031 // Rebuild the dropdown menu
9032 this.dropdownWidget
.getMenu()
9034 .addItems( options
.map( function ( opt
) {
9035 var optValue
= widget
.cleanUpValue( opt
.data
);
9037 if ( opt
.optgroup
=== undefined ) {
9038 return new OO
.ui
.MenuOptionWidget( {
9040 label
: opt
.label
!== undefined ? opt
.label
: optValue
9043 return new OO
.ui
.MenuSectionOptionWidget( {
9049 // Restore the previous value, or reset to something sensible
9050 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
9051 // Previous value is still available, ensure consistency with the dropdown
9052 this.setValue( value
);
9054 // No longer valid, reset
9055 if ( options
.length
) {
9056 this.setValue( options
[ 0 ].data
);
9066 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9067 this.dropdownWidget
.focus();
9074 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9075 this.dropdownWidget
.blur();
9080 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9081 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9082 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9083 * please see the [OOjs UI documentation on MediaWiki][1].
9085 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9088 * // An example of selected, unselected, and disabled radio inputs
9089 * var radio1 = new OO.ui.RadioInputWidget( {
9093 * var radio2 = new OO.ui.RadioInputWidget( {
9096 * var radio3 = new OO.ui.RadioInputWidget( {
9100 * // Create a fieldset layout with fields for each radio button.
9101 * var fieldset = new OO.ui.FieldsetLayout( {
9102 * label: 'Radio inputs'
9104 * fieldset.addItems( [
9105 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9106 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9107 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9109 * $( 'body' ).append( fieldset.$element );
9111 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9114 * @extends OO.ui.InputWidget
9117 * @param {Object} [config] Configuration options
9118 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9120 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9121 // Configuration initialization
9122 config
= config
|| {};
9124 // Parent constructor
9125 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9129 .addClass( 'oo-ui-radioInputWidget' )
9130 // Required for pretty styling in WikimediaUI theme
9131 .append( $( '<span>' ) );
9132 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9137 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9139 /* Static Properties */
9145 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9147 /* Static Methods */
9152 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9153 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9154 state
.checked
= config
.$input
.prop( 'checked' );
9164 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9165 return $( '<input>' ).attr( 'type', 'radio' );
9171 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9172 // RadioInputWidget doesn't track its state.
9176 * Set selection state of this radio button.
9178 * @param {boolean} state `true` for selected
9181 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9182 // RadioInputWidget doesn't track its state.
9183 this.$input
.prop( 'checked', state
);
9188 * Check if this radio button is selected.
9190 * @return {boolean} Radio is selected
9192 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
9193 return this.$input
.prop( 'checked' );
9199 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
9200 if ( !this.isDisabled() ) {
9201 this.$input
.click();
9209 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9210 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9211 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9212 this.setSelected( state
.checked
);
9217 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9218 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9219 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
9220 * more information about input widgets.
9222 * This and OO.ui.DropdownInputWidget support the same configuration options.
9225 * // Example: A RadioSelectInputWidget with three options
9226 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9228 * { data: 'a', label: 'First' },
9229 * { data: 'b', label: 'Second'},
9230 * { data: 'c', label: 'Third' }
9233 * $( 'body' ).append( radioSelectInput.$element );
9235 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9238 * @extends OO.ui.InputWidget
9241 * @param {Object} [config] Configuration options
9242 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9244 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
9245 // Configuration initialization
9246 config
= config
|| {};
9248 // Properties (must be done before parent constructor which calls #setDisabled)
9249 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
9251 // Parent constructor
9252 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
9255 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
9258 this.setOptions( config
.options
|| [] );
9260 .addClass( 'oo-ui-radioSelectInputWidget' )
9261 .append( this.radioSelectWidget
.$element
);
9262 this.setTabIndexedElement( null );
9267 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
9269 /* Static Methods */
9274 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9275 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9276 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9283 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9284 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9285 // Cannot reuse the `<input type=radio>` set
9286 delete config
.$input
;
9296 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
9297 return $( '<input>' ).attr( 'type', 'hidden' );
9301 * Handles menu select events.
9304 * @param {OO.ui.RadioOptionWidget} item Selected menu item
9306 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
9307 this.setValue( item
.getData() );
9313 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
9314 value
= this.cleanUpValue( value
);
9315 this.radioSelectWidget
.selectItemByData( value
);
9316 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9323 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
9324 this.radioSelectWidget
.setDisabled( state
);
9325 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9330 * Set the options available for this input.
9332 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9335 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
9337 value
= this.getValue(),
9340 // Rebuild the radioSelect menu
9341 this.radioSelectWidget
9343 .addItems( options
.map( function ( opt
) {
9344 var optValue
= widget
.cleanUpValue( opt
.data
);
9345 return new OO
.ui
.RadioOptionWidget( {
9347 label
: opt
.label
!== undefined ? opt
.label
: optValue
9351 // Restore the previous value, or reset to something sensible
9352 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
9353 // Previous value is still available, ensure consistency with the radioSelect
9354 this.setValue( value
);
9356 // No longer valid, reset
9357 if ( options
.length
) {
9358 this.setValue( options
[ 0 ].data
);
9368 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
9369 this.radioSelectWidget
.focus();
9376 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
9377 this.radioSelectWidget
.blur();
9382 * CheckboxMultiselectInputWidget is a
9383 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
9384 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
9385 * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on MediaWiki][1] for
9386 * more information about input widgets.
9389 * // Example: A CheckboxMultiselectInputWidget with three options
9390 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
9392 * { data: 'a', label: 'First' },
9393 * { data: 'b', label: 'Second'},
9394 * { data: 'c', label: 'Third' }
9397 * $( 'body' ).append( multiselectInput.$element );
9399 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9402 * @extends OO.ui.InputWidget
9405 * @param {Object} [config] Configuration options
9406 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
9408 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
9409 // Configuration initialization
9410 config
= config
|| {};
9412 // Properties (must be done before parent constructor which calls #setDisabled)
9413 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
9415 // Parent constructor
9416 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
9419 this.inputName
= config
.name
;
9423 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
9424 .append( this.checkboxMultiselectWidget
.$element
);
9425 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
9426 this.$input
.detach();
9427 this.setOptions( config
.options
|| [] );
9428 // Have to repeat this from parent, as we need options to be set up for this to make sense
9429 this.setValue( config
.value
);
9431 // setValue when checkboxMultiselectWidget changes
9432 this.checkboxMultiselectWidget
.on( 'change', function () {
9433 this.setValue( this.checkboxMultiselectWidget
.getSelectedItemsData() );
9439 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
9441 /* Static Methods */
9446 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9447 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9448 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9449 .toArray().map( function ( el
) { return el
.value
; } );
9456 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9457 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9458 // Cannot reuse the `<input type=checkbox>` set
9459 delete config
.$input
;
9469 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
9471 return $( '<unused>' );
9477 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
9478 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9479 .toArray().map( function ( el
) { return el
.value
; } );
9480 if ( this.value
!== value
) {
9481 this.setValue( value
);
9489 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
9490 value
= this.cleanUpValue( value
);
9491 this.checkboxMultiselectWidget
.selectItemsByData( value
);
9492 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9497 * Clean up incoming value.
9499 * @param {string[]} value Original value
9500 * @return {string[]} Cleaned up value
9502 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
9505 if ( !Array
.isArray( value
) ) {
9508 for ( i
= 0; i
< value
.length
; i
++ ) {
9510 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
9511 // Remove options that we don't have here
9512 if ( !this.checkboxMultiselectWidget
.getItemFromData( singleValue
) ) {
9515 cleanValue
.push( singleValue
);
9523 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
9524 this.checkboxMultiselectWidget
.setDisabled( state
);
9525 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9530 * Set the options available for this input.
9532 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
9535 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
9538 // Rebuild the checkboxMultiselectWidget menu
9539 this.checkboxMultiselectWidget
9541 .addItems( options
.map( function ( opt
) {
9542 var optValue
, item
, optDisabled
;
9544 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
9545 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
9546 item
= new OO
.ui
.CheckboxMultioptionWidget( {
9548 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
9549 disabled
: optDisabled
9551 // Set the 'name' and 'value' for form submission
9552 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
9553 item
.checkbox
.setValue( optValue
);
9557 // Re-set the value, checking the checkboxes as needed.
9558 // This will also get rid of any stale options that we just removed.
9559 this.setValue( this.getValue() );
9567 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
9568 this.checkboxMultiselectWidget
.focus();
9573 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
9574 * size of the field as well as its presentation. In addition, these widgets can be configured
9575 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
9576 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
9577 * which modifies incoming values rather than validating them.
9578 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9580 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9583 * // Example of a text input widget
9584 * var textInput = new OO.ui.TextInputWidget( {
9585 * value: 'Text input'
9587 * $( 'body' ).append( textInput.$element );
9589 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9592 * @extends OO.ui.InputWidget
9593 * @mixins OO.ui.mixin.IconElement
9594 * @mixins OO.ui.mixin.IndicatorElement
9595 * @mixins OO.ui.mixin.PendingElement
9596 * @mixins OO.ui.mixin.LabelElement
9599 * @param {Object} [config] Configuration options
9600 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
9601 * 'email', 'url' or 'number'.
9602 * @cfg {string} [placeholder] Placeholder text
9603 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
9604 * instruct the browser to focus this widget.
9605 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
9606 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
9607 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
9608 * the value or placeholder text: `'before'` or `'after'`
9609 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
9610 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
9611 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
9612 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
9613 * (the value must contain only numbers); when RegExp, a regular expression that must match the
9614 * value for it to be considered valid; when Function, a function receiving the value as parameter
9615 * that must return true, or promise resolving to true, for it to be considered valid.
9617 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
9618 // Configuration initialization
9619 config
= $.extend( {
9621 labelPosition
: 'after'
9624 if ( config
.multiline
) {
9625 OO
.ui
.warnDeprecation( 'TextInputWidget: config.multiline is deprecated. Use the MultilineTextInputWidget instead. See T130434.' );
9626 return new OO
.ui
.MultilineTextInputWidget( config
);
9629 // Parent constructor
9630 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
9632 // Mixin constructors
9633 OO
.ui
.mixin
.IconElement
.call( this, config
);
9634 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9635 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
9636 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9639 this.type
= this.getSaneType( config
);
9640 this.readOnly
= false;
9641 this.required
= false;
9642 this.validate
= null;
9643 this.styleHeight
= null;
9644 this.scrollWidth
= null;
9646 this.setValidation( config
.validate
);
9647 this.setLabelPosition( config
.labelPosition
);
9651 keypress
: this.onKeyPress
.bind( this ),
9652 blur
: this.onBlur
.bind( this ),
9653 focus
: this.onFocus
.bind( this )
9655 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
9656 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
9657 this.on( 'labelChange', this.updatePosition
.bind( this ) );
9658 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
9662 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
9663 .append( this.$icon
, this.$indicator
);
9664 this.setReadOnly( !!config
.readOnly
);
9665 this.setRequired( !!config
.required
);
9666 if ( config
.placeholder
!== undefined ) {
9667 this.$input
.attr( 'placeholder', config
.placeholder
);
9669 if ( config
.maxLength
!== undefined ) {
9670 this.$input
.attr( 'maxlength', config
.maxLength
);
9672 if ( config
.autofocus
) {
9673 this.$input
.attr( 'autofocus', 'autofocus' );
9675 if ( config
.autocomplete
=== false ) {
9676 this.$input
.attr( 'autocomplete', 'off' );
9677 // Turning off autocompletion also disables "form caching" when the user navigates to a
9678 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
9680 beforeunload: function () {
9681 this.$input
.removeAttr( 'autocomplete' );
9683 pageshow: function () {
9684 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
9685 // whole page... it shouldn't hurt, though.
9686 this.$input
.attr( 'autocomplete', 'off' );
9691 this.isWaitingToBeAttached
= true;
9692 this.installParentChangeDetector();
9698 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
9699 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
9700 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9701 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
9702 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
9704 /* Static Properties */
9706 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
9711 /* Static Methods */
9716 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9717 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9724 * An `enter` event is emitted when the user presses 'enter' inside the text box.
9732 * Handle icon mouse down events.
9735 * @param {jQuery.Event} e Mouse down event
9737 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
9738 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9745 * Handle indicator mouse down events.
9748 * @param {jQuery.Event} e Mouse down event
9750 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
9751 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9758 * Handle key press events.
9761 * @param {jQuery.Event} e Key press event
9762 * @fires enter If enter key is pressed
9764 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
9765 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
9766 this.emit( 'enter', e
);
9771 * Handle blur events.
9774 * @param {jQuery.Event} e Blur event
9776 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
9777 this.setValidityFlag();
9781 * Handle focus events.
9784 * @param {jQuery.Event} e Focus event
9786 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
9787 if ( this.isWaitingToBeAttached
) {
9788 // If we've received focus, then we must be attached to the document, and if
9789 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
9790 this.onElementAttach();
9792 this.setValidityFlag( true );
9796 * Handle element attach events.
9799 * @param {jQuery.Event} e Element attach event
9801 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
9802 this.isWaitingToBeAttached
= false;
9803 // Any previously calculated size is now probably invalid if we reattached elsewhere
9804 this.valCache
= null;
9805 this.positionLabel();
9809 * Handle debounced change events.
9811 * @param {string} value
9814 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
9815 this.setValidityFlag();
9819 * Check if the input is {@link #readOnly read-only}.
9823 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
9824 return this.readOnly
;
9828 * Set the {@link #readOnly read-only} state of the input.
9830 * @param {boolean} state Make input read-only
9833 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
9834 this.readOnly
= !!state
;
9835 this.$input
.prop( 'readOnly', this.readOnly
);
9840 * Check if the input is {@link #required required}.
9844 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
9845 return this.required
;
9849 * Set the {@link #required required} state of the input.
9851 * @param {boolean} state Make input required
9854 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
9855 this.required
= !!state
;
9856 if ( this.required
) {
9858 .prop( 'required', true )
9859 .attr( 'aria-required', 'true' );
9860 if ( this.getIndicator() === null ) {
9861 this.setIndicator( 'required' );
9865 .prop( 'required', false )
9866 .removeAttr( 'aria-required' );
9867 if ( this.getIndicator() === 'required' ) {
9868 this.setIndicator( null );
9875 * Support function for making #onElementAttach work across browsers.
9877 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
9878 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
9880 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
9881 * first time that the element gets attached to the documented.
9883 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
9884 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
9885 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
9888 if ( MutationObserver
) {
9889 // The new way. If only it wasn't so ugly.
9891 if ( this.isElementAttached() ) {
9892 // Widget is attached already, do nothing. This breaks the functionality of this function when
9893 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
9894 // would require observation of the whole document, which would hurt performance of other,
9895 // more important code.
9899 // Find topmost node in the tree
9900 topmostNode
= this.$element
[ 0 ];
9901 while ( topmostNode
.parentNode
) {
9902 topmostNode
= topmostNode
.parentNode
;
9905 // We have no way to detect the $element being attached somewhere without observing the entire
9906 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
9907 // parent node of $element, and instead detect when $element is removed from it (and thus
9908 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
9909 // doesn't get attached, we end up back here and create the parent.
9911 mutationObserver
= new MutationObserver( function ( mutations
) {
9912 var i
, j
, removedNodes
;
9913 for ( i
= 0; i
< mutations
.length
; i
++ ) {
9914 removedNodes
= mutations
[ i
].removedNodes
;
9915 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
9916 if ( removedNodes
[ j
] === topmostNode
) {
9917 setTimeout( onRemove
, 0 );
9924 onRemove = function () {
9925 // If the node was attached somewhere else, report it
9926 if ( widget
.isElementAttached() ) {
9927 widget
.onElementAttach();
9929 mutationObserver
.disconnect();
9930 widget
.installParentChangeDetector();
9933 // Create a fake parent and observe it
9934 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
9935 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
9937 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
9938 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
9939 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
9947 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
9948 if ( this.getSaneType( config
) === 'number' ) {
9949 return $( '<input>' )
9950 .attr( 'step', 'any' )
9951 .attr( 'type', 'number' );
9953 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
9958 * Get sanitized value for 'type' for given config.
9960 * @param {Object} config Configuration options
9961 * @return {string|null}
9964 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
9965 var allowedTypes
= [
9972 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
9976 * Focus the input and select a specified range within the text.
9978 * @param {number} from Select from offset
9979 * @param {number} [to] Select to offset, defaults to from
9982 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
9983 var isBackwards
, start
, end
,
9984 input
= this.$input
[ 0 ];
9988 isBackwards
= to
< from;
9989 start
= isBackwards
? to
: from;
9990 end
= isBackwards
? from : to
;
9995 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
9997 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
9998 // Rather than expensively check if the input is attached every time, just check
9999 // if it was the cause of an error being thrown. If not, rethrow the error.
10000 if ( this.getElementDocument().body
.contains( input
) ) {
10008 * Get an object describing the current selection range in a directional manner
10010 * @return {Object} Object containing 'from' and 'to' offsets
10012 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10013 var input
= this.$input
[ 0 ],
10014 start
= input
.selectionStart
,
10015 end
= input
.selectionEnd
,
10016 isBackwards
= input
.selectionDirection
=== 'backward';
10019 from: isBackwards
? end
: start
,
10020 to
: isBackwards
? start
: end
10025 * Get the length of the text input value.
10027 * This could differ from the length of #getValue if the
10028 * value gets filtered
10030 * @return {number} Input length
10032 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10033 return this.$input
[ 0 ].value
.length
;
10037 * Focus the input and select the entire text.
10041 OO
.ui
.TextInputWidget
.prototype.select = function () {
10042 return this.selectRange( 0, this.getInputLength() );
10046 * Focus the input and move the cursor to the start.
10050 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10051 return this.selectRange( 0 );
10055 * Focus the input and move the cursor to the end.
10059 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10060 return this.selectRange( this.getInputLength() );
10064 * Insert new content into the input.
10066 * @param {string} content Content to be inserted
10069 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10071 range
= this.getRange(),
10072 value
= this.getValue();
10074 start
= Math
.min( range
.from, range
.to
);
10075 end
= Math
.max( range
.from, range
.to
);
10077 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10078 this.selectRange( start
+ content
.length
);
10083 * Insert new content either side of a selection.
10085 * @param {string} pre Content to be inserted before the selection
10086 * @param {string} post Content to be inserted after the selection
10089 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10091 range
= this.getRange(),
10092 offset
= pre
.length
;
10094 start
= Math
.min( range
.from, range
.to
);
10095 end
= Math
.max( range
.from, range
.to
);
10097 this.selectRange( start
).insertContent( pre
);
10098 this.selectRange( offset
+ end
).insertContent( post
);
10100 this.selectRange( offset
+ start
, offset
+ end
);
10105 * Set the validation pattern.
10107 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10108 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10109 * value must contain only numbers).
10111 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10112 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10114 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
10115 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
10116 this.validate
= validate
;
10118 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
10123 * Sets the 'invalid' flag appropriately.
10125 * @param {boolean} [isValid] Optionally override validation result
10127 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
10129 setFlag = function ( valid
) {
10131 widget
.$input
.attr( 'aria-invalid', 'true' );
10133 widget
.$input
.removeAttr( 'aria-invalid' );
10135 widget
.setFlags( { invalid
: !valid
} );
10138 if ( isValid
!== undefined ) {
10139 setFlag( isValid
);
10141 this.getValidity().then( function () {
10150 * Get the validity of current value.
10152 * This method returns a promise that resolves if the value is valid and rejects if
10153 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
10155 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10157 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
10160 function rejectOrResolve( valid
) {
10162 return $.Deferred().resolve().promise();
10164 return $.Deferred().reject().promise();
10168 // Check browser validity and reject if it is invalid
10170 this.$input
[ 0 ].checkValidity
!== undefined &&
10171 this.$input
[ 0 ].checkValidity() === false
10173 return rejectOrResolve( false );
10176 // Run our checks if the browser thinks the field is valid
10177 if ( this.validate
instanceof Function
) {
10178 result
= this.validate( this.getValue() );
10179 if ( result
&& $.isFunction( result
.promise
) ) {
10180 return result
.promise().then( function ( valid
) {
10181 return rejectOrResolve( valid
);
10184 return rejectOrResolve( result
);
10187 return rejectOrResolve( this.getValue().match( this.validate
) );
10192 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10194 * @param {string} labelPosition Label position, 'before' or 'after'
10197 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10198 this.labelPosition
= labelPosition
;
10199 if ( this.label
) {
10200 // If there is no label and we only change the position, #updatePosition is a no-op,
10201 // but it takes really a lot of work to do nothing.
10202 this.updatePosition();
10208 * Update the position of the inline label.
10210 * This method is called by #setLabelPosition, and can also be called on its own if
10211 * something causes the label to be mispositioned.
10215 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10216 var after
= this.labelPosition
=== 'after';
10219 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10220 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10222 this.valCache
= null;
10223 this.scrollWidth
= null;
10224 this.positionLabel();
10230 * Position the label by setting the correct padding on the input.
10235 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
10236 var after
, rtl
, property
, newCss
;
10238 if ( this.isWaitingToBeAttached
) {
10239 // #onElementAttach will be called soon, which calls this method
10244 'padding-right': '',
10248 if ( this.label
) {
10249 this.$element
.append( this.$label
);
10251 this.$label
.detach();
10252 // Clear old values if present
10253 this.$input
.css( newCss
);
10257 after
= this.labelPosition
=== 'after';
10258 rtl
= this.$element
.css( 'direction' ) === 'rtl';
10259 property
= after
=== rtl
? 'padding-left' : 'padding-right';
10261 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
10262 // We have to clear the padding on the other side, in case the element direction changed
10263 this.$input
.css( newCss
);
10271 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10272 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10273 if ( state
.scrollTop
!== undefined ) {
10274 this.$input
.scrollTop( state
.scrollTop
);
10280 * @extends OO.ui.TextInputWidget
10283 * @param {Object} [config] Configuration options
10285 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
10286 config
= $.extend( {
10290 // Parent constructor
10291 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
10294 this.connect( this, {
10299 this.updateSearchIndicator();
10300 this.connect( this, {
10301 disable
: 'onDisable'
10307 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
10315 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
10322 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10323 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10324 // Clear the text field
10325 this.setValue( '' );
10332 * Update the 'clear' indicator displayed on type: 'search' text
10333 * fields, hiding it when the field is already empty or when it's not
10336 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
10337 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
10338 this.setIndicator( null );
10340 this.setIndicator( 'clear' );
10345 * Handle change events.
10349 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
10350 this.updateSearchIndicator();
10354 * Handle disable events.
10356 * @param {boolean} disabled Element is disabled
10359 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
10360 this.updateSearchIndicator();
10366 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
10367 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
10368 this.updateSearchIndicator();
10374 * @extends OO.ui.TextInputWidget
10377 * @param {Object} [config] Configuration options
10378 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
10379 * specifies minimum number of rows to display.
10380 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10381 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
10382 * Use the #maxRows config to specify a maximum number of displayed rows.
10383 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
10384 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
10386 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
10387 config
= $.extend( {
10390 config
.multiline
= false;
10391 // Parent constructor
10392 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
10395 this.multiline
= true;
10396 this.autosize
= !!config
.autosize
;
10397 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
10398 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
10400 // Clone for resizing
10401 if ( this.autosize
) {
10402 this.$clone
= this.$input
10404 .insertAfter( this.$input
)
10405 .attr( 'aria-hidden', 'true' )
10406 .addClass( 'oo-ui-element-hidden' );
10410 this.connect( this, {
10415 if ( this.multiline
&& config
.rows
) {
10416 this.$input
.attr( 'rows', config
.rows
);
10418 if ( this.autosize
) {
10419 this.isWaitingToBeAttached
= true;
10420 this.installParentChangeDetector();
10426 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
10428 /* Static Methods */
10433 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10434 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10435 state
.scrollTop
= config
.$input
.scrollTop();
10444 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
10445 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
10450 * Handle change events.
10454 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
10461 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
10462 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
10467 * Override TextInputWidget so it doesn't emit the 'enter' event.
10470 * @param {jQuery.Event} e Key press event
10472 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function () {
10477 * Automatically adjust the size of the text input.
10479 * This only affects multiline inputs that are {@link #autosize autosized}.
10484 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
10485 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
10486 idealHeight
, newHeight
, scrollWidth
, property
;
10488 if ( this.$input
.val() !== this.valCache
) {
10489 if ( this.autosize
) {
10491 .val( this.$input
.val() )
10492 .attr( 'rows', this.minRows
)
10493 // Set inline height property to 0 to measure scroll height
10494 .css( 'height', 0 );
10496 this.$clone
.removeClass( 'oo-ui-element-hidden' );
10498 this.valCache
= this.$input
.val();
10500 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
10502 // Remove inline height property to measure natural heights
10503 this.$clone
.css( 'height', '' );
10504 innerHeight
= this.$clone
.innerHeight();
10505 outerHeight
= this.$clone
.outerHeight();
10507 // Measure max rows height
10509 .attr( 'rows', this.maxRows
)
10510 .css( 'height', 'auto' )
10512 maxInnerHeight
= this.$clone
.innerHeight();
10514 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
10515 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
10516 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
10517 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
10519 this.$clone
.addClass( 'oo-ui-element-hidden' );
10521 // Only apply inline height when expansion beyond natural height is needed
10522 // Use the difference between the inner and outer height as a buffer
10523 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
10524 if ( newHeight
!== this.styleHeight
) {
10525 this.$input
.css( 'height', newHeight
);
10526 this.styleHeight
= newHeight
;
10527 this.emit( 'resize' );
10530 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
10531 if ( scrollWidth
!== this.scrollWidth
) {
10532 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
10534 this.$label
.css( { right
: '', left
: '' } );
10535 this.$indicator
.css( { right
: '', left
: '' } );
10537 if ( scrollWidth
) {
10538 this.$indicator
.css( property
, scrollWidth
);
10539 if ( this.labelPosition
=== 'after' ) {
10540 this.$label
.css( property
, scrollWidth
);
10544 this.scrollWidth
= scrollWidth
;
10545 this.positionLabel();
10555 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
10556 return $( '<textarea>' );
10560 * Check if the input supports multiple lines.
10562 * @return {boolean}
10564 OO
.ui
.MultilineTextInputWidget
.prototype.isMultiline = function () {
10565 return !!this.multiline
;
10569 * Check if the input automatically adjusts its size.
10571 * @return {boolean}
10573 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
10574 return !!this.autosize
;
10578 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
10579 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
10580 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
10582 * - by typing a value in the text input field. If the value exactly matches the value of a menu
10583 * option, that option will appear to be selected.
10584 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
10587 * After the user chooses an option, its `data` will be used as a new value for the widget.
10588 * A `label` also can be specified for each option: if given, it will be shown instead of the
10589 * `data` in the dropdown menu.
10591 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10593 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
10596 * // Example: A ComboBoxInputWidget.
10597 * var comboBox = new OO.ui.ComboBoxInputWidget( {
10598 * value: 'Option 1',
10600 * { data: 'Option 1' },
10601 * { data: 'Option 2' },
10602 * { data: 'Option 3' }
10605 * $( 'body' ).append( comboBox.$element );
10608 * // Example: A ComboBoxInputWidget with additional option labels.
10609 * var comboBox = new OO.ui.ComboBoxInputWidget( {
10610 * value: 'Option 1',
10613 * data: 'Option 1',
10614 * label: 'Option One'
10617 * data: 'Option 2',
10618 * label: 'Option Two'
10621 * data: 'Option 3',
10622 * label: 'Option Three'
10626 * $( 'body' ).append( comboBox.$element );
10628 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10631 * @extends OO.ui.TextInputWidget
10634 * @param {Object} [config] Configuration options
10635 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10636 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
10637 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
10638 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
10639 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
10640 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
10642 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
10643 // Configuration initialization
10644 config
= $.extend( {
10645 autocomplete
: false
10648 // ComboBoxInputWidget shouldn't support `multiline`
10649 config
.multiline
= false;
10651 // See InputWidget#reusePreInfuseDOM about `config.$input`
10652 if ( config
.$input
) {
10653 config
.$input
.removeAttr( 'list' );
10656 // Parent constructor
10657 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
10660 this.$overlay
= config
.$overlay
|| this.$element
;
10661 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
10662 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
10664 disabled
: this.disabled
10666 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
10670 $floatableContainer
: this.$element
,
10671 disabled
: this.isDisabled()
10677 this.connect( this, {
10678 change
: 'onInputChange',
10679 enter
: 'onInputEnter'
10681 this.dropdownButton
.connect( this, {
10682 click
: 'onDropdownButtonClick'
10684 this.menu
.connect( this, {
10685 choose
: 'onMenuChoose',
10686 add
: 'onMenuItemsChange',
10687 remove
: 'onMenuItemsChange',
10688 toggle
: 'onMenuToggle'
10692 this.$input
.attr( {
10694 'aria-owns': this.menu
.getElementId(),
10695 'aria-autocomplete': 'list'
10697 // Do not override options set via config.menu.items
10698 if ( config
.options
!== undefined ) {
10699 this.setOptions( config
.options
);
10701 this.$field
= $( '<div>' )
10702 .addClass( 'oo-ui-comboBoxInputWidget-field' )
10703 .append( this.$input
, this.dropdownButton
.$element
);
10705 .addClass( 'oo-ui-comboBoxInputWidget' )
10706 .append( this.$field
);
10707 this.$overlay
.append( this.menu
.$element
);
10708 this.onMenuItemsChange();
10713 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
10718 * Get the combobox's menu.
10720 * @return {OO.ui.MenuSelectWidget} Menu widget
10722 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
10727 * Get the combobox's text input widget.
10729 * @return {OO.ui.TextInputWidget} Text input widget
10731 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
10736 * Handle input change events.
10739 * @param {string} value New value
10741 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
10742 var match
= this.menu
.getItemFromData( value
);
10744 this.menu
.selectItem( match
);
10745 if ( this.menu
.findHighlightedItem() ) {
10746 this.menu
.highlightItem( match
);
10749 if ( !this.isDisabled() ) {
10750 this.menu
.toggle( true );
10755 * Handle input enter events.
10759 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
10760 if ( !this.isDisabled() ) {
10761 this.menu
.toggle( false );
10766 * Handle button click events.
10770 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
10771 this.menu
.toggle();
10776 * Handle menu choose events.
10779 * @param {OO.ui.OptionWidget} item Chosen item
10781 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
10782 this.setValue( item
.getData() );
10786 * Handle menu item change events.
10790 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
10791 var match
= this.menu
.getItemFromData( this.getValue() );
10792 this.menu
.selectItem( match
);
10793 if ( this.menu
.findHighlightedItem() ) {
10794 this.menu
.highlightItem( match
);
10796 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
10800 * Handle menu toggle events.
10803 * @param {boolean} isVisible Menu toggle event
10805 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
10806 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
10812 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
10814 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
10816 if ( this.dropdownButton
) {
10817 this.dropdownButton
.setDisabled( this.isDisabled() );
10820 this.menu
.setDisabled( this.isDisabled() );
10827 * Set the options available for this input.
10829 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10832 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
10835 .addItems( options
.map( function ( opt
) {
10836 return new OO
.ui
.MenuOptionWidget( {
10838 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
10846 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
10847 * which is a widget that is specified by reference before any optional configuration settings.
10849 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
10851 * - **left**: The label is placed before the field-widget and aligned with the left margin.
10852 * A left-alignment is used for forms with many fields.
10853 * - **right**: The label is placed before the field-widget and aligned to the right margin.
10854 * A right-alignment is used for long but familiar forms which users tab through,
10855 * verifying the current field with a quick glance at the label.
10856 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
10857 * that users fill out from top to bottom.
10858 * - **inline**: The label is placed after the field-widget and aligned to the left.
10859 * An inline-alignment is best used with checkboxes or radio buttons.
10861 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
10862 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
10864 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
10867 * @extends OO.ui.Layout
10868 * @mixins OO.ui.mixin.LabelElement
10869 * @mixins OO.ui.mixin.TitledElement
10872 * @param {OO.ui.Widget} fieldWidget Field widget
10873 * @param {Object} [config] Configuration options
10874 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
10875 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
10876 * The array may contain strings or OO.ui.HtmlSnippet instances.
10877 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
10878 * The array may contain strings or OO.ui.HtmlSnippet instances.
10879 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
10880 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
10881 * For important messages, you are advised to use `notices`, as they are always shown.
10882 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
10883 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
10885 * @throws {Error} An error is thrown if no widget is specified
10887 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
10888 // Allow passing positional parameters inside the config object
10889 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
10890 config
= fieldWidget
;
10891 fieldWidget
= config
.fieldWidget
;
10894 // Make sure we have required constructor arguments
10895 if ( fieldWidget
=== undefined ) {
10896 throw new Error( 'Widget not found' );
10899 // Configuration initialization
10900 config
= $.extend( { align
: 'left' }, config
);
10902 // Parent constructor
10903 OO
.ui
.FieldLayout
.parent
.call( this, config
);
10905 // Mixin constructors
10906 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
10907 $label
: $( '<label>' )
10909 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
10912 this.fieldWidget
= fieldWidget
;
10915 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
10916 this.$messages
= $( '<ul>' );
10917 this.$header
= $( '<span>' );
10918 this.$body
= $( '<div>' );
10920 if ( config
.help
) {
10921 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
10922 $overlay
: config
.$overlay
,
10926 classes
: [ 'oo-ui-fieldLayout-help' ],
10930 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
10931 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
10933 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
10935 this.$help
= this.popupButtonWidget
.$element
;
10937 this.$help
= $( [] );
10941 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
10944 if ( config
.help
) {
10945 // Set the 'aria-describedby' attribute on the fieldWidget
10946 // Preference given to an input or a button
10948 this.fieldWidget
.$input
||
10949 this.fieldWidget
.$button
||
10950 this.fieldWidget
.$element
10952 'aria-describedby',
10953 this.popupButtonWidget
.getPopup().getBodyId()
10956 if ( this.fieldWidget
.getInputId() ) {
10957 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
10959 this.$label
.on( 'click', function () {
10960 this.fieldWidget
.simulateLabelClick();
10965 .addClass( 'oo-ui-fieldLayout' )
10966 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
10967 .append( this.$body
);
10968 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
10969 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
10970 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
10972 .addClass( 'oo-ui-fieldLayout-field' )
10973 .append( this.fieldWidget
.$element
);
10975 this.setErrors( config
.errors
|| [] );
10976 this.setNotices( config
.notices
|| [] );
10977 this.setAlignment( config
.align
);
10978 // Call this again to take into account the widget's accessKey
10979 this.updateTitle();
10984 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
10985 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
10986 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
10991 * Handle field disable events.
10994 * @param {boolean} value Field is disabled
10996 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
10997 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11001 * Get the widget contained by the field.
11003 * @return {OO.ui.Widget} Field widget
11005 OO
.ui
.FieldLayout
.prototype.getField = function () {
11006 return this.fieldWidget
;
11010 * Return `true` if the given field widget can be used with `'inline'` alignment (see
11011 * #setAlignment). Return `false` if it can't or if this can't be determined.
11013 * @return {boolean}
11015 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11016 // This is very simplistic, but should be good enough.
11017 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11022 * @param {string} kind 'error' or 'notice'
11023 * @param {string|OO.ui.HtmlSnippet} text
11026 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11027 var $listItem
, $icon
, message
;
11028 $listItem
= $( '<li>' );
11029 if ( kind
=== 'error' ) {
11030 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11031 $listItem
.attr( 'role', 'alert' );
11032 } else if ( kind
=== 'notice' ) {
11033 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
11037 message
= new OO
.ui
.LabelWidget( { label
: text
} );
11039 .append( $icon
, message
.$element
)
11040 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
11045 * Set the field alignment mode.
11048 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
11051 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
11052 if ( value
!== this.align
) {
11053 // Default to 'left'
11054 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
11058 if ( value
=== 'inline' && !this.isFieldInline() ) {
11061 // Reorder elements
11062 if ( value
=== 'top' ) {
11063 this.$header
.append( this.$label
, this.$help
);
11064 this.$body
.append( this.$header
, this.$field
);
11065 } else if ( value
=== 'inline' ) {
11066 this.$header
.append( this.$label
, this.$help
);
11067 this.$body
.append( this.$field
, this.$header
);
11069 this.$header
.append( this.$label
);
11070 this.$body
.append( this.$header
, this.$help
, this.$field
);
11072 // Set classes. The following classes can be used here:
11073 // * oo-ui-fieldLayout-align-left
11074 // * oo-ui-fieldLayout-align-right
11075 // * oo-ui-fieldLayout-align-top
11076 // * oo-ui-fieldLayout-align-inline
11077 if ( this.align
) {
11078 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
11080 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
11081 this.align
= value
;
11088 * Set the list of error messages.
11090 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11091 * The array may contain strings or OO.ui.HtmlSnippet instances.
11094 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
11095 this.errors
= errors
.slice();
11096 this.updateMessages();
11101 * Set the list of notice messages.
11103 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11104 * The array may contain strings or OO.ui.HtmlSnippet instances.
11107 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
11108 this.notices
= notices
.slice();
11109 this.updateMessages();
11114 * Update the rendering of error and notice messages.
11118 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
11120 this.$messages
.empty();
11122 if ( this.errors
.length
|| this.notices
.length
) {
11123 this.$body
.after( this.$messages
);
11125 this.$messages
.remove();
11129 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
11130 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
11132 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
11133 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
11138 * Include information about the widget's accessKey in our title. TitledElement calls this method.
11139 * (This is a bit of a hack.)
11142 * @param {string} title Tooltip label for 'title' attribute
11145 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
11146 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
11147 return this.fieldWidget
.formatTitleWithAccessKey( title
);
11153 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
11154 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
11155 * is required and is specified before any optional configuration settings.
11157 * Labels can be aligned in one of four ways:
11159 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11160 * A left-alignment is used for forms with many fields.
11161 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11162 * A right-alignment is used for long but familiar forms which users tab through,
11163 * verifying the current field with a quick glance at the label.
11164 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11165 * that users fill out from top to bottom.
11166 * - **inline**: The label is placed after the field-widget and aligned to the left.
11167 * An inline-alignment is best used with checkboxes or radio buttons.
11169 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
11170 * text is specified.
11173 * // Example of an ActionFieldLayout
11174 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
11175 * new OO.ui.TextInputWidget( {
11176 * placeholder: 'Field widget'
11178 * new OO.ui.ButtonWidget( {
11182 * label: 'An ActionFieldLayout. This label is aligned top',
11184 * help: 'This is help text'
11188 * $( 'body' ).append( actionFieldLayout.$element );
11191 * @extends OO.ui.FieldLayout
11194 * @param {OO.ui.Widget} fieldWidget Field widget
11195 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
11196 * @param {Object} config
11198 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
11199 // Allow passing positional parameters inside the config object
11200 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11201 config
= fieldWidget
;
11202 fieldWidget
= config
.fieldWidget
;
11203 buttonWidget
= config
.buttonWidget
;
11206 // Parent constructor
11207 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
11210 this.buttonWidget
= buttonWidget
;
11211 this.$button
= $( '<span>' );
11212 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11216 .addClass( 'oo-ui-actionFieldLayout' );
11218 .addClass( 'oo-ui-actionFieldLayout-button' )
11219 .append( this.buttonWidget
.$element
);
11221 .addClass( 'oo-ui-actionFieldLayout-input' )
11222 .append( this.fieldWidget
.$element
);
11224 .append( this.$input
, this.$button
);
11229 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
11232 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
11233 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
11234 * configured with a label as well. For more information and examples,
11235 * please see the [OOjs UI documentation on MediaWiki][1].
11238 * // Example of a fieldset layout
11239 * var input1 = new OO.ui.TextInputWidget( {
11240 * placeholder: 'A text input field'
11243 * var input2 = new OO.ui.TextInputWidget( {
11244 * placeholder: 'A text input field'
11247 * var fieldset = new OO.ui.FieldsetLayout( {
11248 * label: 'Example of a fieldset layout'
11251 * fieldset.addItems( [
11252 * new OO.ui.FieldLayout( input1, {
11253 * label: 'Field One'
11255 * new OO.ui.FieldLayout( input2, {
11256 * label: 'Field Two'
11259 * $( 'body' ).append( fieldset.$element );
11261 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
11264 * @extends OO.ui.Layout
11265 * @mixins OO.ui.mixin.IconElement
11266 * @mixins OO.ui.mixin.LabelElement
11267 * @mixins OO.ui.mixin.GroupElement
11270 * @param {Object} [config] Configuration options
11271 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
11272 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
11273 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
11274 * For important messages, you are advised to use `notices`, as they are always shown.
11275 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
11276 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
11278 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
11279 // Configuration initialization
11280 config
= config
|| {};
11282 // Parent constructor
11283 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
11285 // Mixin constructors
11286 OO
.ui
.mixin
.IconElement
.call( this, config
);
11287 OO
.ui
.mixin
.LabelElement
.call( this, config
);
11288 OO
.ui
.mixin
.GroupElement
.call( this, config
);
11291 this.$header
= $( '<legend>' );
11292 if ( config
.help
) {
11293 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
11294 $overlay
: config
.$overlay
,
11298 classes
: [ 'oo-ui-fieldsetLayout-help' ],
11302 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
11303 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
11305 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
11307 this.$help
= this.popupButtonWidget
.$element
;
11309 this.$help
= $( [] );
11314 .addClass( 'oo-ui-fieldsetLayout-header' )
11315 .append( this.$icon
, this.$label
, this.$help
);
11316 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
11318 .addClass( 'oo-ui-fieldsetLayout' )
11319 .prepend( this.$header
, this.$group
);
11320 if ( Array
.isArray( config
.items
) ) {
11321 this.addItems( config
.items
);
11327 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
11328 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
11329 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
11330 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
11332 /* Static Properties */
11338 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
11341 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
11342 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
11343 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
11344 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
11346 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
11347 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
11348 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
11349 * some fancier controls. Some controls have both regular and InputWidget variants, for example
11350 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
11351 * often have simplified APIs to match the capabilities of HTML forms.
11352 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
11354 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
11355 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11358 * // Example of a form layout that wraps a fieldset layout
11359 * var input1 = new OO.ui.TextInputWidget( {
11360 * placeholder: 'Username'
11362 * var input2 = new OO.ui.TextInputWidget( {
11363 * placeholder: 'Password',
11366 * var submit = new OO.ui.ButtonInputWidget( {
11370 * var fieldset = new OO.ui.FieldsetLayout( {
11371 * label: 'A form layout'
11373 * fieldset.addItems( [
11374 * new OO.ui.FieldLayout( input1, {
11375 * label: 'Username',
11378 * new OO.ui.FieldLayout( input2, {
11379 * label: 'Password',
11382 * new OO.ui.FieldLayout( submit )
11384 * var form = new OO.ui.FormLayout( {
11385 * items: [ fieldset ],
11386 * action: '/api/formhandler',
11389 * $( 'body' ).append( form.$element );
11392 * @extends OO.ui.Layout
11393 * @mixins OO.ui.mixin.GroupElement
11396 * @param {Object} [config] Configuration options
11397 * @cfg {string} [method] HTML form `method` attribute
11398 * @cfg {string} [action] HTML form `action` attribute
11399 * @cfg {string} [enctype] HTML form `enctype` attribute
11400 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
11402 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
11405 // Configuration initialization
11406 config
= config
|| {};
11408 // Parent constructor
11409 OO
.ui
.FormLayout
.parent
.call( this, config
);
11411 // Mixin constructors
11412 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11415 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
11417 // Make sure the action is safe
11418 action
= config
.action
;
11419 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
11420 action
= './' + action
;
11425 .addClass( 'oo-ui-formLayout' )
11427 method
: config
.method
,
11429 enctype
: config
.enctype
11431 if ( Array
.isArray( config
.items
) ) {
11432 this.addItems( config
.items
);
11438 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
11439 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
11444 * A 'submit' event is emitted when the form is submitted.
11449 /* Static Properties */
11455 OO
.ui
.FormLayout
.static.tagName
= 'form';
11460 * Handle form submit events.
11463 * @param {jQuery.Event} e Submit event
11466 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
11467 if ( this.emit( 'submit' ) ) {
11473 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
11474 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
11477 * // Example of a panel layout
11478 * var panel = new OO.ui.PanelLayout( {
11482 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
11484 * $( 'body' ).append( panel.$element );
11487 * @extends OO.ui.Layout
11490 * @param {Object} [config] Configuration options
11491 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
11492 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
11493 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
11494 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
11496 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
11497 // Configuration initialization
11498 config
= $.extend( {
11505 // Parent constructor
11506 OO
.ui
.PanelLayout
.parent
.call( this, config
);
11509 this.$element
.addClass( 'oo-ui-panelLayout' );
11510 if ( config
.scrollable
) {
11511 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
11513 if ( config
.padded
) {
11514 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
11516 if ( config
.expanded
) {
11517 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
11519 if ( config
.framed
) {
11520 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
11526 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
11531 * Focus the panel layout
11533 * The default implementation just focuses the first focusable element in the panel
11535 OO
.ui
.PanelLayout
.prototype.focus = function () {
11536 OO
.ui
.findFocusable( this.$element
).focus();
11540 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
11541 * items), with small margins between them. Convenient when you need to put a number of block-level
11542 * widgets on a single line next to each other.
11544 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
11547 * // HorizontalLayout with a text input and a label
11548 * var layout = new OO.ui.HorizontalLayout( {
11550 * new OO.ui.LabelWidget( { label: 'Label' } ),
11551 * new OO.ui.TextInputWidget( { value: 'Text' } )
11554 * $( 'body' ).append( layout.$element );
11557 * @extends OO.ui.Layout
11558 * @mixins OO.ui.mixin.GroupElement
11561 * @param {Object} [config] Configuration options
11562 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
11564 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
11565 // Configuration initialization
11566 config
= config
|| {};
11568 // Parent constructor
11569 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
11571 // Mixin constructors
11572 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11575 this.$element
.addClass( 'oo-ui-horizontalLayout' );
11576 if ( Array
.isArray( config
.items
) ) {
11577 this.addItems( config
.items
);
11583 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
11584 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
11588 //# sourceMappingURL=oojs-ui-core.js.map