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-02-22T22:33:33Z
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 * Proxy for `node.addEventListener( eventName, handler, true )`.
266 * @param {HTMLElement} node
267 * @param {string} eventName
268 * @param {Function} handler
269 * @deprecated since 0.15.0
271 OO
.ui
.addCaptureEventListener = function ( node
, eventName
, handler
) {
272 node
.addEventListener( eventName
, handler
, true );
276 * Proxy for `node.removeEventListener( eventName, handler, true )`.
278 * @param {HTMLElement} node
279 * @param {string} eventName
280 * @param {Function} handler
281 * @deprecated since 0.15.0
283 OO
.ui
.removeCaptureEventListener = function ( node
, eventName
, handler
) {
284 node
.removeEventListener( eventName
, handler
, true );
288 * Reconstitute a JavaScript object corresponding to a widget created by
289 * the PHP implementation.
291 * This is an alias for `OO.ui.Element.static.infuse()`.
293 * @param {string|HTMLElement|jQuery} idOrNode
294 * A DOM id (if a string) or node for the widget to infuse.
295 * @return {OO.ui.Element}
296 * The `OO.ui.Element` corresponding to this (infusable) document node.
298 OO
.ui
.infuse = function ( idOrNode
) {
299 return OO
.ui
.Element
.static.infuse( idOrNode
);
304 * Message store for the default implementation of OO.ui.msg
306 * Environments that provide a localization system should not use this, but should override
307 * OO.ui.msg altogether.
312 // Tool tip for a button that moves items in a list down one place
313 'ooui-outline-control-move-down': 'Move item down',
314 // Tool tip for a button that moves items in a list up one place
315 'ooui-outline-control-move-up': 'Move item up',
316 // Tool tip for a button that removes items from a list
317 'ooui-outline-control-remove': 'Remove item',
318 // Label for the toolbar group that contains a list of all other available tools
319 'ooui-toolbar-more': 'More',
320 // Label for the fake tool that expands the full list of tools in a toolbar group
321 'ooui-toolgroup-expand': 'More',
322 // Label for the fake tool that collapses the full list of tools in a toolbar group
323 'ooui-toolgroup-collapse': 'Fewer',
324 // Default label for the accept button of a confirmation dialog
325 'ooui-dialog-message-accept': 'OK',
326 // Default label for the reject button of a confirmation dialog
327 'ooui-dialog-message-reject': 'Cancel',
328 // Title for process dialog error description
329 'ooui-dialog-process-error': 'Something went wrong',
330 // Label for process dialog dismiss error button, visible when describing errors
331 'ooui-dialog-process-dismiss': 'Dismiss',
332 // Label for process dialog retry action button, visible when describing only recoverable errors
333 'ooui-dialog-process-retry': 'Try again',
334 // Label for process dialog retry action button, visible when describing only warnings
335 'ooui-dialog-process-continue': 'Continue',
336 // Label for the file selection widget's select file button
337 'ooui-selectfile-button-select': 'Select a file',
338 // Label for the file selection widget if file selection is not supported
339 'ooui-selectfile-not-supported': 'File selection is not supported',
340 // Label for the file selection widget when no file is currently selected
341 'ooui-selectfile-placeholder': 'No file is selected',
342 // Label for the file selection widget's drop target
343 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
347 * Get a localized message.
349 * In environments that provide a localization system, this function should be overridden to
350 * return the message translated in the user's language. The default implementation always returns
353 * After the message key, message parameters may optionally be passed. In the default implementation,
354 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
355 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
356 * they support unnamed, ordered message parameters.
358 * @param {string} key Message key
359 * @param {Mixed...} [params] Message parameters
360 * @return {string} Translated message with parameters substituted
362 OO
.ui
.msg = function ( key
) {
363 var message
= messages
[ key
],
364 params
= Array
.prototype.slice
.call( arguments
, 1 );
365 if ( typeof message
=== 'string' ) {
366 // Perform $1 substitution
367 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
368 var i
= parseInt( n
, 10 );
369 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
372 // Return placeholder if message not found
373 message
= '[' + key
+ ']';
380 * Package a message and arguments for deferred resolution.
382 * Use this when you are statically specifying a message and the message may not yet be present.
384 * @param {string} key Message key
385 * @param {Mixed...} [params] Message parameters
386 * @return {Function} Function that returns the resolved message when executed
388 OO
.ui
.deferMsg = function () {
389 var args
= arguments
;
391 return OO
.ui
.msg
.apply( OO
.ui
, args
);
398 * If the message is a function it will be executed, otherwise it will pass through directly.
400 * @param {Function|string} msg Deferred message, or message text
401 * @return {string} Resolved message
403 OO
.ui
.resolveMsg = function ( msg
) {
404 if ( $.isFunction( msg
) ) {
411 * @param {string} url
414 OO
.ui
.isSafeUrl = function ( url
) {
415 // Keep this function in sync with php/Tag.php
416 var i
, protocolWhitelist
;
418 function stringStartsWith( haystack
, needle
) {
419 return haystack
.substr( 0, needle
.length
) === needle
;
422 protocolWhitelist
= [
423 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
424 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
425 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
432 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
433 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
438 // This matches '//' too
439 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
442 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
454 * Namespace for OOjs UI mixins.
456 * Mixins are named according to the type of object they are intended to
457 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
458 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
459 * is intended to be mixed in to an instance of OO.ui.Widget.
467 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
468 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
469 * connected to them and can't be interacted with.
475 * @param {Object} [config] Configuration options
476 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
477 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
479 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
480 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
481 * @cfg {string} [text] Text to insert
482 * @cfg {Array} [content] An array of content elements to append (after #text).
483 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
484 * Instances of OO.ui.Element will have their $element appended.
485 * @cfg {jQuery} [$content] Content elements to append (after #text).
486 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
487 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
488 * Data can also be specified with the #setData method.
490 OO
.ui
.Element
= function OoUiElement( config
) {
491 // Configuration initialization
492 config
= config
|| {};
497 this.data
= config
.data
;
498 this.$element
= config
.$element
||
499 $( document
.createElement( this.getTagName() ) );
500 this.elementGroup
= null;
501 this.debouncedUpdateThemeClassesHandler
= OO
.ui
.debounce( this.debouncedUpdateThemeClasses
);
504 if ( Array
.isArray( config
.classes
) ) {
505 this.$element
.addClass( config
.classes
.join( ' ' ) );
508 this.$element
.attr( 'id', config
.id
);
511 this.$element
.text( config
.text
);
513 if ( config
.content
) {
514 // The `content` property treats plain strings as text; use an
515 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
516 // appropriate $element appended.
517 this.$element
.append( config
.content
.map( function ( v
) {
518 if ( typeof v
=== 'string' ) {
519 // Escape string so it is properly represented in HTML.
520 return document
.createTextNode( v
);
521 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
524 } else if ( v
instanceof OO
.ui
.Element
) {
530 if ( config
.$content
) {
531 // The `$content` property treats plain strings as HTML.
532 this.$element
.append( config
.$content
);
538 OO
.initClass( OO
.ui
.Element
);
540 /* Static Properties */
543 * The name of the HTML tag used by the element.
545 * The static value may be ignored if the #getTagName method is overridden.
551 OO
.ui
.Element
.static.tagName
= 'div';
556 * Reconstitute a JavaScript object corresponding to a widget created
557 * by the PHP implementation.
559 * @param {string|HTMLElement|jQuery} idOrNode
560 * A DOM id (if a string) or node for the widget to infuse.
561 * @return {OO.ui.Element}
562 * The `OO.ui.Element` corresponding to this (infusable) document node.
563 * For `Tag` objects emitted on the HTML side (used occasionally for content)
564 * the value returned is a newly-created Element wrapping around the existing
567 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
568 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, false );
569 // Verify that the type matches up.
570 // FIXME: uncomment after T89721 is fixed (see T90929)
572 if ( !( obj instanceof this['class'] ) ) {
573 throw new Error( 'Infusion type mismatch!' );
580 * Implementation helper for `infuse`; skips the type check and has an
581 * extra property so that only the top-level invocation touches the DOM.
583 * @param {string|HTMLElement|jQuery} idOrNode
584 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
585 * when the top-level widget of this infusion is inserted into DOM,
586 * replacing the original node; or false for top-level invocation.
587 * @return {OO.ui.Element}
589 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, domPromise
) {
590 // look for a cached result of a previous infusion.
591 var id
, $elem
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
592 if ( typeof idOrNode
=== 'string' ) {
594 $elem
= $( document
.getElementById( id
) );
596 $elem
= $( idOrNode
);
597 id
= $elem
.attr( 'id' );
599 if ( !$elem
.length
) {
600 throw new Error( 'Widget not found: ' + id
);
602 if ( $elem
[ 0 ].oouiInfused
) {
603 $elem
= $elem
[ 0 ].oouiInfused
;
605 data
= $elem
.data( 'ooui-infused' );
608 if ( data
=== true ) {
609 throw new Error( 'Circular dependency! ' + id
);
612 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
613 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
614 // restore dynamic state after the new element is re-inserted into DOM under infused parent
615 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
616 infusedChildren
= $elem
.data( 'ooui-infused-children' );
617 if ( infusedChildren
&& infusedChildren
.length
) {
618 infusedChildren
.forEach( function ( data
) {
619 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
620 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
626 data
= $elem
.attr( 'data-ooui' );
628 throw new Error( 'No infusion data found: ' + id
);
631 data
= $.parseJSON( data
);
635 if ( !( data
&& data
._
) ) {
636 throw new Error( 'No valid infusion data found: ' + id
);
638 if ( data
._
=== 'Tag' ) {
639 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
640 return new OO
.ui
.Element( { $element
: $elem
} );
642 parts
= data
._
.split( '.' );
643 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
644 if ( cls
=== undefined ) {
645 // The PHP output might be old and not including the "OO.ui" prefix
646 // TODO: Remove this back-compat after next major release
647 cls
= OO
.getProp
.apply( OO
, [ OO
.ui
].concat( parts
) );
648 if ( cls
=== undefined ) {
649 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
653 // Verify that we're creating an OO.ui.Element instance
656 while ( parent
!== undefined ) {
657 if ( parent
=== OO
.ui
.Element
) {
662 parent
= parent
.parent
;
665 if ( parent
!== OO
.ui
.Element
) {
666 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
669 if ( domPromise
=== false ) {
671 domPromise
= top
.promise();
673 $elem
.data( 'ooui-infused', true ); // prevent loops
674 data
.id
= id
; // implicit
675 infusedChildren
= [];
676 data
= OO
.copy( data
, null, function deserialize( value
) {
678 if ( OO
.isPlainObject( value
) ) {
680 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, domPromise
);
681 infusedChildren
.push( infused
);
682 // Flatten the structure
683 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
684 infused
.$element
.removeData( 'ooui-infused-children' );
688 return new OO
.ui
.HtmlSnippet( value
.html
);
692 // allow widgets to reuse parts of the DOM
693 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
694 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
695 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
697 // jscs:disable requireCapitalizedConstructors
698 obj
= new cls( data
);
699 // jscs:enable requireCapitalizedConstructors
700 // now replace old DOM with this new DOM.
702 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
703 // so only mutate the DOM if we need to.
704 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
705 $elem
.replaceWith( obj
.$element
);
706 // This element is now gone from the DOM, but if anyone is holding a reference to it,
707 // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
708 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
709 $elem
[ 0 ].oouiInfused
= obj
.$element
;
713 obj
.$element
.data( 'ooui-infused', obj
);
714 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
715 // set the 'data-ooui' attribute so we can identify infused widgets
716 obj
.$element
.attr( 'data-ooui', '' );
717 // restore dynamic state after the new element is inserted into DOM
718 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
723 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
725 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
726 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
727 * constructor, which will be given the enhanced config.
730 * @param {HTMLElement} node
731 * @param {Object} config
734 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
739 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
740 * (and its children) that represent an Element of the same class and the given configuration,
741 * generated by the PHP implementation.
743 * This method is called just before `node` is detached from the DOM. The return value of this
744 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
745 * is inserted into DOM to replace `node`.
748 * @param {HTMLElement} node
749 * @param {Object} config
752 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
757 * Get a jQuery function within a specific document.
760 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
761 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
763 * @return {Function} Bound jQuery function
765 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
766 function wrapper( selector
) {
767 return $( selector
, wrapper
.context
);
770 wrapper
.context
= this.getDocument( context
);
773 wrapper
.$iframe
= $iframe
;
780 * Get the document of an element.
783 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
784 * @return {HTMLDocument|null} Document object
786 OO
.ui
.Element
.static.getDocument = function ( obj
) {
787 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
788 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
789 // Empty jQuery selections might have a context
796 ( obj
.nodeType
=== 9 && obj
) ||
801 * Get the window of an element or document.
804 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
805 * @return {Window} Window object
807 OO
.ui
.Element
.static.getWindow = function ( obj
) {
808 var doc
= this.getDocument( obj
);
809 return doc
.defaultView
;
813 * Get the direction of an element or document.
816 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
817 * @return {string} Text direction, either 'ltr' or 'rtl'
819 OO
.ui
.Element
.static.getDir = function ( obj
) {
822 if ( obj
instanceof jQuery
) {
825 isDoc
= obj
.nodeType
=== 9;
826 isWin
= obj
.document
!== undefined;
827 if ( isDoc
|| isWin
) {
833 return $( obj
).css( 'direction' );
837 * Get the offset between two frames.
839 * TODO: Make this function not use recursion.
842 * @param {Window} from Window of the child frame
843 * @param {Window} [to=window] Window of the parent frame
844 * @param {Object} [offset] Offset to start with, used internally
845 * @return {Object} Offset object, containing left and top properties
847 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
848 var i
, len
, frames
, frame
, rect
;
854 offset
= { top
: 0, left
: 0 };
856 if ( from.parent
=== from ) {
860 // Get iframe element
861 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
862 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
863 if ( frames
[ i
].contentWindow
=== from ) {
869 // Recursively accumulate offset values
871 rect
= frame
.getBoundingClientRect();
872 offset
.left
+= rect
.left
;
873 offset
.top
+= rect
.top
;
875 this.getFrameOffset( from.parent
, offset
);
882 * Get the offset between two elements.
884 * The two elements may be in a different frame, but in that case the frame $element is in must
885 * be contained in the frame $anchor is in.
888 * @param {jQuery} $element Element whose position to get
889 * @param {jQuery} $anchor Element to get $element's position relative to
890 * @return {Object} Translated position coordinates, containing top and left properties
892 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
893 var iframe
, iframePos
,
894 pos
= $element
.offset(),
895 anchorPos
= $anchor
.offset(),
896 elementDocument
= this.getDocument( $element
),
897 anchorDocument
= this.getDocument( $anchor
);
899 // If $element isn't in the same document as $anchor, traverse up
900 while ( elementDocument
!== anchorDocument
) {
901 iframe
= elementDocument
.defaultView
.frameElement
;
903 throw new Error( '$element frame is not contained in $anchor frame' );
905 iframePos
= $( iframe
).offset();
906 pos
.left
+= iframePos
.left
;
907 pos
.top
+= iframePos
.top
;
908 elementDocument
= iframe
.ownerDocument
;
910 pos
.left
-= anchorPos
.left
;
911 pos
.top
-= anchorPos
.top
;
916 * Get element border sizes.
919 * @param {HTMLElement} el Element to measure
920 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
922 OO
.ui
.Element
.static.getBorders = function ( el
) {
923 var doc
= el
.ownerDocument
,
924 win
= doc
.defaultView
,
925 style
= win
.getComputedStyle( el
, null ),
927 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
928 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
929 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
930 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
941 * Get dimensions of an element or window.
944 * @param {HTMLElement|Window} el Element to measure
945 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
947 OO
.ui
.Element
.static.getDimensions = function ( el
) {
949 doc
= el
.ownerDocument
|| el
.document
,
950 win
= doc
.defaultView
;
952 if ( win
=== el
|| el
=== doc
.documentElement
) {
955 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
957 top
: $win
.scrollTop(),
958 left
: $win
.scrollLeft()
960 scrollbar
: { right
: 0, bottom
: 0 },
964 bottom
: $win
.innerHeight(),
965 right
: $win
.innerWidth()
971 borders
: this.getBorders( el
),
973 top
: $el
.scrollTop(),
974 left
: $el
.scrollLeft()
977 right
: $el
.innerWidth() - el
.clientWidth
,
978 bottom
: $el
.innerHeight() - el
.clientHeight
980 rect
: el
.getBoundingClientRect()
986 * Get scrollable object parent
988 * documentElement can't be used to get or set the scrollTop
989 * property on Blink. Changing and testing its value lets us
990 * use 'body' or 'documentElement' based on what is working.
992 * https://code.google.com/p/chromium/issues/detail?id=303131
995 * @param {HTMLElement} el Element to find scrollable parent for
996 * @return {HTMLElement} Scrollable parent
998 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1001 if ( OO
.ui
.scrollableElement
=== undefined ) {
1002 body
= el
.ownerDocument
.body
;
1003 scrollTop
= body
.scrollTop
;
1006 if ( body
.scrollTop
=== 1 ) {
1007 body
.scrollTop
= scrollTop
;
1008 OO
.ui
.scrollableElement
= 'body';
1010 OO
.ui
.scrollableElement
= 'documentElement';
1014 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1018 * Get closest scrollable container.
1020 * Traverses up until either a scrollable element or the root is reached, in which case the window
1024 * @param {HTMLElement} el Element to find scrollable container for
1025 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1026 * @return {HTMLElement} Closest scrollable container
1028 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1030 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1031 props
= [ 'overflow-x', 'overflow-y' ],
1032 $parent
= $( el
).parent();
1034 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1035 props
= [ 'overflow-' + dimension
];
1038 while ( $parent
.length
) {
1039 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1040 return $parent
[ 0 ];
1044 val
= $parent
.css( props
[ i
] );
1045 if ( val
=== 'auto' || val
=== 'scroll' ) {
1046 return $parent
[ 0 ];
1049 $parent
= $parent
.parent();
1051 return this.getDocument( el
).body
;
1055 * Scroll element into view.
1058 * @param {HTMLElement} el Element to scroll into view
1059 * @param {Object} [config] Configuration options
1060 * @param {string} [config.duration='fast'] jQuery animation duration value
1061 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1062 * to scroll in both directions
1063 * @param {Function} [config.complete] Function to call when scrolling completes.
1064 * Deprecated since 0.15.4, use the return promise instead.
1065 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1067 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1068 var position
, animations
, callback
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1069 deferred
= $.Deferred();
1071 // Configuration initialization
1072 config
= config
|| {};
1075 callback
= typeof config
.complete
=== 'function' && config
.complete
;
1076 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1077 $container
= $( container
);
1078 elementDimensions
= this.getDimensions( el
);
1079 containerDimensions
= this.getDimensions( container
);
1080 $window
= $( this.getWindow( el
) );
1082 // Compute the element's position relative to the container
1083 if ( $container
.is( 'html, body' ) ) {
1084 // If the scrollable container is the root, this is easy
1086 top
: elementDimensions
.rect
.top
,
1087 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1088 left
: elementDimensions
.rect
.left
,
1089 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1092 // Otherwise, we have to subtract el's coordinates from container's coordinates
1094 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1095 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1096 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1097 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1101 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1102 if ( position
.top
< 0 ) {
1103 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1104 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1105 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1108 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1109 if ( position
.left
< 0 ) {
1110 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1111 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1112 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1115 if ( !$.isEmptyObject( animations
) ) {
1116 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1117 $container
.queue( function ( next
) {
1130 return deferred
.promise();
1134 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1135 * and reserve space for them, because it probably doesn't.
1137 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1138 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1139 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1140 * and then reattach (or show) them back.
1143 * @param {HTMLElement} el Element to reconsider the scrollbars on
1145 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1146 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1147 // Save scroll position
1148 scrollLeft
= el
.scrollLeft
;
1149 scrollTop
= el
.scrollTop
;
1150 // Detach all children
1151 while ( el
.firstChild
) {
1152 nodes
.push( el
.firstChild
);
1153 el
.removeChild( el
.firstChild
);
1156 void el
.offsetHeight
;
1157 // Reattach all children
1158 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1159 el
.appendChild( nodes
[ i
] );
1161 // Restore scroll position (no-op if scrollbars disappeared)
1162 el
.scrollLeft
= scrollLeft
;
1163 el
.scrollTop
= scrollTop
;
1169 * Toggle visibility of an element.
1171 * @param {boolean} [show] Make element visible, omit to toggle visibility
1175 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1176 show
= show
=== undefined ? !this.visible
: !!show
;
1178 if ( show
!== this.isVisible() ) {
1179 this.visible
= show
;
1180 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1181 this.emit( 'toggle', show
);
1188 * Check if element is visible.
1190 * @return {boolean} element is visible
1192 OO
.ui
.Element
.prototype.isVisible = function () {
1193 return this.visible
;
1199 * @return {Mixed} Element data
1201 OO
.ui
.Element
.prototype.getData = function () {
1208 * @param {Mixed} Element data
1211 OO
.ui
.Element
.prototype.setData = function ( data
) {
1217 * Check if element supports one or more methods.
1219 * @param {string|string[]} methods Method or list of methods to check
1220 * @return {boolean} All methods are supported
1222 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1226 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1227 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1228 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1233 return methods
.length
=== support
;
1237 * Update the theme-provided classes.
1239 * @localdoc This is called in element mixins and widget classes any time state changes.
1240 * Updating is debounced, minimizing overhead of changing multiple attributes and
1241 * guaranteeing that theme updates do not occur within an element's constructor
1243 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1244 this.debouncedUpdateThemeClassesHandler();
1249 * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
1250 * make them synchronous.
1252 OO
.ui
.Element
.prototype.debouncedUpdateThemeClasses = function () {
1253 OO
.ui
.theme
.updateElementClasses( this );
1257 * Get the HTML tag name.
1259 * Override this method to base the result on instance information.
1261 * @return {string} HTML tag name
1263 OO
.ui
.Element
.prototype.getTagName = function () {
1264 return this.constructor.static.tagName
;
1268 * Check if the element is attached to the DOM
1269 * @return {boolean} The element is attached to the DOM
1271 OO
.ui
.Element
.prototype.isElementAttached = function () {
1272 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1276 * Get the DOM document.
1278 * @return {HTMLDocument} Document object
1280 OO
.ui
.Element
.prototype.getElementDocument = function () {
1281 // Don't cache this in other ways either because subclasses could can change this.$element
1282 return OO
.ui
.Element
.static.getDocument( this.$element
);
1286 * Get the DOM window.
1288 * @return {Window} Window object
1290 OO
.ui
.Element
.prototype.getElementWindow = function () {
1291 return OO
.ui
.Element
.static.getWindow( this.$element
);
1295 * Get closest scrollable container.
1297 * @return {HTMLElement} Closest scrollable container
1299 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1300 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1304 * Get group element is in.
1306 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1308 OO
.ui
.Element
.prototype.getElementGroup = function () {
1309 return this.elementGroup
;
1313 * Set group element is in.
1315 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1318 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1319 this.elementGroup
= group
;
1324 * Scroll element into view.
1326 * @param {Object} [config] Configuration options
1327 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1329 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1330 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1334 * Restore the pre-infusion dynamic state for this widget.
1336 * This method is called after #$element has been inserted into DOM. The parameter is the return
1337 * value of #gatherPreInfuseState.
1340 * @param {Object} state
1342 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1346 * Wraps an HTML snippet for use with configuration values which default
1347 * to strings. This bypasses the default html-escaping done to string
1353 * @param {string} [content] HTML content
1355 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1357 this.content
= content
;
1362 OO
.initClass( OO
.ui
.HtmlSnippet
);
1369 * @return {string} Unchanged HTML snippet.
1371 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1372 return this.content
;
1376 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1377 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1378 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1379 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1380 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1384 * @extends OO.ui.Element
1385 * @mixins OO.EventEmitter
1388 * @param {Object} [config] Configuration options
1390 OO
.ui
.Layout
= function OoUiLayout( config
) {
1391 // Configuration initialization
1392 config
= config
|| {};
1394 // Parent constructor
1395 OO
.ui
.Layout
.parent
.call( this, config
);
1397 // Mixin constructors
1398 OO
.EventEmitter
.call( this );
1401 this.$element
.addClass( 'oo-ui-layout' );
1406 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1407 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1410 * Widgets are compositions of one or more OOjs UI elements that users can both view
1411 * and interact with. All widgets can be configured and modified via a standard API,
1412 * and their state can change dynamically according to a model.
1416 * @extends OO.ui.Element
1417 * @mixins OO.EventEmitter
1420 * @param {Object} [config] Configuration options
1421 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1422 * appearance reflects this state.
1424 OO
.ui
.Widget
= function OoUiWidget( config
) {
1425 // Initialize config
1426 config
= $.extend( { disabled
: false }, config
);
1428 // Parent constructor
1429 OO
.ui
.Widget
.parent
.call( this, config
);
1431 // Mixin constructors
1432 OO
.EventEmitter
.call( this );
1435 this.disabled
= null;
1436 this.wasDisabled
= null;
1439 this.$element
.addClass( 'oo-ui-widget' );
1440 this.setDisabled( !!config
.disabled
);
1445 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1446 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1448 /* Static Properties */
1451 * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
1452 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1457 * @property {boolean}
1459 OO
.ui
.Widget
.static.supportsSimpleLabel
= false;
1466 * A 'disable' event is emitted when the disabled state of the widget changes
1467 * (i.e. on disable **and** enable).
1469 * @param {boolean} disabled Widget is disabled
1475 * A 'toggle' event is emitted when the visibility of the widget changes.
1477 * @param {boolean} visible Widget is visible
1483 * Check if the widget is disabled.
1485 * @return {boolean} Widget is disabled
1487 OO
.ui
.Widget
.prototype.isDisabled = function () {
1488 return this.disabled
;
1492 * Set the 'disabled' state of the widget.
1494 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1496 * @param {boolean} disabled Disable widget
1499 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1502 this.disabled
= !!disabled
;
1503 isDisabled
= this.isDisabled();
1504 if ( isDisabled
!== this.wasDisabled
) {
1505 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1506 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1507 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1508 this.emit( 'disable', isDisabled
);
1509 this.updateThemeClasses();
1511 this.wasDisabled
= isDisabled
;
1517 * Update the disabled state, in case of changes in parent widget.
1521 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1522 this.setDisabled( this.disabled
);
1533 * @param {Object} [config] Configuration options
1535 OO
.ui
.Theme
= function OoUiTheme( config
) {
1536 // Configuration initialization
1537 config
= config
|| {};
1542 OO
.initClass( OO
.ui
.Theme
);
1547 * Get a list of classes to be applied to a widget.
1549 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1550 * otherwise state transitions will not work properly.
1552 * @param {OO.ui.Element} element Element for which to get classes
1553 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1555 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1556 return { on
: [], off
: [] };
1560 * Update CSS classes provided by the theme.
1562 * For elements with theme logic hooks, this should be called any time there's a state change.
1564 * @param {OO.ui.Element} element Element for which to update classes
1565 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1567 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1568 var $elements
= $( [] ),
1569 classes
= this.getElementClasses( element
);
1571 if ( element
.$icon
) {
1572 $elements
= $elements
.add( element
.$icon
);
1574 if ( element
.$indicator
) {
1575 $elements
= $elements
.add( element
.$indicator
);
1579 .removeClass( classes
.off
.join( ' ' ) )
1580 .addClass( classes
.on
.join( ' ' ) );
1584 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1585 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1586 * order in which users will navigate through the focusable elements via the "tab" key.
1589 * // TabIndexedElement is mixed into the ButtonWidget class
1590 * // to provide a tabIndex property.
1591 * var button1 = new OO.ui.ButtonWidget( {
1595 * var button2 = new OO.ui.ButtonWidget( {
1599 * var button3 = new OO.ui.ButtonWidget( {
1603 * var button4 = new OO.ui.ButtonWidget( {
1607 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1613 * @param {Object} [config] Configuration options
1614 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1615 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1616 * functionality will be applied to it instead.
1617 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1618 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1619 * to remove the element from the tab-navigation flow.
1621 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1622 // Configuration initialization
1623 config
= $.extend( { tabIndex
: 0 }, config
);
1626 this.$tabIndexed
= null;
1627 this.tabIndex
= null;
1630 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1633 this.setTabIndex( config
.tabIndex
);
1634 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1639 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1644 * Set the element that should use the tabindex functionality.
1646 * This method is used to retarget a tabindex mixin so that its functionality applies
1647 * to the specified element. If an element is currently using the functionality, the mixin’s
1648 * effect on that element is removed before the new element is set up.
1650 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1653 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1654 var tabIndex
= this.tabIndex
;
1655 // Remove attributes from old $tabIndexed
1656 this.setTabIndex( null );
1657 // Force update of new $tabIndexed
1658 this.$tabIndexed
= $tabIndexed
;
1659 this.tabIndex
= tabIndex
;
1660 return this.updateTabIndex();
1664 * Set the value of the tabindex.
1666 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
1669 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1670 tabIndex
= typeof tabIndex
=== 'number' ? tabIndex
: null;
1672 if ( this.tabIndex
!== tabIndex
) {
1673 this.tabIndex
= tabIndex
;
1674 this.updateTabIndex();
1681 * Update the `tabindex` attribute, in case of changes to tab index or
1687 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1688 if ( this.$tabIndexed
) {
1689 if ( this.tabIndex
!== null ) {
1690 // Do not index over disabled elements
1691 this.$tabIndexed
.attr( {
1692 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1693 // Support: ChromeVox and NVDA
1694 // These do not seem to inherit aria-disabled from parent elements
1695 'aria-disabled': this.isDisabled().toString()
1698 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1705 * Handle disable events.
1708 * @param {boolean} disabled Element is disabled
1710 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1711 this.updateTabIndex();
1715 * Get the value of the tabindex.
1717 * @return {number|null} Tabindex value
1719 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
1720 return this.tabIndex
;
1724 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
1725 * interface element that can be configured with access keys for accessibility.
1726 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
1728 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
1733 * @param {Object} [config] Configuration options
1734 * @cfg {jQuery} [$button] The button element created by the class.
1735 * If this configuration is omitted, the button element will use a generated `<a>`.
1736 * @cfg {boolean} [framed=true] Render the button with a frame
1738 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
1739 // Configuration initialization
1740 config
= config
|| {};
1743 this.$button
= null;
1745 this.active
= false;
1746 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
1747 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
1748 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
1749 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
1750 this.onClickHandler
= this.onClick
.bind( this );
1751 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
1754 this.$element
.addClass( 'oo-ui-buttonElement' );
1755 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
1756 this.setButtonElement( config
.$button
|| $( '<a>' ) );
1761 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
1763 /* Static Properties */
1766 * Cancel mouse down events.
1768 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
1769 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
1770 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
1775 * @property {boolean}
1777 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
1782 * A 'click' event is emitted when the button element is clicked.
1790 * Set the button element.
1792 * This method is used to retarget a button mixin so that its functionality applies to
1793 * the specified button element instead of the one created by the class. If a button element
1794 * is already set, the method will remove the mixin’s effect on that element.
1796 * @param {jQuery} $button Element to use as button
1798 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
1799 if ( this.$button
) {
1801 .removeClass( 'oo-ui-buttonElement-button' )
1802 .removeAttr( 'role accesskey' )
1804 mousedown
: this.onMouseDownHandler
,
1805 keydown
: this.onKeyDownHandler
,
1806 click
: this.onClickHandler
,
1807 keypress
: this.onKeyPressHandler
1811 this.$button
= $button
1812 .addClass( 'oo-ui-buttonElement-button' )
1813 .attr( { role
: 'button' } )
1815 mousedown
: this.onMouseDownHandler
,
1816 keydown
: this.onKeyDownHandler
,
1817 click
: this.onClickHandler
,
1818 keypress
: this.onKeyPressHandler
1823 * Handles mouse down events.
1826 * @param {jQuery.Event} e Mouse down event
1828 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
1829 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
1832 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
1833 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
1834 // reliably remove the pressed class
1835 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
1836 // Prevent change of focus unless specifically configured otherwise
1837 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
1843 * Handles mouse up events.
1846 * @param {MouseEvent} e Mouse up event
1848 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
1849 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
1852 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
1853 // Stop listening for mouseup, since we only needed this once
1854 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
1858 * Handles mouse click events.
1861 * @param {jQuery.Event} e Mouse click event
1864 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
1865 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
1866 if ( this.emit( 'click' ) ) {
1873 * Handles key down events.
1876 * @param {jQuery.Event} e Key down event
1878 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
1879 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
1882 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
1883 // Run the keyup handler no matter where the key is when the button is let go, so we can
1884 // reliably remove the pressed class
1885 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
1889 * Handles key up events.
1892 * @param {KeyboardEvent} e Key up event
1894 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
1895 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
1898 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
1899 // Stop listening for keyup, since we only needed this once
1900 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
1904 * Handles key press events.
1907 * @param {jQuery.Event} e Key press event
1910 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
1911 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
1912 if ( this.emit( 'click' ) ) {
1919 * Check if button has a frame.
1921 * @return {boolean} Button is framed
1923 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
1928 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
1930 * @param {boolean} [framed] Make button framed, omit to toggle
1933 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
1934 framed
= framed
=== undefined ? !this.framed
: !!framed
;
1935 if ( framed
!== this.framed
) {
1936 this.framed
= framed
;
1938 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
1939 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
1940 this.updateThemeClasses();
1947 * Set the button's active state.
1949 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
1950 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
1951 * for other button types.
1953 * @param {boolean} value Make button active
1956 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
1957 this.active
= !!value
;
1958 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
1963 * Check if the button is active
1965 * @return {boolean} The button is active
1967 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
1972 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
1973 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
1974 * items from the group is done through the interface the class provides.
1975 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1977 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
1983 * @param {Object} [config] Configuration options
1984 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
1985 * is omitted, the group element will use a generated `<div>`.
1987 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
1988 // Configuration initialization
1989 config
= config
|| {};
1994 this.aggregateItemEvents
= {};
1997 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2003 * Set the group element.
2005 * If an element is already set, items will be moved to the new element.
2007 * @param {jQuery} $group Element to use as group
2009 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2012 this.$group
= $group
;
2013 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2014 this.$group
.append( this.items
[ i
].$element
);
2019 * Check if a group contains no items.
2021 * @return {boolean} Group is empty
2023 OO
.ui
.mixin
.GroupElement
.prototype.isEmpty = function () {
2024 return !this.items
.length
;
2028 * Get all items in the group.
2030 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
2031 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
2034 * @return {OO.ui.Element[]} An array of items.
2036 OO
.ui
.mixin
.GroupElement
.prototype.getItems = function () {
2037 return this.items
.slice( 0 );
2041 * Get an item by its data.
2043 * Only the first item with matching data will be returned. To return all matching items,
2044 * use the #getItemsFromData method.
2046 * @param {Object} data Item data to search for
2047 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2049 OO
.ui
.mixin
.GroupElement
.prototype.getItemFromData = function ( data
) {
2051 hash
= OO
.getHash( data
);
2053 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2054 item
= this.items
[ i
];
2055 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2064 * Get items by their data.
2066 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2068 * @param {Object} data Item data to search for
2069 * @return {OO.ui.Element[]} Items with equivalent data
2071 OO
.ui
.mixin
.GroupElement
.prototype.getItemsFromData = function ( data
) {
2073 hash
= OO
.getHash( data
),
2076 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2077 item
= this.items
[ i
];
2078 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2087 * Aggregate the events emitted by the group.
2089 * When events are aggregated, the group will listen to all contained items for the event,
2090 * and then emit the event under a new name. The new event will contain an additional leading
2091 * parameter containing the item that emitted the original event. Other arguments emitted from
2092 * the original event are passed through.
2094 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
2095 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
2096 * A `null` value will remove aggregated events.
2098 * @throws {Error} An error is thrown if aggregation already exists.
2100 OO
.ui
.mixin
.GroupElement
.prototype.aggregate = function ( events
) {
2101 var i
, len
, item
, add
, remove
, itemEvent
, groupEvent
;
2103 for ( itemEvent
in events
) {
2104 groupEvent
= events
[ itemEvent
];
2106 // Remove existing aggregated event
2107 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2108 // Don't allow duplicate aggregations
2110 throw new Error( 'Duplicate item event aggregation for ' + itemEvent
);
2112 // Remove event aggregation from existing items
2113 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2114 item
= this.items
[ i
];
2115 if ( item
.connect
&& item
.disconnect
) {
2117 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2118 item
.disconnect( this, remove
);
2121 // Prevent future items from aggregating event
2122 delete this.aggregateItemEvents
[ itemEvent
];
2125 // Add new aggregate event
2127 // Make future items aggregate event
2128 this.aggregateItemEvents
[ itemEvent
] = groupEvent
;
2129 // Add event aggregation to existing items
2130 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2131 item
= this.items
[ i
];
2132 if ( item
.connect
&& item
.disconnect
) {
2134 add
[ itemEvent
] = [ 'emit', groupEvent
, item
];
2135 item
.connect( this, add
);
2143 * Add items to the group.
2145 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2146 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2148 * @param {OO.ui.Element[]} items An array of items to add to the group
2149 * @param {number} [index] Index of the insertion point
2152 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2153 var i
, len
, item
, event
, events
, currentIndex
,
2156 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2159 // Check if item exists then remove it first, effectively "moving" it
2160 currentIndex
= this.items
.indexOf( item
);
2161 if ( currentIndex
>= 0 ) {
2162 this.removeItems( [ item
] );
2163 // Adjust index to compensate for removal
2164 if ( currentIndex
< index
) {
2169 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
2171 for ( event
in this.aggregateItemEvents
) {
2172 events
[ event
] = [ 'emit', this.aggregateItemEvents
[ event
], item
];
2174 item
.connect( this, events
);
2176 item
.setElementGroup( this );
2177 itemElements
.push( item
.$element
.get( 0 ) );
2180 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2181 this.$group
.append( itemElements
);
2182 this.items
.push
.apply( this.items
, items
);
2183 } else if ( index
=== 0 ) {
2184 this.$group
.prepend( itemElements
);
2185 this.items
.unshift
.apply( this.items
, items
);
2187 this.items
[ index
].$element
.before( itemElements
);
2188 this.items
.splice
.apply( this.items
, [ index
, 0 ].concat( items
) );
2195 * Remove the specified items from a group.
2197 * Removed items are detached (not removed) from the DOM so that they may be reused.
2198 * To remove all items from a group, you may wish to use the #clearItems method instead.
2200 * @param {OO.ui.Element[]} items An array of items to remove
2203 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2204 var i
, len
, item
, index
, remove
, itemEvent
;
2206 // Remove specific items
2207 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2209 index
= this.items
.indexOf( item
);
2210 if ( index
!== -1 ) {
2212 item
.connect
&& item
.disconnect
&&
2213 !$.isEmptyObject( this.aggregateItemEvents
)
2216 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2217 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2219 item
.disconnect( this, remove
);
2221 item
.setElementGroup( null );
2222 this.items
.splice( index
, 1 );
2223 item
.$element
.detach();
2231 * Clear all items from the group.
2233 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2234 * To remove only a subset of items from a group, use the #removeItems method.
2238 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2239 var i
, len
, item
, remove
, itemEvent
;
2242 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2243 item
= this.items
[ i
];
2245 item
.connect
&& item
.disconnect
&&
2246 !$.isEmptyObject( this.aggregateItemEvents
)
2249 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2250 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2252 item
.disconnect( this, remove
);
2254 item
.setElementGroup( null );
2255 item
.$element
.detach();
2263 * IconElement is often mixed into other classes to generate an icon.
2264 * Icons are graphics, about the size of normal text. They are used to aid the user
2265 * in locating a control or to convey information in a space-efficient way. See the
2266 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2267 * included in the library.
2269 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2275 * @param {Object} [config] Configuration options
2276 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2277 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2278 * the icon element be set to an existing icon instead of the one generated by this class, set a
2279 * value using a jQuery selection. For example:
2281 * // Use a <div> tag instead of a <span>
2283 * // Use an existing icon element instead of the one generated by the class
2284 * $icon: this.$element
2285 * // Use an icon element from a child widget
2286 * $icon: this.childwidget.$element
2287 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2288 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2289 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2290 * by the user's language.
2292 * Example of an i18n map:
2294 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2295 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2296 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2297 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2298 * text. The icon title is displayed when users move the mouse over the icon.
2300 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2301 // Configuration initialization
2302 config
= config
|| {};
2307 this.iconTitle
= null;
2310 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2311 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2312 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2317 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2319 /* Static Properties */
2322 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2323 * for i18n purposes and contains a `default` icon name and additional names keyed by
2324 * language code. The `default` name is used when no icon is keyed by the user's language.
2326 * Example of an i18n map:
2328 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2330 * Note: the static property will be overridden if the #icon configuration is used.
2334 * @property {Object|string}
2336 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2339 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2340 * function that returns title text, or `null` for no title.
2342 * The static property will be overridden if the #iconTitle configuration is used.
2346 * @property {string|Function|null}
2348 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2353 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2354 * applies to the specified icon element instead of the one created by the class. If an icon
2355 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2356 * and mixin methods will no longer affect the element.
2358 * @param {jQuery} $icon Element to use as icon
2360 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2363 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2364 .removeAttr( 'title' );
2368 .addClass( 'oo-ui-iconElement-icon' )
2369 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2370 if ( this.iconTitle
!== null ) {
2371 this.$icon
.attr( 'title', this.iconTitle
);
2374 this.updateThemeClasses();
2378 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2379 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2382 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2383 * by language code, or `null` to remove the icon.
2386 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2387 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2388 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2390 if ( this.icon
!== icon
) {
2392 if ( this.icon
!== null ) {
2393 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2395 if ( icon
!== null ) {
2396 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2402 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2403 this.updateThemeClasses();
2409 * Set the icon title. Use `null` to remove the title.
2411 * @param {string|Function|null} iconTitle A text string used as the icon title,
2412 * a function that returns title text, or `null` for no title.
2415 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2416 iconTitle
= typeof iconTitle
=== 'function' ||
2417 ( typeof iconTitle
=== 'string' && iconTitle
.length
) ?
2418 OO
.ui
.resolveMsg( iconTitle
) : null;
2420 if ( this.iconTitle
!== iconTitle
) {
2421 this.iconTitle
= iconTitle
;
2423 if ( this.iconTitle
!== null ) {
2424 this.$icon
.attr( 'title', iconTitle
);
2426 this.$icon
.removeAttr( 'title' );
2435 * Get the symbolic name of the icon.
2437 * @return {string} Icon name
2439 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2444 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2446 * @return {string} Icon title text
2448 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2449 return this.iconTitle
;
2453 * IndicatorElement is often mixed into other classes to generate an indicator.
2454 * Indicators are small graphics that are generally used in two ways:
2456 * - To draw attention to the status of an item. For example, an indicator might be
2457 * used to show that an item in a list has errors that need to be resolved.
2458 * - To clarify the function of a control that acts in an exceptional way (a button
2459 * that opens a menu instead of performing an action directly, for example).
2461 * For a list of indicators included in the library, please see the
2462 * [OOjs UI documentation on MediaWiki] [1].
2464 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2470 * @param {Object} [config] Configuration options
2471 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2472 * configuration is omitted, the indicator element will use a generated `<span>`.
2473 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2474 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2476 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2477 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2478 * or a function that returns title text. The indicator title is displayed when users move
2479 * the mouse over the indicator.
2481 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2482 // Configuration initialization
2483 config
= config
|| {};
2486 this.$indicator
= null;
2487 this.indicator
= null;
2488 this.indicatorTitle
= null;
2491 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2492 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2493 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2498 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2500 /* Static Properties */
2503 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2504 * The static property will be overridden if the #indicator configuration is used.
2508 * @property {string|null}
2510 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2513 * A text string used as the indicator title, a function that returns title text, or `null`
2514 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2518 * @property {string|Function|null}
2520 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2525 * Set the indicator element.
2527 * If an element is already set, it will be cleaned up before setting up the new element.
2529 * @param {jQuery} $indicator Element to use as indicator
2531 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2532 if ( this.$indicator
) {
2534 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2535 .removeAttr( 'title' );
2538 this.$indicator
= $indicator
2539 .addClass( 'oo-ui-indicatorElement-indicator' )
2540 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2541 if ( this.indicatorTitle
!== null ) {
2542 this.$indicator
.attr( 'title', this.indicatorTitle
);
2545 this.updateThemeClasses();
2549 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2551 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2554 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2555 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2557 if ( this.indicator
!== indicator
) {
2558 if ( this.$indicator
) {
2559 if ( this.indicator
!== null ) {
2560 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2562 if ( indicator
!== null ) {
2563 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2566 this.indicator
= indicator
;
2569 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2570 this.updateThemeClasses();
2576 * Set the indicator title.
2578 * The title is displayed when a user moves the mouse over the indicator.
2580 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
2581 * `null` for no indicator title
2584 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2585 indicatorTitle
= typeof indicatorTitle
=== 'function' ||
2586 ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ?
2587 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2589 if ( this.indicatorTitle
!== indicatorTitle
) {
2590 this.indicatorTitle
= indicatorTitle
;
2591 if ( this.$indicator
) {
2592 if ( this.indicatorTitle
!== null ) {
2593 this.$indicator
.attr( 'title', indicatorTitle
);
2595 this.$indicator
.removeAttr( 'title' );
2604 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2606 * @return {string} Symbolic name of indicator
2608 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2609 return this.indicator
;
2613 * Get the indicator title.
2615 * The title is displayed when a user moves the mouse over the indicator.
2617 * @return {string} Indicator title text
2619 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2620 return this.indicatorTitle
;
2624 * LabelElement is often mixed into other classes to generate a label, which
2625 * helps identify the function of an interface element.
2626 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2628 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2634 * @param {Object} [config] Configuration options
2635 * @cfg {jQuery} [$label] The label element created by the class. If this
2636 * configuration is omitted, the label element will use a generated `<span>`.
2637 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2638 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2639 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2640 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2642 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2643 // Configuration initialization
2644 config
= config
|| {};
2651 this.setLabel( config
.label
|| this.constructor.static.label
);
2652 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2657 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2662 * @event labelChange
2663 * @param {string} value
2666 /* Static Properties */
2669 * The label text. The label can be specified as a plaintext string, a function that will
2670 * produce a string in the future, or `null` for no label. The static value will
2671 * be overridden if a label is specified with the #label config option.
2675 * @property {string|Function|null}
2677 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2679 /* Static methods */
2682 * Highlight the first occurrence of the query in the given text
2684 * @param {string} text Text
2685 * @param {string} query Query to find
2686 * @return {jQuery} Text with the first match of the query
2687 * sub-string wrapped in highlighted span
2689 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
) {
2690 var $result
= $( '<span>' ),
2691 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2693 if ( !query
.length
|| offset
=== -1 ) {
2694 return $result
.text( text
);
2697 document
.createTextNode( text
.slice( 0, offset
) ),
2699 .addClass( 'oo-ui-labelElement-label-highlight' )
2700 .text( text
.slice( offset
, offset
+ query
.length
) ),
2701 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2703 return $result
.contents();
2709 * Set the label element.
2711 * If an element is already set, it will be cleaned up before setting up the new element.
2713 * @param {jQuery} $label Element to use as label
2715 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2716 if ( this.$label
) {
2717 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2720 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2721 this.setLabelContent( this.label
);
2727 * An empty string will result in the label being hidden. A string containing only whitespace will
2728 * be converted to a single ` `.
2730 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2731 * text; or null for no label
2734 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2735 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2736 label
= ( ( typeof label
=== 'string' && label
.length
) || label
instanceof jQuery
|| label
instanceof OO
.ui
.HtmlSnippet
) ? label
: null;
2738 this.$element
.toggleClass( 'oo-ui-labelElement', !!label
);
2740 if ( this.label
!== label
) {
2741 if ( this.$label
) {
2742 this.setLabelContent( label
);
2745 this.emit( 'labelChange' );
2752 * Set the label as plain text with a highlighted query
2754 * @param {string} text Text label to set
2755 * @param {string} query Substring of text to highlight
2758 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
) {
2759 return this.setLabel( this.constructor.static.highlightQuery( text
, query
) );
2765 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2766 * text; or null for no label
2768 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2776 * @deprecated since 0.16.0
2778 OO
.ui
.mixin
.LabelElement
.prototype.fitLabel = function () {
2783 * Set the content of the label.
2785 * Do not call this method until after the label element has been set by #setLabelElement.
2788 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2789 * text; or null for no label
2791 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2792 if ( typeof label
=== 'string' ) {
2793 if ( label
.match( /^\s*$/ ) ) {
2794 // Convert whitespace only string to a single non-breaking space
2795 this.$label
.html( ' ' );
2797 this.$label
.text( label
);
2799 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2800 this.$label
.html( label
.toString() );
2801 } else if ( label
instanceof jQuery
) {
2802 this.$label
.empty().append( label
);
2804 this.$label
.empty();
2809 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
2810 * additional functionality to an element created by another class. The class provides
2811 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
2812 * which are used to customize the look and feel of a widget to better describe its
2813 * importance and functionality.
2815 * The library currently contains the following styling flags for general use:
2817 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
2818 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
2819 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
2821 * The flags affect the appearance of the buttons:
2824 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
2825 * var button1 = new OO.ui.ButtonWidget( {
2826 * label: 'Constructive',
2827 * flags: 'constructive'
2829 * var button2 = new OO.ui.ButtonWidget( {
2830 * label: 'Destructive',
2831 * flags: 'destructive'
2833 * var button3 = new OO.ui.ButtonWidget( {
2834 * label: 'Progressive',
2835 * flags: 'progressive'
2837 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
2839 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
2840 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
2842 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2848 * @param {Object} [config] Configuration options
2849 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
2850 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
2851 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2852 * @cfg {jQuery} [$flagged] The flagged element. By default,
2853 * the flagged functionality is applied to the element created by the class ($element).
2854 * If a different element is specified, the flagged functionality will be applied to it instead.
2856 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
2857 // Configuration initialization
2858 config
= config
|| {};
2862 this.$flagged
= null;
2865 this.setFlags( config
.flags
);
2866 this.setFlaggedElement( config
.$flagged
|| this.$element
);
2873 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
2874 * parameter contains the name of each modified flag and indicates whether it was
2877 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
2878 * that the flag was added, `false` that the flag was removed.
2884 * Set the flagged element.
2886 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
2887 * If an element is already set, the method will remove the mixin’s effect on that element.
2889 * @param {jQuery} $flagged Element that should be flagged
2891 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
2892 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
2893 return 'oo-ui-flaggedElement-' + flag
;
2896 if ( this.$flagged
) {
2897 this.$flagged
.removeClass( classNames
);
2900 this.$flagged
= $flagged
.addClass( classNames
);
2904 * Check if the specified flag is set.
2906 * @param {string} flag Name of flag
2907 * @return {boolean} The flag is set
2909 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
2910 // This may be called before the constructor, thus before this.flags is set
2911 return this.flags
&& ( flag
in this.flags
);
2915 * Get the names of all flags set.
2917 * @return {string[]} Flag names
2919 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
2920 // This may be called before the constructor, thus before this.flags is set
2921 return Object
.keys( this.flags
|| {} );
2930 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
2931 var flag
, className
,
2934 classPrefix
= 'oo-ui-flaggedElement-';
2936 for ( flag
in this.flags
) {
2937 className
= classPrefix
+ flag
;
2938 changes
[ flag
] = false;
2939 delete this.flags
[ flag
];
2940 remove
.push( className
);
2943 if ( this.$flagged
) {
2944 this.$flagged
.removeClass( remove
.join( ' ' ) );
2947 this.updateThemeClasses();
2948 this.emit( 'flag', changes
);
2954 * Add one or more flags.
2956 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
2957 * or an object keyed by flag name with a boolean value that indicates whether the flag should
2958 * be added (`true`) or removed (`false`).
2962 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
2963 var i
, len
, flag
, className
,
2967 classPrefix
= 'oo-ui-flaggedElement-';
2969 if ( typeof flags
=== 'string' ) {
2970 className
= classPrefix
+ flags
;
2972 if ( !this.flags
[ flags
] ) {
2973 this.flags
[ flags
] = true;
2974 add
.push( className
);
2976 } else if ( Array
.isArray( flags
) ) {
2977 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
2979 className
= classPrefix
+ flag
;
2981 if ( !this.flags
[ flag
] ) {
2982 changes
[ flag
] = true;
2983 this.flags
[ flag
] = true;
2984 add
.push( className
);
2987 } else if ( OO
.isPlainObject( flags
) ) {
2988 for ( flag
in flags
) {
2989 className
= classPrefix
+ flag
;
2990 if ( flags
[ flag
] ) {
2992 if ( !this.flags
[ flag
] ) {
2993 changes
[ flag
] = true;
2994 this.flags
[ flag
] = true;
2995 add
.push( className
);
2999 if ( this.flags
[ flag
] ) {
3000 changes
[ flag
] = false;
3001 delete this.flags
[ flag
];
3002 remove
.push( className
);
3008 if ( this.$flagged
) {
3010 .addClass( add
.join( ' ' ) )
3011 .removeClass( remove
.join( ' ' ) );
3014 this.updateThemeClasses();
3015 this.emit( 'flag', changes
);
3021 * TitledElement is mixed into other classes to provide a `title` attribute.
3022 * Titles are rendered by the browser and are made visible when the user moves
3023 * the mouse over the element. Titles are not visible on touch devices.
3026 * // TitledElement provides a 'title' attribute to the
3027 * // ButtonWidget class
3028 * var button = new OO.ui.ButtonWidget( {
3029 * label: 'Button with Title',
3030 * title: 'I am a button'
3032 * $( 'body' ).append( button.$element );
3038 * @param {Object} [config] Configuration options
3039 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3040 * If this config is omitted, the title functionality is applied to $element, the
3041 * element created by the class.
3042 * @cfg {string|Function} [title] The title text or a function that returns text. If
3043 * this config is omitted, the value of the {@link #static-title static title} property is used.
3045 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3046 // Configuration initialization
3047 config
= config
|| {};
3050 this.$titled
= null;
3054 this.setTitle( config
.title
|| this.constructor.static.title
);
3055 this.setTitledElement( config
.$titled
|| this.$element
);
3060 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3062 /* Static Properties */
3065 * The title text, a function that returns text, or `null` for no title. The value of the static property
3066 * is overridden if the #title config option is used.
3070 * @property {string|Function|null}
3072 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3077 * Set the titled element.
3079 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3080 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3082 * @param {jQuery} $titled Element that should use the 'titled' functionality
3084 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3085 if ( this.$titled
) {
3086 this.$titled
.removeAttr( 'title' );
3089 this.$titled
= $titled
;
3091 this.$titled
.attr( 'title', this.title
);
3098 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3101 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3102 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3103 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3105 if ( this.title
!== title
) {
3106 if ( this.$titled
) {
3107 if ( title
!== null ) {
3108 this.$titled
.attr( 'title', title
);
3110 this.$titled
.removeAttr( 'title' );
3122 * @return {string} Title string
3124 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3129 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3130 * Accesskeys allow an user to go to a specific element by using
3131 * a shortcut combination of a browser specific keys + the key
3135 * // AccessKeyedElement provides an 'accesskey' attribute to the
3136 * // ButtonWidget class
3137 * var button = new OO.ui.ButtonWidget( {
3138 * label: 'Button with Accesskey',
3141 * $( 'body' ).append( button.$element );
3147 * @param {Object} [config] Configuration options
3148 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3149 * If this config is omitted, the accesskey functionality is applied to $element, the
3150 * element created by the class.
3151 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3152 * this config is omitted, no accesskey will be added.
3154 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3155 // Configuration initialization
3156 config
= config
|| {};
3159 this.$accessKeyed
= null;
3160 this.accessKey
= null;
3163 this.setAccessKey( config
.accessKey
|| null );
3164 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3169 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3171 /* Static Properties */
3174 * The access key, a function that returns a key, or `null` for no accesskey.
3178 * @property {string|Function|null}
3180 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3185 * Set the accesskeyed element.
3187 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3188 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3190 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3192 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3193 if ( this.$accessKeyed
) {
3194 this.$accessKeyed
.removeAttr( 'accesskey' );
3197 this.$accessKeyed
= $accessKeyed
;
3198 if ( this.accessKey
) {
3199 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3206 * @param {string|Function|null} accesskey Key, a function that returns a key, or `null` for no accesskey
3209 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3210 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3212 if ( this.accessKey
!== accessKey
) {
3213 if ( this.$accessKeyed
) {
3214 if ( accessKey
!== null ) {
3215 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3217 this.$accessKeyed
.removeAttr( 'accesskey' );
3220 this.accessKey
= accessKey
;
3229 * @return {string} accessKey string
3231 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3232 return this.accessKey
;
3236 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3237 * feels, and functionality can be customized via the class’s configuration options
3238 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3241 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3244 * // A button widget
3245 * var button = new OO.ui.ButtonWidget( {
3246 * label: 'Button with Icon',
3248 * iconTitle: 'Remove'
3250 * $( 'body' ).append( button.$element );
3252 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3255 * @extends OO.ui.Widget
3256 * @mixins OO.ui.mixin.ButtonElement
3257 * @mixins OO.ui.mixin.IconElement
3258 * @mixins OO.ui.mixin.IndicatorElement
3259 * @mixins OO.ui.mixin.LabelElement
3260 * @mixins OO.ui.mixin.TitledElement
3261 * @mixins OO.ui.mixin.FlaggedElement
3262 * @mixins OO.ui.mixin.TabIndexedElement
3263 * @mixins OO.ui.mixin.AccessKeyedElement
3266 * @param {Object} [config] Configuration options
3267 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3268 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3269 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3271 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3272 // Configuration initialization
3273 config
= config
|| {};
3275 // Parent constructor
3276 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3278 // Mixin constructors
3279 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3280 OO
.ui
.mixin
.IconElement
.call( this, config
);
3281 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3282 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3283 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3284 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3285 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3286 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3291 this.noFollow
= false;
3294 this.connect( this, { disable
: 'onDisable' } );
3297 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3299 .addClass( 'oo-ui-buttonWidget' )
3300 .append( this.$button
);
3301 this.setHref( config
.href
);
3302 this.setTarget( config
.target
);
3303 this.setNoFollow( config
.noFollow
);
3308 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3309 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3310 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3311 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3312 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3313 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3314 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3315 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3316 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3323 OO
.ui
.ButtonWidget
.prototype.onMouseDown = function ( e
) {
3324 if ( !this.isDisabled() ) {
3325 // Remove the tab-index while the button is down to prevent the button from stealing focus
3326 this.$button
.removeAttr( 'tabindex' );
3329 return OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown
.call( this, e
);
3335 OO
.ui
.ButtonWidget
.prototype.onMouseUp = function ( e
) {
3336 if ( !this.isDisabled() ) {
3337 // Restore the tab-index after the button is up to restore the button's accessibility
3338 this.$button
.attr( 'tabindex', this.tabIndex
);
3341 return OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp
.call( this, e
);
3345 * Get hyperlink location.
3347 * @return {string} Hyperlink location
3349 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3354 * Get hyperlink target.
3356 * @return {string} Hyperlink target
3358 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3363 * Get search engine traversal hint.
3365 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3367 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3368 return this.noFollow
;
3372 * Set hyperlink location.
3374 * @param {string|null} href Hyperlink location, null to remove
3376 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3377 href
= typeof href
=== 'string' ? href
: null;
3378 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3382 if ( href
!== this.href
) {
3391 * Update the `href` attribute, in case of changes to href or
3397 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3398 if ( this.href
!== null && !this.isDisabled() ) {
3399 this.$button
.attr( 'href', this.href
);
3401 this.$button
.removeAttr( 'href' );
3408 * Handle disable events.
3411 * @param {boolean} disabled Element is disabled
3413 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3418 * Set hyperlink target.
3420 * @param {string|null} target Hyperlink target, null to remove
3422 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3423 target
= typeof target
=== 'string' ? target
: null;
3425 if ( target
!== this.target
) {
3426 this.target
= target
;
3427 if ( target
!== null ) {
3428 this.$button
.attr( 'target', target
);
3430 this.$button
.removeAttr( 'target' );
3438 * Set search engine traversal hint.
3440 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3442 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3443 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3445 if ( noFollow
!== this.noFollow
) {
3446 this.noFollow
= noFollow
;
3448 this.$button
.attr( 'rel', 'nofollow' );
3450 this.$button
.removeAttr( 'rel' );
3458 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3459 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3460 * removed, and cleared from the group.
3463 * // Example: A ButtonGroupWidget with two buttons
3464 * var button1 = new OO.ui.PopupButtonWidget( {
3465 * label: 'Select a category',
3468 * $content: $( '<p>List of categories...</p>' ),
3473 * var button2 = new OO.ui.ButtonWidget( {
3476 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3477 * items: [button1, button2]
3479 * $( 'body' ).append( buttonGroup.$element );
3482 * @extends OO.ui.Widget
3483 * @mixins OO.ui.mixin.GroupElement
3486 * @param {Object} [config] Configuration options
3487 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3489 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3490 // Configuration initialization
3491 config
= config
|| {};
3493 // Parent constructor
3494 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3496 // Mixin constructors
3497 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3500 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3501 if ( Array
.isArray( config
.items
) ) {
3502 this.addItems( config
.items
);
3508 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3509 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3512 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3513 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3514 * for a list of icons included in the library.
3517 * // An icon widget with a label
3518 * var myIcon = new OO.ui.IconWidget( {
3522 * // Create a label.
3523 * var iconLabel = new OO.ui.LabelWidget( {
3526 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3528 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3531 * @extends OO.ui.Widget
3532 * @mixins OO.ui.mixin.IconElement
3533 * @mixins OO.ui.mixin.TitledElement
3534 * @mixins OO.ui.mixin.FlaggedElement
3537 * @param {Object} [config] Configuration options
3539 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3540 // Configuration initialization
3541 config
= config
|| {};
3543 // Parent constructor
3544 OO
.ui
.IconWidget
.parent
.call( this, config
);
3546 // Mixin constructors
3547 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3548 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3549 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3552 this.$element
.addClass( 'oo-ui-iconWidget' );
3557 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3558 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3559 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3560 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3562 /* Static Properties */
3564 OO
.ui
.IconWidget
.static.tagName
= 'span';
3567 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3568 * attention to the status of an item or to clarify the function of a control. For a list of
3569 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3572 * // Example of an indicator widget
3573 * var indicator1 = new OO.ui.IndicatorWidget( {
3574 * indicator: 'alert'
3577 * // Create a fieldset layout to add a label
3578 * var fieldset = new OO.ui.FieldsetLayout();
3579 * fieldset.addItems( [
3580 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3582 * $( 'body' ).append( fieldset.$element );
3584 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3587 * @extends OO.ui.Widget
3588 * @mixins OO.ui.mixin.IndicatorElement
3589 * @mixins OO.ui.mixin.TitledElement
3592 * @param {Object} [config] Configuration options
3594 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
3595 // Configuration initialization
3596 config
= config
|| {};
3598 // Parent constructor
3599 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
3601 // Mixin constructors
3602 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
3603 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3606 this.$element
.addClass( 'oo-ui-indicatorWidget' );
3611 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
3612 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
3613 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
3615 /* Static Properties */
3617 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
3620 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3621 * be configured with a `label` option that is set to a string, a label node, or a function:
3623 * - String: a plaintext string
3624 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3625 * label that includes a link or special styling, such as a gray color or additional graphical elements.
3626 * - Function: a function that will produce a string in the future. Functions are used
3627 * in cases where the value of the label is not currently defined.
3629 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3630 * will come into focus when the label is clicked.
3633 * // Examples of LabelWidgets
3634 * var label1 = new OO.ui.LabelWidget( {
3635 * label: 'plaintext label'
3637 * var label2 = new OO.ui.LabelWidget( {
3638 * label: $( '<a href="default.html">jQuery label</a>' )
3640 * // Create a fieldset layout with fields for each example
3641 * var fieldset = new OO.ui.FieldsetLayout();
3642 * fieldset.addItems( [
3643 * new OO.ui.FieldLayout( label1 ),
3644 * new OO.ui.FieldLayout( label2 )
3646 * $( 'body' ).append( fieldset.$element );
3649 * @extends OO.ui.Widget
3650 * @mixins OO.ui.mixin.LabelElement
3653 * @param {Object} [config] Configuration options
3654 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
3655 * Clicking the label will focus the specified input field.
3657 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
3658 // Configuration initialization
3659 config
= config
|| {};
3661 // Parent constructor
3662 OO
.ui
.LabelWidget
.parent
.call( this, config
);
3664 // Mixin constructors
3665 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
3666 OO
.ui
.mixin
.TitledElement
.call( this, config
);
3669 this.input
= config
.input
;
3672 if ( this.input
instanceof OO
.ui
.InputWidget
) {
3673 this.$element
.on( 'click', this.onClick
.bind( this ) );
3677 this.$element
.addClass( 'oo-ui-labelWidget' );
3682 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
3683 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
3684 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
3686 /* Static Properties */
3688 OO
.ui
.LabelWidget
.static.tagName
= 'span';
3693 * Handles label mouse click events.
3696 * @param {jQuery.Event} e Mouse click event
3698 OO
.ui
.LabelWidget
.prototype.onClick = function () {
3699 this.input
.simulateLabelClick();
3704 * PendingElement is a mixin that is used to create elements that notify users that something is happening
3705 * and that they should wait before proceeding. The pending state is visually represented with a pending
3706 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
3707 * field of a {@link OO.ui.TextInputWidget text input widget}.
3709 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
3710 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
3711 * in process dialogs.
3714 * function MessageDialog( config ) {
3715 * MessageDialog.parent.call( this, config );
3717 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
3719 * MessageDialog.static.actions = [
3720 * { action: 'save', label: 'Done', flags: 'primary' },
3721 * { label: 'Cancel', flags: 'safe' }
3724 * MessageDialog.prototype.initialize = function () {
3725 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
3726 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
3727 * 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>' );
3728 * this.$body.append( this.content.$element );
3730 * MessageDialog.prototype.getBodyHeight = function () {
3733 * MessageDialog.prototype.getActionProcess = function ( action ) {
3734 * var dialog = this;
3735 * if ( action === 'save' ) {
3736 * dialog.getActions().get({actions: 'save'})[0].pushPending();
3737 * return new OO.ui.Process()
3739 * .next( function () {
3740 * dialog.getActions().get({actions: 'save'})[0].popPending();
3743 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
3746 * var windowManager = new OO.ui.WindowManager();
3747 * $( 'body' ).append( windowManager.$element );
3749 * var dialog = new MessageDialog();
3750 * windowManager.addWindows( [ dialog ] );
3751 * windowManager.openWindow( dialog );
3757 * @param {Object} [config] Configuration options
3758 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
3760 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
3761 // Configuration initialization
3762 config
= config
|| {};
3766 this.$pending
= null;
3769 this.setPendingElement( config
.$pending
|| this.$element
);
3774 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
3779 * Set the pending element (and clean up any existing one).
3781 * @param {jQuery} $pending The element to set to pending.
3783 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
3784 if ( this.$pending
) {
3785 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3788 this.$pending
= $pending
;
3789 if ( this.pending
> 0 ) {
3790 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3795 * Check if an element is pending.
3797 * @return {boolean} Element is pending
3799 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
3800 return !!this.pending
;
3804 * Increase the pending counter. The pending state will remain active until the counter is zero
3805 * (i.e., the number of calls to #pushPending and #popPending is the same).
3809 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
3810 if ( this.pending
=== 0 ) {
3811 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3812 this.updateThemeClasses();
3820 * Decrease the pending counter. The pending state will remain active until the counter is zero
3821 * (i.e., the number of calls to #pushPending and #popPending is the same).
3825 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
3826 if ( this.pending
=== 1 ) {
3827 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3828 this.updateThemeClasses();
3830 this.pending
= Math
.max( 0, this.pending
- 1 );
3836 * Element that can be automatically clipped to visible boundaries.
3838 * Whenever the element's natural height changes, you have to call
3839 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
3840 * clipping correctly.
3842 * The dimensions of #$clippableContainer will be compared to the boundaries of the
3843 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
3844 * then #$clippable will be given a fixed reduced height and/or width and will be made
3845 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
3846 * but you can build a static footer by setting #$clippableContainer to an element that contains
3847 * #$clippable and the footer.
3853 * @param {Object} [config] Configuration options
3854 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
3855 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
3856 * omit to use #$clippable
3858 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
3859 // Configuration initialization
3860 config
= config
|| {};
3863 this.$clippable
= null;
3864 this.$clippableContainer
= null;
3865 this.clipping
= false;
3866 this.clippedHorizontally
= false;
3867 this.clippedVertically
= false;
3868 this.$clippableScrollableContainer
= null;
3869 this.$clippableScroller
= null;
3870 this.$clippableWindow
= null;
3871 this.idealWidth
= null;
3872 this.idealHeight
= null;
3873 this.onClippableScrollHandler
= this.clip
.bind( this );
3874 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
3877 if ( config
.$clippableContainer
) {
3878 this.setClippableContainer( config
.$clippableContainer
);
3880 this.setClippableElement( config
.$clippable
|| this.$element
);
3886 * Set clippable element.
3888 * If an element is already set, it will be cleaned up before setting up the new element.
3890 * @param {jQuery} $clippable Element to make clippable
3892 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
3893 if ( this.$clippable
) {
3894 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
3895 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
3896 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
3899 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
3904 * Set clippable container.
3906 * This is the container that will be measured when deciding whether to clip. When clipping,
3907 * #$clippable will be resized in order to keep the clippable container fully visible.
3909 * If the clippable container is unset, #$clippable will be used.
3911 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
3913 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
3914 this.$clippableContainer
= $clippableContainer
;
3915 if ( this.$clippable
) {
3923 * Do not turn clipping on until after the element is attached to the DOM and visible.
3925 * @param {boolean} [clipping] Enable clipping, omit to toggle
3928 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
3929 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
3931 if ( this.clipping
!== clipping
) {
3932 this.clipping
= clipping
;
3934 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
3935 // If the clippable container is the root, we have to listen to scroll events and check
3936 // jQuery.scrollTop on the window because of browser inconsistencies
3937 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
3938 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
3939 this.$clippableScrollableContainer
;
3940 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
3941 this.$clippableWindow
= $( this.getElementWindow() )
3942 .on( 'resize', this.onClippableWindowResizeHandler
);
3943 // Initial clip after visible
3946 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
3947 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
3949 this.$clippableScrollableContainer
= null;
3950 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
3951 this.$clippableScroller
= null;
3952 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
3953 this.$clippableWindow
= null;
3961 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
3963 * @return {boolean} Element will be clipped to the visible area
3965 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
3966 return this.clipping
;
3970 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
3972 * @return {boolean} Part of the element is being clipped
3974 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
3975 return this.clippedHorizontally
|| this.clippedVertically
;
3979 * Check if the right of the element is being clipped by the nearest scrollable container.
3981 * @return {boolean} Part of the element is being clipped
3983 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
3984 return this.clippedHorizontally
;
3988 * Check if the bottom of the element is being clipped by the nearest scrollable container.
3990 * @return {boolean} Part of the element is being clipped
3992 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
3993 return this.clippedVertically
;
3997 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
3999 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4000 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4002 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4003 this.idealWidth
= width
;
4004 this.idealHeight
= height
;
4006 if ( !this.clipping
) {
4007 // Update dimensions
4008 this.$clippable
.css( { width
: width
, height
: height
} );
4010 // While clipping, idealWidth and idealHeight are not considered
4014 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
4015 * the element's natural height changes.
4017 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4018 * overlapped by, the visible area of the nearest scrollable container.
4022 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
4023 var $container
, extraHeight
, extraWidth
, ccOffset
,
4024 $scrollableContainer
, scOffset
, scHeight
, scWidth
,
4025 ccWidth
, scrollerIsWindow
, scrollTop
, scrollLeft
,
4026 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
4027 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
4028 buffer
= 7; // Chosen by fair dice roll
4030 if ( !this.clipping
) {
4031 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4035 $container
= this.$clippableContainer
|| this.$clippable
;
4036 extraHeight
= $container
.outerHeight() - this.$clippable
.outerHeight();
4037 extraWidth
= $container
.outerWidth() - this.$clippable
.outerWidth();
4038 ccOffset
= $container
.offset();
4039 $scrollableContainer
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4040 this.$clippableWindow
: this.$clippableScrollableContainer
;
4041 scOffset
= $scrollableContainer
.offset() || { top
: 0, left
: 0 };
4042 scHeight
= $scrollableContainer
.innerHeight() - buffer
;
4043 scWidth
= $scrollableContainer
.innerWidth() - buffer
;
4044 ccWidth
= $container
.outerWidth() + buffer
;
4045 scrollerIsWindow
= this.$clippableScroller
[ 0 ] === this.$clippableWindow
[ 0 ];
4046 scrollTop
= scrollerIsWindow
? this.$clippableScroller
.scrollTop() : 0;
4047 scrollLeft
= scrollerIsWindow
? this.$clippableScroller
.scrollLeft() : 0;
4048 desiredWidth
= ccOffset
.left
< 0 ?
4049 ccWidth
+ ccOffset
.left
:
4050 ( scOffset
.left
+ scrollLeft
+ scWidth
) - ccOffset
.left
;
4051 desiredHeight
= ( scOffset
.top
+ scrollTop
+ scHeight
) - ccOffset
.top
;
4052 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
4053 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
4054 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
4055 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
4056 clipWidth
= allotedWidth
< naturalWidth
;
4057 clipHeight
= allotedHeight
< naturalHeight
;
4060 this.$clippable
.css( { overflowX
: 'scroll', width
: Math
.max( 0, allotedWidth
) } );
4062 this.$clippable
.css( { width
: this.idealWidth
? this.idealWidth
- extraWidth
: '', overflowX
: '' } );
4065 this.$clippable
.css( { overflowY
: 'scroll', height
: Math
.max( 0, allotedHeight
) } );
4067 this.$clippable
.css( { height
: this.idealHeight
? this.idealHeight
- extraHeight
: '', overflowY
: '' } );
4070 // If we stopped clipping in at least one of the dimensions
4071 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
4072 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4075 this.clippedHorizontally
= clipWidth
;
4076 this.clippedVertically
= clipHeight
;
4082 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4083 * By default, each popup has an anchor that points toward its origin.
4084 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4087 * // A popup widget.
4088 * var popup = new OO.ui.PopupWidget( {
4089 * $content: $( '<p>Hi there!</p>' ),
4094 * $( 'body' ).append( popup.$element );
4095 * // To display the popup, toggle the visibility to 'true'.
4096 * popup.toggle( true );
4098 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4101 * @extends OO.ui.Widget
4102 * @mixins OO.ui.mixin.LabelElement
4103 * @mixins OO.ui.mixin.ClippableElement
4106 * @param {Object} [config] Configuration options
4107 * @cfg {number} [width=320] Width of popup in pixels
4108 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4109 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4110 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
4111 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
4112 * popup is leaning towards the right of the screen.
4113 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
4114 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
4115 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
4116 * sentence in the given language.
4117 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4118 * See the [OOjs UI docs on MediaWiki][3] for an example.
4119 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4120 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4121 * @cfg {jQuery} [$content] Content to append to the popup's body
4122 * @cfg {jQuery} [$footer] Content to append to the popup's footer
4123 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4124 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4125 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4127 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4128 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
4130 * @cfg {boolean} [padded=false] Add padding to the popup's body
4132 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
4133 // Configuration initialization
4134 config
= config
|| {};
4136 // Parent constructor
4137 OO
.ui
.PopupWidget
.parent
.call( this, config
);
4139 // Properties (must be set before ClippableElement constructor call)
4140 this.$body
= $( '<div>' );
4141 this.$popup
= $( '<div>' );
4143 // Mixin constructors
4144 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4145 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
4146 $clippable
: this.$body
,
4147 $clippableContainer
: this.$popup
4151 this.$anchor
= $( '<div>' );
4152 // If undefined, will be computed lazily in updateDimensions()
4153 this.$container
= config
.$container
;
4154 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
4155 this.autoClose
= !!config
.autoClose
;
4156 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
4157 this.transitionTimeout
= null;
4159 this.width
= config
.width
!== undefined ? config
.width
: 320;
4160 this.height
= config
.height
!== undefined ? config
.height
: null;
4161 this.setAlignment( config
.align
);
4162 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
4163 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
4166 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
4167 this.$body
.addClass( 'oo-ui-popupWidget-body' );
4168 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
4170 .addClass( 'oo-ui-popupWidget-popup' )
4171 .append( this.$body
);
4173 .addClass( 'oo-ui-popupWidget' )
4174 .append( this.$popup
, this.$anchor
);
4175 // Move content, which was added to #$element by OO.ui.Widget, to the body
4176 // FIXME This is gross, we should use '$body' or something for the config
4177 if ( config
.$content
instanceof jQuery
) {
4178 this.$body
.append( config
.$content
);
4181 if ( config
.padded
) {
4182 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
4185 if ( config
.head
) {
4186 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
4187 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
4188 this.$head
= $( '<div>' )
4189 .addClass( 'oo-ui-popupWidget-head' )
4190 .append( this.$label
, this.closeButton
.$element
);
4191 this.$popup
.prepend( this.$head
);
4194 if ( config
.$footer
) {
4195 this.$footer
= $( '<div>' )
4196 .addClass( 'oo-ui-popupWidget-footer' )
4197 .append( config
.$footer
);
4198 this.$popup
.append( this.$footer
);
4201 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
4202 // that reference properties not initialized at that time of parent class construction
4203 // TODO: Find a better way to handle post-constructor setup
4204 this.visible
= false;
4205 this.$element
.addClass( 'oo-ui-element-hidden' );
4210 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
4211 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
4212 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
4217 * Handles mouse down events.
4220 * @param {MouseEvent} e Mouse down event
4222 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
4225 !$.contains( this.$element
[ 0 ], e
.target
) &&
4226 ( !this.$autoCloseIgnore
|| !this.$autoCloseIgnore
.has( e
.target
).length
)
4228 this.toggle( false );
4233 * Bind mouse down listener.
4237 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
4238 // Capture clicks outside popup
4239 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
4243 * Handles close button click events.
4247 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
4248 if ( this.isVisible() ) {
4249 this.toggle( false );
4254 * Unbind mouse down listener.
4258 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
4259 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
4263 * Handles key down events.
4266 * @param {KeyboardEvent} e Key down event
4268 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
4270 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
4273 this.toggle( false );
4275 e
.stopPropagation();
4280 * Bind key down listener.
4284 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
4285 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4289 * Unbind key down listener.
4293 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
4294 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4298 * Show, hide, or toggle the visibility of the anchor.
4300 * @param {boolean} [show] Show anchor, omit to toggle
4302 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
4303 show
= show
=== undefined ? !this.anchored
: !!show
;
4305 if ( this.anchored
!== show
) {
4307 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
4309 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
4311 this.anchored
= show
;
4316 * Check if the anchor is visible.
4318 * @return {boolean} Anchor is visible
4320 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
4327 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
4329 show
= show
=== undefined ? !this.isVisible() : !!show
;
4331 change
= show
!== this.isVisible();
4334 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
4338 if ( this.autoClose
) {
4339 this.bindMouseDownListener();
4340 this.bindKeyDownListener();
4342 this.updateDimensions();
4343 this.toggleClipping( true );
4345 this.toggleClipping( false );
4346 if ( this.autoClose
) {
4347 this.unbindMouseDownListener();
4348 this.unbindKeyDownListener();
4357 * Set the size of the popup.
4359 * Changing the size may also change the popup's position depending on the alignment.
4361 * @param {number} width Width in pixels
4362 * @param {number} height Height in pixels
4363 * @param {boolean} [transition=false] Use a smooth transition
4366 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
4368 this.height
= height
!== undefined ? height
: null;
4369 if ( this.isVisible() ) {
4370 this.updateDimensions( transition
);
4375 * Update the size and position.
4377 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
4378 * be called automatically.
4380 * @param {boolean} [transition=false] Use a smooth transition
4383 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
4384 var popupOffset
, originOffset
, containerLeft
, containerWidth
, containerRight
,
4385 popupLeft
, popupRight
, overlapLeft
, overlapRight
, anchorWidth
,
4389 if ( !this.$container
) {
4390 // Lazy-initialize $container if not specified in constructor
4391 this.$container
= $( this.getClosestScrollableElementContainer() );
4394 // Set height and width before measuring things, since it might cause our measurements
4395 // to change (e.g. due to scrollbars appearing or disappearing)
4398 height
: this.height
!== null ? this.height
: 'auto'
4401 // If we are in RTL, we need to flip the alignment, unless it is center
4402 if ( align
=== 'forwards' || align
=== 'backwards' ) {
4403 if ( this.$container
.css( 'direction' ) === 'rtl' ) {
4404 align
= ( { forwards
: 'force-left', backwards
: 'force-right' } )[ this.align
];
4406 align
= ( { forwards
: 'force-right', backwards
: 'force-left' } )[ this.align
];
4411 // Compute initial popupOffset based on alignment
4412 popupOffset
= this.width
* ( { 'force-left': -1, center
: -0.5, 'force-right': 0 } )[ align
];
4414 // Figure out if this will cause the popup to go beyond the edge of the container
4415 originOffset
= this.$element
.offset().left
;
4416 containerLeft
= this.$container
.offset().left
;
4417 containerWidth
= this.$container
.innerWidth();
4418 containerRight
= containerLeft
+ containerWidth
;
4419 popupLeft
= popupOffset
- this.containerPadding
;
4420 popupRight
= popupOffset
+ this.containerPadding
+ this.width
+ this.containerPadding
;
4421 overlapLeft
= ( originOffset
+ popupLeft
) - containerLeft
;
4422 overlapRight
= containerRight
- ( originOffset
+ popupRight
);
4424 // Adjust offset to make the popup not go beyond the edge, if needed
4425 if ( overlapRight
< 0 ) {
4426 popupOffset
+= overlapRight
;
4427 } else if ( overlapLeft
< 0 ) {
4428 popupOffset
-= overlapLeft
;
4431 // Adjust offset to avoid anchor being rendered too close to the edge
4432 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
4433 // TODO: Find a measurement that works for CSS anchors and image anchors
4434 anchorWidth
= this.$anchor
[ 0 ].scrollWidth
* 2;
4435 if ( popupOffset
+ this.width
< anchorWidth
) {
4436 popupOffset
= anchorWidth
- this.width
;
4437 } else if ( -popupOffset
< anchorWidth
) {
4438 popupOffset
= -anchorWidth
;
4441 // Prevent transition from being interrupted
4442 clearTimeout( this.transitionTimeout
);
4444 // Enable transition
4445 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
4448 // Position body relative to anchor
4449 this.$popup
.css( 'margin-left', popupOffset
);
4452 // Prevent transitioning after transition is complete
4453 this.transitionTimeout
= setTimeout( function () {
4454 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4457 // Prevent transitioning immediately
4458 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4461 // Reevaluate clipping state since we've relocated and resized the popup
4468 * Set popup alignment
4469 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4470 * `backwards` or `forwards`.
4472 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
4473 // Validate alignment and transform deprecated values
4474 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
4475 this.align
= { left
: 'force-right', right
: 'force-left' }[ align
] || align
;
4477 this.align
= 'center';
4482 * Get popup alignment
4483 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4484 * `backwards` or `forwards`.
4486 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
4491 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
4492 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
4493 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
4494 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
4500 * @param {Object} [config] Configuration options
4501 * @cfg {Object} [popup] Configuration to pass to popup
4502 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
4504 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
4505 // Configuration initialization
4506 config
= config
|| {};
4509 this.popup
= new OO
.ui
.PopupWidget( $.extend(
4510 { autoClose
: true },
4512 { $autoCloseIgnore
: this.$element
}
4521 * @return {OO.ui.PopupWidget} Popup widget
4523 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
4528 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
4529 * which is used to display additional information or options.
4532 * // Example of a popup button.
4533 * var popupButton = new OO.ui.PopupButtonWidget( {
4534 * label: 'Popup button with options',
4537 * $content: $( '<p>Additional options here.</p>' ),
4539 * align: 'force-left'
4542 * // Append the button to the DOM.
4543 * $( 'body' ).append( popupButton.$element );
4546 * @extends OO.ui.ButtonWidget
4547 * @mixins OO.ui.mixin.PopupElement
4550 * @param {Object} [config] Configuration options
4552 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
4553 // Parent constructor
4554 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
4556 // Mixin constructors
4557 OO
.ui
.mixin
.PopupElement
.call( this, config
);
4560 this.connect( this, { click
: 'onAction' } );
4564 .addClass( 'oo-ui-popupButtonWidget' )
4565 .attr( 'aria-haspopup', 'true' )
4566 .append( this.popup
.$element
);
4571 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
4572 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
4577 * Handle the button action being triggered.
4581 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
4582 this.popup
.toggle();
4586 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
4588 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
4593 * @extends OO.ui.mixin.GroupElement
4596 * @param {Object} [config] Configuration options
4598 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
4599 // Parent constructor
4600 OO
.ui
.mixin
.GroupWidget
.parent
.call( this, config
);
4605 OO
.inheritClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
4610 * Set the disabled state of the widget.
4612 * This will also update the disabled state of child widgets.
4614 * @param {boolean} disabled Disable widget
4617 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
4621 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
4622 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
4624 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
4626 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4627 this.items
[ i
].updateDisabled();
4635 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
4637 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
4638 * allows bidirectional communication.
4640 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
4648 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
4655 * Check if widget is disabled.
4657 * Checks parent if present, making disabled state inheritable.
4659 * @return {boolean} Widget is disabled
4661 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
4662 return this.disabled
||
4663 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
4667 * Set group element is in.
4669 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
4672 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
4674 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
4675 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
4677 // Initialize item disabled states
4678 this.updateDisabled();
4684 * OptionWidgets are special elements that can be selected and configured with data. The
4685 * data is often unique for each option, but it does not have to be. OptionWidgets are used
4686 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
4687 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
4689 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4692 * @extends OO.ui.Widget
4693 * @mixins OO.ui.mixin.LabelElement
4694 * @mixins OO.ui.mixin.FlaggedElement
4697 * @param {Object} [config] Configuration options
4699 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
4700 // Configuration initialization
4701 config
= config
|| {};
4703 // Parent constructor
4704 OO
.ui
.OptionWidget
.parent
.call( this, config
);
4706 // Mixin constructors
4707 OO
.ui
.mixin
.ItemWidget
.call( this );
4708 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4709 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
4712 this.selected
= false;
4713 this.highlighted
= false;
4714 this.pressed
= false;
4718 .data( 'oo-ui-optionWidget', this )
4719 .attr( 'role', 'option' )
4720 .attr( 'aria-selected', 'false' )
4721 .addClass( 'oo-ui-optionWidget' )
4722 .append( this.$label
);
4727 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
4728 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
4729 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
4730 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
4732 /* Static Properties */
4734 OO
.ui
.OptionWidget
.static.selectable
= true;
4736 OO
.ui
.OptionWidget
.static.highlightable
= true;
4738 OO
.ui
.OptionWidget
.static.pressable
= true;
4740 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
4745 * Check if the option can be selected.
4747 * @return {boolean} Item is selectable
4749 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
4750 return this.constructor.static.selectable
&& !this.isDisabled() && this.isVisible();
4754 * Check if the option can be highlighted. A highlight indicates that the option
4755 * may be selected when a user presses enter or clicks. Disabled items cannot
4758 * @return {boolean} Item is highlightable
4760 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
4761 return this.constructor.static.highlightable
&& !this.isDisabled() && this.isVisible();
4765 * Check if the option can be pressed. The pressed state occurs when a user mouses
4766 * down on an item, but has not yet let go of the mouse.
4768 * @return {boolean} Item is pressable
4770 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
4771 return this.constructor.static.pressable
&& !this.isDisabled() && this.isVisible();
4775 * Check if the option is selected.
4777 * @return {boolean} Item is selected
4779 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
4780 return this.selected
;
4784 * Check if the option is highlighted. A highlight indicates that the
4785 * item may be selected when a user presses enter or clicks.
4787 * @return {boolean} Item is highlighted
4789 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
4790 return this.highlighted
;
4794 * Check if the option is pressed. The pressed state occurs when a user mouses
4795 * down on an item, but has not yet let go of the mouse. The item may appear
4796 * selected, but it will not be selected until the user releases the mouse.
4798 * @return {boolean} Item is pressed
4800 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
4801 return this.pressed
;
4805 * Set the option’s selected state. In general, all modifications to the selection
4806 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
4807 * method instead of this method.
4809 * @param {boolean} [state=false] Select option
4812 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
4813 if ( this.constructor.static.selectable
) {
4814 this.selected
= !!state
;
4816 .toggleClass( 'oo-ui-optionWidget-selected', state
)
4817 .attr( 'aria-selected', state
.toString() );
4818 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
4819 this.scrollElementIntoView();
4821 this.updateThemeClasses();
4827 * Set the option’s highlighted state. In general, all programmatic
4828 * modifications to the highlight should be handled by the
4829 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
4830 * method instead of this method.
4832 * @param {boolean} [state=false] Highlight option
4835 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
4836 if ( this.constructor.static.highlightable
) {
4837 this.highlighted
= !!state
;
4838 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
4839 this.updateThemeClasses();
4845 * Set the option’s pressed state. In general, all
4846 * programmatic modifications to the pressed state should be handled by the
4847 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
4848 * method instead of this method.
4850 * @param {boolean} [state=false] Press option
4853 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
4854 if ( this.constructor.static.pressable
) {
4855 this.pressed
= !!state
;
4856 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
4857 this.updateThemeClasses();
4863 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
4864 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
4865 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
4868 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
4869 * information, please see the [OOjs UI documentation on MediaWiki][1].
4872 * // Example of a select widget with three options
4873 * var select = new OO.ui.SelectWidget( {
4875 * new OO.ui.OptionWidget( {
4877 * label: 'Option One',
4879 * new OO.ui.OptionWidget( {
4881 * label: 'Option Two',
4883 * new OO.ui.OptionWidget( {
4885 * label: 'Option Three',
4889 * $( 'body' ).append( select.$element );
4891 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4895 * @extends OO.ui.Widget
4896 * @mixins OO.ui.mixin.GroupWidget
4899 * @param {Object} [config] Configuration options
4900 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
4901 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
4902 * the [OOjs UI documentation on MediaWiki] [2] for examples.
4903 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4905 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
4906 // Configuration initialization
4907 config
= config
|| {};
4909 // Parent constructor
4910 OO
.ui
.SelectWidget
.parent
.call( this, config
);
4912 // Mixin constructors
4913 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
4916 this.pressed
= false;
4917 this.selecting
= null;
4918 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
4919 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
4920 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
4921 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
4922 this.keyPressBuffer
= '';
4923 this.keyPressBufferTimer
= null;
4924 this.blockMouseOverEvents
= 0;
4927 this.connect( this, {
4931 mousedown
: this.onMouseDown
.bind( this ),
4932 mouseover
: this.onMouseOver
.bind( this ),
4933 mouseleave
: this.onMouseLeave
.bind( this )
4938 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
4939 .attr( 'role', 'listbox' );
4940 if ( Array
.isArray( config
.items
) ) {
4941 this.addItems( config
.items
);
4947 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
4949 // Need to mixin base class as well
4950 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupElement
);
4951 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
4954 OO
.ui
.SelectWidget
.static.passAllFilter = function () {
4963 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
4965 * @param {OO.ui.OptionWidget|null} item Highlighted item
4971 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
4972 * pressed state of an option.
4974 * @param {OO.ui.OptionWidget|null} item Pressed item
4980 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
4982 * @param {OO.ui.OptionWidget|null} item Selected item
4987 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
4988 * @param {OO.ui.OptionWidget} item Chosen item
4994 * An `add` event is emitted when options are added to the select with the #addItems method.
4996 * @param {OO.ui.OptionWidget[]} items Added items
4997 * @param {number} index Index of insertion point
5003 * A `remove` event is emitted when options are removed from the select with the #clearItems
5004 * or #removeItems methods.
5006 * @param {OO.ui.OptionWidget[]} items Removed items
5012 * Handle mouse down events.
5015 * @param {jQuery.Event} e Mouse down event
5017 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
5020 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
5021 this.togglePressed( true );
5022 item
= this.getTargetItem( e
);
5023 if ( item
&& item
.isSelectable() ) {
5024 this.pressItem( item
);
5025 this.selecting
= item
;
5026 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
5027 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5034 * Handle mouse up events.
5037 * @param {MouseEvent} e Mouse up event
5039 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
5042 this.togglePressed( false );
5043 if ( !this.selecting
) {
5044 item
= this.getTargetItem( e
);
5045 if ( item
&& item
.isSelectable() ) {
5046 this.selecting
= item
;
5049 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
5050 this.pressItem( null );
5051 this.chooseItem( this.selecting
);
5052 this.selecting
= null;
5055 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
5056 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5062 * Handle mouse move events.
5065 * @param {MouseEvent} e Mouse move event
5067 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
5070 if ( !this.isDisabled() && this.pressed
) {
5071 item
= this.getTargetItem( e
);
5072 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
5073 this.pressItem( item
);
5074 this.selecting
= item
;
5080 * Handle mouse over events.
5083 * @param {jQuery.Event} e Mouse over event
5085 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
5087 if ( this.blockMouseOverEvents
) {
5090 if ( !this.isDisabled() ) {
5091 item
= this.getTargetItem( e
);
5092 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
5098 * Handle mouse leave events.
5101 * @param {jQuery.Event} e Mouse over event
5103 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
5104 if ( !this.isDisabled() ) {
5105 this.highlightItem( null );
5111 * Handle key down events.
5114 * @param {KeyboardEvent} e Key down event
5116 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
5119 currentItem
= this.getHighlightedItem() || this.getSelectedItem();
5121 if ( !this.isDisabled() && this.isVisible() ) {
5122 switch ( e
.keyCode
) {
5123 case OO
.ui
.Keys
.ENTER
:
5124 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5125 // Was only highlighted, now let's select it. No-op if already selected.
5126 this.chooseItem( currentItem
);
5131 case OO
.ui
.Keys
.LEFT
:
5132 this.clearKeyPressBuffer();
5133 nextItem
= this.getRelativeSelectableItem( currentItem
, -1 );
5136 case OO
.ui
.Keys
.DOWN
:
5137 case OO
.ui
.Keys
.RIGHT
:
5138 this.clearKeyPressBuffer();
5139 nextItem
= this.getRelativeSelectableItem( currentItem
, 1 );
5142 case OO
.ui
.Keys
.ESCAPE
:
5143 case OO
.ui
.Keys
.TAB
:
5144 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5145 currentItem
.setHighlighted( false );
5147 this.unbindKeyDownListener();
5148 this.unbindKeyPressListener();
5149 // Don't prevent tabbing away / defocusing
5155 if ( nextItem
.constructor.static.highlightable
) {
5156 this.highlightItem( nextItem
);
5158 this.chooseItem( nextItem
);
5160 this.scrollItemIntoView( nextItem
);
5165 e
.stopPropagation();
5171 * Bind key down listener.
5175 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
5176 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
5180 * Unbind key down listener.
5184 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
5185 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
5189 * Scroll item into view, preventing spurious mouse highlight actions from happening.
5191 * @return {OO.ui.OptionWidget} Item to scroll into view
5193 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
5195 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
5196 // and around 100-150 ms after it is finished.
5197 this.blockMouseOverEvents
++;
5198 item
.scrollElementIntoView().done( function () {
5199 setTimeout( function () {
5200 widget
.blockMouseOverEvents
--;
5206 * Clear the key-press buffer
5210 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
5211 if ( this.keyPressBufferTimer
) {
5212 clearTimeout( this.keyPressBufferTimer
);
5213 this.keyPressBufferTimer
= null;
5215 this.keyPressBuffer
= '';
5219 * Handle key press events.
5222 * @param {KeyboardEvent} e Key press event
5224 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
5225 var c
, filter
, item
;
5227 if ( !e
.charCode
) {
5228 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
5229 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
5234 if ( String
.fromCodePoint
) {
5235 c
= String
.fromCodePoint( e
.charCode
);
5237 c
= String
.fromCharCode( e
.charCode
);
5240 if ( this.keyPressBufferTimer
) {
5241 clearTimeout( this.keyPressBufferTimer
);
5243 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
5245 item
= this.getHighlightedItem() || this.getSelectedItem();
5247 if ( this.keyPressBuffer
=== c
) {
5248 // Common (if weird) special case: typing "xxxx" will cycle through all
5249 // the items beginning with "x".
5251 item
= this.getRelativeSelectableItem( item
, 1 );
5254 this.keyPressBuffer
+= c
;
5257 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
5258 if ( !item
|| !filter( item
) ) {
5259 item
= this.getRelativeSelectableItem( item
, 1, filter
);
5262 if ( item
.constructor.static.highlightable
) {
5263 this.highlightItem( item
);
5265 this.chooseItem( item
);
5267 this.scrollItemIntoView( item
);
5271 e
.stopPropagation();
5275 * Get a matcher for the specific string
5278 * @param {string} s String to match against items
5279 * @param {boolean} [exact=false] Only accept exact matches
5280 * @return {Function} function ( OO.ui.OptionItem ) => boolean
5282 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
5285 if ( s
.normalize
) {
5288 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
5289 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
5293 re
= new RegExp( re
, 'i' );
5294 return function ( item
) {
5295 var l
= item
.getLabel();
5296 if ( typeof l
!== 'string' ) {
5297 l
= item
.$label
.text();
5299 if ( l
.normalize
) {
5302 return re
.test( l
);
5307 * Bind key press listener.
5311 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
5312 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
5316 * Unbind key down listener.
5318 * If you override this, be sure to call this.clearKeyPressBuffer() from your
5323 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
5324 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
5325 this.clearKeyPressBuffer();
5329 * Visibility change handler
5332 * @param {boolean} visible
5334 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
5336 this.clearKeyPressBuffer();
5341 * Get the closest item to a jQuery.Event.
5344 * @param {jQuery.Event} e
5345 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
5347 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
5348 return $( e
.target
).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
5352 * Get selected item.
5354 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
5356 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
5359 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5360 if ( this.items
[ i
].isSelected() ) {
5361 return this.items
[ i
];
5368 * Get highlighted item.
5370 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
5372 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
5375 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5376 if ( this.items
[ i
].isHighlighted() ) {
5377 return this.items
[ i
];
5384 * Toggle pressed state.
5386 * Press is a state that occurs when a user mouses down on an item, but
5387 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
5388 * until the user releases the mouse.
5390 * @param {boolean} pressed An option is being pressed
5392 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
5393 if ( pressed
=== undefined ) {
5394 pressed
= !this.pressed
;
5396 if ( pressed
!== this.pressed
) {
5398 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
5399 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
5400 this.pressed
= pressed
;
5405 * Highlight an option. If the `item` param is omitted, no options will be highlighted
5406 * and any existing highlight will be removed. The highlight is mutually exclusive.
5408 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
5412 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
5413 var i
, len
, highlighted
,
5416 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5417 highlighted
= this.items
[ i
] === item
;
5418 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
5419 this.items
[ i
].setHighlighted( highlighted
);
5424 this.emit( 'highlight', item
);
5431 * Fetch an item by its label.
5433 * @param {string} label Label of the item to select.
5434 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5435 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
5437 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
5439 len
= this.items
.length
,
5440 filter
= this.getItemMatcher( label
, true );
5442 for ( i
= 0; i
< len
; i
++ ) {
5443 item
= this.items
[ i
];
5444 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5451 filter
= this.getItemMatcher( label
, false );
5452 for ( i
= 0; i
< len
; i
++ ) {
5453 item
= this.items
[ i
];
5454 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5470 * Programmatically select an option by its label. If the item does not exist,
5471 * all options will be deselected.
5473 * @param {string} [label] Label of the item to select.
5474 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5478 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
5479 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
5480 if ( label
=== undefined || !itemFromLabel
) {
5481 return this.selectItem();
5483 return this.selectItem( itemFromLabel
);
5487 * Programmatically select an option by its data. If the `data` parameter is omitted,
5488 * or if the item does not exist, all options will be deselected.
5490 * @param {Object|string} [data] Value of the item to select, omit to deselect all
5494 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
5495 var itemFromData
= this.getItemFromData( data
);
5496 if ( data
=== undefined || !itemFromData
) {
5497 return this.selectItem();
5499 return this.selectItem( itemFromData
);
5503 * Programmatically select an option by its reference. If the `item` parameter is omitted,
5504 * all options will be deselected.
5506 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
5510 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
5511 var i
, len
, selected
,
5514 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5515 selected
= this.items
[ i
] === item
;
5516 if ( this.items
[ i
].isSelected() !== selected
) {
5517 this.items
[ i
].setSelected( selected
);
5522 this.emit( 'select', item
);
5531 * Press is a state that occurs when a user mouses down on an item, but has not
5532 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
5533 * releases the mouse.
5535 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
5539 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
5540 var i
, len
, pressed
,
5543 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5544 pressed
= this.items
[ i
] === item
;
5545 if ( this.items
[ i
].isPressed() !== pressed
) {
5546 this.items
[ i
].setPressed( pressed
);
5551 this.emit( 'press', item
);
5560 * Note that ‘choose’ should never be modified programmatically. A user can choose
5561 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
5562 * use the #selectItem method.
5564 * This method is identical to #selectItem, but may vary in subclasses that take additional action
5565 * when users choose an item with the keyboard or mouse.
5567 * @param {OO.ui.OptionWidget} item Item to choose
5571 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
5573 this.selectItem( item
);
5574 this.emit( 'choose', item
);
5581 * Get an option by its position relative to the specified item (or to the start of the option array,
5582 * if item is `null`). The direction in which to search through the option array is specified with a
5583 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
5584 * `null` if there are no options in the array.
5586 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
5587 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
5588 * @param {Function} filter Only consider items for which this function returns
5589 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
5590 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
5592 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
, filter
) {
5593 var currentIndex
, nextIndex
, i
,
5594 increase
= direction
> 0 ? 1 : -1,
5595 len
= this.items
.length
;
5597 if ( !$.isFunction( filter
) ) {
5598 filter
= OO
.ui
.SelectWidget
.static.passAllFilter
;
5601 if ( item
instanceof OO
.ui
.OptionWidget
) {
5602 currentIndex
= this.items
.indexOf( item
);
5603 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
5605 // If no item is selected and moving forward, start at the beginning.
5606 // If moving backward, start at the end.
5607 nextIndex
= direction
> 0 ? 0 : len
- 1;
5610 for ( i
= 0; i
< len
; i
++ ) {
5611 item
= this.items
[ nextIndex
];
5612 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5615 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
5621 * Get the next selectable item or `null` if there are no selectable items.
5622 * Disabled options and menu-section markers and breaks are not selectable.
5624 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
5626 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
5629 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5630 item
= this.items
[ i
];
5631 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() ) {
5640 * Add an array of options to the select. Optionally, an index number can be used to
5641 * specify an insertion point.
5643 * @param {OO.ui.OptionWidget[]} items Items to add
5644 * @param {number} [index] Index to insert items after
5648 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
5650 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
5652 // Always provide an index, even if it was omitted
5653 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
5659 * Remove the specified array of options from the select. Options will be detached
5660 * from the DOM, not removed, so they can be reused later. To remove all options from
5661 * the select, you may wish to use the #clearItems method instead.
5663 * @param {OO.ui.OptionWidget[]} items Items to remove
5667 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
5670 // Deselect items being removed
5671 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
5673 if ( item
.isSelected() ) {
5674 this.selectItem( null );
5679 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
5681 this.emit( 'remove', items
);
5687 * Clear all options from the select. Options will be detached from the DOM, not removed,
5688 * so that they can be reused later. To remove a subset of options from the select, use
5689 * the #removeItems method.
5694 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
5695 var items
= this.items
.slice();
5698 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
5701 this.selectItem( null );
5703 this.emit( 'remove', items
);
5709 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
5710 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
5711 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
5712 * options. For more information about options and selects, please see the
5713 * [OOjs UI documentation on MediaWiki][1].
5716 * // Decorated options in a select widget
5717 * var select = new OO.ui.SelectWidget( {
5719 * new OO.ui.DecoratedOptionWidget( {
5721 * label: 'Option with icon',
5724 * new OO.ui.DecoratedOptionWidget( {
5726 * label: 'Option with indicator',
5731 * $( 'body' ).append( select.$element );
5733 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5736 * @extends OO.ui.OptionWidget
5737 * @mixins OO.ui.mixin.IconElement
5738 * @mixins OO.ui.mixin.IndicatorElement
5741 * @param {Object} [config] Configuration options
5743 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
5744 // Parent constructor
5745 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
5747 // Mixin constructors
5748 OO
.ui
.mixin
.IconElement
.call( this, config
);
5749 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
5753 .addClass( 'oo-ui-decoratedOptionWidget' )
5754 .prepend( this.$icon
)
5755 .append( this.$indicator
);
5760 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
5761 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
5762 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
5765 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
5766 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
5767 * the [OOjs UI documentation on MediaWiki] [1] for more information.
5769 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
5772 * @extends OO.ui.DecoratedOptionWidget
5775 * @param {Object} [config] Configuration options
5777 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
5778 // Configuration initialization
5779 config
= $.extend( { icon
: 'check' }, config
);
5781 // Parent constructor
5782 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
5786 .attr( 'role', 'menuitem' )
5787 .addClass( 'oo-ui-menuOptionWidget' );
5792 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
5794 /* Static Properties */
5796 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
5799 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
5800 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
5803 * var myDropdown = new OO.ui.DropdownWidget( {
5806 * new OO.ui.MenuSectionOptionWidget( {
5809 * new OO.ui.MenuOptionWidget( {
5811 * label: 'Welsh Corgi'
5813 * new OO.ui.MenuOptionWidget( {
5815 * label: 'Standard Poodle'
5817 * new OO.ui.MenuSectionOptionWidget( {
5820 * new OO.ui.MenuOptionWidget( {
5827 * $( 'body' ).append( myDropdown.$element );
5830 * @extends OO.ui.DecoratedOptionWidget
5833 * @param {Object} [config] Configuration options
5835 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
5836 // Parent constructor
5837 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
5840 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' );
5845 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
5847 /* Static Properties */
5849 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
5851 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
5854 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
5855 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
5856 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
5857 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
5858 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
5859 * and customized to be opened, closed, and displayed as needed.
5861 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
5862 * mouse outside the menu.
5864 * Menus also have support for keyboard interaction:
5866 * - Enter/Return key: choose and select a menu option
5867 * - Up-arrow key: highlight the previous menu option
5868 * - Down-arrow key: highlight the next menu option
5869 * - Esc key: hide the menu
5871 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
5872 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5875 * @extends OO.ui.SelectWidget
5876 * @mixins OO.ui.mixin.ClippableElement
5879 * @param {Object} [config] Configuration options
5880 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
5881 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
5882 * and {@link OO.ui.mixin.LookupElement LookupElement}
5883 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
5884 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiSelectWidget CapsuleMultiSelectWidget}
5885 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
5886 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
5887 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
5888 * that button, unless the button (or its parent widget) is passed in here.
5889 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
5890 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
5892 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
5893 // Configuration initialization
5894 config
= config
|| {};
5896 // Parent constructor
5897 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
5899 // Mixin constructors
5900 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
5903 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
5904 this.filterFromInput
= !!config
.filterFromInput
;
5905 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
5906 this.$widget
= config
.widget
? config
.widget
.$element
: null;
5907 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5908 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
5912 .addClass( 'oo-ui-menuSelectWidget' )
5913 .attr( 'role', 'menu' );
5915 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5916 // that reference properties not initialized at that time of parent class construction
5917 // TODO: Find a better way to handle post-constructor setup
5918 this.visible
= false;
5919 this.$element
.addClass( 'oo-ui-element-hidden' );
5924 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
5925 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
5930 * Handles document mouse down events.
5933 * @param {MouseEvent} e Mouse down event
5935 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
5937 !OO
.ui
.contains( this.$element
[ 0 ], e
.target
, true ) &&
5938 ( !this.$widget
|| !OO
.ui
.contains( this.$widget
[ 0 ], e
.target
, true ) )
5940 this.toggle( false );
5947 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
5948 var currentItem
= this.getHighlightedItem() || this.getSelectedItem();
5950 if ( !this.isDisabled() && this.isVisible() ) {
5951 switch ( e
.keyCode
) {
5952 case OO
.ui
.Keys
.LEFT
:
5953 case OO
.ui
.Keys
.RIGHT
:
5954 // Do nothing if a text field is associated, arrow keys will be handled natively
5955 if ( !this.$input
) {
5956 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
5959 case OO
.ui
.Keys
.ESCAPE
:
5960 case OO
.ui
.Keys
.TAB
:
5961 if ( currentItem
) {
5962 currentItem
.setHighlighted( false );
5964 this.toggle( false );
5965 // Don't prevent tabbing away, prevent defocusing
5966 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
5968 e
.stopPropagation();
5972 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
5979 * Update menu item visibility after input changes.
5982 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
5984 len
= this.items
.length
,
5985 showAll
= !this.isVisible(),
5986 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
5988 for ( i
= 0; i
< len
; i
++ ) {
5989 item
= this.items
[ i
];
5990 if ( item
instanceof OO
.ui
.OptionWidget
) {
5991 item
.toggle( showAll
|| filter( item
) );
5995 // Reevaluate clipping
6002 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
6003 if ( this.$input
) {
6004 this.$input
.on( 'keydown', this.onKeyDownHandler
);
6006 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
6013 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
6014 if ( this.$input
) {
6015 this.$input
.off( 'keydown', this.onKeyDownHandler
);
6017 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
6024 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
6025 if ( this.$input
) {
6026 if ( this.filterFromInput
) {
6027 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6030 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
6037 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
6038 if ( this.$input
) {
6039 if ( this.filterFromInput
) {
6040 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6041 this.updateItemVisibility();
6044 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
6051 * When a user chooses an item, the menu is closed.
6053 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
6054 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
6055 * @param {OO.ui.OptionWidget} item Item to choose
6058 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
6059 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
6060 this.toggle( false );
6067 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
6069 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
6071 // Reevaluate clipping
6080 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
6082 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
6084 // Reevaluate clipping
6093 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
6095 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
6097 // Reevaluate clipping
6106 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
6109 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
6110 change
= visible
!== this.isVisible();
6113 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
6117 this.bindKeyDownListener();
6118 this.bindKeyPressListener();
6120 this.toggleClipping( true );
6122 if ( this.getSelectedItem() ) {
6123 this.getSelectedItem().scrollElementIntoView( { duration
: 0 } );
6127 if ( this.autoHide
) {
6128 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6131 this.unbindKeyDownListener();
6132 this.unbindKeyPressListener();
6133 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6134 this.toggleClipping( false );
6142 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
6143 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
6144 * users can interact with it.
6146 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6147 * OO.ui.DropdownInputWidget instead.
6150 * // Example: A DropdownWidget with a menu that contains three options
6151 * var dropDown = new OO.ui.DropdownWidget( {
6152 * label: 'Dropdown menu: Select a menu option',
6155 * new OO.ui.MenuOptionWidget( {
6159 * new OO.ui.MenuOptionWidget( {
6163 * new OO.ui.MenuOptionWidget( {
6171 * $( 'body' ).append( dropDown.$element );
6173 * dropDown.getMenu().selectItemByData( 'b' );
6175 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
6177 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
6179 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6182 * @extends OO.ui.Widget
6183 * @mixins OO.ui.mixin.IconElement
6184 * @mixins OO.ui.mixin.IndicatorElement
6185 * @mixins OO.ui.mixin.LabelElement
6186 * @mixins OO.ui.mixin.TitledElement
6187 * @mixins OO.ui.mixin.TabIndexedElement
6190 * @param {Object} [config] Configuration options
6191 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
6192 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
6193 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6194 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
6196 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
6197 // Configuration initialization
6198 config
= $.extend( { indicator
: 'down' }, config
);
6200 // Parent constructor
6201 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
6203 // Properties (must be set before TabIndexedElement constructor call)
6204 this.$handle
= this.$( '<span>' );
6205 this.$overlay
= config
.$overlay
|| this.$element
;
6207 // Mixin constructors
6208 OO
.ui
.mixin
.IconElement
.call( this, config
);
6209 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
6210 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6211 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
6212 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
6215 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend( {
6217 $container
: this.$element
6222 click
: this.onClick
.bind( this ),
6223 keydown
: this.onKeyDown
.bind( this )
6225 this.menu
.connect( this, { select
: 'onMenuSelect' } );
6229 .addClass( 'oo-ui-dropdownWidget-handle' )
6230 .append( this.$icon
, this.$label
, this.$indicator
);
6232 .addClass( 'oo-ui-dropdownWidget' )
6233 .append( this.$handle
);
6234 this.$overlay
.append( this.menu
.$element
);
6239 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
6240 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
6241 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
6242 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
6243 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
6244 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6251 * @return {OO.ui.MenuSelectWidget} Menu of widget
6253 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
6258 * Handles menu select events.
6261 * @param {OO.ui.MenuOptionWidget} item Selected menu item
6263 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
6267 this.setLabel( null );
6271 selectedLabel
= item
.getLabel();
6273 // If the label is a DOM element, clone it, because setLabel will append() it
6274 if ( selectedLabel
instanceof jQuery
) {
6275 selectedLabel
= selectedLabel
.clone();
6278 this.setLabel( selectedLabel
);
6282 * Handle mouse click events.
6285 * @param {jQuery.Event} e Mouse click event
6287 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
6288 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6295 * Handle key down events.
6298 * @param {jQuery.Event} e Key down event
6300 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
6302 !this.isDisabled() &&
6304 e
.which
=== OO
.ui
.Keys
.ENTER
||
6306 !this.menu
.isVisible() &&
6308 e
.which
=== OO
.ui
.Keys
.SPACE
||
6309 e
.which
=== OO
.ui
.Keys
.UP
||
6310 e
.which
=== OO
.ui
.Keys
.DOWN
6321 * RadioOptionWidget is an option widget that looks like a radio button.
6322 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
6323 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6325 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6328 * @extends OO.ui.OptionWidget
6331 * @param {Object} [config] Configuration options
6333 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
6334 // Configuration initialization
6335 config
= config
|| {};
6337 // Properties (must be done before parent constructor which calls #setDisabled)
6338 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
6340 // Parent constructor
6341 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
6344 this.radio
.$input
.on( 'focus', this.onInputFocus
.bind( this ) );
6347 // Remove implicit role, we're handling it ourselves
6348 this.radio
.$input
.attr( 'role', 'presentation' );
6350 .addClass( 'oo-ui-radioOptionWidget' )
6351 .attr( 'role', 'radio' )
6352 .attr( 'aria-checked', 'false' )
6353 .removeAttr( 'aria-selected' )
6354 .prepend( this.radio
.$element
);
6359 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
6361 /* Static Properties */
6363 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
6365 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
6367 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
6369 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
6374 * @param {jQuery.Event} e Focus event
6377 OO
.ui
.RadioOptionWidget
.prototype.onInputFocus = function () {
6378 this.radio
.$input
.blur();
6379 this.$element
.parent().focus();
6385 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
6386 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
6388 this.radio
.setSelected( state
);
6390 .attr( 'aria-checked', state
.toString() )
6391 .removeAttr( 'aria-selected' );
6399 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
6400 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
6402 this.radio
.setDisabled( this.isDisabled() );
6408 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
6409 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
6410 * an interface for adding, removing and selecting options.
6411 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6413 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6414 * OO.ui.RadioSelectInputWidget instead.
6417 * // A RadioSelectWidget with RadioOptions.
6418 * var option1 = new OO.ui.RadioOptionWidget( {
6420 * label: 'Selected radio option'
6423 * var option2 = new OO.ui.RadioOptionWidget( {
6425 * label: 'Unselected radio option'
6428 * var radioSelect=new OO.ui.RadioSelectWidget( {
6429 * items: [ option1, option2 ]
6432 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
6433 * radioSelect.selectItem( option1 );
6435 * $( 'body' ).append( radioSelect.$element );
6437 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6441 * @extends OO.ui.SelectWidget
6442 * @mixins OO.ui.mixin.TabIndexedElement
6445 * @param {Object} [config] Configuration options
6447 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
6448 // Parent constructor
6449 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
6451 // Mixin constructors
6452 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
6456 focus
: this.bindKeyDownListener
.bind( this ),
6457 blur
: this.unbindKeyDownListener
.bind( this )
6462 .addClass( 'oo-ui-radioSelectWidget' )
6463 .attr( 'role', 'radiogroup' );
6468 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
6469 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6472 * Element that will stick under a specified container, even when it is inserted elsewhere in the
6473 * document (for example, in a OO.ui.Window's $overlay).
6475 * The elements's position is automatically calculated and maintained when window is resized or the
6476 * page is scrolled. If you reposition the container manually, you have to call #position to make
6477 * sure the element is still placed correctly.
6479 * As positioning is only possible when both the element and the container are attached to the DOM
6480 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
6481 * the #toggle method to display a floating popup, for example.
6487 * @param {Object} [config] Configuration options
6488 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
6489 * @cfg {jQuery} [$floatableContainer] Node to position below
6491 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
6492 // Configuration initialization
6493 config
= config
|| {};
6496 this.$floatable
= null;
6497 this.$floatableContainer
= null;
6498 this.$floatableWindow
= null;
6499 this.$floatableClosestScrollable
= null;
6500 this.onFloatableScrollHandler
= this.position
.bind( this );
6501 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
6504 this.setFloatableContainer( config
.$floatableContainer
);
6505 this.setFloatableElement( config
.$floatable
|| this.$element
);
6511 * Set floatable element.
6513 * If an element is already set, it will be cleaned up before setting up the new element.
6515 * @param {jQuery} $floatable Element to make floatable
6517 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
6518 if ( this.$floatable
) {
6519 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
6520 this.$floatable
.css( { left
: '', top
: '' } );
6523 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
6528 * Set floatable container.
6530 * The element will be always positioned under the specified container.
6532 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
6534 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
6535 this.$floatableContainer
= $floatableContainer
;
6536 if ( this.$floatable
) {
6542 * Toggle positioning.
6544 * Do not turn positioning on until after the element is attached to the DOM and visible.
6546 * @param {boolean} [positioning] Enable positioning, omit to toggle
6549 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
6550 var closestScrollableOfContainer
, closestScrollableOfFloatable
;
6552 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
6554 if ( this.positioning
!== positioning
) {
6555 this.positioning
= positioning
;
6557 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
6558 closestScrollableOfFloatable
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatable
[ 0 ] );
6559 if ( closestScrollableOfContainer
!== closestScrollableOfFloatable
) {
6560 // If the scrollable is the root, we have to listen to scroll events
6561 // on the window because of browser inconsistencies (or do we? someone should verify this)
6562 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
6563 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
6567 if ( positioning
) {
6568 this.$floatableWindow
= $( this.getElementWindow() );
6569 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
6571 if ( closestScrollableOfContainer
!== closestScrollableOfFloatable
) {
6572 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
6573 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
6576 // Initial position after visible
6579 if ( this.$floatableWindow
) {
6580 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
6581 this.$floatableWindow
= null;
6584 if ( this.$floatableClosestScrollable
) {
6585 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
6586 this.$floatableClosestScrollable
= null;
6589 this.$floatable
.css( { left
: '', top
: '' } );
6597 * Position the floatable below its container.
6599 * This should only be done when both of them are attached to the DOM and visible.
6603 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
6606 if ( !this.positioning
) {
6610 pos
= OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, this.$floatable
.offsetParent() );
6612 // Position under container
6613 pos
.top
+= this.$floatableContainer
.height();
6614 this.$floatable
.css( pos
);
6616 // We updated the position, so re-evaluate the clipping state.
6617 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
6618 // will not notice the need to update itself.)
6619 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
6620 // it not listen to the right events in the right places?
6629 * FloatingMenuSelectWidget is a menu that will stick under a specified
6630 * container, even when it is inserted elsewhere in the document (for example,
6631 * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
6632 * menu from being clipped too aggresively.
6634 * The menu's position is automatically calculated and maintained when the menu
6635 * is toggled or the window is resized.
6637 * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
6640 * @extends OO.ui.MenuSelectWidget
6641 * @mixins OO.ui.mixin.FloatableElement
6644 * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
6645 * Deprecated, omit this parameter and specify `$container` instead.
6646 * @param {Object} [config] Configuration options
6647 * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
6649 OO
.ui
.FloatingMenuSelectWidget
= function OoUiFloatingMenuSelectWidget( inputWidget
, config
) {
6650 // Allow 'inputWidget' parameter and config for backwards compatibility
6651 if ( OO
.isPlainObject( inputWidget
) && config
=== undefined ) {
6652 config
= inputWidget
;
6653 inputWidget
= config
.inputWidget
;
6656 // Configuration initialization
6657 config
= config
|| {};
6659 // Parent constructor
6660 OO
.ui
.FloatingMenuSelectWidget
.parent
.call( this, config
);
6662 // Properties (must be set before mixin constructors)
6663 this.inputWidget
= inputWidget
; // For backwards compatibility
6664 this.$container
= config
.$container
|| this.inputWidget
.$element
;
6666 // Mixins constructors
6667 OO
.ui
.mixin
.FloatableElement
.call( this, $.extend( {}, config
, { $floatableContainer
: this.$container
} ) );
6670 this.$element
.addClass( 'oo-ui-floatingMenuSelectWidget' );
6671 // For backwards compatibility
6672 this.$element
.addClass( 'oo-ui-textInputMenuSelectWidget' );
6677 OO
.inheritClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.MenuSelectWidget
);
6678 OO
.mixinClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
6680 // For backwards compatibility
6681 OO
.ui
.TextInputMenuSelectWidget
= OO
.ui
.FloatingMenuSelectWidget
;
6688 OO
.ui
.FloatingMenuSelectWidget
.prototype.toggle = function ( visible
) {
6690 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
6691 change
= visible
!== this.isVisible();
6693 if ( change
&& visible
) {
6694 // Make sure the width is set before the parent method runs.
6695 this.setIdealSize( this.$container
.width() );
6699 // This will call this.clip(), which is nonsensical since we're not positioned yet...
6700 OO
.ui
.FloatingMenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
6703 this.togglePositioning( this.isVisible() );
6710 * InputWidget is the base class for all input widgets, which
6711 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
6712 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
6713 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
6715 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
6719 * @extends OO.ui.Widget
6720 * @mixins OO.ui.mixin.FlaggedElement
6721 * @mixins OO.ui.mixin.TabIndexedElement
6722 * @mixins OO.ui.mixin.TitledElement
6723 * @mixins OO.ui.mixin.AccessKeyedElement
6726 * @param {Object} [config] Configuration options
6727 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
6728 * @cfg {string} [value=''] The value of the input.
6729 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
6730 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
6731 * before it is accepted.
6733 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
6734 // Configuration initialization
6735 config
= config
|| {};
6737 // Parent constructor
6738 OO
.ui
.InputWidget
.parent
.call( this, config
);
6741 this.$input
= this.getInputElement( config
);
6743 this.inputFilter
= config
.inputFilter
;
6745 // Mixin constructors
6746 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6747 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
6748 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
6749 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
6752 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
6756 .addClass( 'oo-ui-inputWidget-input' )
6757 .attr( 'name', config
.name
)
6758 .prop( 'disabled', this.isDisabled() );
6760 .addClass( 'oo-ui-inputWidget' )
6761 .append( this.$input
);
6762 this.setValue( config
.value
);
6764 this.setDir( config
.dir
);
6770 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
6771 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
6772 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6773 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
6774 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6776 /* Static Properties */
6778 OO
.ui
.InputWidget
.static.supportsSimpleLabel
= true;
6780 /* Static Methods */
6785 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
6786 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
6787 // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
6788 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
6795 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
6796 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
6797 state
.value
= config
.$input
.val();
6798 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
6799 state
.focus
= config
.$input
.is( ':focus' );
6808 * A change event is emitted when the value of the input changes.
6810 * @param {string} value
6816 * Get input element.
6818 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
6819 * different circumstances. The element must have a `value` property (like form elements).
6822 * @param {Object} config Configuration options
6823 * @return {jQuery} Input element
6825 OO
.ui
.InputWidget
.prototype.getInputElement = function ( config
) {
6826 // See #reusePreInfuseDOM about config.$input
6827 return config
.$input
|| $( '<input>' );
6831 * Handle potentially value-changing events.
6834 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
6836 OO
.ui
.InputWidget
.prototype.onEdit = function () {
6838 if ( !this.isDisabled() ) {
6839 // Allow the stack to clear so the value will be updated
6840 setTimeout( function () {
6841 widget
.setValue( widget
.$input
.val() );
6847 * Get the value of the input.
6849 * @return {string} Input value
6851 OO
.ui
.InputWidget
.prototype.getValue = function () {
6852 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
6853 // it, and we won't know unless they're kind enough to trigger a 'change' event.
6854 var value
= this.$input
.val();
6855 if ( this.value
!== value
) {
6856 this.setValue( value
);
6862 * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
6864 * @deprecated since v0.13.1; use #setDir directly
6865 * @param {boolean} isRTL Directionality is right-to-left
6868 OO
.ui
.InputWidget
.prototype.setRTL = function ( isRTL
) {
6869 this.setDir( isRTL
? 'rtl' : 'ltr' );
6874 * Set the directionality of the input.
6876 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
6879 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
6880 this.$input
.prop( 'dir', dir
);
6885 * Set the value of the input.
6887 * @param {string} value New value
6891 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
6892 value
= this.cleanUpValue( value
);
6893 // Update the DOM if it has changed. Note that with cleanUpValue, it
6894 // is possible for the DOM value to change without this.value changing.
6895 if ( this.$input
.val() !== value
) {
6896 this.$input
.val( value
);
6898 if ( this.value
!== value
) {
6900 this.emit( 'change', this.value
);
6906 * Clean up incoming value.
6908 * Ensures value is a string, and converts undefined and null to empty string.
6911 * @param {string} value Original value
6912 * @return {string} Cleaned up value
6914 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
6915 if ( value
=== undefined || value
=== null ) {
6917 } else if ( this.inputFilter
) {
6918 return this.inputFilter( String( value
) );
6920 return String( value
);
6925 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
6926 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
6929 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
6930 if ( !this.isDisabled() ) {
6931 if ( this.$input
.is( ':checkbox, :radio' ) ) {
6932 this.$input
.click();
6934 if ( this.$input
.is( ':input' ) ) {
6935 this.$input
[ 0 ].focus();
6943 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
6944 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
6945 if ( this.$input
) {
6946 this.$input
.prop( 'disabled', this.isDisabled() );
6956 OO
.ui
.InputWidget
.prototype.focus = function () {
6957 this.$input
[ 0 ].focus();
6966 OO
.ui
.InputWidget
.prototype.blur = function () {
6967 this.$input
[ 0 ].blur();
6974 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
6975 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
6976 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
6977 this.setValue( state
.value
);
6979 if ( state
.focus
) {
6985 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
6986 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
6987 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
6988 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
6989 * [OOjs UI documentation on MediaWiki] [1] for more information.
6992 * // A ButtonInputWidget rendered as an HTML button, the default.
6993 * var button = new OO.ui.ButtonInputWidget( {
6994 * label: 'Input button',
6998 * $( 'body' ).append( button.$element );
7000 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
7003 * @extends OO.ui.InputWidget
7004 * @mixins OO.ui.mixin.ButtonElement
7005 * @mixins OO.ui.mixin.IconElement
7006 * @mixins OO.ui.mixin.IndicatorElement
7007 * @mixins OO.ui.mixin.LabelElement
7008 * @mixins OO.ui.mixin.TitledElement
7011 * @param {Object} [config] Configuration options
7012 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
7013 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
7014 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
7015 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
7016 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
7018 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
7019 // Configuration initialization
7020 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
7022 // Properties (must be set before parent constructor, which calls #setValue)
7023 this.useInputTag
= config
.useInputTag
;
7025 // Parent constructor
7026 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
7028 // Mixin constructors
7029 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
7030 OO
.ui
.mixin
.IconElement
.call( this, config
);
7031 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7032 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7033 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
7036 if ( !config
.useInputTag
) {
7037 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
7039 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
7044 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
7045 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
7046 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
7047 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
7048 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
7049 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
7051 /* Static Properties */
7054 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
7055 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
7057 OO
.ui
.ButtonInputWidget
.static.supportsSimpleLabel
= false;
7065 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
7067 // See InputWidget#reusePreInfuseDOM about config.$input
7068 if ( config
.$input
) {
7069 return config
.$input
.empty();
7071 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
7072 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
7078 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
7080 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
7081 * text, or `null` for no label
7084 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
7085 OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
7087 if ( this.useInputTag
) {
7088 if ( typeof label
=== 'function' ) {
7089 label
= OO
.ui
.resolveMsg( label
);
7091 if ( label
instanceof jQuery
) {
7092 label
= label
.text();
7097 this.$input
.val( label
);
7104 * Set the value of the input.
7106 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
7107 * they do not support {@link #value values}.
7109 * @param {string} value New value
7112 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
7113 if ( !this.useInputTag
) {
7114 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
7120 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
7121 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
7122 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
7123 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
7125 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7128 * // An example of selected, unselected, and disabled checkbox inputs
7129 * var checkbox1=new OO.ui.CheckboxInputWidget( {
7133 * var checkbox2=new OO.ui.CheckboxInputWidget( {
7136 * var checkbox3=new OO.ui.CheckboxInputWidget( {
7140 * // Create a fieldset layout with fields for each checkbox.
7141 * var fieldset = new OO.ui.FieldsetLayout( {
7142 * label: 'Checkboxes'
7144 * fieldset.addItems( [
7145 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
7146 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
7147 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
7149 * $( 'body' ).append( fieldset.$element );
7151 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7154 * @extends OO.ui.InputWidget
7157 * @param {Object} [config] Configuration options
7158 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
7160 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
7161 // Configuration initialization
7162 config
= config
|| {};
7164 // Parent constructor
7165 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
7169 .addClass( 'oo-ui-checkboxInputWidget' )
7170 // Required for pretty styling in MediaWiki theme
7171 .append( $( '<span>' ) );
7172 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
7177 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
7179 /* Static Methods */
7184 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7185 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7186 state
.checked
= config
.$input
.prop( 'checked' );
7196 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
7197 return $( '<input>' ).attr( 'type', 'checkbox' );
7203 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
7205 if ( !this.isDisabled() ) {
7206 // Allow the stack to clear so the value will be updated
7207 setTimeout( function () {
7208 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
7214 * Set selection state of this checkbox.
7216 * @param {boolean} state `true` for selected
7219 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
7221 if ( this.selected
!== state
) {
7222 this.selected
= state
;
7223 this.$input
.prop( 'checked', this.selected
);
7224 this.emit( 'change', this.selected
);
7230 * Check if this checkbox is selected.
7232 * @return {boolean} Checkbox is selected
7234 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
7235 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7236 // it, and we won't know unless they're kind enough to trigger a 'change' event.
7237 var selected
= this.$input
.prop( 'checked' );
7238 if ( this.selected
!== selected
) {
7239 this.setSelected( selected
);
7241 return this.selected
;
7247 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
7248 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7249 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
7250 this.setSelected( state
.checked
);
7255 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
7256 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7257 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7258 * more information about input widgets.
7260 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
7261 * are no options. If no `value` configuration option is provided, the first option is selected.
7262 * If you need a state representing no value (no option being selected), use a DropdownWidget.
7264 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
7267 * // Example: A DropdownInputWidget with three options
7268 * var dropdownInput = new OO.ui.DropdownInputWidget( {
7270 * { data: 'a', label: 'First' },
7271 * { data: 'b', label: 'Second'},
7272 * { data: 'c', label: 'Third' }
7275 * $( 'body' ).append( dropdownInput.$element );
7277 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7280 * @extends OO.ui.InputWidget
7281 * @mixins OO.ui.mixin.TitledElement
7284 * @param {Object} [config] Configuration options
7285 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7286 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
7288 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
7289 // Configuration initialization
7290 config
= config
|| {};
7292 // Properties (must be done before parent constructor which calls #setDisabled)
7293 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
7295 // Parent constructor
7296 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
7298 // Mixin constructors
7299 OO
.ui
.mixin
.TitledElement
.call( this, config
);
7302 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
7305 this.setOptions( config
.options
|| [] );
7307 .addClass( 'oo-ui-dropdownInputWidget' )
7308 .append( this.dropdownWidget
.$element
);
7313 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
7314 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
7322 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function ( config
) {
7323 // See InputWidget#reusePreInfuseDOM about config.$input
7324 if ( config
.$input
) {
7325 return config
.$input
.addClass( 'oo-ui-element-hidden' );
7327 return $( '<input>' ).attr( 'type', 'hidden' );
7331 * Handles menu select events.
7334 * @param {OO.ui.MenuOptionWidget} item Selected menu item
7336 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
7337 this.setValue( item
.getData() );
7343 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
7344 value
= this.cleanUpValue( value
);
7345 this.dropdownWidget
.getMenu().selectItemByData( value
);
7346 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
7353 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
7354 this.dropdownWidget
.setDisabled( state
);
7355 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
7360 * Set the options available for this input.
7362 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
7365 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
7367 value
= this.getValue(),
7370 // Rebuild the dropdown menu
7371 this.dropdownWidget
.getMenu()
7373 .addItems( options
.map( function ( opt
) {
7374 var optValue
= widget
.cleanUpValue( opt
.data
);
7375 return new OO
.ui
.MenuOptionWidget( {
7377 label
: opt
.label
!== undefined ? opt
.label
: optValue
7381 // Restore the previous value, or reset to something sensible
7382 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
7383 // Previous value is still available, ensure consistency with the dropdown
7384 this.setValue( value
);
7386 // No longer valid, reset
7387 if ( options
.length
) {
7388 this.setValue( options
[ 0 ].data
);
7398 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
7399 this.dropdownWidget
.getMenu().toggle( true );
7406 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
7407 this.dropdownWidget
.getMenu().toggle( false );
7412 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
7413 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
7414 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
7415 * please see the [OOjs UI documentation on MediaWiki][1].
7417 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7420 * // An example of selected, unselected, and disabled radio inputs
7421 * var radio1 = new OO.ui.RadioInputWidget( {
7425 * var radio2 = new OO.ui.RadioInputWidget( {
7428 * var radio3 = new OO.ui.RadioInputWidget( {
7432 * // Create a fieldset layout with fields for each radio button.
7433 * var fieldset = new OO.ui.FieldsetLayout( {
7434 * label: 'Radio inputs'
7436 * fieldset.addItems( [
7437 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
7438 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
7439 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
7441 * $( 'body' ).append( fieldset.$element );
7443 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7446 * @extends OO.ui.InputWidget
7449 * @param {Object} [config] Configuration options
7450 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
7452 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
7453 // Configuration initialization
7454 config
= config
|| {};
7456 // Parent constructor
7457 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
7461 .addClass( 'oo-ui-radioInputWidget' )
7462 // Required for pretty styling in MediaWiki theme
7463 .append( $( '<span>' ) );
7464 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
7469 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
7471 /* Static Methods */
7476 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7477 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7478 state
.checked
= config
.$input
.prop( 'checked' );
7488 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
7489 return $( '<input>' ).attr( 'type', 'radio' );
7495 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
7496 // RadioInputWidget doesn't track its state.
7500 * Set selection state of this radio button.
7502 * @param {boolean} state `true` for selected
7505 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
7506 // RadioInputWidget doesn't track its state.
7507 this.$input
.prop( 'checked', state
);
7512 * Check if this radio button is selected.
7514 * @return {boolean} Radio is selected
7516 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
7517 return this.$input
.prop( 'checked' );
7523 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
7524 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7525 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
7526 this.setSelected( state
.checked
);
7531 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
7532 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7533 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7534 * more information about input widgets.
7536 * This and OO.ui.DropdownInputWidget support the same configuration options.
7539 * // Example: A RadioSelectInputWidget with three options
7540 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
7542 * { data: 'a', label: 'First' },
7543 * { data: 'b', label: 'Second'},
7544 * { data: 'c', label: 'Third' }
7547 * $( 'body' ).append( radioSelectInput.$element );
7549 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7552 * @extends OO.ui.InputWidget
7555 * @param {Object} [config] Configuration options
7556 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7558 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
7559 // Configuration initialization
7560 config
= config
|| {};
7562 // Properties (must be done before parent constructor which calls #setDisabled)
7563 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
7565 // Parent constructor
7566 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
7569 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
7572 this.setOptions( config
.options
|| [] );
7574 .addClass( 'oo-ui-radioSelectInputWidget' )
7575 .append( this.radioSelectWidget
.$element
);
7580 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
7582 /* Static Properties */
7584 OO
.ui
.RadioSelectInputWidget
.static.supportsSimpleLabel
= false;
7586 /* Static Methods */
7591 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7592 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7593 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
7603 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
7604 return $( '<input>' ).attr( 'type', 'hidden' );
7608 * Handles menu select events.
7611 * @param {OO.ui.RadioOptionWidget} item Selected menu item
7613 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
7614 this.setValue( item
.getData() );
7620 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
7621 value
= this.cleanUpValue( value
);
7622 this.radioSelectWidget
.selectItemByData( value
);
7623 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
7630 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
7631 this.radioSelectWidget
.setDisabled( state
);
7632 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
7637 * Set the options available for this input.
7639 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
7642 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
7644 value
= this.getValue(),
7647 // Rebuild the radioSelect menu
7648 this.radioSelectWidget
7650 .addItems( options
.map( function ( opt
) {
7651 var optValue
= widget
.cleanUpValue( opt
.data
);
7652 return new OO
.ui
.RadioOptionWidget( {
7654 label
: opt
.label
!== undefined ? opt
.label
: optValue
7658 // Restore the previous value, or reset to something sensible
7659 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
7660 // Previous value is still available, ensure consistency with the radioSelect
7661 this.setValue( value
);
7663 // No longer valid, reset
7664 if ( options
.length
) {
7665 this.setValue( options
[ 0 ].data
);
7673 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
7674 * size of the field as well as its presentation. In addition, these widgets can be configured
7675 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
7676 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
7677 * which modifies incoming values rather than validating them.
7678 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
7680 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7683 * // Example of a text input widget
7684 * var textInput = new OO.ui.TextInputWidget( {
7685 * value: 'Text input'
7687 * $( 'body' ).append( textInput.$element );
7689 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7692 * @extends OO.ui.InputWidget
7693 * @mixins OO.ui.mixin.IconElement
7694 * @mixins OO.ui.mixin.IndicatorElement
7695 * @mixins OO.ui.mixin.PendingElement
7696 * @mixins OO.ui.mixin.LabelElement
7699 * @param {Object} [config] Configuration options
7700 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
7701 * 'email' or 'url'. Ignored if `multiline` is true.
7703 * Some values of `type` result in additional behaviors:
7705 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
7706 * empties the text field
7707 * @cfg {string} [placeholder] Placeholder text
7708 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
7709 * instruct the browser to focus this widget.
7710 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
7711 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
7712 * @cfg {boolean} [multiline=false] Allow multiple lines of text
7713 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
7714 * specifies minimum number of rows to display.
7715 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
7716 * Use the #maxRows config to specify a maximum number of displayed rows.
7717 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
7718 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
7719 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
7720 * the value or placeholder text: `'before'` or `'after'`
7721 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
7722 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
7723 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
7724 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
7725 * (the value must contain only numbers); when RegExp, a regular expression that must match the
7726 * value for it to be considered valid; when Function, a function receiving the value as parameter
7727 * that must return true, or promise resolving to true, for it to be considered valid.
7729 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
7730 // Configuration initialization
7731 config
= $.extend( {
7733 labelPosition
: 'after'
7735 if ( config
.type
=== 'search' ) {
7736 if ( config
.icon
=== undefined ) {
7737 config
.icon
= 'search';
7739 // indicator: 'clear' is set dynamically later, depending on value
7741 if ( config
.required
) {
7742 if ( config
.indicator
=== undefined ) {
7743 config
.indicator
= 'required';
7747 // Parent constructor
7748 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
7750 // Mixin constructors
7751 OO
.ui
.mixin
.IconElement
.call( this, config
);
7752 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7753 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
7754 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7757 this.type
= this.getSaneType( config
);
7758 this.readOnly
= false;
7759 this.multiline
= !!config
.multiline
;
7760 this.autosize
= !!config
.autosize
;
7761 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
7762 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
7763 this.validate
= null;
7764 this.styleHeight
= null;
7765 this.scrollWidth
= null;
7767 // Clone for resizing
7768 if ( this.autosize
) {
7769 this.$clone
= this.$input
7771 .insertAfter( this.$input
)
7772 .attr( 'aria-hidden', 'true' )
7773 .addClass( 'oo-ui-element-hidden' );
7776 this.setValidation( config
.validate
);
7777 this.setLabelPosition( config
.labelPosition
);
7781 keypress
: this.onKeyPress
.bind( this ),
7782 blur
: this.onBlur
.bind( this )
7785 focus
: this.onElementAttach
.bind( this )
7787 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
7788 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
7789 this.on( 'labelChange', this.updatePosition
.bind( this ) );
7790 this.connect( this, {
7792 disable
: 'onDisable'
7797 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
7798 .append( this.$icon
, this.$indicator
);
7799 this.setReadOnly( !!config
.readOnly
);
7800 this.updateSearchIndicator();
7801 if ( config
.placeholder
) {
7802 this.$input
.attr( 'placeholder', config
.placeholder
);
7804 if ( config
.maxLength
!== undefined ) {
7805 this.$input
.attr( 'maxlength', config
.maxLength
);
7807 if ( config
.autofocus
) {
7808 this.$input
.attr( 'autofocus', 'autofocus' );
7810 if ( config
.required
) {
7811 this.$input
.attr( 'required', 'required' );
7812 this.$input
.attr( 'aria-required', 'true' );
7814 if ( config
.autocomplete
=== false ) {
7815 this.$input
.attr( 'autocomplete', 'off' );
7816 // Turning off autocompletion also disables "form caching" when the user navigates to a
7817 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
7819 beforeunload: function () {
7820 this.$input
.removeAttr( 'autocomplete' );
7822 pageshow: function () {
7823 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
7824 // whole page... it shouldn't hurt, though.
7825 this.$input
.attr( 'autocomplete', 'off' );
7829 if ( this.multiline
&& config
.rows
) {
7830 this.$input
.attr( 'rows', config
.rows
);
7832 if ( this.label
|| config
.autosize
) {
7833 this.installParentChangeDetector();
7839 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
7840 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
7841 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
7842 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
7843 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
7845 /* Static Properties */
7847 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
7852 /* Static Methods */
7857 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7858 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7859 if ( config
.multiline
) {
7860 state
.scrollTop
= config
.$input
.scrollTop();
7868 * An `enter` event is emitted when the user presses 'enter' inside the text box.
7870 * Not emitted if the input is multiline.
7876 * A `resize` event is emitted when autosize is set and the widget resizes
7884 * Handle icon mouse down events.
7887 * @param {jQuery.Event} e Mouse down event
7890 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
7891 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7892 this.$input
[ 0 ].focus();
7898 * Handle indicator mouse down events.
7901 * @param {jQuery.Event} e Mouse down event
7904 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
7905 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7906 if ( this.type
=== 'search' ) {
7907 // Clear the text field
7908 this.setValue( '' );
7910 this.$input
[ 0 ].focus();
7916 * Handle key press events.
7919 * @param {jQuery.Event} e Key press event
7920 * @fires enter If enter key is pressed and input is not multiline
7922 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
7923 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
7924 this.emit( 'enter', e
);
7929 * Handle blur events.
7932 * @param {jQuery.Event} e Blur event
7934 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
7935 this.setValidityFlag();
7939 * Handle element attach events.
7942 * @param {jQuery.Event} e Element attach event
7944 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
7945 // Any previously calculated size is now probably invalid if we reattached elsewhere
7946 this.valCache
= null;
7948 this.positionLabel();
7952 * Handle change events.
7954 * @param {string} value
7957 OO
.ui
.TextInputWidget
.prototype.onChange = function () {
7958 this.updateSearchIndicator();
7959 this.setValidityFlag();
7964 * Handle disable events.
7966 * @param {boolean} disabled Element is disabled
7969 OO
.ui
.TextInputWidget
.prototype.onDisable = function () {
7970 this.updateSearchIndicator();
7974 * Check if the input is {@link #readOnly read-only}.
7978 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
7979 return this.readOnly
;
7983 * Set the {@link #readOnly read-only} state of the input.
7985 * @param {boolean} state Make input read-only
7988 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
7989 this.readOnly
= !!state
;
7990 this.$input
.prop( 'readOnly', this.readOnly
);
7991 this.updateSearchIndicator();
7996 * Support function for making #onElementAttach work across browsers.
7998 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
7999 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
8001 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
8002 * first time that the element gets attached to the documented.
8004 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
8005 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
8006 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
8009 if ( MutationObserver
) {
8010 // The new way. If only it wasn't so ugly.
8012 if ( this.$element
.closest( 'html' ).length
) {
8013 // Widget is attached already, do nothing. This breaks the functionality of this function when
8014 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
8015 // would require observation of the whole document, which would hurt performance of other,
8016 // more important code.
8020 // Find topmost node in the tree
8021 topmostNode
= this.$element
[ 0 ];
8022 while ( topmostNode
.parentNode
) {
8023 topmostNode
= topmostNode
.parentNode
;
8026 // We have no way to detect the $element being attached somewhere without observing the entire
8027 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
8028 // parent node of $element, and instead detect when $element is removed from it (and thus
8029 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
8030 // doesn't get attached, we end up back here and create the parent.
8032 mutationObserver
= new MutationObserver( function ( mutations
) {
8033 var i
, j
, removedNodes
;
8034 for ( i
= 0; i
< mutations
.length
; i
++ ) {
8035 removedNodes
= mutations
[ i
].removedNodes
;
8036 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
8037 if ( removedNodes
[ j
] === topmostNode
) {
8038 setTimeout( onRemove
, 0 );
8045 onRemove = function () {
8046 // If the node was attached somewhere else, report it
8047 if ( widget
.$element
.closest( 'html' ).length
) {
8048 widget
.onElementAttach();
8050 mutationObserver
.disconnect();
8051 widget
.installParentChangeDetector();
8054 // Create a fake parent and observe it
8055 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
8056 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
8058 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
8059 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
8060 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
8065 * Automatically adjust the size of the text input.
8067 * This only affects #multiline inputs that are {@link #autosize autosized}.
8072 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
8073 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
8074 idealHeight
, newHeight
, scrollWidth
, property
;
8076 if ( this.multiline
&& this.$input
.val() !== this.valCache
) {
8077 if ( this.autosize
) {
8079 .val( this.$input
.val() )
8080 .attr( 'rows', this.minRows
)
8081 // Set inline height property to 0 to measure scroll height
8082 .css( 'height', 0 );
8084 this.$clone
.removeClass( 'oo-ui-element-hidden' );
8086 this.valCache
= this.$input
.val();
8088 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
8090 // Remove inline height property to measure natural heights
8091 this.$clone
.css( 'height', '' );
8092 innerHeight
= this.$clone
.innerHeight();
8093 outerHeight
= this.$clone
.outerHeight();
8095 // Measure max rows height
8097 .attr( 'rows', this.maxRows
)
8098 .css( 'height', 'auto' )
8100 maxInnerHeight
= this.$clone
.innerHeight();
8102 // Difference between reported innerHeight and scrollHeight with no scrollbars present
8103 // Equals 1 on Blink-based browsers and 0 everywhere else
8104 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
8105 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
8107 this.$clone
.addClass( 'oo-ui-element-hidden' );
8109 // Only apply inline height when expansion beyond natural height is needed
8110 // Use the difference between the inner and outer height as a buffer
8111 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
8112 if ( newHeight
!== this.styleHeight
) {
8113 this.$input
.css( 'height', newHeight
);
8114 this.styleHeight
= newHeight
;
8115 this.emit( 'resize' );
8118 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
8119 if ( scrollWidth
!== this.scrollWidth
) {
8120 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
8122 this.$label
.css( { right
: '', left
: '' } );
8123 this.$indicator
.css( { right
: '', left
: '' } );
8125 if ( scrollWidth
) {
8126 this.$indicator
.css( property
, scrollWidth
);
8127 if ( this.labelPosition
=== 'after' ) {
8128 this.$label
.css( property
, scrollWidth
);
8132 this.scrollWidth
= scrollWidth
;
8133 this.positionLabel();
8143 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
8144 return config
.multiline
?
8146 $( '<input>' ).attr( 'type', this.getSaneType( config
) );
8150 * Get sanitized value for 'type' for given config.
8152 * @param {Object} config Configuration options
8153 * @return {string|null}
8156 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
8157 var type
= [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config
.type
) !== -1 ?
8160 return config
.multiline
? 'multiline' : type
;
8164 * Check if the input supports multiple lines.
8168 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
8169 return !!this.multiline
;
8173 * Check if the input automatically adjusts its size.
8177 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
8178 return !!this.autosize
;
8182 * Focus the input and select a specified range within the text.
8184 * @param {number} from Select from offset
8185 * @param {number} [to] Select to offset, defaults to from
8188 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
8189 var isBackwards
, start
, end
,
8190 input
= this.$input
[ 0 ];
8194 isBackwards
= to
< from;
8195 start
= isBackwards
? to
: from;
8196 end
= isBackwards
? from : to
;
8200 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
8205 * Get an object describing the current selection range in a directional manner
8207 * @return {Object} Object containing 'from' and 'to' offsets
8209 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
8210 var input
= this.$input
[ 0 ],
8211 start
= input
.selectionStart
,
8212 end
= input
.selectionEnd
,
8213 isBackwards
= input
.selectionDirection
=== 'backward';
8216 from: isBackwards
? end
: start
,
8217 to
: isBackwards
? start
: end
8222 * Get the length of the text input value.
8224 * This could differ from the length of #getValue if the
8225 * value gets filtered
8227 * @return {number} Input length
8229 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
8230 return this.$input
[ 0 ].value
.length
;
8234 * Focus the input and select the entire text.
8238 OO
.ui
.TextInputWidget
.prototype.select = function () {
8239 return this.selectRange( 0, this.getInputLength() );
8243 * Focus the input and move the cursor to the start.
8247 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
8248 return this.selectRange( 0 );
8252 * Focus the input and move the cursor to the end.
8256 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
8257 return this.selectRange( this.getInputLength() );
8261 * Insert new content into the input.
8263 * @param {string} content Content to be inserted
8266 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
8268 range
= this.getRange(),
8269 value
= this.getValue();
8271 start
= Math
.min( range
.from, range
.to
);
8272 end
= Math
.max( range
.from, range
.to
);
8274 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
8275 this.selectRange( start
+ content
.length
);
8280 * Insert new content either side of a selection.
8282 * @param {string} pre Content to be inserted before the selection
8283 * @param {string} post Content to be inserted after the selection
8286 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
8288 range
= this.getRange(),
8289 offset
= pre
.length
;
8291 start
= Math
.min( range
.from, range
.to
);
8292 end
= Math
.max( range
.from, range
.to
);
8294 this.selectRange( start
).insertContent( pre
);
8295 this.selectRange( offset
+ end
).insertContent( post
);
8297 this.selectRange( offset
+ start
, offset
+ end
);
8302 * Set the validation pattern.
8304 * The validation pattern is either a regular expression, a function, or the symbolic name of a
8305 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
8306 * value must contain only numbers).
8308 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
8309 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
8311 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
8312 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
8313 this.validate
= validate
;
8315 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
8320 * Sets the 'invalid' flag appropriately.
8322 * @param {boolean} [isValid] Optionally override validation result
8324 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
8326 setFlag = function ( valid
) {
8328 widget
.$input
.attr( 'aria-invalid', 'true' );
8330 widget
.$input
.removeAttr( 'aria-invalid' );
8332 widget
.setFlags( { invalid
: !valid
} );
8335 if ( isValid
!== undefined ) {
8338 this.getValidity().then( function () {
8347 * Check if a value is valid.
8349 * This method returns a promise that resolves with a boolean `true` if the current value is
8350 * considered valid according to the supplied {@link #validate validation pattern}.
8352 * @deprecated since v0.12.3
8353 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
8355 OO
.ui
.TextInputWidget
.prototype.isValid = function () {
8358 if ( this.validate
instanceof Function
) {
8359 result
= this.validate( this.getValue() );
8360 if ( result
&& $.isFunction( result
.promise
) ) {
8361 return result
.promise();
8363 return $.Deferred().resolve( !!result
).promise();
8366 return $.Deferred().resolve( !!this.getValue().match( this.validate
) ).promise();
8371 * Get the validity of current value.
8373 * This method returns a promise that resolves if the value is valid and rejects if
8374 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
8376 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
8378 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
8381 function rejectOrResolve( valid
) {
8383 return $.Deferred().resolve().promise();
8385 return $.Deferred().reject().promise();
8389 if ( this.validate
instanceof Function
) {
8390 result
= this.validate( this.getValue() );
8391 if ( result
&& $.isFunction( result
.promise
) ) {
8392 return result
.promise().then( function ( valid
) {
8393 return rejectOrResolve( valid
);
8396 return rejectOrResolve( result
);
8399 return rejectOrResolve( this.getValue().match( this.validate
) );
8404 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
8406 * @param {string} labelPosition Label position, 'before' or 'after'
8409 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
8410 this.labelPosition
= labelPosition
;
8412 // If there is no label and we only change the position, #updatePosition is a no-op,
8413 // but it takes really a lot of work to do nothing.
8414 this.updatePosition();
8420 * Update the position of the inline label.
8422 * This method is called by #setLabelPosition, and can also be called on its own if
8423 * something causes the label to be mispositioned.
8427 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
8428 var after
= this.labelPosition
=== 'after';
8431 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
8432 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
8434 this.valCache
= null;
8435 this.scrollWidth
= null;
8437 this.positionLabel();
8443 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
8444 * already empty or when it's not editable.
8446 OO
.ui
.TextInputWidget
.prototype.updateSearchIndicator = function () {
8447 if ( this.type
=== 'search' ) {
8448 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
8449 this.setIndicator( null );
8451 this.setIndicator( 'clear' );
8457 * Position the label by setting the correct padding on the input.
8462 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
8463 var after
, rtl
, property
;
8466 // Clear old values if present
8468 'padding-right': '',
8473 this.$element
.append( this.$label
);
8475 this.$label
.detach();
8479 after
= this.labelPosition
=== 'after';
8480 rtl
= this.$element
.css( 'direction' ) === 'rtl';
8481 property
= after
=== rtl
? 'padding-left' : 'padding-right';
8483 this.$input
.css( property
, this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 ) );
8491 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8492 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8493 if ( state
.scrollTop
!== undefined ) {
8494 this.$input
.scrollTop( state
.scrollTop
);
8499 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
8500 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
8501 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
8503 * - by typing a value in the text input field. If the value exactly matches the value of a menu
8504 * option, that option will appear to be selected.
8505 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
8508 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8510 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
8513 * // Example: A ComboBoxInputWidget.
8514 * var comboBox = new OO.ui.ComboBoxInputWidget( {
8515 * label: 'ComboBoxInputWidget',
8516 * value: 'Option 1',
8519 * new OO.ui.MenuOptionWidget( {
8521 * label: 'Option One'
8523 * new OO.ui.MenuOptionWidget( {
8525 * label: 'Option Two'
8527 * new OO.ui.MenuOptionWidget( {
8529 * label: 'Option Three'
8531 * new OO.ui.MenuOptionWidget( {
8533 * label: 'Option Four'
8535 * new OO.ui.MenuOptionWidget( {
8537 * label: 'Option Five'
8542 * $( 'body' ).append( comboBox.$element );
8544 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
8547 * @extends OO.ui.TextInputWidget
8550 * @param {Object} [config] Configuration options
8551 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8552 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
8553 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
8554 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
8555 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
8557 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
8558 // Configuration initialization
8559 config
= $.extend( {
8562 // For backwards-compatibility with ComboBoxWidget config
8563 $.extend( config
, config
.input
);
8565 // Parent constructor
8566 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
8569 this.$overlay
= config
.$overlay
|| this.$element
;
8570 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend(
8574 $container
: this.$element
,
8575 disabled
: this.isDisabled()
8579 // For backwards-compatibility with ComboBoxWidget
8583 this.$indicator
.on( {
8584 click
: this.onIndicatorClick
.bind( this ),
8585 keypress
: this.onIndicatorKeyPress
.bind( this )
8587 this.connect( this, {
8588 change
: 'onInputChange',
8589 enter
: 'onInputEnter'
8591 this.menu
.connect( this, {
8592 choose
: 'onMenuChoose',
8593 add
: 'onMenuItemsChange',
8594 remove
: 'onMenuItemsChange'
8600 'aria-autocomplete': 'list'
8602 // Do not override options set via config.menu.items
8603 if ( config
.options
!== undefined ) {
8604 this.setOptions( config
.options
);
8606 // Extra class for backwards-compatibility with ComboBoxWidget
8607 this.$element
.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
8608 this.$overlay
.append( this.menu
.$element
);
8609 this.onMenuItemsChange();
8614 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
8619 * Get the combobox's menu.
8620 * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
8622 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
8627 * Get the combobox's text input widget.
8628 * @return {OO.ui.TextInputWidget} Text input widget
8630 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
8635 * Handle input change events.
8638 * @param {string} value New value
8640 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
8641 var match
= this.menu
.getItemFromData( value
);
8643 this.menu
.selectItem( match
);
8644 if ( this.menu
.getHighlightedItem() ) {
8645 this.menu
.highlightItem( match
);
8648 if ( !this.isDisabled() ) {
8649 this.menu
.toggle( true );
8654 * Handle mouse click events.
8657 * @param {jQuery.Event} e Mouse click event
8659 OO
.ui
.ComboBoxInputWidget
.prototype.onIndicatorClick = function ( e
) {
8660 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8662 this.$input
[ 0 ].focus();
8668 * Handle key press events.
8671 * @param {jQuery.Event} e Key press event
8673 OO
.ui
.ComboBoxInputWidget
.prototype.onIndicatorKeyPress = function ( e
) {
8674 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
8676 this.$input
[ 0 ].focus();
8682 * Handle input enter events.
8686 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
8687 if ( !this.isDisabled() ) {
8688 this.menu
.toggle( false );
8693 * Handle menu choose events.
8696 * @param {OO.ui.OptionWidget} item Chosen item
8698 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
8699 this.setValue( item
.getData() );
8703 * Handle menu item change events.
8707 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
8708 var match
= this.menu
.getItemFromData( this.getValue() );
8709 this.menu
.selectItem( match
);
8710 if ( this.menu
.getHighlightedItem() ) {
8711 this.menu
.highlightItem( match
);
8713 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
8719 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
8721 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8724 this.menu
.setDisabled( this.isDisabled() );
8731 * Set the options available for this input.
8733 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8736 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
8739 .addItems( options
.map( function ( opt
) {
8740 return new OO
.ui
.MenuOptionWidget( {
8742 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
8751 * @deprecated since 0.13.2; use OO.ui.ComboBoxInputWidget instead
8753 OO
.ui
.ComboBoxWidget
= OO
.ui
.ComboBoxInputWidget
;
8756 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
8757 * which is a widget that is specified by reference before any optional configuration settings.
8759 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
8761 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8762 * A left-alignment is used for forms with many fields.
8763 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8764 * A right-alignment is used for long but familiar forms which users tab through,
8765 * verifying the current field with a quick glance at the label.
8766 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8767 * that users fill out from top to bottom.
8768 * - **inline**: The label is placed after the field-widget and aligned to the left.
8769 * An inline-alignment is best used with checkboxes or radio buttons.
8771 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
8772 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
8774 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8776 * @extends OO.ui.Layout
8777 * @mixins OO.ui.mixin.LabelElement
8778 * @mixins OO.ui.mixin.TitledElement
8781 * @param {OO.ui.Widget} fieldWidget Field widget
8782 * @param {Object} [config] Configuration options
8783 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
8784 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
8785 * The array may contain strings or OO.ui.HtmlSnippet instances.
8786 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
8787 * The array may contain strings or OO.ui.HtmlSnippet instances.
8788 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
8789 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
8790 * For important messages, you are advised to use `notices`, as they are always shown.
8792 * @throws {Error} An error is thrown if no widget is specified
8794 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
8795 var hasInputWidget
, div
;
8797 // Allow passing positional parameters inside the config object
8798 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
8799 config
= fieldWidget
;
8800 fieldWidget
= config
.fieldWidget
;
8803 // Make sure we have required constructor arguments
8804 if ( fieldWidget
=== undefined ) {
8805 throw new Error( 'Widget not found' );
8808 hasInputWidget
= fieldWidget
.constructor.static.supportsSimpleLabel
;
8810 // Configuration initialization
8811 config
= $.extend( { align
: 'left' }, config
);
8813 // Parent constructor
8814 OO
.ui
.FieldLayout
.parent
.call( this, config
);
8816 // Mixin constructors
8817 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8818 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
8821 this.fieldWidget
= fieldWidget
;
8824 this.$field
= $( '<div>' );
8825 this.$messages
= $( '<ul>' );
8826 this.$body
= $( '<' + ( hasInputWidget
? 'label' : 'div' ) + '>' );
8828 if ( config
.help
) {
8829 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
8830 classes
: [ 'oo-ui-fieldLayout-help' ],
8836 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
8837 div
.html( config
.help
.toString() );
8839 div
.text( config
.help
);
8841 this.popupButtonWidget
.getPopup().$body
.append(
8842 div
.addClass( 'oo-ui-fieldLayout-help-content' )
8844 this.$help
= this.popupButtonWidget
.$element
;
8846 this.$help
= $( [] );
8850 if ( hasInputWidget
) {
8851 this.$label
.on( 'click', this.onLabelClick
.bind( this ) );
8853 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
8857 .addClass( 'oo-ui-fieldLayout' )
8858 .append( this.$help
, this.$body
);
8859 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
8860 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
8862 .addClass( 'oo-ui-fieldLayout-field' )
8863 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget
.isDisabled() )
8864 .append( this.fieldWidget
.$element
);
8866 this.setErrors( config
.errors
|| [] );
8867 this.setNotices( config
.notices
|| [] );
8868 this.setAlignment( config
.align
);
8873 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
8874 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
8875 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
8880 * Handle field disable events.
8883 * @param {boolean} value Field is disabled
8885 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
8886 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
8890 * Handle label mouse click events.
8893 * @param {jQuery.Event} e Mouse click event
8895 OO
.ui
.FieldLayout
.prototype.onLabelClick = function () {
8896 this.fieldWidget
.simulateLabelClick();
8901 * Get the widget contained by the field.
8903 * @return {OO.ui.Widget} Field widget
8905 OO
.ui
.FieldLayout
.prototype.getField = function () {
8906 return this.fieldWidget
;
8911 * @param {string} kind 'error' or 'notice'
8912 * @param {string|OO.ui.HtmlSnippet} text
8915 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
8916 var $listItem
, $icon
, message
;
8917 $listItem
= $( '<li>' );
8918 if ( kind
=== 'error' ) {
8919 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
8920 } else if ( kind
=== 'notice' ) {
8921 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
8925 message
= new OO
.ui
.LabelWidget( { label
: text
} );
8927 .append( $icon
, message
.$element
)
8928 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
8933 * Set the field alignment mode.
8936 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
8939 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
8940 if ( value
!== this.align
) {
8941 // Default to 'left'
8942 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
8946 if ( value
=== 'inline' ) {
8947 this.$body
.append( this.$field
, this.$label
);
8949 this.$body
.append( this.$label
, this.$field
);
8951 // Set classes. The following classes can be used here:
8952 // * oo-ui-fieldLayout-align-left
8953 // * oo-ui-fieldLayout-align-right
8954 // * oo-ui-fieldLayout-align-top
8955 // * oo-ui-fieldLayout-align-inline
8957 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
8959 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
8967 * Set the list of error messages.
8969 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
8970 * The array may contain strings or OO.ui.HtmlSnippet instances.
8973 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
8974 this.errors
= errors
.slice();
8975 this.updateMessages();
8980 * Set the list of notice messages.
8982 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
8983 * The array may contain strings or OO.ui.HtmlSnippet instances.
8986 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
8987 this.notices
= notices
.slice();
8988 this.updateMessages();
8993 * Update the rendering of error and notice messages.
8997 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
8999 this.$messages
.empty();
9001 if ( this.errors
.length
|| this.notices
.length
) {
9002 this.$body
.after( this.$messages
);
9004 this.$messages
.remove();
9008 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
9009 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
9011 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
9012 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
9017 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
9018 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
9019 * is required and is specified before any optional configuration settings.
9021 * Labels can be aligned in one of four ways:
9023 * - **left**: The label is placed before the field-widget and aligned with the left margin.
9024 * A left-alignment is used for forms with many fields.
9025 * - **right**: The label is placed before the field-widget and aligned to the right margin.
9026 * A right-alignment is used for long but familiar forms which users tab through,
9027 * verifying the current field with a quick glance at the label.
9028 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9029 * that users fill out from top to bottom.
9030 * - **inline**: The label is placed after the field-widget and aligned to the left.
9031 * An inline-alignment is best used with checkboxes or radio buttons.
9033 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
9034 * text is specified.
9037 * // Example of an ActionFieldLayout
9038 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
9039 * new OO.ui.TextInputWidget( {
9040 * placeholder: 'Field widget'
9042 * new OO.ui.ButtonWidget( {
9046 * label: 'An ActionFieldLayout. This label is aligned top',
9048 * help: 'This is help text'
9052 * $( 'body' ).append( actionFieldLayout.$element );
9055 * @extends OO.ui.FieldLayout
9058 * @param {OO.ui.Widget} fieldWidget Field widget
9059 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
9061 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
9062 // Allow passing positional parameters inside the config object
9063 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
9064 config
= fieldWidget
;
9065 fieldWidget
= config
.fieldWidget
;
9066 buttonWidget
= config
.buttonWidget
;
9069 // Parent constructor
9070 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
9073 this.buttonWidget
= buttonWidget
;
9074 this.$button
= $( '<div>' );
9075 this.$input
= $( '<div>' );
9079 .addClass( 'oo-ui-actionFieldLayout' );
9081 .addClass( 'oo-ui-actionFieldLayout-button' )
9082 .append( this.buttonWidget
.$element
);
9084 .addClass( 'oo-ui-actionFieldLayout-input' )
9085 .append( this.fieldWidget
.$element
);
9087 .append( this.$input
, this.$button
);
9092 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
9095 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
9096 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
9097 * configured with a label as well. For more information and examples,
9098 * please see the [OOjs UI documentation on MediaWiki][1].
9101 * // Example of a fieldset layout
9102 * var input1 = new OO.ui.TextInputWidget( {
9103 * placeholder: 'A text input field'
9106 * var input2 = new OO.ui.TextInputWidget( {
9107 * placeholder: 'A text input field'
9110 * var fieldset = new OO.ui.FieldsetLayout( {
9111 * label: 'Example of a fieldset layout'
9114 * fieldset.addItems( [
9115 * new OO.ui.FieldLayout( input1, {
9116 * label: 'Field One'
9118 * new OO.ui.FieldLayout( input2, {
9119 * label: 'Field Two'
9122 * $( 'body' ).append( fieldset.$element );
9124 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9127 * @extends OO.ui.Layout
9128 * @mixins OO.ui.mixin.IconElement
9129 * @mixins OO.ui.mixin.LabelElement
9130 * @mixins OO.ui.mixin.GroupElement
9133 * @param {Object} [config] Configuration options
9134 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
9136 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
9137 // Configuration initialization
9138 config
= config
|| {};
9140 // Parent constructor
9141 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
9143 // Mixin constructors
9144 OO
.ui
.mixin
.IconElement
.call( this, config
);
9145 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9146 OO
.ui
.mixin
.GroupElement
.call( this, config
);
9148 if ( config
.help
) {
9149 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
9150 classes
: [ 'oo-ui-fieldsetLayout-help' ],
9155 this.popupButtonWidget
.getPopup().$body
.append(
9157 .text( config
.help
)
9158 .addClass( 'oo-ui-fieldsetLayout-help-content' )
9160 this.$help
= this.popupButtonWidget
.$element
;
9162 this.$help
= $( [] );
9167 .addClass( 'oo-ui-fieldsetLayout' )
9168 .prepend( this.$help
, this.$icon
, this.$label
, this.$group
);
9169 if ( Array
.isArray( config
.items
) ) {
9170 this.addItems( config
.items
);
9176 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
9177 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
9178 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
9179 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
9182 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
9183 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
9184 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
9185 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9187 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
9188 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
9189 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
9190 * some fancier controls. Some controls have both regular and InputWidget variants, for example
9191 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
9192 * often have simplified APIs to match the capabilities of HTML forms.
9193 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
9195 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
9196 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9199 * // Example of a form layout that wraps a fieldset layout
9200 * var input1 = new OO.ui.TextInputWidget( {
9201 * placeholder: 'Username'
9203 * var input2 = new OO.ui.TextInputWidget( {
9204 * placeholder: 'Password',
9207 * var submit = new OO.ui.ButtonInputWidget( {
9211 * var fieldset = new OO.ui.FieldsetLayout( {
9212 * label: 'A form layout'
9214 * fieldset.addItems( [
9215 * new OO.ui.FieldLayout( input1, {
9216 * label: 'Username',
9219 * new OO.ui.FieldLayout( input2, {
9220 * label: 'Password',
9223 * new OO.ui.FieldLayout( submit )
9225 * var form = new OO.ui.FormLayout( {
9226 * items: [ fieldset ],
9227 * action: '/api/formhandler',
9230 * $( 'body' ).append( form.$element );
9233 * @extends OO.ui.Layout
9234 * @mixins OO.ui.mixin.GroupElement
9237 * @param {Object} [config] Configuration options
9238 * @cfg {string} [method] HTML form `method` attribute
9239 * @cfg {string} [action] HTML form `action` attribute
9240 * @cfg {string} [enctype] HTML form `enctype` attribute
9241 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
9243 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
9246 // Configuration initialization
9247 config
= config
|| {};
9249 // Parent constructor
9250 OO
.ui
.FormLayout
.parent
.call( this, config
);
9252 // Mixin constructors
9253 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
9256 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
9258 // Make sure the action is safe
9259 action
= config
.action
;
9260 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
9261 action
= './' + action
;
9266 .addClass( 'oo-ui-formLayout' )
9268 method
: config
.method
,
9270 enctype
: config
.enctype
9272 if ( Array
.isArray( config
.items
) ) {
9273 this.addItems( config
.items
);
9279 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
9280 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
9285 * A 'submit' event is emitted when the form is submitted.
9290 /* Static Properties */
9292 OO
.ui
.FormLayout
.static.tagName
= 'form';
9297 * Handle form submit events.
9300 * @param {jQuery.Event} e Submit event
9303 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
9304 if ( this.emit( 'submit' ) ) {
9310 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
9311 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
9314 * // Example of a panel layout
9315 * var panel = new OO.ui.PanelLayout( {
9319 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
9321 * $( 'body' ).append( panel.$element );
9324 * @extends OO.ui.Layout
9327 * @param {Object} [config] Configuration options
9328 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
9329 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
9330 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
9331 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
9333 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
9334 // Configuration initialization
9335 config
= $.extend( {
9342 // Parent constructor
9343 OO
.ui
.PanelLayout
.parent
.call( this, config
);
9346 this.$element
.addClass( 'oo-ui-panelLayout' );
9347 if ( config
.scrollable
) {
9348 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
9350 if ( config
.padded
) {
9351 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
9353 if ( config
.expanded
) {
9354 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
9356 if ( config
.framed
) {
9357 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
9363 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
9368 * Focus the panel layout
9370 * The default implementation just focuses the first focusable element in the panel
9372 OO
.ui
.PanelLayout
.prototype.focus = function () {
9373 OO
.ui
.findFocusable( this.$element
).focus();
9377 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
9378 * items), with small margins between them. Convenient when you need to put a number of block-level
9379 * widgets on a single line next to each other.
9381 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
9384 * // HorizontalLayout with a text input and a label
9385 * var layout = new OO.ui.HorizontalLayout( {
9387 * new OO.ui.LabelWidget( { label: 'Label' } ),
9388 * new OO.ui.TextInputWidget( { value: 'Text' } )
9391 * $( 'body' ).append( layout.$element );
9394 * @extends OO.ui.Layout
9395 * @mixins OO.ui.mixin.GroupElement
9398 * @param {Object} [config] Configuration options
9399 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
9401 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
9402 // Configuration initialization
9403 config
= config
|| {};
9405 // Parent constructor
9406 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
9408 // Mixin constructors
9409 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
9412 this.$element
.addClass( 'oo-ui-horizontalLayout' );
9413 if ( Array
.isArray( config
.items
) ) {
9414 this.addItems( config
.items
);
9420 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
9421 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);