3 * https://www.mediawiki.org/wiki/OOjs_UI
5 * Copyright 2011–2016 OOjs UI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2016-12-06T23:32:53Z
16 * Namespace for all classes, static methods and static properties.
48 * Constants for MouseEvent.which
52 OO
.ui
.MouseButtons
= {
64 * Generate a unique ID for element
66 * @return {string} [id]
68 OO
.ui
.generateElementId = function () {
70 return 'oojsui-' + OO
.ui
.elementId
;
74 * Check if an element is focusable.
75 * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
77 * @param {jQuery} $element Element to test
80 OO
.ui
.isFocusableElement = function ( $element
) {
82 element
= $element
[ 0 ];
84 // Anything disabled is not focusable
85 if ( element
.disabled
) {
89 // Check if the element is visible
91 // This is quicker than calling $element.is( ':visible' )
92 $.expr
.filters
.visible( element
) &&
93 // Check that all parents are visible
94 !$element
.parents().addBack().filter( function () {
95 return $.css( this, 'visibility' ) === 'hidden';
101 // Check if the element is ContentEditable, which is the string 'true'
102 if ( element
.contentEditable
=== 'true' ) {
106 // Anything with a non-negative numeric tabIndex is focusable.
107 // Use .prop to avoid browser bugs
108 if ( $element
.prop( 'tabIndex' ) >= 0 ) {
112 // Some element types are naturally focusable
113 // (indexOf is much faster than regex in Chrome and about the
114 // same in FF: https://jsperf.com/regex-vs-indexof-array2)
115 nodeName
= element
.nodeName
.toLowerCase();
116 if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName
) !== -1 ) {
120 // Links and areas are focusable if they have an href
121 if ( ( nodeName
=== 'a' || nodeName
=== 'area' ) && $element
.attr( 'href' ) !== undefined ) {
129 * Find a focusable child
131 * @param {jQuery} $container Container to search in
132 * @param {boolean} [backwards] Search backwards
133 * @return {jQuery} Focusable child, an empty jQuery object if none found
135 OO
.ui
.findFocusable = function ( $container
, backwards
) {
136 var $focusable
= $( [] ),
137 // $focusableCandidates is a superset of things that
138 // could get matched by isFocusableElement
139 $focusableCandidates
= $container
140 .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
143 $focusableCandidates
= Array
.prototype.reverse
.call( $focusableCandidates
);
146 $focusableCandidates
.each( function () {
147 var $this = $( this );
148 if ( OO
.ui
.isFocusableElement( $this ) ) {
157 * Get the user's language and any fallback languages.
159 * These language codes are used to localize user interface elements in the user's language.
161 * In environments that provide a localization system, this function should be overridden to
162 * return the user's language(s). The default implementation returns English (en) only.
164 * @return {string[]} Language codes, in descending order of priority
166 OO
.ui
.getUserLanguages = function () {
171 * Get a value in an object keyed by language code.
173 * @param {Object.<string,Mixed>} obj Object keyed by language code
174 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
175 * @param {string} [fallback] Fallback code, used if no matching language can be found
176 * @return {Mixed} Local value
178 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
181 // Requested language
185 // Known user language
186 langs
= OO
.ui
.getUserLanguages();
187 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
194 if ( obj
[ fallback
] ) {
195 return obj
[ fallback
];
197 // First existing language
198 for ( lang
in obj
) {
206 * Check if a node is contained within another node
208 * Similar to jQuery#contains except a list of containers can be supplied
209 * and a boolean argument allows you to include the container in the match list
211 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
212 * @param {HTMLElement} contained Node to find
213 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
214 * @return {boolean} The node is in the list of target nodes
216 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
218 if ( !Array
.isArray( containers
) ) {
219 containers
= [ containers
];
221 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
222 if ( ( matchContainers
&& contained
=== containers
[ i
] ) || $.contains( containers
[ i
], contained
) ) {
230 * Return a function, that, as long as it continues to be invoked, will not
231 * be triggered. The function will be called after it stops being called for
232 * N milliseconds. If `immediate` is passed, trigger the function on the
233 * leading edge, instead of the trailing.
235 * Ported from: http://underscorejs.org/underscore.js
237 * @param {Function} func
238 * @param {number} wait
239 * @param {boolean} immediate
242 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
247 later = function () {
250 func
.apply( context
, args
);
253 if ( immediate
&& !timeout
) {
254 func
.apply( context
, args
);
256 if ( !timeout
|| wait
) {
257 clearTimeout( timeout
);
258 timeout
= setTimeout( later
, wait
);
264 * Puts a console warning with provided message.
266 * @param {string} message
268 OO
.ui
.warnDeprecation = function ( message
) {
269 if ( OO
.getProp( window
, 'console', 'warn' ) !== undefined ) {
270 // eslint-disable-next-line no-console
271 console
.warn( message
);
276 * Returns a function, that, when invoked, will only be triggered at most once
277 * during a given window of time. If called again during that window, it will
278 * wait until the window ends and then trigger itself again.
280 * As it's not knowable to the caller whether the function will actually run
281 * when the wrapper is called, return values from the function are entirely
284 * @param {Function} func
285 * @param {number} wait
288 OO
.ui
.throttle = function ( func
, wait
) {
289 var context
, args
, timeout
,
293 previous
= OO
.ui
.now();
294 func
.apply( context
, args
);
297 // Check how long it's been since the last time the function was
298 // called, and whether it's more or less than the requested throttle
299 // period. If it's less, run the function immediately. If it's more,
300 // set a timeout for the remaining time -- but don't replace an
301 // existing timeout, since that'd indefinitely prolong the wait.
302 var remaining
= wait
- ( OO
.ui
.now() - previous
);
305 if ( remaining
<= 0 ) {
306 // Note: unless wait was ridiculously large, this means we'll
307 // automatically run the first time the function was called in a
308 // given period. (If you provide a wait period larger than the
309 // current Unix timestamp, you *deserve* unexpected behavior.)
310 clearTimeout( timeout
);
312 } else if ( !timeout
) {
313 timeout
= setTimeout( run
, remaining
);
319 * A (possibly faster) way to get the current timestamp as an integer
321 * @return {number} Current timestamp
323 OO
.ui
.now
= Date
.now
|| function () {
324 return new Date().getTime();
328 * Reconstitute a JavaScript object corresponding to a widget created by
329 * the PHP implementation.
331 * This is an alias for `OO.ui.Element.static.infuse()`.
333 * @param {string|HTMLElement|jQuery} idOrNode
334 * A DOM id (if a string) or node for the widget to infuse.
335 * @return {OO.ui.Element}
336 * The `OO.ui.Element` corresponding to this (infusable) document node.
338 OO
.ui
.infuse = function ( idOrNode
) {
339 return OO
.ui
.Element
.static.infuse( idOrNode
);
344 * Message store for the default implementation of OO.ui.msg
346 * Environments that provide a localization system should not use this, but should override
347 * OO.ui.msg altogether.
352 // Tool tip for a button that moves items in a list down one place
353 'ooui-outline-control-move-down': 'Move item down',
354 // Tool tip for a button that moves items in a list up one place
355 'ooui-outline-control-move-up': 'Move item up',
356 // Tool tip for a button that removes items from a list
357 'ooui-outline-control-remove': 'Remove item',
358 // Label for the toolbar group that contains a list of all other available tools
359 'ooui-toolbar-more': 'More',
360 // Label for the fake tool that expands the full list of tools in a toolbar group
361 'ooui-toolgroup-expand': 'More',
362 // Label for the fake tool that collapses the full list of tools in a toolbar group
363 'ooui-toolgroup-collapse': 'Fewer',
364 // Default label for the accept button of a confirmation dialog
365 'ooui-dialog-message-accept': 'OK',
366 // Default label for the reject button of a confirmation dialog
367 'ooui-dialog-message-reject': 'Cancel',
368 // Title for process dialog error description
369 'ooui-dialog-process-error': 'Something went wrong',
370 // Label for process dialog dismiss error button, visible when describing errors
371 'ooui-dialog-process-dismiss': 'Dismiss',
372 // Label for process dialog retry action button, visible when describing only recoverable errors
373 'ooui-dialog-process-retry': 'Try again',
374 // Label for process dialog retry action button, visible when describing only warnings
375 'ooui-dialog-process-continue': 'Continue',
376 // Label for the file selection widget's select file button
377 'ooui-selectfile-button-select': 'Select a file',
378 // Label for the file selection widget if file selection is not supported
379 'ooui-selectfile-not-supported': 'File selection is not supported',
380 // Label for the file selection widget when no file is currently selected
381 'ooui-selectfile-placeholder': 'No file is selected',
382 // Label for the file selection widget's drop target
383 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
387 * Get a localized message.
389 * In environments that provide a localization system, this function should be overridden to
390 * return the message translated in the user's language. The default implementation always returns
393 * After the message key, message parameters may optionally be passed. In the default implementation,
394 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
395 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
396 * they support unnamed, ordered message parameters.
398 * @param {string} key Message key
399 * @param {...Mixed} [params] Message parameters
400 * @return {string} Translated message with parameters substituted
402 OO
.ui
.msg = function ( key
) {
403 var message
= messages
[ key
],
404 params
= Array
.prototype.slice
.call( arguments
, 1 );
405 if ( typeof message
=== 'string' ) {
406 // Perform $1 substitution
407 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
408 var i
= parseInt( n
, 10 );
409 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
412 // Return placeholder if message not found
413 message
= '[' + key
+ ']';
420 * Package a message and arguments for deferred resolution.
422 * Use this when you are statically specifying a message and the message may not yet be present.
424 * @param {string} key Message key
425 * @param {...Mixed} [params] Message parameters
426 * @return {Function} Function that returns the resolved message when executed
428 OO
.ui
.deferMsg = function () {
429 var args
= arguments
;
431 return OO
.ui
.msg
.apply( OO
.ui
, args
);
438 * If the message is a function it will be executed, otherwise it will pass through directly.
440 * @param {Function|string} msg Deferred message, or message text
441 * @return {string} Resolved message
443 OO
.ui
.resolveMsg = function ( msg
) {
444 if ( $.isFunction( msg
) ) {
451 * @param {string} url
454 OO
.ui
.isSafeUrl = function ( url
) {
455 // Keep this function in sync with php/Tag.php
456 var i
, protocolWhitelist
;
458 function stringStartsWith( haystack
, needle
) {
459 return haystack
.substr( 0, needle
.length
) === needle
;
462 protocolWhitelist
= [
463 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
464 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
465 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
472 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
473 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
478 // This matches '//' too
479 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
482 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
494 * Namespace for OOjs UI mixins.
496 * Mixins are named according to the type of object they are intended to
497 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
498 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
499 * is intended to be mixed in to an instance of OO.ui.Widget.
507 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
508 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
509 * connected to them and can't be interacted with.
515 * @param {Object} [config] Configuration options
516 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
517 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
519 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
520 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
521 * @cfg {string} [text] Text to insert
522 * @cfg {Array} [content] An array of content elements to append (after #text).
523 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
524 * Instances of OO.ui.Element will have their $element appended.
525 * @cfg {jQuery} [$content] Content elements to append (after #text).
526 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
527 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
528 * Data can also be specified with the #setData method.
530 OO
.ui
.Element
= function OoUiElement( config
) {
531 // Configuration initialization
532 config
= config
|| {};
537 this.data
= config
.data
;
538 this.$element
= config
.$element
||
539 $( document
.createElement( this.getTagName() ) );
540 this.elementGroup
= null;
541 this.debouncedUpdateThemeClassesHandler
= OO
.ui
.debounce( this.debouncedUpdateThemeClasses
);
544 if ( Array
.isArray( config
.classes
) ) {
545 this.$element
.addClass( config
.classes
.join( ' ' ) );
548 this.$element
.attr( 'id', config
.id
);
551 this.$element
.text( config
.text
);
553 if ( config
.content
) {
554 // The `content` property treats plain strings as text; use an
555 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
556 // appropriate $element appended.
557 this.$element
.append( config
.content
.map( function ( v
) {
558 if ( typeof v
=== 'string' ) {
559 // Escape string so it is properly represented in HTML.
560 return document
.createTextNode( v
);
561 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
564 } else if ( v
instanceof OO
.ui
.Element
) {
570 if ( config
.$content
) {
571 // The `$content` property treats plain strings as HTML.
572 this.$element
.append( config
.$content
);
578 OO
.initClass( OO
.ui
.Element
);
580 /* Static Properties */
583 * The name of the HTML tag used by the element.
585 * The static value may be ignored if the #getTagName method is overridden.
591 OO
.ui
.Element
.static.tagName
= 'div';
596 * Reconstitute a JavaScript object corresponding to a widget created
597 * by the PHP implementation.
599 * @param {string|HTMLElement|jQuery} idOrNode
600 * A DOM id (if a string) or node for the widget to infuse.
601 * @return {OO.ui.Element}
602 * The `OO.ui.Element` corresponding to this (infusable) document node.
603 * For `Tag` objects emitted on the HTML side (used occasionally for content)
604 * the value returned is a newly-created Element wrapping around the existing
607 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
608 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, false );
609 // Verify that the type matches up.
610 // FIXME: uncomment after T89721 is fixed (see T90929)
612 if ( !( obj instanceof this['class'] ) ) {
613 throw new Error( 'Infusion type mismatch!' );
620 * Implementation helper for `infuse`; skips the type check and has an
621 * extra property so that only the top-level invocation touches the DOM.
624 * @param {string|HTMLElement|jQuery} idOrNode
625 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
626 * when the top-level widget of this infusion is inserted into DOM,
627 * replacing the original node; or false for top-level invocation.
628 * @return {OO.ui.Element}
630 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, domPromise
) {
631 // look for a cached result of a previous infusion.
632 var id
, $elem
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
633 if ( typeof idOrNode
=== 'string' ) {
635 $elem
= $( document
.getElementById( id
) );
637 $elem
= $( idOrNode
);
638 id
= $elem
.attr( 'id' );
640 if ( !$elem
.length
) {
641 throw new Error( 'Widget not found: ' + id
);
643 if ( $elem
[ 0 ].oouiInfused
) {
644 $elem
= $elem
[ 0 ].oouiInfused
;
646 data
= $elem
.data( 'ooui-infused' );
649 if ( data
=== true ) {
650 throw new Error( 'Circular dependency! ' + id
);
653 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
654 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
655 // restore dynamic state after the new element is re-inserted into DOM under infused parent
656 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
657 infusedChildren
= $elem
.data( 'ooui-infused-children' );
658 if ( infusedChildren
&& infusedChildren
.length
) {
659 infusedChildren
.forEach( function ( data
) {
660 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
661 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
667 data
= $elem
.attr( 'data-ooui' );
669 throw new Error( 'No infusion data found: ' + id
);
672 data
= $.parseJSON( data
);
676 if ( !( data
&& data
._
) ) {
677 throw new Error( 'No valid infusion data found: ' + id
);
679 if ( data
._
=== 'Tag' ) {
680 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
681 return new OO
.ui
.Element( { $element
: $elem
} );
683 parts
= data
._
.split( '.' );
684 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
685 if ( cls
=== undefined ) {
686 // The PHP output might be old and not including the "OO.ui" prefix
687 // TODO: Remove this back-compat after next major release
688 cls
= OO
.getProp
.apply( OO
, [ OO
.ui
].concat( parts
) );
689 if ( cls
=== undefined ) {
690 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
694 // Verify that we're creating an OO.ui.Element instance
697 while ( parent
!== undefined ) {
698 if ( parent
=== OO
.ui
.Element
) {
703 parent
= parent
.parent
;
706 if ( parent
!== OO
.ui
.Element
) {
707 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
710 if ( domPromise
=== false ) {
712 domPromise
= top
.promise();
714 $elem
.data( 'ooui-infused', true ); // prevent loops
715 data
.id
= id
; // implicit
716 infusedChildren
= [];
717 data
= OO
.copy( data
, null, function deserialize( value
) {
719 if ( OO
.isPlainObject( value
) ) {
721 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, domPromise
);
722 infusedChildren
.push( infused
);
723 // Flatten the structure
724 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
725 infused
.$element
.removeData( 'ooui-infused-children' );
728 if ( value
.html
!== undefined ) {
729 return new OO
.ui
.HtmlSnippet( value
.html
);
733 // allow widgets to reuse parts of the DOM
734 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
735 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
736 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
738 // eslint-disable-next-line new-cap
739 obj
= new cls( data
);
740 // now replace old DOM with this new DOM.
742 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
743 // so only mutate the DOM if we need to.
744 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
745 $elem
.replaceWith( obj
.$element
);
746 // This element is now gone from the DOM, but if anyone is holding a reference to it,
747 // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
748 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
749 $elem
[ 0 ].oouiInfused
= obj
.$element
;
753 obj
.$element
.data( 'ooui-infused', obj
);
754 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
755 // set the 'data-ooui' attribute so we can identify infused widgets
756 obj
.$element
.attr( 'data-ooui', '' );
757 // restore dynamic state after the new element is inserted into DOM
758 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
763 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
765 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
766 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
767 * constructor, which will be given the enhanced config.
770 * @param {HTMLElement} node
771 * @param {Object} config
774 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
779 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
780 * (and its children) that represent an Element of the same class and the given configuration,
781 * generated by the PHP implementation.
783 * This method is called just before `node` is detached from the DOM. The return value of this
784 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
785 * is inserted into DOM to replace `node`.
788 * @param {HTMLElement} node
789 * @param {Object} config
792 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
797 * Get a jQuery function within a specific document.
800 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
801 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
803 * @return {Function} Bound jQuery function
805 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
806 function wrapper( selector
) {
807 return $( selector
, wrapper
.context
);
810 wrapper
.context
= this.getDocument( context
);
813 wrapper
.$iframe
= $iframe
;
820 * Get the document of an element.
823 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
824 * @return {HTMLDocument|null} Document object
826 OO
.ui
.Element
.static.getDocument = function ( obj
) {
827 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
828 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
829 // Empty jQuery selections might have a context
836 ( obj
.nodeType
=== 9 && obj
) ||
841 * Get the window of an element or document.
844 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
845 * @return {Window} Window object
847 OO
.ui
.Element
.static.getWindow = function ( obj
) {
848 var doc
= this.getDocument( obj
);
849 return doc
.defaultView
;
853 * Get the direction of an element or document.
856 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
857 * @return {string} Text direction, either 'ltr' or 'rtl'
859 OO
.ui
.Element
.static.getDir = function ( obj
) {
862 if ( obj
instanceof jQuery
) {
865 isDoc
= obj
.nodeType
=== 9;
866 isWin
= obj
.document
!== undefined;
867 if ( isDoc
|| isWin
) {
873 return $( obj
).css( 'direction' );
877 * Get the offset between two frames.
879 * TODO: Make this function not use recursion.
882 * @param {Window} from Window of the child frame
883 * @param {Window} [to=window] Window of the parent frame
884 * @param {Object} [offset] Offset to start with, used internally
885 * @return {Object} Offset object, containing left and top properties
887 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
888 var i
, len
, frames
, frame
, rect
;
894 offset
= { top
: 0, left
: 0 };
896 if ( from.parent
=== from ) {
900 // Get iframe element
901 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
902 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
903 if ( frames
[ i
].contentWindow
=== from ) {
909 // Recursively accumulate offset values
911 rect
= frame
.getBoundingClientRect();
912 offset
.left
+= rect
.left
;
913 offset
.top
+= rect
.top
;
915 this.getFrameOffset( from.parent
, offset
);
922 * Get the offset between two elements.
924 * The two elements may be in a different frame, but in that case the frame $element is in must
925 * be contained in the frame $anchor is in.
928 * @param {jQuery} $element Element whose position to get
929 * @param {jQuery} $anchor Element to get $element's position relative to
930 * @return {Object} Translated position coordinates, containing top and left properties
932 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
933 var iframe
, iframePos
,
934 pos
= $element
.offset(),
935 anchorPos
= $anchor
.offset(),
936 elementDocument
= this.getDocument( $element
),
937 anchorDocument
= this.getDocument( $anchor
);
939 // If $element isn't in the same document as $anchor, traverse up
940 while ( elementDocument
!== anchorDocument
) {
941 iframe
= elementDocument
.defaultView
.frameElement
;
943 throw new Error( '$element frame is not contained in $anchor frame' );
945 iframePos
= $( iframe
).offset();
946 pos
.left
+= iframePos
.left
;
947 pos
.top
+= iframePos
.top
;
948 elementDocument
= iframe
.ownerDocument
;
950 pos
.left
-= anchorPos
.left
;
951 pos
.top
-= anchorPos
.top
;
956 * Get element border sizes.
959 * @param {HTMLElement} el Element to measure
960 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
962 OO
.ui
.Element
.static.getBorders = function ( el
) {
963 var doc
= el
.ownerDocument
,
964 win
= doc
.defaultView
,
965 style
= win
.getComputedStyle( el
, null ),
967 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
968 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
969 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
970 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
981 * Get dimensions of an element or window.
984 * @param {HTMLElement|Window} el Element to measure
985 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
987 OO
.ui
.Element
.static.getDimensions = function ( el
) {
989 doc
= el
.ownerDocument
|| el
.document
,
990 win
= doc
.defaultView
;
992 if ( win
=== el
|| el
=== doc
.documentElement
) {
995 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
997 top
: $win
.scrollTop(),
998 left
: $win
.scrollLeft()
1000 scrollbar
: { right
: 0, bottom
: 0 },
1004 bottom
: $win
.innerHeight(),
1005 right
: $win
.innerWidth()
1011 borders
: this.getBorders( el
),
1013 top
: $el
.scrollTop(),
1014 left
: $el
.scrollLeft()
1017 right
: $el
.innerWidth() - el
.clientWidth
,
1018 bottom
: $el
.innerHeight() - el
.clientHeight
1020 rect
: el
.getBoundingClientRect()
1026 * Get scrollable object parent
1028 * documentElement can't be used to get or set the scrollTop
1029 * property on Blink. Changing and testing its value lets us
1030 * use 'body' or 'documentElement' based on what is working.
1032 * https://code.google.com/p/chromium/issues/detail?id=303131
1035 * @param {HTMLElement} el Element to find scrollable parent for
1036 * @return {HTMLElement} Scrollable parent
1038 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1039 var scrollTop
, body
;
1041 if ( OO
.ui
.scrollableElement
=== undefined ) {
1042 body
= el
.ownerDocument
.body
;
1043 scrollTop
= body
.scrollTop
;
1046 if ( body
.scrollTop
=== 1 ) {
1047 body
.scrollTop
= scrollTop
;
1048 OO
.ui
.scrollableElement
= 'body';
1050 OO
.ui
.scrollableElement
= 'documentElement';
1054 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1058 * Get closest scrollable container.
1060 * Traverses up until either a scrollable element or the root is reached, in which case the window
1064 * @param {HTMLElement} el Element to find scrollable container for
1065 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1066 * @return {HTMLElement} Closest scrollable container
1068 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1070 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1071 props
= [ 'overflow-x', 'overflow-y' ],
1072 $parent
= $( el
).parent();
1074 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1075 props
= [ 'overflow-' + dimension
];
1078 while ( $parent
.length
) {
1079 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1080 return $parent
[ 0 ];
1084 val
= $parent
.css( props
[ i
] );
1085 if ( val
=== 'auto' || val
=== 'scroll' ) {
1086 return $parent
[ 0 ];
1089 $parent
= $parent
.parent();
1091 return this.getDocument( el
).body
;
1095 * Scroll element into view.
1098 * @param {HTMLElement} el Element to scroll into view
1099 * @param {Object} [config] Configuration options
1100 * @param {string} [config.duration='fast'] jQuery animation duration value
1101 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1102 * to scroll in both directions
1103 * @param {Function} [config.complete] Function to call when scrolling completes.
1104 * Deprecated since 0.15.4, use the return promise instead.
1105 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1107 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1108 var position
, animations
, callback
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1109 deferred
= $.Deferred();
1111 // Configuration initialization
1112 config
= config
|| {};
1115 callback
= typeof config
.complete
=== 'function' && config
.complete
;
1116 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1117 $container
= $( container
);
1118 elementDimensions
= this.getDimensions( el
);
1119 containerDimensions
= this.getDimensions( container
);
1120 $window
= $( this.getWindow( el
) );
1122 // Compute the element's position relative to the container
1123 if ( $container
.is( 'html, body' ) ) {
1124 // If the scrollable container is the root, this is easy
1126 top
: elementDimensions
.rect
.top
,
1127 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1128 left
: elementDimensions
.rect
.left
,
1129 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1132 // Otherwise, we have to subtract el's coordinates from container's coordinates
1134 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1135 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1136 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1137 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1141 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1142 if ( position
.top
< 0 ) {
1143 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1144 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1145 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1148 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1149 if ( position
.left
< 0 ) {
1150 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1151 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1152 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1155 if ( !$.isEmptyObject( animations
) ) {
1156 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1157 $container
.queue( function ( next
) {
1170 return deferred
.promise();
1174 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1175 * and reserve space for them, because it probably doesn't.
1177 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1178 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1179 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1180 * and then reattach (or show) them back.
1183 * @param {HTMLElement} el Element to reconsider the scrollbars on
1185 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1186 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1187 // Save scroll position
1188 scrollLeft
= el
.scrollLeft
;
1189 scrollTop
= el
.scrollTop
;
1190 // Detach all children
1191 while ( el
.firstChild
) {
1192 nodes
.push( el
.firstChild
);
1193 el
.removeChild( el
.firstChild
);
1196 void el
.offsetHeight
;
1197 // Reattach all children
1198 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1199 el
.appendChild( nodes
[ i
] );
1201 // Restore scroll position (no-op if scrollbars disappeared)
1202 el
.scrollLeft
= scrollLeft
;
1203 el
.scrollTop
= scrollTop
;
1209 * Toggle visibility of an element.
1211 * @param {boolean} [show] Make element visible, omit to toggle visibility
1215 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1216 show
= show
=== undefined ? !this.visible
: !!show
;
1218 if ( show
!== this.isVisible() ) {
1219 this.visible
= show
;
1220 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1221 this.emit( 'toggle', show
);
1228 * Check if element is visible.
1230 * @return {boolean} element is visible
1232 OO
.ui
.Element
.prototype.isVisible = function () {
1233 return this.visible
;
1239 * @return {Mixed} Element data
1241 OO
.ui
.Element
.prototype.getData = function () {
1248 * @param {Mixed} data Element data
1251 OO
.ui
.Element
.prototype.setData = function ( data
) {
1257 * Check if element supports one or more methods.
1259 * @param {string|string[]} methods Method or list of methods to check
1260 * @return {boolean} All methods are supported
1262 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1266 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1267 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1268 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1273 return methods
.length
=== support
;
1277 * Update the theme-provided classes.
1279 * @localdoc This is called in element mixins and widget classes any time state changes.
1280 * Updating is debounced, minimizing overhead of changing multiple attributes and
1281 * guaranteeing that theme updates do not occur within an element's constructor
1283 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1284 this.debouncedUpdateThemeClassesHandler();
1289 * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
1290 * make them synchronous.
1292 OO
.ui
.Element
.prototype.debouncedUpdateThemeClasses = function () {
1293 OO
.ui
.theme
.updateElementClasses( this );
1297 * Get the HTML tag name.
1299 * Override this method to base the result on instance information.
1301 * @return {string} HTML tag name
1303 OO
.ui
.Element
.prototype.getTagName = function () {
1304 return this.constructor.static.tagName
;
1308 * Check if the element is attached to the DOM
1310 * @return {boolean} The element is attached to the DOM
1312 OO
.ui
.Element
.prototype.isElementAttached = function () {
1313 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1317 * Get the DOM document.
1319 * @return {HTMLDocument} Document object
1321 OO
.ui
.Element
.prototype.getElementDocument = function () {
1322 // Don't cache this in other ways either because subclasses could can change this.$element
1323 return OO
.ui
.Element
.static.getDocument( this.$element
);
1327 * Get the DOM window.
1329 * @return {Window} Window object
1331 OO
.ui
.Element
.prototype.getElementWindow = function () {
1332 return OO
.ui
.Element
.static.getWindow( this.$element
);
1336 * Get closest scrollable container.
1338 * @return {HTMLElement} Closest scrollable container
1340 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1341 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1345 * Get group element is in.
1347 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1349 OO
.ui
.Element
.prototype.getElementGroup = function () {
1350 return this.elementGroup
;
1354 * Set group element is in.
1356 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1359 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1360 this.elementGroup
= group
;
1365 * Scroll element into view.
1367 * @param {Object} [config] Configuration options
1368 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1370 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1371 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1375 * Restore the pre-infusion dynamic state for this widget.
1377 * This method is called after #$element has been inserted into DOM. The parameter is the return
1378 * value of #gatherPreInfuseState.
1381 * @param {Object} state
1383 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1387 * Wraps an HTML snippet for use with configuration values which default
1388 * to strings. This bypasses the default html-escaping done to string
1394 * @param {string} [content] HTML content
1396 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1398 this.content
= content
;
1403 OO
.initClass( OO
.ui
.HtmlSnippet
);
1410 * @return {string} Unchanged HTML snippet.
1412 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1413 return this.content
;
1417 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1418 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1419 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1420 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1421 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1425 * @extends OO.ui.Element
1426 * @mixins OO.EventEmitter
1429 * @param {Object} [config] Configuration options
1431 OO
.ui
.Layout
= function OoUiLayout( config
) {
1432 // Configuration initialization
1433 config
= config
|| {};
1435 // Parent constructor
1436 OO
.ui
.Layout
.parent
.call( this, config
);
1438 // Mixin constructors
1439 OO
.EventEmitter
.call( this );
1442 this.$element
.addClass( 'oo-ui-layout' );
1447 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1448 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1451 * Widgets are compositions of one or more OOjs UI elements that users can both view
1452 * and interact with. All widgets can be configured and modified via a standard API,
1453 * and their state can change dynamically according to a model.
1457 * @extends OO.ui.Element
1458 * @mixins OO.EventEmitter
1461 * @param {Object} [config] Configuration options
1462 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1463 * appearance reflects this state.
1465 OO
.ui
.Widget
= function OoUiWidget( config
) {
1466 // Initialize config
1467 config
= $.extend( { disabled
: false }, config
);
1469 // Parent constructor
1470 OO
.ui
.Widget
.parent
.call( this, config
);
1472 // Mixin constructors
1473 OO
.EventEmitter
.call( this );
1476 this.disabled
= null;
1477 this.wasDisabled
= null;
1480 this.$element
.addClass( 'oo-ui-widget' );
1481 this.setDisabled( !!config
.disabled
);
1486 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1487 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1489 /* Static Properties */
1492 * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
1493 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1498 * @property {boolean}
1500 OO
.ui
.Widget
.static.supportsSimpleLabel
= false;
1507 * A 'disable' event is emitted when the disabled state of the widget changes
1508 * (i.e. on disable **and** enable).
1510 * @param {boolean} disabled Widget is disabled
1516 * A 'toggle' event is emitted when the visibility of the widget changes.
1518 * @param {boolean} visible Widget is visible
1524 * Check if the widget is disabled.
1526 * @return {boolean} Widget is disabled
1528 OO
.ui
.Widget
.prototype.isDisabled = function () {
1529 return this.disabled
;
1533 * Set the 'disabled' state of the widget.
1535 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1537 * @param {boolean} disabled Disable widget
1540 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1543 this.disabled
= !!disabled
;
1544 isDisabled
= this.isDisabled();
1545 if ( isDisabled
!== this.wasDisabled
) {
1546 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1547 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1548 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1549 this.emit( 'disable', isDisabled
);
1550 this.updateThemeClasses();
1552 this.wasDisabled
= isDisabled
;
1558 * Update the disabled state, in case of changes in parent widget.
1562 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1563 this.setDisabled( this.disabled
);
1575 OO
.ui
.Theme
= function OoUiTheme() {};
1579 OO
.initClass( OO
.ui
.Theme
);
1584 * Get a list of classes to be applied to a widget.
1586 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1587 * otherwise state transitions will not work properly.
1589 * @param {OO.ui.Element} element Element for which to get classes
1590 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1592 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1593 return { on
: [], off
: [] };
1597 * Update CSS classes provided by the theme.
1599 * For elements with theme logic hooks, this should be called any time there's a state change.
1601 * @param {OO.ui.Element} element Element for which to update classes
1603 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1604 var $elements
= $( [] ),
1605 classes
= this.getElementClasses( element
);
1607 if ( element
.$icon
) {
1608 $elements
= $elements
.add( element
.$icon
);
1610 if ( element
.$indicator
) {
1611 $elements
= $elements
.add( element
.$indicator
);
1615 .removeClass( classes
.off
.join( ' ' ) )
1616 .addClass( classes
.on
.join( ' ' ) );
1620 * Get the transition duration in milliseconds for dialogs opening/closing
1622 * The dialog should be fully rendered this many milliseconds after the
1623 * ready process has executed.
1625 * @return {number} Transition duration in milliseconds
1627 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1632 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1633 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1634 * order in which users will navigate through the focusable elements via the "tab" key.
1637 * // TabIndexedElement is mixed into the ButtonWidget class
1638 * // to provide a tabIndex property.
1639 * var button1 = new OO.ui.ButtonWidget( {
1643 * var button2 = new OO.ui.ButtonWidget( {
1647 * var button3 = new OO.ui.ButtonWidget( {
1651 * var button4 = new OO.ui.ButtonWidget( {
1655 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1661 * @param {Object} [config] Configuration options
1662 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1663 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1664 * functionality will be applied to it instead.
1665 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1666 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1667 * to remove the element from the tab-navigation flow.
1669 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1670 // Configuration initialization
1671 config
= $.extend( { tabIndex
: 0 }, config
);
1674 this.$tabIndexed
= null;
1675 this.tabIndex
= null;
1678 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1681 this.setTabIndex( config
.tabIndex
);
1682 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1687 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1692 * Set the element that should use the tabindex functionality.
1694 * This method is used to retarget a tabindex mixin so that its functionality applies
1695 * to the specified element. If an element is currently using the functionality, the mixin’s
1696 * effect on that element is removed before the new element is set up.
1698 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1701 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1702 var tabIndex
= this.tabIndex
;
1703 // Remove attributes from old $tabIndexed
1704 this.setTabIndex( null );
1705 // Force update of new $tabIndexed
1706 this.$tabIndexed
= $tabIndexed
;
1707 this.tabIndex
= tabIndex
;
1708 return this.updateTabIndex();
1712 * Set the value of the tabindex.
1714 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
1717 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1718 tabIndex
= typeof tabIndex
=== 'number' ? tabIndex
: null;
1720 if ( this.tabIndex
!== tabIndex
) {
1721 this.tabIndex
= tabIndex
;
1722 this.updateTabIndex();
1729 * Update the `tabindex` attribute, in case of changes to tab index or
1735 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1736 if ( this.$tabIndexed
) {
1737 if ( this.tabIndex
!== null ) {
1738 // Do not index over disabled elements
1739 this.$tabIndexed
.attr( {
1740 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1741 // Support: ChromeVox and NVDA
1742 // These do not seem to inherit aria-disabled from parent elements
1743 'aria-disabled': this.isDisabled().toString()
1746 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1753 * Handle disable events.
1756 * @param {boolean} disabled Element is disabled
1758 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1759 this.updateTabIndex();
1763 * Get the value of the tabindex.
1765 * @return {number|null} Tabindex value
1767 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
1768 return this.tabIndex
;
1772 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
1773 * interface element that can be configured with access keys for accessibility.
1774 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
1776 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
1782 * @param {Object} [config] Configuration options
1783 * @cfg {jQuery} [$button] The button element created by the class.
1784 * If this configuration is omitted, the button element will use a generated `<a>`.
1785 * @cfg {boolean} [framed=true] Render the button with a frame
1787 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
1788 // Configuration initialization
1789 config
= config
|| {};
1792 this.$button
= null;
1794 this.active
= config
.active
!== undefined && config
.active
;
1795 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
1796 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
1797 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
1798 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
1799 this.onClickHandler
= this.onClick
.bind( this );
1800 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
1803 this.$element
.addClass( 'oo-ui-buttonElement' );
1804 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
1805 this.setButtonElement( config
.$button
|| $( '<a>' ) );
1810 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
1812 /* Static Properties */
1815 * Cancel mouse down events.
1817 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
1818 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
1819 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
1824 * @property {boolean}
1826 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
1831 * A 'click' event is emitted when the button element is clicked.
1839 * Set the button element.
1841 * This method is used to retarget a button mixin so that its functionality applies to
1842 * the specified button element instead of the one created by the class. If a button element
1843 * is already set, the method will remove the mixin’s effect on that element.
1845 * @param {jQuery} $button Element to use as button
1847 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
1848 if ( this.$button
) {
1850 .removeClass( 'oo-ui-buttonElement-button' )
1851 .removeAttr( 'role accesskey' )
1853 mousedown
: this.onMouseDownHandler
,
1854 keydown
: this.onKeyDownHandler
,
1855 click
: this.onClickHandler
,
1856 keypress
: this.onKeyPressHandler
1860 this.$button
= $button
1861 .addClass( 'oo-ui-buttonElement-button' )
1863 mousedown
: this.onMouseDownHandler
,
1864 keydown
: this.onKeyDownHandler
,
1865 click
: this.onClickHandler
,
1866 keypress
: this.onKeyPressHandler
1869 // Add `role="button"` on `<a>` elements, where it's needed
1870 // `toUppercase()` is added for XHTML documents
1871 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
1872 this.$button
.attr( 'role', 'button' );
1877 * Handles mouse down events.
1880 * @param {jQuery.Event} e Mouse down event
1882 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
1883 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
1886 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
1887 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
1888 // reliably remove the pressed class
1889 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
1890 // Prevent change of focus unless specifically configured otherwise
1891 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
1897 * Handles mouse up events.
1900 * @param {MouseEvent} e Mouse up event
1902 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
1903 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
1906 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
1907 // Stop listening for mouseup, since we only needed this once
1908 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
1912 * Handles mouse click events.
1915 * @param {jQuery.Event} e Mouse click event
1918 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
1919 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
1920 if ( this.emit( 'click' ) ) {
1927 * Handles key down events.
1930 * @param {jQuery.Event} e Key down event
1932 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
1933 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
1936 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
1937 // Run the keyup handler no matter where the key is when the button is let go, so we can
1938 // reliably remove the pressed class
1939 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
1943 * Handles key up events.
1946 * @param {KeyboardEvent} e Key up event
1948 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
1949 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
1952 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
1953 // Stop listening for keyup, since we only needed this once
1954 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
1958 * Handles key press events.
1961 * @param {jQuery.Event} e Key press event
1964 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
1965 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
1966 if ( this.emit( 'click' ) ) {
1973 * Check if button has a frame.
1975 * @return {boolean} Button is framed
1977 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
1982 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
1984 * @param {boolean} [framed] Make button framed, omit to toggle
1987 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
1988 framed
= framed
=== undefined ? !this.framed
: !!framed
;
1989 if ( framed
!== this.framed
) {
1990 this.framed
= framed
;
1992 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
1993 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
1994 this.updateThemeClasses();
2001 * Set the button's active state.
2003 * The active state can be set on:
2005 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2006 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2007 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2010 * @param {boolean} value Make button active
2013 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2014 this.active
= !!value
;
2015 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2016 this.updateThemeClasses();
2021 * Check if the button is active
2024 * @return {boolean} The button is active
2026 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2031 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2032 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2033 * items from the group is done through the interface the class provides.
2034 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2036 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
2042 * @param {Object} [config] Configuration options
2043 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2044 * is omitted, the group element will use a generated `<div>`.
2046 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2047 // Configuration initialization
2048 config
= config
|| {};
2053 this.aggregateItemEvents
= {};
2056 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2064 * A change event is emitted when the set of selected items changes.
2066 * @param {OO.ui.Element[]} items Items currently in the group
2072 * Set the group element.
2074 * If an element is already set, items will be moved to the new element.
2076 * @param {jQuery} $group Element to use as group
2078 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2081 this.$group
= $group
;
2082 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2083 this.$group
.append( this.items
[ i
].$element
);
2088 * Check if a group contains no items.
2090 * @return {boolean} Group is empty
2092 OO
.ui
.mixin
.GroupElement
.prototype.isEmpty = function () {
2093 return !this.items
.length
;
2097 * Get all items in the group.
2099 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
2100 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
2103 * @return {OO.ui.Element[]} An array of items.
2105 OO
.ui
.mixin
.GroupElement
.prototype.getItems = function () {
2106 return this.items
.slice( 0 );
2110 * Get an item by its data.
2112 * Only the first item with matching data will be returned. To return all matching items,
2113 * use the #getItemsFromData method.
2115 * @param {Object} data Item data to search for
2116 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2118 OO
.ui
.mixin
.GroupElement
.prototype.getItemFromData = function ( data
) {
2120 hash
= OO
.getHash( data
);
2122 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2123 item
= this.items
[ i
];
2124 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2133 * Get items by their data.
2135 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2137 * @param {Object} data Item data to search for
2138 * @return {OO.ui.Element[]} Items with equivalent data
2140 OO
.ui
.mixin
.GroupElement
.prototype.getItemsFromData = function ( data
) {
2142 hash
= OO
.getHash( data
),
2145 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2146 item
= this.items
[ i
];
2147 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2156 * Aggregate the events emitted by the group.
2158 * When events are aggregated, the group will listen to all contained items for the event,
2159 * and then emit the event under a new name. The new event will contain an additional leading
2160 * parameter containing the item that emitted the original event. Other arguments emitted from
2161 * the original event are passed through.
2163 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
2164 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
2165 * A `null` value will remove aggregated events.
2167 * @throws {Error} An error is thrown if aggregation already exists.
2169 OO
.ui
.mixin
.GroupElement
.prototype.aggregate = function ( events
) {
2170 var i
, len
, item
, add
, remove
, itemEvent
, groupEvent
;
2172 for ( itemEvent
in events
) {
2173 groupEvent
= events
[ itemEvent
];
2175 // Remove existing aggregated event
2176 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2177 // Don't allow duplicate aggregations
2179 throw new Error( 'Duplicate item event aggregation for ' + itemEvent
);
2181 // Remove event aggregation from existing items
2182 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2183 item
= this.items
[ i
];
2184 if ( item
.connect
&& item
.disconnect
) {
2186 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2187 item
.disconnect( this, remove
);
2190 // Prevent future items from aggregating event
2191 delete this.aggregateItemEvents
[ itemEvent
];
2194 // Add new aggregate event
2196 // Make future items aggregate event
2197 this.aggregateItemEvents
[ itemEvent
] = groupEvent
;
2198 // Add event aggregation to existing items
2199 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2200 item
= this.items
[ i
];
2201 if ( item
.connect
&& item
.disconnect
) {
2203 add
[ itemEvent
] = [ 'emit', groupEvent
, item
];
2204 item
.connect( this, add
);
2212 * Add items to the group.
2214 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2215 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2217 * @param {OO.ui.Element[]} items An array of items to add to the group
2218 * @param {number} [index] Index of the insertion point
2221 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2222 var i
, len
, item
, itemEvent
, events
, currentIndex
,
2225 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2228 // Check if item exists then remove it first, effectively "moving" it
2229 currentIndex
= this.items
.indexOf( item
);
2230 if ( currentIndex
>= 0 ) {
2231 this.removeItems( [ item
] );
2232 // Adjust index to compensate for removal
2233 if ( currentIndex
< index
) {
2238 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
2240 for ( itemEvent
in this.aggregateItemEvents
) {
2241 events
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2243 item
.connect( this, events
);
2245 item
.setElementGroup( this );
2246 itemElements
.push( item
.$element
.get( 0 ) );
2249 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2250 this.$group
.append( itemElements
);
2251 this.items
.push
.apply( this.items
, items
);
2252 } else if ( index
=== 0 ) {
2253 this.$group
.prepend( itemElements
);
2254 this.items
.unshift
.apply( this.items
, items
);
2256 this.items
[ index
].$element
.before( itemElements
);
2257 this.items
.splice
.apply( this.items
, [ index
, 0 ].concat( items
) );
2260 this.emit( 'change', this.getItems() );
2265 * Remove the specified items from a group.
2267 * Removed items are detached (not removed) from the DOM so that they may be reused.
2268 * To remove all items from a group, you may wish to use the #clearItems method instead.
2270 * @param {OO.ui.Element[]} items An array of items to remove
2273 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2274 var i
, len
, item
, index
, events
, itemEvent
;
2276 // Remove specific items
2277 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2279 index
= this.items
.indexOf( item
);
2280 if ( index
!== -1 ) {
2281 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
2283 for ( itemEvent
in this.aggregateItemEvents
) {
2284 events
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2286 item
.disconnect( this, events
);
2288 item
.setElementGroup( null );
2289 this.items
.splice( index
, 1 );
2290 item
.$element
.detach();
2294 this.emit( 'change', this.getItems() );
2299 * Clear all items from the group.
2301 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2302 * To remove only a subset of items from a group, use the #removeItems method.
2306 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2307 var i
, len
, item
, remove
, itemEvent
;
2310 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2311 item
= this.items
[ i
];
2313 item
.connect
&& item
.disconnect
&&
2314 !$.isEmptyObject( this.aggregateItemEvents
)
2317 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2318 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2320 item
.disconnect( this, remove
);
2322 item
.setElementGroup( null );
2323 item
.$element
.detach();
2326 this.emit( 'change', this.getItems() );
2332 * IconElement is often mixed into other classes to generate an icon.
2333 * Icons are graphics, about the size of normal text. They are used to aid the user
2334 * in locating a control or to convey information in a space-efficient way. See the
2335 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2336 * included in the library.
2338 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2344 * @param {Object} [config] Configuration options
2345 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2346 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2347 * the icon element be set to an existing icon instead of the one generated by this class, set a
2348 * value using a jQuery selection. For example:
2350 * // Use a <div> tag instead of a <span>
2352 * // Use an existing icon element instead of the one generated by the class
2353 * $icon: this.$element
2354 * // Use an icon element from a child widget
2355 * $icon: this.childwidget.$element
2356 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2357 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2358 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2359 * by the user's language.
2361 * Example of an i18n map:
2363 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2364 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2365 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2366 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2367 * text. The icon title is displayed when users move the mouse over the icon.
2369 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2370 // Configuration initialization
2371 config
= config
|| {};
2376 this.iconTitle
= null;
2379 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2380 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2381 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2386 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2388 /* Static Properties */
2391 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2392 * for i18n purposes and contains a `default` icon name and additional names keyed by
2393 * language code. The `default` name is used when no icon is keyed by the user's language.
2395 * Example of an i18n map:
2397 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2399 * Note: the static property will be overridden if the #icon configuration is used.
2403 * @property {Object|string}
2405 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2408 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2409 * function that returns title text, or `null` for no title.
2411 * The static property will be overridden if the #iconTitle configuration is used.
2415 * @property {string|Function|null}
2417 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2422 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2423 * applies to the specified icon element instead of the one created by the class. If an icon
2424 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2425 * and mixin methods will no longer affect the element.
2427 * @param {jQuery} $icon Element to use as icon
2429 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2432 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2433 .removeAttr( 'title' );
2437 .addClass( 'oo-ui-iconElement-icon' )
2438 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2439 if ( this.iconTitle
!== null ) {
2440 this.$icon
.attr( 'title', this.iconTitle
);
2443 this.updateThemeClasses();
2447 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2448 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2451 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2452 * by language code, or `null` to remove the icon.
2455 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2456 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2457 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2459 if ( this.icon
!== icon
) {
2461 if ( this.icon
!== null ) {
2462 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2464 if ( icon
!== null ) {
2465 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2471 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2472 this.updateThemeClasses();
2478 * Set the icon title. Use `null` to remove the title.
2480 * @param {string|Function|null} iconTitle A text string used as the icon title,
2481 * a function that returns title text, or `null` for no title.
2484 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2485 iconTitle
= typeof iconTitle
=== 'function' ||
2486 ( typeof iconTitle
=== 'string' && iconTitle
.length
) ?
2487 OO
.ui
.resolveMsg( iconTitle
) : null;
2489 if ( this.iconTitle
!== iconTitle
) {
2490 this.iconTitle
= iconTitle
;
2492 if ( this.iconTitle
!== null ) {
2493 this.$icon
.attr( 'title', iconTitle
);
2495 this.$icon
.removeAttr( 'title' );
2504 * Get the symbolic name of the icon.
2506 * @return {string} Icon name
2508 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2513 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2515 * @return {string} Icon title text
2517 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2518 return this.iconTitle
;
2522 * IndicatorElement is often mixed into other classes to generate an indicator.
2523 * Indicators are small graphics that are generally used in two ways:
2525 * - To draw attention to the status of an item. For example, an indicator might be
2526 * used to show that an item in a list has errors that need to be resolved.
2527 * - To clarify the function of a control that acts in an exceptional way (a button
2528 * that opens a menu instead of performing an action directly, for example).
2530 * For a list of indicators included in the library, please see the
2531 * [OOjs UI documentation on MediaWiki] [1].
2533 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2539 * @param {Object} [config] Configuration options
2540 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2541 * configuration is omitted, the indicator element will use a generated `<span>`.
2542 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2543 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2545 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2546 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2547 * or a function that returns title text. The indicator title is displayed when users move
2548 * the mouse over the indicator.
2550 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2551 // Configuration initialization
2552 config
= config
|| {};
2555 this.$indicator
= null;
2556 this.indicator
= null;
2557 this.indicatorTitle
= null;
2560 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2561 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2562 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2567 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2569 /* Static Properties */
2572 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2573 * The static property will be overridden if the #indicator configuration is used.
2577 * @property {string|null}
2579 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2582 * A text string used as the indicator title, a function that returns title text, or `null`
2583 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2587 * @property {string|Function|null}
2589 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2594 * Set the indicator element.
2596 * If an element is already set, it will be cleaned up before setting up the new element.
2598 * @param {jQuery} $indicator Element to use as indicator
2600 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2601 if ( this.$indicator
) {
2603 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2604 .removeAttr( 'title' );
2607 this.$indicator
= $indicator
2608 .addClass( 'oo-ui-indicatorElement-indicator' )
2609 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2610 if ( this.indicatorTitle
!== null ) {
2611 this.$indicator
.attr( 'title', this.indicatorTitle
);
2614 this.updateThemeClasses();
2618 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2620 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2623 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2624 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2626 if ( this.indicator
!== indicator
) {
2627 if ( this.$indicator
) {
2628 if ( this.indicator
!== null ) {
2629 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2631 if ( indicator
!== null ) {
2632 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2635 this.indicator
= indicator
;
2638 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2639 this.updateThemeClasses();
2645 * Set the indicator title.
2647 * The title is displayed when a user moves the mouse over the indicator.
2649 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2650 * `null` for no indicator title
2653 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2654 indicatorTitle
= typeof indicatorTitle
=== 'function' ||
2655 ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ?
2656 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2658 if ( this.indicatorTitle
!== indicatorTitle
) {
2659 this.indicatorTitle
= indicatorTitle
;
2660 if ( this.$indicator
) {
2661 if ( this.indicatorTitle
!== null ) {
2662 this.$indicator
.attr( 'title', indicatorTitle
);
2664 this.$indicator
.removeAttr( 'title' );
2673 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2675 * @return {string} Symbolic name of indicator
2677 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2678 return this.indicator
;
2682 * Get the indicator title.
2684 * The title is displayed when a user moves the mouse over the indicator.
2686 * @return {string} Indicator title text
2688 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2689 return this.indicatorTitle
;
2693 * LabelElement is often mixed into other classes to generate a label, which
2694 * helps identify the function of an interface element.
2695 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2697 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2703 * @param {Object} [config] Configuration options
2704 * @cfg {jQuery} [$label] The label element created by the class. If this
2705 * configuration is omitted, the label element will use a generated `<span>`.
2706 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2707 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2708 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2709 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2711 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2712 // Configuration initialization
2713 config
= config
|| {};
2720 this.setLabel( config
.label
|| this.constructor.static.label
);
2721 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2726 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2731 * @event labelChange
2732 * @param {string} value
2735 /* Static Properties */
2738 * The label text. The label can be specified as a plaintext string, a function that will
2739 * produce a string in the future, or `null` for no label. The static value will
2740 * be overridden if a label is specified with the #label config option.
2744 * @property {string|Function|null}
2746 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2748 /* Static methods */
2751 * Highlight the first occurrence of the query in the given text
2753 * @param {string} text Text
2754 * @param {string} query Query to find
2755 * @return {jQuery} Text with the first match of the query
2756 * sub-string wrapped in highlighted span
2758 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
) {
2759 var $result
= $( '<span>' ),
2760 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2762 if ( !query
.length
|| offset
=== -1 ) {
2763 return $result
.text( text
);
2766 document
.createTextNode( text
.slice( 0, offset
) ),
2768 .addClass( 'oo-ui-labelElement-label-highlight' )
2769 .text( text
.slice( offset
, offset
+ query
.length
) ),
2770 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2772 return $result
.contents();
2778 * Set the label element.
2780 * If an element is already set, it will be cleaned up before setting up the new element.
2782 * @param {jQuery} $label Element to use as label
2784 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2785 if ( this.$label
) {
2786 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2789 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2790 this.setLabelContent( this.label
);
2796 * An empty string will result in the label being hidden. A string containing only whitespace will
2797 * be converted to a single ` `.
2799 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2800 * text; or null for no label
2803 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2804 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2805 label
= ( ( typeof label
=== 'string' || label
instanceof jQuery
) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2807 if ( this.label
!== label
) {
2808 if ( this.$label
) {
2809 this.setLabelContent( label
);
2812 this.emit( 'labelChange' );
2815 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
);
2821 * Set the label as plain text with a highlighted query
2823 * @param {string} text Text label to set
2824 * @param {string} query Substring of text to highlight
2827 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
) {
2828 return this.setLabel( this.constructor.static.highlightQuery( text
, query
) );
2834 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2835 * text; or null for no label
2837 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2845 * @deprecated since 0.16.0
2847 OO
.ui
.mixin
.LabelElement
.prototype.fitLabel = function () {
2852 * Set the content of the label.
2854 * Do not call this method until after the label element has been set by #setLabelElement.
2857 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2858 * text; or null for no label
2860 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2861 if ( typeof label
=== 'string' ) {
2862 if ( label
.match( /^\s*$/ ) ) {
2863 // Convert whitespace only string to a single non-breaking space
2864 this.$label
.html( ' ' );
2866 this.$label
.text( label
);
2868 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2869 this.$label
.html( label
.toString() );
2870 } else if ( label
instanceof jQuery
) {
2871 this.$label
.empty().append( label
);
2873 this.$label
.empty();
2878 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
2879 * additional functionality to an element created by another class. The class provides
2880 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
2881 * which are used to customize the look and feel of a widget to better describe its
2882 * importance and functionality.
2884 * The library currently contains the following styling flags for general use:
2886 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
2887 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
2888 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
2890 * The flags affect the appearance of the buttons:
2893 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
2894 * var button1 = new OO.ui.ButtonWidget( {
2895 * label: 'Constructive',
2896 * flags: 'constructive'
2898 * var button2 = new OO.ui.ButtonWidget( {
2899 * label: 'Destructive',
2900 * flags: 'destructive'
2902 * var button3 = new OO.ui.ButtonWidget( {
2903 * label: 'Progressive',
2904 * flags: 'progressive'
2906 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
2908 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
2909 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
2911 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2917 * @param {Object} [config] Configuration options
2918 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
2919 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
2920 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2921 * @cfg {jQuery} [$flagged] The flagged element. By default,
2922 * the flagged functionality is applied to the element created by the class ($element).
2923 * If a different element is specified, the flagged functionality will be applied to it instead.
2925 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
2926 // Configuration initialization
2927 config
= config
|| {};
2931 this.$flagged
= null;
2934 this.setFlags( config
.flags
);
2935 this.setFlaggedElement( config
.$flagged
|| this.$element
);
2942 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
2943 * parameter contains the name of each modified flag and indicates whether it was
2946 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
2947 * that the flag was added, `false` that the flag was removed.
2953 * Set the flagged element.
2955 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
2956 * If an element is already set, the method will remove the mixin’s effect on that element.
2958 * @param {jQuery} $flagged Element that should be flagged
2960 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
2961 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
2962 return 'oo-ui-flaggedElement-' + flag
;
2965 if ( this.$flagged
) {
2966 this.$flagged
.removeClass( classNames
);
2969 this.$flagged
= $flagged
.addClass( classNames
);
2973 * Check if the specified flag is set.
2975 * @param {string} flag Name of flag
2976 * @return {boolean} The flag is set
2978 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
2979 // This may be called before the constructor, thus before this.flags is set
2980 return this.flags
&& ( flag
in this.flags
);
2984 * Get the names of all flags set.
2986 * @return {string[]} Flag names
2988 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
2989 // This may be called before the constructor, thus before this.flags is set
2990 return Object
.keys( this.flags
|| {} );
2999 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3000 var flag
, className
,
3003 classPrefix
= 'oo-ui-flaggedElement-';
3005 for ( flag
in this.flags
) {
3006 className
= classPrefix
+ flag
;
3007 changes
[ flag
] = false;
3008 delete this.flags
[ flag
];
3009 remove
.push( className
);
3012 if ( this.$flagged
) {
3013 this.$flagged
.removeClass( remove
.join( ' ' ) );
3016 this.updateThemeClasses();
3017 this.emit( 'flag', changes
);
3023 * Add one or more flags.
3025 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3026 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3027 * be added (`true`) or removed (`false`).
3031 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3032 var i
, len
, flag
, className
,
3036 classPrefix
= 'oo-ui-flaggedElement-';
3038 if ( typeof flags
=== 'string' ) {
3039 className
= classPrefix
+ flags
;
3041 if ( !this.flags
[ flags
] ) {
3042 this.flags
[ flags
] = true;
3043 add
.push( className
);
3045 } else if ( Array
.isArray( flags
) ) {
3046 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3048 className
= classPrefix
+ flag
;
3050 if ( !this.flags
[ flag
] ) {
3051 changes
[ flag
] = true;
3052 this.flags
[ flag
] = true;
3053 add
.push( className
);
3056 } else if ( OO
.isPlainObject( flags
) ) {
3057 for ( flag
in flags
) {
3058 className
= classPrefix
+ flag
;
3059 if ( flags
[ flag
] ) {
3061 if ( !this.flags
[ flag
] ) {
3062 changes
[ flag
] = true;
3063 this.flags
[ flag
] = true;
3064 add
.push( className
);
3068 if ( this.flags
[ flag
] ) {
3069 changes
[ flag
] = false;
3070 delete this.flags
[ flag
];
3071 remove
.push( className
);
3077 if ( this.$flagged
) {
3079 .addClass( add
.join( ' ' ) )
3080 .removeClass( remove
.join( ' ' ) );
3083 this.updateThemeClasses();
3084 this.emit( 'flag', changes
);
3090 * TitledElement is mixed into other classes to provide a `title` attribute.
3091 * Titles are rendered by the browser and are made visible when the user moves
3092 * the mouse over the element. Titles are not visible on touch devices.
3095 * // TitledElement provides a 'title' attribute to the
3096 * // ButtonWidget class
3097 * var button = new OO.ui.ButtonWidget( {
3098 * label: 'Button with Title',
3099 * title: 'I am a button'
3101 * $( 'body' ).append( button.$element );
3107 * @param {Object} [config] Configuration options
3108 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3109 * If this config is omitted, the title functionality is applied to $element, the
3110 * element created by the class.
3111 * @cfg {string|Function} [title] The title text or a function that returns text. If
3112 * this config is omitted, the value of the {@link #static-title static title} property is used.
3114 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3115 // Configuration initialization
3116 config
= config
|| {};
3119 this.$titled
= null;
3123 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3124 this.setTitledElement( config
.$titled
|| this.$element
);
3129 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3131 /* Static Properties */
3134 * The title text, a function that returns text, or `null` for no title. The value of the static property
3135 * is overridden if the #title config option is used.
3139 * @property {string|Function|null}
3141 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3146 * Set the titled element.
3148 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3149 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3151 * @param {jQuery} $titled Element that should use the 'titled' functionality
3153 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3154 if ( this.$titled
) {
3155 this.$titled
.removeAttr( 'title' );
3158 this.$titled
= $titled
;
3160 this.$titled
.attr( 'title', this.title
);
3167 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3170 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3171 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3172 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3174 if ( this.title
!== title
) {
3175 if ( this.$titled
) {
3176 if ( title
!== null ) {
3177 this.$titled
.attr( 'title', title
);
3179 this.$titled
.removeAttr( 'title' );
3191 * @return {string} Title string
3193 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3198 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3199 * Accesskeys allow an user to go to a specific element by using
3200 * a shortcut combination of a browser specific keys + the key
3204 * // AccessKeyedElement provides an 'accesskey' attribute to the
3205 * // ButtonWidget class
3206 * var button = new OO.ui.ButtonWidget( {
3207 * label: 'Button with Accesskey',
3210 * $( 'body' ).append( button.$element );
3216 * @param {Object} [config] Configuration options
3217 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3218 * If this config is omitted, the accesskey functionality is applied to $element, the
3219 * element created by the class.
3220 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3221 * this config is omitted, no accesskey will be added.
3223 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3224 // Configuration initialization
3225 config
= config
|| {};
3228 this.$accessKeyed
= null;
3229 this.accessKey
= null;
3232 this.setAccessKey( config
.accessKey
|| null );
3233 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3238 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3240 /* Static Properties */
3243 * The access key, a function that returns a key, or `null` for no accesskey.
3247 * @property {string|Function|null}
3249 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3254 * Set the accesskeyed element.
3256 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3257 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3259 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3261 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3262 if ( this.$accessKeyed
) {
3263 this.$accessKeyed
.removeAttr( 'accesskey' );
3266 this.$accessKeyed
= $accessKeyed
;
3267 if ( this.accessKey
) {
3268 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3275 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3278 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3279 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3281 if ( this.accessKey
!== accessKey
) {
3282 if ( this.$accessKeyed
) {
3283 if ( accessKey
!== null ) {
3284 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3286 this.$accessKeyed
.removeAttr( 'accesskey' );
3289 this.accessKey
= accessKey
;
3298 * @return {string} accessKey string
3300 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3301 return this.accessKey
;
3305 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3306 * feels, and functionality can be customized via the class’s configuration options
3307 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3310 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3313 * // A button widget
3314 * var button = new OO.ui.ButtonWidget( {
3315 * label: 'Button with Icon',
3317 * iconTitle: 'Remove'
3319 * $( 'body' ).append( button.$element );
3321 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3324 * @extends OO.ui.Widget
3325 * @mixins OO.ui.mixin.ButtonElement
3326 * @mixins OO.ui.mixin.IconElement
3327 * @mixins OO.ui.mixin.IndicatorElement
3328 * @mixins OO.ui.mixin.LabelElement
3329 * @mixins OO.ui.mixin.TitledElement
3330 * @mixins OO.ui.mixin.FlaggedElement
3331 * @mixins OO.ui.mixin.TabIndexedElement
3332 * @mixins OO.ui.mixin.AccessKeyedElement
3335 * @param {Object} [config] Configuration options
3336 * @cfg {boolean} [active=false] Whether button should be shown as active
3337 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3338 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3339 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3341 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3342 // Configuration initialization
3343 config
= config
|| {};
3345 // Parent constructor
3346 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3348 // Mixin constructors
3349 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3350 OO
.ui
.mixin
.IconElement
.call( this, config
);
3351 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3352 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3353 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3354 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3355 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3356 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3361 this.noFollow
= false;
3364 this.connect( this, { disable
: 'onDisable' } );
3367 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3369 .addClass( 'oo-ui-buttonWidget' )
3370 .append( this.$button
);
3371 this.setActive( config
.active
);
3372 this.setHref( config
.href
);
3373 this.setTarget( config
.target
);
3374 this.setNoFollow( config
.noFollow
);
3379 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3380 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3381 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3382 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3383 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3384 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3385 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3386 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3387 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3389 /* Static Properties */
3394 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3399 * Get hyperlink location.
3401 * @return {string} Hyperlink location
3403 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3408 * Get hyperlink target.
3410 * @return {string} Hyperlink target
3412 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3417 * Get search engine traversal hint.
3419 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3421 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3422 return this.noFollow
;
3426 * Set hyperlink location.
3428 * @param {string|null} href Hyperlink location, null to remove
3430 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3431 href
= typeof href
=== 'string' ? href
: null;
3432 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3436 if ( href
!== this.href
) {
3445 * Update the `href` attribute, in case of changes to href or
3451 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3452 if ( this.href
!== null && !this.isDisabled() ) {
3453 this.$button
.attr( 'href', this.href
);
3455 this.$button
.removeAttr( 'href' );
3462 * Handle disable events.
3465 * @param {boolean} disabled Element is disabled
3467 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3472 * Set hyperlink target.
3474 * @param {string|null} target Hyperlink target, null to remove
3476 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3477 target
= typeof target
=== 'string' ? target
: null;
3479 if ( target
!== this.target
) {
3480 this.target
= target
;
3481 if ( target
!== null ) {
3482 this.$button
.attr( 'target', target
);
3484 this.$button
.removeAttr( 'target' );
3492 * Set search engine traversal hint.
3494 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3496 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3497 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3499 if ( noFollow
!== this.noFollow
) {
3500 this.noFollow
= noFollow
;
3502 this.$button
.attr( 'rel', 'nofollow' );
3504 this.$button
.removeAttr( 'rel' );
3511 // Override method visibility hints from ButtonElement
3520 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3521 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3522 * removed, and cleared from the group.
3525 * // Example: A ButtonGroupWidget with two buttons
3526 * var button1 = new OO.ui.PopupButtonWidget( {
3527 * label: 'Select a category',
3530 * $content: $( '<p>List of categories...</p>' ),
3535 * var button2 = new OO.ui.ButtonWidget( {
3538 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3539 * items: [button1, button2]
3541 * $( 'body' ).append( buttonGroup.$element );
3544 * @extends OO.ui.Widget
3545 * @mixins OO.ui.mixin.GroupElement
3548 * @param {Object} [config] Configuration options
3549 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3551 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3552 // Configuration initialization
3553 config
= config
|| {};
3555 // Parent constructor
3556 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3558 // Mixin constructors
3559 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3562 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3563 if ( Array
.isArray( config
.items
) ) {
3564 this.addItems( config
.items
);
3570 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3571 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3574 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3575 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3576 * for a list of icons included in the library.
3579 * // An icon widget with a label
3580 * var myIcon = new OO.ui.IconWidget( {
3584 * // Create a label.
3585 * var iconLabel = new OO.ui.LabelWidget( {
3588 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3590 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3593 * @extends OO.ui.Widget
3594 * @mixins OO.ui.mixin.IconElement
3595 * @mixins OO.ui.mixin.TitledElement
3596 * @mixins OO.ui.mixin.FlaggedElement
3599 * @param {Object} [config] Configuration options
3601 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3602 // Configuration initialization
3603 config
= config
|| {};
3605 // Parent constructor
3606 OO
.ui
.IconWidget
.parent
.call( this, config
);
3608 // Mixin constructors
3609 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3610 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3611 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3614 this.$element
.addClass( 'oo-ui-iconWidget' );
3619 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3620 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3621 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3622 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3624 /* Static Properties */
3626 OO
.ui
.IconWidget
.static.tagName
= 'span';
3629 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3630 * attention to the status of an item or to clarify the function of a control. For a list of
3631 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3634 * // Example of an indicator widget
3635 * var indicator1 = new OO.ui.IndicatorWidget( {
3636 * indicator: 'alert'
3639 * // Create a fieldset layout to add a label
3640 * var fieldset = new OO.ui.FieldsetLayout();
3641 * fieldset.addItems( [
3642 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3644 * $( 'body' ).append( fieldset.$element );
3646 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3649 * @extends OO.ui.Widget
3650 * @mixins OO.ui.mixin.IndicatorElement
3651 * @mixins OO.ui.mixin.TitledElement
3654 * @param {Object} [config] Configuration options
3656 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
3657 // Configuration initialization
3658 config
= config
|| {};
3660 // Parent constructor
3661 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
3663 // Mixin constructors
3664 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
3665 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3668 this.$element
.addClass( 'oo-ui-indicatorWidget' );
3673 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
3674 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
3675 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
3677 /* Static Properties */
3679 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
3682 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3683 * be configured with a `label` option that is set to a string, a label node, or a function:
3685 * - String: a plaintext string
3686 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3687 * label that includes a link or special styling, such as a gray color or additional graphical elements.
3688 * - Function: a function that will produce a string in the future. Functions are used
3689 * in cases where the value of the label is not currently defined.
3691 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3692 * will come into focus when the label is clicked.
3695 * // Examples of LabelWidgets
3696 * var label1 = new OO.ui.LabelWidget( {
3697 * label: 'plaintext label'
3699 * var label2 = new OO.ui.LabelWidget( {
3700 * label: $( '<a href="default.html">jQuery label</a>' )
3702 * // Create a fieldset layout with fields for each example
3703 * var fieldset = new OO.ui.FieldsetLayout();
3704 * fieldset.addItems( [
3705 * new OO.ui.FieldLayout( label1 ),
3706 * new OO.ui.FieldLayout( label2 )
3708 * $( 'body' ).append( fieldset.$element );
3711 * @extends OO.ui.Widget
3712 * @mixins OO.ui.mixin.LabelElement
3713 * @mixins OO.ui.mixin.TitledElement
3716 * @param {Object} [config] Configuration options
3717 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
3718 * Clicking the label will focus the specified input field.
3720 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
3721 // Configuration initialization
3722 config
= config
|| {};
3724 // Parent constructor
3725 OO
.ui
.LabelWidget
.parent
.call( this, config
);
3727 // Mixin constructors
3728 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
3729 OO
.ui
.mixin
.TitledElement
.call( this, config
);
3732 this.input
= config
.input
;
3735 if ( this.input
instanceof OO
.ui
.InputWidget
) {
3736 this.$element
.on( 'click', this.onClick
.bind( this ) );
3740 this.$element
.addClass( 'oo-ui-labelWidget' );
3745 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
3746 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
3747 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
3749 /* Static Properties */
3751 OO
.ui
.LabelWidget
.static.tagName
= 'span';
3756 * Handles label mouse click events.
3759 * @param {jQuery.Event} e Mouse click event
3761 OO
.ui
.LabelWidget
.prototype.onClick = function () {
3762 this.input
.simulateLabelClick();
3767 * PendingElement is a mixin that is used to create elements that notify users that something is happening
3768 * and that they should wait before proceeding. The pending state is visually represented with a pending
3769 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
3770 * field of a {@link OO.ui.TextInputWidget text input widget}.
3772 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
3773 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
3774 * in process dialogs.
3777 * function MessageDialog( config ) {
3778 * MessageDialog.parent.call( this, config );
3780 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
3782 * MessageDialog.static.actions = [
3783 * { action: 'save', label: 'Done', flags: 'primary' },
3784 * { label: 'Cancel', flags: 'safe' }
3787 * MessageDialog.prototype.initialize = function () {
3788 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
3789 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
3790 * 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>' );
3791 * this.$body.append( this.content.$element );
3793 * MessageDialog.prototype.getBodyHeight = function () {
3796 * MessageDialog.prototype.getActionProcess = function ( action ) {
3797 * var dialog = this;
3798 * if ( action === 'save' ) {
3799 * dialog.getActions().get({actions: 'save'})[0].pushPending();
3800 * return new OO.ui.Process()
3802 * .next( function () {
3803 * dialog.getActions().get({actions: 'save'})[0].popPending();
3806 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
3809 * var windowManager = new OO.ui.WindowManager();
3810 * $( 'body' ).append( windowManager.$element );
3812 * var dialog = new MessageDialog();
3813 * windowManager.addWindows( [ dialog ] );
3814 * windowManager.openWindow( dialog );
3820 * @param {Object} [config] Configuration options
3821 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
3823 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
3824 // Configuration initialization
3825 config
= config
|| {};
3829 this.$pending
= null;
3832 this.setPendingElement( config
.$pending
|| this.$element
);
3837 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
3842 * Set the pending element (and clean up any existing one).
3844 * @param {jQuery} $pending The element to set to pending.
3846 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
3847 if ( this.$pending
) {
3848 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3851 this.$pending
= $pending
;
3852 if ( this.pending
> 0 ) {
3853 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3858 * Check if an element is pending.
3860 * @return {boolean} Element is pending
3862 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
3863 return !!this.pending
;
3867 * Increase the pending counter. The pending state will remain active until the counter is zero
3868 * (i.e., the number of calls to #pushPending and #popPending is the same).
3872 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
3873 if ( this.pending
=== 0 ) {
3874 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3875 this.updateThemeClasses();
3883 * Decrease the pending counter. The pending state will remain active until the counter is zero
3884 * (i.e., the number of calls to #pushPending and #popPending is the same).
3888 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
3889 if ( this.pending
=== 1 ) {
3890 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3891 this.updateThemeClasses();
3893 this.pending
= Math
.max( 0, this.pending
- 1 );
3899 * Element that can be automatically clipped to visible boundaries.
3901 * Whenever the element's natural height changes, you have to call
3902 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
3903 * clipping correctly.
3905 * The dimensions of #$clippableContainer will be compared to the boundaries of the
3906 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
3907 * then #$clippable will be given a fixed reduced height and/or width and will be made
3908 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
3909 * but you can build a static footer by setting #$clippableContainer to an element that contains
3910 * #$clippable and the footer.
3916 * @param {Object} [config] Configuration options
3917 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
3918 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
3919 * omit to use #$clippable
3921 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
3922 // Configuration initialization
3923 config
= config
|| {};
3926 this.$clippable
= null;
3927 this.$clippableContainer
= null;
3928 this.clipping
= false;
3929 this.clippedHorizontally
= false;
3930 this.clippedVertically
= false;
3931 this.$clippableScrollableContainer
= null;
3932 this.$clippableScroller
= null;
3933 this.$clippableWindow
= null;
3934 this.idealWidth
= null;
3935 this.idealHeight
= null;
3936 this.onClippableScrollHandler
= this.clip
.bind( this );
3937 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
3940 if ( config
.$clippableContainer
) {
3941 this.setClippableContainer( config
.$clippableContainer
);
3943 this.setClippableElement( config
.$clippable
|| this.$element
);
3949 * Set clippable element.
3951 * If an element is already set, it will be cleaned up before setting up the new element.
3953 * @param {jQuery} $clippable Element to make clippable
3955 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
3956 if ( this.$clippable
) {
3957 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
3958 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
3959 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
3962 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
3967 * Set clippable container.
3969 * This is the container that will be measured when deciding whether to clip. When clipping,
3970 * #$clippable will be resized in order to keep the clippable container fully visible.
3972 * If the clippable container is unset, #$clippable will be used.
3974 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
3976 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
3977 this.$clippableContainer
= $clippableContainer
;
3978 if ( this.$clippable
) {
3986 * Do not turn clipping on until after the element is attached to the DOM and visible.
3988 * @param {boolean} [clipping] Enable clipping, omit to toggle
3991 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
3992 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
3994 if ( this.clipping
!== clipping
) {
3995 this.clipping
= clipping
;
3997 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
3998 // If the clippable container is the root, we have to listen to scroll events and check
3999 // jQuery.scrollTop on the window because of browser inconsistencies
4000 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4001 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4002 this.$clippableScrollableContainer
;
4003 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4004 this.$clippableWindow
= $( this.getElementWindow() )
4005 .on( 'resize', this.onClippableWindowResizeHandler
);
4006 // Initial clip after visible
4009 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4010 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4012 this.$clippableScrollableContainer
= null;
4013 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4014 this.$clippableScroller
= null;
4015 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4016 this.$clippableWindow
= null;
4024 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4026 * @return {boolean} Element will be clipped to the visible area
4028 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4029 return this.clipping
;
4033 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4035 * @return {boolean} Part of the element is being clipped
4037 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4038 return this.clippedHorizontally
|| this.clippedVertically
;
4042 * Check if the right of the element is being clipped by the nearest scrollable container.
4044 * @return {boolean} Part of the element is being clipped
4046 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4047 return this.clippedHorizontally
;
4051 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4053 * @return {boolean} Part of the element is being clipped
4055 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4056 return this.clippedVertically
;
4060 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
4062 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4063 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4065 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4066 this.idealWidth
= width
;
4067 this.idealHeight
= height
;
4069 if ( !this.clipping
) {
4070 // Update dimensions
4071 this.$clippable
.css( { width
: width
, height
: height
} );
4073 // While clipping, idealWidth and idealHeight are not considered
4077 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4078 * when the element's natural height changes.
4080 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4081 * overlapped by, the visible area of the nearest scrollable container.
4083 * Because calling clip() when the natural height changes isn't always possible, we also set
4084 * max-height when the element isn't being clipped. This means that if the element tries to grow
4085 * beyond the edge, something reasonable will happen before clip() is called.
4089 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
4090 var $container
, extraHeight
, extraWidth
, ccOffset
,
4091 $scrollableContainer
, scOffset
, scHeight
, scWidth
,
4092 ccWidth
, scrollerIsWindow
, scrollTop
, scrollLeft
,
4093 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
4094 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
4095 buffer
= 7; // Chosen by fair dice roll
4097 if ( !this.clipping
) {
4098 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4102 $container
= this.$clippableContainer
|| this.$clippable
;
4103 extraHeight
= $container
.outerHeight() - this.$clippable
.outerHeight();
4104 extraWidth
= $container
.outerWidth() - this.$clippable
.outerWidth();
4105 ccOffset
= $container
.offset();
4106 $scrollableContainer
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4107 this.$clippableWindow
: this.$clippableScrollableContainer
;
4108 scOffset
= $scrollableContainer
.offset() || { top
: 0, left
: 0 };
4109 scHeight
= $scrollableContainer
.innerHeight() - buffer
;
4110 scWidth
= $scrollableContainer
.innerWidth() - buffer
;
4111 ccWidth
= $container
.outerWidth() + buffer
;
4112 scrollerIsWindow
= this.$clippableScroller
[ 0 ] === this.$clippableWindow
[ 0 ];
4113 scrollTop
= scrollerIsWindow
? this.$clippableScroller
.scrollTop() : 0;
4114 scrollLeft
= scrollerIsWindow
? this.$clippableScroller
.scrollLeft() : 0;
4115 desiredWidth
= ccOffset
.left
< 0 ?
4116 ccWidth
+ ccOffset
.left
:
4117 ( scOffset
.left
+ scrollLeft
+ scWidth
) - ccOffset
.left
;
4118 desiredHeight
= ( scOffset
.top
+ scrollTop
+ scHeight
) - ccOffset
.top
;
4119 // It should never be desirable to exceed the dimensions of the browser viewport... right?
4120 desiredWidth
= Math
.min( desiredWidth
, document
.documentElement
.clientWidth
);
4121 desiredHeight
= Math
.min( desiredHeight
, document
.documentElement
.clientHeight
);
4122 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
4123 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
4124 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
4125 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
4126 clipWidth
= allotedWidth
< naturalWidth
;
4127 clipHeight
= allotedHeight
< naturalHeight
;
4130 this.$clippable
.css( {
4131 overflowX
: 'scroll',
4132 width
: Math
.max( 0, allotedWidth
),
4136 this.$clippable
.css( {
4138 width
: this.idealWidth
? this.idealWidth
- extraWidth
: '',
4139 maxWidth
: Math
.max( 0, allotedWidth
)
4143 this.$clippable
.css( {
4144 overflowY
: 'scroll',
4145 height
: Math
.max( 0, allotedHeight
),
4149 this.$clippable
.css( {
4151 height
: this.idealHeight
? this.idealHeight
- extraHeight
: '',
4152 maxHeight
: Math
.max( 0, allotedHeight
)
4156 // If we stopped clipping in at least one of the dimensions
4157 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
4158 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4161 this.clippedHorizontally
= clipWidth
;
4162 this.clippedVertically
= clipHeight
;
4168 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4169 * By default, each popup has an anchor that points toward its origin.
4170 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4173 * // A popup widget.
4174 * var popup = new OO.ui.PopupWidget( {
4175 * $content: $( '<p>Hi there!</p>' ),
4180 * $( 'body' ).append( popup.$element );
4181 * // To display the popup, toggle the visibility to 'true'.
4182 * popup.toggle( true );
4184 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4187 * @extends OO.ui.Widget
4188 * @mixins OO.ui.mixin.LabelElement
4189 * @mixins OO.ui.mixin.ClippableElement
4192 * @param {Object} [config] Configuration options
4193 * @cfg {number} [width=320] Width of popup in pixels
4194 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4195 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4196 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
4197 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
4198 * popup is leaning towards the right of the screen.
4199 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
4200 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
4201 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
4202 * sentence in the given language.
4203 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4204 * See the [OOjs UI docs on MediaWiki][3] for an example.
4205 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4206 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4207 * @cfg {jQuery} [$content] Content to append to the popup's body
4208 * @cfg {jQuery} [$footer] Content to append to the popup's footer
4209 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4210 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4211 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4213 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4214 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
4216 * @cfg {boolean} [padded=false] Add padding to the popup's body
4218 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
4219 // Configuration initialization
4220 config
= config
|| {};
4222 // Parent constructor
4223 OO
.ui
.PopupWidget
.parent
.call( this, config
);
4225 // Properties (must be set before ClippableElement constructor call)
4226 this.$body
= $( '<div>' );
4227 this.$popup
= $( '<div>' );
4229 // Mixin constructors
4230 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4231 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
4232 $clippable
: this.$body
,
4233 $clippableContainer
: this.$popup
4237 this.$anchor
= $( '<div>' );
4238 // If undefined, will be computed lazily in updateDimensions()
4239 this.$container
= config
.$container
;
4240 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
4241 this.autoClose
= !!config
.autoClose
;
4242 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
4243 this.transitionTimeout
= null;
4245 this.width
= config
.width
!== undefined ? config
.width
: 320;
4246 this.height
= config
.height
!== undefined ? config
.height
: null;
4247 this.setAlignment( config
.align
);
4248 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
4249 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
4252 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
4253 this.$body
.addClass( 'oo-ui-popupWidget-body' );
4254 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
4256 .addClass( 'oo-ui-popupWidget-popup' )
4257 .append( this.$body
);
4259 .addClass( 'oo-ui-popupWidget' )
4260 .append( this.$popup
, this.$anchor
);
4261 // Move content, which was added to #$element by OO.ui.Widget, to the body
4262 // FIXME This is gross, we should use '$body' or something for the config
4263 if ( config
.$content
instanceof jQuery
) {
4264 this.$body
.append( config
.$content
);
4267 if ( config
.padded
) {
4268 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
4271 if ( config
.head
) {
4272 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
4273 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
4274 this.$head
= $( '<div>' )
4275 .addClass( 'oo-ui-popupWidget-head' )
4276 .append( this.$label
, this.closeButton
.$element
);
4277 this.$popup
.prepend( this.$head
);
4280 if ( config
.$footer
) {
4281 this.$footer
= $( '<div>' )
4282 .addClass( 'oo-ui-popupWidget-footer' )
4283 .append( config
.$footer
);
4284 this.$popup
.append( this.$footer
);
4287 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
4288 // that reference properties not initialized at that time of parent class construction
4289 // TODO: Find a better way to handle post-constructor setup
4290 this.visible
= false;
4291 this.$element
.addClass( 'oo-ui-element-hidden' );
4296 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
4297 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
4298 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
4303 * Handles mouse down events.
4306 * @param {MouseEvent} e Mouse down event
4308 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
4311 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
4313 this.toggle( false );
4318 * Bind mouse down listener.
4322 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
4323 // Capture clicks outside popup
4324 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
4328 * Handles close button click events.
4332 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
4333 if ( this.isVisible() ) {
4334 this.toggle( false );
4339 * Unbind mouse down listener.
4343 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
4344 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
4348 * Handles key down events.
4351 * @param {KeyboardEvent} e Key down event
4353 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
4355 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
4358 this.toggle( false );
4360 e
.stopPropagation();
4365 * Bind key down listener.
4369 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
4370 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4374 * Unbind key down listener.
4378 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
4379 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4383 * Show, hide, or toggle the visibility of the anchor.
4385 * @param {boolean} [show] Show anchor, omit to toggle
4387 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
4388 show
= show
=== undefined ? !this.anchored
: !!show
;
4390 if ( this.anchored
!== show
) {
4392 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
4394 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
4396 this.anchored
= show
;
4401 * Check if the anchor is visible.
4403 * @return {boolean} Anchor is visible
4405 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
4412 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
4414 show
= show
=== undefined ? !this.isVisible() : !!show
;
4416 change
= show
!== this.isVisible();
4419 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
4423 if ( this.autoClose
) {
4424 this.bindMouseDownListener();
4425 this.bindKeyDownListener();
4427 this.updateDimensions();
4428 this.toggleClipping( true );
4430 this.toggleClipping( false );
4431 if ( this.autoClose
) {
4432 this.unbindMouseDownListener();
4433 this.unbindKeyDownListener();
4442 * Set the size of the popup.
4444 * Changing the size may also change the popup's position depending on the alignment.
4446 * @param {number} width Width in pixels
4447 * @param {number} height Height in pixels
4448 * @param {boolean} [transition=false] Use a smooth transition
4451 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
4453 this.height
= height
!== undefined ? height
: null;
4454 if ( this.isVisible() ) {
4455 this.updateDimensions( transition
);
4460 * Update the size and position.
4462 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
4463 * be called automatically.
4465 * @param {boolean} [transition=false] Use a smooth transition
4468 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
4469 var popupOffset
, originOffset
, containerLeft
, containerWidth
, containerRight
,
4470 popupLeft
, popupRight
, overlapLeft
, overlapRight
, anchorWidth
,
4474 if ( !this.$container
) {
4475 // Lazy-initialize $container if not specified in constructor
4476 this.$container
= $( this.getClosestScrollableElementContainer() );
4479 // Set height and width before measuring things, since it might cause our measurements
4480 // to change (e.g. due to scrollbars appearing or disappearing)
4483 height
: this.height
!== null ? this.height
: 'auto'
4486 // If we are in RTL, we need to flip the alignment, unless it is center
4487 if ( align
=== 'forwards' || align
=== 'backwards' ) {
4488 if ( this.$container
.css( 'direction' ) === 'rtl' ) {
4489 align
= ( { forwards
: 'force-left', backwards
: 'force-right' } )[ this.align
];
4491 align
= ( { forwards
: 'force-right', backwards
: 'force-left' } )[ this.align
];
4496 // Compute initial popupOffset based on alignment
4497 popupOffset
= this.width
* ( { 'force-left': -1, center
: -0.5, 'force-right': 0 } )[ align
];
4499 // Figure out if this will cause the popup to go beyond the edge of the container
4500 originOffset
= this.$element
.offset().left
;
4501 containerLeft
= this.$container
.offset().left
;
4502 containerWidth
= this.$container
.innerWidth();
4503 containerRight
= containerLeft
+ containerWidth
;
4504 popupLeft
= popupOffset
- this.containerPadding
;
4505 popupRight
= popupOffset
+ this.containerPadding
+ this.width
+ this.containerPadding
;
4506 overlapLeft
= ( originOffset
+ popupLeft
) - containerLeft
;
4507 overlapRight
= containerRight
- ( originOffset
+ popupRight
);
4509 // Adjust offset to make the popup not go beyond the edge, if needed
4510 if ( overlapRight
< 0 ) {
4511 popupOffset
+= overlapRight
;
4512 } else if ( overlapLeft
< 0 ) {
4513 popupOffset
-= overlapLeft
;
4516 // Adjust offset to avoid anchor being rendered too close to the edge
4517 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
4518 // TODO: Find a measurement that works for CSS anchors and image anchors
4519 anchorWidth
= this.$anchor
[ 0 ].scrollWidth
* 2;
4520 if ( popupOffset
+ this.width
< anchorWidth
) {
4521 popupOffset
= anchorWidth
- this.width
;
4522 } else if ( -popupOffset
< anchorWidth
) {
4523 popupOffset
= -anchorWidth
;
4526 // Prevent transition from being interrupted
4527 clearTimeout( this.transitionTimeout
);
4529 // Enable transition
4530 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
4533 // Position body relative to anchor
4534 this.$popup
.css( 'margin-left', popupOffset
);
4537 // Prevent transitioning after transition is complete
4538 this.transitionTimeout
= setTimeout( function () {
4539 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4542 // Prevent transitioning immediately
4543 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4546 // Reevaluate clipping state since we've relocated and resized the popup
4553 * Set popup alignment
4555 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4556 * `backwards` or `forwards`.
4558 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
4559 // Validate alignment and transform deprecated values
4560 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
4561 this.align
= { left
: 'force-right', right
: 'force-left' }[ align
] || align
;
4563 this.align
= 'center';
4568 * Get popup alignment
4570 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4571 * `backwards` or `forwards`.
4573 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
4578 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
4579 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
4580 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
4581 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
4587 * @param {Object} [config] Configuration options
4588 * @cfg {Object} [popup] Configuration to pass to popup
4589 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
4591 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
4592 // Configuration initialization
4593 config
= config
|| {};
4596 this.popup
= new OO
.ui
.PopupWidget( $.extend(
4597 { autoClose
: true },
4599 { $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
) }
4608 * @return {OO.ui.PopupWidget} Popup widget
4610 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
4615 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
4616 * which is used to display additional information or options.
4619 * // Example of a popup button.
4620 * var popupButton = new OO.ui.PopupButtonWidget( {
4621 * label: 'Popup button with options',
4624 * $content: $( '<p>Additional options here.</p>' ),
4626 * align: 'force-left'
4629 * // Append the button to the DOM.
4630 * $( 'body' ).append( popupButton.$element );
4633 * @extends OO.ui.ButtonWidget
4634 * @mixins OO.ui.mixin.PopupElement
4637 * @param {Object} [config] Configuration options
4639 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
4640 // Parent constructor
4641 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
4643 // Mixin constructors
4644 OO
.ui
.mixin
.PopupElement
.call( this, config
);
4647 this.connect( this, { click
: 'onAction' } );
4651 .addClass( 'oo-ui-popupButtonWidget' )
4652 .attr( 'aria-haspopup', 'true' )
4653 .append( this.popup
.$element
);
4658 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
4659 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
4664 * Handle the button action being triggered.
4668 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
4669 this.popup
.toggle();
4673 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
4675 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
4680 * @mixins OO.ui.mixin.GroupElement
4683 * @param {Object} [config] Configuration options
4685 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
4686 // Mixin constructors
4687 OO
.ui
.mixin
.GroupElement
.call( this, config
);
4692 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
4697 * Set the disabled state of the widget.
4699 * This will also update the disabled state of child widgets.
4701 * @param {boolean} disabled Disable widget
4704 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
4708 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
4709 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
4711 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
4713 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4714 this.items
[ i
].updateDisabled();
4722 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
4724 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
4725 * allows bidirectional communication.
4727 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
4735 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
4742 * Check if widget is disabled.
4744 * Checks parent if present, making disabled state inheritable.
4746 * @return {boolean} Widget is disabled
4748 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
4749 return this.disabled
||
4750 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
4754 * Set group element is in.
4756 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
4759 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
4761 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
4762 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
4764 // Initialize item disabled states
4765 this.updateDisabled();
4771 * OptionWidgets are special elements that can be selected and configured with data. The
4772 * data is often unique for each option, but it does not have to be. OptionWidgets are used
4773 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
4774 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
4776 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4779 * @extends OO.ui.Widget
4780 * @mixins OO.ui.mixin.ItemWidget
4781 * @mixins OO.ui.mixin.LabelElement
4782 * @mixins OO.ui.mixin.FlaggedElement
4783 * @mixins OO.ui.mixin.AccessKeyedElement
4786 * @param {Object} [config] Configuration options
4788 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
4789 // Configuration initialization
4790 config
= config
|| {};
4792 // Parent constructor
4793 OO
.ui
.OptionWidget
.parent
.call( this, config
);
4795 // Mixin constructors
4796 OO
.ui
.mixin
.ItemWidget
.call( this );
4797 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4798 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
4799 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
4802 this.selected
= false;
4803 this.highlighted
= false;
4804 this.pressed
= false;
4808 .data( 'oo-ui-optionWidget', this )
4809 // Allow programmatic focussing (and by accesskey), but not tabbing
4810 .attr( 'tabindex', '-1' )
4811 .attr( 'role', 'option' )
4812 .attr( 'aria-selected', 'false' )
4813 .addClass( 'oo-ui-optionWidget' )
4814 .append( this.$label
);
4819 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
4820 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
4821 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
4822 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
4823 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
4825 /* Static Properties */
4827 OO
.ui
.OptionWidget
.static.selectable
= true;
4829 OO
.ui
.OptionWidget
.static.highlightable
= true;
4831 OO
.ui
.OptionWidget
.static.pressable
= true;
4833 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
4838 * Check if the option can be selected.
4840 * @return {boolean} Item is selectable
4842 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
4843 return this.constructor.static.selectable
&& !this.isDisabled() && this.isVisible();
4847 * Check if the option can be highlighted. A highlight indicates that the option
4848 * may be selected when a user presses enter or clicks. Disabled items cannot
4851 * @return {boolean} Item is highlightable
4853 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
4854 return this.constructor.static.highlightable
&& !this.isDisabled() && this.isVisible();
4858 * Check if the option can be pressed. The pressed state occurs when a user mouses
4859 * down on an item, but has not yet let go of the mouse.
4861 * @return {boolean} Item is pressable
4863 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
4864 return this.constructor.static.pressable
&& !this.isDisabled() && this.isVisible();
4868 * Check if the option is selected.
4870 * @return {boolean} Item is selected
4872 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
4873 return this.selected
;
4877 * Check if the option is highlighted. A highlight indicates that the
4878 * item may be selected when a user presses enter or clicks.
4880 * @return {boolean} Item is highlighted
4882 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
4883 return this.highlighted
;
4887 * Check if the option is pressed. The pressed state occurs when a user mouses
4888 * down on an item, but has not yet let go of the mouse. The item may appear
4889 * selected, but it will not be selected until the user releases the mouse.
4891 * @return {boolean} Item is pressed
4893 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
4894 return this.pressed
;
4898 * Set the option’s selected state. In general, all modifications to the selection
4899 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
4900 * method instead of this method.
4902 * @param {boolean} [state=false] Select option
4905 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
4906 if ( this.constructor.static.selectable
) {
4907 this.selected
= !!state
;
4909 .toggleClass( 'oo-ui-optionWidget-selected', state
)
4910 .attr( 'aria-selected', state
.toString() );
4911 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
4912 this.scrollElementIntoView();
4914 this.updateThemeClasses();
4920 * Set the option’s highlighted state. In general, all programmatic
4921 * modifications to the highlight should be handled by the
4922 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
4923 * method instead of this method.
4925 * @param {boolean} [state=false] Highlight option
4928 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
4929 if ( this.constructor.static.highlightable
) {
4930 this.highlighted
= !!state
;
4931 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
4932 this.updateThemeClasses();
4938 * Set the option’s pressed state. In general, all
4939 * programmatic modifications to the pressed state should be handled by the
4940 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
4941 * method instead of this method.
4943 * @param {boolean} [state=false] Press option
4946 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
4947 if ( this.constructor.static.pressable
) {
4948 this.pressed
= !!state
;
4949 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
4950 this.updateThemeClasses();
4956 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
4957 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
4958 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
4961 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
4962 * information, please see the [OOjs UI documentation on MediaWiki][1].
4965 * // Example of a select widget with three options
4966 * var select = new OO.ui.SelectWidget( {
4968 * new OO.ui.OptionWidget( {
4970 * label: 'Option One',
4972 * new OO.ui.OptionWidget( {
4974 * label: 'Option Two',
4976 * new OO.ui.OptionWidget( {
4978 * label: 'Option Three',
4982 * $( 'body' ).append( select.$element );
4984 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4988 * @extends OO.ui.Widget
4989 * @mixins OO.ui.mixin.GroupWidget
4992 * @param {Object} [config] Configuration options
4993 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
4994 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
4995 * the [OOjs UI documentation on MediaWiki] [2] for examples.
4996 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4998 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
4999 // Configuration initialization
5000 config
= config
|| {};
5002 // Parent constructor
5003 OO
.ui
.SelectWidget
.parent
.call( this, config
);
5005 // Mixin constructors
5006 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
5009 this.pressed
= false;
5010 this.selecting
= null;
5011 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
5012 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
5013 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
5014 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
5015 this.keyPressBuffer
= '';
5016 this.keyPressBufferTimer
= null;
5017 this.blockMouseOverEvents
= 0;
5020 this.connect( this, {
5024 focusin
: this.onFocus
.bind( this ),
5025 mousedown
: this.onMouseDown
.bind( this ),
5026 mouseover
: this.onMouseOver
.bind( this ),
5027 mouseleave
: this.onMouseLeave
.bind( this )
5032 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
5033 .attr( 'role', 'listbox' );
5034 if ( Array
.isArray( config
.items
) ) {
5035 this.addItems( config
.items
);
5041 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
5042 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
5049 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
5051 * @param {OO.ui.OptionWidget|null} item Highlighted item
5057 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
5058 * pressed state of an option.
5060 * @param {OO.ui.OptionWidget|null} item Pressed item
5066 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
5068 * @param {OO.ui.OptionWidget|null} item Selected item
5073 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
5074 * @param {OO.ui.OptionWidget} item Chosen item
5080 * An `add` event is emitted when options are added to the select with the #addItems method.
5082 * @param {OO.ui.OptionWidget[]} items Added items
5083 * @param {number} index Index of insertion point
5089 * A `remove` event is emitted when options are removed from the select with the #clearItems
5090 * or #removeItems methods.
5092 * @param {OO.ui.OptionWidget[]} items Removed items
5098 * Handle focus events
5101 * @param {jQuery.Event} event
5103 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
5105 if ( event
.target
=== this.$element
[ 0 ] ) {
5106 // This widget was focussed, e.g. by the user tabbing to it.
5107 // The styles for focus state depend on one of the items being selected.
5108 if ( !this.getSelectedItem() ) {
5109 item
= this.getFirstSelectableItem();
5112 // One of the options got focussed (and the event bubbled up here).
5113 // They can't be tabbed to, but they can be activated using accesskeys.
5114 item
= this.getTargetItem( event
);
5118 if ( item
.constructor.static.highlightable
) {
5119 this.highlightItem( item
);
5121 this.selectItem( item
);
5125 if ( event
.target
!== this.$element
[ 0 ] ) {
5126 this.$element
.focus();
5131 * Handle mouse down events.
5134 * @param {jQuery.Event} e Mouse down event
5136 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
5139 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
5140 this.togglePressed( true );
5141 item
= this.getTargetItem( e
);
5142 if ( item
&& item
.isSelectable() ) {
5143 this.pressItem( item
);
5144 this.selecting
= item
;
5145 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
5146 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5153 * Handle mouse up events.
5156 * @param {MouseEvent} e Mouse up event
5158 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
5161 this.togglePressed( false );
5162 if ( !this.selecting
) {
5163 item
= this.getTargetItem( e
);
5164 if ( item
&& item
.isSelectable() ) {
5165 this.selecting
= item
;
5168 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
5169 this.pressItem( null );
5170 this.chooseItem( this.selecting
);
5171 this.selecting
= null;
5174 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
5175 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5181 * Handle mouse move events.
5184 * @param {MouseEvent} e Mouse move event
5186 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
5189 if ( !this.isDisabled() && this.pressed
) {
5190 item
= this.getTargetItem( e
);
5191 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
5192 this.pressItem( item
);
5193 this.selecting
= item
;
5199 * Handle mouse over events.
5202 * @param {jQuery.Event} e Mouse over event
5204 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
5206 if ( this.blockMouseOverEvents
) {
5209 if ( !this.isDisabled() ) {
5210 item
= this.getTargetItem( e
);
5211 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
5217 * Handle mouse leave events.
5220 * @param {jQuery.Event} e Mouse over event
5222 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
5223 if ( !this.isDisabled() ) {
5224 this.highlightItem( null );
5230 * Handle key down events.
5233 * @param {KeyboardEvent} e Key down event
5235 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
5238 currentItem
= this.getHighlightedItem() || this.getSelectedItem();
5240 if ( !this.isDisabled() && this.isVisible() ) {
5241 switch ( e
.keyCode
) {
5242 case OO
.ui
.Keys
.ENTER
:
5243 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5244 // Was only highlighted, now let's select it. No-op if already selected.
5245 this.chooseItem( currentItem
);
5250 case OO
.ui
.Keys
.LEFT
:
5251 this.clearKeyPressBuffer();
5252 nextItem
= this.getRelativeSelectableItem( currentItem
, -1 );
5255 case OO
.ui
.Keys
.DOWN
:
5256 case OO
.ui
.Keys
.RIGHT
:
5257 this.clearKeyPressBuffer();
5258 nextItem
= this.getRelativeSelectableItem( currentItem
, 1 );
5261 case OO
.ui
.Keys
.ESCAPE
:
5262 case OO
.ui
.Keys
.TAB
:
5263 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5264 currentItem
.setHighlighted( false );
5266 this.unbindKeyDownListener();
5267 this.unbindKeyPressListener();
5268 // Don't prevent tabbing away / defocusing
5274 if ( nextItem
.constructor.static.highlightable
) {
5275 this.highlightItem( nextItem
);
5277 this.chooseItem( nextItem
);
5279 this.scrollItemIntoView( nextItem
);
5284 e
.stopPropagation();
5290 * Bind key down listener.
5294 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
5295 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
5299 * Unbind key down listener.
5303 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
5304 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
5308 * Scroll item into view, preventing spurious mouse highlight actions from happening.
5310 * @param {OO.ui.OptionWidget} item Item to scroll into view
5312 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
5314 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
5315 // and around 100-150 ms after it is finished.
5316 this.blockMouseOverEvents
++;
5317 item
.scrollElementIntoView().done( function () {
5318 setTimeout( function () {
5319 widget
.blockMouseOverEvents
--;
5325 * Clear the key-press buffer
5329 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
5330 if ( this.keyPressBufferTimer
) {
5331 clearTimeout( this.keyPressBufferTimer
);
5332 this.keyPressBufferTimer
= null;
5334 this.keyPressBuffer
= '';
5338 * Handle key press events.
5341 * @param {KeyboardEvent} e Key press event
5343 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
5344 var c
, filter
, item
;
5346 if ( !e
.charCode
) {
5347 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
5348 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
5353 if ( String
.fromCodePoint
) {
5354 c
= String
.fromCodePoint( e
.charCode
);
5356 c
= String
.fromCharCode( e
.charCode
);
5359 if ( this.keyPressBufferTimer
) {
5360 clearTimeout( this.keyPressBufferTimer
);
5362 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
5364 item
= this.getHighlightedItem() || this.getSelectedItem();
5366 if ( this.keyPressBuffer
=== c
) {
5367 // Common (if weird) special case: typing "xxxx" will cycle through all
5368 // the items beginning with "x".
5370 item
= this.getRelativeSelectableItem( item
, 1 );
5373 this.keyPressBuffer
+= c
;
5376 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
5377 if ( !item
|| !filter( item
) ) {
5378 item
= this.getRelativeSelectableItem( item
, 1, filter
);
5381 if ( this.isVisible() && item
.constructor.static.highlightable
) {
5382 this.highlightItem( item
);
5384 this.chooseItem( item
);
5386 this.scrollItemIntoView( item
);
5390 e
.stopPropagation();
5394 * Get a matcher for the specific string
5397 * @param {string} s String to match against items
5398 * @param {boolean} [exact=false] Only accept exact matches
5399 * @return {Function} function ( OO.ui.OptionItem ) => boolean
5401 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
5404 if ( s
.normalize
) {
5407 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
5408 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
5412 re
= new RegExp( re
, 'i' );
5413 return function ( item
) {
5414 var l
= item
.getLabel();
5415 if ( typeof l
!== 'string' ) {
5416 l
= item
.$label
.text();
5418 if ( l
.normalize
) {
5421 return re
.test( l
);
5426 * Bind key press listener.
5430 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
5431 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
5435 * Unbind key down listener.
5437 * If you override this, be sure to call this.clearKeyPressBuffer() from your
5442 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
5443 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
5444 this.clearKeyPressBuffer();
5448 * Visibility change handler
5451 * @param {boolean} visible
5453 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
5455 this.clearKeyPressBuffer();
5460 * Get the closest item to a jQuery.Event.
5463 * @param {jQuery.Event} e
5464 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
5466 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
5467 return $( e
.target
).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
5471 * Get selected item.
5473 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
5475 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
5478 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5479 if ( this.items
[ i
].isSelected() ) {
5480 return this.items
[ i
];
5487 * Get highlighted item.
5489 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
5491 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
5494 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5495 if ( this.items
[ i
].isHighlighted() ) {
5496 return this.items
[ i
];
5503 * Toggle pressed state.
5505 * Press is a state that occurs when a user mouses down on an item, but
5506 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
5507 * until the user releases the mouse.
5509 * @param {boolean} pressed An option is being pressed
5511 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
5512 if ( pressed
=== undefined ) {
5513 pressed
= !this.pressed
;
5515 if ( pressed
!== this.pressed
) {
5517 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
5518 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
5519 this.pressed
= pressed
;
5524 * Highlight an option. If the `item` param is omitted, no options will be highlighted
5525 * and any existing highlight will be removed. The highlight is mutually exclusive.
5527 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
5531 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
5532 var i
, len
, highlighted
,
5535 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5536 highlighted
= this.items
[ i
] === item
;
5537 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
5538 this.items
[ i
].setHighlighted( highlighted
);
5543 this.emit( 'highlight', item
);
5550 * Fetch an item by its label.
5552 * @param {string} label Label of the item to select.
5553 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5554 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
5556 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
5558 len
= this.items
.length
,
5559 filter
= this.getItemMatcher( label
, true );
5561 for ( i
= 0; i
< len
; i
++ ) {
5562 item
= this.items
[ i
];
5563 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5570 filter
= this.getItemMatcher( label
, false );
5571 for ( i
= 0; i
< len
; i
++ ) {
5572 item
= this.items
[ i
];
5573 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5589 * Programmatically select an option by its label. If the item does not exist,
5590 * all options will be deselected.
5592 * @param {string} [label] Label of the item to select.
5593 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5597 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
5598 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
5599 if ( label
=== undefined || !itemFromLabel
) {
5600 return this.selectItem();
5602 return this.selectItem( itemFromLabel
);
5606 * Programmatically select an option by its data. If the `data` parameter is omitted,
5607 * or if the item does not exist, all options will be deselected.
5609 * @param {Object|string} [data] Value of the item to select, omit to deselect all
5613 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
5614 var itemFromData
= this.getItemFromData( data
);
5615 if ( data
=== undefined || !itemFromData
) {
5616 return this.selectItem();
5618 return this.selectItem( itemFromData
);
5622 * Programmatically select an option by its reference. If the `item` parameter is omitted,
5623 * all options will be deselected.
5625 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
5629 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
5630 var i
, len
, selected
,
5633 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5634 selected
= this.items
[ i
] === item
;
5635 if ( this.items
[ i
].isSelected() !== selected
) {
5636 this.items
[ i
].setSelected( selected
);
5641 this.emit( 'select', item
);
5650 * Press is a state that occurs when a user mouses down on an item, but has not
5651 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
5652 * releases the mouse.
5654 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
5658 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
5659 var i
, len
, pressed
,
5662 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5663 pressed
= this.items
[ i
] === item
;
5664 if ( this.items
[ i
].isPressed() !== pressed
) {
5665 this.items
[ i
].setPressed( pressed
);
5670 this.emit( 'press', item
);
5679 * Note that ‘choose’ should never be modified programmatically. A user can choose
5680 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
5681 * use the #selectItem method.
5683 * This method is identical to #selectItem, but may vary in subclasses that take additional action
5684 * when users choose an item with the keyboard or mouse.
5686 * @param {OO.ui.OptionWidget} item Item to choose
5690 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
5692 this.selectItem( item
);
5693 this.emit( 'choose', item
);
5700 * Get an option by its position relative to the specified item (or to the start of the option array,
5701 * if item is `null`). The direction in which to search through the option array is specified with a
5702 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
5703 * `null` if there are no options in the array.
5705 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
5706 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
5707 * @param {Function} [filter] Only consider items for which this function returns
5708 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
5709 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
5711 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
, filter
) {
5712 var currentIndex
, nextIndex
, i
,
5713 increase
= direction
> 0 ? 1 : -1,
5714 len
= this.items
.length
;
5716 if ( item
instanceof OO
.ui
.OptionWidget
) {
5717 currentIndex
= this.items
.indexOf( item
);
5718 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
5720 // If no item is selected and moving forward, start at the beginning.
5721 // If moving backward, start at the end.
5722 nextIndex
= direction
> 0 ? 0 : len
- 1;
5725 for ( i
= 0; i
< len
; i
++ ) {
5726 item
= this.items
[ nextIndex
];
5728 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
5729 ( !filter
|| filter( item
) )
5733 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
5739 * Get the next selectable item or `null` if there are no selectable items.
5740 * Disabled options and menu-section markers and breaks are not selectable.
5742 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
5744 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
5745 return this.getRelativeSelectableItem( null, 1 );
5749 * Add an array of options to the select. Optionally, an index number can be used to
5750 * specify an insertion point.
5752 * @param {OO.ui.OptionWidget[]} items Items to add
5753 * @param {number} [index] Index to insert items after
5757 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
5759 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
5761 // Always provide an index, even if it was omitted
5762 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
5768 * Remove the specified array of options from the select. Options will be detached
5769 * from the DOM, not removed, so they can be reused later. To remove all options from
5770 * the select, you may wish to use the #clearItems method instead.
5772 * @param {OO.ui.OptionWidget[]} items Items to remove
5776 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
5779 // Deselect items being removed
5780 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
5782 if ( item
.isSelected() ) {
5783 this.selectItem( null );
5788 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
5790 this.emit( 'remove', items
);
5796 * Clear all options from the select. Options will be detached from the DOM, not removed,
5797 * so that they can be reused later. To remove a subset of options from the select, use
5798 * the #removeItems method.
5803 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
5804 var items
= this.items
.slice();
5807 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
5810 this.selectItem( null );
5812 this.emit( 'remove', items
);
5818 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
5819 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
5820 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
5821 * options. For more information about options and selects, please see the
5822 * [OOjs UI documentation on MediaWiki][1].
5825 * // Decorated options in a select widget
5826 * var select = new OO.ui.SelectWidget( {
5828 * new OO.ui.DecoratedOptionWidget( {
5830 * label: 'Option with icon',
5833 * new OO.ui.DecoratedOptionWidget( {
5835 * label: 'Option with indicator',
5840 * $( 'body' ).append( select.$element );
5842 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5845 * @extends OO.ui.OptionWidget
5846 * @mixins OO.ui.mixin.IconElement
5847 * @mixins OO.ui.mixin.IndicatorElement
5850 * @param {Object} [config] Configuration options
5852 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
5853 // Parent constructor
5854 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
5856 // Mixin constructors
5857 OO
.ui
.mixin
.IconElement
.call( this, config
);
5858 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
5862 .addClass( 'oo-ui-decoratedOptionWidget' )
5863 .prepend( this.$icon
)
5864 .append( this.$indicator
);
5869 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
5870 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
5871 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
5874 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
5875 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
5876 * the [OOjs UI documentation on MediaWiki] [1] for more information.
5878 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
5881 * @extends OO.ui.DecoratedOptionWidget
5884 * @param {Object} [config] Configuration options
5886 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
5887 // Configuration initialization
5888 config
= $.extend( { icon
: 'check' }, config
);
5890 // Parent constructor
5891 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
5895 .attr( 'role', 'menuitem' )
5896 .addClass( 'oo-ui-menuOptionWidget' );
5901 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
5903 /* Static Properties */
5905 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
5908 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
5909 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
5912 * var myDropdown = new OO.ui.DropdownWidget( {
5915 * new OO.ui.MenuSectionOptionWidget( {
5918 * new OO.ui.MenuOptionWidget( {
5920 * label: 'Welsh Corgi'
5922 * new OO.ui.MenuOptionWidget( {
5924 * label: 'Standard Poodle'
5926 * new OO.ui.MenuSectionOptionWidget( {
5929 * new OO.ui.MenuOptionWidget( {
5936 * $( 'body' ).append( myDropdown.$element );
5939 * @extends OO.ui.DecoratedOptionWidget
5942 * @param {Object} [config] Configuration options
5944 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
5945 // Parent constructor
5946 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
5949 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' );
5954 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
5956 /* Static Properties */
5958 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
5960 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
5963 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
5964 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
5965 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
5966 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
5967 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
5968 * and customized to be opened, closed, and displayed as needed.
5970 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
5971 * mouse outside the menu.
5973 * Menus also have support for keyboard interaction:
5975 * - Enter/Return key: choose and select a menu option
5976 * - Up-arrow key: highlight the previous menu option
5977 * - Down-arrow key: highlight the next menu option
5978 * - Esc key: hide the menu
5980 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
5981 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5984 * @extends OO.ui.SelectWidget
5985 * @mixins OO.ui.mixin.ClippableElement
5988 * @param {Object} [config] Configuration options
5989 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
5990 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
5991 * and {@link OO.ui.mixin.LookupElement LookupElement}
5992 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
5993 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
5994 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
5995 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
5996 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
5997 * that button, unless the button (or its parent widget) is passed in here.
5998 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
5999 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
6001 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
6002 // Configuration initialization
6003 config
= config
|| {};
6005 // Parent constructor
6006 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
6008 // Mixin constructors
6009 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
6012 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
6013 this.filterFromInput
= !!config
.filterFromInput
;
6014 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
6015 this.$widget
= config
.widget
? config
.widget
.$element
: null;
6016 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
6017 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
6021 .addClass( 'oo-ui-menuSelectWidget' )
6022 .attr( 'role', 'menu' );
6024 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
6025 // that reference properties not initialized at that time of parent class construction
6026 // TODO: Find a better way to handle post-constructor setup
6027 this.visible
= false;
6028 this.$element
.addClass( 'oo-ui-element-hidden' );
6033 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
6034 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
6039 * Handles document mouse down events.
6042 * @param {MouseEvent} e Mouse down event
6044 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
6046 !OO
.ui
.contains( this.$element
[ 0 ], e
.target
, true ) &&
6047 ( !this.$widget
|| !OO
.ui
.contains( this.$widget
[ 0 ], e
.target
, true ) )
6049 this.toggle( false );
6056 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
6057 var currentItem
= this.getHighlightedItem() || this.getSelectedItem();
6059 if ( !this.isDisabled() && this.isVisible() ) {
6060 switch ( e
.keyCode
) {
6061 case OO
.ui
.Keys
.LEFT
:
6062 case OO
.ui
.Keys
.RIGHT
:
6063 // Do nothing if a text field is associated, arrow keys will be handled natively
6064 if ( !this.$input
) {
6065 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
6068 case OO
.ui
.Keys
.ESCAPE
:
6069 case OO
.ui
.Keys
.TAB
:
6070 if ( currentItem
) {
6071 currentItem
.setHighlighted( false );
6073 this.toggle( false );
6074 // Don't prevent tabbing away, prevent defocusing
6075 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
6077 e
.stopPropagation();
6081 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
6088 * Update menu item visibility after input changes.
6092 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
6094 len
= this.items
.length
,
6095 showAll
= !this.isVisible(),
6096 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
6098 for ( i
= 0; i
< len
; i
++ ) {
6099 item
= this.items
[ i
];
6100 if ( item
instanceof OO
.ui
.OptionWidget
) {
6101 item
.toggle( showAll
|| filter( item
) );
6105 // Reevaluate clipping
6112 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
6113 if ( this.$input
) {
6114 this.$input
.on( 'keydown', this.onKeyDownHandler
);
6116 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
6123 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
6124 if ( this.$input
) {
6125 this.$input
.off( 'keydown', this.onKeyDownHandler
);
6127 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
6134 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
6135 if ( this.$input
) {
6136 if ( this.filterFromInput
) {
6137 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6140 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
6147 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
6148 if ( this.$input
) {
6149 if ( this.filterFromInput
) {
6150 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6151 this.updateItemVisibility();
6154 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
6161 * When a user chooses an item, the menu is closed.
6163 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
6164 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
6166 * @param {OO.ui.OptionWidget} item Item to choose
6169 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
6170 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
6171 this.toggle( false );
6178 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
6180 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
6182 // Reevaluate clipping
6191 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
6193 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
6195 // Reevaluate clipping
6204 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
6206 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
6208 // Reevaluate clipping
6217 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
6220 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
6221 change
= visible
!== this.isVisible();
6224 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
6228 this.bindKeyDownListener();
6229 this.bindKeyPressListener();
6231 this.toggleClipping( true );
6233 if ( this.getSelectedItem() ) {
6234 this.getSelectedItem().scrollElementIntoView( { duration
: 0 } );
6238 if ( this.autoHide
) {
6239 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6242 this.unbindKeyDownListener();
6243 this.unbindKeyPressListener();
6244 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6245 this.toggleClipping( false );
6253 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
6254 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
6255 * users can interact with it.
6257 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6258 * OO.ui.DropdownInputWidget instead.
6261 * // Example: A DropdownWidget with a menu that contains three options
6262 * var dropDown = new OO.ui.DropdownWidget( {
6263 * label: 'Dropdown menu: Select a menu option',
6266 * new OO.ui.MenuOptionWidget( {
6270 * new OO.ui.MenuOptionWidget( {
6274 * new OO.ui.MenuOptionWidget( {
6282 * $( 'body' ).append( dropDown.$element );
6284 * dropDown.getMenu().selectItemByData( 'b' );
6286 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
6288 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
6290 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6293 * @extends OO.ui.Widget
6294 * @mixins OO.ui.mixin.IconElement
6295 * @mixins OO.ui.mixin.IndicatorElement
6296 * @mixins OO.ui.mixin.LabelElement
6297 * @mixins OO.ui.mixin.TitledElement
6298 * @mixins OO.ui.mixin.TabIndexedElement
6301 * @param {Object} [config] Configuration options
6302 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
6303 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
6304 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6305 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
6307 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
6308 // Configuration initialization
6309 config
= $.extend( { indicator
: 'down' }, config
);
6311 // Parent constructor
6312 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
6314 // Properties (must be set before TabIndexedElement constructor call)
6315 this.$handle
= this.$( '<span>' );
6316 this.$overlay
= config
.$overlay
|| this.$element
;
6318 // Mixin constructors
6319 OO
.ui
.mixin
.IconElement
.call( this, config
);
6320 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
6321 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6322 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
6323 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
6326 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend( {
6328 $container
: this.$element
6333 click
: this.onClick
.bind( this ),
6334 keydown
: this.onKeyDown
.bind( this ),
6335 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
6336 keypress
: this.menu
.onKeyPressHandler
,
6337 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
6339 this.menu
.connect( this, {
6340 select
: 'onMenuSelect',
6341 toggle
: 'onMenuToggle'
6346 .addClass( 'oo-ui-dropdownWidget-handle' )
6347 .append( this.$icon
, this.$label
, this.$indicator
);
6349 .addClass( 'oo-ui-dropdownWidget' )
6350 .append( this.$handle
);
6351 this.$overlay
.append( this.menu
.$element
);
6356 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
6357 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
6358 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
6359 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
6360 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
6361 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6368 * @return {OO.ui.MenuSelectWidget} Menu of widget
6370 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
6375 * Handles menu select events.
6378 * @param {OO.ui.MenuOptionWidget} item Selected menu item
6380 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
6384 this.setLabel( null );
6388 selectedLabel
= item
.getLabel();
6390 // If the label is a DOM element, clone it, because setLabel will append() it
6391 if ( selectedLabel
instanceof jQuery
) {
6392 selectedLabel
= selectedLabel
.clone();
6395 this.setLabel( selectedLabel
);
6399 * Handle menu toggle events.
6402 * @param {boolean} isVisible Menu toggle event
6404 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
6405 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
6409 * Handle mouse click events.
6412 * @param {jQuery.Event} e Mouse click event
6414 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
6415 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6422 * Handle key down events.
6425 * @param {jQuery.Event} e Key down event
6427 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
6429 !this.isDisabled() &&
6431 e
.which
=== OO
.ui
.Keys
.ENTER
||
6433 !this.menu
.isVisible() &&
6435 e
.which
=== OO
.ui
.Keys
.SPACE
||
6436 e
.which
=== OO
.ui
.Keys
.UP
||
6437 e
.which
=== OO
.ui
.Keys
.DOWN
6448 * RadioOptionWidget is an option widget that looks like a radio button.
6449 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
6450 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6452 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6455 * @extends OO.ui.OptionWidget
6458 * @param {Object} [config] Configuration options
6460 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
6461 // Configuration initialization
6462 config
= config
|| {};
6464 // Properties (must be done before parent constructor which calls #setDisabled)
6465 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
6467 // Parent constructor
6468 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
6471 // Remove implicit role, we're handling it ourselves
6472 this.radio
.$input
.attr( 'role', 'presentation' );
6474 .addClass( 'oo-ui-radioOptionWidget' )
6475 .attr( 'role', 'radio' )
6476 .attr( 'aria-checked', 'false' )
6477 .removeAttr( 'aria-selected' )
6478 .prepend( this.radio
.$element
);
6483 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
6485 /* Static Properties */
6487 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
6489 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
6491 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
6493 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
6500 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
6501 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
6503 this.radio
.setSelected( state
);
6505 .attr( 'aria-checked', state
.toString() )
6506 .removeAttr( 'aria-selected' );
6514 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
6515 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
6517 this.radio
.setDisabled( this.isDisabled() );
6523 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
6524 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
6525 * an interface for adding, removing and selecting options.
6526 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6528 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6529 * OO.ui.RadioSelectInputWidget instead.
6532 * // A RadioSelectWidget with RadioOptions.
6533 * var option1 = new OO.ui.RadioOptionWidget( {
6535 * label: 'Selected radio option'
6538 * var option2 = new OO.ui.RadioOptionWidget( {
6540 * label: 'Unselected radio option'
6543 * var radioSelect=new OO.ui.RadioSelectWidget( {
6544 * items: [ option1, option2 ]
6547 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
6548 * radioSelect.selectItem( option1 );
6550 * $( 'body' ).append( radioSelect.$element );
6552 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6556 * @extends OO.ui.SelectWidget
6557 * @mixins OO.ui.mixin.TabIndexedElement
6560 * @param {Object} [config] Configuration options
6562 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
6563 // Parent constructor
6564 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
6566 // Mixin constructors
6567 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
6571 focus
: this.bindKeyDownListener
.bind( this ),
6572 blur
: this.unbindKeyDownListener
.bind( this )
6577 .addClass( 'oo-ui-radioSelectWidget' )
6578 .attr( 'role', 'radiogroup' );
6583 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
6584 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6587 * MultioptionWidgets are special elements that can be selected and configured with data. The
6588 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
6589 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6590 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
6592 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Multioptions
6595 * @extends OO.ui.Widget
6596 * @mixins OO.ui.mixin.ItemWidget
6597 * @mixins OO.ui.mixin.LabelElement
6600 * @param {Object} [config] Configuration options
6601 * @cfg {boolean} [selected=false] Whether the option is initially selected
6603 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
6604 // Configuration initialization
6605 config
= config
|| {};
6607 // Parent constructor
6608 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
6610 // Mixin constructors
6611 OO
.ui
.mixin
.ItemWidget
.call( this );
6612 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6615 this.selected
= null;
6619 .addClass( 'oo-ui-multioptionWidget' )
6620 .append( this.$label
);
6621 this.setSelected( config
.selected
);
6626 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
6627 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6628 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
6635 * A change event is emitted when the selected state of the option changes.
6637 * @param {boolean} selected Whether the option is now selected
6643 * Check if the option is selected.
6645 * @return {boolean} Item is selected
6647 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
6648 return this.selected
;
6652 * Set the option’s selected state. In general, all modifications to the selection
6653 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6654 * method instead of this method.
6656 * @param {boolean} [state=false] Select option
6659 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
6661 if ( this.selected
!== state
) {
6662 this.selected
= state
;
6663 this.emit( 'change', state
);
6664 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
6670 * MultiselectWidget allows selecting multiple options from a list.
6672 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
6674 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6678 * @extends OO.ui.Widget
6679 * @mixins OO.ui.mixin.GroupWidget
6682 * @param {Object} [config] Configuration options
6683 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
6685 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
6686 // Parent constructor
6687 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
6689 // Configuration initialization
6690 config
= config
|| {};
6692 // Mixin constructors
6693 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
6696 this.aggregate( { change
: 'select' } );
6697 // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
6698 // by GroupElement only when items are added/removed
6699 this.connect( this, { select
: [ 'emit', 'change' ] } );
6702 if ( config
.items
) {
6703 this.addItems( config
.items
);
6705 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
6706 this.$element
.addClass( 'oo-ui-multiselectWidget' )
6707 .append( this.$group
);
6712 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
6713 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
6720 * A change event is emitted when the set of items changes, or an item is selected or deselected.
6726 * A select event is emitted when an item is selected or deselected.
6732 * Get options that are selected.
6734 * @return {OO.ui.MultioptionWidget[]} Selected options
6736 OO
.ui
.MultiselectWidget
.prototype.getSelectedItems = function () {
6737 return this.items
.filter( function ( item
) {
6738 return item
.isSelected();
6743 * Get the data of options that are selected.
6745 * @return {Object[]|string[]} Values of selected options
6747 OO
.ui
.MultiselectWidget
.prototype.getSelectedItemsData = function () {
6748 return this.getSelectedItems().map( function ( item
) {
6754 * Select options by reference. Options not mentioned in the `items` array will be deselected.
6756 * @param {OO.ui.MultioptionWidget[]} items Items to select
6759 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
6760 this.items
.forEach( function ( item
) {
6761 var selected
= items
.indexOf( item
) !== -1;
6762 item
.setSelected( selected
);
6768 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
6770 * @param {Object[]|string[]} datas Values of items to select
6773 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
6776 items
= datas
.map( function ( data
) {
6777 return widget
.getItemFromData( data
);
6779 this.selectItems( items
);
6784 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
6785 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
6786 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6788 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6791 * @extends OO.ui.MultioptionWidget
6794 * @param {Object} [config] Configuration options
6796 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
6797 // Configuration initialization
6798 config
= config
|| {};
6800 // Properties (must be done before parent constructor which calls #setDisabled)
6801 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
6803 // Parent constructor
6804 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
6807 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
6808 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
6812 .addClass( 'oo-ui-checkboxMultioptionWidget' )
6813 .prepend( this.checkbox
.$element
);
6818 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
6820 /* Static Properties */
6822 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
6827 * Handle checkbox selected state change.
6831 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
6832 this.setSelected( this.checkbox
.isSelected() );
6838 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
6839 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
6840 this.checkbox
.setSelected( state
);
6847 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
6848 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
6849 this.checkbox
.setDisabled( this.isDisabled() );
6856 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
6857 this.checkbox
.focus();
6861 * Handle key down events.
6864 * @param {jQuery.Event} e
6866 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
6868 element
= this.getElementGroup(),
6871 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
6872 nextItem
= element
.getRelativeFocusableItem( this, -1 );
6873 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
6874 nextItem
= element
.getRelativeFocusableItem( this, 1 );
6884 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
6885 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
6886 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
6887 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6889 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6890 * OO.ui.CheckboxMultiselectInputWidget instead.
6893 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
6894 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
6897 * label: 'Selected checkbox'
6900 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
6902 * label: 'Unselected checkbox'
6905 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
6906 * items: [ option1, option2 ]
6909 * $( 'body' ).append( multiselect.$element );
6911 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6914 * @extends OO.ui.MultiselectWidget
6917 * @param {Object} [config] Configuration options
6919 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
6920 // Parent constructor
6921 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
6924 this.$lastClicked
= null;
6927 this.$group
.on( 'click', this.onClick
.bind( this ) );
6931 .addClass( 'oo-ui-checkboxMultiselectWidget' );
6936 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
6941 * Get an option by its position relative to the specified item (or to the start of the option array,
6942 * if item is `null`). The direction in which to search through the option array is specified with a
6943 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6944 * `null` if there are no options in the array.
6946 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6947 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6948 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
6950 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
6951 var currentIndex
, nextIndex
, i
,
6952 increase
= direction
> 0 ? 1 : -1,
6953 len
= this.items
.length
;
6956 currentIndex
= this.items
.indexOf( item
);
6957 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
6959 // If no item is selected and moving forward, start at the beginning.
6960 // If moving backward, start at the end.
6961 nextIndex
= direction
> 0 ? 0 : len
- 1;
6964 for ( i
= 0; i
< len
; i
++ ) {
6965 item
= this.items
[ nextIndex
];
6966 if ( item
&& !item
.isDisabled() ) {
6969 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
6975 * Handle click events on checkboxes.
6977 * @param {jQuery.Event} e
6979 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
6980 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
6981 $lastClicked
= this.$lastClicked
,
6982 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
6983 .not( '.oo-ui-widget-disabled' );
6985 // Allow selecting multiple options at once by Shift-clicking them
6986 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
6987 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
6988 lastClickedIndex
= $options
.index( $lastClicked
);
6989 nowClickedIndex
= $options
.index( $nowClicked
);
6990 // If it's the same item, either the user is being silly, or it's a fake event generated by the
6991 // browser. In either case we don't need custom handling.
6992 if ( nowClickedIndex
!== lastClickedIndex
) {
6994 wasSelected
= items
[ nowClickedIndex
].isSelected();
6995 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
6997 // This depends on the DOM order of the items and the order of the .items array being the same.
6998 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
6999 if ( !items
[ i
].isDisabled() ) {
7000 items
[ i
].setSelected( !wasSelected
);
7003 // For the now-clicked element, use immediate timeout to allow the browser to do its own
7004 // handling first, then set our value. The order in which events happen is different for
7005 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
7006 // non-click actions that change the checkboxes.
7008 setTimeout( function () {
7009 if ( !items
[ nowClickedIndex
].isDisabled() ) {
7010 items
[ nowClickedIndex
].setSelected( !wasSelected
);
7016 if ( $nowClicked
.length
) {
7017 this.$lastClicked
= $nowClicked
;
7022 * Element that will stick under a specified container, even when it is inserted elsewhere in the
7023 * document (for example, in a OO.ui.Window's $overlay).
7025 * The elements's position is automatically calculated and maintained when window is resized or the
7026 * page is scrolled. If you reposition the container manually, you have to call #position to make
7027 * sure the element is still placed correctly.
7029 * As positioning is only possible when both the element and the container are attached to the DOM
7030 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
7031 * the #toggle method to display a floating popup, for example.
7037 * @param {Object} [config] Configuration options
7038 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
7039 * @cfg {jQuery} [$floatableContainer] Node to position below
7041 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
7042 // Configuration initialization
7043 config
= config
|| {};
7046 this.$floatable
= null;
7047 this.$floatableContainer
= null;
7048 this.$floatableWindow
= null;
7049 this.$floatableClosestScrollable
= null;
7050 this.onFloatableScrollHandler
= this.position
.bind( this );
7051 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
7054 this.setFloatableContainer( config
.$floatableContainer
);
7055 this.setFloatableElement( config
.$floatable
|| this.$element
);
7061 * Set floatable element.
7063 * If an element is already set, it will be cleaned up before setting up the new element.
7065 * @param {jQuery} $floatable Element to make floatable
7067 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
7068 if ( this.$floatable
) {
7069 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
7070 this.$floatable
.css( { left
: '', top
: '' } );
7073 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
7078 * Set floatable container.
7080 * The element will be always positioned under the specified container.
7082 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
7084 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
7085 this.$floatableContainer
= $floatableContainer
;
7086 if ( this.$floatable
) {
7092 * Toggle positioning.
7094 * Do not turn positioning on until after the element is attached to the DOM and visible.
7096 * @param {boolean} [positioning] Enable positioning, omit to toggle
7099 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
7100 var closestScrollableOfContainer
, closestScrollableOfFloatable
;
7102 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
7104 if ( this.positioning
!== positioning
) {
7105 this.positioning
= positioning
;
7107 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
7108 closestScrollableOfFloatable
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatable
[ 0 ] );
7109 this.needsCustomPosition
= closestScrollableOfContainer
!== closestScrollableOfFloatable
;
7110 // If the scrollable is the root, we have to listen to scroll events
7111 // on the window because of browser inconsistencies.
7112 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
7113 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
7116 if ( positioning
) {
7117 this.$floatableWindow
= $( this.getElementWindow() );
7118 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
7120 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
7121 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
7123 // Initial position after visible
7126 if ( this.$floatableWindow
) {
7127 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
7128 this.$floatableWindow
= null;
7131 if ( this.$floatableClosestScrollable
) {
7132 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
7133 this.$floatableClosestScrollable
= null;
7136 this.$floatable
.css( { left
: '', top
: '' } );
7144 * Check whether the bottom edge of the given element is within the viewport of the given container.
7147 * @param {jQuery} $element
7148 * @param {jQuery} $container
7151 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
7152 var elemRect
, contRect
,
7153 leftEdgeInBounds
= false,
7154 bottomEdgeInBounds
= false,
7155 rightEdgeInBounds
= false;
7157 elemRect
= $element
[ 0 ].getBoundingClientRect();
7158 if ( $container
[ 0 ] === window
) {
7162 right
: document
.documentElement
.clientWidth
,
7163 bottom
: document
.documentElement
.clientHeight
7166 contRect
= $container
[ 0 ].getBoundingClientRect();
7169 // For completeness, if we still cared about topEdgeInBounds, that'd be:
7170 // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
7171 if ( elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
) {
7172 leftEdgeInBounds
= true;
7174 if ( elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
) {
7175 bottomEdgeInBounds
= true;
7177 if ( elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
) {
7178 rightEdgeInBounds
= true;
7181 // We only care that any part of the bottom edge is visible
7182 return bottomEdgeInBounds
&& ( leftEdgeInBounds
|| rightEdgeInBounds
);
7186 * Position the floatable below its container.
7188 * This should only be done when both of them are attached to the DOM and visible.
7192 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
7195 if ( !this.positioning
) {
7199 if ( !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
) ) {
7200 this.$floatable
.addClass( 'oo-ui-floatableElement-hidden' );
7203 this.$floatable
.removeClass( 'oo-ui-floatableElement-hidden' );
7206 if ( !this.needsCustomPosition
) {
7210 pos
= OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, this.$floatable
.offsetParent() );
7212 // Position under container
7213 pos
.top
+= this.$floatableContainer
.height();
7214 this.$floatable
.css( pos
);
7216 // We updated the position, so re-evaluate the clipping state.
7217 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
7218 // will not notice the need to update itself.)
7219 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
7220 // it not listen to the right events in the right places?
7229 * FloatingMenuSelectWidget is a menu that will stick under a specified
7230 * container, even when it is inserted elsewhere in the document (for example,
7231 * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
7232 * menu from being clipped too aggresively.
7234 * The menu's position is automatically calculated and maintained when the menu
7235 * is toggled or the window is resized.
7237 * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
7240 * @extends OO.ui.MenuSelectWidget
7241 * @mixins OO.ui.mixin.FloatableElement
7244 * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
7245 * Deprecated, omit this parameter and specify `$container` instead.
7246 * @param {Object} [config] Configuration options
7247 * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
7249 OO
.ui
.FloatingMenuSelectWidget
= function OoUiFloatingMenuSelectWidget( inputWidget
, config
) {
7250 // Allow 'inputWidget' parameter and config for backwards compatibility
7251 if ( OO
.isPlainObject( inputWidget
) && config
=== undefined ) {
7252 config
= inputWidget
;
7253 inputWidget
= config
.inputWidget
;
7256 // Configuration initialization
7257 config
= config
|| {};
7259 // Parent constructor
7260 OO
.ui
.FloatingMenuSelectWidget
.parent
.call( this, config
);
7262 // Properties (must be set before mixin constructors)
7263 this.inputWidget
= inputWidget
; // For backwards compatibility
7264 this.$container
= config
.$container
|| this.inputWidget
.$element
;
7266 // Mixins constructors
7267 OO
.ui
.mixin
.FloatableElement
.call( this, $.extend( {}, config
, { $floatableContainer
: this.$container
} ) );
7270 this.$element
.addClass( 'oo-ui-floatingMenuSelectWidget' );
7271 // For backwards compatibility
7272 this.$element
.addClass( 'oo-ui-textInputMenuSelectWidget' );
7277 OO
.inheritClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.MenuSelectWidget
);
7278 OO
.mixinClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7280 // For backwards compatibility
7281 OO
.ui
.TextInputMenuSelectWidget
= OO
.ui
.FloatingMenuSelectWidget
;
7288 OO
.ui
.FloatingMenuSelectWidget
.prototype.toggle = function ( visible
) {
7290 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
7291 change
= visible
!== this.isVisible();
7293 if ( change
&& visible
) {
7294 // Make sure the width is set before the parent method runs.
7295 this.setIdealSize( this.$container
.width() );
7299 // This will call this.clip(), which is nonsensical since we're not positioned yet...
7300 OO
.ui
.FloatingMenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7303 this.togglePositioning( this.isVisible() );
7310 * Progress bars visually display the status of an operation, such as a download,
7311 * and can be either determinate or indeterminate:
7313 * - **determinate** process bars show the percent of an operation that is complete.
7315 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
7316 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
7317 * not use percentages.
7319 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
7322 * // Examples of determinate and indeterminate progress bars.
7323 * var progressBar1 = new OO.ui.ProgressBarWidget( {
7326 * var progressBar2 = new OO.ui.ProgressBarWidget();
7328 * // Create a FieldsetLayout to layout progress bars
7329 * var fieldset = new OO.ui.FieldsetLayout;
7330 * fieldset.addItems( [
7331 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
7332 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
7334 * $( 'body' ).append( fieldset.$element );
7337 * @extends OO.ui.Widget
7340 * @param {Object} [config] Configuration options
7341 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
7342 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
7343 * By default, the progress bar is indeterminate.
7345 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
7346 // Configuration initialization
7347 config
= config
|| {};
7349 // Parent constructor
7350 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
7353 this.$bar
= $( '<div>' );
7354 this.progress
= null;
7357 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
7358 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
7361 role
: 'progressbar',
7363 'aria-valuemax': 100
7365 .addClass( 'oo-ui-progressBarWidget' )
7366 .append( this.$bar
);
7371 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
7373 /* Static Properties */
7375 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
7380 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
7382 * @return {number|boolean} Progress percent
7384 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
7385 return this.progress
;
7389 * Set the percent of the process completed or `false` for an indeterminate process.
7391 * @param {number|boolean} progress Progress percent or `false` for indeterminate
7393 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
7394 this.progress
= progress
;
7396 if ( progress
!== false ) {
7397 this.$bar
.css( 'width', this.progress
+ '%' );
7398 this.$element
.attr( 'aria-valuenow', this.progress
);
7400 this.$bar
.css( 'width', '' );
7401 this.$element
.removeAttr( 'aria-valuenow' );
7403 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
7407 * InputWidget is the base class for all input widgets, which
7408 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
7409 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
7410 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
7412 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7416 * @extends OO.ui.Widget
7417 * @mixins OO.ui.mixin.FlaggedElement
7418 * @mixins OO.ui.mixin.TabIndexedElement
7419 * @mixins OO.ui.mixin.TitledElement
7420 * @mixins OO.ui.mixin.AccessKeyedElement
7423 * @param {Object} [config] Configuration options
7424 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
7425 * @cfg {string} [value=''] The value of the input.
7426 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
7427 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
7428 * before it is accepted.
7430 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
7431 // Configuration initialization
7432 config
= config
|| {};
7434 // Parent constructor
7435 OO
.ui
.InputWidget
.parent
.call( this, config
);
7438 // See #reusePreInfuseDOM about config.$input
7439 this.$input
= config
.$input
|| this.getInputElement( config
);
7441 this.inputFilter
= config
.inputFilter
;
7443 // Mixin constructors
7444 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
7445 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
7446 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
7447 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
7450 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
7454 .addClass( 'oo-ui-inputWidget-input' )
7455 .attr( 'name', config
.name
)
7456 .prop( 'disabled', this.isDisabled() );
7458 .addClass( 'oo-ui-inputWidget' )
7459 .append( this.$input
);
7460 this.setValue( config
.value
);
7462 this.setDir( config
.dir
);
7468 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
7469 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
7470 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7471 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
7472 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
7474 /* Static Properties */
7476 OO
.ui
.InputWidget
.static.supportsSimpleLabel
= true;
7478 /* Static Methods */
7483 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
7484 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
7485 // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
7486 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
7493 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7494 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7495 if ( config
.$input
&& config
.$input
.length
) {
7496 state
.value
= config
.$input
.val();
7497 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
7498 state
.focus
= config
.$input
.is( ':focus' );
7508 * A change event is emitted when the value of the input changes.
7510 * @param {string} value
7516 * Get input element.
7518 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
7519 * different circumstances. The element must have a `value` property (like form elements).
7522 * @param {Object} config Configuration options
7523 * @return {jQuery} Input element
7525 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
7526 return $( '<input>' );
7530 * Handle potentially value-changing events.
7533 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
7535 OO
.ui
.InputWidget
.prototype.onEdit = function () {
7537 if ( !this.isDisabled() ) {
7538 // Allow the stack to clear so the value will be updated
7539 setTimeout( function () {
7540 widget
.setValue( widget
.$input
.val() );
7546 * Get the value of the input.
7548 * @return {string} Input value
7550 OO
.ui
.InputWidget
.prototype.getValue = function () {
7551 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7552 // it, and we won't know unless they're kind enough to trigger a 'change' event.
7553 var value
= this.$input
.val();
7554 if ( this.value
!== value
) {
7555 this.setValue( value
);
7561 * Set the directionality of the input.
7563 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
7566 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
7567 this.$input
.prop( 'dir', dir
);
7572 * Set the value of the input.
7574 * @param {string} value New value
7578 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
7579 value
= this.cleanUpValue( value
);
7580 // Update the DOM if it has changed. Note that with cleanUpValue, it
7581 // is possible for the DOM value to change without this.value changing.
7582 if ( this.$input
.val() !== value
) {
7583 this.$input
.val( value
);
7585 if ( this.value
!== value
) {
7587 this.emit( 'change', this.value
);
7593 * Clean up incoming value.
7595 * Ensures value is a string, and converts undefined and null to empty string.
7598 * @param {string} value Original value
7599 * @return {string} Cleaned up value
7601 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
7602 if ( value
=== undefined || value
=== null ) {
7604 } else if ( this.inputFilter
) {
7605 return this.inputFilter( String( value
) );
7607 return String( value
);
7612 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
7613 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
7616 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
7617 if ( !this.isDisabled() ) {
7618 if ( this.$input
.is( ':checkbox, :radio' ) ) {
7619 this.$input
.click();
7621 if ( this.$input
.is( ':input' ) ) {
7622 this.$input
[ 0 ].focus();
7630 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
7631 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
7632 if ( this.$input
) {
7633 this.$input
.prop( 'disabled', this.isDisabled() );
7643 OO
.ui
.InputWidget
.prototype.focus = function () {
7644 this.$input
[ 0 ].focus();
7653 OO
.ui
.InputWidget
.prototype.blur = function () {
7654 this.$input
[ 0 ].blur();
7661 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
7662 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7663 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
7664 this.setValue( state
.value
);
7666 if ( state
.focus
) {
7672 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
7673 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
7674 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
7675 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
7676 * [OOjs UI documentation on MediaWiki] [1] for more information.
7679 * // A ButtonInputWidget rendered as an HTML button, the default.
7680 * var button = new OO.ui.ButtonInputWidget( {
7681 * label: 'Input button',
7685 * $( 'body' ).append( button.$element );
7687 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
7690 * @extends OO.ui.InputWidget
7691 * @mixins OO.ui.mixin.ButtonElement
7692 * @mixins OO.ui.mixin.IconElement
7693 * @mixins OO.ui.mixin.IndicatorElement
7694 * @mixins OO.ui.mixin.LabelElement
7695 * @mixins OO.ui.mixin.TitledElement
7698 * @param {Object} [config] Configuration options
7699 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
7700 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
7701 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
7702 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
7703 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
7705 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
7706 // Configuration initialization
7707 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
7709 // See InputWidget#reusePreInfuseDOM about config.$input
7710 if ( config
.$input
) {
7711 config
.$input
.empty();
7714 // Properties (must be set before parent constructor, which calls #setValue)
7715 this.useInputTag
= config
.useInputTag
;
7717 // Parent constructor
7718 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
7720 // Mixin constructors
7721 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
7722 OO
.ui
.mixin
.IconElement
.call( this, config
);
7723 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7724 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7725 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
7728 if ( !config
.useInputTag
) {
7729 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
7731 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
7736 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
7737 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
7738 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
7739 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
7740 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
7741 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
7743 /* Static Properties */
7746 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
7747 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
7749 OO
.ui
.ButtonInputWidget
.static.supportsSimpleLabel
= false;
7757 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
7759 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
7760 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
7766 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
7768 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
7769 * text, or `null` for no label
7772 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
7773 if ( typeof label
=== 'function' ) {
7774 label
= OO
.ui
.resolveMsg( label
);
7777 if ( this.useInputTag
) {
7778 // Discard non-plaintext labels
7779 if ( typeof label
!== 'string' ) {
7783 this.$input
.val( label
);
7786 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
7790 * Set the value of the input.
7792 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
7793 * they do not support {@link #value values}.
7795 * @param {string} value New value
7798 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
7799 if ( !this.useInputTag
) {
7800 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
7806 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
7807 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
7808 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
7809 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
7811 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7814 * // An example of selected, unselected, and disabled checkbox inputs
7815 * var checkbox1=new OO.ui.CheckboxInputWidget( {
7819 * var checkbox2=new OO.ui.CheckboxInputWidget( {
7822 * var checkbox3=new OO.ui.CheckboxInputWidget( {
7826 * // Create a fieldset layout with fields for each checkbox.
7827 * var fieldset = new OO.ui.FieldsetLayout( {
7828 * label: 'Checkboxes'
7830 * fieldset.addItems( [
7831 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
7832 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
7833 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
7835 * $( 'body' ).append( fieldset.$element );
7837 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7840 * @extends OO.ui.InputWidget
7843 * @param {Object} [config] Configuration options
7844 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
7846 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
7847 // Configuration initialization
7848 config
= config
|| {};
7850 // Parent constructor
7851 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
7855 .addClass( 'oo-ui-checkboxInputWidget' )
7856 // Required for pretty styling in MediaWiki theme
7857 .append( $( '<span>' ) );
7858 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
7863 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
7865 /* Static Methods */
7870 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7871 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7872 state
.checked
= config
.$input
.prop( 'checked' );
7882 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
7883 return $( '<input>' ).attr( 'type', 'checkbox' );
7889 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
7891 if ( !this.isDisabled() ) {
7892 // Allow the stack to clear so the value will be updated
7893 setTimeout( function () {
7894 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
7900 * Set selection state of this checkbox.
7902 * @param {boolean} state `true` for selected
7905 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
7907 if ( this.selected
!== state
) {
7908 this.selected
= state
;
7909 this.$input
.prop( 'checked', this.selected
);
7910 this.emit( 'change', this.selected
);
7916 * Check if this checkbox is selected.
7918 * @return {boolean} Checkbox is selected
7920 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
7921 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7922 // it, and we won't know unless they're kind enough to trigger a 'change' event.
7923 var selected
= this.$input
.prop( 'checked' );
7924 if ( this.selected
!== selected
) {
7925 this.setSelected( selected
);
7927 return this.selected
;
7933 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
7934 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7935 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
7936 this.setSelected( state
.checked
);
7941 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
7942 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7943 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7944 * more information about input widgets.
7946 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
7947 * are no options. If no `value` configuration option is provided, the first option is selected.
7948 * If you need a state representing no value (no option being selected), use a DropdownWidget.
7950 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
7953 * // Example: A DropdownInputWidget with three options
7954 * var dropdownInput = new OO.ui.DropdownInputWidget( {
7956 * { data: 'a', label: 'First' },
7957 * { data: 'b', label: 'Second'},
7958 * { data: 'c', label: 'Third' }
7961 * $( 'body' ).append( dropdownInput.$element );
7963 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7966 * @extends OO.ui.InputWidget
7967 * @mixins OO.ui.mixin.TitledElement
7970 * @param {Object} [config] Configuration options
7971 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7972 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
7974 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
7975 // Configuration initialization
7976 config
= config
|| {};
7978 // See InputWidget#reusePreInfuseDOM about config.$input
7979 if ( config
.$input
) {
7980 config
.$input
.addClass( 'oo-ui-element-hidden' );
7983 // Properties (must be done before parent constructor which calls #setDisabled)
7984 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
7986 // Parent constructor
7987 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
7989 // Mixin constructors
7990 OO
.ui
.mixin
.TitledElement
.call( this, config
);
7993 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
7996 this.setOptions( config
.options
|| [] );
7998 .addClass( 'oo-ui-dropdownInputWidget' )
7999 .append( this.dropdownWidget
.$element
);
8004 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
8005 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
8013 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
8014 return $( '<input>' ).attr( 'type', 'hidden' );
8018 * Handles menu select events.
8021 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8023 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
8024 this.setValue( item
.getData() );
8030 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
8031 value
= this.cleanUpValue( value
);
8032 this.dropdownWidget
.getMenu().selectItemByData( value
);
8033 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
8040 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
8041 this.dropdownWidget
.setDisabled( state
);
8042 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8047 * Set the options available for this input.
8049 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8052 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
8054 value
= this.getValue(),
8057 // Rebuild the dropdown menu
8058 this.dropdownWidget
.getMenu()
8060 .addItems( options
.map( function ( opt
) {
8061 var optValue
= widget
.cleanUpValue( opt
.data
);
8062 return new OO
.ui
.MenuOptionWidget( {
8064 label
: opt
.label
!== undefined ? opt
.label
: optValue
8068 // Restore the previous value, or reset to something sensible
8069 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
8070 // Previous value is still available, ensure consistency with the dropdown
8071 this.setValue( value
);
8073 // No longer valid, reset
8074 if ( options
.length
) {
8075 this.setValue( options
[ 0 ].data
);
8085 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
8086 this.dropdownWidget
.getMenu().toggle( true );
8093 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
8094 this.dropdownWidget
.getMenu().toggle( false );
8099 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
8100 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
8101 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
8102 * please see the [OOjs UI documentation on MediaWiki][1].
8104 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8107 * // An example of selected, unselected, and disabled radio inputs
8108 * var radio1 = new OO.ui.RadioInputWidget( {
8112 * var radio2 = new OO.ui.RadioInputWidget( {
8115 * var radio3 = new OO.ui.RadioInputWidget( {
8119 * // Create a fieldset layout with fields for each radio button.
8120 * var fieldset = new OO.ui.FieldsetLayout( {
8121 * label: 'Radio inputs'
8123 * fieldset.addItems( [
8124 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
8125 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
8126 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
8128 * $( 'body' ).append( fieldset.$element );
8130 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8133 * @extends OO.ui.InputWidget
8136 * @param {Object} [config] Configuration options
8137 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
8139 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
8140 // Configuration initialization
8141 config
= config
|| {};
8143 // Parent constructor
8144 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
8148 .addClass( 'oo-ui-radioInputWidget' )
8149 // Required for pretty styling in MediaWiki theme
8150 .append( $( '<span>' ) );
8151 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
8156 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
8158 /* Static Methods */
8163 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8164 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8165 state
.checked
= config
.$input
.prop( 'checked' );
8175 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
8176 return $( '<input>' ).attr( 'type', 'radio' );
8182 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
8183 // RadioInputWidget doesn't track its state.
8187 * Set selection state of this radio button.
8189 * @param {boolean} state `true` for selected
8192 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
8193 // RadioInputWidget doesn't track its state.
8194 this.$input
.prop( 'checked', state
);
8199 * Check if this radio button is selected.
8201 * @return {boolean} Radio is selected
8203 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
8204 return this.$input
.prop( 'checked' );
8210 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8211 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8212 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
8213 this.setSelected( state
.checked
);
8218 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
8219 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8220 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8221 * more information about input widgets.
8223 * This and OO.ui.DropdownInputWidget support the same configuration options.
8226 * // Example: A RadioSelectInputWidget with three options
8227 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
8229 * { data: 'a', label: 'First' },
8230 * { data: 'b', label: 'Second'},
8231 * { data: 'c', label: 'Third' }
8234 * $( 'body' ).append( radioSelectInput.$element );
8236 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8239 * @extends OO.ui.InputWidget
8242 * @param {Object} [config] Configuration options
8243 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8245 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
8246 // Configuration initialization
8247 config
= config
|| {};
8249 // Properties (must be done before parent constructor which calls #setDisabled)
8250 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
8252 // Parent constructor
8253 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
8256 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
8259 this.setOptions( config
.options
|| [] );
8261 .addClass( 'oo-ui-radioSelectInputWidget' )
8262 .append( this.radioSelectWidget
.$element
);
8267 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
8269 /* Static Properties */
8271 OO
.ui
.RadioSelectInputWidget
.static.supportsSimpleLabel
= false;
8273 /* Static Methods */
8278 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8279 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8280 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
8287 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8288 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8289 // Cannot reuse the `<input type=radio>` set
8290 delete config
.$input
;
8300 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
8301 return $( '<input>' ).attr( 'type', 'hidden' );
8305 * Handles menu select events.
8308 * @param {OO.ui.RadioOptionWidget} item Selected menu item
8310 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
8311 this.setValue( item
.getData() );
8317 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
8318 value
= this.cleanUpValue( value
);
8319 this.radioSelectWidget
.selectItemByData( value
);
8320 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
8327 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
8328 this.radioSelectWidget
.setDisabled( state
);
8329 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8334 * Set the options available for this input.
8336 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8339 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
8341 value
= this.getValue(),
8344 // Rebuild the radioSelect menu
8345 this.radioSelectWidget
8347 .addItems( options
.map( function ( opt
) {
8348 var optValue
= widget
.cleanUpValue( opt
.data
);
8349 return new OO
.ui
.RadioOptionWidget( {
8351 label
: opt
.label
!== undefined ? opt
.label
: optValue
8355 // Restore the previous value, or reset to something sensible
8356 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
8357 // Previous value is still available, ensure consistency with the radioSelect
8358 this.setValue( value
);
8360 // No longer valid, reset
8361 if ( options
.length
) {
8362 this.setValue( options
[ 0 ].data
);
8370 * CheckboxMultiselectInputWidget is a
8371 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
8372 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
8373 * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on MediaWiki][1] for
8374 * more information about input widgets.
8377 * // Example: A CheckboxMultiselectInputWidget with three options
8378 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
8380 * { data: 'a', label: 'First' },
8381 * { data: 'b', label: 'Second'},
8382 * { data: 'c', label: 'Third' }
8385 * $( 'body' ).append( multiselectInput.$element );
8387 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8390 * @extends OO.ui.InputWidget
8393 * @param {Object} [config] Configuration options
8394 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8396 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
8397 // Configuration initialization
8398 config
= config
|| {};
8400 // Properties (must be done before parent constructor which calls #setDisabled)
8401 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
8403 // Parent constructor
8404 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
8407 this.inputName
= config
.name
;
8411 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
8412 .append( this.checkboxMultiselectWidget
.$element
);
8413 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
8414 this.$input
.detach();
8415 this.setOptions( config
.options
|| [] );
8416 // Have to repeat this from parent, as we need options to be set up for this to make sense
8417 this.setValue( config
.value
);
8422 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
8424 /* Static Properties */
8426 OO
.ui
.CheckboxMultiselectInputWidget
.static.supportsSimpleLabel
= false;
8428 /* Static Methods */
8433 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8434 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8435 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
8436 .toArray().map( function ( el
) { return el
.value
; } );
8443 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8444 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8445 // Cannot reuse the `<input type=checkbox>` set
8446 delete config
.$input
;
8456 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
8458 return $( '<div>' );
8464 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
8465 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
8466 .toArray().map( function ( el
) { return el
.value
; } );
8467 if ( this.value
!== value
) {
8468 this.setValue( value
);
8476 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
8477 value
= this.cleanUpValue( value
);
8478 this.checkboxMultiselectWidget
.selectItemsByData( value
);
8479 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
8484 * Clean up incoming value.
8486 * @param {string[]} value Original value
8487 * @return {string[]} Cleaned up value
8489 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
8492 if ( !Array
.isArray( value
) ) {
8495 for ( i
= 0; i
< value
.length
; i
++ ) {
8497 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
8498 // Remove options that we don't have here
8499 if ( !this.checkboxMultiselectWidget
.getItemFromData( singleValue
) ) {
8502 cleanValue
.push( singleValue
);
8510 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
8511 this.checkboxMultiselectWidget
.setDisabled( state
);
8512 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8517 * Set the options available for this input.
8519 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8522 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
8525 // Rebuild the checkboxMultiselectWidget menu
8526 this.checkboxMultiselectWidget
8528 .addItems( options
.map( function ( opt
) {
8531 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
8532 item
= new OO
.ui
.CheckboxMultioptionWidget( {
8534 label
: opt
.label
!== undefined ? opt
.label
: optValue
8536 // Set the 'name' and 'value' for form submission
8537 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
8538 item
.checkbox
.setValue( optValue
);
8542 // Re-set the value, checking the checkboxes as needed.
8543 // This will also get rid of any stale options that we just removed.
8544 this.setValue( this.getValue() );
8550 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
8551 * size of the field as well as its presentation. In addition, these widgets can be configured
8552 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
8553 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
8554 * which modifies incoming values rather than validating them.
8555 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8557 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8560 * // Example of a text input widget
8561 * var textInput = new OO.ui.TextInputWidget( {
8562 * value: 'Text input'
8564 * $( 'body' ).append( textInput.$element );
8566 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8569 * @extends OO.ui.InputWidget
8570 * @mixins OO.ui.mixin.IconElement
8571 * @mixins OO.ui.mixin.IndicatorElement
8572 * @mixins OO.ui.mixin.PendingElement
8573 * @mixins OO.ui.mixin.LabelElement
8576 * @param {Object} [config] Configuration options
8577 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
8578 * 'email', 'url', 'date', 'month' or 'number'. Ignored if `multiline` is true.
8580 * Some values of `type` result in additional behaviors:
8582 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
8583 * empties the text field
8584 * @cfg {string} [placeholder] Placeholder text
8585 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
8586 * instruct the browser to focus this widget.
8587 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
8588 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
8589 * @cfg {boolean} [multiline=false] Allow multiple lines of text
8590 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
8591 * specifies minimum number of rows to display.
8592 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
8593 * Use the #maxRows config to specify a maximum number of displayed rows.
8594 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
8595 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
8596 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
8597 * the value or placeholder text: `'before'` or `'after'`
8598 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
8599 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
8600 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
8601 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
8602 * (the value must contain only numbers); when RegExp, a regular expression that must match the
8603 * value for it to be considered valid; when Function, a function receiving the value as parameter
8604 * that must return true, or promise resolving to true, for it to be considered valid.
8606 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
8607 // Configuration initialization
8608 config
= $.extend( {
8610 labelPosition
: 'after'
8613 if ( config
.type
=== 'search' ) {
8614 OO
.ui
.warnDeprecation( 'TextInputWidget: config.type=\'search\' is deprecated. Use the SearchInputWidget instead. See T148471 for details.' );
8615 if ( config
.icon
=== undefined ) {
8616 config
.icon
= 'search';
8618 // indicator: 'clear' is set dynamically later, depending on value
8621 // Parent constructor
8622 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
8624 // Mixin constructors
8625 OO
.ui
.mixin
.IconElement
.call( this, config
);
8626 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8627 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
8628 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8631 this.type
= this.getSaneType( config
);
8632 this.readOnly
= false;
8633 this.required
= false;
8634 this.multiline
= !!config
.multiline
;
8635 this.autosize
= !!config
.autosize
;
8636 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
8637 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
8638 this.validate
= null;
8639 this.styleHeight
= null;
8640 this.scrollWidth
= null;
8642 // Clone for resizing
8643 if ( this.autosize
) {
8644 this.$clone
= this.$input
8646 .insertAfter( this.$input
)
8647 .attr( 'aria-hidden', 'true' )
8648 .addClass( 'oo-ui-element-hidden' );
8651 this.setValidation( config
.validate
);
8652 this.setLabelPosition( config
.labelPosition
);
8656 keypress
: this.onKeyPress
.bind( this ),
8657 blur
: this.onBlur
.bind( this ),
8658 focus
: this.onFocus
.bind( this )
8661 focus
: this.onElementAttach
.bind( this )
8663 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
8664 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
8665 this.on( 'labelChange', this.updatePosition
.bind( this ) );
8666 this.connect( this, {
8668 disable
: 'onDisable'
8670 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
8674 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
8675 .append( this.$icon
, this.$indicator
);
8676 this.setReadOnly( !!config
.readOnly
);
8677 this.setRequired( !!config
.required
);
8678 this.updateSearchIndicator();
8679 if ( config
.placeholder
!== undefined ) {
8680 this.$input
.attr( 'placeholder', config
.placeholder
);
8682 if ( config
.maxLength
!== undefined ) {
8683 this.$input
.attr( 'maxlength', config
.maxLength
);
8685 if ( config
.autofocus
) {
8686 this.$input
.attr( 'autofocus', 'autofocus' );
8688 if ( config
.autocomplete
=== false ) {
8689 this.$input
.attr( 'autocomplete', 'off' );
8690 // Turning off autocompletion also disables "form caching" when the user navigates to a
8691 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
8693 beforeunload: function () {
8694 this.$input
.removeAttr( 'autocomplete' );
8696 pageshow: function () {
8697 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
8698 // whole page... it shouldn't hurt, though.
8699 this.$input
.attr( 'autocomplete', 'off' );
8703 if ( this.multiline
&& config
.rows
) {
8704 this.$input
.attr( 'rows', config
.rows
);
8706 if ( this.label
|| config
.autosize
) {
8707 this.installParentChangeDetector();
8713 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
8714 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
8715 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
8716 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
8717 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
8719 /* Static Properties */
8721 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
8726 /* Static Methods */
8731 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8732 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8733 if ( config
.multiline
) {
8734 state
.scrollTop
= config
.$input
.scrollTop();
8742 * An `enter` event is emitted when the user presses 'enter' inside the text box.
8744 * Not emitted if the input is multiline.
8750 * A `resize` event is emitted when autosize is set and the widget resizes
8758 * Handle icon mouse down events.
8761 * @param {jQuery.Event} e Mouse down event
8763 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
8764 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8765 this.$input
[ 0 ].focus();
8771 * Handle indicator mouse down events.
8774 * @param {jQuery.Event} e Mouse down event
8776 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
8777 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8778 if ( this.type
=== 'search' ) {
8779 // Clear the text field
8780 this.setValue( '' );
8782 this.$input
[ 0 ].focus();
8788 * Handle key press events.
8791 * @param {jQuery.Event} e Key press event
8792 * @fires enter If enter key is pressed and input is not multiline
8794 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
8795 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
8796 this.emit( 'enter', e
);
8801 * Handle blur events.
8804 * @param {jQuery.Event} e Blur event
8806 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
8807 this.setValidityFlag();
8811 * Handle focus events.
8814 * @param {jQuery.Event} e Focus event
8816 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
8817 this.setValidityFlag( true );
8821 * Handle element attach events.
8824 * @param {jQuery.Event} e Element attach event
8826 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
8827 // Any previously calculated size is now probably invalid if we reattached elsewhere
8828 this.valCache
= null;
8830 this.positionLabel();
8834 * Handle change events.
8836 * @param {string} value
8839 OO
.ui
.TextInputWidget
.prototype.onChange = function () {
8840 this.updateSearchIndicator();
8845 * Handle debounced change events.
8847 * @param {string} value
8850 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
8851 this.setValidityFlag();
8855 * Handle disable events.
8857 * @param {boolean} disabled Element is disabled
8860 OO
.ui
.TextInputWidget
.prototype.onDisable = function () {
8861 this.updateSearchIndicator();
8865 * Check if the input is {@link #readOnly read-only}.
8869 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
8870 return this.readOnly
;
8874 * Set the {@link #readOnly read-only} state of the input.
8876 * @param {boolean} state Make input read-only
8879 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
8880 this.readOnly
= !!state
;
8881 this.$input
.prop( 'readOnly', this.readOnly
);
8882 this.updateSearchIndicator();
8887 * Check if the input is {@link #required required}.
8891 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
8892 return this.required
;
8896 * Set the {@link #required required} state of the input.
8898 * @param {boolean} state Make input required
8901 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
8902 this.required
= !!state
;
8903 if ( this.required
) {
8905 .attr( 'required', 'required' )
8906 .attr( 'aria-required', 'true' );
8907 if ( this.getIndicator() === null ) {
8908 this.setIndicator( 'required' );
8912 .removeAttr( 'required' )
8913 .removeAttr( 'aria-required' );
8914 if ( this.getIndicator() === 'required' ) {
8915 this.setIndicator( null );
8918 this.updateSearchIndicator();
8923 * Support function for making #onElementAttach work across browsers.
8925 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
8926 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
8928 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
8929 * first time that the element gets attached to the documented.
8931 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
8932 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
8933 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
8936 if ( MutationObserver
) {
8937 // The new way. If only it wasn't so ugly.
8939 if ( this.$element
.closest( 'html' ).length
) {
8940 // Widget is attached already, do nothing. This breaks the functionality of this function when
8941 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
8942 // would require observation of the whole document, which would hurt performance of other,
8943 // more important code.
8947 // Find topmost node in the tree
8948 topmostNode
= this.$element
[ 0 ];
8949 while ( topmostNode
.parentNode
) {
8950 topmostNode
= topmostNode
.parentNode
;
8953 // We have no way to detect the $element being attached somewhere without observing the entire
8954 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
8955 // parent node of $element, and instead detect when $element is removed from it (and thus
8956 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
8957 // doesn't get attached, we end up back here and create the parent.
8959 mutationObserver
= new MutationObserver( function ( mutations
) {
8960 var i
, j
, removedNodes
;
8961 for ( i
= 0; i
< mutations
.length
; i
++ ) {
8962 removedNodes
= mutations
[ i
].removedNodes
;
8963 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
8964 if ( removedNodes
[ j
] === topmostNode
) {
8965 setTimeout( onRemove
, 0 );
8972 onRemove = function () {
8973 // If the node was attached somewhere else, report it
8974 if ( widget
.$element
.closest( 'html' ).length
) {
8975 widget
.onElementAttach();
8977 mutationObserver
.disconnect();
8978 widget
.installParentChangeDetector();
8981 // Create a fake parent and observe it
8982 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
8983 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
8985 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
8986 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
8987 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
8992 * Automatically adjust the size of the text input.
8994 * This only affects #multiline inputs that are {@link #autosize autosized}.
8999 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
9000 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
9001 idealHeight
, newHeight
, scrollWidth
, property
;
9003 if ( this.multiline
&& this.$input
.val() !== this.valCache
) {
9004 if ( this.autosize
) {
9006 .val( this.$input
.val() )
9007 .attr( 'rows', this.minRows
)
9008 // Set inline height property to 0 to measure scroll height
9009 .css( 'height', 0 );
9011 this.$clone
.removeClass( 'oo-ui-element-hidden' );
9013 this.valCache
= this.$input
.val();
9015 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
9017 // Remove inline height property to measure natural heights
9018 this.$clone
.css( 'height', '' );
9019 innerHeight
= this.$clone
.innerHeight();
9020 outerHeight
= this.$clone
.outerHeight();
9022 // Measure max rows height
9024 .attr( 'rows', this.maxRows
)
9025 .css( 'height', 'auto' )
9027 maxInnerHeight
= this.$clone
.innerHeight();
9029 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
9030 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
9031 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
9032 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
9034 this.$clone
.addClass( 'oo-ui-element-hidden' );
9036 // Only apply inline height when expansion beyond natural height is needed
9037 // Use the difference between the inner and outer height as a buffer
9038 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
9039 if ( newHeight
!== this.styleHeight
) {
9040 this.$input
.css( 'height', newHeight
);
9041 this.styleHeight
= newHeight
;
9042 this.emit( 'resize' );
9045 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
9046 if ( scrollWidth
!== this.scrollWidth
) {
9047 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
9049 this.$label
.css( { right
: '', left
: '' } );
9050 this.$indicator
.css( { right
: '', left
: '' } );
9052 if ( scrollWidth
) {
9053 this.$indicator
.css( property
, scrollWidth
);
9054 if ( this.labelPosition
=== 'after' ) {
9055 this.$label
.css( property
, scrollWidth
);
9059 this.scrollWidth
= scrollWidth
;
9060 this.positionLabel();
9070 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
9071 if ( config
.multiline
) {
9072 return $( '<textarea>' );
9073 } else if ( this.getSaneType( config
) === 'number' ) {
9074 return $( '<input>' )
9075 .attr( 'step', 'any' )
9076 .attr( 'type', 'number' );
9078 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
9083 * Get sanitized value for 'type' for given config.
9085 * @param {Object} config Configuration options
9086 * @return {string|null}
9089 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
9090 var allowedTypes
= [
9100 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
9104 * Check if the input supports multiple lines.
9108 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
9109 return !!this.multiline
;
9113 * Check if the input automatically adjusts its size.
9117 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
9118 return !!this.autosize
;
9122 * Focus the input and select a specified range within the text.
9124 * @param {number} from Select from offset
9125 * @param {number} [to] Select to offset, defaults to from
9128 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
9129 var isBackwards
, start
, end
,
9130 input
= this.$input
[ 0 ];
9134 isBackwards
= to
< from;
9135 start
= isBackwards
? to
: from;
9136 end
= isBackwards
? from : to
;
9141 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
9143 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
9144 // Rather than expensively check if the input is attached every time, just check
9145 // if it was the cause of an error being thrown. If not, rethrow the error.
9146 if ( this.getElementDocument().body
.contains( input
) ) {
9154 * Get an object describing the current selection range in a directional manner
9156 * @return {Object} Object containing 'from' and 'to' offsets
9158 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
9159 var input
= this.$input
[ 0 ],
9160 start
= input
.selectionStart
,
9161 end
= input
.selectionEnd
,
9162 isBackwards
= input
.selectionDirection
=== 'backward';
9165 from: isBackwards
? end
: start
,
9166 to
: isBackwards
? start
: end
9171 * Get the length of the text input value.
9173 * This could differ from the length of #getValue if the
9174 * value gets filtered
9176 * @return {number} Input length
9178 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
9179 return this.$input
[ 0 ].value
.length
;
9183 * Focus the input and select the entire text.
9187 OO
.ui
.TextInputWidget
.prototype.select = function () {
9188 return this.selectRange( 0, this.getInputLength() );
9192 * Focus the input and move the cursor to the start.
9196 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
9197 return this.selectRange( 0 );
9201 * Focus the input and move the cursor to the end.
9205 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
9206 return this.selectRange( this.getInputLength() );
9210 * Insert new content into the input.
9212 * @param {string} content Content to be inserted
9215 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
9217 range
= this.getRange(),
9218 value
= this.getValue();
9220 start
= Math
.min( range
.from, range
.to
);
9221 end
= Math
.max( range
.from, range
.to
);
9223 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
9224 this.selectRange( start
+ content
.length
);
9229 * Insert new content either side of a selection.
9231 * @param {string} pre Content to be inserted before the selection
9232 * @param {string} post Content to be inserted after the selection
9235 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
9237 range
= this.getRange(),
9238 offset
= pre
.length
;
9240 start
= Math
.min( range
.from, range
.to
);
9241 end
= Math
.max( range
.from, range
.to
);
9243 this.selectRange( start
).insertContent( pre
);
9244 this.selectRange( offset
+ end
).insertContent( post
);
9246 this.selectRange( offset
+ start
, offset
+ end
);
9251 * Set the validation pattern.
9253 * The validation pattern is either a regular expression, a function, or the symbolic name of a
9254 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
9255 * value must contain only numbers).
9257 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
9258 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
9260 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
9261 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
9262 this.validate
= validate
;
9264 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
9269 * Sets the 'invalid' flag appropriately.
9271 * @param {boolean} [isValid] Optionally override validation result
9273 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
9275 setFlag = function ( valid
) {
9277 widget
.$input
.attr( 'aria-invalid', 'true' );
9279 widget
.$input
.removeAttr( 'aria-invalid' );
9281 widget
.setFlags( { invalid
: !valid
} );
9284 if ( isValid
!== undefined ) {
9287 this.getValidity().then( function () {
9296 * Get the validity of current value.
9298 * This method returns a promise that resolves if the value is valid and rejects if
9299 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
9301 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
9303 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
9306 function rejectOrResolve( valid
) {
9308 return $.Deferred().resolve().promise();
9310 return $.Deferred().reject().promise();
9314 if ( this.validate
instanceof Function
) {
9315 result
= this.validate( this.getValue() );
9316 if ( result
&& $.isFunction( result
.promise
) ) {
9317 return result
.promise().then( function ( valid
) {
9318 return rejectOrResolve( valid
);
9321 return rejectOrResolve( result
);
9324 return rejectOrResolve( this.getValue().match( this.validate
) );
9329 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
9331 * @param {string} labelPosition Label position, 'before' or 'after'
9334 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
9335 this.labelPosition
= labelPosition
;
9337 // If there is no label and we only change the position, #updatePosition is a no-op,
9338 // but it takes really a lot of work to do nothing.
9339 this.updatePosition();
9345 * Update the position of the inline label.
9347 * This method is called by #setLabelPosition, and can also be called on its own if
9348 * something causes the label to be mispositioned.
9352 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
9353 var after
= this.labelPosition
=== 'after';
9356 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
9357 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
9359 this.valCache
= null;
9360 this.scrollWidth
= null;
9362 this.positionLabel();
9368 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
9369 * already empty or when it's not editable.
9371 OO
.ui
.TextInputWidget
.prototype.updateSearchIndicator = function () {
9372 if ( this.type
=== 'search' ) {
9373 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
9374 this.setIndicator( null );
9376 this.setIndicator( 'clear' );
9382 * Position the label by setting the correct padding on the input.
9387 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
9388 var after
, rtl
, property
;
9391 // Clear old values if present
9393 'padding-right': '',
9398 this.$element
.append( this.$label
);
9400 this.$label
.detach();
9404 after
= this.labelPosition
=== 'after';
9405 rtl
= this.$element
.css( 'direction' ) === 'rtl';
9406 property
= after
=== rtl
? 'padding-left' : 'padding-right';
9408 this.$input
.css( property
, this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 ) );
9416 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9417 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9418 if ( state
.scrollTop
!== undefined ) {
9419 this.$input
.scrollTop( state
.scrollTop
);
9425 * @extends OO.ui.TextInputWidget
9428 * @param {Object} [config] Configuration options
9430 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
9431 config
= $.extend( {
9435 // Set type to text so that TextInputWidget doesn't
9436 // get stuck in an infinite loop.
9437 config
.type
= 'text';
9439 // Parent constructor
9440 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
9443 this.$element
.addClass( 'oo-ui-textInputWidget-type-search' );
9444 this.updateSearchIndicator();
9445 this.connect( this, {
9446 disable
: 'onDisable'
9452 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
9460 OO
.ui
.SearchInputWidget
.prototype.getInputElement = function () {
9461 return $( '<input>' ).attr( 'type', 'search' );
9467 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
9468 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9469 // Clear the text field
9470 this.setValue( '' );
9471 this.$input
[ 0 ].focus();
9477 * Update the 'clear' indicator displayed on type: 'search' text
9478 * fields, hiding it when the field is already empty or when it's not
9481 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
9482 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
9483 this.setIndicator( null );
9485 this.setIndicator( 'clear' );
9492 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
9493 OO
.ui
.SearchInputWidget
.parent
.prototype.onChange
.call( this );
9494 this.updateSearchIndicator();
9498 * Handle disable events.
9500 * @param {boolean} disabled Element is disabled
9503 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
9504 this.updateSearchIndicator();
9510 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
9511 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
9512 this.updateSearchIndicator();
9517 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
9518 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
9519 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
9521 * - by typing a value in the text input field. If the value exactly matches the value of a menu
9522 * option, that option will appear to be selected.
9523 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
9526 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
9528 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
9531 * // Example: A ComboBoxInputWidget.
9532 * var comboBox = new OO.ui.ComboBoxInputWidget( {
9533 * label: 'ComboBoxInputWidget',
9534 * value: 'Option 1',
9537 * new OO.ui.MenuOptionWidget( {
9539 * label: 'Option One'
9541 * new OO.ui.MenuOptionWidget( {
9543 * label: 'Option Two'
9545 * new OO.ui.MenuOptionWidget( {
9547 * label: 'Option Three'
9549 * new OO.ui.MenuOptionWidget( {
9551 * label: 'Option Four'
9553 * new OO.ui.MenuOptionWidget( {
9555 * label: 'Option Five'
9560 * $( 'body' ).append( comboBox.$element );
9562 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
9565 * @extends OO.ui.TextInputWidget
9568 * @param {Object} [config] Configuration options
9569 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9570 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
9571 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
9572 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
9573 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
9575 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
9576 // Configuration initialization
9577 config
= $.extend( {
9581 // Parent constructor
9582 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
9585 this.$overlay
= config
.$overlay
|| this.$element
;
9586 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
9587 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
9589 disabled
: this.disabled
9591 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend(
9595 $container
: this.$element
,
9596 disabled
: this.isDisabled()
9602 this.connect( this, {
9603 change
: 'onInputChange',
9604 enter
: 'onInputEnter'
9606 this.dropdownButton
.connect( this, {
9607 click
: 'onDropdownButtonClick'
9609 this.menu
.connect( this, {
9610 choose
: 'onMenuChoose',
9611 add
: 'onMenuItemsChange',
9612 remove
: 'onMenuItemsChange'
9618 'aria-autocomplete': 'list'
9620 // Do not override options set via config.menu.items
9621 if ( config
.options
!== undefined ) {
9622 this.setOptions( config
.options
);
9624 this.$field
= $( '<div>' )
9625 .addClass( 'oo-ui-comboBoxInputWidget-field' )
9626 .append( this.$input
, this.dropdownButton
.$element
);
9628 .addClass( 'oo-ui-comboBoxInputWidget' )
9629 .append( this.$field
);
9630 this.$overlay
.append( this.menu
.$element
);
9631 this.onMenuItemsChange();
9636 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
9641 * Get the combobox's menu.
9643 * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
9645 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
9650 * Get the combobox's text input widget.
9652 * @return {OO.ui.TextInputWidget} Text input widget
9654 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
9659 * Handle input change events.
9662 * @param {string} value New value
9664 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
9665 var match
= this.menu
.getItemFromData( value
);
9667 this.menu
.selectItem( match
);
9668 if ( this.menu
.getHighlightedItem() ) {
9669 this.menu
.highlightItem( match
);
9672 if ( !this.isDisabled() ) {
9673 this.menu
.toggle( true );
9678 * Handle input enter events.
9682 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
9683 if ( !this.isDisabled() ) {
9684 this.menu
.toggle( false );
9689 * Handle button click events.
9693 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
9695 this.$input
[ 0 ].focus();
9699 * Handle menu choose events.
9702 * @param {OO.ui.OptionWidget} item Chosen item
9704 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
9705 this.setValue( item
.getData() );
9709 * Handle menu item change events.
9713 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
9714 var match
= this.menu
.getItemFromData( this.getValue() );
9715 this.menu
.selectItem( match
);
9716 if ( this.menu
.getHighlightedItem() ) {
9717 this.menu
.highlightItem( match
);
9719 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
9725 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
9727 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
9729 if ( this.dropdownButton
) {
9730 this.dropdownButton
.setDisabled( this.isDisabled() );
9733 this.menu
.setDisabled( this.isDisabled() );
9740 * Set the options available for this input.
9742 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9745 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
9748 .addItems( options
.map( function ( opt
) {
9749 return new OO
.ui
.MenuOptionWidget( {
9751 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
9759 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
9760 * which is a widget that is specified by reference before any optional configuration settings.
9762 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
9764 * - **left**: The label is placed before the field-widget and aligned with the left margin.
9765 * A left-alignment is used for forms with many fields.
9766 * - **right**: The label is placed before the field-widget and aligned to the right margin.
9767 * A right-alignment is used for long but familiar forms which users tab through,
9768 * verifying the current field with a quick glance at the label.
9769 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9770 * that users fill out from top to bottom.
9771 * - **inline**: The label is placed after the field-widget and aligned to the left.
9772 * An inline-alignment is best used with checkboxes or radio buttons.
9774 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
9775 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
9777 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9780 * @extends OO.ui.Layout
9781 * @mixins OO.ui.mixin.LabelElement
9782 * @mixins OO.ui.mixin.TitledElement
9785 * @param {OO.ui.Widget} fieldWidget Field widget
9786 * @param {Object} [config] Configuration options
9787 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
9788 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
9789 * The array may contain strings or OO.ui.HtmlSnippet instances.
9790 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
9791 * The array may contain strings or OO.ui.HtmlSnippet instances.
9792 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
9793 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
9794 * For important messages, you are advised to use `notices`, as they are always shown.
9796 * @throws {Error} An error is thrown if no widget is specified
9798 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
9799 var hasInputWidget
, $div
;
9801 // Allow passing positional parameters inside the config object
9802 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
9803 config
= fieldWidget
;
9804 fieldWidget
= config
.fieldWidget
;
9807 // Make sure we have required constructor arguments
9808 if ( fieldWidget
=== undefined ) {
9809 throw new Error( 'Widget not found' );
9812 hasInputWidget
= fieldWidget
.constructor.static.supportsSimpleLabel
;
9814 // Configuration initialization
9815 config
= $.extend( { align
: 'left' }, config
);
9817 // Parent constructor
9818 OO
.ui
.FieldLayout
.parent
.call( this, config
);
9820 // Mixin constructors
9821 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9822 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
9825 this.fieldWidget
= fieldWidget
;
9828 this.$field
= $( '<div>' );
9829 this.$messages
= $( '<ul>' );
9830 this.$body
= $( '<' + ( hasInputWidget
? 'label' : 'div' ) + '>' );
9832 if ( config
.help
) {
9833 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
9834 classes
: [ 'oo-ui-fieldLayout-help' ],
9839 $div
= $( '<div>' );
9840 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
9841 $div
.html( config
.help
.toString() );
9843 $div
.text( config
.help
);
9845 this.popupButtonWidget
.getPopup().$body
.append(
9846 $div
.addClass( 'oo-ui-fieldLayout-help-content' )
9848 this.$help
= this.popupButtonWidget
.$element
;
9850 this.$help
= $( [] );
9854 if ( hasInputWidget
) {
9855 this.$label
.on( 'click', this.onLabelClick
.bind( this ) );
9857 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
9861 .addClass( 'oo-ui-fieldLayout' )
9862 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
9863 .append( this.$help
, this.$body
);
9864 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
9865 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
9867 .addClass( 'oo-ui-fieldLayout-field' )
9868 .append( this.fieldWidget
.$element
);
9870 this.setErrors( config
.errors
|| [] );
9871 this.setNotices( config
.notices
|| [] );
9872 this.setAlignment( config
.align
);
9877 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
9878 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
9879 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
9884 * Handle field disable events.
9887 * @param {boolean} value Field is disabled
9889 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
9890 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
9894 * Handle label mouse click events.
9897 * @param {jQuery.Event} e Mouse click event
9899 OO
.ui
.FieldLayout
.prototype.onLabelClick = function () {
9900 this.fieldWidget
.simulateLabelClick();
9905 * Get the widget contained by the field.
9907 * @return {OO.ui.Widget} Field widget
9909 OO
.ui
.FieldLayout
.prototype.getField = function () {
9910 return this.fieldWidget
;
9915 * @param {string} kind 'error' or 'notice'
9916 * @param {string|OO.ui.HtmlSnippet} text
9919 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
9920 var $listItem
, $icon
, message
;
9921 $listItem
= $( '<li>' );
9922 if ( kind
=== 'error' ) {
9923 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
9924 } else if ( kind
=== 'notice' ) {
9925 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
9929 message
= new OO
.ui
.LabelWidget( { label
: text
} );
9931 .append( $icon
, message
.$element
)
9932 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
9937 * Set the field alignment mode.
9940 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
9943 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
9944 if ( value
!== this.align
) {
9945 // Default to 'left'
9946 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
9950 if ( value
=== 'inline' ) {
9951 this.$body
.append( this.$field
, this.$label
);
9953 this.$body
.append( this.$label
, this.$field
);
9955 // Set classes. The following classes can be used here:
9956 // * oo-ui-fieldLayout-align-left
9957 // * oo-ui-fieldLayout-align-right
9958 // * oo-ui-fieldLayout-align-top
9959 // * oo-ui-fieldLayout-align-inline
9961 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
9963 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
9971 * Set the list of error messages.
9973 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
9974 * The array may contain strings or OO.ui.HtmlSnippet instances.
9977 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
9978 this.errors
= errors
.slice();
9979 this.updateMessages();
9984 * Set the list of notice messages.
9986 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
9987 * The array may contain strings or OO.ui.HtmlSnippet instances.
9990 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
9991 this.notices
= notices
.slice();
9992 this.updateMessages();
9997 * Update the rendering of error and notice messages.
10001 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
10003 this.$messages
.empty();
10005 if ( this.errors
.length
|| this.notices
.length
) {
10006 this.$body
.after( this.$messages
);
10008 this.$messages
.remove();
10012 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
10013 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
10015 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
10016 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
10021 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
10022 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
10023 * is required and is specified before any optional configuration settings.
10025 * Labels can be aligned in one of four ways:
10027 * - **left**: The label is placed before the field-widget and aligned with the left margin.
10028 * A left-alignment is used for forms with many fields.
10029 * - **right**: The label is placed before the field-widget and aligned to the right margin.
10030 * A right-alignment is used for long but familiar forms which users tab through,
10031 * verifying the current field with a quick glance at the label.
10032 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
10033 * that users fill out from top to bottom.
10034 * - **inline**: The label is placed after the field-widget and aligned to the left.
10035 * An inline-alignment is best used with checkboxes or radio buttons.
10037 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
10038 * text is specified.
10041 * // Example of an ActionFieldLayout
10042 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
10043 * new OO.ui.TextInputWidget( {
10044 * placeholder: 'Field widget'
10046 * new OO.ui.ButtonWidget( {
10050 * label: 'An ActionFieldLayout. This label is aligned top',
10052 * help: 'This is help text'
10056 * $( 'body' ).append( actionFieldLayout.$element );
10059 * @extends OO.ui.FieldLayout
10062 * @param {OO.ui.Widget} fieldWidget Field widget
10063 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
10064 * @param {Object} config
10066 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
10067 // Allow passing positional parameters inside the config object
10068 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
10069 config
= fieldWidget
;
10070 fieldWidget
= config
.fieldWidget
;
10071 buttonWidget
= config
.buttonWidget
;
10074 // Parent constructor
10075 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
10078 this.buttonWidget
= buttonWidget
;
10079 this.$button
= $( '<div>' );
10080 this.$input
= $( '<div>' );
10084 .addClass( 'oo-ui-actionFieldLayout' );
10086 .addClass( 'oo-ui-actionFieldLayout-button' )
10087 .append( this.buttonWidget
.$element
);
10089 .addClass( 'oo-ui-actionFieldLayout-input' )
10090 .append( this.fieldWidget
.$element
);
10092 .append( this.$input
, this.$button
);
10097 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
10100 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
10101 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
10102 * configured with a label as well. For more information and examples,
10103 * please see the [OOjs UI documentation on MediaWiki][1].
10106 * // Example of a fieldset layout
10107 * var input1 = new OO.ui.TextInputWidget( {
10108 * placeholder: 'A text input field'
10111 * var input2 = new OO.ui.TextInputWidget( {
10112 * placeholder: 'A text input field'
10115 * var fieldset = new OO.ui.FieldsetLayout( {
10116 * label: 'Example of a fieldset layout'
10119 * fieldset.addItems( [
10120 * new OO.ui.FieldLayout( input1, {
10121 * label: 'Field One'
10123 * new OO.ui.FieldLayout( input2, {
10124 * label: 'Field Two'
10127 * $( 'body' ).append( fieldset.$element );
10129 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
10132 * @extends OO.ui.Layout
10133 * @mixins OO.ui.mixin.IconElement
10134 * @mixins OO.ui.mixin.LabelElement
10135 * @mixins OO.ui.mixin.GroupElement
10138 * @param {Object} [config] Configuration options
10139 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
10140 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
10141 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
10142 * For important messages, you are advised to use `notices`, as they are always shown.
10144 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
10147 // Configuration initialization
10148 config
= config
|| {};
10150 // Parent constructor
10151 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
10153 // Mixin constructors
10154 OO
.ui
.mixin
.IconElement
.call( this, config
);
10155 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: $( '<div>' ) } ) );
10156 OO
.ui
.mixin
.GroupElement
.call( this, config
);
10158 if ( config
.help
) {
10159 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
10160 classes
: [ 'oo-ui-fieldsetLayout-help' ],
10165 $div
= $( '<div>' );
10166 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
10167 $div
.html( config
.help
.toString() );
10169 $div
.text( config
.help
);
10171 this.popupButtonWidget
.getPopup().$body
.append(
10172 $div
.addClass( 'oo-ui-fieldsetLayout-help-content' )
10174 this.$help
= this.popupButtonWidget
.$element
;
10176 this.$help
= $( [] );
10180 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
10182 .addClass( 'oo-ui-fieldsetLayout' )
10183 .prepend( this.$label
, this.$help
, this.$icon
, this.$group
);
10184 if ( Array
.isArray( config
.items
) ) {
10185 this.addItems( config
.items
);
10191 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
10192 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
10193 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
10194 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
10196 /* Static Properties */
10198 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
10201 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
10202 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
10203 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
10204 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10206 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
10207 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
10208 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
10209 * some fancier controls. Some controls have both regular and InputWidget variants, for example
10210 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
10211 * often have simplified APIs to match the capabilities of HTML forms.
10212 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
10214 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
10215 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10218 * // Example of a form layout that wraps a fieldset layout
10219 * var input1 = new OO.ui.TextInputWidget( {
10220 * placeholder: 'Username'
10222 * var input2 = new OO.ui.TextInputWidget( {
10223 * placeholder: 'Password',
10226 * var submit = new OO.ui.ButtonInputWidget( {
10230 * var fieldset = new OO.ui.FieldsetLayout( {
10231 * label: 'A form layout'
10233 * fieldset.addItems( [
10234 * new OO.ui.FieldLayout( input1, {
10235 * label: 'Username',
10238 * new OO.ui.FieldLayout( input2, {
10239 * label: 'Password',
10242 * new OO.ui.FieldLayout( submit )
10244 * var form = new OO.ui.FormLayout( {
10245 * items: [ fieldset ],
10246 * action: '/api/formhandler',
10249 * $( 'body' ).append( form.$element );
10252 * @extends OO.ui.Layout
10253 * @mixins OO.ui.mixin.GroupElement
10256 * @param {Object} [config] Configuration options
10257 * @cfg {string} [method] HTML form `method` attribute
10258 * @cfg {string} [action] HTML form `action` attribute
10259 * @cfg {string} [enctype] HTML form `enctype` attribute
10260 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
10262 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
10265 // Configuration initialization
10266 config
= config
|| {};
10268 // Parent constructor
10269 OO
.ui
.FormLayout
.parent
.call( this, config
);
10271 // Mixin constructors
10272 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
10275 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
10277 // Make sure the action is safe
10278 action
= config
.action
;
10279 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
10280 action
= './' + action
;
10285 .addClass( 'oo-ui-formLayout' )
10287 method
: config
.method
,
10289 enctype
: config
.enctype
10291 if ( Array
.isArray( config
.items
) ) {
10292 this.addItems( config
.items
);
10298 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
10299 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
10304 * A 'submit' event is emitted when the form is submitted.
10309 /* Static Properties */
10311 OO
.ui
.FormLayout
.static.tagName
= 'form';
10316 * Handle form submit events.
10319 * @param {jQuery.Event} e Submit event
10322 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
10323 if ( this.emit( 'submit' ) ) {
10329 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
10330 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
10333 * // Example of a panel layout
10334 * var panel = new OO.ui.PanelLayout( {
10338 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
10340 * $( 'body' ).append( panel.$element );
10343 * @extends OO.ui.Layout
10346 * @param {Object} [config] Configuration options
10347 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10348 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10349 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10350 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10352 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
10353 // Configuration initialization
10354 config
= $.extend( {
10361 // Parent constructor
10362 OO
.ui
.PanelLayout
.parent
.call( this, config
);
10365 this.$element
.addClass( 'oo-ui-panelLayout' );
10366 if ( config
.scrollable
) {
10367 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
10369 if ( config
.padded
) {
10370 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
10372 if ( config
.expanded
) {
10373 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
10375 if ( config
.framed
) {
10376 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
10382 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
10387 * Focus the panel layout
10389 * The default implementation just focuses the first focusable element in the panel
10391 OO
.ui
.PanelLayout
.prototype.focus = function () {
10392 OO
.ui
.findFocusable( this.$element
).focus();
10396 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
10397 * items), with small margins between them. Convenient when you need to put a number of block-level
10398 * widgets on a single line next to each other.
10400 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
10403 * // HorizontalLayout with a text input and a label
10404 * var layout = new OO.ui.HorizontalLayout( {
10406 * new OO.ui.LabelWidget( { label: 'Label' } ),
10407 * new OO.ui.TextInputWidget( { value: 'Text' } )
10410 * $( 'body' ).append( layout.$element );
10413 * @extends OO.ui.Layout
10414 * @mixins OO.ui.mixin.GroupElement
10417 * @param {Object} [config] Configuration options
10418 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
10420 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
10421 // Configuration initialization
10422 config
= config
|| {};
10424 // Parent constructor
10425 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
10427 // Mixin constructors
10428 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
10431 this.$element
.addClass( 'oo-ui-horizontalLayout' );
10432 if ( Array
.isArray( config
.items
) ) {
10433 this.addItems( config
.items
);
10439 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
10440 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);