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-17T23:18:51Z
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 !this.menu
.isVisible() &&
7595 e
.which
=== OO
.ui
.Keys
.SPACE
||
7596 e
.which
=== OO
.ui
.Keys
.UP
||
7597 e
.which
=== OO
.ui
.Keys
.DOWN
7608 * RadioOptionWidget is an option widget that looks like a radio button.
7609 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
7610 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
7612 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
7615 * @extends OO.ui.OptionWidget
7618 * @param {Object} [config] Configuration options
7620 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
7621 // Configuration initialization
7622 config
= config
|| {};
7624 // Properties (must be done before parent constructor which calls #setDisabled)
7625 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
7627 // Parent constructor
7628 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
7631 // Remove implicit role, we're handling it ourselves
7632 this.radio
.$input
.attr( 'role', 'presentation' );
7634 .addClass( 'oo-ui-radioOptionWidget' )
7635 .attr( 'role', 'radio' )
7636 .attr( 'aria-checked', 'false' )
7637 .removeAttr( 'aria-selected' )
7638 .prepend( this.radio
.$element
);
7643 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
7645 /* Static Properties */
7651 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
7657 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
7663 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
7669 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
7676 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
7677 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
7679 this.radio
.setSelected( state
);
7681 .attr( 'aria-checked', state
.toString() )
7682 .removeAttr( 'aria-selected' );
7690 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
7691 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
7693 this.radio
.setDisabled( this.isDisabled() );
7699 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
7700 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
7701 * an interface for adding, removing and selecting options.
7702 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
7704 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7705 * OO.ui.RadioSelectInputWidget instead.
7708 * // A RadioSelectWidget with RadioOptions.
7709 * var option1 = new OO.ui.RadioOptionWidget( {
7711 * label: 'Selected radio option'
7714 * var option2 = new OO.ui.RadioOptionWidget( {
7716 * label: 'Unselected radio option'
7719 * var radioSelect=new OO.ui.RadioSelectWidget( {
7720 * items: [ option1, option2 ]
7723 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
7724 * radioSelect.selectItem( option1 );
7726 * $( 'body' ).append( radioSelect.$element );
7728 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
7732 * @extends OO.ui.SelectWidget
7733 * @mixins OO.ui.mixin.TabIndexedElement
7736 * @param {Object} [config] Configuration options
7738 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
7739 // Parent constructor
7740 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
7742 // Mixin constructors
7743 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
7747 focus
: this.bindKeyDownListener
.bind( this ),
7748 blur
: this.unbindKeyDownListener
.bind( this )
7753 .addClass( 'oo-ui-radioSelectWidget' )
7754 .attr( 'role', 'radiogroup' );
7759 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
7760 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7763 * MultioptionWidgets are special elements that can be selected and configured with data. The
7764 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
7765 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
7766 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
7768 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Multioptions
7771 * @extends OO.ui.Widget
7772 * @mixins OO.ui.mixin.ItemWidget
7773 * @mixins OO.ui.mixin.LabelElement
7776 * @param {Object} [config] Configuration options
7777 * @cfg {boolean} [selected=false] Whether the option is initially selected
7779 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
7780 // Configuration initialization
7781 config
= config
|| {};
7783 // Parent constructor
7784 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
7786 // Mixin constructors
7787 OO
.ui
.mixin
.ItemWidget
.call( this );
7788 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7791 this.selected
= null;
7795 .addClass( 'oo-ui-multioptionWidget' )
7796 .append( this.$label
);
7797 this.setSelected( config
.selected
);
7802 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
7803 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
7804 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
7811 * A change event is emitted when the selected state of the option changes.
7813 * @param {boolean} selected Whether the option is now selected
7819 * Check if the option is selected.
7821 * @return {boolean} Item is selected
7823 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
7824 return this.selected
;
7828 * Set the option’s selected state. In general, all modifications to the selection
7829 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
7830 * method instead of this method.
7832 * @param {boolean} [state=false] Select option
7835 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
7837 if ( this.selected
!== state
) {
7838 this.selected
= state
;
7839 this.emit( 'change', state
);
7840 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
7846 * MultiselectWidget allows selecting multiple options from a list.
7848 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
7850 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
7854 * @extends OO.ui.Widget
7855 * @mixins OO.ui.mixin.GroupWidget
7858 * @param {Object} [config] Configuration options
7859 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
7861 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
7862 // Parent constructor
7863 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
7865 // Configuration initialization
7866 config
= config
|| {};
7868 // Mixin constructors
7869 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
7872 this.aggregate( { change
: 'select' } );
7873 // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
7874 // by GroupElement only when items are added/removed
7875 this.connect( this, { select
: [ 'emit', 'change' ] } );
7878 if ( config
.items
) {
7879 this.addItems( config
.items
);
7881 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
7882 this.$element
.addClass( 'oo-ui-multiselectWidget' )
7883 .append( this.$group
);
7888 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
7889 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
7896 * A change event is emitted when the set of items changes, or an item is selected or deselected.
7902 * A select event is emitted when an item is selected or deselected.
7908 * Get options that are selected.
7910 * @return {OO.ui.MultioptionWidget[]} Selected options
7912 OO
.ui
.MultiselectWidget
.prototype.getSelectedItems = function () {
7913 return this.items
.filter( function ( item
) {
7914 return item
.isSelected();
7919 * Get the data of options that are selected.
7921 * @return {Object[]|string[]} Values of selected options
7923 OO
.ui
.MultiselectWidget
.prototype.getSelectedItemsData = function () {
7924 return this.getSelectedItems().map( function ( item
) {
7930 * Select options by reference. Options not mentioned in the `items` array will be deselected.
7932 * @param {OO.ui.MultioptionWidget[]} items Items to select
7935 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
7936 this.items
.forEach( function ( item
) {
7937 var selected
= items
.indexOf( item
) !== -1;
7938 item
.setSelected( selected
);
7944 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
7946 * @param {Object[]|string[]} datas Values of items to select
7949 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
7952 items
= datas
.map( function ( data
) {
7953 return widget
.getItemFromData( data
);
7955 this.selectItems( items
);
7960 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
7961 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
7962 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
7964 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
7967 * @extends OO.ui.MultioptionWidget
7970 * @param {Object} [config] Configuration options
7972 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
7973 // Configuration initialization
7974 config
= config
|| {};
7976 // Properties (must be done before parent constructor which calls #setDisabled)
7977 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
7979 // Parent constructor
7980 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
7983 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
7984 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
7988 .addClass( 'oo-ui-checkboxMultioptionWidget' )
7989 .prepend( this.checkbox
.$element
);
7994 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
7996 /* Static Properties */
8002 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8007 * Handle checkbox selected state change.
8011 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8012 this.setSelected( this.checkbox
.isSelected() );
8018 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8019 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8020 this.checkbox
.setSelected( state
);
8027 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8028 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8029 this.checkbox
.setDisabled( this.isDisabled() );
8036 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8037 this.checkbox
.focus();
8041 * Handle key down events.
8044 * @param {jQuery.Event} e
8046 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8048 element
= this.getElementGroup(),
8051 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8052 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8053 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8054 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8064 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8065 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8066 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8067 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
8069 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8070 * OO.ui.CheckboxMultiselectInputWidget instead.
8073 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8074 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8077 * label: 'Selected checkbox'
8080 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
8082 * label: 'Unselected checkbox'
8085 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
8086 * items: [ option1, option2 ]
8089 * $( 'body' ).append( multiselect.$element );
8091 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
8094 * @extends OO.ui.MultiselectWidget
8097 * @param {Object} [config] Configuration options
8099 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8100 // Parent constructor
8101 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8104 this.$lastClicked
= null;
8107 this.$group
.on( 'click', this.onClick
.bind( this ) );
8111 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8116 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8121 * Get an option by its position relative to the specified item (or to the start of the option array,
8122 * if item is `null`). The direction in which to search through the option array is specified with a
8123 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8124 * `null` if there are no options in the array.
8126 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8127 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8128 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8130 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8131 var currentIndex
, nextIndex
, i
,
8132 increase
= direction
> 0 ? 1 : -1,
8133 len
= this.items
.length
;
8136 currentIndex
= this.items
.indexOf( item
);
8137 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8139 // If no item is selected and moving forward, start at the beginning.
8140 // If moving backward, start at the end.
8141 nextIndex
= direction
> 0 ? 0 : len
- 1;
8144 for ( i
= 0; i
< len
; i
++ ) {
8145 item
= this.items
[ nextIndex
];
8146 if ( item
&& !item
.isDisabled() ) {
8149 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8155 * Handle click events on checkboxes.
8157 * @param {jQuery.Event} e
8159 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8160 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8161 $lastClicked
= this.$lastClicked
,
8162 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8163 .not( '.oo-ui-widget-disabled' );
8165 // Allow selecting multiple options at once by Shift-clicking them
8166 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8167 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8168 lastClickedIndex
= $options
.index( $lastClicked
);
8169 nowClickedIndex
= $options
.index( $nowClicked
);
8170 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8171 // browser. In either case we don't need custom handling.
8172 if ( nowClickedIndex
!== lastClickedIndex
) {
8174 wasSelected
= items
[ nowClickedIndex
].isSelected();
8175 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8177 // This depends on the DOM order of the items and the order of the .items array being the same.
8178 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8179 if ( !items
[ i
].isDisabled() ) {
8180 items
[ i
].setSelected( !wasSelected
);
8183 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8184 // handling first, then set our value. The order in which events happen is different for
8185 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8186 // non-click actions that change the checkboxes.
8188 setTimeout( function () {
8189 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8190 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8196 if ( $nowClicked
.length
) {
8197 this.$lastClicked
= $nowClicked
;
8206 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8208 if ( !this.isDisabled() ) {
8209 item
= this.getRelativeFocusableItem( null, 1 );
8220 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8225 * Progress bars visually display the status of an operation, such as a download,
8226 * and can be either determinate or indeterminate:
8228 * - **determinate** process bars show the percent of an operation that is complete.
8230 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8231 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8232 * not use percentages.
8234 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8237 * // Examples of determinate and indeterminate progress bars.
8238 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8241 * var progressBar2 = new OO.ui.ProgressBarWidget();
8243 * // Create a FieldsetLayout to layout progress bars
8244 * var fieldset = new OO.ui.FieldsetLayout;
8245 * fieldset.addItems( [
8246 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
8247 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
8249 * $( 'body' ).append( fieldset.$element );
8252 * @extends OO.ui.Widget
8255 * @param {Object} [config] Configuration options
8256 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8257 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
8258 * By default, the progress bar is indeterminate.
8260 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8261 // Configuration initialization
8262 config
= config
|| {};
8264 // Parent constructor
8265 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8268 this.$bar
= $( '<div>' );
8269 this.progress
= null;
8272 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8273 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8276 role
: 'progressbar',
8278 'aria-valuemax': 100
8280 .addClass( 'oo-ui-progressBarWidget' )
8281 .append( this.$bar
);
8286 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8288 /* Static Properties */
8294 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8299 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8301 * @return {number|boolean} Progress percent
8303 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8304 return this.progress
;
8308 * Set the percent of the process completed or `false` for an indeterminate process.
8310 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8312 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8313 this.progress
= progress
;
8315 if ( progress
!== false ) {
8316 this.$bar
.css( 'width', this.progress
+ '%' );
8317 this.$element
.attr( 'aria-valuenow', this.progress
);
8319 this.$bar
.css( 'width', '' );
8320 this.$element
.removeAttr( 'aria-valuenow' );
8322 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8326 * InputWidget is the base class for all input widgets, which
8327 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8328 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8329 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8331 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8335 * @extends OO.ui.Widget
8336 * @mixins OO.ui.mixin.FlaggedElement
8337 * @mixins OO.ui.mixin.TabIndexedElement
8338 * @mixins OO.ui.mixin.TitledElement
8339 * @mixins OO.ui.mixin.AccessKeyedElement
8342 * @param {Object} [config] Configuration options
8343 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8344 * @cfg {string} [value=''] The value of the input.
8345 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8346 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8347 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8348 * before it is accepted.
8350 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8351 // Configuration initialization
8352 config
= config
|| {};
8354 // Parent constructor
8355 OO
.ui
.InputWidget
.parent
.call( this, config
);
8358 // See #reusePreInfuseDOM about config.$input
8359 this.$input
= config
.$input
|| this.getInputElement( config
);
8361 this.inputFilter
= config
.inputFilter
;
8363 // Mixin constructors
8364 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8365 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8366 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8367 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8370 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8374 .addClass( 'oo-ui-inputWidget-input' )
8375 .attr( 'name', config
.name
)
8376 .prop( 'disabled', this.isDisabled() );
8378 .addClass( 'oo-ui-inputWidget' )
8379 .append( this.$input
);
8380 this.setValue( config
.value
);
8382 this.setDir( config
.dir
);
8384 if ( config
.inputId
!== undefined ) {
8385 this.setInputId( config
.inputId
);
8391 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8392 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8393 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8394 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8395 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8397 /* Static Methods */
8402 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8403 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8404 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8405 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8412 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8413 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8414 if ( config
.$input
&& config
.$input
.length
) {
8415 state
.value
= config
.$input
.val();
8416 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8417 state
.focus
= config
.$input
.is( ':focus' );
8427 * A change event is emitted when the value of the input changes.
8429 * @param {string} value
8435 * Get input element.
8437 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
8438 * different circumstances. The element must have a `value` property (like form elements).
8441 * @param {Object} config Configuration options
8442 * @return {jQuery} Input element
8444 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
8445 return $( '<input>' );
8449 * Handle potentially value-changing events.
8452 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8454 OO
.ui
.InputWidget
.prototype.onEdit = function () {
8456 if ( !this.isDisabled() ) {
8457 // Allow the stack to clear so the value will be updated
8458 setTimeout( function () {
8459 widget
.setValue( widget
.$input
.val() );
8465 * Get the value of the input.
8467 * @return {string} Input value
8469 OO
.ui
.InputWidget
.prototype.getValue = function () {
8470 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8471 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8472 var value
= this.$input
.val();
8473 if ( this.value
!== value
) {
8474 this.setValue( value
);
8480 * Set the directionality of the input.
8482 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
8485 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
8486 this.$input
.prop( 'dir', dir
);
8491 * Set the value of the input.
8493 * @param {string} value New value
8497 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
8498 value
= this.cleanUpValue( value
);
8499 // Update the DOM if it has changed. Note that with cleanUpValue, it
8500 // is possible for the DOM value to change without this.value changing.
8501 if ( this.$input
.val() !== value
) {
8502 this.$input
.val( value
);
8504 if ( this.value
!== value
) {
8506 this.emit( 'change', this.value
);
8512 * Clean up incoming value.
8514 * Ensures value is a string, and converts undefined and null to empty string.
8517 * @param {string} value Original value
8518 * @return {string} Cleaned up value
8520 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
8521 if ( value
=== undefined || value
=== null ) {
8523 } else if ( this.inputFilter
) {
8524 return this.inputFilter( String( value
) );
8526 return String( value
);
8533 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
8534 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8535 if ( this.$input
) {
8536 this.$input
.prop( 'disabled', this.isDisabled() );
8542 * Set the 'id' attribute of the `<input>` element.
8544 * @param {string} id
8547 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
8548 this.$input
.attr( 'id', id
);
8555 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
8556 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8557 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
8558 this.setValue( state
.value
);
8560 if ( state
.focus
) {
8566 * Data widget intended for creating 'hidden'-type inputs.
8569 * @extends OO.ui.Widget
8572 * @param {Object} [config] Configuration options
8573 * @cfg {string} [value=''] The value of the input.
8574 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8576 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
8577 // Configuration initialization
8578 config
= $.extend( { value
: '', name
: '' }, config
);
8580 // Parent constructor
8581 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
8584 this.$element
.attr( {
8586 value
: config
.value
,
8589 this.$element
.removeAttr( 'aria-disabled' );
8594 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
8596 /* Static Properties */
8602 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
8605 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
8606 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
8607 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
8608 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
8609 * [OOjs UI documentation on MediaWiki] [1] for more information.
8612 * // A ButtonInputWidget rendered as an HTML button, the default.
8613 * var button = new OO.ui.ButtonInputWidget( {
8614 * label: 'Input button',
8618 * $( 'body' ).append( button.$element );
8620 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
8623 * @extends OO.ui.InputWidget
8624 * @mixins OO.ui.mixin.ButtonElement
8625 * @mixins OO.ui.mixin.IconElement
8626 * @mixins OO.ui.mixin.IndicatorElement
8627 * @mixins OO.ui.mixin.LabelElement
8628 * @mixins OO.ui.mixin.TitledElement
8631 * @param {Object} [config] Configuration options
8632 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
8633 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
8634 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
8635 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
8636 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
8638 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
8639 // Configuration initialization
8640 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
8642 // See InputWidget#reusePreInfuseDOM about config.$input
8643 if ( config
.$input
) {
8644 config
.$input
.empty();
8647 // Properties (must be set before parent constructor, which calls #setValue)
8648 this.useInputTag
= config
.useInputTag
;
8650 // Parent constructor
8651 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
8653 // Mixin constructors
8654 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
8655 OO
.ui
.mixin
.IconElement
.call( this, config
);
8656 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8657 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8658 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8661 if ( !config
.useInputTag
) {
8662 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
8664 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
8669 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
8670 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
8671 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
8672 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
8673 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
8674 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
8676 /* Static Properties */
8682 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
8690 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
8692 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
8693 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
8699 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
8701 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
8702 * text, or `null` for no label
8705 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
8706 if ( typeof label
=== 'function' ) {
8707 label
= OO
.ui
.resolveMsg( label
);
8710 if ( this.useInputTag
) {
8711 // Discard non-plaintext labels
8712 if ( typeof label
!== 'string' ) {
8716 this.$input
.val( label
);
8719 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
8723 * Set the value of the input.
8725 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
8726 * they do not support {@link #value values}.
8728 * @param {string} value New value
8731 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
8732 if ( !this.useInputTag
) {
8733 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
8741 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
8742 // Disable generating `<label>` elements for buttons. One would very rarely need additional label
8743 // for a button, and it's already a big clickable target, and it causes unexpected rendering.
8748 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
8749 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
8750 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
8751 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
8753 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
8756 * // An example of selected, unselected, and disabled checkbox inputs
8757 * var checkbox1=new OO.ui.CheckboxInputWidget( {
8761 * var checkbox2=new OO.ui.CheckboxInputWidget( {
8764 * var checkbox3=new OO.ui.CheckboxInputWidget( {
8768 * // Create a fieldset layout with fields for each checkbox.
8769 * var fieldset = new OO.ui.FieldsetLayout( {
8770 * label: 'Checkboxes'
8772 * fieldset.addItems( [
8773 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
8774 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
8775 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
8777 * $( 'body' ).append( fieldset.$element );
8779 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8782 * @extends OO.ui.InputWidget
8785 * @param {Object} [config] Configuration options
8786 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
8788 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
8789 // Configuration initialization
8790 config
= config
|| {};
8792 // Parent constructor
8793 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
8797 .addClass( 'oo-ui-checkboxInputWidget' )
8798 // Required for pretty styling in WikimediaUI theme
8799 .append( $( '<span>' ) );
8800 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
8805 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
8807 /* Static Properties */
8813 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
8815 /* Static Methods */
8820 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8821 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8822 state
.checked
= config
.$input
.prop( 'checked' );
8832 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
8833 return $( '<input>' ).attr( 'type', 'checkbox' );
8839 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
8841 if ( !this.isDisabled() ) {
8842 // Allow the stack to clear so the value will be updated
8843 setTimeout( function () {
8844 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
8850 * Set selection state of this checkbox.
8852 * @param {boolean} state `true` for selected
8855 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
8857 if ( this.selected
!== state
) {
8858 this.selected
= state
;
8859 this.$input
.prop( 'checked', this.selected
);
8860 this.emit( 'change', this.selected
);
8866 * Check if this checkbox is selected.
8868 * @return {boolean} Checkbox is selected
8870 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
8871 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8872 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8873 var selected
= this.$input
.prop( 'checked' );
8874 if ( this.selected
!== selected
) {
8875 this.setSelected( selected
);
8877 return this.selected
;
8883 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
8884 if ( !this.isDisabled() ) {
8885 this.$input
.click();
8893 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8894 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8895 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
8896 this.setSelected( state
.checked
);
8901 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
8902 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8903 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8904 * more information about input widgets.
8906 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
8907 * are no options. If no `value` configuration option is provided, the first option is selected.
8908 * If you need a state representing no value (no option being selected), use a DropdownWidget.
8910 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
8913 * // Example: A DropdownInputWidget with three options
8914 * var dropdownInput = new OO.ui.DropdownInputWidget( {
8916 * { data: 'a', label: 'First' },
8917 * { data: 'b', label: 'Second'},
8918 * { data: 'c', label: 'Third' }
8921 * $( 'body' ).append( dropdownInput.$element );
8923 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8926 * @extends OO.ui.InputWidget
8927 * @mixins OO.ui.mixin.TitledElement
8930 * @param {Object} [config] Configuration options
8931 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8932 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
8934 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
8935 // Configuration initialization
8936 config
= config
|| {};
8938 // See InputWidget#reusePreInfuseDOM about config.$input
8939 if ( config
.$input
) {
8940 config
.$input
.addClass( 'oo-ui-element-hidden' );
8943 // Properties (must be done before parent constructor which calls #setDisabled)
8944 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
8946 // Parent constructor
8947 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
8949 // Mixin constructors
8950 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8953 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
8956 this.setOptions( config
.options
|| [] );
8957 // Set the value again, after we did setOptions(). The call from parent doesn't work because the
8958 // widget has no valid options when it happens.
8959 this.setValue( config
.value
);
8961 .addClass( 'oo-ui-dropdownInputWidget' )
8962 .append( this.dropdownWidget
.$element
);
8963 this.setTabIndexedElement( null );
8968 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
8969 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
8977 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
8978 return $( '<input>' ).attr( 'type', 'hidden' );
8982 * Handles menu select events.
8985 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
8987 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
8988 this.setValue( item
? item
.getData() : '' );
8994 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
8996 value
= this.cleanUpValue( value
);
8997 // Only allow setting values that are actually present in the dropdown
8998 selected
= this.dropdownWidget
.getMenu().getItemFromData( value
) ||
8999 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9000 this.dropdownWidget
.getMenu().selectItem( selected
);
9001 value
= selected
? selected
.getData() : '';
9002 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9009 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9010 this.dropdownWidget
.setDisabled( state
);
9011 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9016 * Set the options available for this input.
9018 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9021 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9023 value
= this.getValue(),
9026 // Rebuild the dropdown menu
9027 this.dropdownWidget
.getMenu()
9029 .addItems( options
.map( function ( opt
) {
9030 var optValue
= widget
.cleanUpValue( opt
.data
);
9032 if ( opt
.optgroup
=== undefined ) {
9033 return new OO
.ui
.MenuOptionWidget( {
9035 label
: opt
.label
!== undefined ? opt
.label
: optValue
9038 return new OO
.ui
.MenuSectionOptionWidget( {
9044 // Restore the previous value, or reset to something sensible
9045 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
9046 // Previous value is still available, ensure consistency with the dropdown
9047 this.setValue( value
);
9049 // No longer valid, reset
9050 if ( options
.length
) {
9051 this.setValue( options
[ 0 ].data
);
9061 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9062 this.dropdownWidget
.focus();
9069 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9070 this.dropdownWidget
.blur();
9075 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9076 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9077 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9078 * please see the [OOjs UI documentation on MediaWiki][1].
9080 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9083 * // An example of selected, unselected, and disabled radio inputs
9084 * var radio1 = new OO.ui.RadioInputWidget( {
9088 * var radio2 = new OO.ui.RadioInputWidget( {
9091 * var radio3 = new OO.ui.RadioInputWidget( {
9095 * // Create a fieldset layout with fields for each radio button.
9096 * var fieldset = new OO.ui.FieldsetLayout( {
9097 * label: 'Radio inputs'
9099 * fieldset.addItems( [
9100 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9101 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9102 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9104 * $( 'body' ).append( fieldset.$element );
9106 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9109 * @extends OO.ui.InputWidget
9112 * @param {Object} [config] Configuration options
9113 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9115 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9116 // Configuration initialization
9117 config
= config
|| {};
9119 // Parent constructor
9120 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9124 .addClass( 'oo-ui-radioInputWidget' )
9125 // Required for pretty styling in WikimediaUI theme
9126 .append( $( '<span>' ) );
9127 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9132 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9134 /* Static Properties */
9140 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9142 /* Static Methods */
9147 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9148 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9149 state
.checked
= config
.$input
.prop( 'checked' );
9159 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9160 return $( '<input>' ).attr( 'type', 'radio' );
9166 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9167 // RadioInputWidget doesn't track its state.
9171 * Set selection state of this radio button.
9173 * @param {boolean} state `true` for selected
9176 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9177 // RadioInputWidget doesn't track its state.
9178 this.$input
.prop( 'checked', state
);
9183 * Check if this radio button is selected.
9185 * @return {boolean} Radio is selected
9187 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
9188 return this.$input
.prop( 'checked' );
9194 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
9195 if ( !this.isDisabled() ) {
9196 this.$input
.click();
9204 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9205 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9206 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9207 this.setSelected( state
.checked
);
9212 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9213 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9214 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
9215 * more information about input widgets.
9217 * This and OO.ui.DropdownInputWidget support the same configuration options.
9220 * // Example: A RadioSelectInputWidget with three options
9221 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9223 * { data: 'a', label: 'First' },
9224 * { data: 'b', label: 'Second'},
9225 * { data: 'c', label: 'Third' }
9228 * $( 'body' ).append( radioSelectInput.$element );
9230 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9233 * @extends OO.ui.InputWidget
9236 * @param {Object} [config] Configuration options
9237 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9239 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
9240 // Configuration initialization
9241 config
= config
|| {};
9243 // Properties (must be done before parent constructor which calls #setDisabled)
9244 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
9246 // Parent constructor
9247 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
9250 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
9253 this.setOptions( config
.options
|| [] );
9255 .addClass( 'oo-ui-radioSelectInputWidget' )
9256 .append( this.radioSelectWidget
.$element
);
9257 this.setTabIndexedElement( null );
9262 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
9264 /* Static Methods */
9269 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9270 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9271 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9278 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9279 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9280 // Cannot reuse the `<input type=radio>` set
9281 delete config
.$input
;
9291 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
9292 return $( '<input>' ).attr( 'type', 'hidden' );
9296 * Handles menu select events.
9299 * @param {OO.ui.RadioOptionWidget} item Selected menu item
9301 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
9302 this.setValue( item
.getData() );
9308 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
9309 value
= this.cleanUpValue( value
);
9310 this.radioSelectWidget
.selectItemByData( value
);
9311 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9318 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
9319 this.radioSelectWidget
.setDisabled( state
);
9320 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9325 * Set the options available for this input.
9327 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9330 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
9332 value
= this.getValue(),
9335 // Rebuild the radioSelect menu
9336 this.radioSelectWidget
9338 .addItems( options
.map( function ( opt
) {
9339 var optValue
= widget
.cleanUpValue( opt
.data
);
9340 return new OO
.ui
.RadioOptionWidget( {
9342 label
: opt
.label
!== undefined ? opt
.label
: optValue
9346 // Restore the previous value, or reset to something sensible
9347 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
9348 // Previous value is still available, ensure consistency with the radioSelect
9349 this.setValue( value
);
9351 // No longer valid, reset
9352 if ( options
.length
) {
9353 this.setValue( options
[ 0 ].data
);
9363 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
9364 this.radioSelectWidget
.focus();
9371 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
9372 this.radioSelectWidget
.blur();
9377 * CheckboxMultiselectInputWidget is a
9378 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
9379 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
9380 * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on MediaWiki][1] for
9381 * more information about input widgets.
9384 * // Example: A CheckboxMultiselectInputWidget with three options
9385 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
9387 * { data: 'a', label: 'First' },
9388 * { data: 'b', label: 'Second'},
9389 * { data: 'c', label: 'Third' }
9392 * $( 'body' ).append( multiselectInput.$element );
9394 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9397 * @extends OO.ui.InputWidget
9400 * @param {Object} [config] Configuration options
9401 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
9403 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
9404 // Configuration initialization
9405 config
= config
|| {};
9407 // Properties (must be done before parent constructor which calls #setDisabled)
9408 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
9410 // Parent constructor
9411 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
9414 this.inputName
= config
.name
;
9418 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
9419 .append( this.checkboxMultiselectWidget
.$element
);
9420 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
9421 this.$input
.detach();
9422 this.setOptions( config
.options
|| [] );
9423 // Have to repeat this from parent, as we need options to be set up for this to make sense
9424 this.setValue( config
.value
);
9426 // setValue when checkboxMultiselectWidget changes
9427 this.checkboxMultiselectWidget
.on( 'change', function () {
9428 this.setValue( this.checkboxMultiselectWidget
.getSelectedItemsData() );
9434 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
9436 /* Static Methods */
9441 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9442 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9443 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9444 .toArray().map( function ( el
) { return el
.value
; } );
9451 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9452 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9453 // Cannot reuse the `<input type=checkbox>` set
9454 delete config
.$input
;
9464 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
9466 return $( '<unused>' );
9472 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
9473 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9474 .toArray().map( function ( el
) { return el
.value
; } );
9475 if ( this.value
!== value
) {
9476 this.setValue( value
);
9484 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
9485 value
= this.cleanUpValue( value
);
9486 this.checkboxMultiselectWidget
.selectItemsByData( value
);
9487 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9492 * Clean up incoming value.
9494 * @param {string[]} value Original value
9495 * @return {string[]} Cleaned up value
9497 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
9500 if ( !Array
.isArray( value
) ) {
9503 for ( i
= 0; i
< value
.length
; i
++ ) {
9505 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
9506 // Remove options that we don't have here
9507 if ( !this.checkboxMultiselectWidget
.getItemFromData( singleValue
) ) {
9510 cleanValue
.push( singleValue
);
9518 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
9519 this.checkboxMultiselectWidget
.setDisabled( state
);
9520 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9525 * Set the options available for this input.
9527 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
9530 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
9533 // Rebuild the checkboxMultiselectWidget menu
9534 this.checkboxMultiselectWidget
9536 .addItems( options
.map( function ( opt
) {
9537 var optValue
, item
, optDisabled
;
9539 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
9540 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
9541 item
= new OO
.ui
.CheckboxMultioptionWidget( {
9543 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
9544 disabled
: optDisabled
9546 // Set the 'name' and 'value' for form submission
9547 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
9548 item
.checkbox
.setValue( optValue
);
9552 // Re-set the value, checking the checkboxes as needed.
9553 // This will also get rid of any stale options that we just removed.
9554 this.setValue( this.getValue() );
9562 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
9563 this.checkboxMultiselectWidget
.focus();
9568 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
9569 * size of the field as well as its presentation. In addition, these widgets can be configured
9570 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
9571 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
9572 * which modifies incoming values rather than validating them.
9573 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9575 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9578 * // Example of a text input widget
9579 * var textInput = new OO.ui.TextInputWidget( {
9580 * value: 'Text input'
9582 * $( 'body' ).append( textInput.$element );
9584 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9587 * @extends OO.ui.InputWidget
9588 * @mixins OO.ui.mixin.IconElement
9589 * @mixins OO.ui.mixin.IndicatorElement
9590 * @mixins OO.ui.mixin.PendingElement
9591 * @mixins OO.ui.mixin.LabelElement
9594 * @param {Object} [config] Configuration options
9595 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
9596 * 'email', 'url' or 'number'.
9597 * @cfg {string} [placeholder] Placeholder text
9598 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
9599 * instruct the browser to focus this widget.
9600 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
9601 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
9602 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
9603 * the value or placeholder text: `'before'` or `'after'`
9604 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
9605 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
9606 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
9607 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
9608 * (the value must contain only numbers); when RegExp, a regular expression that must match the
9609 * value for it to be considered valid; when Function, a function receiving the value as parameter
9610 * that must return true, or promise resolving to true, for it to be considered valid.
9612 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
9613 // Configuration initialization
9614 config
= $.extend( {
9616 labelPosition
: 'after'
9619 if ( config
.multiline
) {
9620 OO
.ui
.warnDeprecation( 'TextInputWidget: config.multiline is deprecated. Use the MultilineTextInputWidget instead. See T130434.' );
9621 return new OO
.ui
.MultilineTextInputWidget( config
);
9624 // Parent constructor
9625 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
9627 // Mixin constructors
9628 OO
.ui
.mixin
.IconElement
.call( this, config
);
9629 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9630 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
9631 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9634 this.type
= this.getSaneType( config
);
9635 this.readOnly
= false;
9636 this.required
= false;
9637 this.validate
= null;
9638 this.styleHeight
= null;
9639 this.scrollWidth
= null;
9641 this.setValidation( config
.validate
);
9642 this.setLabelPosition( config
.labelPosition
);
9646 keypress
: this.onKeyPress
.bind( this ),
9647 blur
: this.onBlur
.bind( this ),
9648 focus
: this.onFocus
.bind( this )
9650 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
9651 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
9652 this.on( 'labelChange', this.updatePosition
.bind( this ) );
9653 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
9657 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
9658 .append( this.$icon
, this.$indicator
);
9659 this.setReadOnly( !!config
.readOnly
);
9660 this.setRequired( !!config
.required
);
9661 if ( config
.placeholder
!== undefined ) {
9662 this.$input
.attr( 'placeholder', config
.placeholder
);
9664 if ( config
.maxLength
!== undefined ) {
9665 this.$input
.attr( 'maxlength', config
.maxLength
);
9667 if ( config
.autofocus
) {
9668 this.$input
.attr( 'autofocus', 'autofocus' );
9670 if ( config
.autocomplete
=== false ) {
9671 this.$input
.attr( 'autocomplete', 'off' );
9672 // Turning off autocompletion also disables "form caching" when the user navigates to a
9673 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
9675 beforeunload: function () {
9676 this.$input
.removeAttr( 'autocomplete' );
9678 pageshow: function () {
9679 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
9680 // whole page... it shouldn't hurt, though.
9681 this.$input
.attr( 'autocomplete', 'off' );
9686 this.isWaitingToBeAttached
= true;
9687 this.installParentChangeDetector();
9693 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
9694 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
9695 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9696 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
9697 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
9699 /* Static Properties */
9701 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
9706 /* Static Methods */
9711 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9712 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9719 * An `enter` event is emitted when the user presses 'enter' inside the text box.
9727 * Handle icon mouse down events.
9730 * @param {jQuery.Event} e Mouse down event
9732 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
9733 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9740 * Handle indicator mouse down events.
9743 * @param {jQuery.Event} e Mouse down event
9745 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
9746 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9753 * Handle key press events.
9756 * @param {jQuery.Event} e Key press event
9757 * @fires enter If enter key is pressed
9759 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
9760 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
9761 this.emit( 'enter', e
);
9766 * Handle blur events.
9769 * @param {jQuery.Event} e Blur event
9771 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
9772 this.setValidityFlag();
9776 * Handle focus events.
9779 * @param {jQuery.Event} e Focus event
9781 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
9782 if ( this.isWaitingToBeAttached
) {
9783 // If we've received focus, then we must be attached to the document, and if
9784 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
9785 this.onElementAttach();
9787 this.setValidityFlag( true );
9791 * Handle element attach events.
9794 * @param {jQuery.Event} e Element attach event
9796 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
9797 this.isWaitingToBeAttached
= false;
9798 // Any previously calculated size is now probably invalid if we reattached elsewhere
9799 this.valCache
= null;
9800 this.positionLabel();
9804 * Handle debounced change events.
9806 * @param {string} value
9809 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
9810 this.setValidityFlag();
9814 * Check if the input is {@link #readOnly read-only}.
9818 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
9819 return this.readOnly
;
9823 * Set the {@link #readOnly read-only} state of the input.
9825 * @param {boolean} state Make input read-only
9828 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
9829 this.readOnly
= !!state
;
9830 this.$input
.prop( 'readOnly', this.readOnly
);
9835 * Check if the input is {@link #required required}.
9839 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
9840 return this.required
;
9844 * Set the {@link #required required} state of the input.
9846 * @param {boolean} state Make input required
9849 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
9850 this.required
= !!state
;
9851 if ( this.required
) {
9853 .prop( 'required', true )
9854 .attr( 'aria-required', 'true' );
9855 if ( this.getIndicator() === null ) {
9856 this.setIndicator( 'required' );
9860 .prop( 'required', false )
9861 .removeAttr( 'aria-required' );
9862 if ( this.getIndicator() === 'required' ) {
9863 this.setIndicator( null );
9870 * Support function for making #onElementAttach work across browsers.
9872 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
9873 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
9875 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
9876 * first time that the element gets attached to the documented.
9878 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
9879 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
9880 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
9883 if ( MutationObserver
) {
9884 // The new way. If only it wasn't so ugly.
9886 if ( this.isElementAttached() ) {
9887 // Widget is attached already, do nothing. This breaks the functionality of this function when
9888 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
9889 // would require observation of the whole document, which would hurt performance of other,
9890 // more important code.
9894 // Find topmost node in the tree
9895 topmostNode
= this.$element
[ 0 ];
9896 while ( topmostNode
.parentNode
) {
9897 topmostNode
= topmostNode
.parentNode
;
9900 // We have no way to detect the $element being attached somewhere without observing the entire
9901 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
9902 // parent node of $element, and instead detect when $element is removed from it (and thus
9903 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
9904 // doesn't get attached, we end up back here and create the parent.
9906 mutationObserver
= new MutationObserver( function ( mutations
) {
9907 var i
, j
, removedNodes
;
9908 for ( i
= 0; i
< mutations
.length
; i
++ ) {
9909 removedNodes
= mutations
[ i
].removedNodes
;
9910 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
9911 if ( removedNodes
[ j
] === topmostNode
) {
9912 setTimeout( onRemove
, 0 );
9919 onRemove = function () {
9920 // If the node was attached somewhere else, report it
9921 if ( widget
.isElementAttached() ) {
9922 widget
.onElementAttach();
9924 mutationObserver
.disconnect();
9925 widget
.installParentChangeDetector();
9928 // Create a fake parent and observe it
9929 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
9930 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
9932 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
9933 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
9934 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
9942 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
9943 if ( this.getSaneType( config
) === 'number' ) {
9944 return $( '<input>' )
9945 .attr( 'step', 'any' )
9946 .attr( 'type', 'number' );
9948 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
9953 * Get sanitized value for 'type' for given config.
9955 * @param {Object} config Configuration options
9956 * @return {string|null}
9959 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
9960 var allowedTypes
= [
9967 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
9971 * Focus the input and select a specified range within the text.
9973 * @param {number} from Select from offset
9974 * @param {number} [to] Select to offset, defaults to from
9977 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
9978 var isBackwards
, start
, end
,
9979 input
= this.$input
[ 0 ];
9983 isBackwards
= to
< from;
9984 start
= isBackwards
? to
: from;
9985 end
= isBackwards
? from : to
;
9990 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
9992 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
9993 // Rather than expensively check if the input is attached every time, just check
9994 // if it was the cause of an error being thrown. If not, rethrow the error.
9995 if ( this.getElementDocument().body
.contains( input
) ) {
10003 * Get an object describing the current selection range in a directional manner
10005 * @return {Object} Object containing 'from' and 'to' offsets
10007 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10008 var input
= this.$input
[ 0 ],
10009 start
= input
.selectionStart
,
10010 end
= input
.selectionEnd
,
10011 isBackwards
= input
.selectionDirection
=== 'backward';
10014 from: isBackwards
? end
: start
,
10015 to
: isBackwards
? start
: end
10020 * Get the length of the text input value.
10022 * This could differ from the length of #getValue if the
10023 * value gets filtered
10025 * @return {number} Input length
10027 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10028 return this.$input
[ 0 ].value
.length
;
10032 * Focus the input and select the entire text.
10036 OO
.ui
.TextInputWidget
.prototype.select = function () {
10037 return this.selectRange( 0, this.getInputLength() );
10041 * Focus the input and move the cursor to the start.
10045 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10046 return this.selectRange( 0 );
10050 * Focus the input and move the cursor to the end.
10054 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10055 return this.selectRange( this.getInputLength() );
10059 * Insert new content into the input.
10061 * @param {string} content Content to be inserted
10064 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10066 range
= this.getRange(),
10067 value
= this.getValue();
10069 start
= Math
.min( range
.from, range
.to
);
10070 end
= Math
.max( range
.from, range
.to
);
10072 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10073 this.selectRange( start
+ content
.length
);
10078 * Insert new content either side of a selection.
10080 * @param {string} pre Content to be inserted before the selection
10081 * @param {string} post Content to be inserted after the selection
10084 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10086 range
= this.getRange(),
10087 offset
= pre
.length
;
10089 start
= Math
.min( range
.from, range
.to
);
10090 end
= Math
.max( range
.from, range
.to
);
10092 this.selectRange( start
).insertContent( pre
);
10093 this.selectRange( offset
+ end
).insertContent( post
);
10095 this.selectRange( offset
+ start
, offset
+ end
);
10100 * Set the validation pattern.
10102 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10103 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10104 * value must contain only numbers).
10106 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10107 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10109 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
10110 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
10111 this.validate
= validate
;
10113 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
10118 * Sets the 'invalid' flag appropriately.
10120 * @param {boolean} [isValid] Optionally override validation result
10122 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
10124 setFlag = function ( valid
) {
10126 widget
.$input
.attr( 'aria-invalid', 'true' );
10128 widget
.$input
.removeAttr( 'aria-invalid' );
10130 widget
.setFlags( { invalid
: !valid
} );
10133 if ( isValid
!== undefined ) {
10134 setFlag( isValid
);
10136 this.getValidity().then( function () {
10145 * Get the validity of current value.
10147 * This method returns a promise that resolves if the value is valid and rejects if
10148 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
10150 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10152 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
10155 function rejectOrResolve( valid
) {
10157 return $.Deferred().resolve().promise();
10159 return $.Deferred().reject().promise();
10163 // Check browser validity and reject if it is invalid
10165 this.$input
[ 0 ].checkValidity
!== undefined &&
10166 this.$input
[ 0 ].checkValidity() === false
10168 return rejectOrResolve( false );
10171 // Run our checks if the browser thinks the field is valid
10172 if ( this.validate
instanceof Function
) {
10173 result
= this.validate( this.getValue() );
10174 if ( result
&& $.isFunction( result
.promise
) ) {
10175 return result
.promise().then( function ( valid
) {
10176 return rejectOrResolve( valid
);
10179 return rejectOrResolve( result
);
10182 return rejectOrResolve( this.getValue().match( this.validate
) );
10187 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10189 * @param {string} labelPosition Label position, 'before' or 'after'
10192 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10193 this.labelPosition
= labelPosition
;
10194 if ( this.label
) {
10195 // If there is no label and we only change the position, #updatePosition is a no-op,
10196 // but it takes really a lot of work to do nothing.
10197 this.updatePosition();
10203 * Update the position of the inline label.
10205 * This method is called by #setLabelPosition, and can also be called on its own if
10206 * something causes the label to be mispositioned.
10210 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10211 var after
= this.labelPosition
=== 'after';
10214 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10215 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10217 this.valCache
= null;
10218 this.scrollWidth
= null;
10219 this.positionLabel();
10225 * Position the label by setting the correct padding on the input.
10230 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
10231 var after
, rtl
, property
, newCss
;
10233 if ( this.isWaitingToBeAttached
) {
10234 // #onElementAttach will be called soon, which calls this method
10239 'padding-right': '',
10243 if ( this.label
) {
10244 this.$element
.append( this.$label
);
10246 this.$label
.detach();
10247 // Clear old values if present
10248 this.$input
.css( newCss
);
10252 after
= this.labelPosition
=== 'after';
10253 rtl
= this.$element
.css( 'direction' ) === 'rtl';
10254 property
= after
=== rtl
? 'padding-left' : 'padding-right';
10256 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
10257 // We have to clear the padding on the other side, in case the element direction changed
10258 this.$input
.css( newCss
);
10266 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10267 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10268 if ( state
.scrollTop
!== undefined ) {
10269 this.$input
.scrollTop( state
.scrollTop
);
10275 * @extends OO.ui.TextInputWidget
10278 * @param {Object} [config] Configuration options
10280 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
10281 config
= $.extend( {
10285 // Parent constructor
10286 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
10289 this.connect( this, {
10294 this.updateSearchIndicator();
10295 this.connect( this, {
10296 disable
: 'onDisable'
10302 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
10310 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
10317 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10318 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10319 // Clear the text field
10320 this.setValue( '' );
10327 * Update the 'clear' indicator displayed on type: 'search' text
10328 * fields, hiding it when the field is already empty or when it's not
10331 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
10332 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
10333 this.setIndicator( null );
10335 this.setIndicator( 'clear' );
10340 * Handle change events.
10344 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
10345 this.updateSearchIndicator();
10349 * Handle disable events.
10351 * @param {boolean} disabled Element is disabled
10354 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
10355 this.updateSearchIndicator();
10361 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
10362 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
10363 this.updateSearchIndicator();
10369 * @extends OO.ui.TextInputWidget
10372 * @param {Object} [config] Configuration options
10373 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
10374 * specifies minimum number of rows to display.
10375 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10376 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
10377 * Use the #maxRows config to specify a maximum number of displayed rows.
10378 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
10379 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
10381 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
10382 config
= $.extend( {
10385 config
.multiline
= false;
10386 // Parent constructor
10387 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
10390 this.multiline
= true;
10391 this.autosize
= !!config
.autosize
;
10392 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
10393 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
10395 // Clone for resizing
10396 if ( this.autosize
) {
10397 this.$clone
= this.$input
10399 .insertAfter( this.$input
)
10400 .attr( 'aria-hidden', 'true' )
10401 .addClass( 'oo-ui-element-hidden' );
10405 this.connect( this, {
10410 if ( this.multiline
&& config
.rows
) {
10411 this.$input
.attr( 'rows', config
.rows
);
10413 if ( this.autosize
) {
10414 this.isWaitingToBeAttached
= true;
10415 this.installParentChangeDetector();
10421 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
10423 /* Static Methods */
10428 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10429 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10430 state
.scrollTop
= config
.$input
.scrollTop();
10439 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
10440 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
10445 * Handle change events.
10449 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
10456 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
10457 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
10462 * Override TextInputWidget so it doesn't emit the 'enter' event.
10465 * @param {jQuery.Event} e Key press event
10467 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function () {
10472 * Automatically adjust the size of the text input.
10474 * This only affects multiline inputs that are {@link #autosize autosized}.
10479 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
10480 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
10481 idealHeight
, newHeight
, scrollWidth
, property
;
10483 if ( this.$input
.val() !== this.valCache
) {
10484 if ( this.autosize
) {
10486 .val( this.$input
.val() )
10487 .attr( 'rows', this.minRows
)
10488 // Set inline height property to 0 to measure scroll height
10489 .css( 'height', 0 );
10491 this.$clone
.removeClass( 'oo-ui-element-hidden' );
10493 this.valCache
= this.$input
.val();
10495 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
10497 // Remove inline height property to measure natural heights
10498 this.$clone
.css( 'height', '' );
10499 innerHeight
= this.$clone
.innerHeight();
10500 outerHeight
= this.$clone
.outerHeight();
10502 // Measure max rows height
10504 .attr( 'rows', this.maxRows
)
10505 .css( 'height', 'auto' )
10507 maxInnerHeight
= this.$clone
.innerHeight();
10509 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
10510 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
10511 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
10512 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
10514 this.$clone
.addClass( 'oo-ui-element-hidden' );
10516 // Only apply inline height when expansion beyond natural height is needed
10517 // Use the difference between the inner and outer height as a buffer
10518 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
10519 if ( newHeight
!== this.styleHeight
) {
10520 this.$input
.css( 'height', newHeight
);
10521 this.styleHeight
= newHeight
;
10522 this.emit( 'resize' );
10525 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
10526 if ( scrollWidth
!== this.scrollWidth
) {
10527 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
10529 this.$label
.css( { right
: '', left
: '' } );
10530 this.$indicator
.css( { right
: '', left
: '' } );
10532 if ( scrollWidth
) {
10533 this.$indicator
.css( property
, scrollWidth
);
10534 if ( this.labelPosition
=== 'after' ) {
10535 this.$label
.css( property
, scrollWidth
);
10539 this.scrollWidth
= scrollWidth
;
10540 this.positionLabel();
10550 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
10551 return $( '<textarea>' );
10555 * Check if the input supports multiple lines.
10557 * @return {boolean}
10559 OO
.ui
.MultilineTextInputWidget
.prototype.isMultiline = function () {
10560 return !!this.multiline
;
10564 * Check if the input automatically adjusts its size.
10566 * @return {boolean}
10568 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
10569 return !!this.autosize
;
10573 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
10574 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
10575 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
10577 * - by typing a value in the text input field. If the value exactly matches the value of a menu
10578 * option, that option will appear to be selected.
10579 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
10582 * After the user chooses an option, its `data` will be used as a new value for the widget.
10583 * A `label` also can be specified for each option: if given, it will be shown instead of the
10584 * `data` in the dropdown menu.
10586 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10588 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
10591 * // Example: A ComboBoxInputWidget.
10592 * var comboBox = new OO.ui.ComboBoxInputWidget( {
10593 * value: 'Option 1',
10595 * { data: 'Option 1' },
10596 * { data: 'Option 2' },
10597 * { data: 'Option 3' }
10600 * $( 'body' ).append( comboBox.$element );
10603 * // Example: A ComboBoxInputWidget with additional option labels.
10604 * var comboBox = new OO.ui.ComboBoxInputWidget( {
10605 * value: 'Option 1',
10608 * data: 'Option 1',
10609 * label: 'Option One'
10612 * data: 'Option 2',
10613 * label: 'Option Two'
10616 * data: 'Option 3',
10617 * label: 'Option Three'
10621 * $( 'body' ).append( comboBox.$element );
10623 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10626 * @extends OO.ui.TextInputWidget
10629 * @param {Object} [config] Configuration options
10630 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10631 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
10632 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
10633 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
10634 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
10635 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
10637 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
10638 // Configuration initialization
10639 config
= $.extend( {
10640 autocomplete
: false
10643 // ComboBoxInputWidget shouldn't support `multiline`
10644 config
.multiline
= false;
10646 // See InputWidget#reusePreInfuseDOM about `config.$input`
10647 if ( config
.$input
) {
10648 config
.$input
.removeAttr( 'list' );
10651 // Parent constructor
10652 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
10655 this.$overlay
= config
.$overlay
|| this.$element
;
10656 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
10657 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
10659 disabled
: this.disabled
10661 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
10665 $floatableContainer
: this.$element
,
10666 disabled
: this.isDisabled()
10672 this.connect( this, {
10673 change
: 'onInputChange',
10674 enter
: 'onInputEnter'
10676 this.dropdownButton
.connect( this, {
10677 click
: 'onDropdownButtonClick'
10679 this.menu
.connect( this, {
10680 choose
: 'onMenuChoose',
10681 add
: 'onMenuItemsChange',
10682 remove
: 'onMenuItemsChange'
10686 this.$input
.attr( {
10688 'aria-owns': this.menu
.getElementId(),
10689 'aria-autocomplete': 'list'
10691 // Do not override options set via config.menu.items
10692 if ( config
.options
!== undefined ) {
10693 this.setOptions( config
.options
);
10695 this.$field
= $( '<div>' )
10696 .addClass( 'oo-ui-comboBoxInputWidget-field' )
10697 .append( this.$input
, this.dropdownButton
.$element
);
10699 .addClass( 'oo-ui-comboBoxInputWidget' )
10700 .append( this.$field
);
10701 this.$overlay
.append( this.menu
.$element
);
10702 this.onMenuItemsChange();
10707 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
10712 * Get the combobox's menu.
10714 * @return {OO.ui.MenuSelectWidget} Menu widget
10716 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
10721 * Get the combobox's text input widget.
10723 * @return {OO.ui.TextInputWidget} Text input widget
10725 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
10730 * Handle input change events.
10733 * @param {string} value New value
10735 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
10736 var match
= this.menu
.getItemFromData( value
);
10738 this.menu
.selectItem( match
);
10739 if ( this.menu
.findHighlightedItem() ) {
10740 this.menu
.highlightItem( match
);
10743 if ( !this.isDisabled() ) {
10744 this.menu
.toggle( true );
10749 * Handle input enter events.
10753 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
10754 if ( !this.isDisabled() ) {
10755 this.menu
.toggle( false );
10760 * Handle button click events.
10764 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
10765 this.menu
.toggle();
10770 * Handle menu choose events.
10773 * @param {OO.ui.OptionWidget} item Chosen item
10775 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
10776 this.setValue( item
.getData() );
10780 * Handle menu item change events.
10784 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
10785 var match
= this.menu
.getItemFromData( this.getValue() );
10786 this.menu
.selectItem( match
);
10787 if ( this.menu
.findHighlightedItem() ) {
10788 this.menu
.highlightItem( match
);
10790 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
10796 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
10798 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
10800 if ( this.dropdownButton
) {
10801 this.dropdownButton
.setDisabled( this.isDisabled() );
10804 this.menu
.setDisabled( this.isDisabled() );
10811 * Set the options available for this input.
10813 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10816 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
10819 .addItems( options
.map( function ( opt
) {
10820 return new OO
.ui
.MenuOptionWidget( {
10822 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
10830 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
10831 * which is a widget that is specified by reference before any optional configuration settings.
10833 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
10835 * - **left**: The label is placed before the field-widget and aligned with the left margin.
10836 * A left-alignment is used for forms with many fields.
10837 * - **right**: The label is placed before the field-widget and aligned to the right margin.
10838 * A right-alignment is used for long but familiar forms which users tab through,
10839 * verifying the current field with a quick glance at the label.
10840 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
10841 * that users fill out from top to bottom.
10842 * - **inline**: The label is placed after the field-widget and aligned to the left.
10843 * An inline-alignment is best used with checkboxes or radio buttons.
10845 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
10846 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
10848 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
10851 * @extends OO.ui.Layout
10852 * @mixins OO.ui.mixin.LabelElement
10853 * @mixins OO.ui.mixin.TitledElement
10856 * @param {OO.ui.Widget} fieldWidget Field widget
10857 * @param {Object} [config] Configuration options
10858 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
10859 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
10860 * The array may contain strings or OO.ui.HtmlSnippet instances.
10861 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
10862 * The array may contain strings or OO.ui.HtmlSnippet instances.
10863 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
10864 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
10865 * For important messages, you are advised to use `notices`, as they are always shown.
10866 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
10867 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
10869 * @throws {Error} An error is thrown if no widget is specified
10871 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
10872 // Allow passing positional parameters inside the config object
10873 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
10874 config
= fieldWidget
;
10875 fieldWidget
= config
.fieldWidget
;
10878 // Make sure we have required constructor arguments
10879 if ( fieldWidget
=== undefined ) {
10880 throw new Error( 'Widget not found' );
10883 // Configuration initialization
10884 config
= $.extend( { align
: 'left' }, config
);
10886 // Parent constructor
10887 OO
.ui
.FieldLayout
.parent
.call( this, config
);
10889 // Mixin constructors
10890 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
10891 $label
: $( '<label>' )
10893 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
10896 this.fieldWidget
= fieldWidget
;
10899 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
10900 this.$messages
= $( '<ul>' );
10901 this.$header
= $( '<span>' );
10902 this.$body
= $( '<div>' );
10904 if ( config
.help
) {
10905 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
10906 $overlay
: config
.$overlay
,
10910 classes
: [ 'oo-ui-fieldLayout-help' ],
10914 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
10915 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
10917 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
10919 this.$help
= this.popupButtonWidget
.$element
;
10921 this.$help
= $( [] );
10925 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
10928 if ( config
.help
) {
10929 // Set the 'aria-describedby' attribute on the fieldWidget
10930 // Preference given to an input or a button
10932 this.fieldWidget
.$input
||
10933 this.fieldWidget
.$button
||
10934 this.fieldWidget
.$element
10936 'aria-describedby',
10937 this.popupButtonWidget
.getPopup().getBodyId()
10940 if ( this.fieldWidget
.getInputId() ) {
10941 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
10943 this.$label
.on( 'click', function () {
10944 this.fieldWidget
.simulateLabelClick();
10949 .addClass( 'oo-ui-fieldLayout' )
10950 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
10951 .append( this.$body
);
10952 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
10953 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
10954 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
10956 .addClass( 'oo-ui-fieldLayout-field' )
10957 .append( this.fieldWidget
.$element
);
10959 this.setErrors( config
.errors
|| [] );
10960 this.setNotices( config
.notices
|| [] );
10961 this.setAlignment( config
.align
);
10962 // Call this again to take into account the widget's accessKey
10963 this.updateTitle();
10968 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
10969 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
10970 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
10975 * Handle field disable events.
10978 * @param {boolean} value Field is disabled
10980 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
10981 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
10985 * Get the widget contained by the field.
10987 * @return {OO.ui.Widget} Field widget
10989 OO
.ui
.FieldLayout
.prototype.getField = function () {
10990 return this.fieldWidget
;
10994 * Return `true` if the given field widget can be used with `'inline'` alignment (see
10995 * #setAlignment). Return `false` if it can't or if this can't be determined.
10997 * @return {boolean}
10999 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11000 // This is very simplistic, but should be good enough.
11001 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11006 * @param {string} kind 'error' or 'notice'
11007 * @param {string|OO.ui.HtmlSnippet} text
11010 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11011 var $listItem
, $icon
, message
;
11012 $listItem
= $( '<li>' );
11013 if ( kind
=== 'error' ) {
11014 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11015 $listItem
.attr( 'role', 'alert' );
11016 } else if ( kind
=== 'notice' ) {
11017 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
11021 message
= new OO
.ui
.LabelWidget( { label
: text
} );
11023 .append( $icon
, message
.$element
)
11024 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
11029 * Set the field alignment mode.
11032 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
11035 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
11036 if ( value
!== this.align
) {
11037 // Default to 'left'
11038 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
11042 if ( value
=== 'inline' && !this.isFieldInline() ) {
11045 // Reorder elements
11046 if ( value
=== 'top' ) {
11047 this.$header
.append( this.$label
, this.$help
);
11048 this.$body
.append( this.$header
, this.$field
);
11049 } else if ( value
=== 'inline' ) {
11050 this.$header
.append( this.$label
, this.$help
);
11051 this.$body
.append( this.$field
, this.$header
);
11053 this.$header
.append( this.$label
);
11054 this.$body
.append( this.$header
, this.$help
, this.$field
);
11056 // Set classes. The following classes can be used here:
11057 // * oo-ui-fieldLayout-align-left
11058 // * oo-ui-fieldLayout-align-right
11059 // * oo-ui-fieldLayout-align-top
11060 // * oo-ui-fieldLayout-align-inline
11061 if ( this.align
) {
11062 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
11064 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
11065 this.align
= value
;
11072 * Set the list of error messages.
11074 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11075 * The array may contain strings or OO.ui.HtmlSnippet instances.
11078 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
11079 this.errors
= errors
.slice();
11080 this.updateMessages();
11085 * Set the list of notice messages.
11087 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11088 * The array may contain strings or OO.ui.HtmlSnippet instances.
11091 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
11092 this.notices
= notices
.slice();
11093 this.updateMessages();
11098 * Update the rendering of error and notice messages.
11102 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
11104 this.$messages
.empty();
11106 if ( this.errors
.length
|| this.notices
.length
) {
11107 this.$body
.after( this.$messages
);
11109 this.$messages
.remove();
11113 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
11114 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
11116 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
11117 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
11122 * Include information about the widget's accessKey in our title. TitledElement calls this method.
11123 * (This is a bit of a hack.)
11126 * @param {string} title Tooltip label for 'title' attribute
11129 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
11130 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
11131 return this.fieldWidget
.formatTitleWithAccessKey( title
);
11137 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
11138 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
11139 * is required and is specified before any optional configuration settings.
11141 * Labels can be aligned in one of four ways:
11143 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11144 * A left-alignment is used for forms with many fields.
11145 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11146 * A right-alignment is used for long but familiar forms which users tab through,
11147 * verifying the current field with a quick glance at the label.
11148 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11149 * that users fill out from top to bottom.
11150 * - **inline**: The label is placed after the field-widget and aligned to the left.
11151 * An inline-alignment is best used with checkboxes or radio buttons.
11153 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
11154 * text is specified.
11157 * // Example of an ActionFieldLayout
11158 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
11159 * new OO.ui.TextInputWidget( {
11160 * placeholder: 'Field widget'
11162 * new OO.ui.ButtonWidget( {
11166 * label: 'An ActionFieldLayout. This label is aligned top',
11168 * help: 'This is help text'
11172 * $( 'body' ).append( actionFieldLayout.$element );
11175 * @extends OO.ui.FieldLayout
11178 * @param {OO.ui.Widget} fieldWidget Field widget
11179 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
11180 * @param {Object} config
11182 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
11183 // Allow passing positional parameters inside the config object
11184 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11185 config
= fieldWidget
;
11186 fieldWidget
= config
.fieldWidget
;
11187 buttonWidget
= config
.buttonWidget
;
11190 // Parent constructor
11191 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
11194 this.buttonWidget
= buttonWidget
;
11195 this.$button
= $( '<span>' );
11196 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11200 .addClass( 'oo-ui-actionFieldLayout' );
11202 .addClass( 'oo-ui-actionFieldLayout-button' )
11203 .append( this.buttonWidget
.$element
);
11205 .addClass( 'oo-ui-actionFieldLayout-input' )
11206 .append( this.fieldWidget
.$element
);
11208 .append( this.$input
, this.$button
);
11213 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
11216 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
11217 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
11218 * configured with a label as well. For more information and examples,
11219 * please see the [OOjs UI documentation on MediaWiki][1].
11222 * // Example of a fieldset layout
11223 * var input1 = new OO.ui.TextInputWidget( {
11224 * placeholder: 'A text input field'
11227 * var input2 = new OO.ui.TextInputWidget( {
11228 * placeholder: 'A text input field'
11231 * var fieldset = new OO.ui.FieldsetLayout( {
11232 * label: 'Example of a fieldset layout'
11235 * fieldset.addItems( [
11236 * new OO.ui.FieldLayout( input1, {
11237 * label: 'Field One'
11239 * new OO.ui.FieldLayout( input2, {
11240 * label: 'Field Two'
11243 * $( 'body' ).append( fieldset.$element );
11245 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
11248 * @extends OO.ui.Layout
11249 * @mixins OO.ui.mixin.IconElement
11250 * @mixins OO.ui.mixin.LabelElement
11251 * @mixins OO.ui.mixin.GroupElement
11254 * @param {Object} [config] Configuration options
11255 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
11256 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
11257 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
11258 * For important messages, you are advised to use `notices`, as they are always shown.
11259 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
11260 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
11262 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
11263 // Configuration initialization
11264 config
= config
|| {};
11266 // Parent constructor
11267 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
11269 // Mixin constructors
11270 OO
.ui
.mixin
.IconElement
.call( this, config
);
11271 OO
.ui
.mixin
.LabelElement
.call( this, config
);
11272 OO
.ui
.mixin
.GroupElement
.call( this, config
);
11275 this.$header
= $( '<legend>' );
11276 if ( config
.help
) {
11277 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
11278 $overlay
: config
.$overlay
,
11282 classes
: [ 'oo-ui-fieldsetLayout-help' ],
11286 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
11287 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
11289 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
11291 this.$help
= this.popupButtonWidget
.$element
;
11293 this.$help
= $( [] );
11298 .addClass( 'oo-ui-fieldsetLayout-header' )
11299 .append( this.$icon
, this.$label
, this.$help
);
11300 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
11302 .addClass( 'oo-ui-fieldsetLayout' )
11303 .prepend( this.$header
, this.$group
);
11304 if ( Array
.isArray( config
.items
) ) {
11305 this.addItems( config
.items
);
11311 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
11312 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
11313 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
11314 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
11316 /* Static Properties */
11322 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
11325 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
11326 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
11327 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
11328 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
11330 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
11331 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
11332 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
11333 * some fancier controls. Some controls have both regular and InputWidget variants, for example
11334 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
11335 * often have simplified APIs to match the capabilities of HTML forms.
11336 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
11338 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
11339 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11342 * // Example of a form layout that wraps a fieldset layout
11343 * var input1 = new OO.ui.TextInputWidget( {
11344 * placeholder: 'Username'
11346 * var input2 = new OO.ui.TextInputWidget( {
11347 * placeholder: 'Password',
11350 * var submit = new OO.ui.ButtonInputWidget( {
11354 * var fieldset = new OO.ui.FieldsetLayout( {
11355 * label: 'A form layout'
11357 * fieldset.addItems( [
11358 * new OO.ui.FieldLayout( input1, {
11359 * label: 'Username',
11362 * new OO.ui.FieldLayout( input2, {
11363 * label: 'Password',
11366 * new OO.ui.FieldLayout( submit )
11368 * var form = new OO.ui.FormLayout( {
11369 * items: [ fieldset ],
11370 * action: '/api/formhandler',
11373 * $( 'body' ).append( form.$element );
11376 * @extends OO.ui.Layout
11377 * @mixins OO.ui.mixin.GroupElement
11380 * @param {Object} [config] Configuration options
11381 * @cfg {string} [method] HTML form `method` attribute
11382 * @cfg {string} [action] HTML form `action` attribute
11383 * @cfg {string} [enctype] HTML form `enctype` attribute
11384 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
11386 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
11389 // Configuration initialization
11390 config
= config
|| {};
11392 // Parent constructor
11393 OO
.ui
.FormLayout
.parent
.call( this, config
);
11395 // Mixin constructors
11396 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11399 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
11401 // Make sure the action is safe
11402 action
= config
.action
;
11403 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
11404 action
= './' + action
;
11409 .addClass( 'oo-ui-formLayout' )
11411 method
: config
.method
,
11413 enctype
: config
.enctype
11415 if ( Array
.isArray( config
.items
) ) {
11416 this.addItems( config
.items
);
11422 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
11423 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
11428 * A 'submit' event is emitted when the form is submitted.
11433 /* Static Properties */
11439 OO
.ui
.FormLayout
.static.tagName
= 'form';
11444 * Handle form submit events.
11447 * @param {jQuery.Event} e Submit event
11450 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
11451 if ( this.emit( 'submit' ) ) {
11457 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
11458 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
11461 * // Example of a panel layout
11462 * var panel = new OO.ui.PanelLayout( {
11466 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
11468 * $( 'body' ).append( panel.$element );
11471 * @extends OO.ui.Layout
11474 * @param {Object} [config] Configuration options
11475 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
11476 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
11477 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
11478 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
11480 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
11481 // Configuration initialization
11482 config
= $.extend( {
11489 // Parent constructor
11490 OO
.ui
.PanelLayout
.parent
.call( this, config
);
11493 this.$element
.addClass( 'oo-ui-panelLayout' );
11494 if ( config
.scrollable
) {
11495 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
11497 if ( config
.padded
) {
11498 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
11500 if ( config
.expanded
) {
11501 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
11503 if ( config
.framed
) {
11504 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
11510 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
11515 * Focus the panel layout
11517 * The default implementation just focuses the first focusable element in the panel
11519 OO
.ui
.PanelLayout
.prototype.focus = function () {
11520 OO
.ui
.findFocusable( this.$element
).focus();
11524 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
11525 * items), with small margins between them. Convenient when you need to put a number of block-level
11526 * widgets on a single line next to each other.
11528 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
11531 * // HorizontalLayout with a text input and a label
11532 * var layout = new OO.ui.HorizontalLayout( {
11534 * new OO.ui.LabelWidget( { label: 'Label' } ),
11535 * new OO.ui.TextInputWidget( { value: 'Text' } )
11538 * $( 'body' ).append( layout.$element );
11541 * @extends OO.ui.Layout
11542 * @mixins OO.ui.mixin.GroupElement
11545 * @param {Object} [config] Configuration options
11546 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
11548 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
11549 // Configuration initialization
11550 config
= config
|| {};
11552 // Parent constructor
11553 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
11555 // Mixin constructors
11556 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11559 this.$element
.addClass( 'oo-ui-horizontalLayout' );
11560 if ( Array
.isArray( config
.items
) ) {
11561 this.addItems( config
.items
);
11567 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
11568 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
11572 //# sourceMappingURL=oojs-ui-core.js.map