3 * https://www.mediawiki.org/wiki/OOjs_UI
5 * Copyright 2011–2017 OOjs UI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2017-03-28T22:19:29Z
16 * Namespace for all classes, static methods and static properties.
48 * Constants for MouseEvent.which
52 OO
.ui
.MouseButtons
= {
65 * Generate a unique ID for element
69 OO
.ui
.generateElementId = function () {
71 return 'oojsui-' + OO
.ui
.elementId
;
75 * Check if an element is focusable.
76 * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
78 * @param {jQuery} $element Element to test
81 OO
.ui
.isFocusableElement = function ( $element
) {
83 element
= $element
[ 0 ];
85 // Anything disabled is not focusable
86 if ( element
.disabled
) {
90 // Check if the element is visible
92 // This is quicker than calling $element.is( ':visible' )
93 $.expr
.pseudos
.visible( element
) &&
94 // Check that all parents are visible
95 !$element
.parents().addBack().filter( function () {
96 return $.css( this, 'visibility' ) === 'hidden';
102 // Check if the element is ContentEditable, which is the string 'true'
103 if ( element
.contentEditable
=== 'true' ) {
107 // Anything with a non-negative numeric tabIndex is focusable.
108 // Use .prop to avoid browser bugs
109 if ( $element
.prop( 'tabIndex' ) >= 0 ) {
113 // Some element types are naturally focusable
114 // (indexOf is much faster than regex in Chrome and about the
115 // same in FF: https://jsperf.com/regex-vs-indexof-array2)
116 nodeName
= element
.nodeName
.toLowerCase();
117 if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName
) !== -1 ) {
121 // Links and areas are focusable if they have an href
122 if ( ( nodeName
=== 'a' || nodeName
=== 'area' ) && $element
.attr( 'href' ) !== undefined ) {
130 * Find a focusable child
132 * @param {jQuery} $container Container to search in
133 * @param {boolean} [backwards] Search backwards
134 * @return {jQuery} Focusable child, an empty jQuery object if none found
136 OO
.ui
.findFocusable = function ( $container
, backwards
) {
137 var $focusable
= $( [] ),
138 // $focusableCandidates is a superset of things that
139 // could get matched by isFocusableElement
140 $focusableCandidates
= $container
141 .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
144 $focusableCandidates
= Array
.prototype.reverse
.call( $focusableCandidates
);
147 $focusableCandidates
.each( function () {
148 var $this = $( this );
149 if ( OO
.ui
.isFocusableElement( $this ) ) {
158 * Get the user's language and any fallback languages.
160 * These language codes are used to localize user interface elements in the user's language.
162 * In environments that provide a localization system, this function should be overridden to
163 * return the user's language(s). The default implementation returns English (en) only.
165 * @return {string[]} Language codes, in descending order of priority
167 OO
.ui
.getUserLanguages = function () {
172 * Get a value in an object keyed by language code.
174 * @param {Object.<string,Mixed>} obj Object keyed by language code
175 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
176 * @param {string} [fallback] Fallback code, used if no matching language can be found
177 * @return {Mixed} Local value
179 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
182 // Requested language
186 // Known user language
187 langs
= OO
.ui
.getUserLanguages();
188 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
195 if ( obj
[ fallback
] ) {
196 return obj
[ fallback
];
198 // First existing language
199 for ( lang
in obj
) {
207 * Check if a node is contained within another node
209 * Similar to jQuery#contains except a list of containers can be supplied
210 * and a boolean argument allows you to include the container in the match list
212 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
213 * @param {HTMLElement} contained Node to find
214 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
215 * @return {boolean} The node is in the list of target nodes
217 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
219 if ( !Array
.isArray( containers
) ) {
220 containers
= [ containers
];
222 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
223 if ( ( matchContainers
&& contained
=== containers
[ i
] ) || $.contains( containers
[ i
], contained
) ) {
231 * Return a function, that, as long as it continues to be invoked, will not
232 * be triggered. The function will be called after it stops being called for
233 * N milliseconds. If `immediate` is passed, trigger the function on the
234 * leading edge, instead of the trailing.
236 * Ported from: http://underscorejs.org/underscore.js
238 * @param {Function} func
239 * @param {number} wait
240 * @param {boolean} immediate
243 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
248 later = function () {
251 func
.apply( context
, args
);
254 if ( immediate
&& !timeout
) {
255 func
.apply( context
, args
);
257 if ( !timeout
|| wait
) {
258 clearTimeout( timeout
);
259 timeout
= setTimeout( later
, wait
);
265 * Puts a console warning with provided message.
267 * @param {string} message
269 OO
.ui
.warnDeprecation = function ( message
) {
270 if ( OO
.getProp( window
, 'console', 'warn' ) !== undefined ) {
271 // eslint-disable-next-line no-console
272 console
.warn( message
);
277 * Returns a function, that, when invoked, will only be triggered at most once
278 * during a given window of time. If called again during that window, it will
279 * wait until the window ends and then trigger itself again.
281 * As it's not knowable to the caller whether the function will actually run
282 * when the wrapper is called, return values from the function are entirely
285 * @param {Function} func
286 * @param {number} wait
289 OO
.ui
.throttle = function ( func
, wait
) {
290 var context
, args
, timeout
,
294 previous
= OO
.ui
.now();
295 func
.apply( context
, args
);
298 // Check how long it's been since the last time the function was
299 // called, and whether it's more or less than the requested throttle
300 // period. If it's less, run the function immediately. If it's more,
301 // set a timeout for the remaining time -- but don't replace an
302 // existing timeout, since that'd indefinitely prolong the wait.
303 var remaining
= wait
- ( OO
.ui
.now() - previous
);
306 if ( remaining
<= 0 ) {
307 // Note: unless wait was ridiculously large, this means we'll
308 // automatically run the first time the function was called in a
309 // given period. (If you provide a wait period larger than the
310 // current Unix timestamp, you *deserve* unexpected behavior.)
311 clearTimeout( timeout
);
313 } else if ( !timeout
) {
314 timeout
= setTimeout( run
, remaining
);
320 * A (possibly faster) way to get the current timestamp as an integer
322 * @return {number} Current timestamp
324 OO
.ui
.now
= Date
.now
|| function () {
325 return new Date().getTime();
329 * Reconstitute a JavaScript object corresponding to a widget created by
330 * the PHP implementation.
332 * This is an alias for `OO.ui.Element.static.infuse()`.
334 * @param {string|HTMLElement|jQuery} idOrNode
335 * A DOM id (if a string) or node for the widget to infuse.
336 * @return {OO.ui.Element}
337 * The `OO.ui.Element` corresponding to this (infusable) document node.
339 OO
.ui
.infuse = function ( idOrNode
) {
340 return OO
.ui
.Element
.static.infuse( idOrNode
);
345 * Message store for the default implementation of OO.ui.msg
347 * Environments that provide a localization system should not use this, but should override
348 * OO.ui.msg altogether.
353 // Tool tip for a button that moves items in a list down one place
354 'ooui-outline-control-move-down': 'Move item down',
355 // Tool tip for a button that moves items in a list up one place
356 'ooui-outline-control-move-up': 'Move item up',
357 // Tool tip for a button that removes items from a list
358 'ooui-outline-control-remove': 'Remove item',
359 // Label for the toolbar group that contains a list of all other available tools
360 'ooui-toolbar-more': 'More',
361 // Label for the fake tool that expands the full list of tools in a toolbar group
362 'ooui-toolgroup-expand': 'More',
363 // Label for the fake tool that collapses the full list of tools in a toolbar group
364 'ooui-toolgroup-collapse': 'Fewer',
365 // Default label for the accept button of a confirmation dialog
366 'ooui-dialog-message-accept': 'OK',
367 // Default label for the reject button of a confirmation dialog
368 'ooui-dialog-message-reject': 'Cancel',
369 // Title for process dialog error description
370 'ooui-dialog-process-error': 'Something went wrong',
371 // Label for process dialog dismiss error button, visible when describing errors
372 'ooui-dialog-process-dismiss': 'Dismiss',
373 // Label for process dialog retry action button, visible when describing only recoverable errors
374 'ooui-dialog-process-retry': 'Try again',
375 // Label for process dialog retry action button, visible when describing only warnings
376 'ooui-dialog-process-continue': 'Continue',
377 // Label for the file selection widget's select file button
378 'ooui-selectfile-button-select': 'Select a file',
379 // Label for the file selection widget if file selection is not supported
380 'ooui-selectfile-not-supported': 'File selection is not supported',
381 // Label for the file selection widget when no file is currently selected
382 'ooui-selectfile-placeholder': 'No file is selected',
383 // Label for the file selection widget's drop target
384 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
388 * Get a localized message.
390 * After the message key, message parameters may optionally be passed. In the default implementation,
391 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
392 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
393 * they support unnamed, ordered message parameters.
395 * In environments that provide a localization system, this function should be overridden to
396 * return the message translated in the user's language. The default implementation always returns
397 * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
401 * var i, iLen, button,
402 * messagePath = 'oojs-ui/dist/i18n/',
403 * languages = [ $.i18n().locale, 'ur', 'en' ],
406 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
407 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
410 * $.i18n().load( languageMap ).done( function() {
411 * // Replace the built-in `msg` only once we've loaded the internationalization.
412 * // OOjs UI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
413 * // you put off creating any widgets until this promise is complete, no English
414 * // will be displayed.
415 * OO.ui.msg = $.i18n;
417 * // A button displaying "OK" in the default locale
418 * button = new OO.ui.ButtonWidget( {
419 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
422 * $( 'body' ).append( button.$element );
424 * // A button displaying "OK" in Urdu
425 * $.i18n().locale = 'ur';
426 * button = new OO.ui.ButtonWidget( {
427 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
430 * $( 'body' ).append( button.$element );
433 * @param {string} key Message key
434 * @param {...Mixed} [params] Message parameters
435 * @return {string} Translated message with parameters substituted
437 OO
.ui
.msg = function ( key
) {
438 var message
= messages
[ key
],
439 params
= Array
.prototype.slice
.call( arguments
, 1 );
440 if ( typeof message
=== 'string' ) {
441 // Perform $1 substitution
442 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
443 var i
= parseInt( n
, 10 );
444 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
447 // Return placeholder if message not found
448 message
= '[' + key
+ ']';
455 * Package a message and arguments for deferred resolution.
457 * Use this when you are statically specifying a message and the message may not yet be present.
459 * @param {string} key Message key
460 * @param {...Mixed} [params] Message parameters
461 * @return {Function} Function that returns the resolved message when executed
463 OO
.ui
.deferMsg = function () {
464 var args
= arguments
;
466 return OO
.ui
.msg
.apply( OO
.ui
, args
);
473 * If the message is a function it will be executed, otherwise it will pass through directly.
475 * @param {Function|string} msg Deferred message, or message text
476 * @return {string} Resolved message
478 OO
.ui
.resolveMsg = function ( msg
) {
479 if ( $.isFunction( msg
) ) {
486 * @param {string} url
489 OO
.ui
.isSafeUrl = function ( url
) {
490 // Keep this function in sync with php/Tag.php
491 var i
, protocolWhitelist
;
493 function stringStartsWith( haystack
, needle
) {
494 return haystack
.substr( 0, needle
.length
) === needle
;
497 protocolWhitelist
= [
498 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
499 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
500 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
507 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
508 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
513 // This matches '//' too
514 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
517 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
525 * Check if the user has a 'mobile' device.
527 * For our purposes this means the user is primarily using an
528 * on-screen keyboard, touch input instead of a mouse and may
529 * have a physically small display.
531 * It is left up to implementors to decide how to compute this
532 * so the default implementation always returns false.
534 * @return {boolean} Use is on a mobile device
536 OO
.ui
.isMobile = function () {
545 * Namespace for OOjs UI mixins.
547 * Mixins are named according to the type of object they are intended to
548 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
549 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
550 * is intended to be mixed in to an instance of OO.ui.Widget.
558 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
559 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
560 * connected to them and can't be interacted with.
566 * @param {Object} [config] Configuration options
567 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
568 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
570 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
571 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
572 * @cfg {string} [text] Text to insert
573 * @cfg {Array} [content] An array of content elements to append (after #text).
574 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
575 * Instances of OO.ui.Element will have their $element appended.
576 * @cfg {jQuery} [$content] Content elements to append (after #text).
577 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
578 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
579 * Data can also be specified with the #setData method.
581 OO
.ui
.Element
= function OoUiElement( config
) {
582 // Configuration initialization
583 config
= config
|| {};
588 this.data
= config
.data
;
589 this.$element
= config
.$element
||
590 $( document
.createElement( this.getTagName() ) );
591 this.elementGroup
= null;
594 if ( Array
.isArray( config
.classes
) ) {
595 this.$element
.addClass( config
.classes
.join( ' ' ) );
598 this.$element
.attr( 'id', config
.id
);
601 this.$element
.text( config
.text
);
603 if ( config
.content
) {
604 // The `content` property treats plain strings as text; use an
605 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
606 // appropriate $element appended.
607 this.$element
.append( config
.content
.map( function ( v
) {
608 if ( typeof v
=== 'string' ) {
609 // Escape string so it is properly represented in HTML.
610 return document
.createTextNode( v
);
611 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
614 } else if ( v
instanceof OO
.ui
.Element
) {
620 if ( config
.$content
) {
621 // The `$content` property treats plain strings as HTML.
622 this.$element
.append( config
.$content
);
628 OO
.initClass( OO
.ui
.Element
);
630 /* Static Properties */
633 * The name of the HTML tag used by the element.
635 * The static value may be ignored if the #getTagName method is overridden.
641 OO
.ui
.Element
.static.tagName
= 'div';
646 * Reconstitute a JavaScript object corresponding to a widget created
647 * by the PHP implementation.
649 * @param {string|HTMLElement|jQuery} idOrNode
650 * A DOM id (if a string) or node for the widget to infuse.
651 * @return {OO.ui.Element}
652 * The `OO.ui.Element` corresponding to this (infusable) document node.
653 * For `Tag` objects emitted on the HTML side (used occasionally for content)
654 * the value returned is a newly-created Element wrapping around the existing
657 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
658 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, false );
659 // Verify that the type matches up.
660 // FIXME: uncomment after T89721 is fixed (see T90929)
662 if ( !( obj instanceof this['class'] ) ) {
663 throw new Error( 'Infusion type mismatch!' );
670 * Implementation helper for `infuse`; skips the type check and has an
671 * extra property so that only the top-level invocation touches the DOM.
674 * @param {string|HTMLElement|jQuery} idOrNode
675 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
676 * when the top-level widget of this infusion is inserted into DOM,
677 * replacing the original node; or false for top-level invocation.
678 * @return {OO.ui.Element}
680 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, domPromise
) {
681 // look for a cached result of a previous infusion.
682 var id
, $elem
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
683 if ( typeof idOrNode
=== 'string' ) {
685 $elem
= $( document
.getElementById( id
) );
687 $elem
= $( idOrNode
);
688 id
= $elem
.attr( 'id' );
690 if ( !$elem
.length
) {
691 throw new Error( 'Widget not found: ' + id
);
693 if ( $elem
[ 0 ].oouiInfused
) {
694 $elem
= $elem
[ 0 ].oouiInfused
;
696 data
= $elem
.data( 'ooui-infused' );
699 if ( data
=== true ) {
700 throw new Error( 'Circular dependency! ' + id
);
703 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
704 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
705 // restore dynamic state after the new element is re-inserted into DOM under infused parent
706 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
707 infusedChildren
= $elem
.data( 'ooui-infused-children' );
708 if ( infusedChildren
&& infusedChildren
.length
) {
709 infusedChildren
.forEach( function ( data
) {
710 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
711 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
717 data
= $elem
.attr( 'data-ooui' );
719 throw new Error( 'No infusion data found: ' + id
);
722 data
= $.parseJSON( data
);
726 if ( !( data
&& data
._
) ) {
727 throw new Error( 'No valid infusion data found: ' + id
);
729 if ( data
._
=== 'Tag' ) {
730 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
731 return new OO
.ui
.Element( { $element
: $elem
} );
733 parts
= data
._
.split( '.' );
734 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
735 if ( cls
=== undefined ) {
736 // The PHP output might be old and not including the "OO.ui" prefix
737 // TODO: Remove this back-compat after next major release
738 cls
= OO
.getProp
.apply( OO
, [ OO
.ui
].concat( parts
) );
739 if ( cls
=== undefined ) {
740 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
744 // Verify that we're creating an OO.ui.Element instance
747 while ( parent
!== undefined ) {
748 if ( parent
=== OO
.ui
.Element
) {
753 parent
= parent
.parent
;
756 if ( parent
!== OO
.ui
.Element
) {
757 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
760 if ( domPromise
=== false ) {
762 domPromise
= top
.promise();
764 $elem
.data( 'ooui-infused', true ); // prevent loops
765 data
.id
= id
; // implicit
766 infusedChildren
= [];
767 data
= OO
.copy( data
, null, function deserialize( value
) {
769 if ( OO
.isPlainObject( value
) ) {
771 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, domPromise
);
772 infusedChildren
.push( infused
);
773 // Flatten the structure
774 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
775 infused
.$element
.removeData( 'ooui-infused-children' );
778 if ( value
.html
!== undefined ) {
779 return new OO
.ui
.HtmlSnippet( value
.html
);
783 // allow widgets to reuse parts of the DOM
784 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
785 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
786 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
788 // eslint-disable-next-line new-cap
789 obj
= new cls( data
);
790 // now replace old DOM with this new DOM.
792 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
793 // so only mutate the DOM if we need to.
794 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
795 $elem
.replaceWith( obj
.$element
);
796 // This element is now gone from the DOM, but if anyone is holding a reference to it,
797 // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
798 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
799 $elem
[ 0 ].oouiInfused
= obj
.$element
;
803 obj
.$element
.data( 'ooui-infused', obj
);
804 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
805 // set the 'data-ooui' attribute so we can identify infused widgets
806 obj
.$element
.attr( 'data-ooui', '' );
807 // restore dynamic state after the new element is inserted into DOM
808 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
813 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
815 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
816 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
817 * constructor, which will be given the enhanced config.
820 * @param {HTMLElement} node
821 * @param {Object} config
824 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
829 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
830 * (and its children) that represent an Element of the same class and the given configuration,
831 * generated by the PHP implementation.
833 * This method is called just before `node` is detached from the DOM. The return value of this
834 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
835 * is inserted into DOM to replace `node`.
838 * @param {HTMLElement} node
839 * @param {Object} config
842 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
847 * Get a jQuery function within a specific document.
850 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
851 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
853 * @return {Function} Bound jQuery function
855 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
856 function wrapper( selector
) {
857 return $( selector
, wrapper
.context
);
860 wrapper
.context
= this.getDocument( context
);
863 wrapper
.$iframe
= $iframe
;
870 * Get the document of an element.
873 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
874 * @return {HTMLDocument|null} Document object
876 OO
.ui
.Element
.static.getDocument = function ( obj
) {
877 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
878 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
879 // Empty jQuery selections might have a context
886 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
891 * Get the window of an element or document.
894 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
895 * @return {Window} Window object
897 OO
.ui
.Element
.static.getWindow = function ( obj
) {
898 var doc
= this.getDocument( obj
);
899 return doc
.defaultView
;
903 * Get the direction of an element or document.
906 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
907 * @return {string} Text direction, either 'ltr' or 'rtl'
909 OO
.ui
.Element
.static.getDir = function ( obj
) {
912 if ( obj
instanceof jQuery
) {
915 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
916 isWin
= obj
.document
!== undefined;
917 if ( isDoc
|| isWin
) {
923 return $( obj
).css( 'direction' );
927 * Get the offset between two frames.
929 * TODO: Make this function not use recursion.
932 * @param {Window} from Window of the child frame
933 * @param {Window} [to=window] Window of the parent frame
934 * @param {Object} [offset] Offset to start with, used internally
935 * @return {Object} Offset object, containing left and top properties
937 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
938 var i
, len
, frames
, frame
, rect
;
944 offset
= { top
: 0, left
: 0 };
946 if ( from.parent
=== from ) {
950 // Get iframe element
951 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
952 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
953 if ( frames
[ i
].contentWindow
=== from ) {
959 // Recursively accumulate offset values
961 rect
= frame
.getBoundingClientRect();
962 offset
.left
+= rect
.left
;
963 offset
.top
+= rect
.top
;
965 this.getFrameOffset( from.parent
, offset
);
972 * Get the offset between two elements.
974 * The two elements may be in a different frame, but in that case the frame $element is in must
975 * be contained in the frame $anchor is in.
978 * @param {jQuery} $element Element whose position to get
979 * @param {jQuery} $anchor Element to get $element's position relative to
980 * @return {Object} Translated position coordinates, containing top and left properties
982 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
983 var iframe
, iframePos
,
984 pos
= $element
.offset(),
985 anchorPos
= $anchor
.offset(),
986 elementDocument
= this.getDocument( $element
),
987 anchorDocument
= this.getDocument( $anchor
);
989 // If $element isn't in the same document as $anchor, traverse up
990 while ( elementDocument
!== anchorDocument
) {
991 iframe
= elementDocument
.defaultView
.frameElement
;
993 throw new Error( '$element frame is not contained in $anchor frame' );
995 iframePos
= $( iframe
).offset();
996 pos
.left
+= iframePos
.left
;
997 pos
.top
+= iframePos
.top
;
998 elementDocument
= iframe
.ownerDocument
;
1000 pos
.left
-= anchorPos
.left
;
1001 pos
.top
-= anchorPos
.top
;
1006 * Get element border sizes.
1009 * @param {HTMLElement} el Element to measure
1010 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1012 OO
.ui
.Element
.static.getBorders = function ( el
) {
1013 var doc
= el
.ownerDocument
,
1014 win
= doc
.defaultView
,
1015 style
= win
.getComputedStyle( el
, null ),
1017 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1018 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1019 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1020 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1031 * Get dimensions of an element or window.
1034 * @param {HTMLElement|Window} el Element to measure
1035 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1037 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1039 doc
= el
.ownerDocument
|| el
.document
,
1040 win
= doc
.defaultView
;
1042 if ( win
=== el
|| el
=== doc
.documentElement
) {
1045 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1047 top
: $win
.scrollTop(),
1048 left
: $win
.scrollLeft()
1050 scrollbar
: { right
: 0, bottom
: 0 },
1054 bottom
: $win
.innerHeight(),
1055 right
: $win
.innerWidth()
1061 borders
: this.getBorders( el
),
1063 top
: $el
.scrollTop(),
1064 left
: $el
.scrollLeft()
1067 right
: $el
.innerWidth() - el
.clientWidth
,
1068 bottom
: $el
.innerHeight() - el
.clientHeight
1070 rect
: el
.getBoundingClientRect()
1076 * Get the number of pixels that an element's content is scrolled to the left.
1078 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1079 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1081 * This function smooths out browser inconsistencies (nicely described in the README at
1082 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1083 * with Firefox's 'scrollLeft', which seems the sanest.
1087 * @param {HTMLElement|Window} el Element to measure
1088 * @return {number} Scroll position from the left.
1089 * If the element's direction is LTR, this is a positive number between `0` (initial scroll position)
1090 * and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1091 * If the element's direction is RTL, this is a negative number between `0` (initial scroll position)
1092 * and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1094 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1095 var rtlScrollType
= null;
1098 var $definer
= $( '<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>' ),
1099 definer
= $definer
[ 0 ];
1101 $definer
.appendTo( 'body' );
1102 if ( definer
.scrollLeft
> 0 ) {
1104 rtlScrollType
= 'default';
1106 definer
.scrollLeft
= 1;
1107 if ( definer
.scrollLeft
=== 0 ) {
1108 // Firefox, old Opera
1109 rtlScrollType
= 'negative';
1111 // Internet Explorer, Edge
1112 rtlScrollType
= 'reverse';
1118 return function getScrollLeft( el
) {
1119 var isRoot
= el
.window
=== el
||
1120 el
=== el
.ownerDocument
.body
||
1121 el
=== el
.ownerDocument
.documentElement
,
1122 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1123 // All browsers use the correct scroll type ('negative') on the root, so don't
1124 // do any fixups when looking at the root element
1125 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1127 if ( direction
=== 'rtl' ) {
1128 if ( rtlScrollType
=== null ) {
1131 if ( rtlScrollType
=== 'reverse' ) {
1132 scrollLeft
= -scrollLeft
;
1133 } else if ( rtlScrollType
=== 'default' ) {
1134 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1143 * Get the root scrollable element of given element's document.
1145 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1146 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1147 * lets us use 'body' or 'documentElement' based on what is working.
1149 * https://code.google.com/p/chromium/issues/detail?id=303131
1152 * @param {HTMLElement} el Element to find root scrollable parent for
1153 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1154 * depending on browser
1156 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1157 var scrollTop
, body
;
1159 if ( OO
.ui
.scrollableElement
=== undefined ) {
1160 body
= el
.ownerDocument
.body
;
1161 scrollTop
= body
.scrollTop
;
1164 if ( body
.scrollTop
=== 1 ) {
1165 body
.scrollTop
= scrollTop
;
1166 OO
.ui
.scrollableElement
= 'body';
1168 OO
.ui
.scrollableElement
= 'documentElement';
1172 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1176 * Get closest scrollable container.
1178 * Traverses up until either a scrollable element or the root is reached, in which case the root
1179 * scrollable element will be returned (see #getRootScrollableElement).
1182 * @param {HTMLElement} el Element to find scrollable container for
1183 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1184 * @return {HTMLElement} Closest scrollable container
1186 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1188 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1189 // 'overflow-y' have different values, so we need to check the separate properties.
1190 props
= [ 'overflow-x', 'overflow-y' ],
1191 $parent
= $( el
).parent();
1193 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1194 props
= [ 'overflow-' + dimension
];
1197 // Special case for the document root (which doesn't really have any scrollable container, since
1198 // it is the ultimate scrollable container, but this is probably saner than null or exception)
1199 if ( $( el
).is( 'html, body' ) ) {
1200 return this.getRootScrollableElement( el
);
1203 while ( $parent
.length
) {
1204 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1205 return $parent
[ 0 ];
1209 val
= $parent
.css( props
[ i
] );
1210 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be
1211 // scrolled in that direction, but they can actually be scrolled programatically. The user can
1212 // unintentionally perform a scroll in such case even if the application doesn't scroll
1213 // programatically, e.g. when jumping to an anchor, or when using built-in find functionality.
1214 // This could cause funny issues...
1215 if ( val
=== 'auto' || val
=== 'scroll' ) {
1216 return $parent
[ 0 ];
1219 $parent
= $parent
.parent();
1221 // The element is unattached... return something mostly sane
1222 return this.getRootScrollableElement( el
);
1226 * Scroll element into view.
1229 * @param {HTMLElement} el Element to scroll into view
1230 * @param {Object} [config] Configuration options
1231 * @param {string} [config.duration='fast'] jQuery animation duration value
1232 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1233 * to scroll in both directions
1234 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1236 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1237 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1238 deferred
= $.Deferred();
1240 // Configuration initialization
1241 config
= config
|| {};
1244 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1245 $container
= $( container
);
1246 elementDimensions
= this.getDimensions( el
);
1247 containerDimensions
= this.getDimensions( container
);
1248 $window
= $( this.getWindow( el
) );
1250 // Compute the element's position relative to the container
1251 if ( $container
.is( 'html, body' ) ) {
1252 // If the scrollable container is the root, this is easy
1254 top
: elementDimensions
.rect
.top
,
1255 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1256 left
: elementDimensions
.rect
.left
,
1257 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1260 // Otherwise, we have to subtract el's coordinates from container's coordinates
1262 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1263 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1264 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1265 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1269 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1270 if ( position
.top
< 0 ) {
1271 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1272 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1273 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1276 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1277 if ( position
.left
< 0 ) {
1278 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1279 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1280 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1283 if ( !$.isEmptyObject( animations
) ) {
1284 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1285 $container
.queue( function ( next
) {
1292 return deferred
.promise();
1296 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1297 * and reserve space for them, because it probably doesn't.
1299 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1300 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1301 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1302 * and then reattach (or show) them back.
1305 * @param {HTMLElement} el Element to reconsider the scrollbars on
1307 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1308 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1309 // Save scroll position
1310 scrollLeft
= el
.scrollLeft
;
1311 scrollTop
= el
.scrollTop
;
1312 // Detach all children
1313 while ( el
.firstChild
) {
1314 nodes
.push( el
.firstChild
);
1315 el
.removeChild( el
.firstChild
);
1318 void el
.offsetHeight
;
1319 // Reattach all children
1320 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1321 el
.appendChild( nodes
[ i
] );
1323 // Restore scroll position (no-op if scrollbars disappeared)
1324 el
.scrollLeft
= scrollLeft
;
1325 el
.scrollTop
= scrollTop
;
1331 * Toggle visibility of an element.
1333 * @param {boolean} [show] Make element visible, omit to toggle visibility
1337 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1338 show
= show
=== undefined ? !this.visible
: !!show
;
1340 if ( show
!== this.isVisible() ) {
1341 this.visible
= show
;
1342 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1343 this.emit( 'toggle', show
);
1350 * Check if element is visible.
1352 * @return {boolean} element is visible
1354 OO
.ui
.Element
.prototype.isVisible = function () {
1355 return this.visible
;
1361 * @return {Mixed} Element data
1363 OO
.ui
.Element
.prototype.getData = function () {
1370 * @param {Mixed} data Element data
1373 OO
.ui
.Element
.prototype.setData = function ( data
) {
1379 * Check if element supports one or more methods.
1381 * @param {string|string[]} methods Method or list of methods to check
1382 * @return {boolean} All methods are supported
1384 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1388 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1389 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1390 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1395 return methods
.length
=== support
;
1399 * Update the theme-provided classes.
1401 * @localdoc This is called in element mixins and widget classes any time state changes.
1402 * Updating is debounced, minimizing overhead of changing multiple attributes and
1403 * guaranteeing that theme updates do not occur within an element's constructor
1405 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1406 OO
.ui
.theme
.queueUpdateElementClasses( this );
1410 * Get the HTML tag name.
1412 * Override this method to base the result on instance information.
1414 * @return {string} HTML tag name
1416 OO
.ui
.Element
.prototype.getTagName = function () {
1417 return this.constructor.static.tagName
;
1421 * Check if the element is attached to the DOM
1423 * @return {boolean} The element is attached to the DOM
1425 OO
.ui
.Element
.prototype.isElementAttached = function () {
1426 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1430 * Get the DOM document.
1432 * @return {HTMLDocument} Document object
1434 OO
.ui
.Element
.prototype.getElementDocument = function () {
1435 // Don't cache this in other ways either because subclasses could can change this.$element
1436 return OO
.ui
.Element
.static.getDocument( this.$element
);
1440 * Get the DOM window.
1442 * @return {Window} Window object
1444 OO
.ui
.Element
.prototype.getElementWindow = function () {
1445 return OO
.ui
.Element
.static.getWindow( this.$element
);
1449 * Get closest scrollable container.
1451 * @return {HTMLElement} Closest scrollable container
1453 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1454 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1458 * Get group element is in.
1460 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1462 OO
.ui
.Element
.prototype.getElementGroup = function () {
1463 return this.elementGroup
;
1467 * Set group element is in.
1469 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1472 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1473 this.elementGroup
= group
;
1478 * Scroll element into view.
1480 * @param {Object} [config] Configuration options
1481 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1483 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1485 !this.isElementAttached() ||
1486 !this.isVisible() ||
1487 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1489 return $.Deferred().resolve();
1491 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1495 * Restore the pre-infusion dynamic state for this widget.
1497 * This method is called after #$element has been inserted into DOM. The parameter is the return
1498 * value of #gatherPreInfuseState.
1501 * @param {Object} state
1503 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1507 * Wraps an HTML snippet for use with configuration values which default
1508 * to strings. This bypasses the default html-escaping done to string
1514 * @param {string} [content] HTML content
1516 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1518 this.content
= content
;
1523 OO
.initClass( OO
.ui
.HtmlSnippet
);
1530 * @return {string} Unchanged HTML snippet.
1532 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1533 return this.content
;
1537 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1538 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1539 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1540 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1541 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1545 * @extends OO.ui.Element
1546 * @mixins OO.EventEmitter
1549 * @param {Object} [config] Configuration options
1551 OO
.ui
.Layout
= function OoUiLayout( config
) {
1552 // Configuration initialization
1553 config
= config
|| {};
1555 // Parent constructor
1556 OO
.ui
.Layout
.parent
.call( this, config
);
1558 // Mixin constructors
1559 OO
.EventEmitter
.call( this );
1562 this.$element
.addClass( 'oo-ui-layout' );
1567 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1568 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1571 * Widgets are compositions of one or more OOjs UI elements that users can both view
1572 * and interact with. All widgets can be configured and modified via a standard API,
1573 * and their state can change dynamically according to a model.
1577 * @extends OO.ui.Element
1578 * @mixins OO.EventEmitter
1581 * @param {Object} [config] Configuration options
1582 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1583 * appearance reflects this state.
1585 OO
.ui
.Widget
= function OoUiWidget( config
) {
1586 // Initialize config
1587 config
= $.extend( { disabled
: false }, config
);
1589 // Parent constructor
1590 OO
.ui
.Widget
.parent
.call( this, config
);
1592 // Mixin constructors
1593 OO
.EventEmitter
.call( this );
1596 this.disabled
= null;
1597 this.wasDisabled
= null;
1600 this.$element
.addClass( 'oo-ui-widget' );
1601 this.setDisabled( !!config
.disabled
);
1606 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1607 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1609 /* Static Properties */
1612 * Whether this widget will behave reasonably when wrapped in an HTML `<label>`. If this is true,
1613 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1618 * @property {boolean}
1620 OO
.ui
.Widget
.static.supportsSimpleLabel
= false;
1627 * A 'disable' event is emitted when the disabled state of the widget changes
1628 * (i.e. on disable **and** enable).
1630 * @param {boolean} disabled Widget is disabled
1636 * A 'toggle' event is emitted when the visibility of the widget changes.
1638 * @param {boolean} visible Widget is visible
1644 * Check if the widget is disabled.
1646 * @return {boolean} Widget is disabled
1648 OO
.ui
.Widget
.prototype.isDisabled = function () {
1649 return this.disabled
;
1653 * Set the 'disabled' state of the widget.
1655 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1657 * @param {boolean} disabled Disable widget
1660 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1663 this.disabled
= !!disabled
;
1664 isDisabled
= this.isDisabled();
1665 if ( isDisabled
!== this.wasDisabled
) {
1666 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1667 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1668 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1669 this.emit( 'disable', isDisabled
);
1670 this.updateThemeClasses();
1672 this.wasDisabled
= isDisabled
;
1678 * Update the disabled state, in case of changes in parent widget.
1682 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1683 this.setDisabled( this.disabled
);
1695 OO
.ui
.Theme
= function OoUiTheme() {
1696 this.elementClassesQueue
= [];
1697 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1702 OO
.initClass( OO
.ui
.Theme
);
1707 * Get a list of classes to be applied to a widget.
1709 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1710 * otherwise state transitions will not work properly.
1712 * @param {OO.ui.Element} element Element for which to get classes
1713 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1715 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1716 return { on
: [], off
: [] };
1720 * Update CSS classes provided by the theme.
1722 * For elements with theme logic hooks, this should be called any time there's a state change.
1724 * @param {OO.ui.Element} element Element for which to update classes
1726 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1727 var $elements
= $( [] ),
1728 classes
= this.getElementClasses( element
);
1730 if ( element
.$icon
) {
1731 $elements
= $elements
.add( element
.$icon
);
1733 if ( element
.$indicator
) {
1734 $elements
= $elements
.add( element
.$indicator
);
1738 .removeClass( classes
.off
.join( ' ' ) )
1739 .addClass( classes
.on
.join( ' ' ) );
1745 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1747 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1748 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1751 this.elementClassesQueue
= [];
1755 * Queue #updateElementClasses to be called for this element.
1757 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1758 * to make them synchronous.
1760 * @param {OO.ui.Element} element Element for which to update classes
1762 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1763 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1764 // the most common case (this method is often called repeatedly for the same element).
1765 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1768 this.elementClassesQueue
.push( element
);
1769 this.debouncedUpdateQueuedElementClasses();
1773 * Get the transition duration in milliseconds for dialogs opening/closing
1775 * The dialog should be fully rendered this many milliseconds after the
1776 * ready process has executed.
1778 * @return {number} Transition duration in milliseconds
1780 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1785 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1786 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1787 * order in which users will navigate through the focusable elements via the "tab" key.
1790 * // TabIndexedElement is mixed into the ButtonWidget class
1791 * // to provide a tabIndex property.
1792 * var button1 = new OO.ui.ButtonWidget( {
1796 * var button2 = new OO.ui.ButtonWidget( {
1800 * var button3 = new OO.ui.ButtonWidget( {
1804 * var button4 = new OO.ui.ButtonWidget( {
1808 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1814 * @param {Object} [config] Configuration options
1815 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1816 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1817 * functionality will be applied to it instead.
1818 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1819 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1820 * to remove the element from the tab-navigation flow.
1822 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1823 // Configuration initialization
1824 config
= $.extend( { tabIndex
: 0 }, config
);
1827 this.$tabIndexed
= null;
1828 this.tabIndex
= null;
1831 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1834 this.setTabIndex( config
.tabIndex
);
1835 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1840 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1845 * Set the element that should use the tabindex functionality.
1847 * This method is used to retarget a tabindex mixin so that its functionality applies
1848 * to the specified element. If an element is currently using the functionality, the mixin’s
1849 * effect on that element is removed before the new element is set up.
1851 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1854 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1855 var tabIndex
= this.tabIndex
;
1856 // Remove attributes from old $tabIndexed
1857 this.setTabIndex( null );
1858 // Force update of new $tabIndexed
1859 this.$tabIndexed
= $tabIndexed
;
1860 this.tabIndex
= tabIndex
;
1861 return this.updateTabIndex();
1865 * Set the value of the tabindex.
1867 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
1870 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1871 tabIndex
= typeof tabIndex
=== 'number' ? tabIndex
: null;
1873 if ( this.tabIndex
!== tabIndex
) {
1874 this.tabIndex
= tabIndex
;
1875 this.updateTabIndex();
1882 * Update the `tabindex` attribute, in case of changes to tab index or
1888 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1889 if ( this.$tabIndexed
) {
1890 if ( this.tabIndex
!== null ) {
1891 // Do not index over disabled elements
1892 this.$tabIndexed
.attr( {
1893 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1894 // Support: ChromeVox and NVDA
1895 // These do not seem to inherit aria-disabled from parent elements
1896 'aria-disabled': this.isDisabled().toString()
1899 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1906 * Handle disable events.
1909 * @param {boolean} disabled Element is disabled
1911 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1912 this.updateTabIndex();
1916 * Get the value of the tabindex.
1918 * @return {number|null} Tabindex value
1920 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
1921 return this.tabIndex
;
1925 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
1926 * interface element that can be configured with access keys for accessibility.
1927 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
1929 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
1935 * @param {Object} [config] Configuration options
1936 * @cfg {jQuery} [$button] The button element created by the class.
1937 * If this configuration is omitted, the button element will use a generated `<a>`.
1938 * @cfg {boolean} [framed=true] Render the button with a frame
1940 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
1941 // Configuration initialization
1942 config
= config
|| {};
1945 this.$button
= null;
1947 this.active
= config
.active
!== undefined && config
.active
;
1948 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
1949 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
1950 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
1951 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
1952 this.onClickHandler
= this.onClick
.bind( this );
1953 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
1956 this.$element
.addClass( 'oo-ui-buttonElement' );
1957 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
1958 this.setButtonElement( config
.$button
|| $( '<a>' ) );
1963 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
1965 /* Static Properties */
1968 * Cancel mouse down events.
1970 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
1971 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
1972 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
1977 * @property {boolean}
1979 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
1984 * A 'click' event is emitted when the button element is clicked.
1992 * Set the button element.
1994 * This method is used to retarget a button mixin so that its functionality applies to
1995 * the specified button element instead of the one created by the class. If a button element
1996 * is already set, the method will remove the mixin’s effect on that element.
1998 * @param {jQuery} $button Element to use as button
2000 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2001 if ( this.$button
) {
2003 .removeClass( 'oo-ui-buttonElement-button' )
2004 .removeAttr( 'role accesskey' )
2006 mousedown
: this.onMouseDownHandler
,
2007 keydown
: this.onKeyDownHandler
,
2008 click
: this.onClickHandler
,
2009 keypress
: this.onKeyPressHandler
2013 this.$button
= $button
2014 .addClass( 'oo-ui-buttonElement-button' )
2016 mousedown
: this.onMouseDownHandler
,
2017 keydown
: this.onKeyDownHandler
,
2018 click
: this.onClickHandler
,
2019 keypress
: this.onKeyPressHandler
2022 // Add `role="button"` on `<a>` elements, where it's needed
2023 // `toUppercase()` is added for XHTML documents
2024 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2025 this.$button
.attr( 'role', 'button' );
2030 * Handles mouse down events.
2033 * @param {jQuery.Event} e Mouse down event
2035 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2036 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2039 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2040 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2041 // reliably remove the pressed class
2042 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
2043 // Prevent change of focus unless specifically configured otherwise
2044 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2050 * Handles mouse up events.
2053 * @param {MouseEvent} e Mouse up event
2055 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
2056 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2059 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2060 // Stop listening for mouseup, since we only needed this once
2061 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
2065 * Handles mouse click events.
2068 * @param {jQuery.Event} e Mouse click event
2071 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2072 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2073 if ( this.emit( 'click' ) ) {
2080 * Handles key down events.
2083 * @param {jQuery.Event} e Key down event
2085 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2086 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2089 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2090 // Run the keyup handler no matter where the key is when the button is let go, so we can
2091 // reliably remove the pressed class
2092 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
2096 * Handles key up events.
2099 * @param {KeyboardEvent} e Key up event
2101 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
2102 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2105 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2106 // Stop listening for keyup, since we only needed this once
2107 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
2111 * Handles key press events.
2114 * @param {jQuery.Event} e Key press event
2117 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2118 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2119 if ( this.emit( 'click' ) ) {
2126 * Check if button has a frame.
2128 * @return {boolean} Button is framed
2130 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2135 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2137 * @param {boolean} [framed] Make button framed, omit to toggle
2140 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2141 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2142 if ( framed
!== this.framed
) {
2143 this.framed
= framed
;
2145 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2146 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2147 this.updateThemeClasses();
2154 * Set the button's active state.
2156 * The active state can be set on:
2158 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2159 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2160 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2163 * @param {boolean} value Make button active
2166 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2167 this.active
= !!value
;
2168 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2169 this.updateThemeClasses();
2174 * Check if the button is active
2177 * @return {boolean} The button is active
2179 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2184 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2185 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2186 * items from the group is done through the interface the class provides.
2187 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2189 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
2192 * @mixins OO.EmitterList
2196 * @param {Object} [config] Configuration options
2197 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2198 * is omitted, the group element will use a generated `<div>`.
2200 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2201 // Configuration initialization
2202 config
= config
|| {};
2204 // Mixin constructors
2205 OO
.EmitterList
.call( this, config
);
2211 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2216 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2223 * A change event is emitted when the set of selected items changes.
2225 * @param {OO.ui.Element[]} items Items currently in the group
2231 * Set the group element.
2233 * If an element is already set, items will be moved to the new element.
2235 * @param {jQuery} $group Element to use as group
2237 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2240 this.$group
= $group
;
2241 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2242 this.$group
.append( this.items
[ i
].$element
);
2247 * Get an item by its data.
2249 * Only the first item with matching data will be returned. To return all matching items,
2250 * use the #getItemsFromData method.
2252 * @param {Object} data Item data to search for
2253 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2255 OO
.ui
.mixin
.GroupElement
.prototype.getItemFromData = function ( data
) {
2257 hash
= OO
.getHash( data
);
2259 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2260 item
= this.items
[ i
];
2261 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2270 * Get items by their data.
2272 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2274 * @param {Object} data Item data to search for
2275 * @return {OO.ui.Element[]} Items with equivalent data
2277 OO
.ui
.mixin
.GroupElement
.prototype.getItemsFromData = function ( data
) {
2279 hash
= OO
.getHash( data
),
2282 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2283 item
= this.items
[ i
];
2284 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2293 * Add items to the group.
2295 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2296 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2298 * @param {OO.ui.Element[]} items An array of items to add to the group
2299 * @param {number} [index] Index of the insertion point
2302 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2307 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2309 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2313 item
.setElementGroup( this );
2314 itemElements
.push( item
.$element
.get( 0 ) );
2317 this.insertItemElements( items
, index
);
2319 this.emit( 'change', this.getItems() );
2326 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2328 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2330 this.insertItemElements( items
, newIndex
);
2338 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2340 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2342 this.insertItemElements( item
, index
);
2348 * Insert element into the group
2350 * @param {OO.ui.Element|OO.ui.Element[]} itemWidgets Items to insert
2351 * @param {number} index Insertion index
2353 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidgets
, index
) {
2356 if ( !Array
.isArray( itemWidgets
) ) {
2357 itemWidgets
= [ itemWidgets
];
2360 for ( i
= 0, len
= itemWidgets
.length
; i
< len
; i
++ ) {
2361 item
= itemWidgets
[ i
];
2363 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2364 this.$group
.append( item
.$element
.get( 0 ) );
2365 } else if ( index
=== 0 ) {
2366 this.$group
.prepend( item
.$element
.get( 0 ) );
2368 this.items
[ index
].$element
.before( item
.$element
.get( 0 ) );
2374 * Remove the specified items from a group.
2376 * Removed items are detached (not removed) from the DOM so that they may be reused.
2377 * To remove all items from a group, you may wish to use the #clearItems method instead.
2379 * @param {OO.ui.Element[]} items An array of items to remove
2382 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2383 var i
, len
, item
, index
;
2385 // Remove specific items elements
2386 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2388 index
= this.items
.indexOf( item
);
2389 if ( index
!== -1 ) {
2390 item
.setElementGroup( null );
2391 item
.$element
.detach();
2396 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2398 this.emit( 'change', this.getItems() );
2403 * Clear all items from the group.
2405 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2406 * To remove only a subset of items from a group, use the #removeItems method.
2410 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2413 // Remove all item elements
2414 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2415 this.items
[ i
].setElementGroup( null );
2416 this.items
[ i
].$element
.detach();
2420 OO
.EmitterList
.prototype.clearItems
.call( this );
2422 this.emit( 'change', this.getItems() );
2427 * IconElement is often mixed into other classes to generate an icon.
2428 * Icons are graphics, about the size of normal text. They are used to aid the user
2429 * in locating a control or to convey information in a space-efficient way. See the
2430 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2431 * included in the library.
2433 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2439 * @param {Object} [config] Configuration options
2440 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2441 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2442 * the icon element be set to an existing icon instead of the one generated by this class, set a
2443 * value using a jQuery selection. For example:
2445 * // Use a <div> tag instead of a <span>
2447 * // Use an existing icon element instead of the one generated by the class
2448 * $icon: this.$element
2449 * // Use an icon element from a child widget
2450 * $icon: this.childwidget.$element
2451 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2452 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2453 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2454 * by the user's language.
2456 * Example of an i18n map:
2458 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2459 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2460 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2461 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2462 * text. The icon title is displayed when users move the mouse over the icon.
2464 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2465 // Configuration initialization
2466 config
= config
|| {};
2471 this.iconTitle
= null;
2474 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2475 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2476 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2481 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2483 /* Static Properties */
2486 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2487 * for i18n purposes and contains a `default` icon name and additional names keyed by
2488 * language code. The `default` name is used when no icon is keyed by the user's language.
2490 * Example of an i18n map:
2492 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2494 * Note: the static property will be overridden if the #icon configuration is used.
2498 * @property {Object|string}
2500 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2503 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2504 * function that returns title text, or `null` for no title.
2506 * The static property will be overridden if the #iconTitle configuration is used.
2510 * @property {string|Function|null}
2512 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2517 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2518 * applies to the specified icon element instead of the one created by the class. If an icon
2519 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2520 * and mixin methods will no longer affect the element.
2522 * @param {jQuery} $icon Element to use as icon
2524 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2527 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2528 .removeAttr( 'title' );
2532 .addClass( 'oo-ui-iconElement-icon' )
2533 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2534 if ( this.iconTitle
!== null ) {
2535 this.$icon
.attr( 'title', this.iconTitle
);
2538 this.updateThemeClasses();
2542 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2543 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2546 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2547 * by language code, or `null` to remove the icon.
2550 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2551 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2552 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2554 if ( this.icon
!== icon
) {
2556 if ( this.icon
!== null ) {
2557 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2559 if ( icon
!== null ) {
2560 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2566 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2567 this.updateThemeClasses();
2573 * Set the icon title. Use `null` to remove the title.
2575 * @param {string|Function|null} iconTitle A text string used as the icon title,
2576 * a function that returns title text, or `null` for no title.
2579 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2580 iconTitle
= typeof iconTitle
=== 'function' ||
2581 ( typeof iconTitle
=== 'string' && iconTitle
.length
) ?
2582 OO
.ui
.resolveMsg( iconTitle
) : null;
2584 if ( this.iconTitle
!== iconTitle
) {
2585 this.iconTitle
= iconTitle
;
2587 if ( this.iconTitle
!== null ) {
2588 this.$icon
.attr( 'title', iconTitle
);
2590 this.$icon
.removeAttr( 'title' );
2599 * Get the symbolic name of the icon.
2601 * @return {string} Icon name
2603 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2608 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2610 * @return {string} Icon title text
2612 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2613 return this.iconTitle
;
2617 * IndicatorElement is often mixed into other classes to generate an indicator.
2618 * Indicators are small graphics that are generally used in two ways:
2620 * - To draw attention to the status of an item. For example, an indicator might be
2621 * used to show that an item in a list has errors that need to be resolved.
2622 * - To clarify the function of a control that acts in an exceptional way (a button
2623 * that opens a menu instead of performing an action directly, for example).
2625 * For a list of indicators included in the library, please see the
2626 * [OOjs UI documentation on MediaWiki] [1].
2628 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2634 * @param {Object} [config] Configuration options
2635 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2636 * configuration is omitted, the indicator element will use a generated `<span>`.
2637 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2638 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2640 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2641 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2642 * or a function that returns title text. The indicator title is displayed when users move
2643 * the mouse over the indicator.
2645 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2646 // Configuration initialization
2647 config
= config
|| {};
2650 this.$indicator
= null;
2651 this.indicator
= null;
2652 this.indicatorTitle
= null;
2655 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2656 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2657 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2662 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2664 /* Static Properties */
2667 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2668 * The static property will be overridden if the #indicator configuration is used.
2672 * @property {string|null}
2674 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2677 * A text string used as the indicator title, a function that returns title text, or `null`
2678 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2682 * @property {string|Function|null}
2684 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2689 * Set the indicator element.
2691 * If an element is already set, it will be cleaned up before setting up the new element.
2693 * @param {jQuery} $indicator Element to use as indicator
2695 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2696 if ( this.$indicator
) {
2698 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2699 .removeAttr( 'title' );
2702 this.$indicator
= $indicator
2703 .addClass( 'oo-ui-indicatorElement-indicator' )
2704 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2705 if ( this.indicatorTitle
!== null ) {
2706 this.$indicator
.attr( 'title', this.indicatorTitle
);
2709 this.updateThemeClasses();
2713 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2715 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2718 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2719 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2721 if ( this.indicator
!== indicator
) {
2722 if ( this.$indicator
) {
2723 if ( this.indicator
!== null ) {
2724 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2726 if ( indicator
!== null ) {
2727 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2730 this.indicator
= indicator
;
2733 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2734 this.updateThemeClasses();
2740 * Set the indicator title.
2742 * The title is displayed when a user moves the mouse over the indicator.
2744 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2745 * `null` for no indicator title
2748 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2749 indicatorTitle
= typeof indicatorTitle
=== 'function' ||
2750 ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ?
2751 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2753 if ( this.indicatorTitle
!== indicatorTitle
) {
2754 this.indicatorTitle
= indicatorTitle
;
2755 if ( this.$indicator
) {
2756 if ( this.indicatorTitle
!== null ) {
2757 this.$indicator
.attr( 'title', indicatorTitle
);
2759 this.$indicator
.removeAttr( 'title' );
2768 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2770 * @return {string} Symbolic name of indicator
2772 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2773 return this.indicator
;
2777 * Get the indicator title.
2779 * The title is displayed when a user moves the mouse over the indicator.
2781 * @return {string} Indicator title text
2783 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2784 return this.indicatorTitle
;
2788 * LabelElement is often mixed into other classes to generate a label, which
2789 * helps identify the function of an interface element.
2790 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2792 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2798 * @param {Object} [config] Configuration options
2799 * @cfg {jQuery} [$label] The label element created by the class. If this
2800 * configuration is omitted, the label element will use a generated `<span>`.
2801 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2802 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2803 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2804 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2806 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2807 // Configuration initialization
2808 config
= config
|| {};
2815 this.setLabel( config
.label
|| this.constructor.static.label
);
2816 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2821 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2826 * @event labelChange
2827 * @param {string} value
2830 /* Static Properties */
2833 * The label text. The label can be specified as a plaintext string, a function that will
2834 * produce a string in the future, or `null` for no label. The static value will
2835 * be overridden if a label is specified with the #label config option.
2839 * @property {string|Function|null}
2841 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2843 /* Static methods */
2846 * Highlight the first occurrence of the query in the given text
2848 * @param {string} text Text
2849 * @param {string} query Query to find
2850 * @return {jQuery} Text with the first match of the query
2851 * sub-string wrapped in highlighted span
2853 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
) {
2854 var $result
= $( '<span>' ),
2855 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2857 if ( !query
.length
|| offset
=== -1 ) {
2858 return $result
.text( text
);
2861 document
.createTextNode( text
.slice( 0, offset
) ),
2863 .addClass( 'oo-ui-labelElement-label-highlight' )
2864 .text( text
.slice( offset
, offset
+ query
.length
) ),
2865 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2867 return $result
.contents();
2873 * Set the label element.
2875 * If an element is already set, it will be cleaned up before setting up the new element.
2877 * @param {jQuery} $label Element to use as label
2879 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2880 if ( this.$label
) {
2881 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2884 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2885 this.setLabelContent( this.label
);
2891 * An empty string will result in the label being hidden. A string containing only whitespace will
2892 * be converted to a single ` `.
2894 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2895 * text; or null for no label
2898 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2899 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2900 label
= ( ( typeof label
=== 'string' || label
instanceof jQuery
) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2902 if ( this.label
!== label
) {
2903 if ( this.$label
) {
2904 this.setLabelContent( label
);
2907 this.emit( 'labelChange' );
2910 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
);
2916 * Set the label as plain text with a highlighted query
2918 * @param {string} text Text label to set
2919 * @param {string} query Substring of text to highlight
2922 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
) {
2923 return this.setLabel( this.constructor.static.highlightQuery( text
, query
) );
2929 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2930 * text; or null for no label
2932 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2937 * Set the content of the label.
2939 * Do not call this method until after the label element has been set by #setLabelElement.
2942 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2943 * text; or null for no label
2945 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2946 if ( typeof label
=== 'string' ) {
2947 if ( label
.match( /^\s*$/ ) ) {
2948 // Convert whitespace only string to a single non-breaking space
2949 this.$label
.html( ' ' );
2951 this.$label
.text( label
);
2953 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2954 this.$label
.html( label
.toString() );
2955 } else if ( label
instanceof jQuery
) {
2956 this.$label
.empty().append( label
);
2958 this.$label
.empty();
2963 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
2964 * additional functionality to an element created by another class. The class provides
2965 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
2966 * which are used to customize the look and feel of a widget to better describe its
2967 * importance and functionality.
2969 * The library currently contains the following styling flags for general use:
2971 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
2972 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
2973 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
2975 * The flags affect the appearance of the buttons:
2978 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
2979 * var button1 = new OO.ui.ButtonWidget( {
2980 * label: 'Constructive',
2981 * flags: 'constructive'
2983 * var button2 = new OO.ui.ButtonWidget( {
2984 * label: 'Destructive',
2985 * flags: 'destructive'
2987 * var button3 = new OO.ui.ButtonWidget( {
2988 * label: 'Progressive',
2989 * flags: 'progressive'
2991 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
2993 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
2994 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
2996 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
3002 * @param {Object} [config] Configuration options
3003 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
3004 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
3005 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
3006 * @cfg {jQuery} [$flagged] The flagged element. By default,
3007 * the flagged functionality is applied to the element created by the class ($element).
3008 * If a different element is specified, the flagged functionality will be applied to it instead.
3010 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3011 // Configuration initialization
3012 config
= config
|| {};
3016 this.$flagged
= null;
3019 this.setFlags( config
.flags
);
3020 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3027 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3028 * parameter contains the name of each modified flag and indicates whether it was
3031 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3032 * that the flag was added, `false` that the flag was removed.
3038 * Set the flagged element.
3040 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3041 * If an element is already set, the method will remove the mixin’s effect on that element.
3043 * @param {jQuery} $flagged Element that should be flagged
3045 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3046 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3047 return 'oo-ui-flaggedElement-' + flag
;
3050 if ( this.$flagged
) {
3051 this.$flagged
.removeClass( classNames
);
3054 this.$flagged
= $flagged
.addClass( classNames
);
3058 * Check if the specified flag is set.
3060 * @param {string} flag Name of flag
3061 * @return {boolean} The flag is set
3063 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3064 // This may be called before the constructor, thus before this.flags is set
3065 return this.flags
&& ( flag
in this.flags
);
3069 * Get the names of all flags set.
3071 * @return {string[]} Flag names
3073 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3074 // This may be called before the constructor, thus before this.flags is set
3075 return Object
.keys( this.flags
|| {} );
3084 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3085 var flag
, className
,
3088 classPrefix
= 'oo-ui-flaggedElement-';
3090 for ( flag
in this.flags
) {
3091 className
= classPrefix
+ flag
;
3092 changes
[ flag
] = false;
3093 delete this.flags
[ flag
];
3094 remove
.push( className
);
3097 if ( this.$flagged
) {
3098 this.$flagged
.removeClass( remove
.join( ' ' ) );
3101 this.updateThemeClasses();
3102 this.emit( 'flag', changes
);
3108 * Add one or more flags.
3110 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3111 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3112 * be added (`true`) or removed (`false`).
3116 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3117 var i
, len
, flag
, className
,
3121 classPrefix
= 'oo-ui-flaggedElement-';
3123 if ( typeof flags
=== 'string' ) {
3124 className
= classPrefix
+ flags
;
3126 if ( !this.flags
[ flags
] ) {
3127 this.flags
[ flags
] = true;
3128 add
.push( className
);
3130 } else if ( Array
.isArray( flags
) ) {
3131 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3133 className
= classPrefix
+ flag
;
3135 if ( !this.flags
[ flag
] ) {
3136 changes
[ flag
] = true;
3137 this.flags
[ flag
] = true;
3138 add
.push( className
);
3141 } else if ( OO
.isPlainObject( flags
) ) {
3142 for ( flag
in flags
) {
3143 className
= classPrefix
+ flag
;
3144 if ( flags
[ flag
] ) {
3146 if ( !this.flags
[ flag
] ) {
3147 changes
[ flag
] = true;
3148 this.flags
[ flag
] = true;
3149 add
.push( className
);
3153 if ( this.flags
[ flag
] ) {
3154 changes
[ flag
] = false;
3155 delete this.flags
[ flag
];
3156 remove
.push( className
);
3162 if ( this.$flagged
) {
3164 .addClass( add
.join( ' ' ) )
3165 .removeClass( remove
.join( ' ' ) );
3168 this.updateThemeClasses();
3169 this.emit( 'flag', changes
);
3175 * TitledElement is mixed into other classes to provide a `title` attribute.
3176 * Titles are rendered by the browser and are made visible when the user moves
3177 * the mouse over the element. Titles are not visible on touch devices.
3180 * // TitledElement provides a 'title' attribute to the
3181 * // ButtonWidget class
3182 * var button = new OO.ui.ButtonWidget( {
3183 * label: 'Button with Title',
3184 * title: 'I am a button'
3186 * $( 'body' ).append( button.$element );
3192 * @param {Object} [config] Configuration options
3193 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3194 * If this config is omitted, the title functionality is applied to $element, the
3195 * element created by the class.
3196 * @cfg {string|Function} [title] The title text or a function that returns text. If
3197 * this config is omitted, the value of the {@link #static-title static title} property is used.
3199 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3200 // Configuration initialization
3201 config
= config
|| {};
3204 this.$titled
= null;
3208 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3209 this.setTitledElement( config
.$titled
|| this.$element
);
3214 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3216 /* Static Properties */
3219 * The title text, a function that returns text, or `null` for no title. The value of the static property
3220 * is overridden if the #title config option is used.
3224 * @property {string|Function|null}
3226 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3231 * Set the titled element.
3233 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3234 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3236 * @param {jQuery} $titled Element that should use the 'titled' functionality
3238 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3239 if ( this.$titled
) {
3240 this.$titled
.removeAttr( 'title' );
3243 this.$titled
= $titled
;
3245 this.$titled
.attr( 'title', this.title
);
3252 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3255 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3256 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3257 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3259 if ( this.title
!== title
) {
3260 if ( this.$titled
) {
3261 if ( title
!== null ) {
3262 this.$titled
.attr( 'title', title
);
3264 this.$titled
.removeAttr( 'title' );
3276 * @return {string} Title string
3278 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3283 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3284 * Accesskeys allow an user to go to a specific element by using
3285 * a shortcut combination of a browser specific keys + the key
3289 * // AccessKeyedElement provides an 'accesskey' attribute to the
3290 * // ButtonWidget class
3291 * var button = new OO.ui.ButtonWidget( {
3292 * label: 'Button with Accesskey',
3295 * $( 'body' ).append( button.$element );
3301 * @param {Object} [config] Configuration options
3302 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3303 * If this config is omitted, the accesskey functionality is applied to $element, the
3304 * element created by the class.
3305 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3306 * this config is omitted, no accesskey will be added.
3308 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3309 // Configuration initialization
3310 config
= config
|| {};
3313 this.$accessKeyed
= null;
3314 this.accessKey
= null;
3317 this.setAccessKey( config
.accessKey
|| null );
3318 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3323 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3325 /* Static Properties */
3328 * The access key, a function that returns a key, or `null` for no accesskey.
3332 * @property {string|Function|null}
3334 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3339 * Set the accesskeyed element.
3341 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3342 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3344 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3346 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3347 if ( this.$accessKeyed
) {
3348 this.$accessKeyed
.removeAttr( 'accesskey' );
3351 this.$accessKeyed
= $accessKeyed
;
3352 if ( this.accessKey
) {
3353 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3360 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3363 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3364 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3366 if ( this.accessKey
!== accessKey
) {
3367 if ( this.$accessKeyed
) {
3368 if ( accessKey
!== null ) {
3369 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3371 this.$accessKeyed
.removeAttr( 'accesskey' );
3374 this.accessKey
= accessKey
;
3383 * @return {string} accessKey string
3385 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3386 return this.accessKey
;
3390 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3391 * feels, and functionality can be customized via the class’s configuration options
3392 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3395 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3398 * // A button widget
3399 * var button = new OO.ui.ButtonWidget( {
3400 * label: 'Button with Icon',
3402 * iconTitle: 'Remove'
3404 * $( 'body' ).append( button.$element );
3406 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3409 * @extends OO.ui.Widget
3410 * @mixins OO.ui.mixin.ButtonElement
3411 * @mixins OO.ui.mixin.IconElement
3412 * @mixins OO.ui.mixin.IndicatorElement
3413 * @mixins OO.ui.mixin.LabelElement
3414 * @mixins OO.ui.mixin.TitledElement
3415 * @mixins OO.ui.mixin.FlaggedElement
3416 * @mixins OO.ui.mixin.TabIndexedElement
3417 * @mixins OO.ui.mixin.AccessKeyedElement
3420 * @param {Object} [config] Configuration options
3421 * @cfg {boolean} [active=false] Whether button should be shown as active
3422 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3423 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3424 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3426 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3427 // Configuration initialization
3428 config
= config
|| {};
3430 // Parent constructor
3431 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3433 // Mixin constructors
3434 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3435 OO
.ui
.mixin
.IconElement
.call( this, config
);
3436 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3437 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3438 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3439 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3440 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3441 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3446 this.noFollow
= false;
3449 this.connect( this, { disable
: 'onDisable' } );
3452 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3454 .addClass( 'oo-ui-buttonWidget' )
3455 .append( this.$button
);
3456 this.setActive( config
.active
);
3457 this.setHref( config
.href
);
3458 this.setTarget( config
.target
);
3459 this.setNoFollow( config
.noFollow
);
3464 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3465 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3466 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3467 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3468 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3469 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3470 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3471 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3472 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3474 /* Static Properties */
3480 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3486 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3491 * Get hyperlink location.
3493 * @return {string} Hyperlink location
3495 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3500 * Get hyperlink target.
3502 * @return {string} Hyperlink target
3504 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3509 * Get search engine traversal hint.
3511 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3513 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3514 return this.noFollow
;
3518 * Set hyperlink location.
3520 * @param {string|null} href Hyperlink location, null to remove
3522 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3523 href
= typeof href
=== 'string' ? href
: null;
3524 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3528 if ( href
!== this.href
) {
3537 * Update the `href` attribute, in case of changes to href or
3543 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3544 if ( this.href
!== null && !this.isDisabled() ) {
3545 this.$button
.attr( 'href', this.href
);
3547 this.$button
.removeAttr( 'href' );
3554 * Handle disable events.
3557 * @param {boolean} disabled Element is disabled
3559 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3564 * Set hyperlink target.
3566 * @param {string|null} target Hyperlink target, null to remove
3568 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3569 target
= typeof target
=== 'string' ? target
: null;
3571 if ( target
!== this.target
) {
3572 this.target
= target
;
3573 if ( target
!== null ) {
3574 this.$button
.attr( 'target', target
);
3576 this.$button
.removeAttr( 'target' );
3584 * Set search engine traversal hint.
3586 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3588 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3589 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3591 if ( noFollow
!== this.noFollow
) {
3592 this.noFollow
= noFollow
;
3594 this.$button
.attr( 'rel', 'nofollow' );
3596 this.$button
.removeAttr( 'rel' );
3603 // Override method visibility hints from ButtonElement
3612 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3613 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3614 * removed, and cleared from the group.
3617 * // Example: A ButtonGroupWidget with two buttons
3618 * var button1 = new OO.ui.PopupButtonWidget( {
3619 * label: 'Select a category',
3622 * $content: $( '<p>List of categories...</p>' ),
3627 * var button2 = new OO.ui.ButtonWidget( {
3630 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3631 * items: [button1, button2]
3633 * $( 'body' ).append( buttonGroup.$element );
3636 * @extends OO.ui.Widget
3637 * @mixins OO.ui.mixin.GroupElement
3640 * @param {Object} [config] Configuration options
3641 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3643 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3644 // Configuration initialization
3645 config
= config
|| {};
3647 // Parent constructor
3648 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3650 // Mixin constructors
3651 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3654 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3655 if ( Array
.isArray( config
.items
) ) {
3656 this.addItems( config
.items
);
3662 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3663 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3665 /* Static Properties */
3671 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
3674 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3675 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3676 * for a list of icons included in the library.
3679 * // An icon widget with a label
3680 * var myIcon = new OO.ui.IconWidget( {
3684 * // Create a label.
3685 * var iconLabel = new OO.ui.LabelWidget( {
3688 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3690 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3693 * @extends OO.ui.Widget
3694 * @mixins OO.ui.mixin.IconElement
3695 * @mixins OO.ui.mixin.TitledElement
3696 * @mixins OO.ui.mixin.FlaggedElement
3699 * @param {Object} [config] Configuration options
3701 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3702 // Configuration initialization
3703 config
= config
|| {};
3705 // Parent constructor
3706 OO
.ui
.IconWidget
.parent
.call( this, config
);
3708 // Mixin constructors
3709 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3710 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3711 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3714 this.$element
.addClass( 'oo-ui-iconWidget' );
3719 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3720 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3721 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3722 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3724 /* Static Properties */
3730 OO
.ui
.IconWidget
.static.tagName
= 'span';
3733 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3734 * attention to the status of an item or to clarify the function of a control. For a list of
3735 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3738 * // Example of an indicator widget
3739 * var indicator1 = new OO.ui.IndicatorWidget( {
3740 * indicator: 'alert'
3743 * // Create a fieldset layout to add a label
3744 * var fieldset = new OO.ui.FieldsetLayout();
3745 * fieldset.addItems( [
3746 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3748 * $( 'body' ).append( fieldset.$element );
3750 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3753 * @extends OO.ui.Widget
3754 * @mixins OO.ui.mixin.IndicatorElement
3755 * @mixins OO.ui.mixin.TitledElement
3758 * @param {Object} [config] Configuration options
3760 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
3761 // Configuration initialization
3762 config
= config
|| {};
3764 // Parent constructor
3765 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
3767 // Mixin constructors
3768 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
3769 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3772 this.$element
.addClass( 'oo-ui-indicatorWidget' );
3777 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
3778 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
3779 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
3781 /* Static Properties */
3787 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
3790 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3791 * be configured with a `label` option that is set to a string, a label node, or a function:
3793 * - String: a plaintext string
3794 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3795 * label that includes a link or special styling, such as a gray color or additional graphical elements.
3796 * - Function: a function that will produce a string in the future. Functions are used
3797 * in cases where the value of the label is not currently defined.
3799 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3800 * will come into focus when the label is clicked.
3803 * // Examples of LabelWidgets
3804 * var label1 = new OO.ui.LabelWidget( {
3805 * label: 'plaintext label'
3807 * var label2 = new OO.ui.LabelWidget( {
3808 * label: $( '<a href="default.html">jQuery label</a>' )
3810 * // Create a fieldset layout with fields for each example
3811 * var fieldset = new OO.ui.FieldsetLayout();
3812 * fieldset.addItems( [
3813 * new OO.ui.FieldLayout( label1 ),
3814 * new OO.ui.FieldLayout( label2 )
3816 * $( 'body' ).append( fieldset.$element );
3819 * @extends OO.ui.Widget
3820 * @mixins OO.ui.mixin.LabelElement
3821 * @mixins OO.ui.mixin.TitledElement
3824 * @param {Object} [config] Configuration options
3825 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
3826 * Clicking the label will focus the specified input field.
3828 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
3829 // Configuration initialization
3830 config
= config
|| {};
3832 // Parent constructor
3833 OO
.ui
.LabelWidget
.parent
.call( this, config
);
3835 // Mixin constructors
3836 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
3837 OO
.ui
.mixin
.TitledElement
.call( this, config
);
3840 this.input
= config
.input
;
3843 if ( this.input
instanceof OO
.ui
.InputWidget
) {
3844 if ( this.input
.getInputId() ) {
3845 this.$element
.attr( 'for', this.input
.getInputId() );
3847 this.$label
.on( 'click', function () {
3848 this.fieldWidget
.focus();
3853 this.$element
.addClass( 'oo-ui-labelWidget' );
3858 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
3859 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
3860 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
3862 /* Static Properties */
3868 OO
.ui
.LabelWidget
.static.tagName
= 'label';
3871 * PendingElement is a mixin that is used to create elements that notify users that something is happening
3872 * and that they should wait before proceeding. The pending state is visually represented with a pending
3873 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
3874 * field of a {@link OO.ui.TextInputWidget text input widget}.
3876 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
3877 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
3878 * in process dialogs.
3881 * function MessageDialog( config ) {
3882 * MessageDialog.parent.call( this, config );
3884 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
3886 * MessageDialog.static.name = 'myMessageDialog';
3887 * MessageDialog.static.actions = [
3888 * { action: 'save', label: 'Done', flags: 'primary' },
3889 * { label: 'Cancel', flags: 'safe' }
3892 * MessageDialog.prototype.initialize = function () {
3893 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
3894 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
3895 * 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>' );
3896 * this.$body.append( this.content.$element );
3898 * MessageDialog.prototype.getBodyHeight = function () {
3901 * MessageDialog.prototype.getActionProcess = function ( action ) {
3902 * var dialog = this;
3903 * if ( action === 'save' ) {
3904 * dialog.getActions().get({actions: 'save'})[0].pushPending();
3905 * return new OO.ui.Process()
3907 * .next( function () {
3908 * dialog.getActions().get({actions: 'save'})[0].popPending();
3911 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
3914 * var windowManager = new OO.ui.WindowManager();
3915 * $( 'body' ).append( windowManager.$element );
3917 * var dialog = new MessageDialog();
3918 * windowManager.addWindows( [ dialog ] );
3919 * windowManager.openWindow( dialog );
3925 * @param {Object} [config] Configuration options
3926 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
3928 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
3929 // Configuration initialization
3930 config
= config
|| {};
3934 this.$pending
= null;
3937 this.setPendingElement( config
.$pending
|| this.$element
);
3942 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
3947 * Set the pending element (and clean up any existing one).
3949 * @param {jQuery} $pending The element to set to pending.
3951 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
3952 if ( this.$pending
) {
3953 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3956 this.$pending
= $pending
;
3957 if ( this.pending
> 0 ) {
3958 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3963 * Check if an element is pending.
3965 * @return {boolean} Element is pending
3967 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
3968 return !!this.pending
;
3972 * Increase the pending counter. The pending state will remain active until the counter is zero
3973 * (i.e., the number of calls to #pushPending and #popPending is the same).
3977 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
3978 if ( this.pending
=== 0 ) {
3979 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3980 this.updateThemeClasses();
3988 * Decrease the pending counter. The pending state will remain active until the counter is zero
3989 * (i.e., the number of calls to #pushPending and #popPending is the same).
3993 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
3994 if ( this.pending
=== 1 ) {
3995 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3996 this.updateThemeClasses();
3998 this.pending
= Math
.max( 0, this.pending
- 1 );
4004 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4005 * in the document (for example, in an OO.ui.Window's $overlay).
4007 * The elements's position is automatically calculated and maintained when window is resized or the
4008 * page is scrolled. If you reposition the container manually, you have to call #position to make
4009 * sure the element is still placed correctly.
4011 * As positioning is only possible when both the element and the container are attached to the DOM
4012 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4013 * the #toggle method to display a floating popup, for example.
4019 * @param {Object} [config] Configuration options
4020 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4021 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4022 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4023 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4024 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4025 * 'top': Align the top edge with $floatableContainer's top edge
4026 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4027 * 'center': Vertically align the center with $floatableContainer's center
4028 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4029 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4030 * 'after': Directly after $floatableContainer, algining f's start edge with fC's end edge
4031 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4032 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4033 * 'center': Horizontally align the center with $floatableContainer's center
4034 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4037 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4038 // Configuration initialization
4039 config
= config
|| {};
4042 this.$floatable
= null;
4043 this.$floatableContainer
= null;
4044 this.$floatableWindow
= null;
4045 this.$floatableClosestScrollable
= null;
4046 this.onFloatableScrollHandler
= this.position
.bind( this );
4047 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4050 this.setFloatableContainer( config
.$floatableContainer
);
4051 this.setFloatableElement( config
.$floatable
|| this.$element
);
4052 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4053 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4054 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ? true : !!config
.hideWhenOutOfView
;
4060 * Set floatable element.
4062 * If an element is already set, it will be cleaned up before setting up the new element.
4064 * @param {jQuery} $floatable Element to make floatable
4066 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4067 if ( this.$floatable
) {
4068 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4069 this.$floatable
.css( { left
: '', top
: '' } );
4072 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4077 * Set floatable container.
4079 * The element will be positioned relative to the specified container.
4081 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4083 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4084 this.$floatableContainer
= $floatableContainer
;
4085 if ( this.$floatable
) {
4091 * Change how the element is positioned vertically.
4093 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4095 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4096 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4097 throw new Error( 'Invalid value for vertical position: ' + position
);
4099 if ( this.verticalPosition
!== position
) {
4100 this.verticalPosition
= position
;
4101 if ( this.$floatable
) {
4108 * Change how the element is positioned horizontally.
4110 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4112 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4113 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4114 throw new Error( 'Invalid value for horizontal position: ' + position
);
4116 if ( this.horizontalPosition
!== position
) {
4117 this.horizontalPosition
= position
;
4118 if ( this.$floatable
) {
4125 * Toggle positioning.
4127 * Do not turn positioning on until after the element is attached to the DOM and visible.
4129 * @param {boolean} [positioning] Enable positioning, omit to toggle
4132 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4133 var closestScrollableOfContainer
;
4135 if ( !this.$floatable
|| !this.$floatableContainer
) {
4139 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4141 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4142 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4143 this.warnedUnattached
= true;
4146 if ( this.positioning
!== positioning
) {
4147 this.positioning
= positioning
;
4149 this.needsCustomPosition
=
4150 this.verticalPostion
!== 'below' ||
4151 this.horizontalPosition
!== 'start' ||
4152 !OO
.ui
.contains( this.$floatableContainer
[ 0 ], this.$floatable
[ 0 ] );
4154 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
4155 // If the scrollable is the root, we have to listen to scroll events
4156 // on the window because of browser inconsistencies.
4157 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4158 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
4161 if ( positioning
) {
4162 this.$floatableWindow
= $( this.getElementWindow() );
4163 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4165 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4166 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4168 // Initial position after visible
4171 if ( this.$floatableWindow
) {
4172 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4173 this.$floatableWindow
= null;
4176 if ( this.$floatableClosestScrollable
) {
4177 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4178 this.$floatableClosestScrollable
= null;
4181 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4189 * Check whether the bottom edge of the given element is within the viewport of the given container.
4192 * @param {jQuery} $element
4193 * @param {jQuery} $container
4196 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4197 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
, rightEdgeInBounds
,
4198 startEdgeInBounds
, endEdgeInBounds
,
4199 direction
= $element
.css( 'direction' );
4201 elemRect
= $element
[ 0 ].getBoundingClientRect();
4202 if ( $container
[ 0 ] === window
) {
4206 right
: document
.documentElement
.clientWidth
,
4207 bottom
: document
.documentElement
.clientHeight
4210 contRect
= $container
[ 0 ].getBoundingClientRect();
4213 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4214 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4215 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4216 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4217 if ( direction
=== 'rtl' ) {
4218 startEdgeInBounds
= rightEdgeInBounds
;
4219 endEdgeInBounds
= leftEdgeInBounds
;
4221 startEdgeInBounds
= leftEdgeInBounds
;
4222 endEdgeInBounds
= rightEdgeInBounds
;
4225 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4228 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4231 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4234 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4238 // The other positioning values are all about being inside the container,
4239 // so in those cases all we care about is that any part of the container is visible.
4240 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4241 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4245 * Position the floatable below its container.
4247 * This should only be done when both of them are attached to the DOM and visible.
4251 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4252 if ( !this.positioning
) {
4257 // To continue, some things need to be true:
4258 // The element must actually be in the DOM
4259 this.isElementAttached() && (
4260 // The closest scrollable is the current window
4261 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4262 // OR is an element in the element's DOM
4263 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4266 // Abort early if important parts of the widget are no longer attached to the DOM
4270 if ( this.hideWhenOutOfView
&& !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
) ) {
4271 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4274 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4277 if ( !this.needsCustomPosition
) {
4281 this.$floatable
.css( this.computePosition() );
4283 // We updated the position, so re-evaluate the clipping state.
4284 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4285 // will not notice the need to update itself.)
4286 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4287 // it not listen to the right events in the right places?
4296 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4297 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4298 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4300 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4302 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4303 var isBody
, scrollableX
, scrollableY
, containerPos
,
4304 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4305 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4306 direction
= this.$floatableContainer
.css( 'direction' ),
4307 $offsetParent
= this.$floatable
.offsetParent();
4309 if ( $offsetParent
.is( 'html' ) ) {
4310 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4311 // <html> element, but they do work on the <body>
4312 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4314 isBody
= $offsetParent
.is( 'body' );
4315 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' || $offsetParent
.css( 'overflow-x' ) === 'auto';
4316 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' || $offsetParent
.css( 'overflow-y' ) === 'auto';
4318 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4319 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4320 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
4321 // or if it isn't scrollable
4322 scrollTop
= scrollableY
&& !isBody
? $offsetParent
.scrollTop() : 0;
4323 scrollLeft
= scrollableX
&& !isBody
? OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4325 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4326 // if the <body> has a margin
4327 containerPos
= isBody
?
4328 this.$floatableContainer
.offset() :
4329 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4330 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4331 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4332 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4333 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4335 if ( this.verticalPosition
=== 'below' ) {
4336 newPos
.top
= containerPos
.bottom
;
4337 } else if ( this.verticalPosition
=== 'above' ) {
4338 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4339 } else if ( this.verticalPosition
=== 'top' ) {
4340 newPos
.top
= containerPos
.top
;
4341 } else if ( this.verticalPosition
=== 'bottom' ) {
4342 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4343 } else if ( this.verticalPosition
=== 'center' ) {
4344 newPos
.top
= containerPos
.top
+
4345 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4348 if ( this.horizontalPosition
=== 'before' ) {
4349 newPos
.end
= containerPos
.start
;
4350 } else if ( this.horizontalPosition
=== 'after' ) {
4351 newPos
.start
= containerPos
.end
;
4352 } else if ( this.horizontalPosition
=== 'start' ) {
4353 newPos
.start
= containerPos
.start
;
4354 } else if ( this.horizontalPosition
=== 'end' ) {
4355 newPos
.end
= containerPos
.end
;
4356 } else if ( this.horizontalPosition
=== 'center' ) {
4357 newPos
.left
= containerPos
.left
+
4358 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4361 if ( newPos
.start
!== undefined ) {
4362 if ( direction
=== 'rtl' ) {
4363 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.start
;
4365 newPos
.left
= newPos
.start
;
4367 delete newPos
.start
;
4369 if ( newPos
.end
!== undefined ) {
4370 if ( direction
=== 'rtl' ) {
4371 newPos
.left
= newPos
.end
;
4373 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.end
;
4378 // Account for scroll position
4379 if ( newPos
.top
!== '' ) {
4380 newPos
.top
+= scrollTop
;
4382 if ( newPos
.bottom
!== '' ) {
4383 newPos
.bottom
-= scrollTop
;
4385 if ( newPos
.left
!== '' ) {
4386 newPos
.left
+= scrollLeft
;
4388 if ( newPos
.right
!== '' ) {
4389 newPos
.right
-= scrollLeft
;
4392 // Account for scrollbar gutter
4393 if ( newPos
.bottom
!== '' ) {
4394 newPos
.bottom
-= horizScrollbarHeight
;
4396 if ( direction
=== 'rtl' ) {
4397 if ( newPos
.left
!== '' ) {
4398 newPos
.left
-= vertScrollbarWidth
;
4401 if ( newPos
.right
!== '' ) {
4402 newPos
.right
-= vertScrollbarWidth
;
4410 * Element that can be automatically clipped to visible boundaries.
4412 * Whenever the element's natural height changes, you have to call
4413 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4414 * clipping correctly.
4416 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4417 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4418 * then #$clippable will be given a fixed reduced height and/or width and will be made
4419 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4420 * but you can build a static footer by setting #$clippableContainer to an element that contains
4421 * #$clippable and the footer.
4427 * @param {Object} [config] Configuration options
4428 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4429 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4430 * omit to use #$clippable
4432 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4433 // Configuration initialization
4434 config
= config
|| {};
4437 this.$clippable
= null;
4438 this.$clippableContainer
= null;
4439 this.clipping
= false;
4440 this.clippedHorizontally
= false;
4441 this.clippedVertically
= false;
4442 this.$clippableScrollableContainer
= null;
4443 this.$clippableScroller
= null;
4444 this.$clippableWindow
= null;
4445 this.idealWidth
= null;
4446 this.idealHeight
= null;
4447 this.onClippableScrollHandler
= this.clip
.bind( this );
4448 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4451 if ( config
.$clippableContainer
) {
4452 this.setClippableContainer( config
.$clippableContainer
);
4454 this.setClippableElement( config
.$clippable
|| this.$element
);
4460 * Set clippable element.
4462 * If an element is already set, it will be cleaned up before setting up the new element.
4464 * @param {jQuery} $clippable Element to make clippable
4466 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4467 if ( this.$clippable
) {
4468 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4469 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4470 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4473 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4478 * Set clippable container.
4480 * This is the container that will be measured when deciding whether to clip. When clipping,
4481 * #$clippable will be resized in order to keep the clippable container fully visible.
4483 * If the clippable container is unset, #$clippable will be used.
4485 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4487 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4488 this.$clippableContainer
= $clippableContainer
;
4489 if ( this.$clippable
) {
4497 * Do not turn clipping on until after the element is attached to the DOM and visible.
4499 * @param {boolean} [clipping] Enable clipping, omit to toggle
4502 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4503 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4505 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4506 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4507 this.warnedUnattached
= true;
4510 if ( this.clipping
!== clipping
) {
4511 this.clipping
= clipping
;
4513 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4514 // If the clippable container is the root, we have to listen to scroll events and check
4515 // jQuery.scrollTop on the window because of browser inconsistencies
4516 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4517 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4518 this.$clippableScrollableContainer
;
4519 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4520 this.$clippableWindow
= $( this.getElementWindow() )
4521 .on( 'resize', this.onClippableWindowResizeHandler
);
4522 // Initial clip after visible
4525 this.$clippable
.css( {
4533 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4535 this.$clippableScrollableContainer
= null;
4536 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4537 this.$clippableScroller
= null;
4538 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4539 this.$clippableWindow
= null;
4547 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4549 * @return {boolean} Element will be clipped to the visible area
4551 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4552 return this.clipping
;
4556 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4558 * @return {boolean} Part of the element is being clipped
4560 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4561 return this.clippedHorizontally
|| this.clippedVertically
;
4565 * Check if the right of the element is being clipped by the nearest scrollable container.
4567 * @return {boolean} Part of the element is being clipped
4569 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4570 return this.clippedHorizontally
;
4574 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4576 * @return {boolean} Part of the element is being clipped
4578 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4579 return this.clippedVertically
;
4583 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
4585 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4586 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4588 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4589 this.idealWidth
= width
;
4590 this.idealHeight
= height
;
4592 if ( !this.clipping
) {
4593 // Update dimensions
4594 this.$clippable
.css( { width
: width
, height
: height
} );
4596 // While clipping, idealWidth and idealHeight are not considered
4600 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4601 * when the element's natural height changes.
4603 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4604 * overlapped by, the visible area of the nearest scrollable container.
4606 * Because calling clip() when the natural height changes isn't always possible, we also set
4607 * max-height when the element isn't being clipped. This means that if the element tries to grow
4608 * beyond the edge, something reasonable will happen before clip() is called.
4612 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
4613 var $container
, extraHeight
, extraWidth
, ccOffset
,
4614 $scrollableContainer
, scOffset
, scHeight
, scWidth
,
4615 ccWidth
, scrollerIsWindow
, scrollTop
, scrollLeft
,
4616 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
4617 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
4618 buffer
= 7; // Chosen by fair dice roll
4620 if ( !this.clipping
) {
4621 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4625 $container
= this.$clippableContainer
|| this.$clippable
;
4626 extraHeight
= $container
.outerHeight() - this.$clippable
.outerHeight();
4627 extraWidth
= $container
.outerWidth() - this.$clippable
.outerWidth();
4628 ccOffset
= $container
.offset();
4629 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
4630 $scrollableContainer
= this.$clippableWindow
;
4631 scOffset
= { top
: 0, left
: 0 };
4633 $scrollableContainer
= this.$clippableScrollableContainer
;
4634 scOffset
= $scrollableContainer
.offset();
4636 scHeight
= $scrollableContainer
.innerHeight() - buffer
;
4637 scWidth
= $scrollableContainer
.innerWidth() - buffer
;
4638 ccWidth
= $container
.outerWidth() + buffer
;
4639 scrollerIsWindow
= this.$clippableScroller
[ 0 ] === this.$clippableWindow
[ 0 ];
4640 scrollTop
= scrollerIsWindow
? this.$clippableScroller
.scrollTop() : 0;
4641 scrollLeft
= scrollerIsWindow
? this.$clippableScroller
.scrollLeft() : 0;
4642 desiredWidth
= ccOffset
.left
< 0 ?
4643 ccWidth
+ ccOffset
.left
:
4644 ( scOffset
.left
+ scrollLeft
+ scWidth
) - ccOffset
.left
;
4645 desiredHeight
= ( scOffset
.top
+ scrollTop
+ scHeight
) - ccOffset
.top
;
4646 // It should never be desirable to exceed the dimensions of the browser viewport... right?
4647 desiredWidth
= Math
.min( desiredWidth
, document
.documentElement
.clientWidth
);
4648 desiredHeight
= Math
.min( desiredHeight
, document
.documentElement
.clientHeight
);
4649 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
4650 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
4651 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
4652 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
4653 clipWidth
= allotedWidth
< naturalWidth
;
4654 clipHeight
= allotedHeight
< naturalHeight
;
4657 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. (T157672)
4658 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
4659 this.$clippable
.css( 'overflowX', 'scroll' );
4660 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
4661 this.$clippable
.css( {
4662 width
: Math
.max( 0, allotedWidth
),
4666 this.$clippable
.css( {
4668 width
: this.idealWidth
? this.idealWidth
- extraWidth
: '',
4669 maxWidth
: Math
.max( 0, allotedWidth
)
4673 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. (T157672)
4674 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
4675 this.$clippable
.css( 'overflowY', 'scroll' );
4676 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
4677 this.$clippable
.css( {
4678 height
: Math
.max( 0, allotedHeight
),
4682 this.$clippable
.css( {
4684 height
: this.idealHeight
? this.idealHeight
- extraHeight
: '',
4685 maxHeight
: Math
.max( 0, allotedHeight
)
4689 // If we stopped clipping in at least one of the dimensions
4690 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
4691 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4694 this.clippedHorizontally
= clipWidth
;
4695 this.clippedVertically
= clipHeight
;
4701 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4702 * By default, each popup has an anchor that points toward its origin.
4703 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4705 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
4708 * // A popup widget.
4709 * var popup = new OO.ui.PopupWidget( {
4710 * $content: $( '<p>Hi there!</p>' ),
4715 * $( 'body' ).append( popup.$element );
4716 * // To display the popup, toggle the visibility to 'true'.
4717 * popup.toggle( true );
4719 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4722 * @extends OO.ui.Widget
4723 * @mixins OO.ui.mixin.LabelElement
4724 * @mixins OO.ui.mixin.ClippableElement
4725 * @mixins OO.ui.mixin.FloatableElement
4728 * @param {Object} [config] Configuration options
4729 * @cfg {number} [width=320] Width of popup in pixels
4730 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4731 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4732 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
4733 * 'above': Put popup above $floatableContainer; anchor points down to the start edge of $floatableContainer
4734 * 'below': Put popup below $floatableContainer; anchor points up to the start edge of $floatableContainer
4735 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
4736 * endwards (right/left) to the vertical center of $floatableContainer
4737 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
4738 * startwards (left/right) to the vertical center of $floatableContainer
4739 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
4740 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
4741 * as possible while still keeping the anchor within the popup;
4742 * if position is before/after, move the popup as far downwards as possible.
4743 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
4744 * as possible while still keeping the anchor within the popup;
4745 * if position in before/after, move the popup as far upwards as possible.
4746 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center
4747 * of the popup with the center of $floatableContainer.
4748 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
4749 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
4750 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4751 * See the [OOjs UI docs on MediaWiki][3] for an example.
4752 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4753 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4754 * @cfg {jQuery} [$content] Content to append to the popup's body
4755 * @cfg {jQuery} [$footer] Content to append to the popup's footer
4756 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4757 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4758 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4760 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4761 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
4763 * @cfg {boolean} [padded=false] Add padding to the popup's body
4765 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
4766 // Configuration initialization
4767 config
= config
|| {};
4769 // Parent constructor
4770 OO
.ui
.PopupWidget
.parent
.call( this, config
);
4772 // Properties (must be set before ClippableElement constructor call)
4773 this.$body
= $( '<div>' );
4774 this.$popup
= $( '<div>' );
4776 // Mixin constructors
4777 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4778 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
4779 $clippable
: this.$body
,
4780 $clippableContainer
: this.$popup
4782 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
4785 this.$anchor
= $( '<div>' );
4786 // If undefined, will be computed lazily in updateDimensions()
4787 this.$container
= config
.$container
;
4788 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
4789 this.autoClose
= !!config
.autoClose
;
4790 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
4791 this.transitionTimeout
= null;
4792 this.anchored
= false;
4793 this.width
= config
.width
!== undefined ? config
.width
: 320;
4794 this.height
= config
.height
!== undefined ? config
.height
: null;
4795 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
4796 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
4799 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
4800 this.setAlignment( config
.align
|| 'center' );
4801 this.setPosition( config
.position
|| 'below' );
4802 this.$body
.addClass( 'oo-ui-popupWidget-body' );
4803 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
4805 .addClass( 'oo-ui-popupWidget-popup' )
4806 .append( this.$body
);
4808 .addClass( 'oo-ui-popupWidget' )
4809 .append( this.$popup
, this.$anchor
);
4810 // Move content, which was added to #$element by OO.ui.Widget, to the body
4811 // FIXME This is gross, we should use '$body' or something for the config
4812 if ( config
.$content
instanceof jQuery
) {
4813 this.$body
.append( config
.$content
);
4816 if ( config
.padded
) {
4817 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
4820 if ( config
.head
) {
4821 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
4822 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
4823 this.$head
= $( '<div>' )
4824 .addClass( 'oo-ui-popupWidget-head' )
4825 .append( this.$label
, this.closeButton
.$element
);
4826 this.$popup
.prepend( this.$head
);
4829 if ( config
.$footer
) {
4830 this.$footer
= $( '<div>' )
4831 .addClass( 'oo-ui-popupWidget-footer' )
4832 .append( config
.$footer
);
4833 this.$popup
.append( this.$footer
);
4836 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
4837 // that reference properties not initialized at that time of parent class construction
4838 // TODO: Find a better way to handle post-constructor setup
4839 this.visible
= false;
4840 this.$element
.addClass( 'oo-ui-element-hidden' );
4845 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
4846 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
4847 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
4848 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
4855 * The popup is ready: it is visible and has been positioned and clipped.
4861 * Handles mouse down events.
4864 * @param {MouseEvent} e Mouse down event
4866 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
4869 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
4871 this.toggle( false );
4876 * Bind mouse down listener.
4880 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
4881 // Capture clicks outside popup
4882 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
4886 * Handles close button click events.
4890 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
4891 if ( this.isVisible() ) {
4892 this.toggle( false );
4897 * Unbind mouse down listener.
4901 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
4902 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
4906 * Handles key down events.
4909 * @param {KeyboardEvent} e Key down event
4911 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
4913 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
4916 this.toggle( false );
4918 e
.stopPropagation();
4923 * Bind key down listener.
4927 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
4928 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4932 * Unbind key down listener.
4936 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
4937 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4941 * Show, hide, or toggle the visibility of the anchor.
4943 * @param {boolean} [show] Show anchor, omit to toggle
4945 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
4946 show
= show
=== undefined ? !this.anchored
: !!show
;
4948 if ( this.anchored
!== show
) {
4950 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
4952 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
4954 this.anchored
= show
;
4958 * Change which edge the anchor appears on.
4960 * @param {string} edge 'top', 'bottom', 'start' or 'end'
4962 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
4963 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
4964 throw new Error( 'Invalid value for edge: ' + edge
);
4966 if ( this.anchorEdge
!== null ) {
4967 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
4969 this.anchorEdge
= edge
;
4970 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
4974 * Check if the anchor is visible.
4976 * @return {boolean} Anchor is visible
4978 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
4979 return this.anchored
;
4983 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
4984 * `.toggle( true )` after its #$element is attached to the DOM.
4986 * Do not show the popup while it is not attached to the DOM. The calculations required to display
4987 * it in the right place and with the right dimensions only work correctly while it is attached.
4988 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
4989 * strictly enforced, so currently it only generates a warning in the browser console.
4994 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
4996 show
= show
=== undefined ? !this.isVisible() : !!show
;
4998 change
= show
!== this.isVisible();
5000 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5001 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5002 this.warnedUnattached
= true;
5004 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5005 // Fall back to the parent node if the floatableContainer is not set
5006 this.setFloatableContainer( this.$element
.parent() );
5010 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5013 this.togglePositioning( show
&& !!this.$floatableContainer
);
5016 if ( this.autoClose
) {
5017 this.bindMouseDownListener();
5018 this.bindKeyDownListener();
5020 this.updateDimensions();
5021 this.toggleClipping( true );
5022 this.emit( 'ready' );
5024 this.toggleClipping( false );
5025 if ( this.autoClose
) {
5026 this.unbindMouseDownListener();
5027 this.unbindKeyDownListener();
5036 * Set the size of the popup.
5038 * Changing the size may also change the popup's position depending on the alignment.
5040 * @param {number} width Width in pixels
5041 * @param {number} height Height in pixels
5042 * @param {boolean} [transition=false] Use a smooth transition
5045 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5047 this.height
= height
!== undefined ? height
: null;
5048 if ( this.isVisible() ) {
5049 this.updateDimensions( transition
);
5054 * Update the size and position.
5056 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5057 * be called automatically.
5059 * @param {boolean} [transition=false] Use a smooth transition
5062 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5065 // Prevent transition from being interrupted
5066 clearTimeout( this.transitionTimeout
);
5068 // Enable transition
5069 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5075 // Prevent transitioning after transition is complete
5076 this.transitionTimeout
= setTimeout( function () {
5077 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5080 // Prevent transitioning immediately
5081 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5088 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5089 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
, anchorPos
,
5090 anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
, floatablePos
,
5091 offsetParentPos
, containerPos
,
5093 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5096 'force-left': 'backwards',
5097 'force-right': 'forwards'
5100 'force-left': 'forwards',
5101 'force-right': 'backwards'
5121 if ( !this.$container
) {
5122 // Lazy-initialize $container if not specified in constructor
5123 this.$container
= $( this.getClosestScrollableElementContainer() );
5125 direction
= this.$container
.css( 'direction' );
5127 // Set height and width before we do anything else, since it might cause our measurements
5128 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5131 height
: this.height
!== null ? this.height
: 'auto'
5134 align
= alignMap
[ direction
][ this.align
] || this.align
;
5135 // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
5136 vertical
= this.popupPosition
=== 'before' || this.popupPosition
=== 'after';
5137 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5138 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5139 near
= vertical
? 'top' : 'left';
5140 far
= vertical
? 'bottom' : 'right';
5141 sizeProp
= vertical
? 'Height' : 'Width';
5142 popupSize
= vertical
? ( this.height
|| this.$popup
.height() ) : this.width
;
5144 this.setAnchorEdge( anchorEdgeMap
[ this.popupPosition
] );
5145 this.horizontalPosition
= vertical
? this.popupPosition
: hPosMap
[ align
];
5146 this.verticalPosition
= vertical
? vPosMap
[ align
] : this.popupPosition
;
5149 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5150 // Find out which property FloatableElement used for positioning, and adjust that value
5151 positionProp
= vertical
?
5152 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5153 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5155 // Figure out where the near and far edges of the popup and $floatableContainer are
5156 floatablePos
= this.$floatableContainer
.offset();
5157 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5158 // Measure where the offsetParent is and compute our position based on that and parentPosition
5159 offsetParentPos
= this.$element
.offsetParent().offset();
5161 if ( positionProp
=== near
) {
5162 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5163 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5165 popupPos
[ far
] = offsetParentPos
[ near
] +
5166 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5167 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5170 // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
5171 // For popups above/below, we point to the start edge; for popups before/after, we point to the center
5172 anchorPos
= vertical
? ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2 : floatablePos
[ start
];
5173 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5175 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
5176 // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
5177 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5178 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5179 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5180 // Not enough space for the anchor on the start side; pull the popup startwards
5181 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5182 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5183 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5184 // Not enough space for the anchor on the end side; pull the popup endwards
5185 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5186 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5188 positionAdjustment
= 0;
5191 // Check if the popup will go beyond the edge of this.$container
5192 containerPos
= this.$container
.offset();
5193 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5194 // Take into account how much the popup will move because of the adjustments we're going to make
5195 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5196 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5197 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5198 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5199 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5200 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5201 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5202 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5203 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5204 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5207 // Adjust anchorOffset for positionAdjustment
5208 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5210 // Position the anchor
5211 anchorCss
[ start
] = anchorOffset
;
5212 this.$anchor
.css( anchorCss
);
5213 // Move the popup if needed
5214 parentPosition
[ positionProp
] += positionAdjustment
;
5216 return parentPosition
;
5220 * Set popup alignment
5222 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5223 * `backwards` or `forwards`.
5225 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5226 // Validate alignment
5227 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5230 this.align
= 'center';
5236 * Get popup alignment
5238 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5239 * `backwards` or `forwards`.
5241 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5246 * Change the positioning of the popup.
5248 * @param {string} position 'above', 'below', 'before' or 'after'
5250 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5251 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5254 this.popupPosition
= position
;
5259 * Get popup positioning.
5261 * @return {string} 'above', 'below', 'before' or 'after'
5263 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5264 return this.popupPosition
;
5268 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5269 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5270 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5271 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5277 * @param {Object} [config] Configuration options
5278 * @cfg {Object} [popup] Configuration to pass to popup
5279 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5281 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5282 // Configuration initialization
5283 config
= config
|| {};
5286 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5289 $floatableContainer
: this.$element
5293 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5303 * @return {OO.ui.PopupWidget} Popup widget
5305 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5310 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5311 * which is used to display additional information or options.
5314 * // Example of a popup button.
5315 * var popupButton = new OO.ui.PopupButtonWidget( {
5316 * label: 'Popup button with options',
5319 * $content: $( '<p>Additional options here.</p>' ),
5321 * align: 'force-left'
5324 * // Append the button to the DOM.
5325 * $( 'body' ).append( popupButton.$element );
5328 * @extends OO.ui.ButtonWidget
5329 * @mixins OO.ui.mixin.PopupElement
5332 * @param {Object} [config] Configuration options
5333 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
5334 * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
5335 * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
5337 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
5338 // Parent constructor
5339 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
5341 // Mixin constructors
5342 OO
.ui
.mixin
.PopupElement
.call( this, config
);
5345 this.$overlay
= config
.$overlay
|| this.$element
;
5348 this.connect( this, { click
: 'onAction' } );
5352 .addClass( 'oo-ui-popupButtonWidget' )
5353 .attr( 'aria-haspopup', 'true' );
5355 .addClass( 'oo-ui-popupButtonWidget-popup' )
5356 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
5357 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
5358 this.$overlay
.append( this.popup
.$element
);
5363 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
5364 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
5369 * Handle the button action being triggered.
5373 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
5374 this.popup
.toggle();
5378 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
5380 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
5385 * @mixins OO.ui.mixin.GroupElement
5388 * @param {Object} [config] Configuration options
5390 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
5391 // Mixin constructors
5392 OO
.ui
.mixin
.GroupElement
.call( this, config
);
5397 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
5402 * Set the disabled state of the widget.
5404 * This will also update the disabled state of child widgets.
5406 * @param {boolean} disabled Disable widget
5409 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
5413 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
5414 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
5416 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
5418 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5419 this.items
[ i
].updateDisabled();
5427 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
5429 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
5430 * allows bidirectional communication.
5432 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
5440 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
5447 * Check if widget is disabled.
5449 * Checks parent if present, making disabled state inheritable.
5451 * @return {boolean} Widget is disabled
5453 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
5454 return this.disabled
||
5455 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
5459 * Set group element is in.
5461 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
5464 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
5466 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
5467 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
5469 // Initialize item disabled states
5470 this.updateDisabled();
5476 * OptionWidgets are special elements that can be selected and configured with data. The
5477 * data is often unique for each option, but it does not have to be. OptionWidgets are used
5478 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
5479 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
5481 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5484 * @extends OO.ui.Widget
5485 * @mixins OO.ui.mixin.ItemWidget
5486 * @mixins OO.ui.mixin.LabelElement
5487 * @mixins OO.ui.mixin.FlaggedElement
5488 * @mixins OO.ui.mixin.AccessKeyedElement
5491 * @param {Object} [config] Configuration options
5493 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
5494 // Configuration initialization
5495 config
= config
|| {};
5497 // Parent constructor
5498 OO
.ui
.OptionWidget
.parent
.call( this, config
);
5500 // Mixin constructors
5501 OO
.ui
.mixin
.ItemWidget
.call( this );
5502 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5503 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
5504 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
5507 this.selected
= false;
5508 this.highlighted
= false;
5509 this.pressed
= false;
5513 .data( 'oo-ui-optionWidget', this )
5514 // Allow programmatic focussing (and by accesskey), but not tabbing
5515 .attr( 'tabindex', '-1' )
5516 .attr( 'role', 'option' )
5517 .attr( 'aria-selected', 'false' )
5518 .addClass( 'oo-ui-optionWidget' )
5519 .append( this.$label
);
5524 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
5525 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
5526 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
5527 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
5528 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
5530 /* Static Properties */
5533 * Whether this option can be selected. See #setSelected.
5537 * @property {boolean}
5539 OO
.ui
.OptionWidget
.static.selectable
= true;
5542 * Whether this option can be highlighted. See #setHighlighted.
5546 * @property {boolean}
5548 OO
.ui
.OptionWidget
.static.highlightable
= true;
5551 * Whether this option can be pressed. See #setPressed.
5555 * @property {boolean}
5557 OO
.ui
.OptionWidget
.static.pressable
= true;
5560 * Whether this option will be scrolled into view when it is selected.
5564 * @property {boolean}
5566 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
5571 * Check if the option can be selected.
5573 * @return {boolean} Item is selectable
5575 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
5576 return this.constructor.static.selectable
&& !this.isDisabled() && this.isVisible();
5580 * Check if the option can be highlighted. A highlight indicates that the option
5581 * may be selected when a user presses enter or clicks. Disabled items cannot
5584 * @return {boolean} Item is highlightable
5586 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
5587 return this.constructor.static.highlightable
&& !this.isDisabled() && this.isVisible();
5591 * Check if the option can be pressed. The pressed state occurs when a user mouses
5592 * down on an item, but has not yet let go of the mouse.
5594 * @return {boolean} Item is pressable
5596 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
5597 return this.constructor.static.pressable
&& !this.isDisabled() && this.isVisible();
5601 * Check if the option is selected.
5603 * @return {boolean} Item is selected
5605 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
5606 return this.selected
;
5610 * Check if the option is highlighted. A highlight indicates that the
5611 * item may be selected when a user presses enter or clicks.
5613 * @return {boolean} Item is highlighted
5615 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
5616 return this.highlighted
;
5620 * Check if the option is pressed. The pressed state occurs when a user mouses
5621 * down on an item, but has not yet let go of the mouse. The item may appear
5622 * selected, but it will not be selected until the user releases the mouse.
5624 * @return {boolean} Item is pressed
5626 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
5627 return this.pressed
;
5631 * Set the option’s selected state. In general, all modifications to the selection
5632 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
5633 * method instead of this method.
5635 * @param {boolean} [state=false] Select option
5638 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
5639 if ( this.constructor.static.selectable
) {
5640 this.selected
= !!state
;
5642 .toggleClass( 'oo-ui-optionWidget-selected', state
)
5643 .attr( 'aria-selected', state
.toString() );
5644 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
5645 this.scrollElementIntoView();
5647 this.updateThemeClasses();
5653 * Set the option’s highlighted state. In general, all programmatic
5654 * modifications to the highlight should be handled by the
5655 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
5656 * method instead of this method.
5658 * @param {boolean} [state=false] Highlight option
5661 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
5662 if ( this.constructor.static.highlightable
) {
5663 this.highlighted
= !!state
;
5664 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
5665 this.updateThemeClasses();
5671 * Set the option’s pressed state. In general, all
5672 * programmatic modifications to the pressed state should be handled by the
5673 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
5674 * method instead of this method.
5676 * @param {boolean} [state=false] Press option
5679 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
5680 if ( this.constructor.static.pressable
) {
5681 this.pressed
= !!state
;
5682 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
5683 this.updateThemeClasses();
5689 * Get text to match search strings against.
5691 * The default implementation returns the label text, but subclasses
5692 * can override this to provide more complex behavior.
5694 * @return {string|boolean} String to match search string against
5696 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
5697 var label
= this.getLabel();
5698 return typeof label
=== 'string' ? label
: this.$label
.text();
5702 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
5703 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
5704 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
5707 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
5708 * information, please see the [OOjs UI documentation on MediaWiki][1].
5711 * // Example of a select widget with three options
5712 * var select = new OO.ui.SelectWidget( {
5714 * new OO.ui.OptionWidget( {
5716 * label: 'Option One',
5718 * new OO.ui.OptionWidget( {
5720 * label: 'Option Two',
5722 * new OO.ui.OptionWidget( {
5724 * label: 'Option Three',
5728 * $( 'body' ).append( select.$element );
5730 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5734 * @extends OO.ui.Widget
5735 * @mixins OO.ui.mixin.GroupWidget
5738 * @param {Object} [config] Configuration options
5739 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
5740 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
5741 * the [OOjs UI documentation on MediaWiki] [2] for examples.
5742 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5744 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
5745 // Configuration initialization
5746 config
= config
|| {};
5748 // Parent constructor
5749 OO
.ui
.SelectWidget
.parent
.call( this, config
);
5751 // Mixin constructors
5752 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
5755 this.pressed
= false;
5756 this.selecting
= null;
5757 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
5758 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
5759 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
5760 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
5761 this.keyPressBuffer
= '';
5762 this.keyPressBufferTimer
= null;
5763 this.blockMouseOverEvents
= 0;
5766 this.connect( this, {
5770 focusin
: this.onFocus
.bind( this ),
5771 mousedown
: this.onMouseDown
.bind( this ),
5772 mouseover
: this.onMouseOver
.bind( this ),
5773 mouseleave
: this.onMouseLeave
.bind( this )
5778 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
5779 .attr( 'role', 'listbox' );
5780 if ( Array
.isArray( config
.items
) ) {
5781 this.addItems( config
.items
);
5787 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
5788 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
5795 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
5797 * @param {OO.ui.OptionWidget|null} item Highlighted item
5803 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
5804 * pressed state of an option.
5806 * @param {OO.ui.OptionWidget|null} item Pressed item
5812 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
5814 * @param {OO.ui.OptionWidget|null} item Selected item
5819 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
5820 * @param {OO.ui.OptionWidget} item Chosen item
5826 * An `add` event is emitted when options are added to the select with the #addItems method.
5828 * @param {OO.ui.OptionWidget[]} items Added items
5829 * @param {number} index Index of insertion point
5835 * A `remove` event is emitted when options are removed from the select with the #clearItems
5836 * or #removeItems methods.
5838 * @param {OO.ui.OptionWidget[]} items Removed items
5844 * Handle focus events
5847 * @param {jQuery.Event} event
5849 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
5851 if ( event
.target
=== this.$element
[ 0 ] ) {
5852 // This widget was focussed, e.g. by the user tabbing to it.
5853 // The styles for focus state depend on one of the items being selected.
5854 if ( !this.getSelectedItem() ) {
5855 item
= this.getFirstSelectableItem();
5858 // One of the options got focussed (and the event bubbled up here).
5859 // They can't be tabbed to, but they can be activated using accesskeys.
5860 item
= this.getTargetItem( event
);
5864 if ( item
.constructor.static.highlightable
) {
5865 this.highlightItem( item
);
5867 this.selectItem( item
);
5871 if ( event
.target
!== this.$element
[ 0 ] ) {
5872 this.$element
.focus();
5877 * Handle mouse down events.
5880 * @param {jQuery.Event} e Mouse down event
5882 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
5885 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
5886 this.togglePressed( true );
5887 item
= this.getTargetItem( e
);
5888 if ( item
&& item
.isSelectable() ) {
5889 this.pressItem( item
);
5890 this.selecting
= item
;
5891 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
5892 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5899 * Handle mouse up events.
5902 * @param {MouseEvent} e Mouse up event
5904 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
5907 this.togglePressed( false );
5908 if ( !this.selecting
) {
5909 item
= this.getTargetItem( e
);
5910 if ( item
&& item
.isSelectable() ) {
5911 this.selecting
= item
;
5914 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
5915 this.pressItem( null );
5916 this.chooseItem( this.selecting
);
5917 this.selecting
= null;
5920 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
5921 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
5927 * Handle mouse move events.
5930 * @param {MouseEvent} e Mouse move event
5932 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
5935 if ( !this.isDisabled() && this.pressed
) {
5936 item
= this.getTargetItem( e
);
5937 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
5938 this.pressItem( item
);
5939 this.selecting
= item
;
5945 * Handle mouse over events.
5948 * @param {jQuery.Event} e Mouse over event
5950 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
5952 if ( this.blockMouseOverEvents
) {
5955 if ( !this.isDisabled() ) {
5956 item
= this.getTargetItem( e
);
5957 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
5963 * Handle mouse leave events.
5966 * @param {jQuery.Event} e Mouse over event
5968 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
5969 if ( !this.isDisabled() ) {
5970 this.highlightItem( null );
5976 * Handle key down events.
5979 * @param {KeyboardEvent} e Key down event
5981 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
5984 currentItem
= this.getHighlightedItem() || this.getSelectedItem();
5986 if ( !this.isDisabled() && this.isVisible() ) {
5987 switch ( e
.keyCode
) {
5988 case OO
.ui
.Keys
.ENTER
:
5989 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5990 // Was only highlighted, now let's select it. No-op if already selected.
5991 this.chooseItem( currentItem
);
5996 case OO
.ui
.Keys
.LEFT
:
5997 this.clearKeyPressBuffer();
5998 nextItem
= this.getRelativeSelectableItem( currentItem
, -1 );
6001 case OO
.ui
.Keys
.DOWN
:
6002 case OO
.ui
.Keys
.RIGHT
:
6003 this.clearKeyPressBuffer();
6004 nextItem
= this.getRelativeSelectableItem( currentItem
, 1 );
6007 case OO
.ui
.Keys
.ESCAPE
:
6008 case OO
.ui
.Keys
.TAB
:
6009 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6010 currentItem
.setHighlighted( false );
6012 this.unbindKeyDownListener();
6013 this.unbindKeyPressListener();
6014 // Don't prevent tabbing away / defocusing
6020 if ( nextItem
.constructor.static.highlightable
) {
6021 this.highlightItem( nextItem
);
6023 this.chooseItem( nextItem
);
6025 this.scrollItemIntoView( nextItem
);
6030 e
.stopPropagation();
6036 * Bind key down listener.
6040 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
6041 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
6045 * Unbind key down listener.
6049 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
6050 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
6054 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6056 * @param {OO.ui.OptionWidget} item Item to scroll into view
6058 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6060 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
6061 // and around 100-150 ms after it is finished.
6062 this.blockMouseOverEvents
++;
6063 item
.scrollElementIntoView().done( function () {
6064 setTimeout( function () {
6065 widget
.blockMouseOverEvents
--;
6071 * Clear the key-press buffer
6075 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6076 if ( this.keyPressBufferTimer
) {
6077 clearTimeout( this.keyPressBufferTimer
);
6078 this.keyPressBufferTimer
= null;
6080 this.keyPressBuffer
= '';
6084 * Handle key press events.
6087 * @param {KeyboardEvent} e Key press event
6089 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
6090 var c
, filter
, item
;
6092 if ( !e
.charCode
) {
6093 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6094 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6099 if ( String
.fromCodePoint
) {
6100 c
= String
.fromCodePoint( e
.charCode
);
6102 c
= String
.fromCharCode( e
.charCode
);
6105 if ( this.keyPressBufferTimer
) {
6106 clearTimeout( this.keyPressBufferTimer
);
6108 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6110 item
= this.getHighlightedItem() || this.getSelectedItem();
6112 if ( this.keyPressBuffer
=== c
) {
6113 // Common (if weird) special case: typing "xxxx" will cycle through all
6114 // the items beginning with "x".
6116 item
= this.getRelativeSelectableItem( item
, 1 );
6119 this.keyPressBuffer
+= c
;
6122 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6123 if ( !item
|| !filter( item
) ) {
6124 item
= this.getRelativeSelectableItem( item
, 1, filter
);
6127 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6128 this.highlightItem( item
);
6130 this.chooseItem( item
);
6132 this.scrollItemIntoView( item
);
6136 e
.stopPropagation();
6140 * Get a matcher for the specific string
6143 * @param {string} s String to match against items
6144 * @param {boolean} [exact=false] Only accept exact matches
6145 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6147 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
6150 if ( s
.normalize
) {
6153 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
6154 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
6158 re
= new RegExp( re
, 'i' );
6159 return function ( item
) {
6160 var matchText
= item
.getMatchText();
6161 if ( matchText
.normalize
) {
6162 matchText
= matchText
.normalize();
6164 return re
.test( matchText
);
6169 * Bind key press listener.
6173 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
6174 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
6178 * Unbind key down listener.
6180 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6185 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
6186 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
6187 this.clearKeyPressBuffer();
6191 * Visibility change handler
6194 * @param {boolean} visible
6196 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6198 this.clearKeyPressBuffer();
6203 * Get the closest item to a jQuery.Event.
6206 * @param {jQuery.Event} e
6207 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6209 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
6210 return $( e
.target
).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
6214 * Get selected item.
6216 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6218 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
6221 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6222 if ( this.items
[ i
].isSelected() ) {
6223 return this.items
[ i
];
6230 * Get highlighted item.
6232 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6234 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
6237 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6238 if ( this.items
[ i
].isHighlighted() ) {
6239 return this.items
[ i
];
6246 * Toggle pressed state.
6248 * Press is a state that occurs when a user mouses down on an item, but
6249 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
6250 * until the user releases the mouse.
6252 * @param {boolean} pressed An option is being pressed
6254 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
6255 if ( pressed
=== undefined ) {
6256 pressed
= !this.pressed
;
6258 if ( pressed
!== this.pressed
) {
6260 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
6261 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
6262 this.pressed
= pressed
;
6267 * Highlight an option. If the `item` param is omitted, no options will be highlighted
6268 * and any existing highlight will be removed. The highlight is mutually exclusive.
6270 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
6274 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
6275 var i
, len
, highlighted
,
6278 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6279 highlighted
= this.items
[ i
] === item
;
6280 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
6281 this.items
[ i
].setHighlighted( highlighted
);
6286 this.emit( 'highlight', item
);
6293 * Fetch an item by its label.
6295 * @param {string} label Label of the item to select.
6296 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6297 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
6299 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
6301 len
= this.items
.length
,
6302 filter
= this.getItemMatcher( label
, true );
6304 for ( i
= 0; i
< len
; i
++ ) {
6305 item
= this.items
[ i
];
6306 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6313 filter
= this.getItemMatcher( label
, false );
6314 for ( i
= 0; i
< len
; i
++ ) {
6315 item
= this.items
[ i
];
6316 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6332 * Programmatically select an option by its label. If the item does not exist,
6333 * all options will be deselected.
6335 * @param {string} [label] Label of the item to select.
6336 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6340 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
6341 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
6342 if ( label
=== undefined || !itemFromLabel
) {
6343 return this.selectItem();
6345 return this.selectItem( itemFromLabel
);
6349 * Programmatically select an option by its data. If the `data` parameter is omitted,
6350 * or if the item does not exist, all options will be deselected.
6352 * @param {Object|string} [data] Value of the item to select, omit to deselect all
6356 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
6357 var itemFromData
= this.getItemFromData( data
);
6358 if ( data
=== undefined || !itemFromData
) {
6359 return this.selectItem();
6361 return this.selectItem( itemFromData
);
6365 * Programmatically select an option by its reference. If the `item` parameter is omitted,
6366 * all options will be deselected.
6368 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
6372 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
6373 var i
, len
, selected
,
6376 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6377 selected
= this.items
[ i
] === item
;
6378 if ( this.items
[ i
].isSelected() !== selected
) {
6379 this.items
[ i
].setSelected( selected
);
6384 this.emit( 'select', item
);
6393 * Press is a state that occurs when a user mouses down on an item, but has not
6394 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
6395 * releases the mouse.
6397 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
6401 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
6402 var i
, len
, pressed
,
6405 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6406 pressed
= this.items
[ i
] === item
;
6407 if ( this.items
[ i
].isPressed() !== pressed
) {
6408 this.items
[ i
].setPressed( pressed
);
6413 this.emit( 'press', item
);
6422 * Note that ‘choose’ should never be modified programmatically. A user can choose
6423 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
6424 * use the #selectItem method.
6426 * This method is identical to #selectItem, but may vary in subclasses that take additional action
6427 * when users choose an item with the keyboard or mouse.
6429 * @param {OO.ui.OptionWidget} item Item to choose
6433 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
6435 this.selectItem( item
);
6436 this.emit( 'choose', item
);
6443 * Get an option by its position relative to the specified item (or to the start of the option array,
6444 * if item is `null`). The direction in which to search through the option array is specified with a
6445 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6446 * `null` if there are no options in the array.
6448 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6449 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6450 * @param {Function} [filter] Only consider items for which this function returns
6451 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
6452 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
6454 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
, filter
) {
6455 var currentIndex
, nextIndex
, i
,
6456 increase
= direction
> 0 ? 1 : -1,
6457 len
= this.items
.length
;
6459 if ( item
instanceof OO
.ui
.OptionWidget
) {
6460 currentIndex
= this.items
.indexOf( item
);
6461 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
6463 // If no item is selected and moving forward, start at the beginning.
6464 // If moving backward, start at the end.
6465 nextIndex
= direction
> 0 ? 0 : len
- 1;
6468 for ( i
= 0; i
< len
; i
++ ) {
6469 item
= this.items
[ nextIndex
];
6471 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
6472 ( !filter
|| filter( item
) )
6476 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
6482 * Get the next selectable item or `null` if there are no selectable items.
6483 * Disabled options and menu-section markers and breaks are not selectable.
6485 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
6487 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
6488 return this.getRelativeSelectableItem( null, 1 );
6492 * Add an array of options to the select. Optionally, an index number can be used to
6493 * specify an insertion point.
6495 * @param {OO.ui.OptionWidget[]} items Items to add
6496 * @param {number} [index] Index to insert items after
6500 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
6502 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
6504 // Always provide an index, even if it was omitted
6505 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
6511 * Remove the specified array of options from the select. Options will be detached
6512 * from the DOM, not removed, so they can be reused later. To remove all options from
6513 * the select, you may wish to use the #clearItems method instead.
6515 * @param {OO.ui.OptionWidget[]} items Items to remove
6519 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
6522 // Deselect items being removed
6523 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
6525 if ( item
.isSelected() ) {
6526 this.selectItem( null );
6531 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
6533 this.emit( 'remove', items
);
6539 * Clear all options from the select. Options will be detached from the DOM, not removed,
6540 * so that they can be reused later. To remove a subset of options from the select, use
6541 * the #removeItems method.
6546 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
6547 var items
= this.items
.slice();
6550 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
6553 this.selectItem( null );
6555 this.emit( 'remove', items
);
6561 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
6562 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
6563 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
6564 * options. For more information about options and selects, please see the
6565 * [OOjs UI documentation on MediaWiki][1].
6568 * // Decorated options in a select widget
6569 * var select = new OO.ui.SelectWidget( {
6571 * new OO.ui.DecoratedOptionWidget( {
6573 * label: 'Option with icon',
6576 * new OO.ui.DecoratedOptionWidget( {
6578 * label: 'Option with indicator',
6583 * $( 'body' ).append( select.$element );
6585 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6588 * @extends OO.ui.OptionWidget
6589 * @mixins OO.ui.mixin.IconElement
6590 * @mixins OO.ui.mixin.IndicatorElement
6593 * @param {Object} [config] Configuration options
6595 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
6596 // Parent constructor
6597 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
6599 // Mixin constructors
6600 OO
.ui
.mixin
.IconElement
.call( this, config
);
6601 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
6605 .addClass( 'oo-ui-decoratedOptionWidget' )
6606 .prepend( this.$icon
)
6607 .append( this.$indicator
);
6612 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
6613 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
6614 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
6617 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
6618 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
6619 * the [OOjs UI documentation on MediaWiki] [1] for more information.
6621 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6624 * @extends OO.ui.DecoratedOptionWidget
6627 * @param {Object} [config] Configuration options
6629 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
6630 // Configuration initialization
6631 config
= $.extend( { icon
: 'check' }, config
);
6633 // Parent constructor
6634 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
6638 .attr( 'role', 'menuitem' )
6639 .addClass( 'oo-ui-menuOptionWidget' );
6644 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
6646 /* Static Properties */
6652 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
6655 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
6656 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
6659 * var myDropdown = new OO.ui.DropdownWidget( {
6662 * new OO.ui.MenuSectionOptionWidget( {
6665 * new OO.ui.MenuOptionWidget( {
6667 * label: 'Welsh Corgi'
6669 * new OO.ui.MenuOptionWidget( {
6671 * label: 'Standard Poodle'
6673 * new OO.ui.MenuSectionOptionWidget( {
6676 * new OO.ui.MenuOptionWidget( {
6683 * $( 'body' ).append( myDropdown.$element );
6686 * @extends OO.ui.DecoratedOptionWidget
6689 * @param {Object} [config] Configuration options
6691 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
6692 // Parent constructor
6693 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
6696 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' )
6697 .attr( 'role', '' );
6702 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
6704 /* Static Properties */
6710 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
6716 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
6719 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
6720 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
6721 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
6722 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
6723 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
6724 * and customized to be opened, closed, and displayed as needed.
6726 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
6727 * mouse outside the menu.
6729 * Menus also have support for keyboard interaction:
6731 * - Enter/Return key: choose and select a menu option
6732 * - Up-arrow key: highlight the previous menu option
6733 * - Down-arrow key: highlight the next menu option
6734 * - Esc key: hide the menu
6736 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
6738 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6739 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6742 * @extends OO.ui.SelectWidget
6743 * @mixins OO.ui.mixin.ClippableElement
6746 * @param {Object} [config] Configuration options
6747 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
6748 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
6749 * and {@link OO.ui.mixin.LookupElement LookupElement}
6750 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
6751 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
6752 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
6753 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
6754 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
6755 * that button, unless the button (or its parent widget) is passed in here.
6756 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
6757 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
6758 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
6760 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
6761 // Configuration initialization
6762 config
= config
|| {};
6764 // Parent constructor
6765 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
6767 // Mixin constructors
6768 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
6771 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
6772 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
6773 this.filterFromInput
= !!config
.filterFromInput
;
6774 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
6775 this.$widget
= config
.widget
? config
.widget
.$element
: null;
6776 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
6777 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
6781 .addClass( 'oo-ui-menuSelectWidget' )
6782 .attr( 'role', 'menu' );
6784 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
6785 // that reference properties not initialized at that time of parent class construction
6786 // TODO: Find a better way to handle post-constructor setup
6787 this.visible
= false;
6788 this.$element
.addClass( 'oo-ui-element-hidden' );
6793 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
6794 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
6799 * Handles document mouse down events.
6802 * @param {MouseEvent} e Mouse down event
6804 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
6806 !OO
.ui
.contains( this.$element
[ 0 ], e
.target
, true ) &&
6807 ( !this.$widget
|| !OO
.ui
.contains( this.$widget
[ 0 ], e
.target
, true ) )
6809 this.toggle( false );
6816 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
6817 var currentItem
= this.getHighlightedItem() || this.getSelectedItem();
6819 if ( !this.isDisabled() && this.isVisible() ) {
6820 switch ( e
.keyCode
) {
6821 case OO
.ui
.Keys
.LEFT
:
6822 case OO
.ui
.Keys
.RIGHT
:
6823 // Do nothing if a text field is associated, arrow keys will be handled natively
6824 if ( !this.$input
) {
6825 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
6828 case OO
.ui
.Keys
.ESCAPE
:
6829 case OO
.ui
.Keys
.TAB
:
6830 if ( currentItem
) {
6831 currentItem
.setHighlighted( false );
6833 this.toggle( false );
6834 // Don't prevent tabbing away, prevent defocusing
6835 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
6837 e
.stopPropagation();
6841 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
6848 * Update menu item visibility after input changes.
6852 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
6853 var i
, item
, visible
, section
, sectionEmpty
,
6855 len
= this.items
.length
,
6856 showAll
= !this.isVisible(),
6857 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
6859 // Hide non-matching options, and also hide section headers if all options
6860 // in their section are hidden.
6861 for ( i
= 0; i
< len
; i
++ ) {
6862 item
= this.items
[ i
];
6863 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
6865 // If the previous section was empty, hide its header
6866 section
.toggle( showAll
|| !sectionEmpty
);
6869 sectionEmpty
= true;
6870 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
6871 visible
= showAll
|| filter( item
);
6872 anyVisible
= anyVisible
|| visible
;
6873 sectionEmpty
= sectionEmpty
&& !visible
;
6874 item
.toggle( visible
);
6877 // Process the final section
6879 section
.toggle( showAll
|| !sectionEmpty
);
6882 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
6884 // Reevaluate clipping
6891 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
6892 if ( this.$input
) {
6893 this.$input
.on( 'keydown', this.onKeyDownHandler
);
6895 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
6902 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
6903 if ( this.$input
) {
6904 this.$input
.off( 'keydown', this.onKeyDownHandler
);
6906 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
6913 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
6914 if ( this.$input
) {
6915 if ( this.filterFromInput
) {
6916 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6919 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
6926 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
6927 if ( this.$input
) {
6928 if ( this.filterFromInput
) {
6929 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
6930 this.updateItemVisibility();
6933 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
6940 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
6942 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
6943 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
6945 * @param {OO.ui.OptionWidget} item Item to choose
6948 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
6949 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
6950 if ( this.hideOnChoose
) {
6951 this.toggle( false );
6959 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
6961 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
6963 // Reevaluate clipping
6972 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
6974 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
6976 // Reevaluate clipping
6985 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
6987 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
6989 // Reevaluate clipping
6996 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
6997 * `.toggle( true )` after its #$element is attached to the DOM.
6999 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7000 * it in the right place and with the right dimensions only work correctly while it is attached.
7001 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7002 * strictly enforced, so currently it only generates a warning in the browser console.
7006 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7009 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7010 change
= visible
!== this.isVisible();
7012 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7013 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7014 this.warnedUnattached
= true;
7018 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7022 this.bindKeyDownListener();
7023 this.bindKeyPressListener();
7025 this.toggleClipping( true );
7027 if ( this.getSelectedItem() ) {
7028 this.getSelectedItem().scrollElementIntoView( { duration
: 0 } );
7032 if ( this.autoHide
) {
7033 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7036 this.unbindKeyDownListener();
7037 this.unbindKeyPressListener();
7038 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7039 this.toggleClipping( false );
7047 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
7048 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
7049 * users can interact with it.
7051 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7052 * OO.ui.DropdownInputWidget instead.
7055 * // Example: A DropdownWidget with a menu that contains three options
7056 * var dropDown = new OO.ui.DropdownWidget( {
7057 * label: 'Dropdown menu: Select a menu option',
7060 * new OO.ui.MenuOptionWidget( {
7064 * new OO.ui.MenuOptionWidget( {
7068 * new OO.ui.MenuOptionWidget( {
7076 * $( 'body' ).append( dropDown.$element );
7078 * dropDown.getMenu().selectItemByData( 'b' );
7080 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
7082 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
7084 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
7087 * @extends OO.ui.Widget
7088 * @mixins OO.ui.mixin.IconElement
7089 * @mixins OO.ui.mixin.IndicatorElement
7090 * @mixins OO.ui.mixin.LabelElement
7091 * @mixins OO.ui.mixin.TitledElement
7092 * @mixins OO.ui.mixin.TabIndexedElement
7095 * @param {Object} [config] Configuration options
7096 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
7097 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
7098 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
7099 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
7101 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
7102 // Configuration initialization
7103 config
= $.extend( { indicator
: 'down' }, config
);
7105 // Parent constructor
7106 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
7108 // Properties (must be set before TabIndexedElement constructor call)
7109 this.$handle
= this.$( '<span>' );
7110 this.$overlay
= config
.$overlay
|| this.$element
;
7112 // Mixin constructors
7113 OO
.ui
.mixin
.IconElement
.call( this, config
);
7114 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7115 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7116 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
7117 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
7120 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend( {
7122 $container
: this.$element
7127 click
: this.onClick
.bind( this ),
7128 keydown
: this.onKeyDown
.bind( this ),
7129 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
7130 keypress
: this.menu
.onKeyPressHandler
,
7131 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
7133 this.menu
.connect( this, {
7134 select
: 'onMenuSelect',
7135 toggle
: 'onMenuToggle'
7140 .addClass( 'oo-ui-dropdownWidget-handle' )
7141 .append( this.$icon
, this.$label
, this.$indicator
);
7143 .addClass( 'oo-ui-dropdownWidget' )
7144 .append( this.$handle
);
7145 this.$overlay
.append( this.menu
.$element
);
7150 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
7151 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
7152 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
7153 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
7154 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
7155 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7162 * @return {OO.ui.MenuSelectWidget} Menu of widget
7164 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
7169 * Handles menu select events.
7172 * @param {OO.ui.MenuOptionWidget} item Selected menu item
7174 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
7178 this.setLabel( null );
7182 selectedLabel
= item
.getLabel();
7184 // If the label is a DOM element, clone it, because setLabel will append() it
7185 if ( selectedLabel
instanceof jQuery
) {
7186 selectedLabel
= selectedLabel
.clone();
7189 this.setLabel( selectedLabel
);
7193 * Handle menu toggle events.
7196 * @param {boolean} isVisible Menu toggle event
7198 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
7199 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
7203 * Handle mouse click events.
7206 * @param {jQuery.Event} e Mouse click event
7208 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
7209 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7216 * Handle key down events.
7219 * @param {jQuery.Event} e Key down event
7221 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
7223 !this.isDisabled() &&
7225 e
.which
=== OO
.ui
.Keys
.ENTER
||
7227 !this.menu
.isVisible() &&
7229 e
.which
=== OO
.ui
.Keys
.SPACE
||
7230 e
.which
=== OO
.ui
.Keys
.UP
||
7231 e
.which
=== OO
.ui
.Keys
.DOWN
7242 * RadioOptionWidget is an option widget that looks like a radio button.
7243 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
7244 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
7246 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
7249 * @extends OO.ui.OptionWidget
7252 * @param {Object} [config] Configuration options
7254 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
7255 // Configuration initialization
7256 config
= config
|| {};
7258 // Properties (must be done before parent constructor which calls #setDisabled)
7259 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
7261 // Parent constructor
7262 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
7265 // Remove implicit role, we're handling it ourselves
7266 this.radio
.$input
.attr( 'role', 'presentation' );
7268 .addClass( 'oo-ui-radioOptionWidget' )
7269 .attr( 'role', 'radio' )
7270 .attr( 'aria-checked', 'false' )
7271 .removeAttr( 'aria-selected' )
7272 .prepend( this.radio
.$element
);
7277 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
7279 /* Static Properties */
7285 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
7291 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
7297 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
7303 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
7310 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
7311 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
7313 this.radio
.setSelected( state
);
7315 .attr( 'aria-checked', state
.toString() )
7316 .removeAttr( 'aria-selected' );
7324 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
7325 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
7327 this.radio
.setDisabled( this.isDisabled() );
7333 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
7334 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
7335 * an interface for adding, removing and selecting options.
7336 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
7338 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7339 * OO.ui.RadioSelectInputWidget instead.
7342 * // A RadioSelectWidget with RadioOptions.
7343 * var option1 = new OO.ui.RadioOptionWidget( {
7345 * label: 'Selected radio option'
7348 * var option2 = new OO.ui.RadioOptionWidget( {
7350 * label: 'Unselected radio option'
7353 * var radioSelect=new OO.ui.RadioSelectWidget( {
7354 * items: [ option1, option2 ]
7357 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
7358 * radioSelect.selectItem( option1 );
7360 * $( 'body' ).append( radioSelect.$element );
7362 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
7366 * @extends OO.ui.SelectWidget
7367 * @mixins OO.ui.mixin.TabIndexedElement
7370 * @param {Object} [config] Configuration options
7372 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
7373 // Parent constructor
7374 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
7376 // Mixin constructors
7377 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
7381 focus
: this.bindKeyDownListener
.bind( this ),
7382 blur
: this.unbindKeyDownListener
.bind( this )
7387 .addClass( 'oo-ui-radioSelectWidget' )
7388 .attr( 'role', 'radiogroup' );
7393 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
7394 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7397 * MultioptionWidgets are special elements that can be selected and configured with data. The
7398 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
7399 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
7400 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
7402 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Multioptions
7405 * @extends OO.ui.Widget
7406 * @mixins OO.ui.mixin.ItemWidget
7407 * @mixins OO.ui.mixin.LabelElement
7410 * @param {Object} [config] Configuration options
7411 * @cfg {boolean} [selected=false] Whether the option is initially selected
7413 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
7414 // Configuration initialization
7415 config
= config
|| {};
7417 // Parent constructor
7418 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
7420 // Mixin constructors
7421 OO
.ui
.mixin
.ItemWidget
.call( this );
7422 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7425 this.selected
= null;
7429 .addClass( 'oo-ui-multioptionWidget' )
7430 .append( this.$label
);
7431 this.setSelected( config
.selected
);
7436 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
7437 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
7438 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
7445 * A change event is emitted when the selected state of the option changes.
7447 * @param {boolean} selected Whether the option is now selected
7453 * Check if the option is selected.
7455 * @return {boolean} Item is selected
7457 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
7458 return this.selected
;
7462 * Set the option’s selected state. In general, all modifications to the selection
7463 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
7464 * method instead of this method.
7466 * @param {boolean} [state=false] Select option
7469 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
7471 if ( this.selected
!== state
) {
7472 this.selected
= state
;
7473 this.emit( 'change', state
);
7474 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
7480 * MultiselectWidget allows selecting multiple options from a list.
7482 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
7484 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
7488 * @extends OO.ui.Widget
7489 * @mixins OO.ui.mixin.GroupWidget
7492 * @param {Object} [config] Configuration options
7493 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
7495 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
7496 // Parent constructor
7497 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
7499 // Configuration initialization
7500 config
= config
|| {};
7502 // Mixin constructors
7503 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
7506 this.aggregate( { change
: 'select' } );
7507 // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
7508 // by GroupElement only when items are added/removed
7509 this.connect( this, { select
: [ 'emit', 'change' ] } );
7512 if ( config
.items
) {
7513 this.addItems( config
.items
);
7515 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
7516 this.$element
.addClass( 'oo-ui-multiselectWidget' )
7517 .append( this.$group
);
7522 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
7523 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
7530 * A change event is emitted when the set of items changes, or an item is selected or deselected.
7536 * A select event is emitted when an item is selected or deselected.
7542 * Get options that are selected.
7544 * @return {OO.ui.MultioptionWidget[]} Selected options
7546 OO
.ui
.MultiselectWidget
.prototype.getSelectedItems = function () {
7547 return this.items
.filter( function ( item
) {
7548 return item
.isSelected();
7553 * Get the data of options that are selected.
7555 * @return {Object[]|string[]} Values of selected options
7557 OO
.ui
.MultiselectWidget
.prototype.getSelectedItemsData = function () {
7558 return this.getSelectedItems().map( function ( item
) {
7564 * Select options by reference. Options not mentioned in the `items` array will be deselected.
7566 * @param {OO.ui.MultioptionWidget[]} items Items to select
7569 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
7570 this.items
.forEach( function ( item
) {
7571 var selected
= items
.indexOf( item
) !== -1;
7572 item
.setSelected( selected
);
7578 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
7580 * @param {Object[]|string[]} datas Values of items to select
7583 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
7586 items
= datas
.map( function ( data
) {
7587 return widget
.getItemFromData( data
);
7589 this.selectItems( items
);
7594 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
7595 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
7596 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
7598 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
7601 * @extends OO.ui.MultioptionWidget
7604 * @param {Object} [config] Configuration options
7606 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
7607 // Configuration initialization
7608 config
= config
|| {};
7610 // Properties (must be done before parent constructor which calls #setDisabled)
7611 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
7613 // Parent constructor
7614 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
7617 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
7618 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
7622 .addClass( 'oo-ui-checkboxMultioptionWidget' )
7623 .prepend( this.checkbox
.$element
);
7628 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
7630 /* Static Properties */
7636 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
7641 * Handle checkbox selected state change.
7645 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
7646 this.setSelected( this.checkbox
.isSelected() );
7652 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
7653 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
7654 this.checkbox
.setSelected( state
);
7661 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
7662 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
7663 this.checkbox
.setDisabled( this.isDisabled() );
7670 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
7671 this.checkbox
.focus();
7675 * Handle key down events.
7678 * @param {jQuery.Event} e
7680 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
7682 element
= this.getElementGroup(),
7685 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
7686 nextItem
= element
.getRelativeFocusableItem( this, -1 );
7687 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
7688 nextItem
= element
.getRelativeFocusableItem( this, 1 );
7698 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
7699 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
7700 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
7701 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
7703 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7704 * OO.ui.CheckboxMultiselectInputWidget instead.
7707 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
7708 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
7711 * label: 'Selected checkbox'
7714 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
7716 * label: 'Unselected checkbox'
7719 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
7720 * items: [ option1, option2 ]
7723 * $( 'body' ).append( multiselect.$element );
7725 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
7728 * @extends OO.ui.MultiselectWidget
7731 * @param {Object} [config] Configuration options
7733 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
7734 // Parent constructor
7735 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
7738 this.$lastClicked
= null;
7741 this.$group
.on( 'click', this.onClick
.bind( this ) );
7745 .addClass( 'oo-ui-checkboxMultiselectWidget' );
7750 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
7755 * Get an option by its position relative to the specified item (or to the start of the option array,
7756 * if item is `null`). The direction in which to search through the option array is specified with a
7757 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
7758 * `null` if there are no options in the array.
7760 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
7761 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7762 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
7764 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
7765 var currentIndex
, nextIndex
, i
,
7766 increase
= direction
> 0 ? 1 : -1,
7767 len
= this.items
.length
;
7770 currentIndex
= this.items
.indexOf( item
);
7771 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7773 // If no item is selected and moving forward, start at the beginning.
7774 // If moving backward, start at the end.
7775 nextIndex
= direction
> 0 ? 0 : len
- 1;
7778 for ( i
= 0; i
< len
; i
++ ) {
7779 item
= this.items
[ nextIndex
];
7780 if ( item
&& !item
.isDisabled() ) {
7783 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7789 * Handle click events on checkboxes.
7791 * @param {jQuery.Event} e
7793 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
7794 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
7795 $lastClicked
= this.$lastClicked
,
7796 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
7797 .not( '.oo-ui-widget-disabled' );
7799 // Allow selecting multiple options at once by Shift-clicking them
7800 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
7801 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
7802 lastClickedIndex
= $options
.index( $lastClicked
);
7803 nowClickedIndex
= $options
.index( $nowClicked
);
7804 // If it's the same item, either the user is being silly, or it's a fake event generated by the
7805 // browser. In either case we don't need custom handling.
7806 if ( nowClickedIndex
!== lastClickedIndex
) {
7808 wasSelected
= items
[ nowClickedIndex
].isSelected();
7809 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
7811 // This depends on the DOM order of the items and the order of the .items array being the same.
7812 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
7813 if ( !items
[ i
].isDisabled() ) {
7814 items
[ i
].setSelected( !wasSelected
);
7817 // For the now-clicked element, use immediate timeout to allow the browser to do its own
7818 // handling first, then set our value. The order in which events happen is different for
7819 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
7820 // non-click actions that change the checkboxes.
7822 setTimeout( function () {
7823 if ( !items
[ nowClickedIndex
].isDisabled() ) {
7824 items
[ nowClickedIndex
].setSelected( !wasSelected
);
7830 if ( $nowClicked
.length
) {
7831 this.$lastClicked
= $nowClicked
;
7836 * FloatingMenuSelectWidget is a menu that will stick under a specified
7837 * container, even when it is inserted elsewhere in the document (for example,
7838 * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
7839 * menu from being clipped too aggresively.
7841 * The menu's position is automatically calculated and maintained when the menu
7842 * is toggled or the window is resized.
7844 * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
7847 * @extends OO.ui.MenuSelectWidget
7848 * @mixins OO.ui.mixin.FloatableElement
7851 * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
7852 * Deprecated, omit this parameter and specify `$container` instead.
7853 * @param {Object} [config] Configuration options
7854 * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
7856 OO
.ui
.FloatingMenuSelectWidget
= function OoUiFloatingMenuSelectWidget( inputWidget
, config
) {
7857 // Allow 'inputWidget' parameter and config for backwards compatibility
7858 if ( OO
.isPlainObject( inputWidget
) && config
=== undefined ) {
7859 config
= inputWidget
;
7860 inputWidget
= config
.inputWidget
;
7863 // Configuration initialization
7864 config
= config
|| {};
7866 // Parent constructor
7867 OO
.ui
.FloatingMenuSelectWidget
.parent
.call( this, config
);
7869 // Properties (must be set before mixin constructors)
7870 this.inputWidget
= inputWidget
; // For backwards compatibility
7871 this.$container
= config
.$container
|| this.inputWidget
.$element
;
7873 // Mixins constructors
7874 OO
.ui
.mixin
.FloatableElement
.call( this, $.extend( {}, config
, { $floatableContainer
: this.$container
} ) );
7877 this.$element
.addClass( 'oo-ui-floatingMenuSelectWidget' );
7878 // For backwards compatibility
7879 this.$element
.addClass( 'oo-ui-textInputMenuSelectWidget' );
7884 OO
.inheritClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.MenuSelectWidget
);
7885 OO
.mixinClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7892 OO
.ui
.FloatingMenuSelectWidget
.prototype.toggle = function ( visible
) {
7894 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
7895 change
= visible
!== this.isVisible();
7897 if ( change
&& visible
) {
7898 // Make sure the width is set before the parent method runs.
7899 this.setIdealSize( this.$container
.width() );
7903 // This will call this.clip(), which is nonsensical since we're not positioned yet...
7904 OO
.ui
.FloatingMenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7907 this.togglePositioning( this.isVisible() );
7914 * Progress bars visually display the status of an operation, such as a download,
7915 * and can be either determinate or indeterminate:
7917 * - **determinate** process bars show the percent of an operation that is complete.
7919 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
7920 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
7921 * not use percentages.
7923 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
7926 * // Examples of determinate and indeterminate progress bars.
7927 * var progressBar1 = new OO.ui.ProgressBarWidget( {
7930 * var progressBar2 = new OO.ui.ProgressBarWidget();
7932 * // Create a FieldsetLayout to layout progress bars
7933 * var fieldset = new OO.ui.FieldsetLayout;
7934 * fieldset.addItems( [
7935 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
7936 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
7938 * $( 'body' ).append( fieldset.$element );
7941 * @extends OO.ui.Widget
7944 * @param {Object} [config] Configuration options
7945 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
7946 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
7947 * By default, the progress bar is indeterminate.
7949 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
7950 // Configuration initialization
7951 config
= config
|| {};
7953 // Parent constructor
7954 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
7957 this.$bar
= $( '<div>' );
7958 this.progress
= null;
7961 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
7962 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
7965 role
: 'progressbar',
7967 'aria-valuemax': 100
7969 .addClass( 'oo-ui-progressBarWidget' )
7970 .append( this.$bar
);
7975 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
7977 /* Static Properties */
7983 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
7988 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
7990 * @return {number|boolean} Progress percent
7992 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
7993 return this.progress
;
7997 * Set the percent of the process completed or `false` for an indeterminate process.
7999 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8001 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8002 this.progress
= progress
;
8004 if ( progress
!== false ) {
8005 this.$bar
.css( 'width', this.progress
+ '%' );
8006 this.$element
.attr( 'aria-valuenow', this.progress
);
8008 this.$bar
.css( 'width', '' );
8009 this.$element
.removeAttr( 'aria-valuenow' );
8011 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8015 * InputWidget is the base class for all input widgets, which
8016 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8017 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8018 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8020 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8024 * @extends OO.ui.Widget
8025 * @mixins OO.ui.mixin.FlaggedElement
8026 * @mixins OO.ui.mixin.TabIndexedElement
8027 * @mixins OO.ui.mixin.TitledElement
8028 * @mixins OO.ui.mixin.AccessKeyedElement
8031 * @param {Object} [config] Configuration options
8032 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8033 * @cfg {string} [value=''] The value of the input.
8034 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8035 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8036 * before it is accepted.
8038 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8039 // Configuration initialization
8040 config
= config
|| {};
8042 // Parent constructor
8043 OO
.ui
.InputWidget
.parent
.call( this, config
);
8046 // See #reusePreInfuseDOM about config.$input
8047 this.$input
= config
.$input
|| this.getInputElement( config
);
8049 this.inputFilter
= config
.inputFilter
;
8051 // Mixin constructors
8052 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8053 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8054 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8055 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8058 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8062 .addClass( 'oo-ui-inputWidget-input' )
8063 .attr( 'name', config
.name
)
8064 .prop( 'disabled', this.isDisabled() );
8066 .addClass( 'oo-ui-inputWidget' )
8067 .append( this.$input
);
8068 this.setValue( config
.value
);
8070 this.setDir( config
.dir
);
8076 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8077 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8078 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8079 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8080 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8082 /* Static Properties */
8088 OO
.ui
.InputWidget
.static.supportsSimpleLabel
= true;
8090 /* Static Methods */
8095 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8096 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8097 // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
8098 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8105 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8106 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8107 if ( config
.$input
&& config
.$input
.length
) {
8108 state
.value
= config
.$input
.val();
8109 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8110 state
.focus
= config
.$input
.is( ':focus' );
8120 * A change event is emitted when the value of the input changes.
8122 * @param {string} value
8128 * Get input element.
8130 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
8131 * different circumstances. The element must have a `value` property (like form elements).
8134 * @param {Object} config Configuration options
8135 * @return {jQuery} Input element
8137 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
8138 return $( '<input>' );
8142 * Get input element's ID.
8144 * If the element already has an ID then that is returned, otherwise unique ID is
8145 * generated, set on the element, and returned.
8147 * @return {string} The ID of the element
8149 OO
.ui
.InputWidget
.prototype.getInputId = function () {
8150 var id
= this.$input
.attr( 'id' );
8152 if ( id
=== undefined ) {
8153 id
= OO
.ui
.generateElementId();
8154 this.$input
.attr( 'id', id
);
8161 * Handle potentially value-changing events.
8164 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8166 OO
.ui
.InputWidget
.prototype.onEdit = function () {
8168 if ( !this.isDisabled() ) {
8169 // Allow the stack to clear so the value will be updated
8170 setTimeout( function () {
8171 widget
.setValue( widget
.$input
.val() );
8177 * Get the value of the input.
8179 * @return {string} Input value
8181 OO
.ui
.InputWidget
.prototype.getValue = function () {
8182 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8183 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8184 var value
= this.$input
.val();
8185 if ( this.value
!== value
) {
8186 this.setValue( value
);
8192 * Set the directionality of the input.
8194 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
8197 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
8198 this.$input
.prop( 'dir', dir
);
8203 * Set the value of the input.
8205 * @param {string} value New value
8209 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
8210 value
= this.cleanUpValue( value
);
8211 // Update the DOM if it has changed. Note that with cleanUpValue, it
8212 // is possible for the DOM value to change without this.value changing.
8213 if ( this.$input
.val() !== value
) {
8214 this.$input
.val( value
);
8216 if ( this.value
!== value
) {
8218 this.emit( 'change', this.value
);
8224 * Clean up incoming value.
8226 * Ensures value is a string, and converts undefined and null to empty string.
8229 * @param {string} value Original value
8230 * @return {string} Cleaned up value
8232 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
8233 if ( value
=== undefined || value
=== null ) {
8235 } else if ( this.inputFilter
) {
8236 return this.inputFilter( String( value
) );
8238 return String( value
);
8243 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
8244 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
8247 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
8248 OO
.ui
.warnDeprecation( 'InputWidget: simulateLabelClick() is deprecated.' );
8249 if ( !this.isDisabled() ) {
8250 if ( this.$input
.is( ':checkbox, :radio' ) ) {
8251 this.$input
.click();
8253 if ( this.$input
.is( ':input' ) ) {
8254 this.$input
[ 0 ].focus();
8262 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
8263 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8264 if ( this.$input
) {
8265 this.$input
.prop( 'disabled', this.isDisabled() );
8275 OO
.ui
.InputWidget
.prototype.focus = function () {
8276 this.$input
[ 0 ].focus();
8285 OO
.ui
.InputWidget
.prototype.blur = function () {
8286 this.$input
[ 0 ].blur();
8293 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
8294 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8295 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
8296 this.setValue( state
.value
);
8298 if ( state
.focus
) {
8304 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
8305 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
8306 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
8307 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
8308 * [OOjs UI documentation on MediaWiki] [1] for more information.
8311 * // A ButtonInputWidget rendered as an HTML button, the default.
8312 * var button = new OO.ui.ButtonInputWidget( {
8313 * label: 'Input button',
8317 * $( 'body' ).append( button.$element );
8319 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
8322 * @extends OO.ui.InputWidget
8323 * @mixins OO.ui.mixin.ButtonElement
8324 * @mixins OO.ui.mixin.IconElement
8325 * @mixins OO.ui.mixin.IndicatorElement
8326 * @mixins OO.ui.mixin.LabelElement
8327 * @mixins OO.ui.mixin.TitledElement
8330 * @param {Object} [config] Configuration options
8331 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
8332 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
8333 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
8334 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
8335 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
8337 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
8338 // Configuration initialization
8339 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
8341 // See InputWidget#reusePreInfuseDOM about config.$input
8342 if ( config
.$input
) {
8343 config
.$input
.empty();
8346 // Properties (must be set before parent constructor, which calls #setValue)
8347 this.useInputTag
= config
.useInputTag
;
8349 // Parent constructor
8350 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
8352 // Mixin constructors
8353 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
8354 OO
.ui
.mixin
.IconElement
.call( this, config
);
8355 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8356 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8357 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8360 if ( !config
.useInputTag
) {
8361 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
8363 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
8368 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
8369 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
8370 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
8371 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
8372 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
8373 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
8375 /* Static Properties */
8378 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
8379 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
8384 OO
.ui
.ButtonInputWidget
.static.supportsSimpleLabel
= false;
8390 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
8398 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
8400 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
8401 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
8407 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
8409 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
8410 * text, or `null` for no label
8413 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
8414 if ( typeof label
=== 'function' ) {
8415 label
= OO
.ui
.resolveMsg( label
);
8418 if ( this.useInputTag
) {
8419 // Discard non-plaintext labels
8420 if ( typeof label
!== 'string' ) {
8424 this.$input
.val( label
);
8427 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
8431 * Set the value of the input.
8433 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
8434 * they do not support {@link #value values}.
8436 * @param {string} value New value
8439 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
8440 if ( !this.useInputTag
) {
8441 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
8447 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
8448 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
8449 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
8450 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
8452 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
8455 * // An example of selected, unselected, and disabled checkbox inputs
8456 * var checkbox1=new OO.ui.CheckboxInputWidget( {
8460 * var checkbox2=new OO.ui.CheckboxInputWidget( {
8463 * var checkbox3=new OO.ui.CheckboxInputWidget( {
8467 * // Create a fieldset layout with fields for each checkbox.
8468 * var fieldset = new OO.ui.FieldsetLayout( {
8469 * label: 'Checkboxes'
8471 * fieldset.addItems( [
8472 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
8473 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
8474 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
8476 * $( 'body' ).append( fieldset.$element );
8478 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8481 * @extends OO.ui.InputWidget
8484 * @param {Object} [config] Configuration options
8485 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
8487 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
8488 // Configuration initialization
8489 config
= config
|| {};
8491 // Parent constructor
8492 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
8496 .addClass( 'oo-ui-checkboxInputWidget' )
8497 // Required for pretty styling in MediaWiki theme
8498 .append( $( '<span>' ) );
8499 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
8504 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
8506 /* Static Properties */
8512 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
8514 /* Static Methods */
8519 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8520 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8521 state
.checked
= config
.$input
.prop( 'checked' );
8531 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
8532 return $( '<input>' ).attr( 'type', 'checkbox' );
8538 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
8540 if ( !this.isDisabled() ) {
8541 // Allow the stack to clear so the value will be updated
8542 setTimeout( function () {
8543 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
8549 * Set selection state of this checkbox.
8551 * @param {boolean} state `true` for selected
8554 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
8556 if ( this.selected
!== state
) {
8557 this.selected
= state
;
8558 this.$input
.prop( 'checked', this.selected
);
8559 this.emit( 'change', this.selected
);
8565 * Check if this checkbox is selected.
8567 * @return {boolean} Checkbox is selected
8569 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
8570 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8571 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8572 var selected
= this.$input
.prop( 'checked' );
8573 if ( this.selected
!== selected
) {
8574 this.setSelected( selected
);
8576 return this.selected
;
8582 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8583 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8584 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
8585 this.setSelected( state
.checked
);
8590 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
8591 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8592 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8593 * more information about input widgets.
8595 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
8596 * are no options. If no `value` configuration option is provided, the first option is selected.
8597 * If you need a state representing no value (no option being selected), use a DropdownWidget.
8599 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
8602 * // Example: A DropdownInputWidget with three options
8603 * var dropdownInput = new OO.ui.DropdownInputWidget( {
8605 * { data: 'a', label: 'First' },
8606 * { data: 'b', label: 'Second'},
8607 * { data: 'c', label: 'Third' }
8610 * $( 'body' ).append( dropdownInput.$element );
8612 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8615 * @extends OO.ui.InputWidget
8616 * @mixins OO.ui.mixin.TitledElement
8619 * @param {Object} [config] Configuration options
8620 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8621 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
8623 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
8624 // Configuration initialization
8625 config
= config
|| {};
8627 // See InputWidget#reusePreInfuseDOM about config.$input
8628 if ( config
.$input
) {
8629 config
.$input
.addClass( 'oo-ui-element-hidden' );
8632 // Properties (must be done before parent constructor which calls #setDisabled)
8633 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
8635 // Parent constructor
8636 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
8638 // Mixin constructors
8639 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8642 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
8645 this.setOptions( config
.options
|| [] );
8647 .addClass( 'oo-ui-dropdownInputWidget' )
8648 .append( this.dropdownWidget
.$element
);
8653 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
8654 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
8662 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
8663 return $( '<input>' ).attr( 'type', 'hidden' );
8667 * Handles menu select events.
8670 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8672 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
8673 this.setValue( item
.getData() );
8679 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
8680 value
= this.cleanUpValue( value
);
8681 this.dropdownWidget
.getMenu().selectItemByData( value
);
8682 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
8689 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
8690 this.dropdownWidget
.setDisabled( state
);
8691 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8696 * Set the options available for this input.
8698 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8701 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
8703 value
= this.getValue(),
8706 // Rebuild the dropdown menu
8707 this.dropdownWidget
.getMenu()
8709 .addItems( options
.map( function ( opt
) {
8710 var optValue
= widget
.cleanUpValue( opt
.data
);
8712 if ( opt
.optgroup
=== undefined ) {
8713 return new OO
.ui
.MenuOptionWidget( {
8715 label
: opt
.label
!== undefined ? opt
.label
: optValue
8718 return new OO
.ui
.MenuSectionOptionWidget( {
8724 // Restore the previous value, or reset to something sensible
8725 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
8726 // Previous value is still available, ensure consistency with the dropdown
8727 this.setValue( value
);
8729 // No longer valid, reset
8730 if ( options
.length
) {
8731 this.setValue( options
[ 0 ].data
);
8741 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
8742 this.dropdownWidget
.getMenu().toggle( true );
8749 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
8750 this.dropdownWidget
.getMenu().toggle( false );
8755 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
8756 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
8757 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
8758 * please see the [OOjs UI documentation on MediaWiki][1].
8760 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
8763 * // An example of selected, unselected, and disabled radio inputs
8764 * var radio1 = new OO.ui.RadioInputWidget( {
8768 * var radio2 = new OO.ui.RadioInputWidget( {
8771 * var radio3 = new OO.ui.RadioInputWidget( {
8775 * // Create a fieldset layout with fields for each radio button.
8776 * var fieldset = new OO.ui.FieldsetLayout( {
8777 * label: 'Radio inputs'
8779 * fieldset.addItems( [
8780 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
8781 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
8782 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
8784 * $( 'body' ).append( fieldset.$element );
8786 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8789 * @extends OO.ui.InputWidget
8792 * @param {Object} [config] Configuration options
8793 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
8795 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
8796 // Configuration initialization
8797 config
= config
|| {};
8799 // Parent constructor
8800 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
8804 .addClass( 'oo-ui-radioInputWidget' )
8805 // Required for pretty styling in MediaWiki theme
8806 .append( $( '<span>' ) );
8807 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
8812 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
8814 /* Static Properties */
8820 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
8822 /* Static Methods */
8827 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8828 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8829 state
.checked
= config
.$input
.prop( 'checked' );
8839 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
8840 return $( '<input>' ).attr( 'type', 'radio' );
8846 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
8847 // RadioInputWidget doesn't track its state.
8851 * Set selection state of this radio button.
8853 * @param {boolean} state `true` for selected
8856 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
8857 // RadioInputWidget doesn't track its state.
8858 this.$input
.prop( 'checked', state
);
8863 * Check if this radio button is selected.
8865 * @return {boolean} Radio is selected
8867 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
8868 return this.$input
.prop( 'checked' );
8874 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8875 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8876 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
8877 this.setSelected( state
.checked
);
8882 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
8883 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8884 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8885 * more information about input widgets.
8887 * This and OO.ui.DropdownInputWidget support the same configuration options.
8890 * // Example: A RadioSelectInputWidget with three options
8891 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
8893 * { data: 'a', label: 'First' },
8894 * { data: 'b', label: 'Second'},
8895 * { data: 'c', label: 'Third' }
8898 * $( 'body' ).append( radioSelectInput.$element );
8900 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8903 * @extends OO.ui.InputWidget
8906 * @param {Object} [config] Configuration options
8907 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8909 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
8910 // Configuration initialization
8911 config
= config
|| {};
8913 // Properties (must be done before parent constructor which calls #setDisabled)
8914 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
8916 // Parent constructor
8917 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
8920 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
8923 this.setOptions( config
.options
|| [] );
8925 .addClass( 'oo-ui-radioSelectInputWidget' )
8926 .append( this.radioSelectWidget
.$element
);
8931 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
8933 /* Static Properties */
8939 OO
.ui
.RadioSelectInputWidget
.static.supportsSimpleLabel
= false;
8941 /* Static Methods */
8946 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8947 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8948 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
8955 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8956 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8957 // Cannot reuse the `<input type=radio>` set
8958 delete config
.$input
;
8968 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
8969 return $( '<input>' ).attr( 'type', 'hidden' );
8973 * Handles menu select events.
8976 * @param {OO.ui.RadioOptionWidget} item Selected menu item
8978 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
8979 this.setValue( item
.getData() );
8985 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
8986 value
= this.cleanUpValue( value
);
8987 this.radioSelectWidget
.selectItemByData( value
);
8988 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
8995 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
8996 this.radioSelectWidget
.setDisabled( state
);
8997 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9002 * Set the options available for this input.
9004 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9007 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
9009 value
= this.getValue(),
9012 // Rebuild the radioSelect menu
9013 this.radioSelectWidget
9015 .addItems( options
.map( function ( opt
) {
9016 var optValue
= widget
.cleanUpValue( opt
.data
);
9017 return new OO
.ui
.RadioOptionWidget( {
9019 label
: opt
.label
!== undefined ? opt
.label
: optValue
9023 // Restore the previous value, or reset to something sensible
9024 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
9025 // Previous value is still available, ensure consistency with the radioSelect
9026 this.setValue( value
);
9028 // No longer valid, reset
9029 if ( options
.length
) {
9030 this.setValue( options
[ 0 ].data
);
9038 * CheckboxMultiselectInputWidget is a
9039 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
9040 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
9041 * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on MediaWiki][1] for
9042 * more information about input widgets.
9045 * // Example: A CheckboxMultiselectInputWidget with three options
9046 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
9048 * { data: 'a', label: 'First' },
9049 * { data: 'b', label: 'Second'},
9050 * { data: 'c', label: 'Third' }
9053 * $( 'body' ).append( multiselectInput.$element );
9055 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9058 * @extends OO.ui.InputWidget
9061 * @param {Object} [config] Configuration options
9062 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
9064 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
9065 // Configuration initialization
9066 config
= config
|| {};
9068 // Properties (must be done before parent constructor which calls #setDisabled)
9069 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
9071 // Parent constructor
9072 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
9075 this.inputName
= config
.name
;
9079 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
9080 .append( this.checkboxMultiselectWidget
.$element
);
9081 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
9082 this.$input
.detach();
9083 this.setOptions( config
.options
|| [] );
9084 // Have to repeat this from parent, as we need options to be set up for this to make sense
9085 this.setValue( config
.value
);
9090 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
9092 /* Static Properties */
9098 OO
.ui
.CheckboxMultiselectInputWidget
.static.supportsSimpleLabel
= false;
9100 /* Static Methods */
9105 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9106 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9107 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9108 .toArray().map( function ( el
) { return el
.value
; } );
9115 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9116 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9117 // Cannot reuse the `<input type=checkbox>` set
9118 delete config
.$input
;
9128 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
9130 return $( '<div>' );
9136 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
9137 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9138 .toArray().map( function ( el
) { return el
.value
; } );
9139 if ( this.value
!== value
) {
9140 this.setValue( value
);
9148 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
9149 value
= this.cleanUpValue( value
);
9150 this.checkboxMultiselectWidget
.selectItemsByData( value
);
9151 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9156 * Clean up incoming value.
9158 * @param {string[]} value Original value
9159 * @return {string[]} Cleaned up value
9161 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
9164 if ( !Array
.isArray( value
) ) {
9167 for ( i
= 0; i
< value
.length
; i
++ ) {
9169 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
9170 // Remove options that we don't have here
9171 if ( !this.checkboxMultiselectWidget
.getItemFromData( singleValue
) ) {
9174 cleanValue
.push( singleValue
);
9182 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
9183 this.checkboxMultiselectWidget
.setDisabled( state
);
9184 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9189 * Set the options available for this input.
9191 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
9194 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
9197 // Rebuild the checkboxMultiselectWidget menu
9198 this.checkboxMultiselectWidget
9200 .addItems( options
.map( function ( opt
) {
9201 var optValue
, item
, optDisabled
;
9203 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
9204 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
9205 item
= new OO
.ui
.CheckboxMultioptionWidget( {
9207 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
9208 disabled
: optDisabled
9210 // Set the 'name' and 'value' for form submission
9211 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
9212 item
.checkbox
.setValue( optValue
);
9216 // Re-set the value, checking the checkboxes as needed.
9217 // This will also get rid of any stale options that we just removed.
9218 this.setValue( this.getValue() );
9224 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
9225 * size of the field as well as its presentation. In addition, these widgets can be configured
9226 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
9227 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
9228 * which modifies incoming values rather than validating them.
9229 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9231 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9234 * // Example of a text input widget
9235 * var textInput = new OO.ui.TextInputWidget( {
9236 * value: 'Text input'
9238 * $( 'body' ).append( textInput.$element );
9240 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9243 * @extends OO.ui.InputWidget
9244 * @mixins OO.ui.mixin.IconElement
9245 * @mixins OO.ui.mixin.IndicatorElement
9246 * @mixins OO.ui.mixin.PendingElement
9247 * @mixins OO.ui.mixin.LabelElement
9250 * @param {Object} [config] Configuration options
9251 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
9252 * 'email', 'url' or 'number'. Ignored if `multiline` is true.
9254 * Some values of `type` result in additional behaviors:
9256 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
9257 * empties the text field
9258 * @cfg {string} [placeholder] Placeholder text
9259 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
9260 * instruct the browser to focus this widget.
9261 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
9262 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
9263 * @cfg {boolean} [multiline=false] Allow multiple lines of text
9264 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
9265 * specifies minimum number of rows to display.
9266 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
9267 * Use the #maxRows config to specify a maximum number of displayed rows.
9268 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
9269 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
9270 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
9271 * the value or placeholder text: `'before'` or `'after'`
9272 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
9273 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
9274 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
9275 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
9276 * (the value must contain only numbers); when RegExp, a regular expression that must match the
9277 * value for it to be considered valid; when Function, a function receiving the value as parameter
9278 * that must return true, or promise resolving to true, for it to be considered valid.
9280 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
9281 // Configuration initialization
9282 config
= $.extend( {
9284 labelPosition
: 'after'
9287 if ( config
.type
=== 'search' ) {
9288 OO
.ui
.warnDeprecation( 'TextInputWidget: config.type=\'search\' is deprecated. Use the SearchInputWidget instead. See T148471 for details.' );
9289 if ( config
.icon
=== undefined ) {
9290 config
.icon
= 'search';
9292 // indicator: 'clear' is set dynamically later, depending on value
9295 // Parent constructor
9296 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
9298 // Mixin constructors
9299 OO
.ui
.mixin
.IconElement
.call( this, config
);
9300 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9301 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
9302 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9305 this.type
= this.getSaneType( config
);
9306 this.readOnly
= false;
9307 this.required
= false;
9308 this.multiline
= !!config
.multiline
;
9309 this.autosize
= !!config
.autosize
;
9310 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
9311 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
9312 this.validate
= null;
9313 this.styleHeight
= null;
9314 this.scrollWidth
= null;
9316 // Clone for resizing
9317 if ( this.autosize
) {
9318 this.$clone
= this.$input
9320 .insertAfter( this.$input
)
9321 .attr( 'aria-hidden', 'true' )
9322 .addClass( 'oo-ui-element-hidden' );
9325 this.setValidation( config
.validate
);
9326 this.setLabelPosition( config
.labelPosition
);
9330 keypress
: this.onKeyPress
.bind( this ),
9331 blur
: this.onBlur
.bind( this ),
9332 focus
: this.onFocus
.bind( this )
9334 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
9335 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
9336 this.on( 'labelChange', this.updatePosition
.bind( this ) );
9337 this.connect( this, {
9339 disable
: 'onDisable'
9341 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
9345 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
9346 .append( this.$icon
, this.$indicator
);
9347 this.setReadOnly( !!config
.readOnly
);
9348 this.setRequired( !!config
.required
);
9349 this.updateSearchIndicator();
9350 if ( config
.placeholder
!== undefined ) {
9351 this.$input
.attr( 'placeholder', config
.placeholder
);
9353 if ( config
.maxLength
!== undefined ) {
9354 this.$input
.attr( 'maxlength', config
.maxLength
);
9356 if ( config
.autofocus
) {
9357 this.$input
.attr( 'autofocus', 'autofocus' );
9359 if ( config
.autocomplete
=== false ) {
9360 this.$input
.attr( 'autocomplete', 'off' );
9361 // Turning off autocompletion also disables "form caching" when the user navigates to a
9362 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
9364 beforeunload: function () {
9365 this.$input
.removeAttr( 'autocomplete' );
9367 pageshow: function () {
9368 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
9369 // whole page... it shouldn't hurt, though.
9370 this.$input
.attr( 'autocomplete', 'off' );
9374 if ( this.multiline
&& config
.rows
) {
9375 this.$input
.attr( 'rows', config
.rows
);
9377 if ( this.label
|| config
.autosize
) {
9378 this.isWaitingToBeAttached
= true;
9379 this.installParentChangeDetector();
9385 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
9386 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
9387 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9388 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
9389 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
9391 /* Static Properties */
9393 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
9398 /* Static Methods */
9403 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9404 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9405 if ( config
.multiline
) {
9406 state
.scrollTop
= config
.$input
.scrollTop();
9414 * An `enter` event is emitted when the user presses 'enter' inside the text box.
9416 * Not emitted if the input is multiline.
9422 * A `resize` event is emitted when autosize is set and the widget resizes
9430 * Handle icon mouse down events.
9433 * @param {jQuery.Event} e Mouse down event
9435 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
9436 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9437 this.$input
[ 0 ].focus();
9443 * Handle indicator mouse down events.
9446 * @param {jQuery.Event} e Mouse down event
9448 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
9449 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9450 if ( this.type
=== 'search' ) {
9451 // Clear the text field
9452 this.setValue( '' );
9454 this.$input
[ 0 ].focus();
9460 * Handle key press events.
9463 * @param {jQuery.Event} e Key press event
9464 * @fires enter If enter key is pressed and input is not multiline
9466 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
9467 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
9468 this.emit( 'enter', e
);
9473 * Handle blur events.
9476 * @param {jQuery.Event} e Blur event
9478 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
9479 this.setValidityFlag();
9483 * Handle focus events.
9486 * @param {jQuery.Event} e Focus event
9488 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
9489 if ( this.isWaitingToBeAttached
) {
9490 // If we've received focus, then we must be attached to the document, and if
9491 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
9492 this.onElementAttach();
9494 this.setValidityFlag( true );
9498 * Handle element attach events.
9501 * @param {jQuery.Event} e Element attach event
9503 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
9504 this.isWaitingToBeAttached
= false;
9505 // Any previously calculated size is now probably invalid if we reattached elsewhere
9506 this.valCache
= null;
9508 this.positionLabel();
9512 * Handle change events.
9514 * @param {string} value
9517 OO
.ui
.TextInputWidget
.prototype.onChange = function () {
9518 this.updateSearchIndicator();
9523 * Handle debounced change events.
9525 * @param {string} value
9528 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
9529 this.setValidityFlag();
9533 * Handle disable events.
9535 * @param {boolean} disabled Element is disabled
9538 OO
.ui
.TextInputWidget
.prototype.onDisable = function () {
9539 this.updateSearchIndicator();
9543 * Check if the input is {@link #readOnly read-only}.
9547 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
9548 return this.readOnly
;
9552 * Set the {@link #readOnly read-only} state of the input.
9554 * @param {boolean} state Make input read-only
9557 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
9558 this.readOnly
= !!state
;
9559 this.$input
.prop( 'readOnly', this.readOnly
);
9560 this.updateSearchIndicator();
9565 * Check if the input is {@link #required required}.
9569 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
9570 return this.required
;
9574 * Set the {@link #required required} state of the input.
9576 * @param {boolean} state Make input required
9579 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
9580 this.required
= !!state
;
9581 if ( this.required
) {
9583 .attr( 'required', 'required' )
9584 .attr( 'aria-required', 'true' );
9585 if ( this.getIndicator() === null ) {
9586 this.setIndicator( 'required' );
9590 .removeAttr( 'required' )
9591 .removeAttr( 'aria-required' );
9592 if ( this.getIndicator() === 'required' ) {
9593 this.setIndicator( null );
9596 this.updateSearchIndicator();
9601 * Support function for making #onElementAttach work across browsers.
9603 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
9604 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
9606 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
9607 * first time that the element gets attached to the documented.
9609 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
9610 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
9611 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
9614 if ( MutationObserver
) {
9615 // The new way. If only it wasn't so ugly.
9617 if ( this.isElementAttached() ) {
9618 // Widget is attached already, do nothing. This breaks the functionality of this function when
9619 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
9620 // would require observation of the whole document, which would hurt performance of other,
9621 // more important code.
9625 // Find topmost node in the tree
9626 topmostNode
= this.$element
[ 0 ];
9627 while ( topmostNode
.parentNode
) {
9628 topmostNode
= topmostNode
.parentNode
;
9631 // We have no way to detect the $element being attached somewhere without observing the entire
9632 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
9633 // parent node of $element, and instead detect when $element is removed from it (and thus
9634 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
9635 // doesn't get attached, we end up back here and create the parent.
9637 mutationObserver
= new MutationObserver( function ( mutations
) {
9638 var i
, j
, removedNodes
;
9639 for ( i
= 0; i
< mutations
.length
; i
++ ) {
9640 removedNodes
= mutations
[ i
].removedNodes
;
9641 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
9642 if ( removedNodes
[ j
] === topmostNode
) {
9643 setTimeout( onRemove
, 0 );
9650 onRemove = function () {
9651 // If the node was attached somewhere else, report it
9652 if ( widget
.isElementAttached() ) {
9653 widget
.onElementAttach();
9655 mutationObserver
.disconnect();
9656 widget
.installParentChangeDetector();
9659 // Create a fake parent and observe it
9660 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
9661 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
9663 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
9664 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
9665 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
9670 * Automatically adjust the size of the text input.
9672 * This only affects #multiline inputs that are {@link #autosize autosized}.
9677 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
9678 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
9679 idealHeight
, newHeight
, scrollWidth
, property
;
9681 if ( this.isWaitingToBeAttached
) {
9682 // #onElementAttach will be called soon, which calls this method
9686 if ( this.multiline
&& this.$input
.val() !== this.valCache
) {
9687 if ( this.autosize
) {
9689 .val( this.$input
.val() )
9690 .attr( 'rows', this.minRows
)
9691 // Set inline height property to 0 to measure scroll height
9692 .css( 'height', 0 );
9694 this.$clone
.removeClass( 'oo-ui-element-hidden' );
9696 this.valCache
= this.$input
.val();
9698 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
9700 // Remove inline height property to measure natural heights
9701 this.$clone
.css( 'height', '' );
9702 innerHeight
= this.$clone
.innerHeight();
9703 outerHeight
= this.$clone
.outerHeight();
9705 // Measure max rows height
9707 .attr( 'rows', this.maxRows
)
9708 .css( 'height', 'auto' )
9710 maxInnerHeight
= this.$clone
.innerHeight();
9712 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
9713 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
9714 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
9715 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
9717 this.$clone
.addClass( 'oo-ui-element-hidden' );
9719 // Only apply inline height when expansion beyond natural height is needed
9720 // Use the difference between the inner and outer height as a buffer
9721 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
9722 if ( newHeight
!== this.styleHeight
) {
9723 this.$input
.css( 'height', newHeight
);
9724 this.styleHeight
= newHeight
;
9725 this.emit( 'resize' );
9728 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
9729 if ( scrollWidth
!== this.scrollWidth
) {
9730 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
9732 this.$label
.css( { right
: '', left
: '' } );
9733 this.$indicator
.css( { right
: '', left
: '' } );
9735 if ( scrollWidth
) {
9736 this.$indicator
.css( property
, scrollWidth
);
9737 if ( this.labelPosition
=== 'after' ) {
9738 this.$label
.css( property
, scrollWidth
);
9742 this.scrollWidth
= scrollWidth
;
9743 this.positionLabel();
9753 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
9754 if ( config
.multiline
) {
9755 return $( '<textarea>' );
9756 } else if ( this.getSaneType( config
) === 'number' ) {
9757 return $( '<input>' )
9758 .attr( 'step', 'any' )
9759 .attr( 'type', 'number' );
9761 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
9766 * Get sanitized value for 'type' for given config.
9768 * @param {Object} config Configuration options
9769 * @return {string|null}
9772 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
9773 var allowedTypes
= [
9781 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
9785 * Check if the input supports multiple lines.
9789 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
9790 return !!this.multiline
;
9794 * Check if the input automatically adjusts its size.
9798 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
9799 return !!this.autosize
;
9803 * Focus the input and select a specified range within the text.
9805 * @param {number} from Select from offset
9806 * @param {number} [to] Select to offset, defaults to from
9809 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
9810 var isBackwards
, start
, end
,
9811 input
= this.$input
[ 0 ];
9815 isBackwards
= to
< from;
9816 start
= isBackwards
? to
: from;
9817 end
= isBackwards
? from : to
;
9822 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
9824 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
9825 // Rather than expensively check if the input is attached every time, just check
9826 // if it was the cause of an error being thrown. If not, rethrow the error.
9827 if ( this.getElementDocument().body
.contains( input
) ) {
9835 * Get an object describing the current selection range in a directional manner
9837 * @return {Object} Object containing 'from' and 'to' offsets
9839 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
9840 var input
= this.$input
[ 0 ],
9841 start
= input
.selectionStart
,
9842 end
= input
.selectionEnd
,
9843 isBackwards
= input
.selectionDirection
=== 'backward';
9846 from: isBackwards
? end
: start
,
9847 to
: isBackwards
? start
: end
9852 * Get the length of the text input value.
9854 * This could differ from the length of #getValue if the
9855 * value gets filtered
9857 * @return {number} Input length
9859 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
9860 return this.$input
[ 0 ].value
.length
;
9864 * Focus the input and select the entire text.
9868 OO
.ui
.TextInputWidget
.prototype.select = function () {
9869 return this.selectRange( 0, this.getInputLength() );
9873 * Focus the input and move the cursor to the start.
9877 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
9878 return this.selectRange( 0 );
9882 * Focus the input and move the cursor to the end.
9886 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
9887 return this.selectRange( this.getInputLength() );
9891 * Insert new content into the input.
9893 * @param {string} content Content to be inserted
9896 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
9898 range
= this.getRange(),
9899 value
= this.getValue();
9901 start
= Math
.min( range
.from, range
.to
);
9902 end
= Math
.max( range
.from, range
.to
);
9904 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
9905 this.selectRange( start
+ content
.length
);
9910 * Insert new content either side of a selection.
9912 * @param {string} pre Content to be inserted before the selection
9913 * @param {string} post Content to be inserted after the selection
9916 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
9918 range
= this.getRange(),
9919 offset
= pre
.length
;
9921 start
= Math
.min( range
.from, range
.to
);
9922 end
= Math
.max( range
.from, range
.to
);
9924 this.selectRange( start
).insertContent( pre
);
9925 this.selectRange( offset
+ end
).insertContent( post
);
9927 this.selectRange( offset
+ start
, offset
+ end
);
9932 * Set the validation pattern.
9934 * The validation pattern is either a regular expression, a function, or the symbolic name of a
9935 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
9936 * value must contain only numbers).
9938 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
9939 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
9941 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
9942 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
9943 this.validate
= validate
;
9945 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
9950 * Sets the 'invalid' flag appropriately.
9952 * @param {boolean} [isValid] Optionally override validation result
9954 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
9956 setFlag = function ( valid
) {
9958 widget
.$input
.attr( 'aria-invalid', 'true' );
9960 widget
.$input
.removeAttr( 'aria-invalid' );
9962 widget
.setFlags( { invalid
: !valid
} );
9965 if ( isValid
!== undefined ) {
9968 this.getValidity().then( function () {
9977 * Get the validity of current value.
9979 * This method returns a promise that resolves if the value is valid and rejects if
9980 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
9982 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
9984 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
9987 function rejectOrResolve( valid
) {
9989 return $.Deferred().resolve().promise();
9991 return $.Deferred().reject().promise();
9995 // Check browser validity and reject if it is invalid
9997 this.$input
[ 0 ].checkValidity
!== undefined &&
9998 this.$input
[ 0 ].checkValidity() === false
10000 return rejectOrResolve( false );
10003 // Run our checks if the browser thinks the field is valid
10004 if ( this.validate
instanceof Function
) {
10005 result
= this.validate( this.getValue() );
10006 if ( result
&& $.isFunction( result
.promise
) ) {
10007 return result
.promise().then( function ( valid
) {
10008 return rejectOrResolve( valid
);
10011 return rejectOrResolve( result
);
10014 return rejectOrResolve( this.getValue().match( this.validate
) );
10019 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10021 * @param {string} labelPosition Label position, 'before' or 'after'
10024 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10025 this.labelPosition
= labelPosition
;
10026 if ( this.label
) {
10027 // If there is no label and we only change the position, #updatePosition is a no-op,
10028 // but it takes really a lot of work to do nothing.
10029 this.updatePosition();
10035 * Update the position of the inline label.
10037 * This method is called by #setLabelPosition, and can also be called on its own if
10038 * something causes the label to be mispositioned.
10042 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10043 var after
= this.labelPosition
=== 'after';
10046 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10047 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10049 this.valCache
= null;
10050 this.scrollWidth
= null;
10052 this.positionLabel();
10058 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
10059 * already empty or when it's not editable.
10061 OO
.ui
.TextInputWidget
.prototype.updateSearchIndicator = function () {
10062 if ( this.type
=== 'search' ) {
10063 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
10064 this.setIndicator( null );
10066 this.setIndicator( 'clear' );
10072 * Position the label by setting the correct padding on the input.
10077 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
10078 var after
, rtl
, property
;
10080 if ( this.isWaitingToBeAttached
) {
10081 // #onElementAttach will be called soon, which calls this method
10085 // Clear old values
10087 // Clear old values if present
10089 'padding-right': '',
10093 if ( this.label
) {
10094 this.$element
.append( this.$label
);
10096 this.$label
.detach();
10100 after
= this.labelPosition
=== 'after';
10101 rtl
= this.$element
.css( 'direction' ) === 'rtl';
10102 property
= after
=== rtl
? 'padding-left' : 'padding-right';
10104 this.$input
.css( property
, this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 ) );
10112 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10113 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10114 if ( state
.scrollTop
!== undefined ) {
10115 this.$input
.scrollTop( state
.scrollTop
);
10121 * @extends OO.ui.TextInputWidget
10124 * @param {Object} [config] Configuration options
10126 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
10127 config
= $.extend( {
10131 // Set type to text so that TextInputWidget doesn't
10132 // get stuck in an infinite loop.
10133 config
.type
= 'text';
10135 // Parent constructor
10136 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
10139 this.$element
.addClass( 'oo-ui-textInputWidget-type-search' );
10140 this.updateSearchIndicator();
10141 this.connect( this, {
10142 disable
: 'onDisable'
10148 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
10156 OO
.ui
.SearchInputWidget
.prototype.getInputElement = function () {
10157 return $( '<input>' ).attr( 'type', 'search' );
10163 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10164 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10165 // Clear the text field
10166 this.setValue( '' );
10167 this.$input
[ 0 ].focus();
10173 * Update the 'clear' indicator displayed on type: 'search' text
10174 * fields, hiding it when the field is already empty or when it's not
10177 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
10178 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
10179 this.setIndicator( null );
10181 this.setIndicator( 'clear' );
10188 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
10189 OO
.ui
.SearchInputWidget
.parent
.prototype.onChange
.call( this );
10190 this.updateSearchIndicator();
10194 * Handle disable events.
10196 * @param {boolean} disabled Element is disabled
10199 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
10200 this.updateSearchIndicator();
10206 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
10207 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
10208 this.updateSearchIndicator();
10213 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
10214 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
10215 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
10217 * - by typing a value in the text input field. If the value exactly matches the value of a menu
10218 * option, that option will appear to be selected.
10219 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
10222 * After the user chooses an option, its `data` will be used as a new value for the widget.
10223 * A `label` also can be specified for each option: if given, it will be shown instead of the
10224 * `data` in the dropdown menu.
10226 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10228 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
10231 * // Example: A ComboBoxInputWidget.
10232 * var comboBox = new OO.ui.ComboBoxInputWidget( {
10233 * value: 'Option 1',
10235 * { data: 'Option 1' },
10236 * { data: 'Option 2' },
10237 * { data: 'Option 3' }
10240 * $( 'body' ).append( comboBox.$element );
10243 * // Example: A ComboBoxInputWidget with additional option labels.
10244 * var comboBox = new OO.ui.ComboBoxInputWidget( {
10245 * value: 'Option 1',
10248 * data: 'Option 1',
10249 * label: 'Option One'
10252 * data: 'Option 2',
10253 * label: 'Option Two'
10256 * data: 'Option 3',
10257 * label: 'Option Three'
10261 * $( 'body' ).append( comboBox.$element );
10263 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10266 * @extends OO.ui.TextInputWidget
10269 * @param {Object} [config] Configuration options
10270 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10271 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
10272 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
10273 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
10274 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
10276 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
10277 // Configuration initialization
10278 config
= $.extend( {
10279 autocomplete
: false
10282 // ComboBoxInputWidget shouldn't support multiline
10283 config
.multiline
= false;
10285 // Parent constructor
10286 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
10289 this.$overlay
= config
.$overlay
|| this.$element
;
10290 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
10291 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
10293 disabled
: this.disabled
10295 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend(
10299 $container
: this.$element
,
10300 disabled
: this.isDisabled()
10306 this.connect( this, {
10307 change
: 'onInputChange',
10308 enter
: 'onInputEnter'
10310 this.dropdownButton
.connect( this, {
10311 click
: 'onDropdownButtonClick'
10313 this.menu
.connect( this, {
10314 choose
: 'onMenuChoose',
10315 add
: 'onMenuItemsChange',
10316 remove
: 'onMenuItemsChange'
10320 this.$input
.attr( {
10322 'aria-autocomplete': 'list'
10324 // Do not override options set via config.menu.items
10325 if ( config
.options
!== undefined ) {
10326 this.setOptions( config
.options
);
10328 this.$field
= $( '<div>' )
10329 .addClass( 'oo-ui-comboBoxInputWidget-field' )
10330 .append( this.$input
, this.dropdownButton
.$element
);
10332 .addClass( 'oo-ui-comboBoxInputWidget' )
10333 .append( this.$field
);
10334 this.$overlay
.append( this.menu
.$element
);
10335 this.onMenuItemsChange();
10340 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
10345 * Get the combobox's menu.
10347 * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
10349 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
10354 * Get the combobox's text input widget.
10356 * @return {OO.ui.TextInputWidget} Text input widget
10358 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
10363 * Handle input change events.
10366 * @param {string} value New value
10368 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
10369 var match
= this.menu
.getItemFromData( value
);
10371 this.menu
.selectItem( match
);
10372 if ( this.menu
.getHighlightedItem() ) {
10373 this.menu
.highlightItem( match
);
10376 if ( !this.isDisabled() ) {
10377 this.menu
.toggle( true );
10382 * Handle input enter events.
10386 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
10387 if ( !this.isDisabled() ) {
10388 this.menu
.toggle( false );
10393 * Handle button click events.
10397 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
10398 this.menu
.toggle();
10399 this.$input
[ 0 ].focus();
10403 * Handle menu choose events.
10406 * @param {OO.ui.OptionWidget} item Chosen item
10408 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
10409 this.setValue( item
.getData() );
10413 * Handle menu item change events.
10417 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
10418 var match
= this.menu
.getItemFromData( this.getValue() );
10419 this.menu
.selectItem( match
);
10420 if ( this.menu
.getHighlightedItem() ) {
10421 this.menu
.highlightItem( match
);
10423 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
10429 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
10431 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
10433 if ( this.dropdownButton
) {
10434 this.dropdownButton
.setDisabled( this.isDisabled() );
10437 this.menu
.setDisabled( this.isDisabled() );
10444 * Set the options available for this input.
10446 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10449 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
10452 .addItems( options
.map( function ( opt
) {
10453 return new OO
.ui
.MenuOptionWidget( {
10455 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
10463 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
10464 * which is a widget that is specified by reference before any optional configuration settings.
10466 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
10468 * - **left**: The label is placed before the field-widget and aligned with the left margin.
10469 * A left-alignment is used for forms with many fields.
10470 * - **right**: The label is placed before the field-widget and aligned to the right margin.
10471 * A right-alignment is used for long but familiar forms which users tab through,
10472 * verifying the current field with a quick glance at the label.
10473 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
10474 * that users fill out from top to bottom.
10475 * - **inline**: The label is placed after the field-widget and aligned to the left.
10476 * An inline-alignment is best used with checkboxes or radio buttons.
10478 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
10479 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
10481 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
10484 * @extends OO.ui.Layout
10485 * @mixins OO.ui.mixin.LabelElement
10486 * @mixins OO.ui.mixin.TitledElement
10489 * @param {OO.ui.Widget} fieldWidget Field widget
10490 * @param {Object} [config] Configuration options
10491 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
10492 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
10493 * The array may contain strings or OO.ui.HtmlSnippet instances.
10494 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
10495 * The array may contain strings or OO.ui.HtmlSnippet instances.
10496 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
10497 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
10498 * For important messages, you are advised to use `notices`, as they are always shown.
10499 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
10501 * @throws {Error} An error is thrown if no widget is specified
10503 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
10504 // Allow passing positional parameters inside the config object
10505 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
10506 config
= fieldWidget
;
10507 fieldWidget
= config
.fieldWidget
;
10510 // Make sure we have required constructor arguments
10511 if ( fieldWidget
=== undefined ) {
10512 throw new Error( 'Widget not found' );
10515 // Configuration initialization
10516 config
= $.extend( { align
: 'left' }, config
);
10518 // Parent constructor
10519 OO
.ui
.FieldLayout
.parent
.call( this, config
);
10521 // Mixin constructors
10522 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
10523 $label
: $( '<label>' )
10525 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
10528 this.fieldWidget
= fieldWidget
;
10531 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
10532 this.$messages
= $( '<ul>' );
10533 this.$header
= $( '<span>' );
10534 this.$body
= $( '<div>' );
10536 if ( config
.help
) {
10537 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
10538 $overlay
: config
.$overlay
,
10542 classes
: [ 'oo-ui-fieldLayout-help' ],
10546 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
10547 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
10549 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
10551 this.$help
= this.popupButtonWidget
.$element
;
10553 this.$help
= $( [] );
10557 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
10560 if ( fieldWidget
.constructor.static.supportsSimpleLabel
) {
10561 if ( this.fieldWidget
.getInputId() ) {
10562 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
10564 this.$label
.on( 'click', function () {
10565 this.fieldWidget
.focus();
10571 .addClass( 'oo-ui-fieldLayout' )
10572 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
10573 .append( this.$body
);
10574 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
10575 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
10576 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
10578 .addClass( 'oo-ui-fieldLayout-field' )
10579 .append( this.fieldWidget
.$element
);
10581 this.setErrors( config
.errors
|| [] );
10582 this.setNotices( config
.notices
|| [] );
10583 this.setAlignment( config
.align
);
10588 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
10589 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
10590 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
10595 * Handle field disable events.
10598 * @param {boolean} value Field is disabled
10600 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
10601 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
10605 * Get the widget contained by the field.
10607 * @return {OO.ui.Widget} Field widget
10609 OO
.ui
.FieldLayout
.prototype.getField = function () {
10610 return this.fieldWidget
;
10614 * Return `true` if the given field widget can be used with `'inline'` alignment (see
10615 * #setAlignment). Return `false` if it can't or if this can't be determined.
10617 * @return {boolean}
10619 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
10620 // This is very simplistic, but should be good enough.
10621 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
10626 * @param {string} kind 'error' or 'notice'
10627 * @param {string|OO.ui.HtmlSnippet} text
10630 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
10631 var $listItem
, $icon
, message
;
10632 $listItem
= $( '<li>' );
10633 if ( kind
=== 'error' ) {
10634 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
10635 } else if ( kind
=== 'notice' ) {
10636 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
10640 message
= new OO
.ui
.LabelWidget( { label
: text
} );
10642 .append( $icon
, message
.$element
)
10643 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
10648 * Set the field alignment mode.
10651 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
10654 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
10655 if ( value
!== this.align
) {
10656 // Default to 'left'
10657 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
10661 if ( value
=== 'inline' && !this.isFieldInline() ) {
10664 // Reorder elements
10665 if ( value
=== 'top' ) {
10666 this.$header
.append( this.$label
, this.$help
);
10667 this.$body
.append( this.$header
, this.$field
);
10668 } else if ( value
=== 'inline' ) {
10669 this.$header
.append( this.$label
, this.$help
);
10670 this.$body
.append( this.$field
, this.$header
);
10672 this.$header
.append( this.$label
);
10673 this.$body
.append( this.$header
, this.$help
, this.$field
);
10675 // Set classes. The following classes can be used here:
10676 // * oo-ui-fieldLayout-align-left
10677 // * oo-ui-fieldLayout-align-right
10678 // * oo-ui-fieldLayout-align-top
10679 // * oo-ui-fieldLayout-align-inline
10680 if ( this.align
) {
10681 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
10683 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
10684 this.align
= value
;
10691 * Set the list of error messages.
10693 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
10694 * The array may contain strings or OO.ui.HtmlSnippet instances.
10697 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
10698 this.errors
= errors
.slice();
10699 this.updateMessages();
10704 * Set the list of notice messages.
10706 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
10707 * The array may contain strings or OO.ui.HtmlSnippet instances.
10710 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
10711 this.notices
= notices
.slice();
10712 this.updateMessages();
10717 * Update the rendering of error and notice messages.
10721 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
10723 this.$messages
.empty();
10725 if ( this.errors
.length
|| this.notices
.length
) {
10726 this.$body
.after( this.$messages
);
10728 this.$messages
.remove();
10732 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
10733 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
10735 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
10736 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
10741 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
10742 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
10743 * is required and is specified before any optional configuration settings.
10745 * Labels can be aligned in one of four ways:
10747 * - **left**: The label is placed before the field-widget and aligned with the left margin.
10748 * A left-alignment is used for forms with many fields.
10749 * - **right**: The label is placed before the field-widget and aligned to the right margin.
10750 * A right-alignment is used for long but familiar forms which users tab through,
10751 * verifying the current field with a quick glance at the label.
10752 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
10753 * that users fill out from top to bottom.
10754 * - **inline**: The label is placed after the field-widget and aligned to the left.
10755 * An inline-alignment is best used with checkboxes or radio buttons.
10757 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
10758 * text is specified.
10761 * // Example of an ActionFieldLayout
10762 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
10763 * new OO.ui.TextInputWidget( {
10764 * placeholder: 'Field widget'
10766 * new OO.ui.ButtonWidget( {
10770 * label: 'An ActionFieldLayout. This label is aligned top',
10772 * help: 'This is help text'
10776 * $( 'body' ).append( actionFieldLayout.$element );
10779 * @extends OO.ui.FieldLayout
10782 * @param {OO.ui.Widget} fieldWidget Field widget
10783 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
10784 * @param {Object} config
10786 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
10787 // Allow passing positional parameters inside the config object
10788 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
10789 config
= fieldWidget
;
10790 fieldWidget
= config
.fieldWidget
;
10791 buttonWidget
= config
.buttonWidget
;
10794 // Parent constructor
10795 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
10798 this.buttonWidget
= buttonWidget
;
10799 this.$button
= $( '<span>' );
10800 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
10804 .addClass( 'oo-ui-actionFieldLayout' );
10806 .addClass( 'oo-ui-actionFieldLayout-button' )
10807 .append( this.buttonWidget
.$element
);
10809 .addClass( 'oo-ui-actionFieldLayout-input' )
10810 .append( this.fieldWidget
.$element
);
10812 .append( this.$input
, this.$button
);
10817 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
10820 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
10821 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
10822 * configured with a label as well. For more information and examples,
10823 * please see the [OOjs UI documentation on MediaWiki][1].
10826 * // Example of a fieldset layout
10827 * var input1 = new OO.ui.TextInputWidget( {
10828 * placeholder: 'A text input field'
10831 * var input2 = new OO.ui.TextInputWidget( {
10832 * placeholder: 'A text input field'
10835 * var fieldset = new OO.ui.FieldsetLayout( {
10836 * label: 'Example of a fieldset layout'
10839 * fieldset.addItems( [
10840 * new OO.ui.FieldLayout( input1, {
10841 * label: 'Field One'
10843 * new OO.ui.FieldLayout( input2, {
10844 * label: 'Field Two'
10847 * $( 'body' ).append( fieldset.$element );
10849 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
10852 * @extends OO.ui.Layout
10853 * @mixins OO.ui.mixin.IconElement
10854 * @mixins OO.ui.mixin.LabelElement
10855 * @mixins OO.ui.mixin.GroupElement
10858 * @param {Object} [config] Configuration options
10859 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
10860 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
10861 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
10862 * For important messages, you are advised to use `notices`, as they are always shown.
10863 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
10865 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
10866 // Configuration initialization
10867 config
= config
|| {};
10869 // Parent constructor
10870 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
10872 // Mixin constructors
10873 OO
.ui
.mixin
.IconElement
.call( this, config
);
10874 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: $( '<div>' ) } ) );
10875 OO
.ui
.mixin
.GroupElement
.call( this, config
);
10878 this.$header
= $( '<div>' );
10879 if ( config
.help
) {
10880 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
10881 $overlay
: config
.$overlay
,
10885 classes
: [ 'oo-ui-fieldsetLayout-help' ],
10889 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
10890 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
10892 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
10894 this.$help
= this.popupButtonWidget
.$element
;
10896 this.$help
= $( [] );
10901 .addClass( 'oo-ui-fieldsetLayout-header' )
10902 .append( this.$icon
, this.$label
, this.$help
);
10903 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
10905 .addClass( 'oo-ui-fieldsetLayout' )
10906 .prepend( this.$header
, this.$group
);
10907 if ( Array
.isArray( config
.items
) ) {
10908 this.addItems( config
.items
);
10914 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
10915 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
10916 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
10917 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
10919 /* Static Properties */
10925 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
10928 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
10929 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
10930 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
10931 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10933 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
10934 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
10935 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
10936 * some fancier controls. Some controls have both regular and InputWidget variants, for example
10937 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
10938 * often have simplified APIs to match the capabilities of HTML forms.
10939 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
10941 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
10942 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10945 * // Example of a form layout that wraps a fieldset layout
10946 * var input1 = new OO.ui.TextInputWidget( {
10947 * placeholder: 'Username'
10949 * var input2 = new OO.ui.TextInputWidget( {
10950 * placeholder: 'Password',
10953 * var submit = new OO.ui.ButtonInputWidget( {
10957 * var fieldset = new OO.ui.FieldsetLayout( {
10958 * label: 'A form layout'
10960 * fieldset.addItems( [
10961 * new OO.ui.FieldLayout( input1, {
10962 * label: 'Username',
10965 * new OO.ui.FieldLayout( input2, {
10966 * label: 'Password',
10969 * new OO.ui.FieldLayout( submit )
10971 * var form = new OO.ui.FormLayout( {
10972 * items: [ fieldset ],
10973 * action: '/api/formhandler',
10976 * $( 'body' ).append( form.$element );
10979 * @extends OO.ui.Layout
10980 * @mixins OO.ui.mixin.GroupElement
10983 * @param {Object} [config] Configuration options
10984 * @cfg {string} [method] HTML form `method` attribute
10985 * @cfg {string} [action] HTML form `action` attribute
10986 * @cfg {string} [enctype] HTML form `enctype` attribute
10987 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
10989 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
10992 // Configuration initialization
10993 config
= config
|| {};
10995 // Parent constructor
10996 OO
.ui
.FormLayout
.parent
.call( this, config
);
10998 // Mixin constructors
10999 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11002 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
11004 // Make sure the action is safe
11005 action
= config
.action
;
11006 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
11007 action
= './' + action
;
11012 .addClass( 'oo-ui-formLayout' )
11014 method
: config
.method
,
11016 enctype
: config
.enctype
11018 if ( Array
.isArray( config
.items
) ) {
11019 this.addItems( config
.items
);
11025 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
11026 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
11031 * A 'submit' event is emitted when the form is submitted.
11036 /* Static Properties */
11042 OO
.ui
.FormLayout
.static.tagName
= 'form';
11047 * Handle form submit events.
11050 * @param {jQuery.Event} e Submit event
11053 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
11054 if ( this.emit( 'submit' ) ) {
11060 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
11061 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
11064 * // Example of a panel layout
11065 * var panel = new OO.ui.PanelLayout( {
11069 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
11071 * $( 'body' ).append( panel.$element );
11074 * @extends OO.ui.Layout
11077 * @param {Object} [config] Configuration options
11078 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
11079 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
11080 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
11081 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
11083 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
11084 // Configuration initialization
11085 config
= $.extend( {
11092 // Parent constructor
11093 OO
.ui
.PanelLayout
.parent
.call( this, config
);
11096 this.$element
.addClass( 'oo-ui-panelLayout' );
11097 if ( config
.scrollable
) {
11098 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
11100 if ( config
.padded
) {
11101 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
11103 if ( config
.expanded
) {
11104 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
11106 if ( config
.framed
) {
11107 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
11113 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
11118 * Focus the panel layout
11120 * The default implementation just focuses the first focusable element in the panel
11122 OO
.ui
.PanelLayout
.prototype.focus = function () {
11123 OO
.ui
.findFocusable( this.$element
).focus();
11127 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
11128 * items), with small margins between them. Convenient when you need to put a number of block-level
11129 * widgets on a single line next to each other.
11131 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
11134 * // HorizontalLayout with a text input and a label
11135 * var layout = new OO.ui.HorizontalLayout( {
11137 * new OO.ui.LabelWidget( { label: 'Label' } ),
11138 * new OO.ui.TextInputWidget( { value: 'Text' } )
11141 * $( 'body' ).append( layout.$element );
11144 * @extends OO.ui.Layout
11145 * @mixins OO.ui.mixin.GroupElement
11148 * @param {Object} [config] Configuration options
11149 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
11151 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
11152 // Configuration initialization
11153 config
= config
|| {};
11155 // Parent constructor
11156 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
11158 // Mixin constructors
11159 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11162 this.$element
.addClass( 'oo-ui-horizontalLayout' );
11163 if ( Array
.isArray( config
.items
) ) {
11164 this.addItems( config
.items
);
11170 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
11171 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);