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-11-09T00:52:37Z
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 !$.contains( this.$element
[ 0 ], e
.target
) &&
4312 ( !this.$autoCloseIgnore
|| !this.$autoCloseIgnore
.has( e
.target
).length
)
4314 this.toggle( false );
4319 * Bind mouse down listener.
4323 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
4324 // Capture clicks outside popup
4325 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
4329 * Handles close button click events.
4333 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
4334 if ( this.isVisible() ) {
4335 this.toggle( false );
4340 * Unbind mouse down listener.
4344 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
4345 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
4349 * Handles key down events.
4352 * @param {KeyboardEvent} e Key down event
4354 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
4356 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
4359 this.toggle( false );
4361 e
.stopPropagation();
4366 * Bind key down listener.
4370 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
4371 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4375 * Unbind key down listener.
4379 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
4380 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4384 * Show, hide, or toggle the visibility of the anchor.
4386 * @param {boolean} [show] Show anchor, omit to toggle
4388 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
4389 show
= show
=== undefined ? !this.anchored
: !!show
;
4391 if ( this.anchored
!== show
) {
4393 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
4395 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
4397 this.anchored
= show
;
4402 * Check if the anchor is visible.
4404 * @return {boolean} Anchor is visible
4406 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
4413 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
4415 show
= show
=== undefined ? !this.isVisible() : !!show
;
4417 change
= show
!== this.isVisible();
4420 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
4424 if ( this.autoClose
) {
4425 this.bindMouseDownListener();
4426 this.bindKeyDownListener();
4428 this.updateDimensions();
4429 this.toggleClipping( true );
4431 this.toggleClipping( false );
4432 if ( this.autoClose
) {
4433 this.unbindMouseDownListener();
4434 this.unbindKeyDownListener();
4443 * Set the size of the popup.
4445 * Changing the size may also change the popup's position depending on the alignment.
4447 * @param {number} width Width in pixels
4448 * @param {number} height Height in pixels
4449 * @param {boolean} [transition=false] Use a smooth transition
4452 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
4454 this.height
= height
!== undefined ? height
: null;
4455 if ( this.isVisible() ) {
4456 this.updateDimensions( transition
);
4461 * Update the size and position.
4463 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
4464 * be called automatically.
4466 * @param {boolean} [transition=false] Use a smooth transition
4469 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
4470 var popupOffset
, originOffset
, containerLeft
, containerWidth
, containerRight
,
4471 popupLeft
, popupRight
, overlapLeft
, overlapRight
, anchorWidth
,
4475 if ( !this.$container
) {
4476 // Lazy-initialize $container if not specified in constructor
4477 this.$container
= $( this.getClosestScrollableElementContainer() );
4480 // Set height and width before measuring things, since it might cause our measurements
4481 // to change (e.g. due to scrollbars appearing or disappearing)
4484 height
: this.height
!== null ? this.height
: 'auto'
4487 // If we are in RTL, we need to flip the alignment, unless it is center
4488 if ( align
=== 'forwards' || align
=== 'backwards' ) {
4489 if ( this.$container
.css( 'direction' ) === 'rtl' ) {
4490 align
= ( { forwards
: 'force-left', backwards
: 'force-right' } )[ this.align
];
4492 align
= ( { forwards
: 'force-right', backwards
: 'force-left' } )[ this.align
];
4497 // Compute initial popupOffset based on alignment
4498 popupOffset
= this.width
* ( { 'force-left': -1, center
: -0.5, 'force-right': 0 } )[ align
];
4500 // Figure out if this will cause the popup to go beyond the edge of the container
4501 originOffset
= this.$element
.offset().left
;
4502 containerLeft
= this.$container
.offset().left
;
4503 containerWidth
= this.$container
.innerWidth();
4504 containerRight
= containerLeft
+ containerWidth
;
4505 popupLeft
= popupOffset
- this.containerPadding
;
4506 popupRight
= popupOffset
+ this.containerPadding
+ this.width
+ this.containerPadding
;
4507 overlapLeft
= ( originOffset
+ popupLeft
) - containerLeft
;
4508 overlapRight
= containerRight
- ( originOffset
+ popupRight
);
4510 // Adjust offset to make the popup not go beyond the edge, if needed
4511 if ( overlapRight
< 0 ) {
4512 popupOffset
+= overlapRight
;
4513 } else if ( overlapLeft
< 0 ) {
4514 popupOffset
-= overlapLeft
;
4517 // Adjust offset to avoid anchor being rendered too close to the edge
4518 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
4519 // TODO: Find a measurement that works for CSS anchors and image anchors
4520 anchorWidth
= this.$anchor
[ 0 ].scrollWidth
* 2;
4521 if ( popupOffset
+ this.width
< anchorWidth
) {
4522 popupOffset
= anchorWidth
- this.width
;
4523 } else if ( -popupOffset
< anchorWidth
) {
4524 popupOffset
= -anchorWidth
;
4527 // Prevent transition from being interrupted
4528 clearTimeout( this.transitionTimeout
);
4530 // Enable transition
4531 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
4534 // Position body relative to anchor
4535 this.$popup
.css( 'margin-left', popupOffset
);
4538 // Prevent transitioning after transition is complete
4539 this.transitionTimeout
= setTimeout( function () {
4540 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4543 // Prevent transitioning immediately
4544 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4547 // Reevaluate clipping state since we've relocated and resized the popup
4554 * Set popup alignment
4556 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4557 * `backwards` or `forwards`.
4559 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
4560 // Validate alignment and transform deprecated values
4561 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
4562 this.align
= { left
: 'force-right', right
: 'force-left' }[ align
] || align
;
4564 this.align
= 'center';
4569 * Get popup alignment
4571 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4572 * `backwards` or `forwards`.
4574 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
4579 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
4580 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
4581 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
4582 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
4588 * @param {Object} [config] Configuration options
4589 * @cfg {Object} [popup] Configuration to pass to popup
4590 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
4592 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
4593 // Configuration initialization
4594 config
= config
|| {};
4597 this.popup
= new OO
.ui
.PopupWidget( $.extend(
4598 { autoClose
: true },
4600 { $autoCloseIgnore
: this.$element
}
4609 * @return {OO.ui.PopupWidget} Popup widget
4611 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
4616 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
4617 * which is used to display additional information or options.
4620 * // Example of a popup button.
4621 * var popupButton = new OO.ui.PopupButtonWidget( {
4622 * label: 'Popup button with options',
4625 * $content: $( '<p>Additional options here.</p>' ),
4627 * align: 'force-left'
4630 * // Append the button to the DOM.
4631 * $( 'body' ).append( popupButton.$element );
4634 * @extends OO.ui.ButtonWidget
4635 * @mixins OO.ui.mixin.PopupElement
4638 * @param {Object} [config] Configuration options
4640 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
4641 // Parent constructor
4642 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
4644 // Mixin constructors
4645 OO
.ui
.mixin
.PopupElement
.call( this, config
);
4648 this.connect( this, { click
: 'onAction' } );
4652 .addClass( 'oo-ui-popupButtonWidget' )
4653 .attr( 'aria-haspopup', 'true' )
4654 .append( this.popup
.$element
);
4659 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
4660 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
4665 * Handle the button action being triggered.
4669 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
4670 this.popup
.toggle();
4674 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
4676 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
4681 * @mixins OO.ui.mixin.GroupElement
4684 * @param {Object} [config] Configuration options
4686 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
4687 // Mixin constructors
4688 OO
.ui
.mixin
.GroupElement
.call( this, config
);
4693 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
4698 * Set the disabled state of the widget.
4700 * This will also update the disabled state of child widgets.
4702 * @param {boolean} disabled Disable widget
4705 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
4709 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
4710 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
4712 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
4714 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4715 this.items
[ i
].updateDisabled();
4723 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
4725 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
4726 * allows bidirectional communication.
4728 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
4736 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
4743 * Check if widget is disabled.
4745 * Checks parent if present, making disabled state inheritable.
4747 * @return {boolean} Widget is disabled
4749 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
4750 return this.disabled
||
4751 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
4755 * Set group element is in.
4757 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
4760 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
4762 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
4763 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
4765 // Initialize item disabled states
4766 this.updateDisabled();
4772 * OptionWidgets are special elements that can be selected and configured with data. The
4773 * data is often unique for each option, but it does not have to be. OptionWidgets are used
4774 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
4775 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
4777 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4780 * @extends OO.ui.Widget
4781 * @mixins OO.ui.mixin.ItemWidget
4782 * @mixins OO.ui.mixin.LabelElement
4783 * @mixins OO.ui.mixin.FlaggedElement
4784 * @mixins OO.ui.mixin.AccessKeyedElement
4787 * @param {Object} [config] Configuration options
4789 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
4790 // Configuration initialization
4791 config
= config
|| {};
4793 // Parent constructor
4794 OO
.ui
.OptionWidget
.parent
.call( this, config
);
4796 // Mixin constructors
4797 OO
.ui
.mixin
.ItemWidget
.call( this );
4798 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4799 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
4800 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
4803 this.selected
= false;
4804 this.highlighted
= false;
4805 this.pressed
= false;
4809 .data( 'oo-ui-optionWidget', this )
4810 // Allow programmatic focussing (and by accesskey), but not tabbing
4811 .attr( 'tabindex', '-1' )
4812 .attr( 'role', 'option' )
4813 .attr( 'aria-selected', 'false' )
4814 .addClass( 'oo-ui-optionWidget' )
4815 .append( this.$label
);
4820 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
4821 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
4822 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
4823 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
4824 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
4826 /* Static Properties */
4828 OO
.ui
.OptionWidget
.static.selectable
= true;
4830 OO
.ui
.OptionWidget
.static.highlightable
= true;
4832 OO
.ui
.OptionWidget
.static.pressable
= true;
4834 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
4839 * Check if the option can be selected.
4841 * @return {boolean} Item is selectable
4843 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
4844 return this.constructor.static.selectable
&& !this.isDisabled() && this.isVisible();
4848 * Check if the option can be highlighted. A highlight indicates that the option
4849 * may be selected when a user presses enter or clicks. Disabled items cannot
4852 * @return {boolean} Item is highlightable
4854 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
4855 return this.constructor.static.highlightable
&& !this.isDisabled() && this.isVisible();
4859 * Check if the option can be pressed. The pressed state occurs when a user mouses
4860 * down on an item, but has not yet let go of the mouse.
4862 * @return {boolean} Item is pressable
4864 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
4865 return this.constructor.static.pressable
&& !this.isDisabled() && this.isVisible();
4869 * Check if the option is selected.
4871 * @return {boolean} Item is selected
4873 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
4874 return this.selected
;
4878 * Check if the option is highlighted. A highlight indicates that the
4879 * item may be selected when a user presses enter or clicks.
4881 * @return {boolean} Item is highlighted
4883 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
4884 return this.highlighted
;
4888 * Check if the option is pressed. The pressed state occurs when a user mouses
4889 * down on an item, but has not yet let go of the mouse. The item may appear
4890 * selected, but it will not be selected until the user releases the mouse.
4892 * @return {boolean} Item is pressed
4894 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
4895 return this.pressed
;
4899 * Set the option’s selected state. In general, all modifications to the selection
4900 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
4901 * method instead of this method.
4903 * @param {boolean} [state=false] Select option
4906 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
4907 if ( this.constructor.static.selectable
) {
4908 this.selected
= !!state
;
4910 .toggleClass( 'oo-ui-optionWidget-selected', state
)
4911 .attr( 'aria-selected', state
.toString() );
4912 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
4913 this.scrollElementIntoView();
4915 this.updateThemeClasses();
4921 * Set the option’s highlighted state. In general, all programmatic
4922 * modifications to the highlight should be handled by the
4923 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
4924 * method instead of this method.
4926 * @param {boolean} [state=false] Highlight option
4929 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
4930 if ( this.constructor.static.highlightable
) {
4931 this.highlighted
= !!state
;
4932 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
4933 this.updateThemeClasses();
4939 * Set the option’s pressed state. In general, all
4940 * programmatic modifications to the pressed state should be handled by the
4941 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
4942 * method instead of this method.
4944 * @param {boolean} [state=false] Press option
4947 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
4948 if ( this.constructor.static.pressable
) {
4949 this.pressed
= !!state
;
4950 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
4951 this.updateThemeClasses();
4957 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
4958 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
4959 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
4962 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
4963 * information, please see the [OOjs UI documentation on MediaWiki][1].
4966 * // Example of a select widget with three options
4967 * var select = new OO.ui.SelectWidget( {
4969 * new OO.ui.OptionWidget( {
4971 * label: 'Option One',
4973 * new OO.ui.OptionWidget( {
4975 * label: 'Option Two',
4977 * new OO.ui.OptionWidget( {
4979 * label: 'Option Three',
4983 * $( 'body' ).append( select.$element );
4985 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4989 * @extends OO.ui.Widget
4990 * @mixins OO.ui.mixin.GroupWidget
4993 * @param {Object} [config] Configuration options
4994 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
4995 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
4996 * the [OOjs UI documentation on MediaWiki] [2] for examples.
4997 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4999 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
5000 // Configuration initialization
5001 config
= config
|| {};
5003 // Parent constructor
5004 OO
.ui
.SelectWidget
.parent
.call( this, config
);
5006 // Mixin constructors
5007 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
5010 this.pressed
= false;
5011 this.selecting
= null;
5012 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
5013 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
5014 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
5015 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
5016 this.keyPressBuffer
= '';
5017 this.keyPressBufferTimer
= null;
5018 this.blockMouseOverEvents
= 0;
5021 this.connect( this, {
5025 focusin
: this.onFocus
.bind( this ),
5026 mousedown
: this.onMouseDown
.bind( this ),
5027 mouseover
: this.onMouseOver
.bind( this ),
5028 mouseleave
: this.onMouseLeave
.bind( this )
5033 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
5034 .attr( 'role', 'listbox' );
5035 if ( Array
.isArray( config
.items
) ) {
5036 this.addItems( config
.items
);
5042 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
5043 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
5050 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
5052 * @param {OO.ui.OptionWidget|null} item Highlighted item
5058 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
5059 * pressed state of an option.
5061 * @param {OO.ui.OptionWidget|null} item Pressed item
5067 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
5069 * @param {OO.ui.OptionWidget|null} item Selected item
5074 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
5075 * @param {OO.ui.OptionWidget} item Chosen item
5081 * An `add` event is emitted when options are added to the select with the #addItems method.
5083 * @param {OO.ui.OptionWidget[]} items Added items
5084 * @param {number} index Index of insertion point
5090 * A `remove` event is emitted when options are removed from the select with the #clearItems
5091 * or #removeItems methods.
5093 * @param {OO.ui.OptionWidget[]} items Removed items
5099 * Handle focus events
5102 * @param {jQuery.Event} event
5104 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
5106 if ( event
.target
=== this.$element
[ 0 ] ) {
5107 // This widget was focussed, e.g. by the user tabbing to it.
5108 // The styles for focus state depend on one of the items being selected.
5109 if ( !this.getSelectedItem() ) {
5110 item
= this.getFirstSelectableItem();
5113 // One of the options got focussed (and the event bubbled up here).
5114 // They can't be tabbed to, but they can be activated using accesskeys.
5115 item
= this.getTargetItem( event
);
5119 if ( item
.constructor.static.highlightable
) {
5120 this.highlightItem( item
);
5122 this.selectItem( item
);
5126 if ( event
.target
!== this.$element
[ 0 ] ) {
5127 this.$element
.focus();
5132 * Handle mouse down events.
5135 * @param {jQuery.Event} e Mouse down event
5137 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
5140 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
5141 this.togglePressed( true );
5142 item
= this.getTargetItem( e
);
5143 if ( item
&& item
.isSelectable() ) {
5144 this.pressItem( item
);
5145 this.selecting
= item
;
5146 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
5147 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5154 * Handle mouse up events.
5157 * @param {MouseEvent} e Mouse up event
5159 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
5162 this.togglePressed( false );
5163 if ( !this.selecting
) {
5164 item
= this.getTargetItem( e
);
5165 if ( item
&& item
.isSelectable() ) {
5166 this.selecting
= item
;
5169 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
5170 this.pressItem( null );
5171 this.chooseItem( this.selecting
);
5172 this.selecting
= null;
5175 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
5176 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5182 * Handle mouse move events.
5185 * @param {MouseEvent} e Mouse move event
5187 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
5190 if ( !this.isDisabled() && this.pressed
) {
5191 item
= this.getTargetItem( e
);
5192 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
5193 this.pressItem( item
);
5194 this.selecting
= item
;
5200 * Handle mouse over events.
5203 * @param {jQuery.Event} e Mouse over event
5205 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
5207 if ( this.blockMouseOverEvents
) {
5210 if ( !this.isDisabled() ) {
5211 item
= this.getTargetItem( e
);
5212 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
5218 * Handle mouse leave events.
5221 * @param {jQuery.Event} e Mouse over event
5223 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
5224 if ( !this.isDisabled() ) {
5225 this.highlightItem( null );
5231 * Handle key down events.
5234 * @param {KeyboardEvent} e Key down event
5236 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
5239 currentItem
= this.getHighlightedItem() || this.getSelectedItem();
5241 if ( !this.isDisabled() && this.isVisible() ) {
5242 switch ( e
.keyCode
) {
5243 case OO
.ui
.Keys
.ENTER
:
5244 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5245 // Was only highlighted, now let's select it. No-op if already selected.
5246 this.chooseItem( currentItem
);
5251 case OO
.ui
.Keys
.LEFT
:
5252 this.clearKeyPressBuffer();
5253 nextItem
= this.getRelativeSelectableItem( currentItem
, -1 );
5256 case OO
.ui
.Keys
.DOWN
:
5257 case OO
.ui
.Keys
.RIGHT
:
5258 this.clearKeyPressBuffer();
5259 nextItem
= this.getRelativeSelectableItem( currentItem
, 1 );
5262 case OO
.ui
.Keys
.ESCAPE
:
5263 case OO
.ui
.Keys
.TAB
:
5264 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5265 currentItem
.setHighlighted( false );
5267 this.unbindKeyDownListener();
5268 this.unbindKeyPressListener();
5269 // Don't prevent tabbing away / defocusing
5275 if ( nextItem
.constructor.static.highlightable
) {
5276 this.highlightItem( nextItem
);
5278 this.chooseItem( nextItem
);
5280 this.scrollItemIntoView( nextItem
);
5285 e
.stopPropagation();
5291 * Bind key down listener.
5295 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
5296 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
5300 * Unbind key down listener.
5304 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
5305 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
5309 * Scroll item into view, preventing spurious mouse highlight actions from happening.
5311 * @param {OO.ui.OptionWidget} item Item to scroll into view
5313 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
5315 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
5316 // and around 100-150 ms after it is finished.
5317 this.blockMouseOverEvents
++;
5318 item
.scrollElementIntoView().done( function () {
5319 setTimeout( function () {
5320 widget
.blockMouseOverEvents
--;
5326 * Clear the key-press buffer
5330 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
5331 if ( this.keyPressBufferTimer
) {
5332 clearTimeout( this.keyPressBufferTimer
);
5333 this.keyPressBufferTimer
= null;
5335 this.keyPressBuffer
= '';
5339 * Handle key press events.
5342 * @param {KeyboardEvent} e Key press event
5344 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
5345 var c
, filter
, item
;
5347 if ( !e
.charCode
) {
5348 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
5349 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
5354 if ( String
.fromCodePoint
) {
5355 c
= String
.fromCodePoint( e
.charCode
);
5357 c
= String
.fromCharCode( e
.charCode
);
5360 if ( this.keyPressBufferTimer
) {
5361 clearTimeout( this.keyPressBufferTimer
);
5363 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
5365 item
= this.getHighlightedItem() || this.getSelectedItem();
5367 if ( this.keyPressBuffer
=== c
) {
5368 // Common (if weird) special case: typing "xxxx" will cycle through all
5369 // the items beginning with "x".
5371 item
= this.getRelativeSelectableItem( item
, 1 );
5374 this.keyPressBuffer
+= c
;
5377 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
5378 if ( !item
|| !filter( item
) ) {
5379 item
= this.getRelativeSelectableItem( item
, 1, filter
);
5382 if ( this.isVisible() && item
.constructor.static.highlightable
) {
5383 this.highlightItem( item
);
5385 this.chooseItem( item
);
5387 this.scrollItemIntoView( item
);
5391 e
.stopPropagation();
5395 * Get a matcher for the specific string
5398 * @param {string} s String to match against items
5399 * @param {boolean} [exact=false] Only accept exact matches
5400 * @return {Function} function ( OO.ui.OptionItem ) => boolean
5402 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
5405 if ( s
.normalize
) {
5408 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
5409 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
5413 re
= new RegExp( re
, 'i' );
5414 return function ( item
) {
5415 var l
= item
.getLabel();
5416 if ( typeof l
!== 'string' ) {
5417 l
= item
.$label
.text();
5419 if ( l
.normalize
) {
5422 return re
.test( l
);
5427 * Bind key press listener.
5431 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
5432 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
5436 * Unbind key down listener.
5438 * If you override this, be sure to call this.clearKeyPressBuffer() from your
5443 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
5444 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
5445 this.clearKeyPressBuffer();
5449 * Visibility change handler
5452 * @param {boolean} visible
5454 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
5456 this.clearKeyPressBuffer();
5461 * Get the closest item to a jQuery.Event.
5464 * @param {jQuery.Event} e
5465 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
5467 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
5468 return $( e
.target
).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
5472 * Get selected item.
5474 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
5476 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
5479 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5480 if ( this.items
[ i
].isSelected() ) {
5481 return this.items
[ i
];
5488 * Get highlighted item.
5490 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
5492 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
5495 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5496 if ( this.items
[ i
].isHighlighted() ) {
5497 return this.items
[ i
];
5504 * Toggle pressed state.
5506 * Press is a state that occurs when a user mouses down on an item, but
5507 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
5508 * until the user releases the mouse.
5510 * @param {boolean} pressed An option is being pressed
5512 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
5513 if ( pressed
=== undefined ) {
5514 pressed
= !this.pressed
;
5516 if ( pressed
!== this.pressed
) {
5518 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
5519 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
5520 this.pressed
= pressed
;
5525 * Highlight an option. If the `item` param is omitted, no options will be highlighted
5526 * and any existing highlight will be removed. The highlight is mutually exclusive.
5528 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
5532 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
5533 var i
, len
, highlighted
,
5536 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5537 highlighted
= this.items
[ i
] === item
;
5538 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
5539 this.items
[ i
].setHighlighted( highlighted
);
5544 this.emit( 'highlight', item
);
5551 * Fetch an item by its label.
5553 * @param {string} label Label of the item to select.
5554 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5555 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
5557 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
5559 len
= this.items
.length
,
5560 filter
= this.getItemMatcher( label
, true );
5562 for ( i
= 0; i
< len
; i
++ ) {
5563 item
= this.items
[ i
];
5564 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5571 filter
= this.getItemMatcher( label
, false );
5572 for ( i
= 0; i
< len
; i
++ ) {
5573 item
= this.items
[ i
];
5574 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5590 * Programmatically select an option by its label. If the item does not exist,
5591 * all options will be deselected.
5593 * @param {string} [label] Label of the item to select.
5594 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5598 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
5599 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
5600 if ( label
=== undefined || !itemFromLabel
) {
5601 return this.selectItem();
5603 return this.selectItem( itemFromLabel
);
5607 * Programmatically select an option by its data. If the `data` parameter is omitted,
5608 * or if the item does not exist, all options will be deselected.
5610 * @param {Object|string} [data] Value of the item to select, omit to deselect all
5614 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
5615 var itemFromData
= this.getItemFromData( data
);
5616 if ( data
=== undefined || !itemFromData
) {
5617 return this.selectItem();
5619 return this.selectItem( itemFromData
);
5623 * Programmatically select an option by its reference. If the `item` parameter is omitted,
5624 * all options will be deselected.
5626 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
5630 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
5631 var i
, len
, selected
,
5634 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5635 selected
= this.items
[ i
] === item
;
5636 if ( this.items
[ i
].isSelected() !== selected
) {
5637 this.items
[ i
].setSelected( selected
);
5642 this.emit( 'select', item
);
5651 * Press is a state that occurs when a user mouses down on an item, but has not
5652 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
5653 * releases the mouse.
5655 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
5659 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
5660 var i
, len
, pressed
,
5663 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5664 pressed
= this.items
[ i
] === item
;
5665 if ( this.items
[ i
].isPressed() !== pressed
) {
5666 this.items
[ i
].setPressed( pressed
);
5671 this.emit( 'press', item
);
5680 * Note that ‘choose’ should never be modified programmatically. A user can choose
5681 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
5682 * use the #selectItem method.
5684 * This method is identical to #selectItem, but may vary in subclasses that take additional action
5685 * when users choose an item with the keyboard or mouse.
5687 * @param {OO.ui.OptionWidget} item Item to choose
5691 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
5693 this.selectItem( item
);
5694 this.emit( 'choose', item
);
5701 * Get an option by its position relative to the specified item (or to the start of the option array,
5702 * if item is `null`). The direction in which to search through the option array is specified with a
5703 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
5704 * `null` if there are no options in the array.
5706 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
5707 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
5708 * @param {Function} [filter] Only consider items for which this function returns
5709 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
5710 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
5712 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
, filter
) {
5713 var currentIndex
, nextIndex
, i
,
5714 increase
= direction
> 0 ? 1 : -1,
5715 len
= this.items
.length
;
5717 if ( item
instanceof OO
.ui
.OptionWidget
) {
5718 currentIndex
= this.items
.indexOf( item
);
5719 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
5721 // If no item is selected and moving forward, start at the beginning.
5722 // If moving backward, start at the end.
5723 nextIndex
= direction
> 0 ? 0 : len
- 1;
5726 for ( i
= 0; i
< len
; i
++ ) {
5727 item
= this.items
[ nextIndex
];
5729 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
5730 ( !filter
|| filter( item
) )
5734 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
5740 * Get the next selectable item or `null` if there are no selectable items.
5741 * Disabled options and menu-section markers and breaks are not selectable.
5743 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
5745 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
5746 return this.getRelativeSelectableItem( null, 1 );
5750 * Add an array of options to the select. Optionally, an index number can be used to
5751 * specify an insertion point.
5753 * @param {OO.ui.OptionWidget[]} items Items to add
5754 * @param {number} [index] Index to insert items after
5758 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
5760 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
5762 // Always provide an index, even if it was omitted
5763 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
5769 * Remove the specified array of options from the select. Options will be detached
5770 * from the DOM, not removed, so they can be reused later. To remove all options from
5771 * the select, you may wish to use the #clearItems method instead.
5773 * @param {OO.ui.OptionWidget[]} items Items to remove
5777 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
5780 // Deselect items being removed
5781 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
5783 if ( item
.isSelected() ) {
5784 this.selectItem( null );
5789 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
5791 this.emit( 'remove', items
);
5797 * Clear all options from the select. Options will be detached from the DOM, not removed,
5798 * so that they can be reused later. To remove a subset of options from the select, use
5799 * the #removeItems method.
5804 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
5805 var items
= this.items
.slice();
5808 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
5811 this.selectItem( null );
5813 this.emit( 'remove', items
);
5819 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
5820 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
5821 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
5822 * options. For more information about options and selects, please see the
5823 * [OOjs UI documentation on MediaWiki][1].
5826 * // Decorated options in a select widget
5827 * var select = new OO.ui.SelectWidget( {
5829 * new OO.ui.DecoratedOptionWidget( {
5831 * label: 'Option with icon',
5834 * new OO.ui.DecoratedOptionWidget( {
5836 * label: 'Option with indicator',
5841 * $( 'body' ).append( select.$element );
5843 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5846 * @extends OO.ui.OptionWidget
5847 * @mixins OO.ui.mixin.IconElement
5848 * @mixins OO.ui.mixin.IndicatorElement
5851 * @param {Object} [config] Configuration options
5853 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
5854 // Parent constructor
5855 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
5857 // Mixin constructors
5858 OO
.ui
.mixin
.IconElement
.call( this, config
);
5859 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
5863 .addClass( 'oo-ui-decoratedOptionWidget' )
5864 .prepend( this.$icon
)
5865 .append( this.$indicator
);
5870 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
5871 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
5872 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
5875 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
5876 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
5877 * the [OOjs UI documentation on MediaWiki] [1] for more information.
5879 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
5882 * @extends OO.ui.DecoratedOptionWidget
5885 * @param {Object} [config] Configuration options
5887 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
5888 // Configuration initialization
5889 config
= $.extend( { icon
: 'check' }, config
);
5891 // Parent constructor
5892 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
5896 .attr( 'role', 'menuitem' )
5897 .addClass( 'oo-ui-menuOptionWidget' );
5902 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
5904 /* Static Properties */
5906 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
5909 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
5910 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
5913 * var myDropdown = new OO.ui.DropdownWidget( {
5916 * new OO.ui.MenuSectionOptionWidget( {
5919 * new OO.ui.MenuOptionWidget( {
5921 * label: 'Welsh Corgi'
5923 * new OO.ui.MenuOptionWidget( {
5925 * label: 'Standard Poodle'
5927 * new OO.ui.MenuSectionOptionWidget( {
5930 * new OO.ui.MenuOptionWidget( {
5937 * $( 'body' ).append( myDropdown.$element );
5940 * @extends OO.ui.DecoratedOptionWidget
5943 * @param {Object} [config] Configuration options
5945 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
5946 // Parent constructor
5947 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
5950 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' );
5955 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
5957 /* Static Properties */
5959 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
5961 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
5964 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
5965 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
5966 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
5967 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
5968 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
5969 * and customized to be opened, closed, and displayed as needed.
5971 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
5972 * mouse outside the menu.
5974 * Menus also have support for keyboard interaction:
5976 * - Enter/Return key: choose and select a menu option
5977 * - Up-arrow key: highlight the previous menu option
5978 * - Down-arrow key: highlight the next menu option
5979 * - Esc key: hide the menu
5981 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
5982 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5985 * @extends OO.ui.SelectWidget
5986 * @mixins OO.ui.mixin.ClippableElement
5989 * @param {Object} [config] Configuration options
5990 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
5991 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
5992 * and {@link OO.ui.mixin.LookupElement LookupElement}
5993 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
5994 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
5995 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
5996 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
5997 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
5998 * that button, unless the button (or its parent widget) is passed in here.
5999 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
6000 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
6002 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
6003 // Configuration initialization
6004 config
= config
|| {};
6006 // Parent constructor
6007 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
6009 // Mixin constructors
6010 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
6013 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
6014 this.filterFromInput
= !!config
.filterFromInput
;
6015 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
6016 this.$widget
= config
.widget
? config
.widget
.$element
: null;
6017 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
6018 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
6022 .addClass( 'oo-ui-menuSelectWidget' )
6023 .attr( 'role', 'menu' );
6025 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
6026 // that reference properties not initialized at that time of parent class construction
6027 // TODO: Find a better way to handle post-constructor setup
6028 this.visible
= false;
6029 this.$element
.addClass( 'oo-ui-element-hidden' );
6034 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
6035 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
6040 * Handles document mouse down events.
6043 * @param {MouseEvent} e Mouse down event
6045 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
6047 !OO
.ui
.contains( this.$element
[ 0 ], e
.target
, true ) &&
6048 ( !this.$widget
|| !OO
.ui
.contains( this.$widget
[ 0 ], e
.target
, true ) )
6050 this.toggle( false );
6057 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
6058 var currentItem
= this.getHighlightedItem() || this.getSelectedItem();
6060 if ( !this.isDisabled() && this.isVisible() ) {
6061 switch ( e
.keyCode
) {
6062 case OO
.ui
.Keys
.LEFT
:
6063 case OO
.ui
.Keys
.RIGHT
:
6064 // Do nothing if a text field is associated, arrow keys will be handled natively
6065 if ( !this.$input
) {
6066 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
6069 case OO
.ui
.Keys
.ESCAPE
:
6070 case OO
.ui
.Keys
.TAB
:
6071 if ( currentItem
) {
6072 currentItem
.setHighlighted( false );
6074 this.toggle( false );
6075 // Don't prevent tabbing away, prevent defocusing
6076 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
6078 e
.stopPropagation();
6082 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
6089 * Update menu item visibility after input changes.
6093 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
6095 len
= this.items
.length
,
6096 showAll
= !this.isVisible(),
6097 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
6099 for ( i
= 0; i
< len
; i
++ ) {
6100 item
= this.items
[ i
];
6101 if ( item
instanceof OO
.ui
.OptionWidget
) {
6102 item
.toggle( showAll
|| filter( item
) );
6106 // Reevaluate clipping
6113 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
6114 if ( this.$input
) {
6115 this.$input
.on( 'keydown', this.onKeyDownHandler
);
6117 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
6124 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
6125 if ( this.$input
) {
6126 this.$input
.off( 'keydown', this.onKeyDownHandler
);
6128 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
6135 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
6136 if ( this.$input
) {
6137 if ( this.filterFromInput
) {
6138 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6141 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
6148 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
6149 if ( this.$input
) {
6150 if ( this.filterFromInput
) {
6151 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6152 this.updateItemVisibility();
6155 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
6162 * When a user chooses an item, the menu is closed.
6164 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
6165 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
6167 * @param {OO.ui.OptionWidget} item Item to choose
6170 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
6171 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
6172 this.toggle( false );
6179 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
6181 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
6183 // Reevaluate clipping
6192 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
6194 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
6196 // Reevaluate clipping
6205 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
6207 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
6209 // Reevaluate clipping
6218 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
6221 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
6222 change
= visible
!== this.isVisible();
6225 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
6229 this.bindKeyDownListener();
6230 this.bindKeyPressListener();
6232 this.toggleClipping( true );
6234 if ( this.getSelectedItem() ) {
6235 this.getSelectedItem().scrollElementIntoView( { duration
: 0 } );
6239 if ( this.autoHide
) {
6240 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6243 this.unbindKeyDownListener();
6244 this.unbindKeyPressListener();
6245 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6246 this.toggleClipping( false );
6254 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
6255 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
6256 * users can interact with it.
6258 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6259 * OO.ui.DropdownInputWidget instead.
6262 * // Example: A DropdownWidget with a menu that contains three options
6263 * var dropDown = new OO.ui.DropdownWidget( {
6264 * label: 'Dropdown menu: Select a menu option',
6267 * new OO.ui.MenuOptionWidget( {
6271 * new OO.ui.MenuOptionWidget( {
6275 * new OO.ui.MenuOptionWidget( {
6283 * $( 'body' ).append( dropDown.$element );
6285 * dropDown.getMenu().selectItemByData( 'b' );
6287 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
6289 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
6291 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6294 * @extends OO.ui.Widget
6295 * @mixins OO.ui.mixin.IconElement
6296 * @mixins OO.ui.mixin.IndicatorElement
6297 * @mixins OO.ui.mixin.LabelElement
6298 * @mixins OO.ui.mixin.TitledElement
6299 * @mixins OO.ui.mixin.TabIndexedElement
6302 * @param {Object} [config] Configuration options
6303 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
6304 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
6305 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6306 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
6308 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
6309 // Configuration initialization
6310 config
= $.extend( { indicator
: 'down' }, config
);
6312 // Parent constructor
6313 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
6315 // Properties (must be set before TabIndexedElement constructor call)
6316 this.$handle
= this.$( '<span>' );
6317 this.$overlay
= config
.$overlay
|| this.$element
;
6319 // Mixin constructors
6320 OO
.ui
.mixin
.IconElement
.call( this, config
);
6321 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
6322 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6323 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
6324 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
6327 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend( {
6329 $container
: this.$element
6334 click
: this.onClick
.bind( this ),
6335 keydown
: this.onKeyDown
.bind( this ),
6336 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
6337 keypress
: this.menu
.onKeyPressHandler
,
6338 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
6340 this.menu
.connect( this, {
6341 select
: 'onMenuSelect',
6342 toggle
: 'onMenuToggle'
6347 .addClass( 'oo-ui-dropdownWidget-handle' )
6348 .append( this.$icon
, this.$label
, this.$indicator
);
6350 .addClass( 'oo-ui-dropdownWidget' )
6351 .append( this.$handle
);
6352 this.$overlay
.append( this.menu
.$element
);
6357 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
6358 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
6359 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
6360 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
6361 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
6362 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6369 * @return {OO.ui.MenuSelectWidget} Menu of widget
6371 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
6376 * Handles menu select events.
6379 * @param {OO.ui.MenuOptionWidget} item Selected menu item
6381 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
6385 this.setLabel( null );
6389 selectedLabel
= item
.getLabel();
6391 // If the label is a DOM element, clone it, because setLabel will append() it
6392 if ( selectedLabel
instanceof jQuery
) {
6393 selectedLabel
= selectedLabel
.clone();
6396 this.setLabel( selectedLabel
);
6400 * Handle menu toggle events.
6403 * @param {boolean} isVisible Menu toggle event
6405 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
6406 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
6410 * Handle mouse click events.
6413 * @param {jQuery.Event} e Mouse click event
6415 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
6416 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6423 * Handle key down events.
6426 * @param {jQuery.Event} e Key down event
6428 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
6430 !this.isDisabled() &&
6432 e
.which
=== OO
.ui
.Keys
.ENTER
||
6434 !this.menu
.isVisible() &&
6436 e
.which
=== OO
.ui
.Keys
.SPACE
||
6437 e
.which
=== OO
.ui
.Keys
.UP
||
6438 e
.which
=== OO
.ui
.Keys
.DOWN
6449 * RadioOptionWidget is an option widget that looks like a radio button.
6450 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
6451 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6453 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6456 * @extends OO.ui.OptionWidget
6459 * @param {Object} [config] Configuration options
6461 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
6462 // Configuration initialization
6463 config
= config
|| {};
6465 // Properties (must be done before parent constructor which calls #setDisabled)
6466 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
6468 // Parent constructor
6469 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
6472 // Remove implicit role, we're handling it ourselves
6473 this.radio
.$input
.attr( 'role', 'presentation' );
6475 .addClass( 'oo-ui-radioOptionWidget' )
6476 .attr( 'role', 'radio' )
6477 .attr( 'aria-checked', 'false' )
6478 .removeAttr( 'aria-selected' )
6479 .prepend( this.radio
.$element
);
6484 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
6486 /* Static Properties */
6488 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
6490 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
6492 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
6494 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
6501 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
6502 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
6504 this.radio
.setSelected( state
);
6506 .attr( 'aria-checked', state
.toString() )
6507 .removeAttr( 'aria-selected' );
6515 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
6516 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
6518 this.radio
.setDisabled( this.isDisabled() );
6524 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
6525 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
6526 * an interface for adding, removing and selecting options.
6527 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6529 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6530 * OO.ui.RadioSelectInputWidget instead.
6533 * // A RadioSelectWidget with RadioOptions.
6534 * var option1 = new OO.ui.RadioOptionWidget( {
6536 * label: 'Selected radio option'
6539 * var option2 = new OO.ui.RadioOptionWidget( {
6541 * label: 'Unselected radio option'
6544 * var radioSelect=new OO.ui.RadioSelectWidget( {
6545 * items: [ option1, option2 ]
6548 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
6549 * radioSelect.selectItem( option1 );
6551 * $( 'body' ).append( radioSelect.$element );
6553 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6557 * @extends OO.ui.SelectWidget
6558 * @mixins OO.ui.mixin.TabIndexedElement
6561 * @param {Object} [config] Configuration options
6563 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
6564 // Parent constructor
6565 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
6567 // Mixin constructors
6568 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
6572 focus
: this.bindKeyDownListener
.bind( this ),
6573 blur
: this.unbindKeyDownListener
.bind( this )
6578 .addClass( 'oo-ui-radioSelectWidget' )
6579 .attr( 'role', 'radiogroup' );
6584 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
6585 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6588 * MultioptionWidgets are special elements that can be selected and configured with data. The
6589 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
6590 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6591 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
6593 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Multioptions
6596 * @extends OO.ui.Widget
6597 * @mixins OO.ui.mixin.ItemWidget
6598 * @mixins OO.ui.mixin.LabelElement
6601 * @param {Object} [config] Configuration options
6602 * @cfg {boolean} [selected=false] Whether the option is initially selected
6604 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
6605 // Configuration initialization
6606 config
= config
|| {};
6608 // Parent constructor
6609 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
6611 // Mixin constructors
6612 OO
.ui
.mixin
.ItemWidget
.call( this );
6613 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6616 this.selected
= null;
6620 .addClass( 'oo-ui-multioptionWidget' )
6621 .append( this.$label
);
6622 this.setSelected( config
.selected
);
6627 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
6628 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6629 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
6636 * A change event is emitted when the selected state of the option changes.
6638 * @param {boolean} selected Whether the option is now selected
6644 * Check if the option is selected.
6646 * @return {boolean} Item is selected
6648 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
6649 return this.selected
;
6653 * Set the option’s selected state. In general, all modifications to the selection
6654 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6655 * method instead of this method.
6657 * @param {boolean} [state=false] Select option
6660 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
6662 if ( this.selected
!== state
) {
6663 this.selected
= state
;
6664 this.emit( 'change', state
);
6665 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
6671 * MultiselectWidget allows selecting multiple options from a list.
6673 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
6675 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6679 * @extends OO.ui.Widget
6680 * @mixins OO.ui.mixin.GroupWidget
6683 * @param {Object} [config] Configuration options
6684 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
6686 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
6687 // Parent constructor
6688 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
6690 // Configuration initialization
6691 config
= config
|| {};
6693 // Mixin constructors
6694 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
6697 this.aggregate( { change
: 'select' } );
6698 // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
6699 // by GroupElement only when items are added/removed
6700 this.connect( this, { select
: [ 'emit', 'change' ] } );
6703 if ( config
.items
) {
6704 this.addItems( config
.items
);
6706 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
6707 this.$element
.addClass( 'oo-ui-multiselectWidget' )
6708 .append( this.$group
);
6713 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
6714 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
6721 * A change event is emitted when the set of items changes, or an item is selected or deselected.
6727 * A select event is emitted when an item is selected or deselected.
6733 * Get options that are selected.
6735 * @return {OO.ui.MultioptionWidget[]} Selected options
6737 OO
.ui
.MultiselectWidget
.prototype.getSelectedItems = function () {
6738 return this.items
.filter( function ( item
) {
6739 return item
.isSelected();
6744 * Get the data of options that are selected.
6746 * @return {Object[]|string[]} Values of selected options
6748 OO
.ui
.MultiselectWidget
.prototype.getSelectedItemsData = function () {
6749 return this.getSelectedItems().map( function ( item
) {
6755 * Select options by reference. Options not mentioned in the `items` array will be deselected.
6757 * @param {OO.ui.MultioptionWidget[]} items Items to select
6760 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
6761 this.items
.forEach( function ( item
) {
6762 var selected
= items
.indexOf( item
) !== -1;
6763 item
.setSelected( selected
);
6769 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
6771 * @param {Object[]|string[]} datas Values of items to select
6774 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
6777 items
= datas
.map( function ( data
) {
6778 return widget
.getItemFromData( data
);
6780 this.selectItems( items
);
6785 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
6786 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
6787 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6789 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6792 * @extends OO.ui.MultioptionWidget
6795 * @param {Object} [config] Configuration options
6797 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
6798 // Configuration initialization
6799 config
= config
|| {};
6801 // Properties (must be done before parent constructor which calls #setDisabled)
6802 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
6804 // Parent constructor
6805 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
6808 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
6809 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
6813 .addClass( 'oo-ui-checkboxMultioptionWidget' )
6814 .prepend( this.checkbox
.$element
);
6819 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
6821 /* Static Properties */
6823 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
6828 * Handle checkbox selected state change.
6832 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
6833 this.setSelected( this.checkbox
.isSelected() );
6839 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
6840 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
6841 this.checkbox
.setSelected( state
);
6848 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
6849 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
6850 this.checkbox
.setDisabled( this.isDisabled() );
6857 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
6858 this.checkbox
.focus();
6862 * Handle key down events.
6865 * @param {jQuery.Event} e
6867 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
6869 element
= this.getElementGroup(),
6872 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
6873 nextItem
= element
.getRelativeFocusableItem( this, -1 );
6874 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
6875 nextItem
= element
.getRelativeFocusableItem( this, 1 );
6885 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
6886 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
6887 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
6888 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6890 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6891 * OO.ui.CheckboxMultiselectInputWidget instead.
6894 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
6895 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
6898 * label: 'Selected checkbox'
6901 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
6903 * label: 'Unselected checkbox'
6906 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
6907 * items: [ option1, option2 ]
6910 * $( 'body' ).append( multiselect.$element );
6912 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6915 * @extends OO.ui.MultiselectWidget
6918 * @param {Object} [config] Configuration options
6920 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
6921 // Parent constructor
6922 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
6925 this.$lastClicked
= null;
6928 this.$group
.on( 'click', this.onClick
.bind( this ) );
6932 .addClass( 'oo-ui-checkboxMultiselectWidget' );
6937 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
6942 * Get an option by its position relative to the specified item (or to the start of the option array,
6943 * if item is `null`). The direction in which to search through the option array is specified with a
6944 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6945 * `null` if there are no options in the array.
6947 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6948 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6949 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
6951 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
6952 var currentIndex
, nextIndex
, i
,
6953 increase
= direction
> 0 ? 1 : -1,
6954 len
= this.items
.length
;
6957 currentIndex
= this.items
.indexOf( item
);
6958 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
6960 // If no item is selected and moving forward, start at the beginning.
6961 // If moving backward, start at the end.
6962 nextIndex
= direction
> 0 ? 0 : len
- 1;
6965 for ( i
= 0; i
< len
; i
++ ) {
6966 item
= this.items
[ nextIndex
];
6967 if ( item
&& !item
.isDisabled() ) {
6970 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
6976 * Handle click events on checkboxes.
6978 * @param {jQuery.Event} e
6980 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
6981 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
6982 $lastClicked
= this.$lastClicked
,
6983 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
6984 .not( '.oo-ui-widget-disabled' );
6986 // Allow selecting multiple options at once by Shift-clicking them
6987 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
6988 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
6989 lastClickedIndex
= $options
.index( $lastClicked
);
6990 nowClickedIndex
= $options
.index( $nowClicked
);
6991 // If it's the same item, either the user is being silly, or it's a fake event generated by the
6992 // browser. In either case we don't need custom handling.
6993 if ( nowClickedIndex
!== lastClickedIndex
) {
6995 wasSelected
= items
[ nowClickedIndex
].isSelected();
6996 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
6998 // This depends on the DOM order of the items and the order of the .items array being the same.
6999 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
7000 if ( !items
[ i
].isDisabled() ) {
7001 items
[ i
].setSelected( !wasSelected
);
7004 // For the now-clicked element, use immediate timeout to allow the browser to do its own
7005 // handling first, then set our value. The order in which events happen is different for
7006 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
7007 // non-click actions that change the checkboxes.
7009 setTimeout( function () {
7010 if ( !items
[ nowClickedIndex
].isDisabled() ) {
7011 items
[ nowClickedIndex
].setSelected( !wasSelected
);
7017 if ( $nowClicked
.length
) {
7018 this.$lastClicked
= $nowClicked
;
7023 * Element that will stick under a specified container, even when it is inserted elsewhere in the
7024 * document (for example, in a OO.ui.Window's $overlay).
7026 * The elements's position is automatically calculated and maintained when window is resized or the
7027 * page is scrolled. If you reposition the container manually, you have to call #position to make
7028 * sure the element is still placed correctly.
7030 * As positioning is only possible when both the element and the container are attached to the DOM
7031 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
7032 * the #toggle method to display a floating popup, for example.
7038 * @param {Object} [config] Configuration options
7039 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
7040 * @cfg {jQuery} [$floatableContainer] Node to position below
7042 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
7043 // Configuration initialization
7044 config
= config
|| {};
7047 this.$floatable
= null;
7048 this.$floatableContainer
= null;
7049 this.$floatableWindow
= null;
7050 this.$floatableClosestScrollable
= null;
7051 this.onFloatableScrollHandler
= this.position
.bind( this );
7052 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
7055 this.setFloatableContainer( config
.$floatableContainer
);
7056 this.setFloatableElement( config
.$floatable
|| this.$element
);
7062 * Set floatable element.
7064 * If an element is already set, it will be cleaned up before setting up the new element.
7066 * @param {jQuery} $floatable Element to make floatable
7068 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
7069 if ( this.$floatable
) {
7070 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
7071 this.$floatable
.css( { left
: '', top
: '' } );
7074 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
7079 * Set floatable container.
7081 * The element will be always positioned under the specified container.
7083 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
7085 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
7086 this.$floatableContainer
= $floatableContainer
;
7087 if ( this.$floatable
) {
7093 * Toggle positioning.
7095 * Do not turn positioning on until after the element is attached to the DOM and visible.
7097 * @param {boolean} [positioning] Enable positioning, omit to toggle
7100 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
7101 var closestScrollableOfContainer
, closestScrollableOfFloatable
;
7103 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
7105 if ( this.positioning
!== positioning
) {
7106 this.positioning
= positioning
;
7108 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
7109 closestScrollableOfFloatable
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatable
[ 0 ] );
7110 this.needsCustomPosition
= closestScrollableOfContainer
!== closestScrollableOfFloatable
;
7111 // If the scrollable is the root, we have to listen to scroll events
7112 // on the window because of browser inconsistencies.
7113 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
7114 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
7117 if ( positioning
) {
7118 this.$floatableWindow
= $( this.getElementWindow() );
7119 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
7121 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
7122 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
7124 // Initial position after visible
7127 if ( this.$floatableWindow
) {
7128 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
7129 this.$floatableWindow
= null;
7132 if ( this.$floatableClosestScrollable
) {
7133 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
7134 this.$floatableClosestScrollable
= null;
7137 this.$floatable
.css( { left
: '', top
: '' } );
7145 * Check whether the bottom edge of the given element is within the viewport of the given container.
7148 * @param {jQuery} $element
7149 * @param {jQuery} $container
7152 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
7153 var elemRect
, contRect
,
7154 leftEdgeInBounds
= false,
7155 bottomEdgeInBounds
= false,
7156 rightEdgeInBounds
= false;
7158 elemRect
= $element
[ 0 ].getBoundingClientRect();
7159 if ( $container
[ 0 ] === window
) {
7163 right
: document
.documentElement
.clientWidth
,
7164 bottom
: document
.documentElement
.clientHeight
7167 contRect
= $container
[ 0 ].getBoundingClientRect();
7170 // For completeness, if we still cared about topEdgeInBounds, that'd be:
7171 // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
7172 if ( elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
) {
7173 leftEdgeInBounds
= true;
7175 if ( elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
) {
7176 bottomEdgeInBounds
= true;
7178 if ( elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
) {
7179 rightEdgeInBounds
= true;
7182 // We only care that any part of the bottom edge is visible
7183 return bottomEdgeInBounds
&& ( leftEdgeInBounds
|| rightEdgeInBounds
);
7187 * Position the floatable below its container.
7189 * This should only be done when both of them are attached to the DOM and visible.
7193 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
7196 if ( !this.positioning
) {
7200 if ( !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
) ) {
7201 this.$floatable
.addClass( 'oo-ui-floatableElement-hidden' );
7204 this.$floatable
.removeClass( 'oo-ui-floatableElement-hidden' );
7207 if ( !this.needsCustomPosition
) {
7211 pos
= OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, this.$floatable
.offsetParent() );
7213 // Position under container
7214 pos
.top
+= this.$floatableContainer
.height();
7215 this.$floatable
.css( pos
);
7217 // We updated the position, so re-evaluate the clipping state.
7218 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
7219 // will not notice the need to update itself.)
7220 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
7221 // it not listen to the right events in the right places?
7230 * FloatingMenuSelectWidget is a menu that will stick under a specified
7231 * container, even when it is inserted elsewhere in the document (for example,
7232 * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
7233 * menu from being clipped too aggresively.
7235 * The menu's position is automatically calculated and maintained when the menu
7236 * is toggled or the window is resized.
7238 * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
7241 * @extends OO.ui.MenuSelectWidget
7242 * @mixins OO.ui.mixin.FloatableElement
7245 * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
7246 * Deprecated, omit this parameter and specify `$container` instead.
7247 * @param {Object} [config] Configuration options
7248 * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
7250 OO
.ui
.FloatingMenuSelectWidget
= function OoUiFloatingMenuSelectWidget( inputWidget
, config
) {
7251 // Allow 'inputWidget' parameter and config for backwards compatibility
7252 if ( OO
.isPlainObject( inputWidget
) && config
=== undefined ) {
7253 config
= inputWidget
;
7254 inputWidget
= config
.inputWidget
;
7257 // Configuration initialization
7258 config
= config
|| {};
7260 // Parent constructor
7261 OO
.ui
.FloatingMenuSelectWidget
.parent
.call( this, config
);
7263 // Properties (must be set before mixin constructors)
7264 this.inputWidget
= inputWidget
; // For backwards compatibility
7265 this.$container
= config
.$container
|| this.inputWidget
.$element
;
7267 // Mixins constructors
7268 OO
.ui
.mixin
.FloatableElement
.call( this, $.extend( {}, config
, { $floatableContainer
: this.$container
} ) );
7271 this.$element
.addClass( 'oo-ui-floatingMenuSelectWidget' );
7272 // For backwards compatibility
7273 this.$element
.addClass( 'oo-ui-textInputMenuSelectWidget' );
7278 OO
.inheritClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.MenuSelectWidget
);
7279 OO
.mixinClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7281 // For backwards compatibility
7282 OO
.ui
.TextInputMenuSelectWidget
= OO
.ui
.FloatingMenuSelectWidget
;
7289 OO
.ui
.FloatingMenuSelectWidget
.prototype.toggle = function ( visible
) {
7291 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
7292 change
= visible
!== this.isVisible();
7294 if ( change
&& visible
) {
7295 // Make sure the width is set before the parent method runs.
7296 this.setIdealSize( this.$container
.width() );
7300 // This will call this.clip(), which is nonsensical since we're not positioned yet...
7301 OO
.ui
.FloatingMenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7304 this.togglePositioning( this.isVisible() );
7311 * Progress bars visually display the status of an operation, such as a download,
7312 * and can be either determinate or indeterminate:
7314 * - **determinate** process bars show the percent of an operation that is complete.
7316 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
7317 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
7318 * not use percentages.
7320 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
7323 * // Examples of determinate and indeterminate progress bars.
7324 * var progressBar1 = new OO.ui.ProgressBarWidget( {
7327 * var progressBar2 = new OO.ui.ProgressBarWidget();
7329 * // Create a FieldsetLayout to layout progress bars
7330 * var fieldset = new OO.ui.FieldsetLayout;
7331 * fieldset.addItems( [
7332 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
7333 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
7335 * $( 'body' ).append( fieldset.$element );
7338 * @extends OO.ui.Widget
7341 * @param {Object} [config] Configuration options
7342 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
7343 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
7344 * By default, the progress bar is indeterminate.
7346 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
7347 // Configuration initialization
7348 config
= config
|| {};
7350 // Parent constructor
7351 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
7354 this.$bar
= $( '<div>' );
7355 this.progress
= null;
7358 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
7359 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
7362 role
: 'progressbar',
7364 'aria-valuemax': 100
7366 .addClass( 'oo-ui-progressBarWidget' )
7367 .append( this.$bar
);
7372 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
7374 /* Static Properties */
7376 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
7381 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
7383 * @return {number|boolean} Progress percent
7385 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
7386 return this.progress
;
7390 * Set the percent of the process completed or `false` for an indeterminate process.
7392 * @param {number|boolean} progress Progress percent or `false` for indeterminate
7394 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
7395 this.progress
= progress
;
7397 if ( progress
!== false ) {
7398 this.$bar
.css( 'width', this.progress
+ '%' );
7399 this.$element
.attr( 'aria-valuenow', this.progress
);
7401 this.$bar
.css( 'width', '' );
7402 this.$element
.removeAttr( 'aria-valuenow' );
7404 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
7408 * InputWidget is the base class for all input widgets, which
7409 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
7410 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
7411 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
7413 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7417 * @extends OO.ui.Widget
7418 * @mixins OO.ui.mixin.FlaggedElement
7419 * @mixins OO.ui.mixin.TabIndexedElement
7420 * @mixins OO.ui.mixin.TitledElement
7421 * @mixins OO.ui.mixin.AccessKeyedElement
7424 * @param {Object} [config] Configuration options
7425 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
7426 * @cfg {string} [value=''] The value of the input.
7427 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
7428 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
7429 * before it is accepted.
7431 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
7432 // Configuration initialization
7433 config
= config
|| {};
7435 // Parent constructor
7436 OO
.ui
.InputWidget
.parent
.call( this, config
);
7439 // See #reusePreInfuseDOM about config.$input
7440 this.$input
= config
.$input
|| this.getInputElement( config
);
7442 this.inputFilter
= config
.inputFilter
;
7444 // Mixin constructors
7445 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
7446 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
7447 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
7448 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
7451 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
7455 .addClass( 'oo-ui-inputWidget-input' )
7456 .attr( 'name', config
.name
)
7457 .prop( 'disabled', this.isDisabled() );
7459 .addClass( 'oo-ui-inputWidget' )
7460 .append( this.$input
);
7461 this.setValue( config
.value
);
7463 this.setDir( config
.dir
);
7469 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
7470 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
7471 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7472 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
7473 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
7475 /* Static Properties */
7477 OO
.ui
.InputWidget
.static.supportsSimpleLabel
= true;
7479 /* Static Methods */
7484 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
7485 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
7486 // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
7487 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
7494 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7495 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7496 if ( config
.$input
&& config
.$input
.length
) {
7497 state
.value
= config
.$input
.val();
7498 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
7499 state
.focus
= config
.$input
.is( ':focus' );
7509 * A change event is emitted when the value of the input changes.
7511 * @param {string} value
7517 * Get input element.
7519 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
7520 * different circumstances. The element must have a `value` property (like form elements).
7523 * @param {Object} config Configuration options
7524 * @return {jQuery} Input element
7526 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
7527 return $( '<input>' );
7531 * Handle potentially value-changing events.
7534 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
7536 OO
.ui
.InputWidget
.prototype.onEdit = function () {
7538 if ( !this.isDisabled() ) {
7539 // Allow the stack to clear so the value will be updated
7540 setTimeout( function () {
7541 widget
.setValue( widget
.$input
.val() );
7547 * Get the value of the input.
7549 * @return {string} Input value
7551 OO
.ui
.InputWidget
.prototype.getValue = function () {
7552 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7553 // it, and we won't know unless they're kind enough to trigger a 'change' event.
7554 var value
= this.$input
.val();
7555 if ( this.value
!== value
) {
7556 this.setValue( value
);
7562 * Set the directionality of the input.
7564 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
7567 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
7568 this.$input
.prop( 'dir', dir
);
7573 * Set the value of the input.
7575 * @param {string} value New value
7579 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
7580 value
= this.cleanUpValue( value
);
7581 // Update the DOM if it has changed. Note that with cleanUpValue, it
7582 // is possible for the DOM value to change without this.value changing.
7583 if ( this.$input
.val() !== value
) {
7584 this.$input
.val( value
);
7586 if ( this.value
!== value
) {
7588 this.emit( 'change', this.value
);
7594 * Clean up incoming value.
7596 * Ensures value is a string, and converts undefined and null to empty string.
7599 * @param {string} value Original value
7600 * @return {string} Cleaned up value
7602 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
7603 if ( value
=== undefined || value
=== null ) {
7605 } else if ( this.inputFilter
) {
7606 return this.inputFilter( String( value
) );
7608 return String( value
);
7613 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
7614 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
7617 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
7618 if ( !this.isDisabled() ) {
7619 if ( this.$input
.is( ':checkbox, :radio' ) ) {
7620 this.$input
.click();
7622 if ( this.$input
.is( ':input' ) ) {
7623 this.$input
[ 0 ].focus();
7631 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
7632 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
7633 if ( this.$input
) {
7634 this.$input
.prop( 'disabled', this.isDisabled() );
7644 OO
.ui
.InputWidget
.prototype.focus = function () {
7645 this.$input
[ 0 ].focus();
7654 OO
.ui
.InputWidget
.prototype.blur = function () {
7655 this.$input
[ 0 ].blur();
7662 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
7663 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7664 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
7665 this.setValue( state
.value
);
7667 if ( state
.focus
) {
7673 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
7674 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
7675 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
7676 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
7677 * [OOjs UI documentation on MediaWiki] [1] for more information.
7680 * // A ButtonInputWidget rendered as an HTML button, the default.
7681 * var button = new OO.ui.ButtonInputWidget( {
7682 * label: 'Input button',
7686 * $( 'body' ).append( button.$element );
7688 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
7691 * @extends OO.ui.InputWidget
7692 * @mixins OO.ui.mixin.ButtonElement
7693 * @mixins OO.ui.mixin.IconElement
7694 * @mixins OO.ui.mixin.IndicatorElement
7695 * @mixins OO.ui.mixin.LabelElement
7696 * @mixins OO.ui.mixin.TitledElement
7699 * @param {Object} [config] Configuration options
7700 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
7701 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
7702 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
7703 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
7704 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
7706 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
7707 // Configuration initialization
7708 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
7710 // See InputWidget#reusePreInfuseDOM about config.$input
7711 if ( config
.$input
) {
7712 config
.$input
.empty();
7715 // Properties (must be set before parent constructor, which calls #setValue)
7716 this.useInputTag
= config
.useInputTag
;
7718 // Parent constructor
7719 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
7721 // Mixin constructors
7722 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
7723 OO
.ui
.mixin
.IconElement
.call( this, config
);
7724 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7725 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7726 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
7729 if ( !config
.useInputTag
) {
7730 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
7732 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
7737 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
7738 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
7739 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
7740 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
7741 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
7742 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
7744 /* Static Properties */
7747 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
7748 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
7750 OO
.ui
.ButtonInputWidget
.static.supportsSimpleLabel
= false;
7758 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
7760 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
7761 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
7767 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
7769 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
7770 * text, or `null` for no label
7773 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
7774 if ( typeof label
=== 'function' ) {
7775 label
= OO
.ui
.resolveMsg( label
);
7778 if ( this.useInputTag
) {
7779 // Discard non-plaintext labels
7780 if ( typeof label
!== 'string' ) {
7784 this.$input
.val( label
);
7787 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
7791 * Set the value of the input.
7793 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
7794 * they do not support {@link #value values}.
7796 * @param {string} value New value
7799 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
7800 if ( !this.useInputTag
) {
7801 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
7807 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
7808 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
7809 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
7810 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
7812 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7815 * // An example of selected, unselected, and disabled checkbox inputs
7816 * var checkbox1=new OO.ui.CheckboxInputWidget( {
7820 * var checkbox2=new OO.ui.CheckboxInputWidget( {
7823 * var checkbox3=new OO.ui.CheckboxInputWidget( {
7827 * // Create a fieldset layout with fields for each checkbox.
7828 * var fieldset = new OO.ui.FieldsetLayout( {
7829 * label: 'Checkboxes'
7831 * fieldset.addItems( [
7832 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
7833 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
7834 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
7836 * $( 'body' ).append( fieldset.$element );
7838 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7841 * @extends OO.ui.InputWidget
7844 * @param {Object} [config] Configuration options
7845 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
7847 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
7848 // Configuration initialization
7849 config
= config
|| {};
7851 // Parent constructor
7852 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
7856 .addClass( 'oo-ui-checkboxInputWidget' )
7857 // Required for pretty styling in MediaWiki theme
7858 .append( $( '<span>' ) );
7859 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
7864 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
7866 /* Static Methods */
7871 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7872 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7873 state
.checked
= config
.$input
.prop( 'checked' );
7883 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
7884 return $( '<input>' ).attr( 'type', 'checkbox' );
7890 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
7892 if ( !this.isDisabled() ) {
7893 // Allow the stack to clear so the value will be updated
7894 setTimeout( function () {
7895 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
7901 * Set selection state of this checkbox.
7903 * @param {boolean} state `true` for selected
7906 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
7908 if ( this.selected
!== state
) {
7909 this.selected
= state
;
7910 this.$input
.prop( 'checked', this.selected
);
7911 this.emit( 'change', this.selected
);
7917 * Check if this checkbox is selected.
7919 * @return {boolean} Checkbox is selected
7921 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
7922 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7923 // it, and we won't know unless they're kind enough to trigger a 'change' event.
7924 var selected
= this.$input
.prop( 'checked' );
7925 if ( this.selected
!== selected
) {
7926 this.setSelected( selected
);
7928 return this.selected
;
7934 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
7935 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7936 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
7937 this.setSelected( state
.checked
);
7942 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
7943 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7944 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7945 * more information about input widgets.
7947 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
7948 * are no options. If no `value` configuration option is provided, the first option is selected.
7949 * If you need a state representing no value (no option being selected), use a DropdownWidget.
7951 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
7954 * // Example: A DropdownInputWidget with three options
7955 * var dropdownInput = new OO.ui.DropdownInputWidget( {
7957 * { data: 'a', label: 'First' },
7958 * { data: 'b', label: 'Second'},
7959 * { data: 'c', label: 'Third' }
7962 * $( 'body' ).append( dropdownInput.$element );
7964 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7967 * @extends OO.ui.InputWidget
7968 * @mixins OO.ui.mixin.TitledElement
7971 * @param {Object} [config] Configuration options
7972 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7973 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
7975 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
7976 // Configuration initialization
7977 config
= config
|| {};
7979 // See InputWidget#reusePreInfuseDOM about config.$input
7980 if ( config
.$input
) {
7981 config
.$input
.addClass( 'oo-ui-element-hidden' );
7984 // Properties (must be done before parent constructor which calls #setDisabled)
7985 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
7987 // Parent constructor
7988 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
7990 // Mixin constructors
7991 OO
.ui
.mixin
.TitledElement
.call( this, config
);
7994 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
7997 this.setOptions( config
.options
|| [] );
7999 .addClass( 'oo-ui-dropdownInputWidget' )
8000 .append( this.dropdownWidget
.$element
);
8005 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
8006 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
8014 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
8015 return $( '<input>' ).attr( 'type', 'hidden' );
8019 * Handles menu select events.
8022 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8024 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
8025 this.setValue( item
.getData() );
8031 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
8032 value
= this.cleanUpValue( value
);
8033 this.dropdownWidget
.getMenu().selectItemByData( value
);
8034 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
8041 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
8042 this.dropdownWidget
.setDisabled( state
);
8043 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8048 * Set the options available for this input.
8050 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8053 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
8055 value
= this.getValue(),
8058 // Rebuild the dropdown menu
8059 this.dropdownWidget
.getMenu()
8061 .addItems( options
.map( function ( opt
) {
8062 var optValue
= widget
.cleanUpValue( opt
.data
);
8063 return new OO
.ui
.MenuOptionWidget( {
8065 label
: opt
.label
!== undefined ? opt
.label
: optValue
8069 // Restore the previous value, or reset to something sensible
8070 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
8071 // Previous value is still available, ensure consistency with the dropdown
8072 this.setValue( value
);
8074 // No longer valid, reset
8075 if ( options
.length
) {
8076 this.setValue( options
[ 0 ].data
);
8086 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
8087 this.dropdownWidget
.getMenu().toggle( true );
8094 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
8095 this.dropdownWidget
.getMenu().toggle( false );
8100 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
8101 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
8102 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
8103 * please see the [OOjs UI documentation on MediaWiki][1].
8105 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8108 * // An example of selected, unselected, and disabled radio inputs
8109 * var radio1 = new OO.ui.RadioInputWidget( {
8113 * var radio2 = new OO.ui.RadioInputWidget( {
8116 * var radio3 = new OO.ui.RadioInputWidget( {
8120 * // Create a fieldset layout with fields for each radio button.
8121 * var fieldset = new OO.ui.FieldsetLayout( {
8122 * label: 'Radio inputs'
8124 * fieldset.addItems( [
8125 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
8126 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
8127 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
8129 * $( 'body' ).append( fieldset.$element );
8131 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8134 * @extends OO.ui.InputWidget
8137 * @param {Object} [config] Configuration options
8138 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
8140 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
8141 // Configuration initialization
8142 config
= config
|| {};
8144 // Parent constructor
8145 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
8149 .addClass( 'oo-ui-radioInputWidget' )
8150 // Required for pretty styling in MediaWiki theme
8151 .append( $( '<span>' ) );
8152 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
8157 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
8159 /* Static Methods */
8164 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8165 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8166 state
.checked
= config
.$input
.prop( 'checked' );
8176 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
8177 return $( '<input>' ).attr( 'type', 'radio' );
8183 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
8184 // RadioInputWidget doesn't track its state.
8188 * Set selection state of this radio button.
8190 * @param {boolean} state `true` for selected
8193 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
8194 // RadioInputWidget doesn't track its state.
8195 this.$input
.prop( 'checked', state
);
8200 * Check if this radio button is selected.
8202 * @return {boolean} Radio is selected
8204 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
8205 return this.$input
.prop( 'checked' );
8211 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8212 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8213 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
8214 this.setSelected( state
.checked
);
8219 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
8220 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8221 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8222 * more information about input widgets.
8224 * This and OO.ui.DropdownInputWidget support the same configuration options.
8227 * // Example: A RadioSelectInputWidget with three options
8228 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
8230 * { data: 'a', label: 'First' },
8231 * { data: 'b', label: 'Second'},
8232 * { data: 'c', label: 'Third' }
8235 * $( 'body' ).append( radioSelectInput.$element );
8237 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8240 * @extends OO.ui.InputWidget
8243 * @param {Object} [config] Configuration options
8244 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8246 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
8247 // Configuration initialization
8248 config
= config
|| {};
8250 // Properties (must be done before parent constructor which calls #setDisabled)
8251 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
8253 // Parent constructor
8254 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
8257 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
8260 this.setOptions( config
.options
|| [] );
8262 .addClass( 'oo-ui-radioSelectInputWidget' )
8263 .append( this.radioSelectWidget
.$element
);
8268 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
8270 /* Static Properties */
8272 OO
.ui
.RadioSelectInputWidget
.static.supportsSimpleLabel
= false;
8274 /* Static Methods */
8279 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8280 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8281 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
8288 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8289 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8290 // Cannot reuse the `<input type=radio>` set
8291 delete config
.$input
;
8301 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
8302 return $( '<input>' ).attr( 'type', 'hidden' );
8306 * Handles menu select events.
8309 * @param {OO.ui.RadioOptionWidget} item Selected menu item
8311 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
8312 this.setValue( item
.getData() );
8318 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
8319 value
= this.cleanUpValue( value
);
8320 this.radioSelectWidget
.selectItemByData( value
);
8321 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
8328 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
8329 this.radioSelectWidget
.setDisabled( state
);
8330 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8335 * Set the options available for this input.
8337 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8340 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
8342 value
= this.getValue(),
8345 // Rebuild the radioSelect menu
8346 this.radioSelectWidget
8348 .addItems( options
.map( function ( opt
) {
8349 var optValue
= widget
.cleanUpValue( opt
.data
);
8350 return new OO
.ui
.RadioOptionWidget( {
8352 label
: opt
.label
!== undefined ? opt
.label
: optValue
8356 // Restore the previous value, or reset to something sensible
8357 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
8358 // Previous value is still available, ensure consistency with the radioSelect
8359 this.setValue( value
);
8361 // No longer valid, reset
8362 if ( options
.length
) {
8363 this.setValue( options
[ 0 ].data
);
8371 * CheckboxMultiselectInputWidget is a
8372 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
8373 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
8374 * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on MediaWiki][1] for
8375 * more information about input widgets.
8378 * // Example: A CheckboxMultiselectInputWidget with three options
8379 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
8381 * { data: 'a', label: 'First' },
8382 * { data: 'b', label: 'Second'},
8383 * { data: 'c', label: 'Third' }
8386 * $( 'body' ).append( multiselectInput.$element );
8388 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8391 * @extends OO.ui.InputWidget
8394 * @param {Object} [config] Configuration options
8395 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8397 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
8398 // Configuration initialization
8399 config
= config
|| {};
8401 // Properties (must be done before parent constructor which calls #setDisabled)
8402 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
8404 // Parent constructor
8405 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
8408 this.inputName
= config
.name
;
8412 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
8413 .append( this.checkboxMultiselectWidget
.$element
);
8414 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
8415 this.$input
.detach();
8416 this.setOptions( config
.options
|| [] );
8417 // Have to repeat this from parent, as we need options to be set up for this to make sense
8418 this.setValue( config
.value
);
8423 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
8425 /* Static Properties */
8427 OO
.ui
.CheckboxMultiselectInputWidget
.static.supportsSimpleLabel
= false;
8429 /* Static Methods */
8434 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8435 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8436 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
8437 .toArray().map( function ( el
) { return el
.value
; } );
8444 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8445 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8446 // Cannot reuse the `<input type=checkbox>` set
8447 delete config
.$input
;
8457 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
8459 return $( '<div>' );
8465 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
8466 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
8467 .toArray().map( function ( el
) { return el
.value
; } );
8468 if ( this.value
!== value
) {
8469 this.setValue( value
);
8477 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
8478 value
= this.cleanUpValue( value
);
8479 this.checkboxMultiselectWidget
.selectItemsByData( value
);
8480 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
8485 * Clean up incoming value.
8487 * @param {string[]} value Original value
8488 * @return {string[]} Cleaned up value
8490 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
8493 if ( !Array
.isArray( value
) ) {
8496 for ( i
= 0; i
< value
.length
; i
++ ) {
8498 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
8499 // Remove options that we don't have here
8500 if ( !this.checkboxMultiselectWidget
.getItemFromData( singleValue
) ) {
8503 cleanValue
.push( singleValue
);
8511 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
8512 this.checkboxMultiselectWidget
.setDisabled( state
);
8513 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8518 * Set the options available for this input.
8520 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8523 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
8526 // Rebuild the checkboxMultiselectWidget menu
8527 this.checkboxMultiselectWidget
8529 .addItems( options
.map( function ( opt
) {
8532 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
8533 item
= new OO
.ui
.CheckboxMultioptionWidget( {
8535 label
: opt
.label
!== undefined ? opt
.label
: optValue
8537 // Set the 'name' and 'value' for form submission
8538 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
8539 item
.checkbox
.setValue( optValue
);
8543 // Re-set the value, checking the checkboxes as needed.
8544 // This will also get rid of any stale options that we just removed.
8545 this.setValue( this.getValue() );
8551 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
8552 * size of the field as well as its presentation. In addition, these widgets can be configured
8553 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
8554 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
8555 * which modifies incoming values rather than validating them.
8556 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8558 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8561 * // Example of a text input widget
8562 * var textInput = new OO.ui.TextInputWidget( {
8563 * value: 'Text input'
8565 * $( 'body' ).append( textInput.$element );
8567 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8570 * @extends OO.ui.InputWidget
8571 * @mixins OO.ui.mixin.IconElement
8572 * @mixins OO.ui.mixin.IndicatorElement
8573 * @mixins OO.ui.mixin.PendingElement
8574 * @mixins OO.ui.mixin.LabelElement
8577 * @param {Object} [config] Configuration options
8578 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
8579 * 'email', 'url', 'date', 'month' or 'number'. Ignored if `multiline` is true.
8581 * Some values of `type` result in additional behaviors:
8583 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
8584 * empties the text field
8585 * @cfg {string} [placeholder] Placeholder text
8586 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
8587 * instruct the browser to focus this widget.
8588 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
8589 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
8590 * @cfg {boolean} [multiline=false] Allow multiple lines of text
8591 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
8592 * specifies minimum number of rows to display.
8593 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
8594 * Use the #maxRows config to specify a maximum number of displayed rows.
8595 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
8596 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
8597 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
8598 * the value or placeholder text: `'before'` or `'after'`
8599 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
8600 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
8601 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
8602 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
8603 * (the value must contain only numbers); when RegExp, a regular expression that must match the
8604 * value for it to be considered valid; when Function, a function receiving the value as parameter
8605 * that must return true, or promise resolving to true, for it to be considered valid.
8607 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
8608 // Configuration initialization
8609 config
= $.extend( {
8611 labelPosition
: 'after'
8614 if ( config
.type
=== 'search' ) {
8615 OO
.ui
.warnDeprecation( 'TextInputWidget: config.type=\'search\' is deprecated. Use the SearchInputWidget instead. See T148471 for details.' );
8616 if ( config
.icon
=== undefined ) {
8617 config
.icon
= 'search';
8619 // indicator: 'clear' is set dynamically later, depending on value
8622 // Parent constructor
8623 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
8625 // Mixin constructors
8626 OO
.ui
.mixin
.IconElement
.call( this, config
);
8627 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8628 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
8629 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8632 this.type
= this.getSaneType( config
);
8633 this.readOnly
= false;
8634 this.required
= false;
8635 this.multiline
= !!config
.multiline
;
8636 this.autosize
= !!config
.autosize
;
8637 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
8638 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
8639 this.validate
= null;
8640 this.styleHeight
= null;
8641 this.scrollWidth
= null;
8643 // Clone for resizing
8644 if ( this.autosize
) {
8645 this.$clone
= this.$input
8647 .insertAfter( this.$input
)
8648 .attr( 'aria-hidden', 'true' )
8649 .addClass( 'oo-ui-element-hidden' );
8652 this.setValidation( config
.validate
);
8653 this.setLabelPosition( config
.labelPosition
);
8657 keypress
: this.onKeyPress
.bind( this ),
8658 blur
: this.onBlur
.bind( this ),
8659 focus
: this.onFocus
.bind( this )
8662 focus
: this.onElementAttach
.bind( this )
8664 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
8665 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
8666 this.on( 'labelChange', this.updatePosition
.bind( this ) );
8667 this.connect( this, {
8669 disable
: 'onDisable'
8671 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
8675 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
8676 .append( this.$icon
, this.$indicator
);
8677 this.setReadOnly( !!config
.readOnly
);
8678 this.setRequired( !!config
.required
);
8679 this.updateSearchIndicator();
8680 if ( config
.placeholder
!== undefined ) {
8681 this.$input
.attr( 'placeholder', config
.placeholder
);
8683 if ( config
.maxLength
!== undefined ) {
8684 this.$input
.attr( 'maxlength', config
.maxLength
);
8686 if ( config
.autofocus
) {
8687 this.$input
.attr( 'autofocus', 'autofocus' );
8689 if ( config
.autocomplete
=== false ) {
8690 this.$input
.attr( 'autocomplete', 'off' );
8691 // Turning off autocompletion also disables "form caching" when the user navigates to a
8692 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
8694 beforeunload: function () {
8695 this.$input
.removeAttr( 'autocomplete' );
8697 pageshow: function () {
8698 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
8699 // whole page... it shouldn't hurt, though.
8700 this.$input
.attr( 'autocomplete', 'off' );
8704 if ( this.multiline
&& config
.rows
) {
8705 this.$input
.attr( 'rows', config
.rows
);
8707 if ( this.label
|| config
.autosize
) {
8708 this.installParentChangeDetector();
8714 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
8715 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
8716 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
8717 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
8718 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
8720 /* Static Properties */
8722 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
8727 /* Static Methods */
8732 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8733 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8734 if ( config
.multiline
) {
8735 state
.scrollTop
= config
.$input
.scrollTop();
8743 * An `enter` event is emitted when the user presses 'enter' inside the text box.
8745 * Not emitted if the input is multiline.
8751 * A `resize` event is emitted when autosize is set and the widget resizes
8759 * Handle icon mouse down events.
8762 * @param {jQuery.Event} e Mouse down event
8764 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
8765 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8766 this.$input
[ 0 ].focus();
8772 * Handle indicator mouse down events.
8775 * @param {jQuery.Event} e Mouse down event
8777 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
8778 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8779 if ( this.type
=== 'search' ) {
8780 // Clear the text field
8781 this.setValue( '' );
8783 this.$input
[ 0 ].focus();
8789 * Handle key press events.
8792 * @param {jQuery.Event} e Key press event
8793 * @fires enter If enter key is pressed and input is not multiline
8795 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
8796 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
8797 this.emit( 'enter', e
);
8802 * Handle blur events.
8805 * @param {jQuery.Event} e Blur event
8807 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
8808 this.setValidityFlag();
8812 * Handle focus events.
8815 * @param {jQuery.Event} e Focus event
8817 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
8818 this.setValidityFlag( true );
8822 * Handle element attach events.
8825 * @param {jQuery.Event} e Element attach event
8827 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
8828 // Any previously calculated size is now probably invalid if we reattached elsewhere
8829 this.valCache
= null;
8831 this.positionLabel();
8835 * Handle change events.
8837 * @param {string} value
8840 OO
.ui
.TextInputWidget
.prototype.onChange = function () {
8841 this.updateSearchIndicator();
8846 * Handle debounced change events.
8848 * @param {string} value
8851 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
8852 this.setValidityFlag();
8856 * Handle disable events.
8858 * @param {boolean} disabled Element is disabled
8861 OO
.ui
.TextInputWidget
.prototype.onDisable = function () {
8862 this.updateSearchIndicator();
8866 * Check if the input is {@link #readOnly read-only}.
8870 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
8871 return this.readOnly
;
8875 * Set the {@link #readOnly read-only} state of the input.
8877 * @param {boolean} state Make input read-only
8880 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
8881 this.readOnly
= !!state
;
8882 this.$input
.prop( 'readOnly', this.readOnly
);
8883 this.updateSearchIndicator();
8888 * Check if the input is {@link #required required}.
8892 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
8893 return this.required
;
8897 * Set the {@link #required required} state of the input.
8899 * @param {boolean} state Make input required
8902 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
8903 this.required
= !!state
;
8904 if ( this.required
) {
8906 .attr( 'required', 'required' )
8907 .attr( 'aria-required', 'true' );
8908 if ( this.getIndicator() === null ) {
8909 this.setIndicator( 'required' );
8913 .removeAttr( 'required' )
8914 .removeAttr( 'aria-required' );
8915 if ( this.getIndicator() === 'required' ) {
8916 this.setIndicator( null );
8919 this.updateSearchIndicator();
8924 * Support function for making #onElementAttach work across browsers.
8926 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
8927 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
8929 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
8930 * first time that the element gets attached to the documented.
8932 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
8933 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
8934 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
8937 if ( MutationObserver
) {
8938 // The new way. If only it wasn't so ugly.
8940 if ( this.$element
.closest( 'html' ).length
) {
8941 // Widget is attached already, do nothing. This breaks the functionality of this function when
8942 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
8943 // would require observation of the whole document, which would hurt performance of other,
8944 // more important code.
8948 // Find topmost node in the tree
8949 topmostNode
= this.$element
[ 0 ];
8950 while ( topmostNode
.parentNode
) {
8951 topmostNode
= topmostNode
.parentNode
;
8954 // We have no way to detect the $element being attached somewhere without observing the entire
8955 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
8956 // parent node of $element, and instead detect when $element is removed from it (and thus
8957 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
8958 // doesn't get attached, we end up back here and create the parent.
8960 mutationObserver
= new MutationObserver( function ( mutations
) {
8961 var i
, j
, removedNodes
;
8962 for ( i
= 0; i
< mutations
.length
; i
++ ) {
8963 removedNodes
= mutations
[ i
].removedNodes
;
8964 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
8965 if ( removedNodes
[ j
] === topmostNode
) {
8966 setTimeout( onRemove
, 0 );
8973 onRemove = function () {
8974 // If the node was attached somewhere else, report it
8975 if ( widget
.$element
.closest( 'html' ).length
) {
8976 widget
.onElementAttach();
8978 mutationObserver
.disconnect();
8979 widget
.installParentChangeDetector();
8982 // Create a fake parent and observe it
8983 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
8984 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
8986 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
8987 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
8988 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
8993 * Automatically adjust the size of the text input.
8995 * This only affects #multiline inputs that are {@link #autosize autosized}.
9000 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
9001 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
9002 idealHeight
, newHeight
, scrollWidth
, property
;
9004 if ( this.multiline
&& this.$input
.val() !== this.valCache
) {
9005 if ( this.autosize
) {
9007 .val( this.$input
.val() )
9008 .attr( 'rows', this.minRows
)
9009 // Set inline height property to 0 to measure scroll height
9010 .css( 'height', 0 );
9012 this.$clone
.removeClass( 'oo-ui-element-hidden' );
9014 this.valCache
= this.$input
.val();
9016 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
9018 // Remove inline height property to measure natural heights
9019 this.$clone
.css( 'height', '' );
9020 innerHeight
= this.$clone
.innerHeight();
9021 outerHeight
= this.$clone
.outerHeight();
9023 // Measure max rows height
9025 .attr( 'rows', this.maxRows
)
9026 .css( 'height', 'auto' )
9028 maxInnerHeight
= this.$clone
.innerHeight();
9030 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
9031 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
9032 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
9033 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
9035 this.$clone
.addClass( 'oo-ui-element-hidden' );
9037 // Only apply inline height when expansion beyond natural height is needed
9038 // Use the difference between the inner and outer height as a buffer
9039 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
9040 if ( newHeight
!== this.styleHeight
) {
9041 this.$input
.css( 'height', newHeight
);
9042 this.styleHeight
= newHeight
;
9043 this.emit( 'resize' );
9046 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
9047 if ( scrollWidth
!== this.scrollWidth
) {
9048 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
9050 this.$label
.css( { right
: '', left
: '' } );
9051 this.$indicator
.css( { right
: '', left
: '' } );
9053 if ( scrollWidth
) {
9054 this.$indicator
.css( property
, scrollWidth
);
9055 if ( this.labelPosition
=== 'after' ) {
9056 this.$label
.css( property
, scrollWidth
);
9060 this.scrollWidth
= scrollWidth
;
9061 this.positionLabel();
9071 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
9072 if ( config
.multiline
) {
9073 return $( '<textarea>' );
9074 } else if ( this.getSaneType( config
) === 'number' ) {
9075 return $( '<input>' )
9076 .attr( 'step', 'any' )
9077 .attr( 'type', 'number' );
9079 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
9084 * Get sanitized value for 'type' for given config.
9086 * @param {Object} config Configuration options
9087 * @return {string|null}
9090 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
9091 var allowedTypes
= [
9101 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
9105 * Check if the input supports multiple lines.
9109 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
9110 return !!this.multiline
;
9114 * Check if the input automatically adjusts its size.
9118 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
9119 return !!this.autosize
;
9123 * Focus the input and select a specified range within the text.
9125 * @param {number} from Select from offset
9126 * @param {number} [to] Select to offset, defaults to from
9129 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
9130 var isBackwards
, start
, end
,
9131 input
= this.$input
[ 0 ];
9135 isBackwards
= to
< from;
9136 start
= isBackwards
? to
: from;
9137 end
= isBackwards
? from : to
;
9142 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
9144 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
9145 // Rather than expensively check if the input is attached every time, just check
9146 // if it was the cause of an error being thrown. If not, rethrow the error.
9147 if ( this.getElementDocument().body
.contains( input
) ) {
9155 * Get an object describing the current selection range in a directional manner
9157 * @return {Object} Object containing 'from' and 'to' offsets
9159 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
9160 var input
= this.$input
[ 0 ],
9161 start
= input
.selectionStart
,
9162 end
= input
.selectionEnd
,
9163 isBackwards
= input
.selectionDirection
=== 'backward';
9166 from: isBackwards
? end
: start
,
9167 to
: isBackwards
? start
: end
9172 * Get the length of the text input value.
9174 * This could differ from the length of #getValue if the
9175 * value gets filtered
9177 * @return {number} Input length
9179 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
9180 return this.$input
[ 0 ].value
.length
;
9184 * Focus the input and select the entire text.
9188 OO
.ui
.TextInputWidget
.prototype.select = function () {
9189 return this.selectRange( 0, this.getInputLength() );
9193 * Focus the input and move the cursor to the start.
9197 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
9198 return this.selectRange( 0 );
9202 * Focus the input and move the cursor to the end.
9206 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
9207 return this.selectRange( this.getInputLength() );
9211 * Insert new content into the input.
9213 * @param {string} content Content to be inserted
9216 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
9218 range
= this.getRange(),
9219 value
= this.getValue();
9221 start
= Math
.min( range
.from, range
.to
);
9222 end
= Math
.max( range
.from, range
.to
);
9224 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
9225 this.selectRange( start
+ content
.length
);
9230 * Insert new content either side of a selection.
9232 * @param {string} pre Content to be inserted before the selection
9233 * @param {string} post Content to be inserted after the selection
9236 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
9238 range
= this.getRange(),
9239 offset
= pre
.length
;
9241 start
= Math
.min( range
.from, range
.to
);
9242 end
= Math
.max( range
.from, range
.to
);
9244 this.selectRange( start
).insertContent( pre
);
9245 this.selectRange( offset
+ end
).insertContent( post
);
9247 this.selectRange( offset
+ start
, offset
+ end
);
9252 * Set the validation pattern.
9254 * The validation pattern is either a regular expression, a function, or the symbolic name of a
9255 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
9256 * value must contain only numbers).
9258 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
9259 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
9261 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
9262 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
9263 this.validate
= validate
;
9265 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
9270 * Sets the 'invalid' flag appropriately.
9272 * @param {boolean} [isValid] Optionally override validation result
9274 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
9276 setFlag = function ( valid
) {
9278 widget
.$input
.attr( 'aria-invalid', 'true' );
9280 widget
.$input
.removeAttr( 'aria-invalid' );
9282 widget
.setFlags( { invalid
: !valid
} );
9285 if ( isValid
!== undefined ) {
9288 this.getValidity().then( function () {
9297 * Get the validity of current value.
9299 * This method returns a promise that resolves if the value is valid and rejects if
9300 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
9302 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
9304 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
9307 function rejectOrResolve( valid
) {
9309 return $.Deferred().resolve().promise();
9311 return $.Deferred().reject().promise();
9315 if ( this.validate
instanceof Function
) {
9316 result
= this.validate( this.getValue() );
9317 if ( result
&& $.isFunction( result
.promise
) ) {
9318 return result
.promise().then( function ( valid
) {
9319 return rejectOrResolve( valid
);
9322 return rejectOrResolve( result
);
9325 return rejectOrResolve( this.getValue().match( this.validate
) );
9330 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
9332 * @param {string} labelPosition Label position, 'before' or 'after'
9335 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
9336 this.labelPosition
= labelPosition
;
9338 // If there is no label and we only change the position, #updatePosition is a no-op,
9339 // but it takes really a lot of work to do nothing.
9340 this.updatePosition();
9346 * Update the position of the inline label.
9348 * This method is called by #setLabelPosition, and can also be called on its own if
9349 * something causes the label to be mispositioned.
9353 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
9354 var after
= this.labelPosition
=== 'after';
9357 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
9358 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
9360 this.valCache
= null;
9361 this.scrollWidth
= null;
9363 this.positionLabel();
9369 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
9370 * already empty or when it's not editable.
9372 OO
.ui
.TextInputWidget
.prototype.updateSearchIndicator = function () {
9373 if ( this.type
=== 'search' ) {
9374 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
9375 this.setIndicator( null );
9377 this.setIndicator( 'clear' );
9383 * Position the label by setting the correct padding on the input.
9388 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
9389 var after
, rtl
, property
;
9392 // Clear old values if present
9394 'padding-right': '',
9399 this.$element
.append( this.$label
);
9401 this.$label
.detach();
9405 after
= this.labelPosition
=== 'after';
9406 rtl
= this.$element
.css( 'direction' ) === 'rtl';
9407 property
= after
=== rtl
? 'padding-left' : 'padding-right';
9409 this.$input
.css( property
, this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 ) );
9417 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9418 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9419 if ( state
.scrollTop
!== undefined ) {
9420 this.$input
.scrollTop( state
.scrollTop
);
9426 * @extends OO.ui.TextInputWidget
9429 * @param {Object} [config] Configuration options
9431 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
9432 config
= $.extend( {
9436 // Set type to text so that TextInputWidget doesn't
9437 // get stuck in an infinite loop.
9438 config
.type
= 'text';
9440 // Parent constructor
9441 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
9444 this.$element
.addClass( 'oo-ui-textInputWidget-type-search' );
9445 this.updateSearchIndicator();
9446 this.connect( this, {
9447 disable
: 'onDisable'
9453 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
9461 OO
.ui
.SearchInputWidget
.prototype.getInputElement = function () {
9462 return $( '<input>' ).attr( 'type', 'search' );
9468 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
9469 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9470 // Clear the text field
9471 this.setValue( '' );
9472 this.$input
[ 0 ].focus();
9478 * Update the 'clear' indicator displayed on type: 'search' text
9479 * fields, hiding it when the field is already empty or when it's not
9482 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
9483 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
9484 this.setIndicator( null );
9486 this.setIndicator( 'clear' );
9493 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
9494 OO
.ui
.SearchInputWidget
.parent
.prototype.onChange
.call( this );
9495 this.updateSearchIndicator();
9499 * Handle disable events.
9501 * @param {boolean} disabled Element is disabled
9504 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
9505 this.updateSearchIndicator();
9511 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
9512 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
9513 this.updateSearchIndicator();
9518 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
9519 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
9520 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
9522 * - by typing a value in the text input field. If the value exactly matches the value of a menu
9523 * option, that option will appear to be selected.
9524 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
9527 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
9529 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
9532 * // Example: A ComboBoxInputWidget.
9533 * var comboBox = new OO.ui.ComboBoxInputWidget( {
9534 * label: 'ComboBoxInputWidget',
9535 * value: 'Option 1',
9538 * new OO.ui.MenuOptionWidget( {
9540 * label: 'Option One'
9542 * new OO.ui.MenuOptionWidget( {
9544 * label: 'Option Two'
9546 * new OO.ui.MenuOptionWidget( {
9548 * label: 'Option Three'
9550 * new OO.ui.MenuOptionWidget( {
9552 * label: 'Option Four'
9554 * new OO.ui.MenuOptionWidget( {
9556 * label: 'Option Five'
9561 * $( 'body' ).append( comboBox.$element );
9563 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
9566 * @extends OO.ui.TextInputWidget
9569 * @param {Object} [config] Configuration options
9570 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9571 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
9572 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
9573 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
9574 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
9576 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
9577 // Configuration initialization
9578 config
= $.extend( {
9582 // Parent constructor
9583 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
9586 this.$overlay
= config
.$overlay
|| this.$element
;
9587 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
9588 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
9590 disabled
: this.disabled
9592 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend(
9596 $container
: this.$element
,
9597 disabled
: this.isDisabled()
9603 this.connect( this, {
9604 change
: 'onInputChange',
9605 enter
: 'onInputEnter'
9607 this.dropdownButton
.connect( this, {
9608 click
: 'onDropdownButtonClick'
9610 this.menu
.connect( this, {
9611 choose
: 'onMenuChoose',
9612 add
: 'onMenuItemsChange',
9613 remove
: 'onMenuItemsChange'
9619 'aria-autocomplete': 'list'
9621 // Do not override options set via config.menu.items
9622 if ( config
.options
!== undefined ) {
9623 this.setOptions( config
.options
);
9625 this.$field
= $( '<div>' )
9626 .addClass( 'oo-ui-comboBoxInputWidget-field' )
9627 .append( this.$input
, this.dropdownButton
.$element
);
9629 .addClass( 'oo-ui-comboBoxInputWidget' )
9630 .append( this.$field
);
9631 this.$overlay
.append( this.menu
.$element
);
9632 this.onMenuItemsChange();
9637 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
9642 * Get the combobox's menu.
9644 * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
9646 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
9651 * Get the combobox's text input widget.
9653 * @return {OO.ui.TextInputWidget} Text input widget
9655 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
9660 * Handle input change events.
9663 * @param {string} value New value
9665 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
9666 var match
= this.menu
.getItemFromData( value
);
9668 this.menu
.selectItem( match
);
9669 if ( this.menu
.getHighlightedItem() ) {
9670 this.menu
.highlightItem( match
);
9673 if ( !this.isDisabled() ) {
9674 this.menu
.toggle( true );
9679 * Handle input enter events.
9683 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
9684 if ( !this.isDisabled() ) {
9685 this.menu
.toggle( false );
9690 * Handle button click events.
9694 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
9696 this.$input
[ 0 ].focus();
9700 * Handle menu choose events.
9703 * @param {OO.ui.OptionWidget} item Chosen item
9705 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
9706 this.setValue( item
.getData() );
9710 * Handle menu item change events.
9714 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
9715 var match
= this.menu
.getItemFromData( this.getValue() );
9716 this.menu
.selectItem( match
);
9717 if ( this.menu
.getHighlightedItem() ) {
9718 this.menu
.highlightItem( match
);
9720 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
9726 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
9728 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
9730 if ( this.dropdownButton
) {
9731 this.dropdownButton
.setDisabled( this.isDisabled() );
9734 this.menu
.setDisabled( this.isDisabled() );
9741 * Set the options available for this input.
9743 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9746 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
9749 .addItems( options
.map( function ( opt
) {
9750 return new OO
.ui
.MenuOptionWidget( {
9752 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
9760 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
9761 * which is a widget that is specified by reference before any optional configuration settings.
9763 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
9765 * - **left**: The label is placed before the field-widget and aligned with the left margin.
9766 * A left-alignment is used for forms with many fields.
9767 * - **right**: The label is placed before the field-widget and aligned to the right margin.
9768 * A right-alignment is used for long but familiar forms which users tab through,
9769 * verifying the current field with a quick glance at the label.
9770 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9771 * that users fill out from top to bottom.
9772 * - **inline**: The label is placed after the field-widget and aligned to the left.
9773 * An inline-alignment is best used with checkboxes or radio buttons.
9775 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
9776 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
9778 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9781 * @extends OO.ui.Layout
9782 * @mixins OO.ui.mixin.LabelElement
9783 * @mixins OO.ui.mixin.TitledElement
9786 * @param {OO.ui.Widget} fieldWidget Field widget
9787 * @param {Object} [config] Configuration options
9788 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
9789 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
9790 * The array may contain strings or OO.ui.HtmlSnippet instances.
9791 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
9792 * The array may contain strings or OO.ui.HtmlSnippet instances.
9793 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
9794 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
9795 * For important messages, you are advised to use `notices`, as they are always shown.
9797 * @throws {Error} An error is thrown if no widget is specified
9799 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
9800 var hasInputWidget
, div
;
9802 // Allow passing positional parameters inside the config object
9803 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
9804 config
= fieldWidget
;
9805 fieldWidget
= config
.fieldWidget
;
9808 // Make sure we have required constructor arguments
9809 if ( fieldWidget
=== undefined ) {
9810 throw new Error( 'Widget not found' );
9813 hasInputWidget
= fieldWidget
.constructor.static.supportsSimpleLabel
;
9815 // Configuration initialization
9816 config
= $.extend( { align
: 'left' }, config
);
9818 // Parent constructor
9819 OO
.ui
.FieldLayout
.parent
.call( this, config
);
9821 // Mixin constructors
9822 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9823 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
9826 this.fieldWidget
= fieldWidget
;
9829 this.$field
= $( '<div>' );
9830 this.$messages
= $( '<ul>' );
9831 this.$body
= $( '<' + ( hasInputWidget
? 'label' : 'div' ) + '>' );
9833 if ( config
.help
) {
9834 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
9835 classes
: [ 'oo-ui-fieldLayout-help' ],
9841 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
9842 div
.html( config
.help
.toString() );
9844 div
.text( config
.help
);
9846 this.popupButtonWidget
.getPopup().$body
.append(
9847 div
.addClass( 'oo-ui-fieldLayout-help-content' )
9849 this.$help
= this.popupButtonWidget
.$element
;
9851 this.$help
= $( [] );
9855 if ( hasInputWidget
) {
9856 this.$label
.on( 'click', this.onLabelClick
.bind( this ) );
9858 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
9862 .addClass( 'oo-ui-fieldLayout' )
9863 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
9864 .append( this.$help
, this.$body
);
9865 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
9866 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
9868 .addClass( 'oo-ui-fieldLayout-field' )
9869 .append( this.fieldWidget
.$element
);
9871 this.setErrors( config
.errors
|| [] );
9872 this.setNotices( config
.notices
|| [] );
9873 this.setAlignment( config
.align
);
9878 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
9879 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
9880 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
9885 * Handle field disable events.
9888 * @param {boolean} value Field is disabled
9890 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
9891 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
9895 * Handle label mouse click events.
9898 * @param {jQuery.Event} e Mouse click event
9900 OO
.ui
.FieldLayout
.prototype.onLabelClick = function () {
9901 this.fieldWidget
.simulateLabelClick();
9906 * Get the widget contained by the field.
9908 * @return {OO.ui.Widget} Field widget
9910 OO
.ui
.FieldLayout
.prototype.getField = function () {
9911 return this.fieldWidget
;
9916 * @param {string} kind 'error' or 'notice'
9917 * @param {string|OO.ui.HtmlSnippet} text
9920 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
9921 var $listItem
, $icon
, message
;
9922 $listItem
= $( '<li>' );
9923 if ( kind
=== 'error' ) {
9924 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
9925 } else if ( kind
=== 'notice' ) {
9926 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
9930 message
= new OO
.ui
.LabelWidget( { label
: text
} );
9932 .append( $icon
, message
.$element
)
9933 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
9938 * Set the field alignment mode.
9941 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
9944 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
9945 if ( value
!== this.align
) {
9946 // Default to 'left'
9947 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
9951 if ( value
=== 'inline' ) {
9952 this.$body
.append( this.$field
, this.$label
);
9954 this.$body
.append( this.$label
, this.$field
);
9956 // Set classes. The following classes can be used here:
9957 // * oo-ui-fieldLayout-align-left
9958 // * oo-ui-fieldLayout-align-right
9959 // * oo-ui-fieldLayout-align-top
9960 // * oo-ui-fieldLayout-align-inline
9962 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
9964 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
9972 * Set the list of error messages.
9974 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
9975 * The array may contain strings or OO.ui.HtmlSnippet instances.
9978 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
9979 this.errors
= errors
.slice();
9980 this.updateMessages();
9985 * Set the list of notice messages.
9987 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
9988 * The array may contain strings or OO.ui.HtmlSnippet instances.
9991 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
9992 this.notices
= notices
.slice();
9993 this.updateMessages();
9998 * Update the rendering of error and notice messages.
10002 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
10004 this.$messages
.empty();
10006 if ( this.errors
.length
|| this.notices
.length
) {
10007 this.$body
.after( this.$messages
);
10009 this.$messages
.remove();
10013 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
10014 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
10016 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
10017 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
10022 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
10023 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
10024 * is required and is specified before any optional configuration settings.
10026 * Labels can be aligned in one of four ways:
10028 * - **left**: The label is placed before the field-widget and aligned with the left margin.
10029 * A left-alignment is used for forms with many fields.
10030 * - **right**: The label is placed before the field-widget and aligned to the right margin.
10031 * A right-alignment is used for long but familiar forms which users tab through,
10032 * verifying the current field with a quick glance at the label.
10033 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
10034 * that users fill out from top to bottom.
10035 * - **inline**: The label is placed after the field-widget and aligned to the left.
10036 * An inline-alignment is best used with checkboxes or radio buttons.
10038 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
10039 * text is specified.
10042 * // Example of an ActionFieldLayout
10043 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
10044 * new OO.ui.TextInputWidget( {
10045 * placeholder: 'Field widget'
10047 * new OO.ui.ButtonWidget( {
10051 * label: 'An ActionFieldLayout. This label is aligned top',
10053 * help: 'This is help text'
10057 * $( 'body' ).append( actionFieldLayout.$element );
10060 * @extends OO.ui.FieldLayout
10063 * @param {OO.ui.Widget} fieldWidget Field widget
10064 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
10065 * @param {Object} config
10067 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
10068 // Allow passing positional parameters inside the config object
10069 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
10070 config
= fieldWidget
;
10071 fieldWidget
= config
.fieldWidget
;
10072 buttonWidget
= config
.buttonWidget
;
10075 // Parent constructor
10076 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
10079 this.buttonWidget
= buttonWidget
;
10080 this.$button
= $( '<div>' );
10081 this.$input
= $( '<div>' );
10085 .addClass( 'oo-ui-actionFieldLayout' );
10087 .addClass( 'oo-ui-actionFieldLayout-button' )
10088 .append( this.buttonWidget
.$element
);
10090 .addClass( 'oo-ui-actionFieldLayout-input' )
10091 .append( this.fieldWidget
.$element
);
10093 .append( this.$input
, this.$button
);
10098 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
10101 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
10102 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
10103 * configured with a label as well. For more information and examples,
10104 * please see the [OOjs UI documentation on MediaWiki][1].
10107 * // Example of a fieldset layout
10108 * var input1 = new OO.ui.TextInputWidget( {
10109 * placeholder: 'A text input field'
10112 * var input2 = new OO.ui.TextInputWidget( {
10113 * placeholder: 'A text input field'
10116 * var fieldset = new OO.ui.FieldsetLayout( {
10117 * label: 'Example of a fieldset layout'
10120 * fieldset.addItems( [
10121 * new OO.ui.FieldLayout( input1, {
10122 * label: 'Field One'
10124 * new OO.ui.FieldLayout( input2, {
10125 * label: 'Field Two'
10128 * $( 'body' ).append( fieldset.$element );
10130 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
10133 * @extends OO.ui.Layout
10134 * @mixins OO.ui.mixin.IconElement
10135 * @mixins OO.ui.mixin.LabelElement
10136 * @mixins OO.ui.mixin.GroupElement
10139 * @param {Object} [config] Configuration options
10140 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
10142 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
10143 // Configuration initialization
10144 config
= config
|| {};
10146 // Parent constructor
10147 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
10149 // Mixin constructors
10150 OO
.ui
.mixin
.IconElement
.call( this, config
);
10151 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: $( '<legend>' ) } ) );
10152 OO
.ui
.mixin
.GroupElement
.call( this, config
);
10154 if ( config
.help
) {
10155 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
10156 classes
: [ 'oo-ui-fieldsetLayout-help' ],
10161 this.popupButtonWidget
.getPopup().$body
.append(
10163 .text( config
.help
)
10164 .addClass( 'oo-ui-fieldsetLayout-help-content' )
10166 this.$help
= this.popupButtonWidget
.$element
;
10168 this.$help
= $( [] );
10172 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
10174 .addClass( 'oo-ui-fieldsetLayout' )
10175 .prepend( this.$label
, this.$help
, this.$icon
, this.$group
);
10176 if ( Array
.isArray( config
.items
) ) {
10177 this.addItems( config
.items
);
10183 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
10184 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
10185 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
10186 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
10188 /* Static Properties */
10190 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
10193 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
10194 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
10195 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
10196 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10198 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
10199 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
10200 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
10201 * some fancier controls. Some controls have both regular and InputWidget variants, for example
10202 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
10203 * often have simplified APIs to match the capabilities of HTML forms.
10204 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
10206 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
10207 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10210 * // Example of a form layout that wraps a fieldset layout
10211 * var input1 = new OO.ui.TextInputWidget( {
10212 * placeholder: 'Username'
10214 * var input2 = new OO.ui.TextInputWidget( {
10215 * placeholder: 'Password',
10218 * var submit = new OO.ui.ButtonInputWidget( {
10222 * var fieldset = new OO.ui.FieldsetLayout( {
10223 * label: 'A form layout'
10225 * fieldset.addItems( [
10226 * new OO.ui.FieldLayout( input1, {
10227 * label: 'Username',
10230 * new OO.ui.FieldLayout( input2, {
10231 * label: 'Password',
10234 * new OO.ui.FieldLayout( submit )
10236 * var form = new OO.ui.FormLayout( {
10237 * items: [ fieldset ],
10238 * action: '/api/formhandler',
10241 * $( 'body' ).append( form.$element );
10244 * @extends OO.ui.Layout
10245 * @mixins OO.ui.mixin.GroupElement
10248 * @param {Object} [config] Configuration options
10249 * @cfg {string} [method] HTML form `method` attribute
10250 * @cfg {string} [action] HTML form `action` attribute
10251 * @cfg {string} [enctype] HTML form `enctype` attribute
10252 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
10254 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
10257 // Configuration initialization
10258 config
= config
|| {};
10260 // Parent constructor
10261 OO
.ui
.FormLayout
.parent
.call( this, config
);
10263 // Mixin constructors
10264 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
10267 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
10269 // Make sure the action is safe
10270 action
= config
.action
;
10271 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
10272 action
= './' + action
;
10277 .addClass( 'oo-ui-formLayout' )
10279 method
: config
.method
,
10281 enctype
: config
.enctype
10283 if ( Array
.isArray( config
.items
) ) {
10284 this.addItems( config
.items
);
10290 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
10291 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
10296 * A 'submit' event is emitted when the form is submitted.
10301 /* Static Properties */
10303 OO
.ui
.FormLayout
.static.tagName
= 'form';
10308 * Handle form submit events.
10311 * @param {jQuery.Event} e Submit event
10314 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
10315 if ( this.emit( 'submit' ) ) {
10321 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
10322 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
10325 * // Example of a panel layout
10326 * var panel = new OO.ui.PanelLayout( {
10330 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
10332 * $( 'body' ).append( panel.$element );
10335 * @extends OO.ui.Layout
10338 * @param {Object} [config] Configuration options
10339 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10340 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10341 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10342 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10344 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
10345 // Configuration initialization
10346 config
= $.extend( {
10353 // Parent constructor
10354 OO
.ui
.PanelLayout
.parent
.call( this, config
);
10357 this.$element
.addClass( 'oo-ui-panelLayout' );
10358 if ( config
.scrollable
) {
10359 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
10361 if ( config
.padded
) {
10362 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
10364 if ( config
.expanded
) {
10365 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
10367 if ( config
.framed
) {
10368 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
10374 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
10379 * Focus the panel layout
10381 * The default implementation just focuses the first focusable element in the panel
10383 OO
.ui
.PanelLayout
.prototype.focus = function () {
10384 OO
.ui
.findFocusable( this.$element
).focus();
10388 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
10389 * items), with small margins between them. Convenient when you need to put a number of block-level
10390 * widgets on a single line next to each other.
10392 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
10395 * // HorizontalLayout with a text input and a label
10396 * var layout = new OO.ui.HorizontalLayout( {
10398 * new OO.ui.LabelWidget( { label: 'Label' } ),
10399 * new OO.ui.TextInputWidget( { value: 'Text' } )
10402 * $( 'body' ).append( layout.$element );
10405 * @extends OO.ui.Layout
10406 * @mixins OO.ui.mixin.GroupElement
10409 * @param {Object} [config] Configuration options
10410 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
10412 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
10413 // Configuration initialization
10414 config
= config
|| {};
10416 // Parent constructor
10417 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
10419 // Mixin constructors
10420 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
10423 this.$element
.addClass( 'oo-ui-horizontalLayout' );
10424 if ( Array
.isArray( config
.items
) ) {
10425 this.addItems( config
.items
);
10431 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
10432 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);