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-02-14T22:47:20Z
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 from :focusable in jQueryUI v1.11.4 - 2015-04-14
78 * @param {jQuery} $element Element to test
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
.filters
.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, 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
239 * @param {number} wait
240 * @param {boolean} immediate
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
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
286 * @param {number} wait
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
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 accept button of a confirmation dialog
366 'ooui-dialog-message-accept': 'OK',
367 // Default label for the reject button of a confirmation dialog
368 'ooui-dialog-message-reject': 'Cancel',
369 // Title for process dialog error description
370 'ooui-dialog-process-error': 'Something went wrong',
371 // Label for process dialog dismiss error button, visible when describing errors
372 'ooui-dialog-process-dismiss': 'Dismiss',
373 // Label for process dialog retry action button, visible when describing only recoverable errors
374 'ooui-dialog-process-retry': 'Try again',
375 // Label for process dialog retry action button, visible when describing only warnings
376 'ooui-dialog-process-continue': 'Continue',
377 // Label for the file selection widget's select file button
378 'ooui-selectfile-button-select': 'Select a file',
379 // Label for the file selection widget if file selection is not supported
380 'ooui-selectfile-not-supported': 'File selection is not supported',
381 // Label for the file selection widget when no file is currently selected
382 'ooui-selectfile-placeholder': 'No file is selected',
383 // Label for the file selection widget's drop target
384 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
388 * Get a localized message.
390 * After the message key, message parameters may optionally be passed. In the default implementation,
391 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
392 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
393 * they support unnamed, ordered message parameters.
395 * In environments that provide a localization system, this function should be overridden to
396 * return the message translated in the user's language. The default implementation always returns
397 * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
401 * var i, iLen, button,
402 * messagePath = 'oojs-ui/dist/i18n/',
403 * languages = [ $.i18n().locale, 'ur', 'en' ],
406 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
407 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
410 * $.i18n().load( languageMap ).done( function() {
411 * // Replace the built-in `msg` only once we've loaded the internationalization.
412 * // OOjs UI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
413 * // you put off creating any widgets until this promise is complete, no English
414 * // will be displayed.
415 * OO.ui.msg = $.i18n;
417 * // A button displaying "OK" in the default locale
418 * button = new OO.ui.ButtonWidget( {
419 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
422 * $( 'body' ).append( button.$element );
424 * // A button displaying "OK" in Urdu
425 * $.i18n().locale = 'ur';
426 * button = new OO.ui.ButtonWidget( {
427 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
430 * $( 'body' ).append( button.$element );
433 * @param {string} key Message key
434 * @param {...Mixed} [params] Message parameters
435 * @return {string} Translated message with parameters substituted
437 OO
.ui
.msg = function ( key
) {
438 var message
= messages
[ key
],
439 params
= Array
.prototype.slice
.call( arguments
, 1 );
440 if ( typeof message
=== 'string' ) {
441 // Perform $1 substitution
442 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
443 var i
= parseInt( n
, 10 );
444 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
447 // Return placeholder if message not found
448 message
= '[' + key
+ ']';
455 * Package a message and arguments for deferred resolution.
457 * Use this when you are statically specifying a message and the message may not yet be present.
459 * @param {string} key Message key
460 * @param {...Mixed} [params] Message parameters
461 * @return {Function} Function that returns the resolved message when executed
463 OO
.ui
.deferMsg = function () {
464 var args
= arguments
;
466 return OO
.ui
.msg
.apply( OO
.ui
, args
);
473 * If the message is a function it will be executed, otherwise it will pass through directly.
475 * @param {Function|string} msg Deferred message, or message text
476 * @return {string} Resolved message
478 OO
.ui
.resolveMsg = function ( msg
) {
479 if ( $.isFunction( msg
) ) {
486 * @param {string} url
489 OO
.ui
.isSafeUrl = function ( url
) {
490 // Keep this function in sync with php/Tag.php
491 var i
, protocolWhitelist
;
493 function stringStartsWith( haystack
, needle
) {
494 return haystack
.substr( 0, needle
.length
) === needle
;
497 protocolWhitelist
= [
498 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
499 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
500 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
507 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
508 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
513 // This matches '//' too
514 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
517 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
525 * Check if the user has a 'mobile' device.
527 * For our purposes this means the user is primarily using an
528 * on-screen keyboard, touch input instead of a mouse and may
529 * have a physically small display.
531 * It is left up to implementors to decide how to compute this
532 * so the default implementation always returns false.
534 * @return {boolean} Use is on a mobile device
536 OO
.ui
.isMobile = function () {
545 * Namespace for OOjs UI mixins.
547 * Mixins are named according to the type of object they are intended to
548 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
549 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
550 * is intended to be mixed in to an instance of OO.ui.Widget.
558 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
559 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
560 * connected to them and can't be interacted with.
566 * @param {Object} [config] Configuration options
567 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
568 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
570 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
571 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
572 * @cfg {string} [text] Text to insert
573 * @cfg {Array} [content] An array of content elements to append (after #text).
574 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
575 * Instances of OO.ui.Element will have their $element appended.
576 * @cfg {jQuery} [$content] Content elements to append (after #text).
577 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
578 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
579 * Data can also be specified with the #setData method.
581 OO
.ui
.Element
= function OoUiElement( config
) {
582 // Configuration initialization
583 config
= config
|| {};
588 this.data
= config
.data
;
589 this.$element
= config
.$element
||
590 $( document
.createElement( this.getTagName() ) );
591 this.elementGroup
= null;
594 if ( Array
.isArray( config
.classes
) ) {
595 this.$element
.addClass( config
.classes
.join( ' ' ) );
598 this.$element
.attr( 'id', config
.id
);
601 this.$element
.text( config
.text
);
603 if ( config
.content
) {
604 // The `content` property treats plain strings as text; use an
605 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
606 // appropriate $element appended.
607 this.$element
.append( config
.content
.map( function ( v
) {
608 if ( typeof v
=== 'string' ) {
609 // Escape string so it is properly represented in HTML.
610 return document
.createTextNode( v
);
611 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
614 } else if ( v
instanceof OO
.ui
.Element
) {
620 if ( config
.$content
) {
621 // The `$content` property treats plain strings as HTML.
622 this.$element
.append( config
.$content
);
628 OO
.initClass( OO
.ui
.Element
);
630 /* Static Properties */
633 * The name of the HTML tag used by the element.
635 * The static value may be ignored if the #getTagName method is overridden.
641 OO
.ui
.Element
.static.tagName
= 'div';
646 * Reconstitute a JavaScript object corresponding to a widget created
647 * by the PHP implementation.
649 * @param {string|HTMLElement|jQuery} idOrNode
650 * A DOM id (if a string) or node for the widget to infuse.
651 * @return {OO.ui.Element}
652 * The `OO.ui.Element` corresponding to this (infusable) document node.
653 * For `Tag` objects emitted on the HTML side (used occasionally for content)
654 * the value returned is a newly-created Element wrapping around the existing
657 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
658 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, false );
659 // Verify that the type matches up.
660 // FIXME: uncomment after T89721 is fixed (see T90929)
662 if ( !( obj instanceof this['class'] ) ) {
663 throw new Error( 'Infusion type mismatch!' );
670 * Implementation helper for `infuse`; skips the type check and has an
671 * extra property so that only the top-level invocation touches the DOM.
674 * @param {string|HTMLElement|jQuery} idOrNode
675 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
676 * when the top-level widget of this infusion is inserted into DOM,
677 * replacing the original node; or false for top-level invocation.
678 * @return {OO.ui.Element}
680 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, domPromise
) {
681 // look for a cached result of a previous infusion.
682 var id
, $elem
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
683 if ( typeof idOrNode
=== 'string' ) {
685 $elem
= $( document
.getElementById( id
) );
687 $elem
= $( idOrNode
);
688 id
= $elem
.attr( 'id' );
690 if ( !$elem
.length
) {
691 throw new Error( 'Widget not found: ' + id
);
693 if ( $elem
[ 0 ].oouiInfused
) {
694 $elem
= $elem
[ 0 ].oouiInfused
;
696 data
= $elem
.data( 'ooui-infused' );
699 if ( data
=== true ) {
700 throw new Error( 'Circular dependency! ' + id
);
703 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
704 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
705 // restore dynamic state after the new element is re-inserted into DOM under infused parent
706 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
707 infusedChildren
= $elem
.data( 'ooui-infused-children' );
708 if ( infusedChildren
&& infusedChildren
.length
) {
709 infusedChildren
.forEach( function ( data
) {
710 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
711 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
717 data
= $elem
.attr( 'data-ooui' );
719 throw new Error( 'No infusion data found: ' + id
);
722 data
= $.parseJSON( data
);
726 if ( !( data
&& data
._
) ) {
727 throw new Error( 'No valid infusion data found: ' + id
);
729 if ( data
._
=== 'Tag' ) {
730 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
731 return new OO
.ui
.Element( { $element
: $elem
} );
733 parts
= data
._
.split( '.' );
734 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
735 if ( cls
=== undefined ) {
736 // The PHP output might be old and not including the "OO.ui" prefix
737 // TODO: Remove this back-compat after next major release
738 cls
= OO
.getProp
.apply( OO
, [ OO
.ui
].concat( parts
) );
739 if ( cls
=== undefined ) {
740 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
744 // Verify that we're creating an OO.ui.Element instance
747 while ( parent
!== undefined ) {
748 if ( parent
=== OO
.ui
.Element
) {
753 parent
= parent
.parent
;
756 if ( parent
!== OO
.ui
.Element
) {
757 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
760 if ( domPromise
=== false ) {
762 domPromise
= top
.promise();
764 $elem
.data( 'ooui-infused', true ); // prevent loops
765 data
.id
= id
; // implicit
766 infusedChildren
= [];
767 data
= OO
.copy( data
, null, function deserialize( value
) {
769 if ( OO
.isPlainObject( value
) ) {
771 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, domPromise
);
772 infusedChildren
.push( infused
);
773 // Flatten the structure
774 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
775 infused
.$element
.removeData( 'ooui-infused-children' );
778 if ( value
.html
!== undefined ) {
779 return new OO
.ui
.HtmlSnippet( value
.html
);
783 // allow widgets to reuse parts of the DOM
784 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
785 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
786 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
788 // eslint-disable-next-line new-cap
789 obj
= new cls( data
);
790 // now replace old DOM with this new DOM.
792 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
793 // so only mutate the DOM if we need to.
794 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
795 $elem
.replaceWith( obj
.$element
);
796 // This element is now gone from the DOM, but if anyone is holding a reference to it,
797 // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
798 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
799 $elem
[ 0 ].oouiInfused
= obj
.$element
;
803 obj
.$element
.data( 'ooui-infused', obj
);
804 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
805 // set the 'data-ooui' attribute so we can identify infused widgets
806 obj
.$element
.attr( 'data-ooui', '' );
807 // restore dynamic state after the new element is inserted into DOM
808 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
813 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
815 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
816 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
817 * constructor, which will be given the enhanced config.
820 * @param {HTMLElement} node
821 * @param {Object} config
824 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
829 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
830 * (and its children) that represent an Element of the same class and the given configuration,
831 * generated by the PHP implementation.
833 * This method is called just before `node` is detached from the DOM. The return value of this
834 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
835 * is inserted into DOM to replace `node`.
838 * @param {HTMLElement} node
839 * @param {Object} config
842 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
847 * Get a jQuery function within a specific document.
850 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
851 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
853 * @return {Function} Bound jQuery function
855 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
856 function wrapper( selector
) {
857 return $( selector
, wrapper
.context
);
860 wrapper
.context
= this.getDocument( context
);
863 wrapper
.$iframe
= $iframe
;
870 * Get the document of an element.
873 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
874 * @return {HTMLDocument|null} Document object
876 OO
.ui
.Element
.static.getDocument = function ( obj
) {
877 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
878 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
879 // Empty jQuery selections might have a context
886 ( obj
.nodeType
=== 9 && obj
) ||
891 * Get the window of an element or document.
894 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
895 * @return {Window} Window object
897 OO
.ui
.Element
.static.getWindow = function ( obj
) {
898 var doc
= this.getDocument( obj
);
899 return doc
.defaultView
;
903 * Get the direction of an element or document.
906 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
907 * @return {string} Text direction, either 'ltr' or 'rtl'
909 OO
.ui
.Element
.static.getDir = function ( obj
) {
912 if ( obj
instanceof jQuery
) {
915 isDoc
= obj
.nodeType
=== 9;
916 isWin
= obj
.document
!== undefined;
917 if ( isDoc
|| isWin
) {
923 return $( obj
).css( 'direction' );
927 * Get the offset between two frames.
929 * TODO: Make this function not use recursion.
932 * @param {Window} from Window of the child frame
933 * @param {Window} [to=window] Window of the parent frame
934 * @param {Object} [offset] Offset to start with, used internally
935 * @return {Object} Offset object, containing left and top properties
937 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
938 var i
, len
, frames
, frame
, rect
;
944 offset
= { top
: 0, left
: 0 };
946 if ( from.parent
=== from ) {
950 // Get iframe element
951 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
952 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
953 if ( frames
[ i
].contentWindow
=== from ) {
959 // Recursively accumulate offset values
961 rect
= frame
.getBoundingClientRect();
962 offset
.left
+= rect
.left
;
963 offset
.top
+= rect
.top
;
965 this.getFrameOffset( from.parent
, offset
);
972 * Get the offset between two elements.
974 * The two elements may be in a different frame, but in that case the frame $element is in must
975 * be contained in the frame $anchor is in.
978 * @param {jQuery} $element Element whose position to get
979 * @param {jQuery} $anchor Element to get $element's position relative to
980 * @return {Object} Translated position coordinates, containing top and left properties
982 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
983 var iframe
, iframePos
,
984 pos
= $element
.offset(),
985 anchorPos
= $anchor
.offset(),
986 elementDocument
= this.getDocument( $element
),
987 anchorDocument
= this.getDocument( $anchor
);
989 // If $element isn't in the same document as $anchor, traverse up
990 while ( elementDocument
!== anchorDocument
) {
991 iframe
= elementDocument
.defaultView
.frameElement
;
993 throw new Error( '$element frame is not contained in $anchor frame' );
995 iframePos
= $( iframe
).offset();
996 pos
.left
+= iframePos
.left
;
997 pos
.top
+= iframePos
.top
;
998 elementDocument
= iframe
.ownerDocument
;
1000 pos
.left
-= anchorPos
.left
;
1001 pos
.top
-= anchorPos
.top
;
1006 * Get element border sizes.
1009 * @param {HTMLElement} el Element to measure
1010 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1012 OO
.ui
.Element
.static.getBorders = function ( el
) {
1013 var doc
= el
.ownerDocument
,
1014 win
= doc
.defaultView
,
1015 style
= win
.getComputedStyle( el
, null ),
1017 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1018 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1019 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1020 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1031 * Get dimensions of an element or window.
1034 * @param {HTMLElement|Window} el Element to measure
1035 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1037 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1039 doc
= el
.ownerDocument
|| el
.document
,
1040 win
= doc
.defaultView
;
1042 if ( win
=== el
|| el
=== doc
.documentElement
) {
1045 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1047 top
: $win
.scrollTop(),
1048 left
: $win
.scrollLeft()
1050 scrollbar
: { right
: 0, bottom
: 0 },
1054 bottom
: $win
.innerHeight(),
1055 right
: $win
.innerWidth()
1061 borders
: this.getBorders( el
),
1063 top
: $el
.scrollTop(),
1064 left
: $el
.scrollLeft()
1067 right
: $el
.innerWidth() - el
.clientWidth
,
1068 bottom
: $el
.innerHeight() - el
.clientHeight
1070 rect
: el
.getBoundingClientRect()
1076 * Get scrollable object parent
1078 * documentElement can't be used to get or set the scrollTop
1079 * property on Blink. Changing and testing its value lets us
1080 * use 'body' or 'documentElement' based on what is working.
1082 * https://code.google.com/p/chromium/issues/detail?id=303131
1085 * @param {HTMLElement} el Element to find scrollable parent for
1086 * @return {HTMLElement} Scrollable parent
1088 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1089 var scrollTop
, body
;
1091 if ( OO
.ui
.scrollableElement
=== undefined ) {
1092 body
= el
.ownerDocument
.body
;
1093 scrollTop
= body
.scrollTop
;
1096 if ( body
.scrollTop
=== 1 ) {
1097 body
.scrollTop
= scrollTop
;
1098 OO
.ui
.scrollableElement
= 'body';
1100 OO
.ui
.scrollableElement
= 'documentElement';
1104 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1108 * Get closest scrollable container.
1110 * Traverses up until either a scrollable element or the root is reached, in which case the window
1114 * @param {HTMLElement} el Element to find scrollable container for
1115 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1116 * @return {HTMLElement} Closest scrollable container
1118 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1120 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1121 props
= [ 'overflow-x', 'overflow-y' ],
1122 $parent
= $( el
).parent();
1124 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1125 props
= [ 'overflow-' + dimension
];
1128 while ( $parent
.length
) {
1129 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1130 return $parent
[ 0 ];
1134 val
= $parent
.css( props
[ i
] );
1135 if ( val
=== 'auto' || val
=== 'scroll' ) {
1136 return $parent
[ 0 ];
1139 $parent
= $parent
.parent();
1141 return this.getDocument( el
).body
;
1145 * Scroll element into view.
1148 * @param {HTMLElement} el Element to scroll into view
1149 * @param {Object} [config] Configuration options
1150 * @param {string} [config.duration='fast'] jQuery animation duration value
1151 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1152 * to scroll in both directions
1153 * @param {Function} [config.complete] Function to call when scrolling completes.
1154 * Deprecated since 0.15.4, use the return promise instead.
1155 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1157 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1158 var position
, animations
, callback
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1159 deferred
= $.Deferred();
1161 // Configuration initialization
1162 config
= config
|| {};
1165 callback
= typeof config
.complete
=== 'function' && config
.complete
;
1167 OO
.ui
.warnDeprecation( 'Element#scrollIntoView: The `complete` callback config option is deprecated. Use the return promise instead.' );
1169 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1170 $container
= $( container
);
1171 elementDimensions
= this.getDimensions( el
);
1172 containerDimensions
= this.getDimensions( container
);
1173 $window
= $( this.getWindow( el
) );
1175 // Compute the element's position relative to the container
1176 if ( $container
.is( 'html, body' ) ) {
1177 // If the scrollable container is the root, this is easy
1179 top
: elementDimensions
.rect
.top
,
1180 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1181 left
: elementDimensions
.rect
.left
,
1182 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1185 // Otherwise, we have to subtract el's coordinates from container's coordinates
1187 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1188 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1189 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1190 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1194 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1195 if ( position
.top
< 0 ) {
1196 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1197 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1198 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1201 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1202 if ( position
.left
< 0 ) {
1203 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1204 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1205 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1208 if ( !$.isEmptyObject( animations
) ) {
1209 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1210 $container
.queue( function ( next
) {
1223 return deferred
.promise();
1227 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1228 * and reserve space for them, because it probably doesn't.
1230 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1231 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1232 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1233 * and then reattach (or show) them back.
1236 * @param {HTMLElement} el Element to reconsider the scrollbars on
1238 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1239 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1240 // Save scroll position
1241 scrollLeft
= el
.scrollLeft
;
1242 scrollTop
= el
.scrollTop
;
1243 // Detach all children
1244 while ( el
.firstChild
) {
1245 nodes
.push( el
.firstChild
);
1246 el
.removeChild( el
.firstChild
);
1249 void el
.offsetHeight
;
1250 // Reattach all children
1251 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1252 el
.appendChild( nodes
[ i
] );
1254 // Restore scroll position (no-op if scrollbars disappeared)
1255 el
.scrollLeft
= scrollLeft
;
1256 el
.scrollTop
= scrollTop
;
1262 * Toggle visibility of an element.
1264 * @param {boolean} [show] Make element visible, omit to toggle visibility
1268 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1269 show
= show
=== undefined ? !this.visible
: !!show
;
1271 if ( show
!== this.isVisible() ) {
1272 this.visible
= show
;
1273 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1274 this.emit( 'toggle', show
);
1281 * Check if element is visible.
1283 * @return {boolean} element is visible
1285 OO
.ui
.Element
.prototype.isVisible = function () {
1286 return this.visible
;
1292 * @return {Mixed} Element data
1294 OO
.ui
.Element
.prototype.getData = function () {
1301 * @param {Mixed} data Element data
1304 OO
.ui
.Element
.prototype.setData = function ( data
) {
1310 * Check if element supports one or more methods.
1312 * @param {string|string[]} methods Method or list of methods to check
1313 * @return {boolean} All methods are supported
1315 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1319 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1320 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1321 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1326 return methods
.length
=== support
;
1330 * Update the theme-provided classes.
1332 * @localdoc This is called in element mixins and widget classes any time state changes.
1333 * Updating is debounced, minimizing overhead of changing multiple attributes and
1334 * guaranteeing that theme updates do not occur within an element's constructor
1336 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1337 OO
.ui
.theme
.queueUpdateElementClasses( this );
1341 * Get the HTML tag name.
1343 * Override this method to base the result on instance information.
1345 * @return {string} HTML tag name
1347 OO
.ui
.Element
.prototype.getTagName = function () {
1348 return this.constructor.static.tagName
;
1352 * Check if the element is attached to the DOM
1354 * @return {boolean} The element is attached to the DOM
1356 OO
.ui
.Element
.prototype.isElementAttached = function () {
1357 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1361 * Get the DOM document.
1363 * @return {HTMLDocument} Document object
1365 OO
.ui
.Element
.prototype.getElementDocument = function () {
1366 // Don't cache this in other ways either because subclasses could can change this.$element
1367 return OO
.ui
.Element
.static.getDocument( this.$element
);
1371 * Get the DOM window.
1373 * @return {Window} Window object
1375 OO
.ui
.Element
.prototype.getElementWindow = function () {
1376 return OO
.ui
.Element
.static.getWindow( this.$element
);
1380 * Get closest scrollable container.
1382 * @return {HTMLElement} Closest scrollable container
1384 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1385 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1389 * Get group element is in.
1391 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1393 OO
.ui
.Element
.prototype.getElementGroup = function () {
1394 return this.elementGroup
;
1398 * Set group element is in.
1400 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1403 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1404 this.elementGroup
= group
;
1409 * Scroll element into view.
1411 * @param {Object} [config] Configuration options
1412 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1414 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1416 !this.isElementAttached() ||
1417 !this.isVisible() ||
1418 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1420 return $.Deferred().resolve();
1422 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1426 * Restore the pre-infusion dynamic state for this widget.
1428 * This method is called after #$element has been inserted into DOM. The parameter is the return
1429 * value of #gatherPreInfuseState.
1432 * @param {Object} state
1434 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1438 * Wraps an HTML snippet for use with configuration values which default
1439 * to strings. This bypasses the default html-escaping done to string
1445 * @param {string} [content] HTML content
1447 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1449 this.content
= content
;
1454 OO
.initClass( OO
.ui
.HtmlSnippet
);
1461 * @return {string} Unchanged HTML snippet.
1463 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1464 return this.content
;
1468 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1469 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1470 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1471 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1472 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1476 * @extends OO.ui.Element
1477 * @mixins OO.EventEmitter
1480 * @param {Object} [config] Configuration options
1482 OO
.ui
.Layout
= function OoUiLayout( config
) {
1483 // Configuration initialization
1484 config
= config
|| {};
1486 // Parent constructor
1487 OO
.ui
.Layout
.parent
.call( this, config
);
1489 // Mixin constructors
1490 OO
.EventEmitter
.call( this );
1493 this.$element
.addClass( 'oo-ui-layout' );
1498 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1499 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1502 * Widgets are compositions of one or more OOjs UI elements that users can both view
1503 * and interact with. All widgets can be configured and modified via a standard API,
1504 * and their state can change dynamically according to a model.
1508 * @extends OO.ui.Element
1509 * @mixins OO.EventEmitter
1512 * @param {Object} [config] Configuration options
1513 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1514 * appearance reflects this state.
1516 OO
.ui
.Widget
= function OoUiWidget( config
) {
1517 // Initialize config
1518 config
= $.extend( { disabled
: false }, config
);
1520 // Parent constructor
1521 OO
.ui
.Widget
.parent
.call( this, config
);
1523 // Mixin constructors
1524 OO
.EventEmitter
.call( this );
1527 this.disabled
= null;
1528 this.wasDisabled
= null;
1531 this.$element
.addClass( 'oo-ui-widget' );
1532 this.setDisabled( !!config
.disabled
);
1537 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1538 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1540 /* Static Properties */
1543 * Whether this widget will behave reasonably when wrapped in an HTML `<label>`. If this is true,
1544 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1549 * @property {boolean}
1551 OO
.ui
.Widget
.static.supportsSimpleLabel
= false;
1558 * A 'disable' event is emitted when the disabled state of the widget changes
1559 * (i.e. on disable **and** enable).
1561 * @param {boolean} disabled Widget is disabled
1567 * A 'toggle' event is emitted when the visibility of the widget changes.
1569 * @param {boolean} visible Widget is visible
1575 * Check if the widget is disabled.
1577 * @return {boolean} Widget is disabled
1579 OO
.ui
.Widget
.prototype.isDisabled = function () {
1580 return this.disabled
;
1584 * Set the 'disabled' state of the widget.
1586 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1588 * @param {boolean} disabled Disable widget
1591 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1594 this.disabled
= !!disabled
;
1595 isDisabled
= this.isDisabled();
1596 if ( isDisabled
!== this.wasDisabled
) {
1597 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1598 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1599 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1600 this.emit( 'disable', isDisabled
);
1601 this.updateThemeClasses();
1603 this.wasDisabled
= isDisabled
;
1609 * Update the disabled state, in case of changes in parent widget.
1613 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1614 this.setDisabled( this.disabled
);
1626 OO
.ui
.Theme
= function OoUiTheme() {
1627 this.elementClassesQueue
= [];
1628 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1633 OO
.initClass( OO
.ui
.Theme
);
1638 * Get a list of classes to be applied to a widget.
1640 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1641 * otherwise state transitions will not work properly.
1643 * @param {OO.ui.Element} element Element for which to get classes
1644 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1646 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1647 return { on
: [], off
: [] };
1651 * Update CSS classes provided by the theme.
1653 * For elements with theme logic hooks, this should be called any time there's a state change.
1655 * @param {OO.ui.Element} element Element for which to update classes
1657 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1658 var $elements
= $( [] ),
1659 classes
= this.getElementClasses( element
);
1661 if ( element
.$icon
) {
1662 $elements
= $elements
.add( element
.$icon
);
1664 if ( element
.$indicator
) {
1665 $elements
= $elements
.add( element
.$indicator
);
1669 .removeClass( classes
.off
.join( ' ' ) )
1670 .addClass( classes
.on
.join( ' ' ) );
1676 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1678 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1679 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1682 this.elementClassesQueue
= [];
1686 * Queue #updateElementClasses to be called for this element.
1688 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1689 * to make them synchronous.
1691 * @param {OO.ui.Element} element Element for which to update classes
1693 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1694 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1695 // the most common case (this method is often called repeatedly for the same element).
1696 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1699 this.elementClassesQueue
.push( element
);
1700 this.debouncedUpdateQueuedElementClasses();
1704 * Get the transition duration in milliseconds for dialogs opening/closing
1706 * The dialog should be fully rendered this many milliseconds after the
1707 * ready process has executed.
1709 * @return {number} Transition duration in milliseconds
1711 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1716 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1717 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1718 * order in which users will navigate through the focusable elements via the "tab" key.
1721 * // TabIndexedElement is mixed into the ButtonWidget class
1722 * // to provide a tabIndex property.
1723 * var button1 = new OO.ui.ButtonWidget( {
1727 * var button2 = new OO.ui.ButtonWidget( {
1731 * var button3 = new OO.ui.ButtonWidget( {
1735 * var button4 = new OO.ui.ButtonWidget( {
1739 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1745 * @param {Object} [config] Configuration options
1746 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1747 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1748 * functionality will be applied to it instead.
1749 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1750 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1751 * to remove the element from the tab-navigation flow.
1753 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1754 // Configuration initialization
1755 config
= $.extend( { tabIndex
: 0 }, config
);
1758 this.$tabIndexed
= null;
1759 this.tabIndex
= null;
1762 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1765 this.setTabIndex( config
.tabIndex
);
1766 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1771 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1776 * Set the element that should use the tabindex functionality.
1778 * This method is used to retarget a tabindex mixin so that its functionality applies
1779 * to the specified element. If an element is currently using the functionality, the mixin’s
1780 * effect on that element is removed before the new element is set up.
1782 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1785 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1786 var tabIndex
= this.tabIndex
;
1787 // Remove attributes from old $tabIndexed
1788 this.setTabIndex( null );
1789 // Force update of new $tabIndexed
1790 this.$tabIndexed
= $tabIndexed
;
1791 this.tabIndex
= tabIndex
;
1792 return this.updateTabIndex();
1796 * Set the value of the tabindex.
1798 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
1801 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1802 tabIndex
= typeof tabIndex
=== 'number' ? tabIndex
: null;
1804 if ( this.tabIndex
!== tabIndex
) {
1805 this.tabIndex
= tabIndex
;
1806 this.updateTabIndex();
1813 * Update the `tabindex` attribute, in case of changes to tab index or
1819 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1820 if ( this.$tabIndexed
) {
1821 if ( this.tabIndex
!== null ) {
1822 // Do not index over disabled elements
1823 this.$tabIndexed
.attr( {
1824 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1825 // Support: ChromeVox and NVDA
1826 // These do not seem to inherit aria-disabled from parent elements
1827 'aria-disabled': this.isDisabled().toString()
1830 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1837 * Handle disable events.
1840 * @param {boolean} disabled Element is disabled
1842 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1843 this.updateTabIndex();
1847 * Get the value of the tabindex.
1849 * @return {number|null} Tabindex value
1851 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
1852 return this.tabIndex
;
1856 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
1857 * interface element that can be configured with access keys for accessibility.
1858 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
1860 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
1866 * @param {Object} [config] Configuration options
1867 * @cfg {jQuery} [$button] The button element created by the class.
1868 * If this configuration is omitted, the button element will use a generated `<a>`.
1869 * @cfg {boolean} [framed=true] Render the button with a frame
1871 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
1872 // Configuration initialization
1873 config
= config
|| {};
1876 this.$button
= null;
1878 this.active
= config
.active
!== undefined && config
.active
;
1879 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
1880 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
1881 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
1882 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
1883 this.onClickHandler
= this.onClick
.bind( this );
1884 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
1887 this.$element
.addClass( 'oo-ui-buttonElement' );
1888 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
1889 this.setButtonElement( config
.$button
|| $( '<a>' ) );
1894 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
1896 /* Static Properties */
1899 * Cancel mouse down events.
1901 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
1902 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
1903 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
1908 * @property {boolean}
1910 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
1915 * A 'click' event is emitted when the button element is clicked.
1923 * Set the button element.
1925 * This method is used to retarget a button mixin so that its functionality applies to
1926 * the specified button element instead of the one created by the class. If a button element
1927 * is already set, the method will remove the mixin’s effect on that element.
1929 * @param {jQuery} $button Element to use as button
1931 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
1932 if ( this.$button
) {
1934 .removeClass( 'oo-ui-buttonElement-button' )
1935 .removeAttr( 'role accesskey' )
1937 mousedown
: this.onMouseDownHandler
,
1938 keydown
: this.onKeyDownHandler
,
1939 click
: this.onClickHandler
,
1940 keypress
: this.onKeyPressHandler
1944 this.$button
= $button
1945 .addClass( 'oo-ui-buttonElement-button' )
1947 mousedown
: this.onMouseDownHandler
,
1948 keydown
: this.onKeyDownHandler
,
1949 click
: this.onClickHandler
,
1950 keypress
: this.onKeyPressHandler
1953 // Add `role="button"` on `<a>` elements, where it's needed
1954 // `toUppercase()` is added for XHTML documents
1955 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
1956 this.$button
.attr( 'role', 'button' );
1961 * Handles mouse down events.
1964 * @param {jQuery.Event} e Mouse down event
1966 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
1967 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
1970 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
1971 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
1972 // reliably remove the pressed class
1973 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
1974 // Prevent change of focus unless specifically configured otherwise
1975 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
1981 * Handles mouse up events.
1984 * @param {MouseEvent} e Mouse up event
1986 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
1987 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
1990 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
1991 // Stop listening for mouseup, since we only needed this once
1992 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
1996 * Handles mouse click events.
1999 * @param {jQuery.Event} e Mouse click event
2002 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2003 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2004 if ( this.emit( 'click' ) ) {
2011 * Handles key down events.
2014 * @param {jQuery.Event} e Key down event
2016 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2017 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2020 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2021 // Run the keyup handler no matter where the key is when the button is let go, so we can
2022 // reliably remove the pressed class
2023 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
2027 * Handles key up events.
2030 * @param {KeyboardEvent} e Key up event
2032 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
2033 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2036 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2037 // Stop listening for keyup, since we only needed this once
2038 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
2042 * Handles key press events.
2045 * @param {jQuery.Event} e Key press event
2048 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2049 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2050 if ( this.emit( 'click' ) ) {
2057 * Check if button has a frame.
2059 * @return {boolean} Button is framed
2061 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2066 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2068 * @param {boolean} [framed] Make button framed, omit to toggle
2071 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2072 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2073 if ( framed
!== this.framed
) {
2074 this.framed
= framed
;
2076 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2077 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2078 this.updateThemeClasses();
2085 * Set the button's active state.
2087 * The active state can be set on:
2089 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2090 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2091 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2094 * @param {boolean} value Make button active
2097 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2098 this.active
= !!value
;
2099 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2100 this.updateThemeClasses();
2105 * Check if the button is active
2108 * @return {boolean} The button is active
2110 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2115 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2116 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2117 * items from the group is done through the interface the class provides.
2118 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2120 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
2126 * @param {Object} [config] Configuration options
2127 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2128 * is omitted, the group element will use a generated `<div>`.
2130 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2131 // Configuration initialization
2132 config
= config
|| {};
2137 this.aggregateItemEvents
= {};
2140 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2148 * A change event is emitted when the set of selected items changes.
2150 * @param {OO.ui.Element[]} items Items currently in the group
2156 * Set the group element.
2158 * If an element is already set, items will be moved to the new element.
2160 * @param {jQuery} $group Element to use as group
2162 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2165 this.$group
= $group
;
2166 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2167 this.$group
.append( this.items
[ i
].$element
);
2172 * Check if a group contains no items.
2174 * @return {boolean} Group is empty
2176 OO
.ui
.mixin
.GroupElement
.prototype.isEmpty = function () {
2177 return !this.items
.length
;
2181 * Get all items in the group.
2183 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
2184 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
2187 * @return {OO.ui.Element[]} An array of items.
2189 OO
.ui
.mixin
.GroupElement
.prototype.getItems = function () {
2190 return this.items
.slice( 0 );
2194 * Get an item by its data.
2196 * Only the first item with matching data will be returned. To return all matching items,
2197 * use the #getItemsFromData method.
2199 * @param {Object} data Item data to search for
2200 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2202 OO
.ui
.mixin
.GroupElement
.prototype.getItemFromData = function ( data
) {
2204 hash
= OO
.getHash( data
);
2206 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2207 item
= this.items
[ i
];
2208 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2217 * Get items by their data.
2219 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2221 * @param {Object} data Item data to search for
2222 * @return {OO.ui.Element[]} Items with equivalent data
2224 OO
.ui
.mixin
.GroupElement
.prototype.getItemsFromData = function ( data
) {
2226 hash
= OO
.getHash( data
),
2229 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2230 item
= this.items
[ i
];
2231 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2240 * Aggregate the events emitted by the group.
2242 * When events are aggregated, the group will listen to all contained items for the event,
2243 * and then emit the event under a new name. The new event will contain an additional leading
2244 * parameter containing the item that emitted the original event. Other arguments emitted from
2245 * the original event are passed through.
2247 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
2248 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
2249 * A `null` value will remove aggregated events.
2251 * @throws {Error} An error is thrown if aggregation already exists.
2253 OO
.ui
.mixin
.GroupElement
.prototype.aggregate = function ( events
) {
2254 var i
, len
, item
, add
, remove
, itemEvent
, groupEvent
;
2256 for ( itemEvent
in events
) {
2257 groupEvent
= events
[ itemEvent
];
2259 // Remove existing aggregated event
2260 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2261 // Don't allow duplicate aggregations
2263 throw new Error( 'Duplicate item event aggregation for ' + itemEvent
);
2265 // Remove event aggregation from existing items
2266 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2267 item
= this.items
[ i
];
2268 if ( item
.connect
&& item
.disconnect
) {
2270 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2271 item
.disconnect( this, remove
);
2274 // Prevent future items from aggregating event
2275 delete this.aggregateItemEvents
[ itemEvent
];
2278 // Add new aggregate event
2280 // Make future items aggregate event
2281 this.aggregateItemEvents
[ itemEvent
] = groupEvent
;
2282 // Add event aggregation to existing items
2283 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2284 item
= this.items
[ i
];
2285 if ( item
.connect
&& item
.disconnect
) {
2287 add
[ itemEvent
] = [ 'emit', groupEvent
, item
];
2288 item
.connect( this, add
);
2296 * Add items to the group.
2298 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2299 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2301 * @param {OO.ui.Element[]} items An array of items to add to the group
2302 * @param {number} [index] Index of the insertion point
2305 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2306 var i
, len
, item
, itemEvent
, events
, currentIndex
,
2309 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2312 // Check if item exists then remove it first, effectively "moving" it
2313 currentIndex
= this.items
.indexOf( item
);
2314 if ( currentIndex
>= 0 ) {
2315 this.removeItems( [ item
] );
2316 // Adjust index to compensate for removal
2317 if ( currentIndex
< index
) {
2322 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
2324 for ( itemEvent
in this.aggregateItemEvents
) {
2325 events
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2327 item
.connect( this, events
);
2329 item
.setElementGroup( this );
2330 itemElements
.push( item
.$element
.get( 0 ) );
2333 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2334 this.$group
.append( itemElements
);
2335 this.items
.push
.apply( this.items
, items
);
2336 } else if ( index
=== 0 ) {
2337 this.$group
.prepend( itemElements
);
2338 this.items
.unshift
.apply( this.items
, items
);
2340 this.items
[ index
].$element
.before( itemElements
);
2341 this.items
.splice
.apply( this.items
, [ index
, 0 ].concat( items
) );
2344 this.emit( 'change', this.getItems() );
2349 * Remove the specified items from a group.
2351 * Removed items are detached (not removed) from the DOM so that they may be reused.
2352 * To remove all items from a group, you may wish to use the #clearItems method instead.
2354 * @param {OO.ui.Element[]} items An array of items to remove
2357 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2358 var i
, len
, item
, index
, events
, itemEvent
;
2360 // Remove specific items
2361 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2363 index
= this.items
.indexOf( item
);
2364 if ( index
!== -1 ) {
2365 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
2367 for ( itemEvent
in this.aggregateItemEvents
) {
2368 events
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2370 item
.disconnect( this, events
);
2372 item
.setElementGroup( null );
2373 this.items
.splice( index
, 1 );
2374 item
.$element
.detach();
2378 this.emit( 'change', this.getItems() );
2383 * Clear all items from the group.
2385 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2386 * To remove only a subset of items from a group, use the #removeItems method.
2390 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2391 var i
, len
, item
, remove
, itemEvent
;
2394 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2395 item
= this.items
[ i
];
2397 item
.connect
&& item
.disconnect
&&
2398 !$.isEmptyObject( this.aggregateItemEvents
)
2401 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2402 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2404 item
.disconnect( this, remove
);
2406 item
.setElementGroup( null );
2407 item
.$element
.detach();
2410 this.emit( 'change', this.getItems() );
2416 * IconElement is often mixed into other classes to generate an icon.
2417 * Icons are graphics, about the size of normal text. They are used to aid the user
2418 * in locating a control or to convey information in a space-efficient way. See the
2419 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2420 * included in the library.
2422 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2428 * @param {Object} [config] Configuration options
2429 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2430 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2431 * the icon element be set to an existing icon instead of the one generated by this class, set a
2432 * value using a jQuery selection. For example:
2434 * // Use a <div> tag instead of a <span>
2436 * // Use an existing icon element instead of the one generated by the class
2437 * $icon: this.$element
2438 * // Use an icon element from a child widget
2439 * $icon: this.childwidget.$element
2440 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2441 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2442 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2443 * by the user's language.
2445 * Example of an i18n map:
2447 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2448 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2449 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2450 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2451 * text. The icon title is displayed when users move the mouse over the icon.
2453 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2454 // Configuration initialization
2455 config
= config
|| {};
2460 this.iconTitle
= null;
2463 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2464 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2465 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2470 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2472 /* Static Properties */
2475 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2476 * for i18n purposes and contains a `default` icon name and additional names keyed by
2477 * language code. The `default` name is used when no icon is keyed by the user's language.
2479 * Example of an i18n map:
2481 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2483 * Note: the static property will be overridden if the #icon configuration is used.
2487 * @property {Object|string}
2489 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2492 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2493 * function that returns title text, or `null` for no title.
2495 * The static property will be overridden if the #iconTitle configuration is used.
2499 * @property {string|Function|null}
2501 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2506 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2507 * applies to the specified icon element instead of the one created by the class. If an icon
2508 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2509 * and mixin methods will no longer affect the element.
2511 * @param {jQuery} $icon Element to use as icon
2513 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2516 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2517 .removeAttr( 'title' );
2521 .addClass( 'oo-ui-iconElement-icon' )
2522 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2523 if ( this.iconTitle
!== null ) {
2524 this.$icon
.attr( 'title', this.iconTitle
);
2527 this.updateThemeClasses();
2531 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2532 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2535 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2536 * by language code, or `null` to remove the icon.
2539 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2540 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2541 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2543 if ( this.icon
!== icon
) {
2545 if ( this.icon
!== null ) {
2546 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2548 if ( icon
!== null ) {
2549 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2555 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2556 this.updateThemeClasses();
2562 * Set the icon title. Use `null` to remove the title.
2564 * @param {string|Function|null} iconTitle A text string used as the icon title,
2565 * a function that returns title text, or `null` for no title.
2568 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2569 iconTitle
= typeof iconTitle
=== 'function' ||
2570 ( typeof iconTitle
=== 'string' && iconTitle
.length
) ?
2571 OO
.ui
.resolveMsg( iconTitle
) : null;
2573 if ( this.iconTitle
!== iconTitle
) {
2574 this.iconTitle
= iconTitle
;
2576 if ( this.iconTitle
!== null ) {
2577 this.$icon
.attr( 'title', iconTitle
);
2579 this.$icon
.removeAttr( 'title' );
2588 * Get the symbolic name of the icon.
2590 * @return {string} Icon name
2592 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2597 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2599 * @return {string} Icon title text
2601 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2602 return this.iconTitle
;
2606 * IndicatorElement is often mixed into other classes to generate an indicator.
2607 * Indicators are small graphics that are generally used in two ways:
2609 * - To draw attention to the status of an item. For example, an indicator might be
2610 * used to show that an item in a list has errors that need to be resolved.
2611 * - To clarify the function of a control that acts in an exceptional way (a button
2612 * that opens a menu instead of performing an action directly, for example).
2614 * For a list of indicators included in the library, please see the
2615 * [OOjs UI documentation on MediaWiki] [1].
2617 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2623 * @param {Object} [config] Configuration options
2624 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2625 * configuration is omitted, the indicator element will use a generated `<span>`.
2626 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2627 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2629 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2630 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2631 * or a function that returns title text. The indicator title is displayed when users move
2632 * the mouse over the indicator.
2634 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2635 // Configuration initialization
2636 config
= config
|| {};
2639 this.$indicator
= null;
2640 this.indicator
= null;
2641 this.indicatorTitle
= null;
2644 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2645 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2646 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2651 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2653 /* Static Properties */
2656 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2657 * The static property will be overridden if the #indicator configuration is used.
2661 * @property {string|null}
2663 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2666 * A text string used as the indicator title, a function that returns title text, or `null`
2667 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2671 * @property {string|Function|null}
2673 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2678 * Set the indicator element.
2680 * If an element is already set, it will be cleaned up before setting up the new element.
2682 * @param {jQuery} $indicator Element to use as indicator
2684 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2685 if ( this.$indicator
) {
2687 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2688 .removeAttr( 'title' );
2691 this.$indicator
= $indicator
2692 .addClass( 'oo-ui-indicatorElement-indicator' )
2693 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2694 if ( this.indicatorTitle
!== null ) {
2695 this.$indicator
.attr( 'title', this.indicatorTitle
);
2698 this.updateThemeClasses();
2702 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2704 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2707 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2708 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2710 if ( this.indicator
!== indicator
) {
2711 if ( this.$indicator
) {
2712 if ( this.indicator
!== null ) {
2713 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2715 if ( indicator
!== null ) {
2716 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2719 this.indicator
= indicator
;
2722 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2723 this.updateThemeClasses();
2729 * Set the indicator title.
2731 * The title is displayed when a user moves the mouse over the indicator.
2733 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2734 * `null` for no indicator title
2737 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2738 indicatorTitle
= typeof indicatorTitle
=== 'function' ||
2739 ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ?
2740 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2742 if ( this.indicatorTitle
!== indicatorTitle
) {
2743 this.indicatorTitle
= indicatorTitle
;
2744 if ( this.$indicator
) {
2745 if ( this.indicatorTitle
!== null ) {
2746 this.$indicator
.attr( 'title', indicatorTitle
);
2748 this.$indicator
.removeAttr( 'title' );
2757 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2759 * @return {string} Symbolic name of indicator
2761 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2762 return this.indicator
;
2766 * Get the indicator title.
2768 * The title is displayed when a user moves the mouse over the indicator.
2770 * @return {string} Indicator title text
2772 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2773 return this.indicatorTitle
;
2777 * LabelElement is often mixed into other classes to generate a label, which
2778 * helps identify the function of an interface element.
2779 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2781 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2787 * @param {Object} [config] Configuration options
2788 * @cfg {jQuery} [$label] The label element created by the class. If this
2789 * configuration is omitted, the label element will use a generated `<span>`.
2790 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2791 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2792 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2793 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2795 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2796 // Configuration initialization
2797 config
= config
|| {};
2804 this.setLabel( config
.label
|| this.constructor.static.label
);
2805 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2810 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2815 * @event labelChange
2816 * @param {string} value
2819 /* Static Properties */
2822 * The label text. The label can be specified as a plaintext string, a function that will
2823 * produce a string in the future, or `null` for no label. The static value will
2824 * be overridden if a label is specified with the #label config option.
2828 * @property {string|Function|null}
2830 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2832 /* Static methods */
2835 * Highlight the first occurrence of the query in the given text
2837 * @param {string} text Text
2838 * @param {string} query Query to find
2839 * @return {jQuery} Text with the first match of the query
2840 * sub-string wrapped in highlighted span
2842 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
) {
2843 var $result
= $( '<span>' ),
2844 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2846 if ( !query
.length
|| offset
=== -1 ) {
2847 return $result
.text( text
);
2850 document
.createTextNode( text
.slice( 0, offset
) ),
2852 .addClass( 'oo-ui-labelElement-label-highlight' )
2853 .text( text
.slice( offset
, offset
+ query
.length
) ),
2854 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2856 return $result
.contents();
2862 * Set the label element.
2864 * If an element is already set, it will be cleaned up before setting up the new element.
2866 * @param {jQuery} $label Element to use as label
2868 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2869 if ( this.$label
) {
2870 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2873 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2874 this.setLabelContent( this.label
);
2880 * An empty string will result in the label being hidden. A string containing only whitespace will
2881 * be converted to a single ` `.
2883 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2884 * text; or null for no label
2887 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2888 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2889 label
= ( ( typeof label
=== 'string' || label
instanceof jQuery
) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2891 if ( this.label
!== label
) {
2892 if ( this.$label
) {
2893 this.setLabelContent( label
);
2896 this.emit( 'labelChange' );
2899 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
);
2905 * Set the label as plain text with a highlighted query
2907 * @param {string} text Text label to set
2908 * @param {string} query Substring of text to highlight
2911 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
) {
2912 return this.setLabel( this.constructor.static.highlightQuery( text
, query
) );
2918 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2919 * text; or null for no label
2921 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2926 * Set the content of the label.
2928 * Do not call this method until after the label element has been set by #setLabelElement.
2931 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2932 * text; or null for no label
2934 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2935 if ( typeof label
=== 'string' ) {
2936 if ( label
.match( /^\s*$/ ) ) {
2937 // Convert whitespace only string to a single non-breaking space
2938 this.$label
.html( ' ' );
2940 this.$label
.text( label
);
2942 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2943 this.$label
.html( label
.toString() );
2944 } else if ( label
instanceof jQuery
) {
2945 this.$label
.empty().append( label
);
2947 this.$label
.empty();
2952 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
2953 * additional functionality to an element created by another class. The class provides
2954 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
2955 * which are used to customize the look and feel of a widget to better describe its
2956 * importance and functionality.
2958 * The library currently contains the following styling flags for general use:
2960 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
2961 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
2962 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
2964 * The flags affect the appearance of the buttons:
2967 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
2968 * var button1 = new OO.ui.ButtonWidget( {
2969 * label: 'Constructive',
2970 * flags: 'constructive'
2972 * var button2 = new OO.ui.ButtonWidget( {
2973 * label: 'Destructive',
2974 * flags: 'destructive'
2976 * var button3 = new OO.ui.ButtonWidget( {
2977 * label: 'Progressive',
2978 * flags: 'progressive'
2980 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
2982 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
2983 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
2985 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2991 * @param {Object} [config] Configuration options
2992 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
2993 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
2994 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2995 * @cfg {jQuery} [$flagged] The flagged element. By default,
2996 * the flagged functionality is applied to the element created by the class ($element).
2997 * If a different element is specified, the flagged functionality will be applied to it instead.
2999 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3000 // Configuration initialization
3001 config
= config
|| {};
3005 this.$flagged
= null;
3008 this.setFlags( config
.flags
);
3009 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3016 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3017 * parameter contains the name of each modified flag and indicates whether it was
3020 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3021 * that the flag was added, `false` that the flag was removed.
3027 * Set the flagged element.
3029 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3030 * If an element is already set, the method will remove the mixin’s effect on that element.
3032 * @param {jQuery} $flagged Element that should be flagged
3034 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3035 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3036 return 'oo-ui-flaggedElement-' + flag
;
3039 if ( this.$flagged
) {
3040 this.$flagged
.removeClass( classNames
);
3043 this.$flagged
= $flagged
.addClass( classNames
);
3047 * Check if the specified flag is set.
3049 * @param {string} flag Name of flag
3050 * @return {boolean} The flag is set
3052 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3053 // This may be called before the constructor, thus before this.flags is set
3054 return this.flags
&& ( flag
in this.flags
);
3058 * Get the names of all flags set.
3060 * @return {string[]} Flag names
3062 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3063 // This may be called before the constructor, thus before this.flags is set
3064 return Object
.keys( this.flags
|| {} );
3073 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3074 var flag
, className
,
3077 classPrefix
= 'oo-ui-flaggedElement-';
3079 for ( flag
in this.flags
) {
3080 className
= classPrefix
+ flag
;
3081 changes
[ flag
] = false;
3082 delete this.flags
[ flag
];
3083 remove
.push( className
);
3086 if ( this.$flagged
) {
3087 this.$flagged
.removeClass( remove
.join( ' ' ) );
3090 this.updateThemeClasses();
3091 this.emit( 'flag', changes
);
3097 * Add one or more flags.
3099 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3100 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3101 * be added (`true`) or removed (`false`).
3105 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3106 var i
, len
, flag
, className
,
3110 classPrefix
= 'oo-ui-flaggedElement-';
3112 if ( typeof flags
=== 'string' ) {
3113 className
= classPrefix
+ flags
;
3115 if ( !this.flags
[ flags
] ) {
3116 this.flags
[ flags
] = true;
3117 add
.push( className
);
3119 } else if ( Array
.isArray( flags
) ) {
3120 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3122 className
= classPrefix
+ flag
;
3124 if ( !this.flags
[ flag
] ) {
3125 changes
[ flag
] = true;
3126 this.flags
[ flag
] = true;
3127 add
.push( className
);
3130 } else if ( OO
.isPlainObject( flags
) ) {
3131 for ( flag
in flags
) {
3132 className
= classPrefix
+ flag
;
3133 if ( flags
[ flag
] ) {
3135 if ( !this.flags
[ flag
] ) {
3136 changes
[ flag
] = true;
3137 this.flags
[ flag
] = true;
3138 add
.push( className
);
3142 if ( this.flags
[ flag
] ) {
3143 changes
[ flag
] = false;
3144 delete this.flags
[ flag
];
3145 remove
.push( className
);
3151 if ( this.$flagged
) {
3153 .addClass( add
.join( ' ' ) )
3154 .removeClass( remove
.join( ' ' ) );
3157 this.updateThemeClasses();
3158 this.emit( 'flag', changes
);
3164 * TitledElement is mixed into other classes to provide a `title` attribute.
3165 * Titles are rendered by the browser and are made visible when the user moves
3166 * the mouse over the element. Titles are not visible on touch devices.
3169 * // TitledElement provides a 'title' attribute to the
3170 * // ButtonWidget class
3171 * var button = new OO.ui.ButtonWidget( {
3172 * label: 'Button with Title',
3173 * title: 'I am a button'
3175 * $( 'body' ).append( button.$element );
3181 * @param {Object} [config] Configuration options
3182 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3183 * If this config is omitted, the title functionality is applied to $element, the
3184 * element created by the class.
3185 * @cfg {string|Function} [title] The title text or a function that returns text. If
3186 * this config is omitted, the value of the {@link #static-title static title} property is used.
3188 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3189 // Configuration initialization
3190 config
= config
|| {};
3193 this.$titled
= null;
3197 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3198 this.setTitledElement( config
.$titled
|| this.$element
);
3203 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3205 /* Static Properties */
3208 * The title text, a function that returns text, or `null` for no title. The value of the static property
3209 * is overridden if the #title config option is used.
3213 * @property {string|Function|null}
3215 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3220 * Set the titled element.
3222 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3223 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3225 * @param {jQuery} $titled Element that should use the 'titled' functionality
3227 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3228 if ( this.$titled
) {
3229 this.$titled
.removeAttr( 'title' );
3232 this.$titled
= $titled
;
3234 this.$titled
.attr( 'title', this.title
);
3241 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3244 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3245 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3246 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3248 if ( this.title
!== title
) {
3249 if ( this.$titled
) {
3250 if ( title
!== null ) {
3251 this.$titled
.attr( 'title', title
);
3253 this.$titled
.removeAttr( 'title' );
3265 * @return {string} Title string
3267 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3272 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3273 * Accesskeys allow an user to go to a specific element by using
3274 * a shortcut combination of a browser specific keys + the key
3278 * // AccessKeyedElement provides an 'accesskey' attribute to the
3279 * // ButtonWidget class
3280 * var button = new OO.ui.ButtonWidget( {
3281 * label: 'Button with Accesskey',
3284 * $( 'body' ).append( button.$element );
3290 * @param {Object} [config] Configuration options
3291 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3292 * If this config is omitted, the accesskey functionality is applied to $element, the
3293 * element created by the class.
3294 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3295 * this config is omitted, no accesskey will be added.
3297 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3298 // Configuration initialization
3299 config
= config
|| {};
3302 this.$accessKeyed
= null;
3303 this.accessKey
= null;
3306 this.setAccessKey( config
.accessKey
|| null );
3307 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3312 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3314 /* Static Properties */
3317 * The access key, a function that returns a key, or `null` for no accesskey.
3321 * @property {string|Function|null}
3323 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3328 * Set the accesskeyed element.
3330 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3331 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3333 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3335 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3336 if ( this.$accessKeyed
) {
3337 this.$accessKeyed
.removeAttr( 'accesskey' );
3340 this.$accessKeyed
= $accessKeyed
;
3341 if ( this.accessKey
) {
3342 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3349 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3352 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3353 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3355 if ( this.accessKey
!== accessKey
) {
3356 if ( this.$accessKeyed
) {
3357 if ( accessKey
!== null ) {
3358 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3360 this.$accessKeyed
.removeAttr( 'accesskey' );
3363 this.accessKey
= accessKey
;
3372 * @return {string} accessKey string
3374 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3375 return this.accessKey
;
3379 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3380 * feels, and functionality can be customized via the class’s configuration options
3381 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3384 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3387 * // A button widget
3388 * var button = new OO.ui.ButtonWidget( {
3389 * label: 'Button with Icon',
3391 * iconTitle: 'Remove'
3393 * $( 'body' ).append( button.$element );
3395 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3398 * @extends OO.ui.Widget
3399 * @mixins OO.ui.mixin.ButtonElement
3400 * @mixins OO.ui.mixin.IconElement
3401 * @mixins OO.ui.mixin.IndicatorElement
3402 * @mixins OO.ui.mixin.LabelElement
3403 * @mixins OO.ui.mixin.TitledElement
3404 * @mixins OO.ui.mixin.FlaggedElement
3405 * @mixins OO.ui.mixin.TabIndexedElement
3406 * @mixins OO.ui.mixin.AccessKeyedElement
3409 * @param {Object} [config] Configuration options
3410 * @cfg {boolean} [active=false] Whether button should be shown as active
3411 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3412 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3413 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3415 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3416 // Configuration initialization
3417 config
= config
|| {};
3419 // Parent constructor
3420 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3422 // Mixin constructors
3423 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3424 OO
.ui
.mixin
.IconElement
.call( this, config
);
3425 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3426 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3427 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3428 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3429 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3430 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3435 this.noFollow
= false;
3438 this.connect( this, { disable
: 'onDisable' } );
3441 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3443 .addClass( 'oo-ui-buttonWidget' )
3444 .append( this.$button
);
3445 this.setActive( config
.active
);
3446 this.setHref( config
.href
);
3447 this.setTarget( config
.target
);
3448 this.setNoFollow( config
.noFollow
);
3453 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3454 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3455 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3456 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3457 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3458 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3459 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3460 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3461 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3463 /* Static Properties */
3469 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3474 * Get hyperlink location.
3476 * @return {string} Hyperlink location
3478 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3483 * Get hyperlink target.
3485 * @return {string} Hyperlink target
3487 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3492 * Get search engine traversal hint.
3494 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3496 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3497 return this.noFollow
;
3501 * Set hyperlink location.
3503 * @param {string|null} href Hyperlink location, null to remove
3505 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3506 href
= typeof href
=== 'string' ? href
: null;
3507 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3511 if ( href
!== this.href
) {
3520 * Update the `href` attribute, in case of changes to href or
3526 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3527 if ( this.href
!== null && !this.isDisabled() ) {
3528 this.$button
.attr( 'href', this.href
);
3530 this.$button
.removeAttr( 'href' );
3537 * Handle disable events.
3540 * @param {boolean} disabled Element is disabled
3542 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3547 * Set hyperlink target.
3549 * @param {string|null} target Hyperlink target, null to remove
3551 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3552 target
= typeof target
=== 'string' ? target
: null;
3554 if ( target
!== this.target
) {
3555 this.target
= target
;
3556 if ( target
!== null ) {
3557 this.$button
.attr( 'target', target
);
3559 this.$button
.removeAttr( 'target' );
3567 * Set search engine traversal hint.
3569 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3571 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3572 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3574 if ( noFollow
!== this.noFollow
) {
3575 this.noFollow
= noFollow
;
3577 this.$button
.attr( 'rel', 'nofollow' );
3579 this.$button
.removeAttr( 'rel' );
3586 // Override method visibility hints from ButtonElement
3595 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3596 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3597 * removed, and cleared from the group.
3600 * // Example: A ButtonGroupWidget with two buttons
3601 * var button1 = new OO.ui.PopupButtonWidget( {
3602 * label: 'Select a category',
3605 * $content: $( '<p>List of categories...</p>' ),
3610 * var button2 = new OO.ui.ButtonWidget( {
3613 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3614 * items: [button1, button2]
3616 * $( 'body' ).append( buttonGroup.$element );
3619 * @extends OO.ui.Widget
3620 * @mixins OO.ui.mixin.GroupElement
3623 * @param {Object} [config] Configuration options
3624 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3626 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3627 // Configuration initialization
3628 config
= config
|| {};
3630 // Parent constructor
3631 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3633 // Mixin constructors
3634 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3637 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3638 if ( Array
.isArray( config
.items
) ) {
3639 this.addItems( config
.items
);
3645 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3646 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3649 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3650 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3651 * for a list of icons included in the library.
3654 * // An icon widget with a label
3655 * var myIcon = new OO.ui.IconWidget( {
3659 * // Create a label.
3660 * var iconLabel = new OO.ui.LabelWidget( {
3663 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3665 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3668 * @extends OO.ui.Widget
3669 * @mixins OO.ui.mixin.IconElement
3670 * @mixins OO.ui.mixin.TitledElement
3671 * @mixins OO.ui.mixin.FlaggedElement
3674 * @param {Object} [config] Configuration options
3676 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3677 // Configuration initialization
3678 config
= config
|| {};
3680 // Parent constructor
3681 OO
.ui
.IconWidget
.parent
.call( this, config
);
3683 // Mixin constructors
3684 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3685 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3686 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3689 this.$element
.addClass( 'oo-ui-iconWidget' );
3694 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3695 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3696 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3697 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3699 /* Static Properties */
3705 OO
.ui
.IconWidget
.static.tagName
= 'span';
3708 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3709 * attention to the status of an item or to clarify the function of a control. For a list of
3710 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3713 * // Example of an indicator widget
3714 * var indicator1 = new OO.ui.IndicatorWidget( {
3715 * indicator: 'alert'
3718 * // Create a fieldset layout to add a label
3719 * var fieldset = new OO.ui.FieldsetLayout();
3720 * fieldset.addItems( [
3721 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3723 * $( 'body' ).append( fieldset.$element );
3725 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3728 * @extends OO.ui.Widget
3729 * @mixins OO.ui.mixin.IndicatorElement
3730 * @mixins OO.ui.mixin.TitledElement
3733 * @param {Object} [config] Configuration options
3735 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
3736 // Configuration initialization
3737 config
= config
|| {};
3739 // Parent constructor
3740 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
3742 // Mixin constructors
3743 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
3744 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3747 this.$element
.addClass( 'oo-ui-indicatorWidget' );
3752 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
3753 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
3754 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
3756 /* Static Properties */
3762 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
3765 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3766 * be configured with a `label` option that is set to a string, a label node, or a function:
3768 * - String: a plaintext string
3769 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3770 * label that includes a link or special styling, such as a gray color or additional graphical elements.
3771 * - Function: a function that will produce a string in the future. Functions are used
3772 * in cases where the value of the label is not currently defined.
3774 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3775 * will come into focus when the label is clicked.
3778 * // Examples of LabelWidgets
3779 * var label1 = new OO.ui.LabelWidget( {
3780 * label: 'plaintext label'
3782 * var label2 = new OO.ui.LabelWidget( {
3783 * label: $( '<a href="default.html">jQuery label</a>' )
3785 * // Create a fieldset layout with fields for each example
3786 * var fieldset = new OO.ui.FieldsetLayout();
3787 * fieldset.addItems( [
3788 * new OO.ui.FieldLayout( label1 ),
3789 * new OO.ui.FieldLayout( label2 )
3791 * $( 'body' ).append( fieldset.$element );
3794 * @extends OO.ui.Widget
3795 * @mixins OO.ui.mixin.LabelElement
3796 * @mixins OO.ui.mixin.TitledElement
3799 * @param {Object} [config] Configuration options
3800 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
3801 * Clicking the label will focus the specified input field.
3803 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
3804 // Configuration initialization
3805 config
= config
|| {};
3807 // Parent constructor
3808 OO
.ui
.LabelWidget
.parent
.call( this, config
);
3810 // Mixin constructors
3811 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
3812 OO
.ui
.mixin
.TitledElement
.call( this, config
);
3815 this.input
= config
.input
;
3818 if ( this.input
instanceof OO
.ui
.InputWidget
) {
3819 if ( this.input
.getInputId() ) {
3820 this.$element
.attr( 'for', this.input
.getInputId() );
3823 this.$element
.addClass( 'oo-ui-labelWidget' );
3828 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
3829 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
3830 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
3832 /* Static Properties */
3838 OO
.ui
.LabelWidget
.static.tagName
= 'label';
3841 * PendingElement is a mixin that is used to create elements that notify users that something is happening
3842 * and that they should wait before proceeding. The pending state is visually represented with a pending
3843 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
3844 * field of a {@link OO.ui.TextInputWidget text input widget}.
3846 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
3847 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
3848 * in process dialogs.
3851 * function MessageDialog( config ) {
3852 * MessageDialog.parent.call( this, config );
3854 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
3856 * MessageDialog.static.name = 'myMessageDialog';
3857 * MessageDialog.static.actions = [
3858 * { action: 'save', label: 'Done', flags: 'primary' },
3859 * { label: 'Cancel', flags: 'safe' }
3862 * MessageDialog.prototype.initialize = function () {
3863 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
3864 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
3865 * 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>' );
3866 * this.$body.append( this.content.$element );
3868 * MessageDialog.prototype.getBodyHeight = function () {
3871 * MessageDialog.prototype.getActionProcess = function ( action ) {
3872 * var dialog = this;
3873 * if ( action === 'save' ) {
3874 * dialog.getActions().get({actions: 'save'})[0].pushPending();
3875 * return new OO.ui.Process()
3877 * .next( function () {
3878 * dialog.getActions().get({actions: 'save'})[0].popPending();
3881 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
3884 * var windowManager = new OO.ui.WindowManager();
3885 * $( 'body' ).append( windowManager.$element );
3887 * var dialog = new MessageDialog();
3888 * windowManager.addWindows( [ dialog ] );
3889 * windowManager.openWindow( dialog );
3895 * @param {Object} [config] Configuration options
3896 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
3898 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
3899 // Configuration initialization
3900 config
= config
|| {};
3904 this.$pending
= null;
3907 this.setPendingElement( config
.$pending
|| this.$element
);
3912 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
3917 * Set the pending element (and clean up any existing one).
3919 * @param {jQuery} $pending The element to set to pending.
3921 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
3922 if ( this.$pending
) {
3923 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3926 this.$pending
= $pending
;
3927 if ( this.pending
> 0 ) {
3928 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3933 * Check if an element is pending.
3935 * @return {boolean} Element is pending
3937 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
3938 return !!this.pending
;
3942 * Increase the pending counter. The pending state will remain active until the counter is zero
3943 * (i.e., the number of calls to #pushPending and #popPending is the same).
3947 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
3948 if ( this.pending
=== 0 ) {
3949 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3950 this.updateThemeClasses();
3958 * Decrease the pending counter. The pending state will remain active until the counter is zero
3959 * (i.e., the number of calls to #pushPending and #popPending is the same).
3963 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
3964 if ( this.pending
=== 1 ) {
3965 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3966 this.updateThemeClasses();
3968 this.pending
= Math
.max( 0, this.pending
- 1 );
3974 * Element that will stick under a specified container, even when it is inserted elsewhere in the
3975 * document (for example, in a OO.ui.Window's $overlay).
3977 * The elements's position is automatically calculated and maintained when window is resized or the
3978 * page is scrolled. If you reposition the container manually, you have to call #position to make
3979 * sure the element is still placed correctly.
3981 * As positioning is only possible when both the element and the container are attached to the DOM
3982 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
3983 * the #toggle method to display a floating popup, for example.
3989 * @param {Object} [config] Configuration options
3990 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
3991 * @cfg {jQuery} [$floatableContainer] Node to position below
3993 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
3994 // Configuration initialization
3995 config
= config
|| {};
3998 this.$floatable
= null;
3999 this.$floatableContainer
= null;
4000 this.$floatableWindow
= null;
4001 this.$floatableClosestScrollable
= null;
4002 this.onFloatableScrollHandler
= this.position
.bind( this );
4003 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4006 this.setFloatableContainer( config
.$floatableContainer
);
4007 this.setFloatableElement( config
.$floatable
|| this.$element
);
4013 * Set floatable element.
4015 * If an element is already set, it will be cleaned up before setting up the new element.
4017 * @param {jQuery} $floatable Element to make floatable
4019 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4020 if ( this.$floatable
) {
4021 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4022 this.$floatable
.css( { left
: '', top
: '' } );
4025 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4030 * Set floatable container.
4032 * The element will be always positioned under the specified container.
4034 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4036 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4037 this.$floatableContainer
= $floatableContainer
;
4038 if ( this.$floatable
) {
4044 * Toggle positioning.
4046 * Do not turn positioning on until after the element is attached to the DOM and visible.
4048 * @param {boolean} [positioning] Enable positioning, omit to toggle
4051 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4052 var closestScrollableOfContainer
;
4054 if ( !this.$floatable
|| !this.$floatableContainer
) {
4058 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4060 if ( this.positioning
!== positioning
) {
4061 this.positioning
= positioning
;
4063 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
4064 this.needsCustomPosition
= !OO
.ui
.contains( this.$floatableContainer
[ 0 ], this.$floatable
[ 0 ] );
4065 // If the scrollable is the root, we have to listen to scroll events
4066 // on the window because of browser inconsistencies.
4067 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4068 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
4071 if ( positioning
) {
4072 this.$floatableWindow
= $( this.getElementWindow() );
4073 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4075 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4076 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4078 // Initial position after visible
4081 if ( this.$floatableWindow
) {
4082 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4083 this.$floatableWindow
= null;
4086 if ( this.$floatableClosestScrollable
) {
4087 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4088 this.$floatableClosestScrollable
= null;
4091 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4099 * Check whether the bottom edge of the given element is within the viewport of the given container.
4102 * @param {jQuery} $element
4103 * @param {jQuery} $container
4106 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4107 var elemRect
, contRect
,
4108 leftEdgeInBounds
= false,
4109 bottomEdgeInBounds
= false,
4110 rightEdgeInBounds
= false;
4112 elemRect
= $element
[ 0 ].getBoundingClientRect();
4113 if ( $container
[ 0 ] === window
) {
4117 right
: document
.documentElement
.clientWidth
,
4118 bottom
: document
.documentElement
.clientHeight
4121 contRect
= $container
[ 0 ].getBoundingClientRect();
4124 // For completeness, if we still cared about topEdgeInBounds, that'd be:
4125 // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
4126 if ( elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
) {
4127 leftEdgeInBounds
= true;
4129 if ( elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
) {
4130 bottomEdgeInBounds
= true;
4132 if ( elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
) {
4133 rightEdgeInBounds
= true;
4136 // We only care that any part of the bottom edge is visible
4137 return bottomEdgeInBounds
&& ( leftEdgeInBounds
|| rightEdgeInBounds
);
4141 * Position the floatable below its container.
4143 * This should only be done when both of them are attached to the DOM and visible.
4147 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4150 if ( !this.positioning
) {
4154 if ( !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
) ) {
4155 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4158 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4161 if ( !this.needsCustomPosition
) {
4165 pos
= OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, this.$floatable
.offsetParent() );
4166 // Position under container
4167 pos
.top
+= this.$floatableContainer
.height();
4168 // In LTR, we position from the left, and pos.left is already set
4169 // In RTL, we position from the right instead.
4170 if ( this.$floatableContainer
.css( 'direction' ) === 'rtl' ) {
4171 pos
.right
= this.$floatable
.offsetParent().width() - pos
.left
- this.$floatableContainer
.outerWidth();
4174 this.$floatable
.css( pos
);
4176 // We updated the position, so re-evaluate the clipping state.
4177 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4178 // will not notice the need to update itself.)
4179 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4180 // it not listen to the right events in the right places?
4189 * Element that can be automatically clipped to visible boundaries.
4191 * Whenever the element's natural height changes, you have to call
4192 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4193 * clipping correctly.
4195 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4196 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4197 * then #$clippable will be given a fixed reduced height and/or width and will be made
4198 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4199 * but you can build a static footer by setting #$clippableContainer to an element that contains
4200 * #$clippable and the footer.
4206 * @param {Object} [config] Configuration options
4207 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4208 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4209 * omit to use #$clippable
4211 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4212 // Configuration initialization
4213 config
= config
|| {};
4216 this.$clippable
= null;
4217 this.$clippableContainer
= null;
4218 this.clipping
= false;
4219 this.clippedHorizontally
= false;
4220 this.clippedVertically
= false;
4221 this.$clippableScrollableContainer
= null;
4222 this.$clippableScroller
= null;
4223 this.$clippableWindow
= null;
4224 this.idealWidth
= null;
4225 this.idealHeight
= null;
4226 this.onClippableScrollHandler
= this.clip
.bind( this );
4227 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4230 if ( config
.$clippableContainer
) {
4231 this.setClippableContainer( config
.$clippableContainer
);
4233 this.setClippableElement( config
.$clippable
|| this.$element
);
4239 * Set clippable element.
4241 * If an element is already set, it will be cleaned up before setting up the new element.
4243 * @param {jQuery} $clippable Element to make clippable
4245 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4246 if ( this.$clippable
) {
4247 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4248 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4249 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4252 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4257 * Set clippable container.
4259 * This is the container that will be measured when deciding whether to clip. When clipping,
4260 * #$clippable will be resized in order to keep the clippable container fully visible.
4262 * If the clippable container is unset, #$clippable will be used.
4264 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4266 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4267 this.$clippableContainer
= $clippableContainer
;
4268 if ( this.$clippable
) {
4276 * Do not turn clipping on until after the element is attached to the DOM and visible.
4278 * @param {boolean} [clipping] Enable clipping, omit to toggle
4281 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4282 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4284 if ( this.clipping
!== clipping
) {
4285 this.clipping
= clipping
;
4287 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4288 // If the clippable container is the root, we have to listen to scroll events and check
4289 // jQuery.scrollTop on the window because of browser inconsistencies
4290 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4291 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4292 this.$clippableScrollableContainer
;
4293 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4294 this.$clippableWindow
= $( this.getElementWindow() )
4295 .on( 'resize', this.onClippableWindowResizeHandler
);
4296 // Initial clip after visible
4299 this.$clippable
.css( {
4307 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4309 this.$clippableScrollableContainer
= null;
4310 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4311 this.$clippableScroller
= null;
4312 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4313 this.$clippableWindow
= null;
4321 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4323 * @return {boolean} Element will be clipped to the visible area
4325 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4326 return this.clipping
;
4330 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4332 * @return {boolean} Part of the element is being clipped
4334 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4335 return this.clippedHorizontally
|| this.clippedVertically
;
4339 * Check if the right of the element is being clipped by the nearest scrollable container.
4341 * @return {boolean} Part of the element is being clipped
4343 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4344 return this.clippedHorizontally
;
4348 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4350 * @return {boolean} Part of the element is being clipped
4352 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4353 return this.clippedVertically
;
4357 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
4359 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4360 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4362 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4363 this.idealWidth
= width
;
4364 this.idealHeight
= height
;
4366 if ( !this.clipping
) {
4367 // Update dimensions
4368 this.$clippable
.css( { width
: width
, height
: height
} );
4370 // While clipping, idealWidth and idealHeight are not considered
4374 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4375 * when the element's natural height changes.
4377 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4378 * overlapped by, the visible area of the nearest scrollable container.
4380 * Because calling clip() when the natural height changes isn't always possible, we also set
4381 * max-height when the element isn't being clipped. This means that if the element tries to grow
4382 * beyond the edge, something reasonable will happen before clip() is called.
4386 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
4387 var $container
, extraHeight
, extraWidth
, ccOffset
,
4388 $scrollableContainer
, scOffset
, scHeight
, scWidth
,
4389 ccWidth
, scrollerIsWindow
, scrollTop
, scrollLeft
,
4390 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
4391 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
4392 buffer
= 7; // Chosen by fair dice roll
4394 if ( !this.clipping
) {
4395 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4399 $container
= this.$clippableContainer
|| this.$clippable
;
4400 extraHeight
= $container
.outerHeight() - this.$clippable
.outerHeight();
4401 extraWidth
= $container
.outerWidth() - this.$clippable
.outerWidth();
4402 ccOffset
= $container
.offset();
4403 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
4404 $scrollableContainer
= this.$clippableWindow
;
4405 scOffset
= { top
: 0, left
: 0 };
4407 $scrollableContainer
= this.$clippableScrollableContainer
;
4408 scOffset
= $scrollableContainer
.offset();
4410 scHeight
= $scrollableContainer
.innerHeight() - buffer
;
4411 scWidth
= $scrollableContainer
.innerWidth() - buffer
;
4412 ccWidth
= $container
.outerWidth() + buffer
;
4413 scrollerIsWindow
= this.$clippableScroller
[ 0 ] === this.$clippableWindow
[ 0 ];
4414 scrollTop
= scrollerIsWindow
? this.$clippableScroller
.scrollTop() : 0;
4415 scrollLeft
= scrollerIsWindow
? this.$clippableScroller
.scrollLeft() : 0;
4416 desiredWidth
= ccOffset
.left
< 0 ?
4417 ccWidth
+ ccOffset
.left
:
4418 ( scOffset
.left
+ scrollLeft
+ scWidth
) - ccOffset
.left
;
4419 desiredHeight
= ( scOffset
.top
+ scrollTop
+ scHeight
) - ccOffset
.top
;
4420 // It should never be desirable to exceed the dimensions of the browser viewport... right?
4421 desiredWidth
= Math
.min( desiredWidth
, document
.documentElement
.clientWidth
);
4422 desiredHeight
= Math
.min( desiredHeight
, document
.documentElement
.clientHeight
);
4423 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
4424 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
4425 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
4426 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
4427 clipWidth
= allotedWidth
< naturalWidth
;
4428 clipHeight
= allotedHeight
< naturalHeight
;
4431 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. (T157672)
4432 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
4433 this.$clippable
.css( 'overflowX', 'scroll' );
4434 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
4435 this.$clippable
.css( {
4436 width
: Math
.max( 0, allotedWidth
),
4440 this.$clippable
.css( {
4442 width
: this.idealWidth
? this.idealWidth
- extraWidth
: '',
4443 maxWidth
: Math
.max( 0, allotedWidth
)
4447 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. (T157672)
4448 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
4449 this.$clippable
.css( 'overflowY', 'scroll' );
4450 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
4451 this.$clippable
.css( {
4452 height
: Math
.max( 0, allotedHeight
),
4456 this.$clippable
.css( {
4458 height
: this.idealHeight
? this.idealHeight
- extraHeight
: '',
4459 maxHeight
: Math
.max( 0, allotedHeight
)
4463 // If we stopped clipping in at least one of the dimensions
4464 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
4465 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4468 this.clippedHorizontally
= clipWidth
;
4469 this.clippedVertically
= clipHeight
;
4475 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4476 * By default, each popup has an anchor that points toward its origin.
4477 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4480 * // A popup widget.
4481 * var popup = new OO.ui.PopupWidget( {
4482 * $content: $( '<p>Hi there!</p>' ),
4487 * $( 'body' ).append( popup.$element );
4488 * // To display the popup, toggle the visibility to 'true'.
4489 * popup.toggle( true );
4491 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4494 * @extends OO.ui.Widget
4495 * @mixins OO.ui.mixin.LabelElement
4496 * @mixins OO.ui.mixin.ClippableElement
4497 * @mixins OO.ui.mixin.FloatableElement
4500 * @param {Object} [config] Configuration options
4501 * @cfg {number} [width=320] Width of popup in pixels
4502 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4503 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4504 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
4505 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
4506 * popup is leaning towards the right of the screen.
4507 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
4508 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
4509 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
4510 * sentence in the given language.
4511 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4512 * See the [OOjs UI docs on MediaWiki][3] for an example.
4513 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4514 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4515 * @cfg {jQuery} [$content] Content to append to the popup's body
4516 * @cfg {jQuery} [$footer] Content to append to the popup's footer
4517 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4518 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4519 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4521 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4522 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
4524 * @cfg {boolean} [padded=false] Add padding to the popup's body
4526 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
4527 // Configuration initialization
4528 config
= config
|| {};
4530 // Parent constructor
4531 OO
.ui
.PopupWidget
.parent
.call( this, config
);
4533 // Properties (must be set before ClippableElement constructor call)
4534 this.$body
= $( '<div>' );
4535 this.$popup
= $( '<div>' );
4537 // Mixin constructors
4538 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4539 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
4540 $clippable
: this.$body
,
4541 $clippableContainer
: this.$popup
4543 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
4546 this.$anchor
= $( '<div>' );
4547 // If undefined, will be computed lazily in updateDimensions()
4548 this.$container
= config
.$container
;
4549 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
4550 this.autoClose
= !!config
.autoClose
;
4551 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
4552 this.transitionTimeout
= null;
4554 this.width
= config
.width
!== undefined ? config
.width
: 320;
4555 this.height
= config
.height
!== undefined ? config
.height
: null;
4556 this.setAlignment( config
.align
);
4557 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
4558 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
4561 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
4562 this.$body
.addClass( 'oo-ui-popupWidget-body' );
4563 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
4565 .addClass( 'oo-ui-popupWidget-popup' )
4566 .append( this.$body
);
4568 .addClass( 'oo-ui-popupWidget' )
4569 .append( this.$popup
, this.$anchor
);
4570 // Move content, which was added to #$element by OO.ui.Widget, to the body
4571 // FIXME This is gross, we should use '$body' or something for the config
4572 if ( config
.$content
instanceof jQuery
) {
4573 this.$body
.append( config
.$content
);
4576 if ( config
.padded
) {
4577 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
4580 if ( config
.head
) {
4581 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
4582 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
4583 this.$head
= $( '<div>' )
4584 .addClass( 'oo-ui-popupWidget-head' )
4585 .append( this.$label
, this.closeButton
.$element
);
4586 this.$popup
.prepend( this.$head
);
4589 if ( config
.$footer
) {
4590 this.$footer
= $( '<div>' )
4591 .addClass( 'oo-ui-popupWidget-footer' )
4592 .append( config
.$footer
);
4593 this.$popup
.append( this.$footer
);
4596 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
4597 // that reference properties not initialized at that time of parent class construction
4598 // TODO: Find a better way to handle post-constructor setup
4599 this.visible
= false;
4600 this.$element
.addClass( 'oo-ui-element-hidden' );
4605 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
4606 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
4607 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
4608 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
4613 * Handles mouse down events.
4616 * @param {MouseEvent} e Mouse down event
4618 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
4621 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
4623 this.toggle( false );
4628 * Bind mouse down listener.
4632 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
4633 // Capture clicks outside popup
4634 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
4638 * Handles close button click events.
4642 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
4643 if ( this.isVisible() ) {
4644 this.toggle( false );
4649 * Unbind mouse down listener.
4653 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
4654 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
4658 * Handles key down events.
4661 * @param {KeyboardEvent} e Key down event
4663 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
4665 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
4668 this.toggle( false );
4670 e
.stopPropagation();
4675 * Bind key down listener.
4679 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
4680 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4684 * Unbind key down listener.
4688 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
4689 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4693 * Show, hide, or toggle the visibility of the anchor.
4695 * @param {boolean} [show] Show anchor, omit to toggle
4697 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
4698 show
= show
=== undefined ? !this.anchored
: !!show
;
4700 if ( this.anchored
!== show
) {
4702 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
4704 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
4706 this.anchored
= show
;
4711 * Check if the anchor is visible.
4713 * @return {boolean} Anchor is visible
4715 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
4722 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
4724 show
= show
=== undefined ? !this.isVisible() : !!show
;
4726 change
= show
!== this.isVisible();
4729 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
4732 this.togglePositioning( show
&& !!this.$floatableContainer
);
4735 if ( this.autoClose
) {
4736 this.bindMouseDownListener();
4737 this.bindKeyDownListener();
4739 this.updateDimensions();
4740 this.toggleClipping( true );
4742 this.toggleClipping( false );
4743 if ( this.autoClose
) {
4744 this.unbindMouseDownListener();
4745 this.unbindKeyDownListener();
4754 * Set the size of the popup.
4756 * Changing the size may also change the popup's position depending on the alignment.
4758 * @param {number} width Width in pixels
4759 * @param {number} height Height in pixels
4760 * @param {boolean} [transition=false] Use a smooth transition
4763 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
4765 this.height
= height
!== undefined ? height
: null;
4766 if ( this.isVisible() ) {
4767 this.updateDimensions( transition
);
4772 * Update the size and position.
4774 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
4775 * be called automatically.
4777 * @param {boolean} [transition=false] Use a smooth transition
4780 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
4781 var popupOffset
, originOffset
, containerLeft
, containerWidth
, containerRight
,
4782 popupLeft
, popupRight
, overlapLeft
, overlapRight
, anchorWidth
, direction
,
4786 'force-left': 'backwards',
4787 'force-right': 'forwards'
4790 'force-left': 'forwards',
4791 'force-right': 'backwards'
4796 if ( !this.$container
) {
4797 // Lazy-initialize $container if not specified in constructor
4798 this.$container
= $( this.getClosestScrollableElementContainer() );
4800 direction
= this.$container
.css( 'direction' );
4801 dirFactor
= direction
=== 'rtl' ? -1 : 1;
4802 align
= alignMap
[ direction
][ this.align
] || this.align
;
4804 // Set height and width before measuring things, since it might cause our measurements
4805 // to change (e.g. due to scrollbars appearing or disappearing)
4808 height
: this.height
!== null ? this.height
: 'auto'
4811 // Compute initial popupOffset based on alignment
4812 popupOffset
= this.width
* ( { backwards
: -1, center
: -0.5, forwards
: 0 } )[ align
];
4814 // Figure out if this will cause the popup to go beyond the edge of the container
4815 originOffset
= this.$element
.offset().left
;
4816 containerLeft
= this.$container
.offset().left
;
4817 containerWidth
= this.$container
.innerWidth();
4818 containerRight
= containerLeft
+ containerWidth
;
4819 popupLeft
= dirFactor
* popupOffset
- this.containerPadding
;
4820 popupRight
= dirFactor
* popupOffset
+ this.containerPadding
+ this.width
+ this.containerPadding
;
4821 overlapLeft
= ( originOffset
+ popupLeft
) - containerLeft
;
4822 overlapRight
= containerRight
- ( originOffset
+ popupRight
);
4824 // Adjust offset to make the popup not go beyond the edge, if needed
4825 if ( overlapRight
< 0 ) {
4826 popupOffset
+= dirFactor
* overlapRight
;
4827 } else if ( overlapLeft
< 0 ) {
4828 popupOffset
-= dirFactor
* overlapLeft
;
4831 // Adjust offset to avoid anchor being rendered too close to the edge
4832 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
4833 // TODO: Find a measurement that works for CSS anchors and image anchors
4834 anchorWidth
= this.$anchor
[ 0 ].scrollWidth
* 2;
4835 if ( popupOffset
+ this.width
< anchorWidth
) {
4836 popupOffset
= anchorWidth
- this.width
;
4837 } else if ( -popupOffset
< anchorWidth
) {
4838 popupOffset
= -anchorWidth
;
4841 // Prevent transition from being interrupted
4842 clearTimeout( this.transitionTimeout
);
4844 // Enable transition
4845 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
4848 // Position body relative to anchor
4849 this.$popup
.css( direction
=== 'rtl' ? 'margin-right' : 'margin-left', popupOffset
);
4852 // Prevent transitioning after transition is complete
4853 this.transitionTimeout
= setTimeout( function () {
4854 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4857 // Prevent transitioning immediately
4858 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4861 // Reevaluate clipping state since we've relocated and resized the popup
4868 * Set popup alignment
4870 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
4871 * `backwards` or `forwards`.
4873 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
4874 // Transform values deprecated since v0.11.0
4875 if ( align
=== 'left' || align
=== 'right' ) {
4876 OO
.ui
.warnDeprecation( 'PopupWidget#setAlignment parameter value `' + align
+ '` is deprecated. Use `force-right` or `force-left` instead.' );
4877 align
= { left
: 'force-right', right
: 'force-left' }[ align
];
4880 // Validate alignment
4881 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
4884 this.align
= 'center';
4889 * Get popup alignment
4891 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4892 * `backwards` or `forwards`.
4894 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
4899 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
4900 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
4901 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
4902 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
4908 * @param {Object} [config] Configuration options
4909 * @cfg {Object} [popup] Configuration to pass to popup
4910 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
4912 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
4913 // Configuration initialization
4914 config
= config
|| {};
4917 this.popup
= new OO
.ui
.PopupWidget( $.extend(
4918 { autoClose
: true },
4920 { $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
) }
4929 * @return {OO.ui.PopupWidget} Popup widget
4931 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
4936 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
4937 * which is used to display additional information or options.
4940 * // Example of a popup button.
4941 * var popupButton = new OO.ui.PopupButtonWidget( {
4942 * label: 'Popup button with options',
4945 * $content: $( '<p>Additional options here.</p>' ),
4947 * align: 'force-left'
4950 * // Append the button to the DOM.
4951 * $( 'body' ).append( popupButton.$element );
4954 * @extends OO.ui.ButtonWidget
4955 * @mixins OO.ui.mixin.PopupElement
4958 * @param {Object} [config] Configuration options
4959 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
4960 * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
4961 * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
4963 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
4964 // Parent constructor
4965 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
4967 // Mixin constructors
4968 OO
.ui
.mixin
.PopupElement
.call( this, $.extend( true, {}, config
, {
4970 $floatableContainer
: this.$element
4975 this.$overlay
= config
.$overlay
|| this.$element
;
4978 this.connect( this, { click
: 'onAction' } );
4982 .addClass( 'oo-ui-popupButtonWidget' )
4983 .attr( 'aria-haspopup', 'true' );
4985 .addClass( 'oo-ui-popupButtonWidget-popup' )
4986 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
4987 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
4988 this.$overlay
.append( this.popup
.$element
);
4993 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
4994 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
4999 * Handle the button action being triggered.
5003 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
5004 this.popup
.toggle();
5008 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
5010 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
5015 * @mixins OO.ui.mixin.GroupElement
5018 * @param {Object} [config] Configuration options
5020 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
5021 // Mixin constructors
5022 OO
.ui
.mixin
.GroupElement
.call( this, config
);
5027 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
5032 * Set the disabled state of the widget.
5034 * This will also update the disabled state of child widgets.
5036 * @param {boolean} disabled Disable widget
5039 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
5043 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
5044 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
5046 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
5048 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5049 this.items
[ i
].updateDisabled();
5057 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
5059 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
5060 * allows bidirectional communication.
5062 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
5070 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
5077 * Check if widget is disabled.
5079 * Checks parent if present, making disabled state inheritable.
5081 * @return {boolean} Widget is disabled
5083 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
5084 return this.disabled
||
5085 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
5089 * Set group element is in.
5091 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
5094 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
5096 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
5097 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
5099 // Initialize item disabled states
5100 this.updateDisabled();
5106 * OptionWidgets are special elements that can be selected and configured with data. The
5107 * data is often unique for each option, but it does not have to be. OptionWidgets are used
5108 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
5109 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
5111 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5114 * @extends OO.ui.Widget
5115 * @mixins OO.ui.mixin.ItemWidget
5116 * @mixins OO.ui.mixin.LabelElement
5117 * @mixins OO.ui.mixin.FlaggedElement
5118 * @mixins OO.ui.mixin.AccessKeyedElement
5121 * @param {Object} [config] Configuration options
5123 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
5124 // Configuration initialization
5125 config
= config
|| {};
5127 // Parent constructor
5128 OO
.ui
.OptionWidget
.parent
.call( this, config
);
5130 // Mixin constructors
5131 OO
.ui
.mixin
.ItemWidget
.call( this );
5132 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5133 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
5134 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
5137 this.selected
= false;
5138 this.highlighted
= false;
5139 this.pressed
= false;
5143 .data( 'oo-ui-optionWidget', this )
5144 // Allow programmatic focussing (and by accesskey), but not tabbing
5145 .attr( 'tabindex', '-1' )
5146 .attr( 'role', 'option' )
5147 .attr( 'aria-selected', 'false' )
5148 .addClass( 'oo-ui-optionWidget' )
5149 .append( this.$label
);
5154 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
5155 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
5156 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
5157 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
5158 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
5160 /* Static Properties */
5163 * Whether this option can be selected. See #setSelected.
5167 * @property {boolean}
5169 OO
.ui
.OptionWidget
.static.selectable
= true;
5172 * Whether this option can be highlighted. See #setHighlighted.
5176 * @property {boolean}
5178 OO
.ui
.OptionWidget
.static.highlightable
= true;
5181 * Whether this option can be pressed. See #setPressed.
5185 * @property {boolean}
5187 OO
.ui
.OptionWidget
.static.pressable
= true;
5190 * Whether this option will be scrolled into view when it is selected.
5194 * @property {boolean}
5196 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
5201 * Check if the option can be selected.
5203 * @return {boolean} Item is selectable
5205 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
5206 return this.constructor.static.selectable
&& !this.isDisabled() && this.isVisible();
5210 * Check if the option can be highlighted. A highlight indicates that the option
5211 * may be selected when a user presses enter or clicks. Disabled items cannot
5214 * @return {boolean} Item is highlightable
5216 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
5217 return this.constructor.static.highlightable
&& !this.isDisabled() && this.isVisible();
5221 * Check if the option can be pressed. The pressed state occurs when a user mouses
5222 * down on an item, but has not yet let go of the mouse.
5224 * @return {boolean} Item is pressable
5226 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
5227 return this.constructor.static.pressable
&& !this.isDisabled() && this.isVisible();
5231 * Check if the option is selected.
5233 * @return {boolean} Item is selected
5235 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
5236 return this.selected
;
5240 * Check if the option is highlighted. A highlight indicates that the
5241 * item may be selected when a user presses enter or clicks.
5243 * @return {boolean} Item is highlighted
5245 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
5246 return this.highlighted
;
5250 * Check if the option is pressed. The pressed state occurs when a user mouses
5251 * down on an item, but has not yet let go of the mouse. The item may appear
5252 * selected, but it will not be selected until the user releases the mouse.
5254 * @return {boolean} Item is pressed
5256 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
5257 return this.pressed
;
5261 * Set the option’s selected state. In general, all modifications to the selection
5262 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
5263 * method instead of this method.
5265 * @param {boolean} [state=false] Select option
5268 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
5269 if ( this.constructor.static.selectable
) {
5270 this.selected
= !!state
;
5272 .toggleClass( 'oo-ui-optionWidget-selected', state
)
5273 .attr( 'aria-selected', state
.toString() );
5274 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
5275 this.scrollElementIntoView();
5277 this.updateThemeClasses();
5283 * Set the option’s highlighted state. In general, all programmatic
5284 * modifications to the highlight should be handled by the
5285 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
5286 * method instead of this method.
5288 * @param {boolean} [state=false] Highlight option
5291 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
5292 if ( this.constructor.static.highlightable
) {
5293 this.highlighted
= !!state
;
5294 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
5295 this.updateThemeClasses();
5301 * Set the option’s pressed state. In general, all
5302 * programmatic modifications to the pressed state should be handled by the
5303 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
5304 * method instead of this method.
5306 * @param {boolean} [state=false] Press option
5309 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
5310 if ( this.constructor.static.pressable
) {
5311 this.pressed
= !!state
;
5312 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
5313 this.updateThemeClasses();
5319 * Get text to match search strings against.
5321 * The default implementation returns the label text, but subclasses
5322 * can override this to provide more complex behavior.
5324 * @return {string|boolean} String to match search string against
5326 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
5327 var label
= this.getLabel();
5328 return typeof label
=== 'string' ? label
: this.$label
.text();
5332 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
5333 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
5334 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
5337 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
5338 * information, please see the [OOjs UI documentation on MediaWiki][1].
5341 * // Example of a select widget with three options
5342 * var select = new OO.ui.SelectWidget( {
5344 * new OO.ui.OptionWidget( {
5346 * label: 'Option One',
5348 * new OO.ui.OptionWidget( {
5350 * label: 'Option Two',
5352 * new OO.ui.OptionWidget( {
5354 * label: 'Option Three',
5358 * $( 'body' ).append( select.$element );
5360 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5364 * @extends OO.ui.Widget
5365 * @mixins OO.ui.mixin.GroupWidget
5368 * @param {Object} [config] Configuration options
5369 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
5370 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
5371 * the [OOjs UI documentation on MediaWiki] [2] for examples.
5372 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5374 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
5375 // Configuration initialization
5376 config
= config
|| {};
5378 // Parent constructor
5379 OO
.ui
.SelectWidget
.parent
.call( this, config
);
5381 // Mixin constructors
5382 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
5385 this.pressed
= false;
5386 this.selecting
= null;
5387 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
5388 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
5389 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
5390 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
5391 this.keyPressBuffer
= '';
5392 this.keyPressBufferTimer
= null;
5393 this.blockMouseOverEvents
= 0;
5396 this.connect( this, {
5400 focusin
: this.onFocus
.bind( this ),
5401 mousedown
: this.onMouseDown
.bind( this ),
5402 mouseover
: this.onMouseOver
.bind( this ),
5403 mouseleave
: this.onMouseLeave
.bind( this )
5408 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
5409 .attr( 'role', 'listbox' );
5410 if ( Array
.isArray( config
.items
) ) {
5411 this.addItems( config
.items
);
5417 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
5418 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
5425 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
5427 * @param {OO.ui.OptionWidget|null} item Highlighted item
5433 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
5434 * pressed state of an option.
5436 * @param {OO.ui.OptionWidget|null} item Pressed item
5442 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
5444 * @param {OO.ui.OptionWidget|null} item Selected item
5449 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
5450 * @param {OO.ui.OptionWidget} item Chosen item
5456 * An `add` event is emitted when options are added to the select with the #addItems method.
5458 * @param {OO.ui.OptionWidget[]} items Added items
5459 * @param {number} index Index of insertion point
5465 * A `remove` event is emitted when options are removed from the select with the #clearItems
5466 * or #removeItems methods.
5468 * @param {OO.ui.OptionWidget[]} items Removed items
5474 * Handle focus events
5477 * @param {jQuery.Event} event
5479 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
5481 if ( event
.target
=== this.$element
[ 0 ] ) {
5482 // This widget was focussed, e.g. by the user tabbing to it.
5483 // The styles for focus state depend on one of the items being selected.
5484 if ( !this.getSelectedItem() ) {
5485 item
= this.getFirstSelectableItem();
5488 // One of the options got focussed (and the event bubbled up here).
5489 // They can't be tabbed to, but they can be activated using accesskeys.
5490 item
= this.getTargetItem( event
);
5494 if ( item
.constructor.static.highlightable
) {
5495 this.highlightItem( item
);
5497 this.selectItem( item
);
5501 if ( event
.target
!== this.$element
[ 0 ] ) {
5502 this.$element
.focus();
5507 * Handle mouse down events.
5510 * @param {jQuery.Event} e Mouse down event
5512 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
5515 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
5516 this.togglePressed( true );
5517 item
= this.getTargetItem( e
);
5518 if ( item
&& item
.isSelectable() ) {
5519 this.pressItem( item
);
5520 this.selecting
= item
;
5521 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
5522 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5529 * Handle mouse up events.
5532 * @param {MouseEvent} e Mouse up event
5534 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
5537 this.togglePressed( false );
5538 if ( !this.selecting
) {
5539 item
= this.getTargetItem( e
);
5540 if ( item
&& item
.isSelectable() ) {
5541 this.selecting
= item
;
5544 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
5545 this.pressItem( null );
5546 this.chooseItem( this.selecting
);
5547 this.selecting
= null;
5550 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
5551 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5557 * Handle mouse move events.
5560 * @param {MouseEvent} e Mouse move event
5562 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
5565 if ( !this.isDisabled() && this.pressed
) {
5566 item
= this.getTargetItem( e
);
5567 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
5568 this.pressItem( item
);
5569 this.selecting
= item
;
5575 * Handle mouse over events.
5578 * @param {jQuery.Event} e Mouse over event
5580 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
5582 if ( this.blockMouseOverEvents
) {
5585 if ( !this.isDisabled() ) {
5586 item
= this.getTargetItem( e
);
5587 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
5593 * Handle mouse leave events.
5596 * @param {jQuery.Event} e Mouse over event
5598 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
5599 if ( !this.isDisabled() ) {
5600 this.highlightItem( null );
5606 * Handle key down events.
5609 * @param {KeyboardEvent} e Key down event
5611 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
5614 currentItem
= this.getHighlightedItem() || this.getSelectedItem();
5616 if ( !this.isDisabled() && this.isVisible() ) {
5617 switch ( e
.keyCode
) {
5618 case OO
.ui
.Keys
.ENTER
:
5619 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5620 // Was only highlighted, now let's select it. No-op if already selected.
5621 this.chooseItem( currentItem
);
5626 case OO
.ui
.Keys
.LEFT
:
5627 this.clearKeyPressBuffer();
5628 nextItem
= this.getRelativeSelectableItem( currentItem
, -1 );
5631 case OO
.ui
.Keys
.DOWN
:
5632 case OO
.ui
.Keys
.RIGHT
:
5633 this.clearKeyPressBuffer();
5634 nextItem
= this.getRelativeSelectableItem( currentItem
, 1 );
5637 case OO
.ui
.Keys
.ESCAPE
:
5638 case OO
.ui
.Keys
.TAB
:
5639 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5640 currentItem
.setHighlighted( false );
5642 this.unbindKeyDownListener();
5643 this.unbindKeyPressListener();
5644 // Don't prevent tabbing away / defocusing
5650 if ( nextItem
.constructor.static.highlightable
) {
5651 this.highlightItem( nextItem
);
5653 this.chooseItem( nextItem
);
5655 this.scrollItemIntoView( nextItem
);
5660 e
.stopPropagation();
5666 * Bind key down listener.
5670 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
5671 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
5675 * Unbind key down listener.
5679 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
5680 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
5684 * Scroll item into view, preventing spurious mouse highlight actions from happening.
5686 * @param {OO.ui.OptionWidget} item Item to scroll into view
5688 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
5690 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
5691 // and around 100-150 ms after it is finished.
5692 this.blockMouseOverEvents
++;
5693 item
.scrollElementIntoView().done( function () {
5694 setTimeout( function () {
5695 widget
.blockMouseOverEvents
--;
5701 * Clear the key-press buffer
5705 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
5706 if ( this.keyPressBufferTimer
) {
5707 clearTimeout( this.keyPressBufferTimer
);
5708 this.keyPressBufferTimer
= null;
5710 this.keyPressBuffer
= '';
5714 * Handle key press events.
5717 * @param {KeyboardEvent} e Key press event
5719 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
5720 var c
, filter
, item
;
5722 if ( !e
.charCode
) {
5723 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
5724 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
5729 if ( String
.fromCodePoint
) {
5730 c
= String
.fromCodePoint( e
.charCode
);
5732 c
= String
.fromCharCode( e
.charCode
);
5735 if ( this.keyPressBufferTimer
) {
5736 clearTimeout( this.keyPressBufferTimer
);
5738 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
5740 item
= this.getHighlightedItem() || this.getSelectedItem();
5742 if ( this.keyPressBuffer
=== c
) {
5743 // Common (if weird) special case: typing "xxxx" will cycle through all
5744 // the items beginning with "x".
5746 item
= this.getRelativeSelectableItem( item
, 1 );
5749 this.keyPressBuffer
+= c
;
5752 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
5753 if ( !item
|| !filter( item
) ) {
5754 item
= this.getRelativeSelectableItem( item
, 1, filter
);
5757 if ( this.isVisible() && item
.constructor.static.highlightable
) {
5758 this.highlightItem( item
);
5760 this.chooseItem( item
);
5762 this.scrollItemIntoView( item
);
5766 e
.stopPropagation();
5770 * Get a matcher for the specific string
5773 * @param {string} s String to match against items
5774 * @param {boolean} [exact=false] Only accept exact matches
5775 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
5777 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
5780 if ( s
.normalize
) {
5783 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
5784 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
5788 re
= new RegExp( re
, 'i' );
5789 return function ( item
) {
5790 var matchText
= item
.getMatchText();
5791 if ( matchText
.normalize
) {
5792 matchText
= matchText
.normalize();
5794 return re
.test( matchText
);
5799 * Bind key press listener.
5803 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
5804 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
5808 * Unbind key down listener.
5810 * If you override this, be sure to call this.clearKeyPressBuffer() from your
5815 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
5816 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
5817 this.clearKeyPressBuffer();
5821 * Visibility change handler
5824 * @param {boolean} visible
5826 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
5828 this.clearKeyPressBuffer();
5833 * Get the closest item to a jQuery.Event.
5836 * @param {jQuery.Event} e
5837 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
5839 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
5840 return $( e
.target
).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
5844 * Get selected item.
5846 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
5848 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
5851 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5852 if ( this.items
[ i
].isSelected() ) {
5853 return this.items
[ i
];
5860 * Get highlighted item.
5862 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
5864 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
5867 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5868 if ( this.items
[ i
].isHighlighted() ) {
5869 return this.items
[ i
];
5876 * Toggle pressed state.
5878 * Press is a state that occurs when a user mouses down on an item, but
5879 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
5880 * until the user releases the mouse.
5882 * @param {boolean} pressed An option is being pressed
5884 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
5885 if ( pressed
=== undefined ) {
5886 pressed
= !this.pressed
;
5888 if ( pressed
!== this.pressed
) {
5890 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
5891 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
5892 this.pressed
= pressed
;
5897 * Highlight an option. If the `item` param is omitted, no options will be highlighted
5898 * and any existing highlight will be removed. The highlight is mutually exclusive.
5900 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
5904 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
5905 var i
, len
, highlighted
,
5908 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5909 highlighted
= this.items
[ i
] === item
;
5910 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
5911 this.items
[ i
].setHighlighted( highlighted
);
5916 this.emit( 'highlight', item
);
5923 * Fetch an item by its label.
5925 * @param {string} label Label of the item to select.
5926 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5927 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
5929 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
5931 len
= this.items
.length
,
5932 filter
= this.getItemMatcher( label
, true );
5934 for ( i
= 0; i
< len
; i
++ ) {
5935 item
= this.items
[ i
];
5936 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5943 filter
= this.getItemMatcher( label
, false );
5944 for ( i
= 0; i
< len
; i
++ ) {
5945 item
= this.items
[ i
];
5946 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5962 * Programmatically select an option by its label. If the item does not exist,
5963 * all options will be deselected.
5965 * @param {string} [label] Label of the item to select.
5966 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5970 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
5971 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
5972 if ( label
=== undefined || !itemFromLabel
) {
5973 return this.selectItem();
5975 return this.selectItem( itemFromLabel
);
5979 * Programmatically select an option by its data. If the `data` parameter is omitted,
5980 * or if the item does not exist, all options will be deselected.
5982 * @param {Object|string} [data] Value of the item to select, omit to deselect all
5986 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
5987 var itemFromData
= this.getItemFromData( data
);
5988 if ( data
=== undefined || !itemFromData
) {
5989 return this.selectItem();
5991 return this.selectItem( itemFromData
);
5995 * Programmatically select an option by its reference. If the `item` parameter is omitted,
5996 * all options will be deselected.
5998 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
6002 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
6003 var i
, len
, selected
,
6006 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6007 selected
= this.items
[ i
] === item
;
6008 if ( this.items
[ i
].isSelected() !== selected
) {
6009 this.items
[ i
].setSelected( selected
);
6014 this.emit( 'select', item
);
6023 * Press is a state that occurs when a user mouses down on an item, but has not
6024 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
6025 * releases the mouse.
6027 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
6031 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
6032 var i
, len
, pressed
,
6035 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6036 pressed
= this.items
[ i
] === item
;
6037 if ( this.items
[ i
].isPressed() !== pressed
) {
6038 this.items
[ i
].setPressed( pressed
);
6043 this.emit( 'press', item
);
6052 * Note that ‘choose’ should never be modified programmatically. A user can choose
6053 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
6054 * use the #selectItem method.
6056 * This method is identical to #selectItem, but may vary in subclasses that take additional action
6057 * when users choose an item with the keyboard or mouse.
6059 * @param {OO.ui.OptionWidget} item Item to choose
6063 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
6065 this.selectItem( item
);
6066 this.emit( 'choose', item
);
6073 * Get an option by its position relative to the specified item (or to the start of the option array,
6074 * if item is `null`). The direction in which to search through the option array is specified with a
6075 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6076 * `null` if there are no options in the array.
6078 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6079 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6080 * @param {Function} [filter] Only consider items for which this function returns
6081 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
6082 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
6084 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
, filter
) {
6085 var currentIndex
, nextIndex
, i
,
6086 increase
= direction
> 0 ? 1 : -1,
6087 len
= this.items
.length
;
6089 if ( item
instanceof OO
.ui
.OptionWidget
) {
6090 currentIndex
= this.items
.indexOf( item
);
6091 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
6093 // If no item is selected and moving forward, start at the beginning.
6094 // If moving backward, start at the end.
6095 nextIndex
= direction
> 0 ? 0 : len
- 1;
6098 for ( i
= 0; i
< len
; i
++ ) {
6099 item
= this.items
[ nextIndex
];
6101 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
6102 ( !filter
|| filter( item
) )
6106 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
6112 * Get the next selectable item or `null` if there are no selectable items.
6113 * Disabled options and menu-section markers and breaks are not selectable.
6115 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
6117 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
6118 return this.getRelativeSelectableItem( null, 1 );
6122 * Add an array of options to the select. Optionally, an index number can be used to
6123 * specify an insertion point.
6125 * @param {OO.ui.OptionWidget[]} items Items to add
6126 * @param {number} [index] Index to insert items after
6130 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
6132 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
6134 // Always provide an index, even if it was omitted
6135 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
6141 * Remove the specified array of options from the select. Options will be detached
6142 * from the DOM, not removed, so they can be reused later. To remove all options from
6143 * the select, you may wish to use the #clearItems method instead.
6145 * @param {OO.ui.OptionWidget[]} items Items to remove
6149 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
6152 // Deselect items being removed
6153 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
6155 if ( item
.isSelected() ) {
6156 this.selectItem( null );
6161 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
6163 this.emit( 'remove', items
);
6169 * Clear all options from the select. Options will be detached from the DOM, not removed,
6170 * so that they can be reused later. To remove a subset of options from the select, use
6171 * the #removeItems method.
6176 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
6177 var items
= this.items
.slice();
6180 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
6183 this.selectItem( null );
6185 this.emit( 'remove', items
);
6191 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
6192 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
6193 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
6194 * options. For more information about options and selects, please see the
6195 * [OOjs UI documentation on MediaWiki][1].
6198 * // Decorated options in a select widget
6199 * var select = new OO.ui.SelectWidget( {
6201 * new OO.ui.DecoratedOptionWidget( {
6203 * label: 'Option with icon',
6206 * new OO.ui.DecoratedOptionWidget( {
6208 * label: 'Option with indicator',
6213 * $( 'body' ).append( select.$element );
6215 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6218 * @extends OO.ui.OptionWidget
6219 * @mixins OO.ui.mixin.IconElement
6220 * @mixins OO.ui.mixin.IndicatorElement
6223 * @param {Object} [config] Configuration options
6225 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
6226 // Parent constructor
6227 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
6229 // Mixin constructors
6230 OO
.ui
.mixin
.IconElement
.call( this, config
);
6231 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
6235 .addClass( 'oo-ui-decoratedOptionWidget' )
6236 .prepend( this.$icon
)
6237 .append( this.$indicator
);
6242 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
6243 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
6244 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
6247 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
6248 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
6249 * the [OOjs UI documentation on MediaWiki] [1] for more information.
6251 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6254 * @extends OO.ui.DecoratedOptionWidget
6257 * @param {Object} [config] Configuration options
6259 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
6260 // Configuration initialization
6261 config
= $.extend( { icon
: 'check' }, config
);
6263 // Parent constructor
6264 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
6268 .attr( 'role', 'menuitem' )
6269 .addClass( 'oo-ui-menuOptionWidget' );
6274 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
6276 /* Static Properties */
6282 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
6285 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
6286 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
6289 * var myDropdown = new OO.ui.DropdownWidget( {
6292 * new OO.ui.MenuSectionOptionWidget( {
6295 * new OO.ui.MenuOptionWidget( {
6297 * label: 'Welsh Corgi'
6299 * new OO.ui.MenuOptionWidget( {
6301 * label: 'Standard Poodle'
6303 * new OO.ui.MenuSectionOptionWidget( {
6306 * new OO.ui.MenuOptionWidget( {
6313 * $( 'body' ).append( myDropdown.$element );
6316 * @extends OO.ui.DecoratedOptionWidget
6319 * @param {Object} [config] Configuration options
6321 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
6322 // Parent constructor
6323 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
6326 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' );
6331 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
6333 /* Static Properties */
6339 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
6345 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
6348 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
6349 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
6350 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
6351 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
6352 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
6353 * and customized to be opened, closed, and displayed as needed.
6355 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
6356 * mouse outside the menu.
6358 * Menus also have support for keyboard interaction:
6360 * - Enter/Return key: choose and select a menu option
6361 * - Up-arrow key: highlight the previous menu option
6362 * - Down-arrow key: highlight the next menu option
6363 * - Esc key: hide the menu
6365 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6366 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6369 * @extends OO.ui.SelectWidget
6370 * @mixins OO.ui.mixin.ClippableElement
6373 * @param {Object} [config] Configuration options
6374 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
6375 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
6376 * and {@link OO.ui.mixin.LookupElement LookupElement}
6377 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
6378 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
6379 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
6380 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
6381 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
6382 * that button, unless the button (or its parent widget) is passed in here.
6383 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
6384 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
6386 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
6387 // Configuration initialization
6388 config
= config
|| {};
6390 // Parent constructor
6391 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
6393 // Mixin constructors
6394 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
6397 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
6398 this.filterFromInput
= !!config
.filterFromInput
;
6399 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
6400 this.$widget
= config
.widget
? config
.widget
.$element
: null;
6401 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
6402 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
6406 .addClass( 'oo-ui-menuSelectWidget' )
6407 .attr( 'role', 'menu' );
6409 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
6410 // that reference properties not initialized at that time of parent class construction
6411 // TODO: Find a better way to handle post-constructor setup
6412 this.visible
= false;
6413 this.$element
.addClass( 'oo-ui-element-hidden' );
6418 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
6419 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
6424 * Handles document mouse down events.
6427 * @param {MouseEvent} e Mouse down event
6429 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
6431 !OO
.ui
.contains( this.$element
[ 0 ], e
.target
, true ) &&
6432 ( !this.$widget
|| !OO
.ui
.contains( this.$widget
[ 0 ], e
.target
, true ) )
6434 this.toggle( false );
6441 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
6442 var currentItem
= this.getHighlightedItem() || this.getSelectedItem();
6444 if ( !this.isDisabled() && this.isVisible() ) {
6445 switch ( e
.keyCode
) {
6446 case OO
.ui
.Keys
.LEFT
:
6447 case OO
.ui
.Keys
.RIGHT
:
6448 // Do nothing if a text field is associated, arrow keys will be handled natively
6449 if ( !this.$input
) {
6450 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
6453 case OO
.ui
.Keys
.ESCAPE
:
6454 case OO
.ui
.Keys
.TAB
:
6455 if ( currentItem
) {
6456 currentItem
.setHighlighted( false );
6458 this.toggle( false );
6459 // Don't prevent tabbing away, prevent defocusing
6460 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
6462 e
.stopPropagation();
6466 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
6473 * Update menu item visibility after input changes.
6477 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
6478 var i
, item
, visible
,
6480 len
= this.items
.length
,
6481 showAll
= !this.isVisible(),
6482 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
6484 for ( i
= 0; i
< len
; i
++ ) {
6485 item
= this.items
[ i
];
6486 if ( item
instanceof OO
.ui
.OptionWidget
) {
6487 visible
= showAll
|| filter( item
);
6488 anyVisible
= anyVisible
|| visible
;
6489 item
.toggle( visible
);
6493 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
6495 // Reevaluate clipping
6502 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
6503 if ( this.$input
) {
6504 this.$input
.on( 'keydown', this.onKeyDownHandler
);
6506 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
6513 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
6514 if ( this.$input
) {
6515 this.$input
.off( 'keydown', this.onKeyDownHandler
);
6517 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
6524 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
6525 if ( this.$input
) {
6526 if ( this.filterFromInput
) {
6527 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6530 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
6537 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
6538 if ( this.$input
) {
6539 if ( this.filterFromInput
) {
6540 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6541 this.updateItemVisibility();
6544 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
6551 * When a user chooses an item, the menu is closed.
6553 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
6554 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
6556 * @param {OO.ui.OptionWidget} item Item to choose
6559 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
6560 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
6561 this.toggle( false );
6568 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
6570 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
6572 // Reevaluate clipping
6581 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
6583 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
6585 // Reevaluate clipping
6594 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
6596 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
6598 // Reevaluate clipping
6607 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
6610 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
6611 change
= visible
!== this.isVisible();
6614 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
6618 this.bindKeyDownListener();
6619 this.bindKeyPressListener();
6621 this.toggleClipping( true );
6623 if ( this.getSelectedItem() ) {
6624 this.getSelectedItem().scrollElementIntoView( { duration
: 0 } );
6628 if ( this.autoHide
) {
6629 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6632 this.unbindKeyDownListener();
6633 this.unbindKeyPressListener();
6634 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6635 this.toggleClipping( false );
6643 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
6644 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
6645 * users can interact with it.
6647 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
6648 * OO.ui.DropdownInputWidget instead.
6651 * // Example: A DropdownWidget with a menu that contains three options
6652 * var dropDown = new OO.ui.DropdownWidget( {
6653 * label: 'Dropdown menu: Select a menu option',
6656 * new OO.ui.MenuOptionWidget( {
6660 * new OO.ui.MenuOptionWidget( {
6664 * new OO.ui.MenuOptionWidget( {
6672 * $( 'body' ).append( dropDown.$element );
6674 * dropDown.getMenu().selectItemByData( 'b' );
6676 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
6678 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
6680 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6683 * @extends OO.ui.Widget
6684 * @mixins OO.ui.mixin.IconElement
6685 * @mixins OO.ui.mixin.IndicatorElement
6686 * @mixins OO.ui.mixin.LabelElement
6687 * @mixins OO.ui.mixin.TitledElement
6688 * @mixins OO.ui.mixin.TabIndexedElement
6691 * @param {Object} [config] Configuration options
6692 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
6693 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
6694 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6695 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
6697 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
6698 // Configuration initialization
6699 config
= $.extend( { indicator
: 'down' }, config
);
6701 // Parent constructor
6702 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
6704 // Properties (must be set before TabIndexedElement constructor call)
6705 this.$handle
= this.$( '<span>' );
6706 this.$overlay
= config
.$overlay
|| this.$element
;
6708 // Mixin constructors
6709 OO
.ui
.mixin
.IconElement
.call( this, config
);
6710 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
6711 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6712 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
6713 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
6716 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend( {
6718 $container
: this.$element
6723 click
: this.onClick
.bind( this ),
6724 keydown
: this.onKeyDown
.bind( this ),
6725 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
6726 keypress
: this.menu
.onKeyPressHandler
,
6727 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
6729 this.menu
.connect( this, {
6730 select
: 'onMenuSelect',
6731 toggle
: 'onMenuToggle'
6736 .addClass( 'oo-ui-dropdownWidget-handle' )
6737 .append( this.$icon
, this.$label
, this.$indicator
);
6739 .addClass( 'oo-ui-dropdownWidget' )
6740 .append( this.$handle
);
6741 this.$overlay
.append( this.menu
.$element
);
6746 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
6747 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
6748 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
6749 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
6750 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
6751 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6758 * @return {OO.ui.MenuSelectWidget} Menu of widget
6760 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
6765 * Handles menu select events.
6768 * @param {OO.ui.MenuOptionWidget} item Selected menu item
6770 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
6774 this.setLabel( null );
6778 selectedLabel
= item
.getLabel();
6780 // If the label is a DOM element, clone it, because setLabel will append() it
6781 if ( selectedLabel
instanceof jQuery
) {
6782 selectedLabel
= selectedLabel
.clone();
6785 this.setLabel( selectedLabel
);
6789 * Handle menu toggle events.
6792 * @param {boolean} isVisible Menu toggle event
6794 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
6795 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
6799 * Handle mouse click events.
6802 * @param {jQuery.Event} e Mouse click event
6804 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
6805 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6812 * Handle key down events.
6815 * @param {jQuery.Event} e Key down event
6817 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
6819 !this.isDisabled() &&
6821 e
.which
=== OO
.ui
.Keys
.ENTER
||
6823 !this.menu
.isVisible() &&
6825 e
.which
=== OO
.ui
.Keys
.SPACE
||
6826 e
.which
=== OO
.ui
.Keys
.UP
||
6827 e
.which
=== OO
.ui
.Keys
.DOWN
6838 * RadioOptionWidget is an option widget that looks like a radio button.
6839 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
6840 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6842 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6845 * @extends OO.ui.OptionWidget
6848 * @param {Object} [config] Configuration options
6850 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
6851 // Configuration initialization
6852 config
= config
|| {};
6854 // Properties (must be done before parent constructor which calls #setDisabled)
6855 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
6857 // Parent constructor
6858 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
6861 // Remove implicit role, we're handling it ourselves
6862 this.radio
.$input
.attr( 'role', 'presentation' );
6864 .addClass( 'oo-ui-radioOptionWidget' )
6865 .attr( 'role', 'radio' )
6866 .attr( 'aria-checked', 'false' )
6867 .removeAttr( 'aria-selected' )
6868 .prepend( this.radio
.$element
);
6873 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
6875 /* Static Properties */
6881 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
6887 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
6893 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
6899 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
6906 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
6907 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
6909 this.radio
.setSelected( state
);
6911 .attr( 'aria-checked', state
.toString() )
6912 .removeAttr( 'aria-selected' );
6920 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
6921 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
6923 this.radio
.setDisabled( this.isDisabled() );
6929 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
6930 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
6931 * an interface for adding, removing and selecting options.
6932 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6934 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
6935 * OO.ui.RadioSelectInputWidget instead.
6938 * // A RadioSelectWidget with RadioOptions.
6939 * var option1 = new OO.ui.RadioOptionWidget( {
6941 * label: 'Selected radio option'
6944 * var option2 = new OO.ui.RadioOptionWidget( {
6946 * label: 'Unselected radio option'
6949 * var radioSelect=new OO.ui.RadioSelectWidget( {
6950 * items: [ option1, option2 ]
6953 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
6954 * radioSelect.selectItem( option1 );
6956 * $( 'body' ).append( radioSelect.$element );
6958 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6962 * @extends OO.ui.SelectWidget
6963 * @mixins OO.ui.mixin.TabIndexedElement
6966 * @param {Object} [config] Configuration options
6968 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
6969 // Parent constructor
6970 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
6972 // Mixin constructors
6973 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
6977 focus
: this.bindKeyDownListener
.bind( this ),
6978 blur
: this.unbindKeyDownListener
.bind( this )
6983 .addClass( 'oo-ui-radioSelectWidget' )
6984 .attr( 'role', 'radiogroup' );
6989 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
6990 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6993 * MultioptionWidgets are special elements that can be selected and configured with data. The
6994 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
6995 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6996 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
6998 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Multioptions
7001 * @extends OO.ui.Widget
7002 * @mixins OO.ui.mixin.ItemWidget
7003 * @mixins OO.ui.mixin.LabelElement
7006 * @param {Object} [config] Configuration options
7007 * @cfg {boolean} [selected=false] Whether the option is initially selected
7009 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
7010 // Configuration initialization
7011 config
= config
|| {};
7013 // Parent constructor
7014 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
7016 // Mixin constructors
7017 OO
.ui
.mixin
.ItemWidget
.call( this );
7018 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7021 this.selected
= null;
7025 .addClass( 'oo-ui-multioptionWidget' )
7026 .append( this.$label
);
7027 this.setSelected( config
.selected
);
7032 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
7033 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
7034 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
7041 * A change event is emitted when the selected state of the option changes.
7043 * @param {boolean} selected Whether the option is now selected
7049 * Check if the option is selected.
7051 * @return {boolean} Item is selected
7053 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
7054 return this.selected
;
7058 * Set the option’s selected state. In general, all modifications to the selection
7059 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
7060 * method instead of this method.
7062 * @param {boolean} [state=false] Select option
7065 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
7067 if ( this.selected
!== state
) {
7068 this.selected
= state
;
7069 this.emit( 'change', state
);
7070 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
7076 * MultiselectWidget allows selecting multiple options from a list.
7078 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
7080 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
7084 * @extends OO.ui.Widget
7085 * @mixins OO.ui.mixin.GroupWidget
7088 * @param {Object} [config] Configuration options
7089 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
7091 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
7092 // Parent constructor
7093 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
7095 // Configuration initialization
7096 config
= config
|| {};
7098 // Mixin constructors
7099 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
7102 this.aggregate( { change
: 'select' } );
7103 // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
7104 // by GroupElement only when items are added/removed
7105 this.connect( this, { select
: [ 'emit', 'change' ] } );
7108 if ( config
.items
) {
7109 this.addItems( config
.items
);
7111 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
7112 this.$element
.addClass( 'oo-ui-multiselectWidget' )
7113 .append( this.$group
);
7118 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
7119 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
7126 * A change event is emitted when the set of items changes, or an item is selected or deselected.
7132 * A select event is emitted when an item is selected or deselected.
7138 * Get options that are selected.
7140 * @return {OO.ui.MultioptionWidget[]} Selected options
7142 OO
.ui
.MultiselectWidget
.prototype.getSelectedItems = function () {
7143 return this.items
.filter( function ( item
) {
7144 return item
.isSelected();
7149 * Get the data of options that are selected.
7151 * @return {Object[]|string[]} Values of selected options
7153 OO
.ui
.MultiselectWidget
.prototype.getSelectedItemsData = function () {
7154 return this.getSelectedItems().map( function ( item
) {
7160 * Select options by reference. Options not mentioned in the `items` array will be deselected.
7162 * @param {OO.ui.MultioptionWidget[]} items Items to select
7165 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
7166 this.items
.forEach( function ( item
) {
7167 var selected
= items
.indexOf( item
) !== -1;
7168 item
.setSelected( selected
);
7174 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
7176 * @param {Object[]|string[]} datas Values of items to select
7179 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
7182 items
= datas
.map( function ( data
) {
7183 return widget
.getItemFromData( data
);
7185 this.selectItems( items
);
7190 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
7191 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
7192 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
7194 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
7197 * @extends OO.ui.MultioptionWidget
7200 * @param {Object} [config] Configuration options
7202 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
7203 // Configuration initialization
7204 config
= config
|| {};
7206 // Properties (must be done before parent constructor which calls #setDisabled)
7207 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
7209 // Parent constructor
7210 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
7213 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
7214 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
7218 .addClass( 'oo-ui-checkboxMultioptionWidget' )
7219 .prepend( this.checkbox
.$element
);
7224 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
7226 /* Static Properties */
7232 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
7237 * Handle checkbox selected state change.
7241 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
7242 this.setSelected( this.checkbox
.isSelected() );
7248 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
7249 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
7250 this.checkbox
.setSelected( state
);
7257 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
7258 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
7259 this.checkbox
.setDisabled( this.isDisabled() );
7266 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
7267 this.checkbox
.focus();
7271 * Handle key down events.
7274 * @param {jQuery.Event} e
7276 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
7278 element
= this.getElementGroup(),
7281 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
7282 nextItem
= element
.getRelativeFocusableItem( this, -1 );
7283 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
7284 nextItem
= element
.getRelativeFocusableItem( this, 1 );
7294 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
7295 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
7296 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
7297 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
7299 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7300 * OO.ui.CheckboxMultiselectInputWidget instead.
7303 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
7304 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
7307 * label: 'Selected checkbox'
7310 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
7312 * label: 'Unselected checkbox'
7315 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
7316 * items: [ option1, option2 ]
7319 * $( 'body' ).append( multiselect.$element );
7321 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
7324 * @extends OO.ui.MultiselectWidget
7327 * @param {Object} [config] Configuration options
7329 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
7330 // Parent constructor
7331 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
7334 this.$lastClicked
= null;
7337 this.$group
.on( 'click', this.onClick
.bind( this ) );
7341 .addClass( 'oo-ui-checkboxMultiselectWidget' );
7346 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
7351 * Get an option by its position relative to the specified item (or to the start of the option array,
7352 * if item is `null`). The direction in which to search through the option array is specified with a
7353 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
7354 * `null` if there are no options in the array.
7356 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
7357 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7358 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
7360 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
7361 var currentIndex
, nextIndex
, i
,
7362 increase
= direction
> 0 ? 1 : -1,
7363 len
= this.items
.length
;
7366 currentIndex
= this.items
.indexOf( item
);
7367 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7369 // If no item is selected and moving forward, start at the beginning.
7370 // If moving backward, start at the end.
7371 nextIndex
= direction
> 0 ? 0 : len
- 1;
7374 for ( i
= 0; i
< len
; i
++ ) {
7375 item
= this.items
[ nextIndex
];
7376 if ( item
&& !item
.isDisabled() ) {
7379 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7385 * Handle click events on checkboxes.
7387 * @param {jQuery.Event} e
7389 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
7390 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
7391 $lastClicked
= this.$lastClicked
,
7392 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
7393 .not( '.oo-ui-widget-disabled' );
7395 // Allow selecting multiple options at once by Shift-clicking them
7396 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
7397 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
7398 lastClickedIndex
= $options
.index( $lastClicked
);
7399 nowClickedIndex
= $options
.index( $nowClicked
);
7400 // If it's the same item, either the user is being silly, or it's a fake event generated by the
7401 // browser. In either case we don't need custom handling.
7402 if ( nowClickedIndex
!== lastClickedIndex
) {
7404 wasSelected
= items
[ nowClickedIndex
].isSelected();
7405 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
7407 // This depends on the DOM order of the items and the order of the .items array being the same.
7408 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
7409 if ( !items
[ i
].isDisabled() ) {
7410 items
[ i
].setSelected( !wasSelected
);
7413 // For the now-clicked element, use immediate timeout to allow the browser to do its own
7414 // handling first, then set our value. The order in which events happen is different for
7415 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
7416 // non-click actions that change the checkboxes.
7418 setTimeout( function () {
7419 if ( !items
[ nowClickedIndex
].isDisabled() ) {
7420 items
[ nowClickedIndex
].setSelected( !wasSelected
);
7426 if ( $nowClicked
.length
) {
7427 this.$lastClicked
= $nowClicked
;
7432 * FloatingMenuSelectWidget is a menu that will stick under a specified
7433 * container, even when it is inserted elsewhere in the document (for example,
7434 * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
7435 * menu from being clipped too aggresively.
7437 * The menu's position is automatically calculated and maintained when the menu
7438 * is toggled or the window is resized.
7440 * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
7443 * @extends OO.ui.MenuSelectWidget
7444 * @mixins OO.ui.mixin.FloatableElement
7447 * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
7448 * Deprecated, omit this parameter and specify `$container` instead.
7449 * @param {Object} [config] Configuration options
7450 * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
7452 OO
.ui
.FloatingMenuSelectWidget
= function OoUiFloatingMenuSelectWidget( inputWidget
, config
) {
7453 // Allow 'inputWidget' parameter and config for backwards compatibility
7454 if ( OO
.isPlainObject( inputWidget
) && config
=== undefined ) {
7455 config
= inputWidget
;
7456 inputWidget
= config
.inputWidget
;
7459 // Configuration initialization
7460 config
= config
|| {};
7462 // Parent constructor
7463 OO
.ui
.FloatingMenuSelectWidget
.parent
.call( this, config
);
7465 // Properties (must be set before mixin constructors)
7466 this.inputWidget
= inputWidget
; // For backwards compatibility
7467 this.$container
= config
.$container
|| this.inputWidget
.$element
;
7469 // Mixins constructors
7470 OO
.ui
.mixin
.FloatableElement
.call( this, $.extend( {}, config
, { $floatableContainer
: this.$container
} ) );
7473 this.$element
.addClass( 'oo-ui-floatingMenuSelectWidget' );
7474 // For backwards compatibility
7475 this.$element
.addClass( 'oo-ui-textInputMenuSelectWidget' );
7480 OO
.inheritClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.MenuSelectWidget
);
7481 OO
.mixinClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7488 OO
.ui
.FloatingMenuSelectWidget
.prototype.toggle = function ( visible
) {
7490 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
7491 change
= visible
!== this.isVisible();
7493 if ( change
&& visible
) {
7494 // Make sure the width is set before the parent method runs.
7495 this.setIdealSize( this.$container
.width() );
7499 // This will call this.clip(), which is nonsensical since we're not positioned yet...
7500 OO
.ui
.FloatingMenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7503 this.togglePositioning( this.isVisible() );
7510 * The old name for the FloatingMenuSelectWidget widget, provided for backwards-compatibility.
7513 * @extends OO.ui.FloatingMenuSelectWidget
7516 * @deprecated since v0.12.5.
7518 OO
.ui
.TextInputMenuSelectWidget
= function OoUiTextInputMenuSelectWidget() {
7519 OO
.ui
.warnDeprecation( 'TextInputMenuSelectWidget is deprecated. Use the FloatingMenuSelectWidget instead.' );
7520 // Parent constructor
7521 OO
.ui
.TextInputMenuSelectWidget
.parent
.apply( this, arguments
);
7524 OO
.inheritClass( OO
.ui
.TextInputMenuSelectWidget
, OO
.ui
.FloatingMenuSelectWidget
);
7527 * Progress bars visually display the status of an operation, such as a download,
7528 * and can be either determinate or indeterminate:
7530 * - **determinate** process bars show the percent of an operation that is complete.
7532 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
7533 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
7534 * not use percentages.
7536 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
7539 * // Examples of determinate and indeterminate progress bars.
7540 * var progressBar1 = new OO.ui.ProgressBarWidget( {
7543 * var progressBar2 = new OO.ui.ProgressBarWidget();
7545 * // Create a FieldsetLayout to layout progress bars
7546 * var fieldset = new OO.ui.FieldsetLayout;
7547 * fieldset.addItems( [
7548 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
7549 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
7551 * $( 'body' ).append( fieldset.$element );
7554 * @extends OO.ui.Widget
7557 * @param {Object} [config] Configuration options
7558 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
7559 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
7560 * By default, the progress bar is indeterminate.
7562 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
7563 // Configuration initialization
7564 config
= config
|| {};
7566 // Parent constructor
7567 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
7570 this.$bar
= $( '<div>' );
7571 this.progress
= null;
7574 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
7575 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
7578 role
: 'progressbar',
7580 'aria-valuemax': 100
7582 .addClass( 'oo-ui-progressBarWidget' )
7583 .append( this.$bar
);
7588 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
7590 /* Static Properties */
7596 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
7601 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
7603 * @return {number|boolean} Progress percent
7605 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
7606 return this.progress
;
7610 * Set the percent of the process completed or `false` for an indeterminate process.
7612 * @param {number|boolean} progress Progress percent or `false` for indeterminate
7614 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
7615 this.progress
= progress
;
7617 if ( progress
!== false ) {
7618 this.$bar
.css( 'width', this.progress
+ '%' );
7619 this.$element
.attr( 'aria-valuenow', this.progress
);
7621 this.$bar
.css( 'width', '' );
7622 this.$element
.removeAttr( 'aria-valuenow' );
7624 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
7628 * InputWidget is the base class for all input widgets, which
7629 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
7630 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
7631 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
7633 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7637 * @extends OO.ui.Widget
7638 * @mixins OO.ui.mixin.FlaggedElement
7639 * @mixins OO.ui.mixin.TabIndexedElement
7640 * @mixins OO.ui.mixin.TitledElement
7641 * @mixins OO.ui.mixin.AccessKeyedElement
7644 * @param {Object} [config] Configuration options
7645 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
7646 * @cfg {string} [value=''] The value of the input.
7647 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
7648 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
7649 * before it is accepted.
7651 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
7652 // Configuration initialization
7653 config
= config
|| {};
7655 // Parent constructor
7656 OO
.ui
.InputWidget
.parent
.call( this, config
);
7659 // See #reusePreInfuseDOM about config.$input
7660 this.$input
= config
.$input
|| this.getInputElement( config
);
7662 this.inputFilter
= config
.inputFilter
;
7664 // Mixin constructors
7665 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
7666 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
7667 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
7668 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
7671 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
7675 .addClass( 'oo-ui-inputWidget-input' )
7676 .attr( 'name', config
.name
)
7677 .prop( 'disabled', this.isDisabled() );
7679 .addClass( 'oo-ui-inputWidget' )
7680 .append( this.$input
);
7681 this.setValue( config
.value
);
7683 this.setDir( config
.dir
);
7689 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
7690 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
7691 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7692 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
7693 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
7695 /* Static Properties */
7701 OO
.ui
.InputWidget
.static.supportsSimpleLabel
= true;
7703 /* Static Methods */
7708 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
7709 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
7710 // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
7711 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
7718 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7719 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7720 if ( config
.$input
&& config
.$input
.length
) {
7721 state
.value
= config
.$input
.val();
7722 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
7723 state
.focus
= config
.$input
.is( ':focus' );
7733 * A change event is emitted when the value of the input changes.
7735 * @param {string} value
7741 * Get input element.
7743 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
7744 * different circumstances. The element must have a `value` property (like form elements).
7747 * @param {Object} config Configuration options
7748 * @return {jQuery} Input element
7750 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
7751 return $( '<input>' );
7755 * Get input element's ID.
7757 * If the element already has an ID then that is returned, otherwise unique ID is
7758 * generated, set on the element, and returned.
7760 * @return {string} The ID of the element
7762 OO
.ui
.InputWidget
.prototype.getInputId = function () {
7763 var id
= this.$input
.attr( 'id' );
7765 if ( id
=== undefined ) {
7766 id
= OO
.ui
.generateElementId();
7767 this.$input
.attr( 'id', id
);
7774 * Handle potentially value-changing events.
7777 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
7779 OO
.ui
.InputWidget
.prototype.onEdit = function () {
7781 if ( !this.isDisabled() ) {
7782 // Allow the stack to clear so the value will be updated
7783 setTimeout( function () {
7784 widget
.setValue( widget
.$input
.val() );
7790 * Get the value of the input.
7792 * @return {string} Input value
7794 OO
.ui
.InputWidget
.prototype.getValue = function () {
7795 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7796 // it, and we won't know unless they're kind enough to trigger a 'change' event.
7797 var value
= this.$input
.val();
7798 if ( this.value
!== value
) {
7799 this.setValue( value
);
7805 * Set the directionality of the input.
7807 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
7810 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
7811 this.$input
.prop( 'dir', dir
);
7816 * Set the value of the input.
7818 * @param {string} value New value
7822 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
7823 value
= this.cleanUpValue( value
);
7824 // Update the DOM if it has changed. Note that with cleanUpValue, it
7825 // is possible for the DOM value to change without this.value changing.
7826 if ( this.$input
.val() !== value
) {
7827 this.$input
.val( value
);
7829 if ( this.value
!== value
) {
7831 this.emit( 'change', this.value
);
7837 * Clean up incoming value.
7839 * Ensures value is a string, and converts undefined and null to empty string.
7842 * @param {string} value Original value
7843 * @return {string} Cleaned up value
7845 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
7846 if ( value
=== undefined || value
=== null ) {
7848 } else if ( this.inputFilter
) {
7849 return this.inputFilter( String( value
) );
7851 return String( value
);
7856 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
7857 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
7860 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
7861 OO
.ui
.warnDeprecation( 'InputWidget: simulateLabelClick() is deprecated.' );
7862 if ( !this.isDisabled() ) {
7863 if ( this.$input
.is( ':checkbox, :radio' ) ) {
7864 this.$input
.click();
7866 if ( this.$input
.is( ':input' ) ) {
7867 this.$input
[ 0 ].focus();
7875 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
7876 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
7877 if ( this.$input
) {
7878 this.$input
.prop( 'disabled', this.isDisabled() );
7888 OO
.ui
.InputWidget
.prototype.focus = function () {
7889 this.$input
[ 0 ].focus();
7898 OO
.ui
.InputWidget
.prototype.blur = function () {
7899 this.$input
[ 0 ].blur();
7906 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
7907 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7908 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
7909 this.setValue( state
.value
);
7911 if ( state
.focus
) {
7917 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
7918 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
7919 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
7920 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
7921 * [OOjs UI documentation on MediaWiki] [1] for more information.
7924 * // A ButtonInputWidget rendered as an HTML button, the default.
7925 * var button = new OO.ui.ButtonInputWidget( {
7926 * label: 'Input button',
7930 * $( 'body' ).append( button.$element );
7932 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
7935 * @extends OO.ui.InputWidget
7936 * @mixins OO.ui.mixin.ButtonElement
7937 * @mixins OO.ui.mixin.IconElement
7938 * @mixins OO.ui.mixin.IndicatorElement
7939 * @mixins OO.ui.mixin.LabelElement
7940 * @mixins OO.ui.mixin.TitledElement
7943 * @param {Object} [config] Configuration options
7944 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
7945 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
7946 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
7947 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
7948 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
7950 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
7951 // Configuration initialization
7952 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
7954 // See InputWidget#reusePreInfuseDOM about config.$input
7955 if ( config
.$input
) {
7956 config
.$input
.empty();
7959 // Properties (must be set before parent constructor, which calls #setValue)
7960 this.useInputTag
= config
.useInputTag
;
7962 // Parent constructor
7963 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
7965 // Mixin constructors
7966 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
7967 OO
.ui
.mixin
.IconElement
.call( this, config
);
7968 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7969 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7970 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
7973 if ( !config
.useInputTag
) {
7974 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
7976 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
7981 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
7982 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
7983 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
7984 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
7985 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
7986 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
7988 /* Static Properties */
7991 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
7992 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
7997 OO
.ui
.ButtonInputWidget
.static.supportsSimpleLabel
= false;
8005 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
8007 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
8008 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
8014 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
8016 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
8017 * text, or `null` for no label
8020 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
8021 if ( typeof label
=== 'function' ) {
8022 label
= OO
.ui
.resolveMsg( label
);
8025 if ( this.useInputTag
) {
8026 // Discard non-plaintext labels
8027 if ( typeof label
!== 'string' ) {
8031 this.$input
.val( label
);
8034 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
8038 * Set the value of the input.
8040 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
8041 * they do not support {@link #value values}.
8043 * @param {string} value New value
8046 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
8047 if ( !this.useInputTag
) {
8048 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
8054 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
8055 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
8056 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
8057 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
8059 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
8062 * // An example of selected, unselected, and disabled checkbox inputs
8063 * var checkbox1=new OO.ui.CheckboxInputWidget( {
8067 * var checkbox2=new OO.ui.CheckboxInputWidget( {
8070 * var checkbox3=new OO.ui.CheckboxInputWidget( {
8074 * // Create a fieldset layout with fields for each checkbox.
8075 * var fieldset = new OO.ui.FieldsetLayout( {
8076 * label: 'Checkboxes'
8078 * fieldset.addItems( [
8079 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
8080 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
8081 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
8083 * $( 'body' ).append( fieldset.$element );
8085 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8088 * @extends OO.ui.InputWidget
8091 * @param {Object} [config] Configuration options
8092 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
8094 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
8095 // Configuration initialization
8096 config
= config
|| {};
8098 // Parent constructor
8099 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
8103 .addClass( 'oo-ui-checkboxInputWidget' )
8104 // Required for pretty styling in MediaWiki theme
8105 .append( $( '<span>' ) );
8106 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
8111 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
8113 /* Static Methods */
8118 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8119 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8120 state
.checked
= config
.$input
.prop( 'checked' );
8130 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
8131 return $( '<input>' ).attr( 'type', 'checkbox' );
8137 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
8139 if ( !this.isDisabled() ) {
8140 // Allow the stack to clear so the value will be updated
8141 setTimeout( function () {
8142 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
8148 * Set selection state of this checkbox.
8150 * @param {boolean} state `true` for selected
8153 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
8155 if ( this.selected
!== state
) {
8156 this.selected
= state
;
8157 this.$input
.prop( 'checked', this.selected
);
8158 this.emit( 'change', this.selected
);
8164 * Check if this checkbox is selected.
8166 * @return {boolean} Checkbox is selected
8168 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
8169 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8170 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8171 var selected
= this.$input
.prop( 'checked' );
8172 if ( this.selected
!== selected
) {
8173 this.setSelected( selected
);
8175 return this.selected
;
8181 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8182 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8183 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
8184 this.setSelected( state
.checked
);
8189 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
8190 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8191 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8192 * more information about input widgets.
8194 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
8195 * are no options. If no `value` configuration option is provided, the first option is selected.
8196 * If you need a state representing no value (no option being selected), use a DropdownWidget.
8198 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
8201 * // Example: A DropdownInputWidget with three options
8202 * var dropdownInput = new OO.ui.DropdownInputWidget( {
8204 * { data: 'a', label: 'First' },
8205 * { data: 'b', label: 'Second'},
8206 * { data: 'c', label: 'Third' }
8209 * $( 'body' ).append( dropdownInput.$element );
8211 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8214 * @extends OO.ui.InputWidget
8215 * @mixins OO.ui.mixin.TitledElement
8218 * @param {Object} [config] Configuration options
8219 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8220 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
8222 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
8223 // Configuration initialization
8224 config
= config
|| {};
8226 // See InputWidget#reusePreInfuseDOM about config.$input
8227 if ( config
.$input
) {
8228 config
.$input
.addClass( 'oo-ui-element-hidden' );
8231 // Properties (must be done before parent constructor which calls #setDisabled)
8232 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
8234 // Parent constructor
8235 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
8237 // Mixin constructors
8238 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8241 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
8244 this.setOptions( config
.options
|| [] );
8246 .addClass( 'oo-ui-dropdownInputWidget' )
8247 .append( this.dropdownWidget
.$element
);
8252 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
8253 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
8261 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
8262 return $( '<input>' ).attr( 'type', 'hidden' );
8266 * Handles menu select events.
8269 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8271 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
8272 this.setValue( item
.getData() );
8278 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
8279 value
= this.cleanUpValue( value
);
8280 this.dropdownWidget
.getMenu().selectItemByData( value
);
8281 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
8288 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
8289 this.dropdownWidget
.setDisabled( state
);
8290 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8295 * Set the options available for this input.
8297 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8300 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
8302 value
= this.getValue(),
8305 // Rebuild the dropdown menu
8306 this.dropdownWidget
.getMenu()
8308 .addItems( options
.map( function ( opt
) {
8309 var optValue
= widget
.cleanUpValue( opt
.data
);
8310 return new OO
.ui
.MenuOptionWidget( {
8312 label
: opt
.label
!== undefined ? opt
.label
: optValue
8316 // Restore the previous value, or reset to something sensible
8317 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
8318 // Previous value is still available, ensure consistency with the dropdown
8319 this.setValue( value
);
8321 // No longer valid, reset
8322 if ( options
.length
) {
8323 this.setValue( options
[ 0 ].data
);
8333 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
8334 this.dropdownWidget
.getMenu().toggle( true );
8341 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
8342 this.dropdownWidget
.getMenu().toggle( false );
8347 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
8348 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
8349 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
8350 * please see the [OOjs UI documentation on MediaWiki][1].
8352 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
8355 * // An example of selected, unselected, and disabled radio inputs
8356 * var radio1 = new OO.ui.RadioInputWidget( {
8360 * var radio2 = new OO.ui.RadioInputWidget( {
8363 * var radio3 = new OO.ui.RadioInputWidget( {
8367 * // Create a fieldset layout with fields for each radio button.
8368 * var fieldset = new OO.ui.FieldsetLayout( {
8369 * label: 'Radio inputs'
8371 * fieldset.addItems( [
8372 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
8373 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
8374 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
8376 * $( 'body' ).append( fieldset.$element );
8378 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8381 * @extends OO.ui.InputWidget
8384 * @param {Object} [config] Configuration options
8385 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
8387 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
8388 // Configuration initialization
8389 config
= config
|| {};
8391 // Parent constructor
8392 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
8396 .addClass( 'oo-ui-radioInputWidget' )
8397 // Required for pretty styling in MediaWiki theme
8398 .append( $( '<span>' ) );
8399 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
8404 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
8406 /* Static Methods */
8411 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8412 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8413 state
.checked
= config
.$input
.prop( 'checked' );
8423 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
8424 return $( '<input>' ).attr( 'type', 'radio' );
8430 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
8431 // RadioInputWidget doesn't track its state.
8435 * Set selection state of this radio button.
8437 * @param {boolean} state `true` for selected
8440 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
8441 // RadioInputWidget doesn't track its state.
8442 this.$input
.prop( 'checked', state
);
8447 * Check if this radio button is selected.
8449 * @return {boolean} Radio is selected
8451 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
8452 return this.$input
.prop( 'checked' );
8458 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8459 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8460 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
8461 this.setSelected( state
.checked
);
8466 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
8467 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8468 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8469 * more information about input widgets.
8471 * This and OO.ui.DropdownInputWidget support the same configuration options.
8474 * // Example: A RadioSelectInputWidget with three options
8475 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
8477 * { data: 'a', label: 'First' },
8478 * { data: 'b', label: 'Second'},
8479 * { data: 'c', label: 'Third' }
8482 * $( 'body' ).append( radioSelectInput.$element );
8484 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8487 * @extends OO.ui.InputWidget
8490 * @param {Object} [config] Configuration options
8491 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8493 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
8494 // Configuration initialization
8495 config
= config
|| {};
8497 // Properties (must be done before parent constructor which calls #setDisabled)
8498 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
8500 // Parent constructor
8501 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
8504 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
8507 this.setOptions( config
.options
|| [] );
8509 .addClass( 'oo-ui-radioSelectInputWidget' )
8510 .append( this.radioSelectWidget
.$element
);
8515 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
8517 /* Static Properties */
8523 OO
.ui
.RadioSelectInputWidget
.static.supportsSimpleLabel
= false;
8525 /* Static Methods */
8530 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8531 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8532 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
8539 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8540 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8541 // Cannot reuse the `<input type=radio>` set
8542 delete config
.$input
;
8552 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
8553 return $( '<input>' ).attr( 'type', 'hidden' );
8557 * Handles menu select events.
8560 * @param {OO.ui.RadioOptionWidget} item Selected menu item
8562 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
8563 this.setValue( item
.getData() );
8569 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
8570 value
= this.cleanUpValue( value
);
8571 this.radioSelectWidget
.selectItemByData( value
);
8572 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
8579 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
8580 this.radioSelectWidget
.setDisabled( state
);
8581 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8586 * Set the options available for this input.
8588 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8591 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
8593 value
= this.getValue(),
8596 // Rebuild the radioSelect menu
8597 this.radioSelectWidget
8599 .addItems( options
.map( function ( opt
) {
8600 var optValue
= widget
.cleanUpValue( opt
.data
);
8601 return new OO
.ui
.RadioOptionWidget( {
8603 label
: opt
.label
!== undefined ? opt
.label
: optValue
8607 // Restore the previous value, or reset to something sensible
8608 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
8609 // Previous value is still available, ensure consistency with the radioSelect
8610 this.setValue( value
);
8612 // No longer valid, reset
8613 if ( options
.length
) {
8614 this.setValue( options
[ 0 ].data
);
8622 * CheckboxMultiselectInputWidget is a
8623 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
8624 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
8625 * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on MediaWiki][1] for
8626 * more information about input widgets.
8629 * // Example: A CheckboxMultiselectInputWidget with three options
8630 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
8632 * { data: 'a', label: 'First' },
8633 * { data: 'b', label: 'Second'},
8634 * { data: 'c', label: 'Third' }
8637 * $( 'body' ).append( multiselectInput.$element );
8639 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8642 * @extends OO.ui.InputWidget
8645 * @param {Object} [config] Configuration options
8646 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
8648 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
8649 // Configuration initialization
8650 config
= config
|| {};
8652 // Properties (must be done before parent constructor which calls #setDisabled)
8653 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
8655 // Parent constructor
8656 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
8659 this.inputName
= config
.name
;
8663 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
8664 .append( this.checkboxMultiselectWidget
.$element
);
8665 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
8666 this.$input
.detach();
8667 this.setOptions( config
.options
|| [] );
8668 // Have to repeat this from parent, as we need options to be set up for this to make sense
8669 this.setValue( config
.value
);
8674 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
8676 /* Static Properties */
8682 OO
.ui
.CheckboxMultiselectInputWidget
.static.supportsSimpleLabel
= false;
8684 /* Static Methods */
8689 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8690 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8691 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
8692 .toArray().map( function ( el
) { return el
.value
; } );
8699 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8700 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8701 // Cannot reuse the `<input type=checkbox>` set
8702 delete config
.$input
;
8712 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
8714 return $( '<div>' );
8720 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
8721 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
8722 .toArray().map( function ( el
) { return el
.value
; } );
8723 if ( this.value
!== value
) {
8724 this.setValue( value
);
8732 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
8733 value
= this.cleanUpValue( value
);
8734 this.checkboxMultiselectWidget
.selectItemsByData( value
);
8735 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
8740 * Clean up incoming value.
8742 * @param {string[]} value Original value
8743 * @return {string[]} Cleaned up value
8745 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
8748 if ( !Array
.isArray( value
) ) {
8751 for ( i
= 0; i
< value
.length
; i
++ ) {
8753 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
8754 // Remove options that we don't have here
8755 if ( !this.checkboxMultiselectWidget
.getItemFromData( singleValue
) ) {
8758 cleanValue
.push( singleValue
);
8766 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
8767 this.checkboxMultiselectWidget
.setDisabled( state
);
8768 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8773 * Set the options available for this input.
8775 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
8778 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
8781 // Rebuild the checkboxMultiselectWidget menu
8782 this.checkboxMultiselectWidget
8784 .addItems( options
.map( function ( opt
) {
8785 var optValue
, item
, optDisabled
;
8787 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
8788 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
8789 item
= new OO
.ui
.CheckboxMultioptionWidget( {
8791 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
8792 disabled
: optDisabled
8794 // Set the 'name' and 'value' for form submission
8795 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
8796 item
.checkbox
.setValue( optValue
);
8800 // Re-set the value, checking the checkboxes as needed.
8801 // This will also get rid of any stale options that we just removed.
8802 this.setValue( this.getValue() );
8808 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
8809 * size of the field as well as its presentation. In addition, these widgets can be configured
8810 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
8811 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
8812 * which modifies incoming values rather than validating them.
8813 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8815 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
8818 * // Example of a text input widget
8819 * var textInput = new OO.ui.TextInputWidget( {
8820 * value: 'Text input'
8822 * $( 'body' ).append( textInput.$element );
8824 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8827 * @extends OO.ui.InputWidget
8828 * @mixins OO.ui.mixin.IconElement
8829 * @mixins OO.ui.mixin.IndicatorElement
8830 * @mixins OO.ui.mixin.PendingElement
8831 * @mixins OO.ui.mixin.LabelElement
8834 * @param {Object} [config] Configuration options
8835 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
8836 * 'email', 'url', 'date', 'month' or 'number'. Ignored if `multiline` is true.
8838 * Some values of `type` result in additional behaviors:
8840 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
8841 * empties the text field
8842 * @cfg {string} [placeholder] Placeholder text
8843 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
8844 * instruct the browser to focus this widget.
8845 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
8846 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
8847 * @cfg {boolean} [multiline=false] Allow multiple lines of text
8848 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
8849 * specifies minimum number of rows to display.
8850 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
8851 * Use the #maxRows config to specify a maximum number of displayed rows.
8852 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
8853 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
8854 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
8855 * the value or placeholder text: `'before'` or `'after'`
8856 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
8857 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
8858 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
8859 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
8860 * (the value must contain only numbers); when RegExp, a regular expression that must match the
8861 * value for it to be considered valid; when Function, a function receiving the value as parameter
8862 * that must return true, or promise resolving to true, for it to be considered valid.
8864 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
8865 // Configuration initialization
8866 config
= $.extend( {
8868 labelPosition
: 'after'
8871 if ( config
.type
=== 'search' ) {
8872 OO
.ui
.warnDeprecation( 'TextInputWidget: config.type=\'search\' is deprecated. Use the SearchInputWidget instead. See T148471 for details.' );
8873 if ( config
.icon
=== undefined ) {
8874 config
.icon
= 'search';
8876 // indicator: 'clear' is set dynamically later, depending on value
8879 // Parent constructor
8880 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
8882 // Mixin constructors
8883 OO
.ui
.mixin
.IconElement
.call( this, config
);
8884 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8885 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
8886 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8889 this.type
= this.getSaneType( config
);
8890 this.readOnly
= false;
8891 this.required
= false;
8892 this.multiline
= !!config
.multiline
;
8893 this.autosize
= !!config
.autosize
;
8894 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
8895 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
8896 this.validate
= null;
8897 this.styleHeight
= null;
8898 this.scrollWidth
= null;
8900 // Clone for resizing
8901 if ( this.autosize
) {
8902 this.$clone
= this.$input
8904 .insertAfter( this.$input
)
8905 .attr( 'aria-hidden', 'true' )
8906 .addClass( 'oo-ui-element-hidden' );
8909 this.setValidation( config
.validate
);
8910 this.setLabelPosition( config
.labelPosition
);
8914 keypress
: this.onKeyPress
.bind( this ),
8915 blur
: this.onBlur
.bind( this ),
8916 focus
: this.onFocus
.bind( this )
8918 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
8919 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
8920 this.on( 'labelChange', this.updatePosition
.bind( this ) );
8921 this.connect( this, {
8923 disable
: 'onDisable'
8925 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
8929 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
8930 .append( this.$icon
, this.$indicator
);
8931 this.setReadOnly( !!config
.readOnly
);
8932 this.setRequired( !!config
.required
);
8933 this.updateSearchIndicator();
8934 if ( config
.placeholder
!== undefined ) {
8935 this.$input
.attr( 'placeholder', config
.placeholder
);
8937 if ( config
.maxLength
!== undefined ) {
8938 this.$input
.attr( 'maxlength', config
.maxLength
);
8940 if ( config
.autofocus
) {
8941 this.$input
.attr( 'autofocus', 'autofocus' );
8943 if ( config
.autocomplete
=== false ) {
8944 this.$input
.attr( 'autocomplete', 'off' );
8945 // Turning off autocompletion also disables "form caching" when the user navigates to a
8946 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
8948 beforeunload: function () {
8949 this.$input
.removeAttr( 'autocomplete' );
8951 pageshow: function () {
8952 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
8953 // whole page... it shouldn't hurt, though.
8954 this.$input
.attr( 'autocomplete', 'off' );
8958 if ( this.multiline
&& config
.rows
) {
8959 this.$input
.attr( 'rows', config
.rows
);
8961 if ( this.label
|| config
.autosize
) {
8962 this.isWaitingToBeAttached
= true;
8963 this.installParentChangeDetector();
8969 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
8970 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
8971 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
8972 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
8973 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
8975 /* Static Properties */
8977 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
8982 /* Static Methods */
8987 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8988 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8989 if ( config
.multiline
) {
8990 state
.scrollTop
= config
.$input
.scrollTop();
8998 * An `enter` event is emitted when the user presses 'enter' inside the text box.
9000 * Not emitted if the input is multiline.
9006 * A `resize` event is emitted when autosize is set and the widget resizes
9014 * Handle icon mouse down events.
9017 * @param {jQuery.Event} e Mouse down event
9019 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
9020 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9021 this.$input
[ 0 ].focus();
9027 * Handle indicator mouse down events.
9030 * @param {jQuery.Event} e Mouse down event
9032 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
9033 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9034 if ( this.type
=== 'search' ) {
9035 // Clear the text field
9036 this.setValue( '' );
9038 this.$input
[ 0 ].focus();
9044 * Handle key press events.
9047 * @param {jQuery.Event} e Key press event
9048 * @fires enter If enter key is pressed and input is not multiline
9050 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
9051 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
9052 this.emit( 'enter', e
);
9057 * Handle blur events.
9060 * @param {jQuery.Event} e Blur event
9062 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
9063 this.setValidityFlag();
9067 * Handle focus events.
9070 * @param {jQuery.Event} e Focus event
9072 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
9073 if ( this.isWaitingToBeAttached
) {
9074 // If we've received focus, then we must be attached to the document, and if
9075 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
9076 this.onElementAttach();
9078 this.setValidityFlag( true );
9082 * Handle element attach events.
9085 * @param {jQuery.Event} e Element attach event
9087 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
9088 this.isWaitingToBeAttached
= false;
9089 // Any previously calculated size is now probably invalid if we reattached elsewhere
9090 this.valCache
= null;
9092 this.positionLabel();
9096 * Handle change events.
9098 * @param {string} value
9101 OO
.ui
.TextInputWidget
.prototype.onChange = function () {
9102 this.updateSearchIndicator();
9107 * Handle debounced change events.
9109 * @param {string} value
9112 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
9113 this.setValidityFlag();
9117 * Handle disable events.
9119 * @param {boolean} disabled Element is disabled
9122 OO
.ui
.TextInputWidget
.prototype.onDisable = function () {
9123 this.updateSearchIndicator();
9127 * Check if the input is {@link #readOnly read-only}.
9131 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
9132 return this.readOnly
;
9136 * Set the {@link #readOnly read-only} state of the input.
9138 * @param {boolean} state Make input read-only
9141 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
9142 this.readOnly
= !!state
;
9143 this.$input
.prop( 'readOnly', this.readOnly
);
9144 this.updateSearchIndicator();
9149 * Check if the input is {@link #required required}.
9153 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
9154 return this.required
;
9158 * Set the {@link #required required} state of the input.
9160 * @param {boolean} state Make input required
9163 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
9164 this.required
= !!state
;
9165 if ( this.required
) {
9167 .attr( 'required', 'required' )
9168 .attr( 'aria-required', 'true' );
9169 if ( this.getIndicator() === null ) {
9170 this.setIndicator( 'required' );
9174 .removeAttr( 'required' )
9175 .removeAttr( 'aria-required' );
9176 if ( this.getIndicator() === 'required' ) {
9177 this.setIndicator( null );
9180 this.updateSearchIndicator();
9185 * Support function for making #onElementAttach work across browsers.
9187 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
9188 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
9190 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
9191 * first time that the element gets attached to the documented.
9193 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
9194 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
9195 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
9198 if ( MutationObserver
) {
9199 // The new way. If only it wasn't so ugly.
9201 if ( this.isElementAttached() ) {
9202 // Widget is attached already, do nothing. This breaks the functionality of this function when
9203 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
9204 // would require observation of the whole document, which would hurt performance of other,
9205 // more important code.
9209 // Find topmost node in the tree
9210 topmostNode
= this.$element
[ 0 ];
9211 while ( topmostNode
.parentNode
) {
9212 topmostNode
= topmostNode
.parentNode
;
9215 // We have no way to detect the $element being attached somewhere without observing the entire
9216 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
9217 // parent node of $element, and instead detect when $element is removed from it (and thus
9218 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
9219 // doesn't get attached, we end up back here and create the parent.
9221 mutationObserver
= new MutationObserver( function ( mutations
) {
9222 var i
, j
, removedNodes
;
9223 for ( i
= 0; i
< mutations
.length
; i
++ ) {
9224 removedNodes
= mutations
[ i
].removedNodes
;
9225 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
9226 if ( removedNodes
[ j
] === topmostNode
) {
9227 setTimeout( onRemove
, 0 );
9234 onRemove = function () {
9235 // If the node was attached somewhere else, report it
9236 if ( widget
.isElementAttached() ) {
9237 widget
.onElementAttach();
9239 mutationObserver
.disconnect();
9240 widget
.installParentChangeDetector();
9243 // Create a fake parent and observe it
9244 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
9245 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
9247 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
9248 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
9249 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
9254 * Automatically adjust the size of the text input.
9256 * This only affects #multiline inputs that are {@link #autosize autosized}.
9261 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
9262 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
9263 idealHeight
, newHeight
, scrollWidth
, property
;
9265 if ( this.isWaitingToBeAttached
) {
9266 // #onElementAttach will be called soon, which calls this method
9270 if ( this.multiline
&& this.$input
.val() !== this.valCache
) {
9271 if ( this.autosize
) {
9273 .val( this.$input
.val() )
9274 .attr( 'rows', this.minRows
)
9275 // Set inline height property to 0 to measure scroll height
9276 .css( 'height', 0 );
9278 this.$clone
.removeClass( 'oo-ui-element-hidden' );
9280 this.valCache
= this.$input
.val();
9282 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
9284 // Remove inline height property to measure natural heights
9285 this.$clone
.css( 'height', '' );
9286 innerHeight
= this.$clone
.innerHeight();
9287 outerHeight
= this.$clone
.outerHeight();
9289 // Measure max rows height
9291 .attr( 'rows', this.maxRows
)
9292 .css( 'height', 'auto' )
9294 maxInnerHeight
= this.$clone
.innerHeight();
9296 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
9297 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
9298 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
9299 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
9301 this.$clone
.addClass( 'oo-ui-element-hidden' );
9303 // Only apply inline height when expansion beyond natural height is needed
9304 // Use the difference between the inner and outer height as a buffer
9305 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
9306 if ( newHeight
!== this.styleHeight
) {
9307 this.$input
.css( 'height', newHeight
);
9308 this.styleHeight
= newHeight
;
9309 this.emit( 'resize' );
9312 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
9313 if ( scrollWidth
!== this.scrollWidth
) {
9314 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
9316 this.$label
.css( { right
: '', left
: '' } );
9317 this.$indicator
.css( { right
: '', left
: '' } );
9319 if ( scrollWidth
) {
9320 this.$indicator
.css( property
, scrollWidth
);
9321 if ( this.labelPosition
=== 'after' ) {
9322 this.$label
.css( property
, scrollWidth
);
9326 this.scrollWidth
= scrollWidth
;
9327 this.positionLabel();
9337 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
9338 if ( config
.multiline
) {
9339 return $( '<textarea>' );
9340 } else if ( this.getSaneType( config
) === 'number' ) {
9341 return $( '<input>' )
9342 .attr( 'step', 'any' )
9343 .attr( 'type', 'number' );
9345 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
9350 * Get sanitized value for 'type' for given config.
9352 * @param {Object} config Configuration options
9353 * @return {string|null}
9356 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
9357 var allowedTypes
= [
9367 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
9371 * Check if the input supports multiple lines.
9375 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
9376 return !!this.multiline
;
9380 * Check if the input automatically adjusts its size.
9384 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
9385 return !!this.autosize
;
9389 * Focus the input and select a specified range within the text.
9391 * @param {number} from Select from offset
9392 * @param {number} [to] Select to offset, defaults to from
9395 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
9396 var isBackwards
, start
, end
,
9397 input
= this.$input
[ 0 ];
9401 isBackwards
= to
< from;
9402 start
= isBackwards
? to
: from;
9403 end
= isBackwards
? from : to
;
9408 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
9410 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
9411 // Rather than expensively check if the input is attached every time, just check
9412 // if it was the cause of an error being thrown. If not, rethrow the error.
9413 if ( this.getElementDocument().body
.contains( input
) ) {
9421 * Get an object describing the current selection range in a directional manner
9423 * @return {Object} Object containing 'from' and 'to' offsets
9425 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
9426 var input
= this.$input
[ 0 ],
9427 start
= input
.selectionStart
,
9428 end
= input
.selectionEnd
,
9429 isBackwards
= input
.selectionDirection
=== 'backward';
9432 from: isBackwards
? end
: start
,
9433 to
: isBackwards
? start
: end
9438 * Get the length of the text input value.
9440 * This could differ from the length of #getValue if the
9441 * value gets filtered
9443 * @return {number} Input length
9445 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
9446 return this.$input
[ 0 ].value
.length
;
9450 * Focus the input and select the entire text.
9454 OO
.ui
.TextInputWidget
.prototype.select = function () {
9455 return this.selectRange( 0, this.getInputLength() );
9459 * Focus the input and move the cursor to the start.
9463 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
9464 return this.selectRange( 0 );
9468 * Focus the input and move the cursor to the end.
9472 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
9473 return this.selectRange( this.getInputLength() );
9477 * Insert new content into the input.
9479 * @param {string} content Content to be inserted
9482 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
9484 range
= this.getRange(),
9485 value
= this.getValue();
9487 start
= Math
.min( range
.from, range
.to
);
9488 end
= Math
.max( range
.from, range
.to
);
9490 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
9491 this.selectRange( start
+ content
.length
);
9496 * Insert new content either side of a selection.
9498 * @param {string} pre Content to be inserted before the selection
9499 * @param {string} post Content to be inserted after the selection
9502 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
9504 range
= this.getRange(),
9505 offset
= pre
.length
;
9507 start
= Math
.min( range
.from, range
.to
);
9508 end
= Math
.max( range
.from, range
.to
);
9510 this.selectRange( start
).insertContent( pre
);
9511 this.selectRange( offset
+ end
).insertContent( post
);
9513 this.selectRange( offset
+ start
, offset
+ end
);
9518 * Set the validation pattern.
9520 * The validation pattern is either a regular expression, a function, or the symbolic name of a
9521 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
9522 * value must contain only numbers).
9524 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
9525 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
9527 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
9528 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
9529 this.validate
= validate
;
9531 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
9536 * Sets the 'invalid' flag appropriately.
9538 * @param {boolean} [isValid] Optionally override validation result
9540 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
9542 setFlag = function ( valid
) {
9544 widget
.$input
.attr( 'aria-invalid', 'true' );
9546 widget
.$input
.removeAttr( 'aria-invalid' );
9548 widget
.setFlags( { invalid
: !valid
} );
9551 if ( isValid
!== undefined ) {
9554 this.getValidity().then( function () {
9563 * Get the validity of current value.
9565 * This method returns a promise that resolves if the value is valid and rejects if
9566 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
9568 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
9570 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
9573 function rejectOrResolve( valid
) {
9575 return $.Deferred().resolve().promise();
9577 return $.Deferred().reject().promise();
9581 // Check browser validity and reject if it is invalid
9583 this.$input
[ 0 ].checkValidity
!== undefined &&
9584 this.$input
[ 0 ].checkValidity() === false
9586 return rejectOrResolve( false );
9589 // Run our checks if the browser thinks the field is valid
9590 if ( this.validate
instanceof Function
) {
9591 result
= this.validate( this.getValue() );
9592 if ( result
&& $.isFunction( result
.promise
) ) {
9593 return result
.promise().then( function ( valid
) {
9594 return rejectOrResolve( valid
);
9597 return rejectOrResolve( result
);
9600 return rejectOrResolve( this.getValue().match( this.validate
) );
9605 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
9607 * @param {string} labelPosition Label position, 'before' or 'after'
9610 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
9611 this.labelPosition
= labelPosition
;
9613 // If there is no label and we only change the position, #updatePosition is a no-op,
9614 // but it takes really a lot of work to do nothing.
9615 this.updatePosition();
9621 * Update the position of the inline label.
9623 * This method is called by #setLabelPosition, and can also be called on its own if
9624 * something causes the label to be mispositioned.
9628 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
9629 var after
= this.labelPosition
=== 'after';
9632 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
9633 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
9635 this.valCache
= null;
9636 this.scrollWidth
= null;
9638 this.positionLabel();
9644 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
9645 * already empty or when it's not editable.
9647 OO
.ui
.TextInputWidget
.prototype.updateSearchIndicator = function () {
9648 if ( this.type
=== 'search' ) {
9649 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
9650 this.setIndicator( null );
9652 this.setIndicator( 'clear' );
9658 * Position the label by setting the correct padding on the input.
9663 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
9664 var after
, rtl
, property
;
9666 if ( this.isWaitingToBeAttached
) {
9667 // #onElementAttach will be called soon, which calls this method
9673 // Clear old values if present
9675 'padding-right': '',
9680 this.$element
.append( this.$label
);
9682 this.$label
.detach();
9686 after
= this.labelPosition
=== 'after';
9687 rtl
= this.$element
.css( 'direction' ) === 'rtl';
9688 property
= after
=== rtl
? 'padding-left' : 'padding-right';
9690 this.$input
.css( property
, this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 ) );
9698 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9699 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9700 if ( state
.scrollTop
!== undefined ) {
9701 this.$input
.scrollTop( state
.scrollTop
);
9707 * @extends OO.ui.TextInputWidget
9710 * @param {Object} [config] Configuration options
9712 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
9713 config
= $.extend( {
9717 // Set type to text so that TextInputWidget doesn't
9718 // get stuck in an infinite loop.
9719 config
.type
= 'text';
9721 // Parent constructor
9722 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
9725 this.$element
.addClass( 'oo-ui-textInputWidget-type-search' );
9726 this.updateSearchIndicator();
9727 this.connect( this, {
9728 disable
: 'onDisable'
9734 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
9742 OO
.ui
.SearchInputWidget
.prototype.getInputElement = function () {
9743 return $( '<input>' ).attr( 'type', 'search' );
9749 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
9750 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9751 // Clear the text field
9752 this.setValue( '' );
9753 this.$input
[ 0 ].focus();
9759 * Update the 'clear' indicator displayed on type: 'search' text
9760 * fields, hiding it when the field is already empty or when it's not
9763 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
9764 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
9765 this.setIndicator( null );
9767 this.setIndicator( 'clear' );
9774 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
9775 OO
.ui
.SearchInputWidget
.parent
.prototype.onChange
.call( this );
9776 this.updateSearchIndicator();
9780 * Handle disable events.
9782 * @param {boolean} disabled Element is disabled
9785 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
9786 this.updateSearchIndicator();
9792 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
9793 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
9794 this.updateSearchIndicator();
9799 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
9800 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
9801 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
9803 * - by typing a value in the text input field. If the value exactly matches the value of a menu
9804 * option, that option will appear to be selected.
9805 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
9808 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9810 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
9813 * // Example: A ComboBoxInputWidget.
9814 * var comboBox = new OO.ui.ComboBoxInputWidget( {
9815 * label: 'ComboBoxInputWidget',
9816 * value: 'Option 1',
9819 * new OO.ui.MenuOptionWidget( {
9821 * label: 'Option One'
9823 * new OO.ui.MenuOptionWidget( {
9825 * label: 'Option Two'
9827 * new OO.ui.MenuOptionWidget( {
9829 * label: 'Option Three'
9831 * new OO.ui.MenuOptionWidget( {
9833 * label: 'Option Four'
9835 * new OO.ui.MenuOptionWidget( {
9837 * label: 'Option Five'
9842 * $( 'body' ).append( comboBox.$element );
9844 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
9847 * @extends OO.ui.TextInputWidget
9850 * @param {Object} [config] Configuration options
9851 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9852 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
9853 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
9854 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
9855 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
9857 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
9858 // Configuration initialization
9859 config
= $.extend( {
9863 // ComboBoxInputWidget shouldn't support multiline
9864 config
.multiline
= false;
9866 // Parent constructor
9867 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
9870 this.$overlay
= config
.$overlay
|| this.$element
;
9871 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
9872 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
9874 disabled
: this.disabled
9876 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend(
9880 $container
: this.$element
,
9881 disabled
: this.isDisabled()
9887 this.connect( this, {
9888 change
: 'onInputChange',
9889 enter
: 'onInputEnter'
9891 this.dropdownButton
.connect( this, {
9892 click
: 'onDropdownButtonClick'
9894 this.menu
.connect( this, {
9895 choose
: 'onMenuChoose',
9896 add
: 'onMenuItemsChange',
9897 remove
: 'onMenuItemsChange'
9903 'aria-autocomplete': 'list'
9905 // Do not override options set via config.menu.items
9906 if ( config
.options
!== undefined ) {
9907 this.setOptions( config
.options
);
9909 this.$field
= $( '<div>' )
9910 .addClass( 'oo-ui-comboBoxInputWidget-field' )
9911 .append( this.$input
, this.dropdownButton
.$element
);
9913 .addClass( 'oo-ui-comboBoxInputWidget' )
9914 .append( this.$field
);
9915 this.$overlay
.append( this.menu
.$element
);
9916 this.onMenuItemsChange();
9921 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
9926 * Get the combobox's menu.
9928 * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
9930 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
9935 * Get the combobox's text input widget.
9937 * @return {OO.ui.TextInputWidget} Text input widget
9939 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
9944 * Handle input change events.
9947 * @param {string} value New value
9949 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
9950 var match
= this.menu
.getItemFromData( value
);
9952 this.menu
.selectItem( match
);
9953 if ( this.menu
.getHighlightedItem() ) {
9954 this.menu
.highlightItem( match
);
9957 if ( !this.isDisabled() ) {
9958 this.menu
.toggle( true );
9963 * Handle input enter events.
9967 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
9968 if ( !this.isDisabled() ) {
9969 this.menu
.toggle( false );
9974 * Handle button click events.
9978 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
9980 this.$input
[ 0 ].focus();
9984 * Handle menu choose events.
9987 * @param {OO.ui.OptionWidget} item Chosen item
9989 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
9990 this.setValue( item
.getData() );
9994 * Handle menu item change events.
9998 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
9999 var match
= this.menu
.getItemFromData( this.getValue() );
10000 this.menu
.selectItem( match
);
10001 if ( this.menu
.getHighlightedItem() ) {
10002 this.menu
.highlightItem( match
);
10004 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
10010 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
10012 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
10014 if ( this.dropdownButton
) {
10015 this.dropdownButton
.setDisabled( this.isDisabled() );
10018 this.menu
.setDisabled( this.isDisabled() );
10025 * Set the options available for this input.
10027 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10030 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
10033 .addItems( options
.map( function ( opt
) {
10034 return new OO
.ui
.MenuOptionWidget( {
10036 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
10044 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
10045 * which is a widget that is specified by reference before any optional configuration settings.
10047 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
10049 * - **left**: The label is placed before the field-widget and aligned with the left margin.
10050 * A left-alignment is used for forms with many fields.
10051 * - **right**: The label is placed before the field-widget and aligned to the right margin.
10052 * A right-alignment is used for long but familiar forms which users tab through,
10053 * verifying the current field with a quick glance at the label.
10054 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
10055 * that users fill out from top to bottom.
10056 * - **inline**: The label is placed after the field-widget and aligned to the left.
10057 * An inline-alignment is best used with checkboxes or radio buttons.
10059 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
10060 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
10062 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
10065 * @extends OO.ui.Layout
10066 * @mixins OO.ui.mixin.LabelElement
10067 * @mixins OO.ui.mixin.TitledElement
10070 * @param {OO.ui.Widget} fieldWidget Field widget
10071 * @param {Object} [config] Configuration options
10072 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
10073 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
10074 * The array may contain strings or OO.ui.HtmlSnippet instances.
10075 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
10076 * The array may contain strings or OO.ui.HtmlSnippet instances.
10077 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
10078 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
10079 * For important messages, you are advised to use `notices`, as they are always shown.
10081 * @throws {Error} An error is thrown if no widget is specified
10083 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
10084 // Allow passing positional parameters inside the config object
10085 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
10086 config
= fieldWidget
;
10087 fieldWidget
= config
.fieldWidget
;
10090 // Make sure we have required constructor arguments
10091 if ( fieldWidget
=== undefined ) {
10092 throw new Error( 'Widget not found' );
10095 // Configuration initialization
10096 config
= $.extend( { align
: 'left' }, config
);
10098 // Parent constructor
10099 OO
.ui
.FieldLayout
.parent
.call( this, config
);
10101 // Mixin constructors
10102 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
10103 $label
: $( '<label>' )
10105 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
10108 this.fieldWidget
= fieldWidget
;
10111 this.$field
= $( '<div>' );
10112 this.$messages
= $( '<ul>' );
10113 this.$header
= $( '<div>' );
10114 this.$body
= $( '<div>' );
10116 if ( config
.help
) {
10117 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
10121 classes
: [ 'oo-ui-fieldLayout-help' ],
10125 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
10126 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
10128 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
10130 this.$help
= this.popupButtonWidget
.$element
;
10132 this.$help
= $( [] );
10136 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
10139 if ( fieldWidget
.constructor.static.supportsSimpleLabel
) {
10140 if ( this.fieldWidget
.getInputId() ) {
10141 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
10145 .addClass( 'oo-ui-fieldLayout' )
10146 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
10147 .append( this.$body
);
10148 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
10149 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
10150 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
10152 .addClass( 'oo-ui-fieldLayout-field' )
10153 .append( this.fieldWidget
.$element
);
10155 this.setErrors( config
.errors
|| [] );
10156 this.setNotices( config
.notices
|| [] );
10157 this.setAlignment( config
.align
);
10162 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
10163 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
10164 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
10169 * Handle field disable events.
10172 * @param {boolean} value Field is disabled
10174 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
10175 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
10179 * Get the widget contained by the field.
10181 * @return {OO.ui.Widget} Field widget
10183 OO
.ui
.FieldLayout
.prototype.getField = function () {
10184 return this.fieldWidget
;
10189 * @param {string} kind 'error' or 'notice'
10190 * @param {string|OO.ui.HtmlSnippet} text
10193 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
10194 var $listItem
, $icon
, message
;
10195 $listItem
= $( '<li>' );
10196 if ( kind
=== 'error' ) {
10197 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
10198 } else if ( kind
=== 'notice' ) {
10199 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
10203 message
= new OO
.ui
.LabelWidget( { label
: text
} );
10205 .append( $icon
, message
.$element
)
10206 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
10211 * Set the field alignment mode.
10214 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
10217 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
10218 if ( value
!== this.align
) {
10219 // Default to 'left'
10220 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
10223 // Reorder elements
10224 if ( value
=== 'top' ) {
10225 this.$header
.append( this.$label
, this.$help
);
10226 this.$body
.append( this.$header
, this.$field
);
10227 } else if ( value
=== 'inline' ) {
10228 this.$header
.append( this.$label
, this.$help
);
10229 this.$body
.append( this.$field
, this.$header
);
10231 this.$header
.append( this.$label
);
10232 this.$body
.append( this.$header
, this.$help
, this.$field
);
10234 // Set classes. The following classes can be used here:
10235 // * oo-ui-fieldLayout-align-left
10236 // * oo-ui-fieldLayout-align-right
10237 // * oo-ui-fieldLayout-align-top
10238 // * oo-ui-fieldLayout-align-inline
10239 if ( this.align
) {
10240 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
10242 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
10243 this.align
= value
;
10250 * Set the list of error messages.
10252 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
10253 * The array may contain strings or OO.ui.HtmlSnippet instances.
10256 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
10257 this.errors
= errors
.slice();
10258 this.updateMessages();
10263 * Set the list of notice messages.
10265 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
10266 * The array may contain strings or OO.ui.HtmlSnippet instances.
10269 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
10270 this.notices
= notices
.slice();
10271 this.updateMessages();
10276 * Update the rendering of error and notice messages.
10280 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
10282 this.$messages
.empty();
10284 if ( this.errors
.length
|| this.notices
.length
) {
10285 this.$body
.after( this.$messages
);
10287 this.$messages
.remove();
10291 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
10292 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
10294 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
10295 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
10300 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
10301 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
10302 * is required and is specified before any optional configuration settings.
10304 * Labels can be aligned in one of four ways:
10306 * - **left**: The label is placed before the field-widget and aligned with the left margin.
10307 * A left-alignment is used for forms with many fields.
10308 * - **right**: The label is placed before the field-widget and aligned to the right margin.
10309 * A right-alignment is used for long but familiar forms which users tab through,
10310 * verifying the current field with a quick glance at the label.
10311 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
10312 * that users fill out from top to bottom.
10313 * - **inline**: The label is placed after the field-widget and aligned to the left.
10314 * An inline-alignment is best used with checkboxes or radio buttons.
10316 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
10317 * text is specified.
10320 * // Example of an ActionFieldLayout
10321 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
10322 * new OO.ui.TextInputWidget( {
10323 * placeholder: 'Field widget'
10325 * new OO.ui.ButtonWidget( {
10329 * label: 'An ActionFieldLayout. This label is aligned top',
10331 * help: 'This is help text'
10335 * $( 'body' ).append( actionFieldLayout.$element );
10338 * @extends OO.ui.FieldLayout
10341 * @param {OO.ui.Widget} fieldWidget Field widget
10342 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
10343 * @param {Object} config
10345 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
10346 // Allow passing positional parameters inside the config object
10347 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
10348 config
= fieldWidget
;
10349 fieldWidget
= config
.fieldWidget
;
10350 buttonWidget
= config
.buttonWidget
;
10353 // Parent constructor
10354 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
10357 this.buttonWidget
= buttonWidget
;
10358 this.$button
= $( '<div>' );
10359 this.$input
= $( '<div>' );
10363 .addClass( 'oo-ui-actionFieldLayout' );
10365 .addClass( 'oo-ui-actionFieldLayout-button' )
10366 .append( this.buttonWidget
.$element
);
10368 .addClass( 'oo-ui-actionFieldLayout-input' )
10369 .append( this.fieldWidget
.$element
);
10371 .append( this.$input
, this.$button
);
10376 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
10379 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
10380 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
10381 * configured with a label as well. For more information and examples,
10382 * please see the [OOjs UI documentation on MediaWiki][1].
10385 * // Example of a fieldset layout
10386 * var input1 = new OO.ui.TextInputWidget( {
10387 * placeholder: 'A text input field'
10390 * var input2 = new OO.ui.TextInputWidget( {
10391 * placeholder: 'A text input field'
10394 * var fieldset = new OO.ui.FieldsetLayout( {
10395 * label: 'Example of a fieldset layout'
10398 * fieldset.addItems( [
10399 * new OO.ui.FieldLayout( input1, {
10400 * label: 'Field One'
10402 * new OO.ui.FieldLayout( input2, {
10403 * label: 'Field Two'
10406 * $( 'body' ).append( fieldset.$element );
10408 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
10411 * @extends OO.ui.Layout
10412 * @mixins OO.ui.mixin.IconElement
10413 * @mixins OO.ui.mixin.LabelElement
10414 * @mixins OO.ui.mixin.GroupElement
10417 * @param {Object} [config] Configuration options
10418 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
10419 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
10420 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
10421 * For important messages, you are advised to use `notices`, as they are always shown.
10423 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
10424 // Configuration initialization
10425 config
= config
|| {};
10427 // Parent constructor
10428 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
10430 // Mixin constructors
10431 OO
.ui
.mixin
.IconElement
.call( this, config
);
10432 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: $( '<div>' ) } ) );
10433 OO
.ui
.mixin
.GroupElement
.call( this, config
);
10436 this.$header
= $( '<div>' );
10437 if ( config
.help
) {
10438 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
10442 classes
: [ 'oo-ui-fieldsetLayout-help' ],
10446 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
10447 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
10449 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
10451 this.$help
= this.popupButtonWidget
.$element
;
10453 this.$help
= $( [] );
10458 .addClass( 'oo-ui-fieldsetLayout-header' )
10459 .append( this.$icon
, this.$label
, this.$help
);
10460 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
10462 .addClass( 'oo-ui-fieldsetLayout' )
10463 .prepend( this.$header
, this.$group
);
10464 if ( Array
.isArray( config
.items
) ) {
10465 this.addItems( config
.items
);
10471 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
10472 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
10473 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
10474 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
10476 /* Static Properties */
10482 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
10485 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
10486 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
10487 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
10488 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10490 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
10491 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
10492 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
10493 * some fancier controls. Some controls have both regular and InputWidget variants, for example
10494 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
10495 * often have simplified APIs to match the capabilities of HTML forms.
10496 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
10498 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
10499 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10502 * // Example of a form layout that wraps a fieldset layout
10503 * var input1 = new OO.ui.TextInputWidget( {
10504 * placeholder: 'Username'
10506 * var input2 = new OO.ui.TextInputWidget( {
10507 * placeholder: 'Password',
10510 * var submit = new OO.ui.ButtonInputWidget( {
10514 * var fieldset = new OO.ui.FieldsetLayout( {
10515 * label: 'A form layout'
10517 * fieldset.addItems( [
10518 * new OO.ui.FieldLayout( input1, {
10519 * label: 'Username',
10522 * new OO.ui.FieldLayout( input2, {
10523 * label: 'Password',
10526 * new OO.ui.FieldLayout( submit )
10528 * var form = new OO.ui.FormLayout( {
10529 * items: [ fieldset ],
10530 * action: '/api/formhandler',
10533 * $( 'body' ).append( form.$element );
10536 * @extends OO.ui.Layout
10537 * @mixins OO.ui.mixin.GroupElement
10540 * @param {Object} [config] Configuration options
10541 * @cfg {string} [method] HTML form `method` attribute
10542 * @cfg {string} [action] HTML form `action` attribute
10543 * @cfg {string} [enctype] HTML form `enctype` attribute
10544 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
10546 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
10549 // Configuration initialization
10550 config
= config
|| {};
10552 // Parent constructor
10553 OO
.ui
.FormLayout
.parent
.call( this, config
);
10555 // Mixin constructors
10556 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
10559 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
10561 // Make sure the action is safe
10562 action
= config
.action
;
10563 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
10564 action
= './' + action
;
10569 .addClass( 'oo-ui-formLayout' )
10571 method
: config
.method
,
10573 enctype
: config
.enctype
10575 if ( Array
.isArray( config
.items
) ) {
10576 this.addItems( config
.items
);
10582 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
10583 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
10588 * A 'submit' event is emitted when the form is submitted.
10593 /* Static Properties */
10599 OO
.ui
.FormLayout
.static.tagName
= 'form';
10604 * Handle form submit events.
10607 * @param {jQuery.Event} e Submit event
10610 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
10611 if ( this.emit( 'submit' ) ) {
10617 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
10618 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
10621 * // Example of a panel layout
10622 * var panel = new OO.ui.PanelLayout( {
10626 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
10628 * $( 'body' ).append( panel.$element );
10631 * @extends OO.ui.Layout
10634 * @param {Object} [config] Configuration options
10635 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10636 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10637 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10638 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10640 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
10641 // Configuration initialization
10642 config
= $.extend( {
10649 // Parent constructor
10650 OO
.ui
.PanelLayout
.parent
.call( this, config
);
10653 this.$element
.addClass( 'oo-ui-panelLayout' );
10654 if ( config
.scrollable
) {
10655 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
10657 if ( config
.padded
) {
10658 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
10660 if ( config
.expanded
) {
10661 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
10663 if ( config
.framed
) {
10664 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
10670 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
10675 * Focus the panel layout
10677 * The default implementation just focuses the first focusable element in the panel
10679 OO
.ui
.PanelLayout
.prototype.focus = function () {
10680 OO
.ui
.findFocusable( this.$element
).focus();
10684 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
10685 * items), with small margins between them. Convenient when you need to put a number of block-level
10686 * widgets on a single line next to each other.
10688 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
10691 * // HorizontalLayout with a text input and a label
10692 * var layout = new OO.ui.HorizontalLayout( {
10694 * new OO.ui.LabelWidget( { label: 'Label' } ),
10695 * new OO.ui.TextInputWidget( { value: 'Text' } )
10698 * $( 'body' ).append( layout.$element );
10701 * @extends OO.ui.Layout
10702 * @mixins OO.ui.mixin.GroupElement
10705 * @param {Object} [config] Configuration options
10706 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
10708 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
10709 // Configuration initialization
10710 config
= config
|| {};
10712 // Parent constructor
10713 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
10715 // Mixin constructors
10716 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
10719 this.$element
.addClass( 'oo-ui-horizontalLayout' );
10720 if ( Array
.isArray( config
.items
) ) {
10721 this.addItems( config
.items
);
10727 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
10728 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);