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-11-28T23:28:05Z
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 by :focusable in jQueryUI v1.11.4 - 2015-04-14
78 * @param {jQuery} $element Element to test
79 * @return {boolean} Element is focusable
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, or 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 Function to debounce
239 * @param {number} [wait=0] Wait period in milliseconds
240 * @param {boolean} [immediate] Trigger on leading edge
241 * @return {Function} Debounced function
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 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 Function to throttle
286 * @param {number} wait Throttle window length, in milliseconds
287 * @return {Function} Throttled function
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, in milliseconds since the Unix epoch
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 tooltip for the button that removes a tag item
366 'ooui-item-remove': 'Remove',
367 // Default label for the accept button of a confirmation dialog
368 'ooui-dialog-message-accept': 'OK',
369 // Default label for the reject button of a confirmation dialog
370 'ooui-dialog-message-reject': 'Cancel',
371 // Title for process dialog error description
372 'ooui-dialog-process-error': 'Something went wrong',
373 // Label for process dialog dismiss error button, visible when describing errors
374 'ooui-dialog-process-dismiss': 'Dismiss',
375 // Label for process dialog retry action button, visible when describing only recoverable errors
376 'ooui-dialog-process-retry': 'Try again',
377 // Label for process dialog retry action button, visible when describing only warnings
378 'ooui-dialog-process-continue': 'Continue',
379 // Label for the file selection widget's select file button
380 'ooui-selectfile-button-select': 'Select a file',
381 // Label for the file selection widget if file selection is not supported
382 'ooui-selectfile-not-supported': 'File selection is not supported',
383 // Label for the file selection widget when no file is currently selected
384 'ooui-selectfile-placeholder': 'No file is selected',
385 // Label for the file selection widget's drop target
386 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
390 * Get a localized message.
392 * After the message key, message parameters may optionally be passed. In the default implementation,
393 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
394 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
395 * they support unnamed, ordered message parameters.
397 * In environments that provide a localization system, this function should be overridden to
398 * return the message translated in the user's language. The default implementation always returns
399 * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
403 * var i, iLen, button,
404 * messagePath = 'oojs-ui/dist/i18n/',
405 * languages = [ $.i18n().locale, 'ur', 'en' ],
408 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
409 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
412 * $.i18n().load( languageMap ).done( function() {
413 * // Replace the built-in `msg` only once we've loaded the internationalization.
414 * // OOjs UI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
415 * // you put off creating any widgets until this promise is complete, no English
416 * // will be displayed.
417 * OO.ui.msg = $.i18n;
419 * // A button displaying "OK" in the default locale
420 * button = new OO.ui.ButtonWidget( {
421 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
424 * $( 'body' ).append( button.$element );
426 * // A button displaying "OK" in Urdu
427 * $.i18n().locale = 'ur';
428 * button = new OO.ui.ButtonWidget( {
429 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
432 * $( 'body' ).append( button.$element );
435 * @param {string} key Message key
436 * @param {...Mixed} [params] Message parameters
437 * @return {string} Translated message with parameters substituted
439 OO
.ui
.msg = function ( key
) {
440 var message
= messages
[ key
],
441 params
= Array
.prototype.slice
.call( arguments
, 1 );
442 if ( typeof message
=== 'string' ) {
443 // Perform $1 substitution
444 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
445 var i
= parseInt( n
, 10 );
446 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
449 // Return placeholder if message not found
450 message
= '[' + key
+ ']';
457 * Package a message and arguments for deferred resolution.
459 * Use this when you are statically specifying a message and the message may not yet be present.
461 * @param {string} key Message key
462 * @param {...Mixed} [params] Message parameters
463 * @return {Function} Function that returns the resolved message when executed
465 OO
.ui
.deferMsg = function () {
466 var args
= arguments
;
468 return OO
.ui
.msg
.apply( OO
.ui
, args
);
475 * If the message is a function it will be executed, otherwise it will pass through directly.
477 * @param {Function|string} msg Deferred message, or message text
478 * @return {string} Resolved message
480 OO
.ui
.resolveMsg = function ( msg
) {
481 if ( $.isFunction( msg
) ) {
488 * @param {string} url
491 OO
.ui
.isSafeUrl = function ( url
) {
492 // Keep this function in sync with php/Tag.php
493 var i
, protocolWhitelist
;
495 function stringStartsWith( haystack
, needle
) {
496 return haystack
.substr( 0, needle
.length
) === needle
;
499 protocolWhitelist
= [
500 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
501 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
502 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
509 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
510 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
515 // This matches '//' too
516 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
519 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
527 * Check if the user has a 'mobile' device.
529 * For our purposes this means the user is primarily using an
530 * on-screen keyboard, touch input instead of a mouse and may
531 * have a physically small display.
533 * It is left up to implementors to decide how to compute this
534 * so the default implementation always returns false.
536 * @return {boolean} Use is on a mobile device
538 OO
.ui
.isMobile = function () {
543 * Get the additional spacing that should be taken into account when displaying elements that are
544 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
545 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
547 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
548 * the extra spacing from that edge of viewport (in pixels)
550 OO
.ui
.getViewportSpacing = function () {
564 * Namespace for OOjs UI mixins.
566 * Mixins are named according to the type of object they are intended to
567 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
568 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
569 * is intended to be mixed in to an instance of OO.ui.Widget.
577 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
578 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
579 * connected to them and can't be interacted with.
585 * @param {Object} [config] Configuration options
586 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
587 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
589 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
590 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
591 * @cfg {string} [text] Text to insert
592 * @cfg {Array} [content] An array of content elements to append (after #text).
593 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
594 * Instances of OO.ui.Element will have their $element appended.
595 * @cfg {jQuery} [$content] Content elements to append (after #text).
596 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
597 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
598 * Data can also be specified with the #setData method.
600 OO
.ui
.Element
= function OoUiElement( config
) {
601 if ( OO
.ui
.isDemo
) {
602 this.initialConfig
= config
;
604 // Configuration initialization
605 config
= config
|| {};
609 this.elementId
= null;
611 this.data
= config
.data
;
612 this.$element
= config
.$element
||
613 $( document
.createElement( this.getTagName() ) );
614 this.elementGroup
= null;
617 if ( Array
.isArray( config
.classes
) ) {
618 this.$element
.addClass( config
.classes
.join( ' ' ) );
621 this.setElementId( config
.id
);
624 this.$element
.text( config
.text
);
626 if ( config
.content
) {
627 // The `content` property treats plain strings as text; use an
628 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
629 // appropriate $element appended.
630 this.$element
.append( config
.content
.map( function ( v
) {
631 if ( typeof v
=== 'string' ) {
632 // Escape string so it is properly represented in HTML.
633 return document
.createTextNode( v
);
634 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
637 } else if ( v
instanceof OO
.ui
.Element
) {
643 if ( config
.$content
) {
644 // The `$content` property treats plain strings as HTML.
645 this.$element
.append( config
.$content
);
651 OO
.initClass( OO
.ui
.Element
);
653 /* Static Properties */
656 * The name of the HTML tag used by the element.
658 * The static value may be ignored if the #getTagName method is overridden.
664 OO
.ui
.Element
.static.tagName
= 'div';
669 * Reconstitute a JavaScript object corresponding to a widget created
670 * by the PHP implementation.
672 * @param {string|HTMLElement|jQuery} idOrNode
673 * A DOM id (if a string) or node for the widget to infuse.
674 * @return {OO.ui.Element}
675 * The `OO.ui.Element` corresponding to this (infusable) document node.
676 * For `Tag` objects emitted on the HTML side (used occasionally for content)
677 * the value returned is a newly-created Element wrapping around the existing
680 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
681 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, false );
682 // Verify that the type matches up.
683 // FIXME: uncomment after T89721 is fixed, see T90929.
685 if ( !( obj instanceof this['class'] ) ) {
686 throw new Error( 'Infusion type mismatch!' );
693 * Implementation helper for `infuse`; skips the type check and has an
694 * extra property so that only the top-level invocation touches the DOM.
697 * @param {string|HTMLElement|jQuery} idOrNode
698 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
699 * when the top-level widget of this infusion is inserted into DOM,
700 * replacing the original node; or false for top-level invocation.
701 * @return {OO.ui.Element}
703 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, domPromise
) {
704 // look for a cached result of a previous infusion.
705 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
706 if ( typeof idOrNode
=== 'string' ) {
708 $elem
= $( document
.getElementById( id
) );
710 $elem
= $( idOrNode
);
711 id
= $elem
.attr( 'id' );
713 if ( !$elem
.length
) {
714 if ( typeof idOrNode
=== 'string' ) {
715 error
= 'Widget not found: ' + idOrNode
;
716 } else if ( idOrNode
&& idOrNode
.selector
) {
717 error
= 'Widget not found: ' + idOrNode
.selector
;
719 error
= 'Widget not found';
721 throw new Error( error
);
723 if ( $elem
[ 0 ].oouiInfused
) {
724 $elem
= $elem
[ 0 ].oouiInfused
;
726 data
= $elem
.data( 'ooui-infused' );
729 if ( data
=== true ) {
730 throw new Error( 'Circular dependency! ' + id
);
733 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
734 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
735 // restore dynamic state after the new element is re-inserted into DOM under infused parent
736 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
737 infusedChildren
= $elem
.data( 'ooui-infused-children' );
738 if ( infusedChildren
&& infusedChildren
.length
) {
739 infusedChildren
.forEach( function ( data
) {
740 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
741 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
747 data
= $elem
.attr( 'data-ooui' );
749 throw new Error( 'No infusion data found: ' + id
);
752 data
= JSON
.parse( data
);
756 if ( !( data
&& data
._
) ) {
757 throw new Error( 'No valid infusion data found: ' + id
);
759 if ( data
._
=== 'Tag' ) {
760 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
761 return new OO
.ui
.Element( { $element
: $elem
} );
763 parts
= data
._
.split( '.' );
764 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
765 if ( cls
=== undefined ) {
766 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
769 // Verify that we're creating an OO.ui.Element instance
772 while ( parent
!== undefined ) {
773 if ( parent
=== OO
.ui
.Element
) {
778 parent
= parent
.parent
;
781 if ( parent
!== OO
.ui
.Element
) {
782 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
785 if ( domPromise
=== false ) {
787 domPromise
= top
.promise();
789 $elem
.data( 'ooui-infused', true ); // prevent loops
790 data
.id
= id
; // implicit
791 infusedChildren
= [];
792 data
= OO
.copy( data
, null, function deserialize( value
) {
794 if ( OO
.isPlainObject( value
) ) {
796 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, domPromise
);
797 infusedChildren
.push( infused
);
798 // Flatten the structure
799 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
800 infused
.$element
.removeData( 'ooui-infused-children' );
803 if ( value
.html
!== undefined ) {
804 return new OO
.ui
.HtmlSnippet( value
.html
);
808 // allow widgets to reuse parts of the DOM
809 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
810 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
811 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
813 // eslint-disable-next-line new-cap
814 obj
= new cls( data
);
815 // now replace old DOM with this new DOM.
817 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
818 // so only mutate the DOM if we need to.
819 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
820 $elem
.replaceWith( obj
.$element
);
821 // This element is now gone from the DOM, but if anyone is holding a reference to it,
822 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
823 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
824 $elem
[ 0 ].oouiInfused
= obj
.$element
;
828 obj
.$element
.data( 'ooui-infused', obj
);
829 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
830 // set the 'data-ooui' attribute so we can identify infused widgets
831 obj
.$element
.attr( 'data-ooui', '' );
832 // restore dynamic state after the new element is inserted into DOM
833 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
838 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
840 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
841 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
842 * constructor, which will be given the enhanced config.
845 * @param {HTMLElement} node
846 * @param {Object} config
849 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
854 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
855 * (and its children) that represent an Element of the same class and the given configuration,
856 * generated by the PHP implementation.
858 * This method is called just before `node` is detached from the DOM. The return value of this
859 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
860 * is inserted into DOM to replace `node`.
863 * @param {HTMLElement} node
864 * @param {Object} config
867 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
872 * Get a jQuery function within a specific document.
875 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
876 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
878 * @return {Function} Bound jQuery function
880 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
881 function wrapper( selector
) {
882 return $( selector
, wrapper
.context
);
885 wrapper
.context
= this.getDocument( context
);
888 wrapper
.$iframe
= $iframe
;
895 * Get the document of an element.
898 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
899 * @return {HTMLDocument|null} Document object
901 OO
.ui
.Element
.static.getDocument = function ( obj
) {
902 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
903 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
904 // Empty jQuery selections might have a context
911 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
916 * Get the window of an element or document.
919 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
920 * @return {Window} Window object
922 OO
.ui
.Element
.static.getWindow = function ( obj
) {
923 var doc
= this.getDocument( obj
);
924 return doc
.defaultView
;
928 * Get the direction of an element or document.
931 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
932 * @return {string} Text direction, either 'ltr' or 'rtl'
934 OO
.ui
.Element
.static.getDir = function ( obj
) {
937 if ( obj
instanceof jQuery
) {
940 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
941 isWin
= obj
.document
!== undefined;
942 if ( isDoc
|| isWin
) {
948 return $( obj
).css( 'direction' );
952 * Get the offset between two frames.
954 * TODO: Make this function not use recursion.
957 * @param {Window} from Window of the child frame
958 * @param {Window} [to=window] Window of the parent frame
959 * @param {Object} [offset] Offset to start with, used internally
960 * @return {Object} Offset object, containing left and top properties
962 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
963 var i
, len
, frames
, frame
, rect
;
969 offset
= { top
: 0, left
: 0 };
971 if ( from.parent
=== from ) {
975 // Get iframe element
976 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
977 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
978 if ( frames
[ i
].contentWindow
=== from ) {
984 // Recursively accumulate offset values
986 rect
= frame
.getBoundingClientRect();
987 offset
.left
+= rect
.left
;
988 offset
.top
+= rect
.top
;
990 this.getFrameOffset( from.parent
, offset
);
997 * Get the offset between two elements.
999 * The two elements may be in a different frame, but in that case the frame $element is in must
1000 * be contained in the frame $anchor is in.
1003 * @param {jQuery} $element Element whose position to get
1004 * @param {jQuery} $anchor Element to get $element's position relative to
1005 * @return {Object} Translated position coordinates, containing top and left properties
1007 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1008 var iframe
, iframePos
,
1009 pos
= $element
.offset(),
1010 anchorPos
= $anchor
.offset(),
1011 elementDocument
= this.getDocument( $element
),
1012 anchorDocument
= this.getDocument( $anchor
);
1014 // If $element isn't in the same document as $anchor, traverse up
1015 while ( elementDocument
!== anchorDocument
) {
1016 iframe
= elementDocument
.defaultView
.frameElement
;
1018 throw new Error( '$element frame is not contained in $anchor frame' );
1020 iframePos
= $( iframe
).offset();
1021 pos
.left
+= iframePos
.left
;
1022 pos
.top
+= iframePos
.top
;
1023 elementDocument
= iframe
.ownerDocument
;
1025 pos
.left
-= anchorPos
.left
;
1026 pos
.top
-= anchorPos
.top
;
1031 * Get element border sizes.
1034 * @param {HTMLElement} el Element to measure
1035 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1037 OO
.ui
.Element
.static.getBorders = function ( el
) {
1038 var doc
= el
.ownerDocument
,
1039 win
= doc
.defaultView
,
1040 style
= win
.getComputedStyle( el
, null ),
1042 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1043 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1044 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1045 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1056 * Get dimensions of an element or window.
1059 * @param {HTMLElement|Window} el Element to measure
1060 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1062 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1064 doc
= el
.ownerDocument
|| el
.document
,
1065 win
= doc
.defaultView
;
1067 if ( win
=== el
|| el
=== doc
.documentElement
) {
1070 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1072 top
: $win
.scrollTop(),
1073 left
: $win
.scrollLeft()
1075 scrollbar
: { right
: 0, bottom
: 0 },
1079 bottom
: $win
.innerHeight(),
1080 right
: $win
.innerWidth()
1086 borders
: this.getBorders( el
),
1088 top
: $el
.scrollTop(),
1089 left
: $el
.scrollLeft()
1092 right
: $el
.innerWidth() - el
.clientWidth
,
1093 bottom
: $el
.innerHeight() - el
.clientHeight
1095 rect
: el
.getBoundingClientRect()
1101 * Get the number of pixels that an element's content is scrolled to the left.
1103 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1104 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1106 * This function smooths out browser inconsistencies (nicely described in the README at
1107 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1108 * with Firefox's 'scrollLeft', which seems the sanest.
1112 * @param {HTMLElement|Window} el Element to measure
1113 * @return {number} Scroll position from the left.
1114 * If the element's direction is LTR, this is a positive number between `0` (initial scroll position)
1115 * and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1116 * If the element's direction is RTL, this is a negative number between `0` (initial scroll position)
1117 * and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1119 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1120 var rtlScrollType
= null;
1123 var $definer
= $( '<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>' ),
1124 definer
= $definer
[ 0 ];
1126 $definer
.appendTo( 'body' );
1127 if ( definer
.scrollLeft
> 0 ) {
1129 rtlScrollType
= 'default';
1131 definer
.scrollLeft
= 1;
1132 if ( definer
.scrollLeft
=== 0 ) {
1133 // Firefox, old Opera
1134 rtlScrollType
= 'negative';
1136 // Internet Explorer, Edge
1137 rtlScrollType
= 'reverse';
1143 return function getScrollLeft( el
) {
1144 var isRoot
= el
.window
=== el
||
1145 el
=== el
.ownerDocument
.body
||
1146 el
=== el
.ownerDocument
.documentElement
,
1147 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1148 // All browsers use the correct scroll type ('negative') on the root, so don't
1149 // do any fixups when looking at the root element
1150 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1152 if ( direction
=== 'rtl' ) {
1153 if ( rtlScrollType
=== null ) {
1156 if ( rtlScrollType
=== 'reverse' ) {
1157 scrollLeft
= -scrollLeft
;
1158 } else if ( rtlScrollType
=== 'default' ) {
1159 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1168 * Get the root scrollable element of given element's document.
1170 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1171 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1172 * lets us use 'body' or 'documentElement' based on what is working.
1174 * https://code.google.com/p/chromium/issues/detail?id=303131
1177 * @param {HTMLElement} el Element to find root scrollable parent for
1178 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1179 * depending on browser
1181 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1182 var scrollTop
, body
;
1184 if ( OO
.ui
.scrollableElement
=== undefined ) {
1185 body
= el
.ownerDocument
.body
;
1186 scrollTop
= body
.scrollTop
;
1189 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1190 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1191 if ( Math
.round( body
.scrollTop
) === 1 ) {
1192 body
.scrollTop
= scrollTop
;
1193 OO
.ui
.scrollableElement
= 'body';
1195 OO
.ui
.scrollableElement
= 'documentElement';
1199 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1203 * Get closest scrollable container.
1205 * Traverses up until either a scrollable element or the root is reached, in which case the root
1206 * scrollable element will be returned (see #getRootScrollableElement).
1209 * @param {HTMLElement} el Element to find scrollable container for
1210 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1211 * @return {HTMLElement} Closest scrollable container
1213 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1215 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1216 // 'overflow-y' have different values, so we need to check the separate properties.
1217 props
= [ 'overflow-x', 'overflow-y' ],
1218 $parent
= $( el
).parent();
1220 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1221 props
= [ 'overflow-' + dimension
];
1224 // Special case for the document root (which doesn't really have any scrollable container, since
1225 // it is the ultimate scrollable container, but this is probably saner than null or exception)
1226 if ( $( el
).is( 'html, body' ) ) {
1227 return this.getRootScrollableElement( el
);
1230 while ( $parent
.length
) {
1231 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1232 return $parent
[ 0 ];
1236 val
= $parent
.css( props
[ i
] );
1237 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be
1238 // scrolled in that direction, but they can actually be scrolled programatically. The user can
1239 // unintentionally perform a scroll in such case even if the application doesn't scroll
1240 // programatically, e.g. when jumping to an anchor, or when using built-in find functionality.
1241 // This could cause funny issues...
1242 if ( val
=== 'auto' || val
=== 'scroll' ) {
1243 return $parent
[ 0 ];
1246 $parent
= $parent
.parent();
1248 // The element is unattached... return something mostly sane
1249 return this.getRootScrollableElement( el
);
1253 * Scroll element into view.
1256 * @param {HTMLElement} el Element to scroll into view
1257 * @param {Object} [config] Configuration options
1258 * @param {string} [config.duration='fast'] jQuery animation duration value
1259 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1260 * to scroll in both directions
1261 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1263 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1264 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1265 deferred
= $.Deferred();
1267 // Configuration initialization
1268 config
= config
|| {};
1271 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1272 $container
= $( container
);
1273 elementDimensions
= this.getDimensions( el
);
1274 containerDimensions
= this.getDimensions( container
);
1275 $window
= $( this.getWindow( el
) );
1277 // Compute the element's position relative to the container
1278 if ( $container
.is( 'html, body' ) ) {
1279 // If the scrollable container is the root, this is easy
1281 top
: elementDimensions
.rect
.top
,
1282 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1283 left
: elementDimensions
.rect
.left
,
1284 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1287 // Otherwise, we have to subtract el's coordinates from container's coordinates
1289 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1290 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1291 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1292 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1296 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1297 if ( position
.top
< 0 ) {
1298 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1299 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1300 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1303 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1304 if ( position
.left
< 0 ) {
1305 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1306 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1307 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1310 if ( !$.isEmptyObject( animations
) ) {
1311 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1312 $container
.queue( function ( next
) {
1319 return deferred
.promise();
1323 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1324 * and reserve space for them, because it probably doesn't.
1326 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1327 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1328 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1329 * and then reattach (or show) them back.
1332 * @param {HTMLElement} el Element to reconsider the scrollbars on
1334 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1335 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1336 // Save scroll position
1337 scrollLeft
= el
.scrollLeft
;
1338 scrollTop
= el
.scrollTop
;
1339 // Detach all children
1340 while ( el
.firstChild
) {
1341 nodes
.push( el
.firstChild
);
1342 el
.removeChild( el
.firstChild
);
1345 void el
.offsetHeight
;
1346 // Reattach all children
1347 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1348 el
.appendChild( nodes
[ i
] );
1350 // Restore scroll position (no-op if scrollbars disappeared)
1351 el
.scrollLeft
= scrollLeft
;
1352 el
.scrollTop
= scrollTop
;
1358 * Toggle visibility of an element.
1360 * @param {boolean} [show] Make element visible, omit to toggle visibility
1364 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1365 show
= show
=== undefined ? !this.visible
: !!show
;
1367 if ( show
!== this.isVisible() ) {
1368 this.visible
= show
;
1369 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1370 this.emit( 'toggle', show
);
1377 * Check if element is visible.
1379 * @return {boolean} element is visible
1381 OO
.ui
.Element
.prototype.isVisible = function () {
1382 return this.visible
;
1388 * @return {Mixed} Element data
1390 OO
.ui
.Element
.prototype.getData = function () {
1397 * @param {Mixed} data Element data
1400 OO
.ui
.Element
.prototype.setData = function ( data
) {
1406 * Set the element has an 'id' attribute.
1408 * @param {string} id
1411 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1412 this.elementId
= id
;
1413 this.$element
.attr( 'id', id
);
1418 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1419 * and return its value.
1423 OO
.ui
.Element
.prototype.getElementId = function () {
1424 if ( this.elementId
=== null ) {
1425 this.setElementId( OO
.ui
.generateElementId() );
1427 return this.elementId
;
1431 * Check if element supports one or more methods.
1433 * @param {string|string[]} methods Method or list of methods to check
1434 * @return {boolean} All methods are supported
1436 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1440 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1441 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1442 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1447 return methods
.length
=== support
;
1451 * Update the theme-provided classes.
1453 * @localdoc This is called in element mixins and widget classes any time state changes.
1454 * Updating is debounced, minimizing overhead of changing multiple attributes and
1455 * guaranteeing that theme updates do not occur within an element's constructor
1457 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1458 OO
.ui
.theme
.queueUpdateElementClasses( this );
1462 * Get the HTML tag name.
1464 * Override this method to base the result on instance information.
1466 * @return {string} HTML tag name
1468 OO
.ui
.Element
.prototype.getTagName = function () {
1469 return this.constructor.static.tagName
;
1473 * Check if the element is attached to the DOM
1475 * @return {boolean} The element is attached to the DOM
1477 OO
.ui
.Element
.prototype.isElementAttached = function () {
1478 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1482 * Get the DOM document.
1484 * @return {HTMLDocument} Document object
1486 OO
.ui
.Element
.prototype.getElementDocument = function () {
1487 // Don't cache this in other ways either because subclasses could can change this.$element
1488 return OO
.ui
.Element
.static.getDocument( this.$element
);
1492 * Get the DOM window.
1494 * @return {Window} Window object
1496 OO
.ui
.Element
.prototype.getElementWindow = function () {
1497 return OO
.ui
.Element
.static.getWindow( this.$element
);
1501 * Get closest scrollable container.
1503 * @return {HTMLElement} Closest scrollable container
1505 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1506 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1510 * Get group element is in.
1512 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1514 OO
.ui
.Element
.prototype.getElementGroup = function () {
1515 return this.elementGroup
;
1519 * Set group element is in.
1521 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1524 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1525 this.elementGroup
= group
;
1530 * Scroll element into view.
1532 * @param {Object} [config] Configuration options
1533 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1535 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1537 !this.isElementAttached() ||
1538 !this.isVisible() ||
1539 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1541 return $.Deferred().resolve();
1543 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1547 * Restore the pre-infusion dynamic state for this widget.
1549 * This method is called after #$element has been inserted into DOM. The parameter is the return
1550 * value of #gatherPreInfuseState.
1553 * @param {Object} state
1555 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1559 * Wraps an HTML snippet for use with configuration values which default
1560 * to strings. This bypasses the default html-escaping done to string
1566 * @param {string} [content] HTML content
1568 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1570 this.content
= content
;
1575 OO
.initClass( OO
.ui
.HtmlSnippet
);
1582 * @return {string} Unchanged HTML snippet.
1584 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1585 return this.content
;
1589 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1590 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1591 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1592 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1593 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1597 * @extends OO.ui.Element
1598 * @mixins OO.EventEmitter
1601 * @param {Object} [config] Configuration options
1603 OO
.ui
.Layout
= function OoUiLayout( config
) {
1604 // Configuration initialization
1605 config
= config
|| {};
1607 // Parent constructor
1608 OO
.ui
.Layout
.parent
.call( this, config
);
1610 // Mixin constructors
1611 OO
.EventEmitter
.call( this );
1614 this.$element
.addClass( 'oo-ui-layout' );
1619 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1620 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1623 * Widgets are compositions of one or more OOjs UI elements that users can both view
1624 * and interact with. All widgets can be configured and modified via a standard API,
1625 * and their state can change dynamically according to a model.
1629 * @extends OO.ui.Element
1630 * @mixins OO.EventEmitter
1633 * @param {Object} [config] Configuration options
1634 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1635 * appearance reflects this state.
1637 OO
.ui
.Widget
= function OoUiWidget( config
) {
1638 // Initialize config
1639 config
= $.extend( { disabled
: false }, config
);
1641 // Parent constructor
1642 OO
.ui
.Widget
.parent
.call( this, config
);
1644 // Mixin constructors
1645 OO
.EventEmitter
.call( this );
1648 this.disabled
= null;
1649 this.wasDisabled
= null;
1652 this.$element
.addClass( 'oo-ui-widget' );
1653 this.setDisabled( !!config
.disabled
);
1658 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1659 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1666 * A 'disable' event is emitted when the disabled state of the widget changes
1667 * (i.e. on disable **and** enable).
1669 * @param {boolean} disabled Widget is disabled
1675 * A 'toggle' event is emitted when the visibility of the widget changes.
1677 * @param {boolean} visible Widget is visible
1683 * Check if the widget is disabled.
1685 * @return {boolean} Widget is disabled
1687 OO
.ui
.Widget
.prototype.isDisabled = function () {
1688 return this.disabled
;
1692 * Set the 'disabled' state of the widget.
1694 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1696 * @param {boolean} disabled Disable widget
1699 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1702 this.disabled
= !!disabled
;
1703 isDisabled
= this.isDisabled();
1704 if ( isDisabled
!== this.wasDisabled
) {
1705 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1706 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1707 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1708 this.emit( 'disable', isDisabled
);
1709 this.updateThemeClasses();
1711 this.wasDisabled
= isDisabled
;
1717 * Update the disabled state, in case of changes in parent widget.
1721 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1722 this.setDisabled( this.disabled
);
1727 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1730 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1733 * @return {string|null} The ID of the labelable element
1735 OO
.ui
.Widget
.prototype.getInputId = function () {
1740 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1741 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1742 * override this method to provide intuitive, accessible behavior.
1744 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1745 * Individual widgets may override it too.
1747 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1750 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1761 OO
.ui
.Theme
= function OoUiTheme() {
1762 this.elementClassesQueue
= [];
1763 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1768 OO
.initClass( OO
.ui
.Theme
);
1773 * Get a list of classes to be applied to a widget.
1775 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1776 * otherwise state transitions will not work properly.
1778 * @param {OO.ui.Element} element Element for which to get classes
1779 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1781 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1782 return { on
: [], off
: [] };
1786 * Update CSS classes provided by the theme.
1788 * For elements with theme logic hooks, this should be called any time there's a state change.
1790 * @param {OO.ui.Element} element Element for which to update classes
1792 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1793 var $elements
= $( [] ),
1794 classes
= this.getElementClasses( element
);
1796 if ( element
.$icon
) {
1797 $elements
= $elements
.add( element
.$icon
);
1799 if ( element
.$indicator
) {
1800 $elements
= $elements
.add( element
.$indicator
);
1804 .removeClass( classes
.off
.join( ' ' ) )
1805 .addClass( classes
.on
.join( ' ' ) );
1811 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1813 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1814 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1817 this.elementClassesQueue
= [];
1821 * Queue #updateElementClasses to be called for this element.
1823 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1824 * to make them synchronous.
1826 * @param {OO.ui.Element} element Element for which to update classes
1828 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1829 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1830 // the most common case (this method is often called repeatedly for the same element).
1831 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1834 this.elementClassesQueue
.push( element
);
1835 this.debouncedUpdateQueuedElementClasses();
1839 * Get the transition duration in milliseconds for dialogs opening/closing
1841 * The dialog should be fully rendered this many milliseconds after the
1842 * ready process has executed.
1844 * @return {number} Transition duration in milliseconds
1846 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1851 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1852 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1853 * order in which users will navigate through the focusable elements via the "tab" key.
1856 * // TabIndexedElement is mixed into the ButtonWidget class
1857 * // to provide a tabIndex property.
1858 * var button1 = new OO.ui.ButtonWidget( {
1862 * var button2 = new OO.ui.ButtonWidget( {
1866 * var button3 = new OO.ui.ButtonWidget( {
1870 * var button4 = new OO.ui.ButtonWidget( {
1874 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1880 * @param {Object} [config] Configuration options
1881 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1882 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1883 * functionality will be applied to it instead.
1884 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1885 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1886 * to remove the element from the tab-navigation flow.
1888 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1889 // Configuration initialization
1890 config
= $.extend( { tabIndex
: 0 }, config
);
1893 this.$tabIndexed
= null;
1894 this.tabIndex
= null;
1897 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1900 this.setTabIndex( config
.tabIndex
);
1901 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1906 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1911 * Set the element that should use the tabindex functionality.
1913 * This method is used to retarget a tabindex mixin so that its functionality applies
1914 * to the specified element. If an element is currently using the functionality, the mixin’s
1915 * effect on that element is removed before the new element is set up.
1917 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1920 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1921 var tabIndex
= this.tabIndex
;
1922 // Remove attributes from old $tabIndexed
1923 this.setTabIndex( null );
1924 // Force update of new $tabIndexed
1925 this.$tabIndexed
= $tabIndexed
;
1926 this.tabIndex
= tabIndex
;
1927 return this.updateTabIndex();
1931 * Set the value of the tabindex.
1933 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
1936 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1937 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
1939 if ( this.tabIndex
!== tabIndex
) {
1940 this.tabIndex
= tabIndex
;
1941 this.updateTabIndex();
1948 * Update the `tabindex` attribute, in case of changes to tab index or
1954 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1955 if ( this.$tabIndexed
) {
1956 if ( this.tabIndex
!== null ) {
1957 // Do not index over disabled elements
1958 this.$tabIndexed
.attr( {
1959 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1960 // Support: ChromeVox and NVDA
1961 // These do not seem to inherit aria-disabled from parent elements
1962 'aria-disabled': this.isDisabled().toString()
1965 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1972 * Handle disable events.
1975 * @param {boolean} disabled Element is disabled
1977 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1978 this.updateTabIndex();
1982 * Get the value of the tabindex.
1984 * @return {number|null} Tabindex value
1986 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
1987 return this.tabIndex
;
1991 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
1993 * If the element already has an ID then that is returned, otherwise unique ID is
1994 * generated, set on the element, and returned.
1996 * @return {string|null} The ID of the focusable element
1998 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2001 if ( !this.$tabIndexed
) {
2004 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2008 id
= this.$tabIndexed
.attr( 'id' );
2009 if ( id
=== undefined ) {
2010 id
= OO
.ui
.generateElementId();
2011 this.$tabIndexed
.attr( 'id', id
);
2018 * Whether the node is 'labelable' according to the HTML spec
2019 * (i.e., whether it can be interacted with through a `<label for="…">`).
2020 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2023 * @param {jQuery} $node
2026 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2028 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2029 tagName
= $node
.prop( 'tagName' ).toLowerCase();
2031 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2034 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2041 * Focus this element.
2045 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2046 if ( !this.isDisabled() ) {
2047 this.$tabIndexed
.focus();
2053 * Blur this element.
2057 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2058 this.$tabIndexed
.blur();
2063 * @inheritdoc OO.ui.Widget
2065 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2070 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2071 * interface element that can be configured with access keys for accessibility.
2072 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
2074 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
2080 * @param {Object} [config] Configuration options
2081 * @cfg {jQuery} [$button] The button element created by the class.
2082 * If this configuration is omitted, the button element will use a generated `<a>`.
2083 * @cfg {boolean} [framed=true] Render the button with a frame
2085 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2086 // Configuration initialization
2087 config
= config
|| {};
2090 this.$button
= null;
2092 this.active
= config
.active
!== undefined && config
.active
;
2093 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
2094 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2095 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2096 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
2097 this.onClickHandler
= this.onClick
.bind( this );
2098 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2101 this.$element
.addClass( 'oo-ui-buttonElement' );
2102 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2103 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2108 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2110 /* Static Properties */
2113 * Cancel mouse down events.
2115 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
2116 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
2117 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
2122 * @property {boolean}
2124 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2129 * A 'click' event is emitted when the button element is clicked.
2137 * Set the button element.
2139 * This method is used to retarget a button mixin so that its functionality applies to
2140 * the specified button element instead of the one created by the class. If a button element
2141 * is already set, the method will remove the mixin’s effect on that element.
2143 * @param {jQuery} $button Element to use as button
2145 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2146 if ( this.$button
) {
2148 .removeClass( 'oo-ui-buttonElement-button' )
2149 .removeAttr( 'role accesskey' )
2151 mousedown
: this.onMouseDownHandler
,
2152 keydown
: this.onKeyDownHandler
,
2153 click
: this.onClickHandler
,
2154 keypress
: this.onKeyPressHandler
2158 this.$button
= $button
2159 .addClass( 'oo-ui-buttonElement-button' )
2161 mousedown
: this.onMouseDownHandler
,
2162 keydown
: this.onKeyDownHandler
,
2163 click
: this.onClickHandler
,
2164 keypress
: this.onKeyPressHandler
2167 // Add `role="button"` on `<a>` elements, where it's needed
2168 // `toUppercase()` is added for XHTML documents
2169 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2170 this.$button
.attr( 'role', 'button' );
2175 * Handles mouse down events.
2178 * @param {jQuery.Event} e Mouse down event
2180 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2181 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2184 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2185 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2186 // reliably remove the pressed class
2187 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
2188 // Prevent change of focus unless specifically configured otherwise
2189 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2195 * Handles mouse up events.
2198 * @param {MouseEvent} e Mouse up event
2200 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
2201 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2204 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2205 // Stop listening for mouseup, since we only needed this once
2206 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
2210 * Handles mouse click events.
2213 * @param {jQuery.Event} e Mouse click event
2216 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2217 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2218 if ( this.emit( 'click' ) ) {
2225 * Handles key down events.
2228 * @param {jQuery.Event} e Key down event
2230 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2231 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2234 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2235 // Run the keyup handler no matter where the key is when the button is let go, so we can
2236 // reliably remove the pressed class
2237 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
2241 * Handles key up events.
2244 * @param {KeyboardEvent} e Key up event
2246 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
2247 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2250 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2251 // Stop listening for keyup, since we only needed this once
2252 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
2256 * Handles key press events.
2259 * @param {jQuery.Event} e Key press event
2262 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2263 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2264 if ( this.emit( 'click' ) ) {
2271 * Check if button has a frame.
2273 * @return {boolean} Button is framed
2275 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2280 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2282 * @param {boolean} [framed] Make button framed, omit to toggle
2285 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2286 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2287 if ( framed
!== this.framed
) {
2288 this.framed
= framed
;
2290 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2291 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2292 this.updateThemeClasses();
2299 * Set the button's active state.
2301 * The active state can be set on:
2303 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2304 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2305 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2308 * @param {boolean} value Make button active
2311 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2312 this.active
= !!value
;
2313 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2314 this.updateThemeClasses();
2319 * Check if the button is active
2322 * @return {boolean} The button is active
2324 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2329 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2330 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2331 * items from the group is done through the interface the class provides.
2332 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2334 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
2337 * @mixins OO.EmitterList
2341 * @param {Object} [config] Configuration options
2342 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2343 * is omitted, the group element will use a generated `<div>`.
2345 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2346 // Configuration initialization
2347 config
= config
|| {};
2349 // Mixin constructors
2350 OO
.EmitterList
.call( this, config
);
2356 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2361 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2368 * A change event is emitted when the set of selected items changes.
2370 * @param {OO.ui.Element[]} items Items currently in the group
2376 * Set the group element.
2378 * If an element is already set, items will be moved to the new element.
2380 * @param {jQuery} $group Element to use as group
2382 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2385 this.$group
= $group
;
2386 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2387 this.$group
.append( this.items
[ i
].$element
);
2392 * Get an item by its data.
2394 * Only the first item with matching data will be returned. To return all matching items,
2395 * use the #getItemsFromData method.
2397 * @param {Object} data Item data to search for
2398 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2400 OO
.ui
.mixin
.GroupElement
.prototype.getItemFromData = function ( data
) {
2402 hash
= OO
.getHash( data
);
2404 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2405 item
= this.items
[ i
];
2406 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2415 * Get items by their data.
2417 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2419 * @param {Object} data Item data to search for
2420 * @return {OO.ui.Element[]} Items with equivalent data
2422 OO
.ui
.mixin
.GroupElement
.prototype.getItemsFromData = function ( data
) {
2424 hash
= OO
.getHash( data
),
2427 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2428 item
= this.items
[ i
];
2429 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2438 * Add items to the group.
2440 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2441 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2443 * @param {OO.ui.Element[]} items An array of items to add to the group
2444 * @param {number} [index] Index of the insertion point
2447 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2449 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2451 this.emit( 'change', this.getItems() );
2458 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2459 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2460 this.insertItemElements( items
, newIndex
);
2463 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2471 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2472 item
.setElementGroup( this );
2473 this.insertItemElements( item
, index
);
2476 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2482 * Insert elements into the group
2485 * @param {OO.ui.Element} itemWidget Item to insert
2486 * @param {number} index Insertion index
2488 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2489 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2490 this.$group
.append( itemWidget
.$element
);
2491 } else if ( index
=== 0 ) {
2492 this.$group
.prepend( itemWidget
.$element
);
2494 this.items
[ index
].$element
.before( itemWidget
.$element
);
2499 * Remove the specified items from a group.
2501 * Removed items are detached (not removed) from the DOM so that they may be reused.
2502 * To remove all items from a group, you may wish to use the #clearItems method instead.
2504 * @param {OO.ui.Element[]} items An array of items to remove
2507 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2508 var i
, len
, item
, index
;
2510 // Remove specific items elements
2511 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2513 index
= this.items
.indexOf( item
);
2514 if ( index
!== -1 ) {
2515 item
.setElementGroup( null );
2516 item
.$element
.detach();
2521 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2523 this.emit( 'change', this.getItems() );
2528 * Clear all items from the group.
2530 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2531 * To remove only a subset of items from a group, use the #removeItems method.
2535 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2538 // Remove all item elements
2539 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2540 this.items
[ i
].setElementGroup( null );
2541 this.items
[ i
].$element
.detach();
2545 OO
.EmitterList
.prototype.clearItems
.call( this );
2547 this.emit( 'change', this.getItems() );
2552 * IconElement is often mixed into other classes to generate an icon.
2553 * Icons are graphics, about the size of normal text. They are used to aid the user
2554 * in locating a control or to convey information in a space-efficient way. See the
2555 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2556 * included in the library.
2558 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2564 * @param {Object} [config] Configuration options
2565 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2566 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2567 * the icon element be set to an existing icon instead of the one generated by this class, set a
2568 * value using a jQuery selection. For example:
2570 * // Use a <div> tag instead of a <span>
2572 * // Use an existing icon element instead of the one generated by the class
2573 * $icon: this.$element
2574 * // Use an icon element from a child widget
2575 * $icon: this.childwidget.$element
2576 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2577 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2578 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2579 * by the user's language.
2581 * Example of an i18n map:
2583 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2584 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2585 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2586 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2587 * text. The icon title is displayed when users move the mouse over the icon.
2589 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2590 // Configuration initialization
2591 config
= config
|| {};
2596 this.iconTitle
= null;
2599 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2600 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2601 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2606 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2608 /* Static Properties */
2611 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2612 * for i18n purposes and contains a `default` icon name and additional names keyed by
2613 * language code. The `default` name is used when no icon is keyed by the user's language.
2615 * Example of an i18n map:
2617 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2619 * Note: the static property will be overridden if the #icon configuration is used.
2623 * @property {Object|string}
2625 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2628 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2629 * function that returns title text, or `null` for no title.
2631 * The static property will be overridden if the #iconTitle configuration is used.
2635 * @property {string|Function|null}
2637 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2642 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2643 * applies to the specified icon element instead of the one created by the class. If an icon
2644 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2645 * and mixin methods will no longer affect the element.
2647 * @param {jQuery} $icon Element to use as icon
2649 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2652 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2653 .removeAttr( 'title' );
2657 .addClass( 'oo-ui-iconElement-icon' )
2658 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2659 if ( this.iconTitle
!== null ) {
2660 this.$icon
.attr( 'title', this.iconTitle
);
2663 this.updateThemeClasses();
2667 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2668 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2671 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2672 * by language code, or `null` to remove the icon.
2675 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2676 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2677 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2679 if ( this.icon
!== icon
) {
2681 if ( this.icon
!== null ) {
2682 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2684 if ( icon
!== null ) {
2685 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2691 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2692 this.updateThemeClasses();
2698 * Set the icon title. Use `null` to remove the title.
2700 * @param {string|Function|null} iconTitle A text string used as the icon title,
2701 * a function that returns title text, or `null` for no title.
2704 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2706 ( typeof iconTitle
=== 'function' || ( typeof iconTitle
=== 'string' && iconTitle
.length
) ) ?
2707 OO
.ui
.resolveMsg( iconTitle
) : null;
2709 if ( this.iconTitle
!== iconTitle
) {
2710 this.iconTitle
= iconTitle
;
2712 if ( this.iconTitle
!== null ) {
2713 this.$icon
.attr( 'title', iconTitle
);
2715 this.$icon
.removeAttr( 'title' );
2724 * Get the symbolic name of the icon.
2726 * @return {string} Icon name
2728 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2733 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2735 * @return {string} Icon title text
2737 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2738 return this.iconTitle
;
2742 * IndicatorElement is often mixed into other classes to generate an indicator.
2743 * Indicators are small graphics that are generally used in two ways:
2745 * - To draw attention to the status of an item. For example, an indicator might be
2746 * used to show that an item in a list has errors that need to be resolved.
2747 * - To clarify the function of a control that acts in an exceptional way (a button
2748 * that opens a menu instead of performing an action directly, for example).
2750 * For a list of indicators included in the library, please see the
2751 * [OOjs UI documentation on MediaWiki] [1].
2753 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2759 * @param {Object} [config] Configuration options
2760 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2761 * configuration is omitted, the indicator element will use a generated `<span>`.
2762 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2763 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2765 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2766 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2767 * or a function that returns title text. The indicator title is displayed when users move
2768 * the mouse over the indicator.
2770 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2771 // Configuration initialization
2772 config
= config
|| {};
2775 this.$indicator
= null;
2776 this.indicator
= null;
2777 this.indicatorTitle
= null;
2780 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2781 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2782 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2787 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2789 /* Static Properties */
2792 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2793 * The static property will be overridden if the #indicator configuration is used.
2797 * @property {string|null}
2799 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2802 * A text string used as the indicator title, a function that returns title text, or `null`
2803 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2807 * @property {string|Function|null}
2809 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2814 * Set the indicator element.
2816 * If an element is already set, it will be cleaned up before setting up the new element.
2818 * @param {jQuery} $indicator Element to use as indicator
2820 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2821 if ( this.$indicator
) {
2823 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2824 .removeAttr( 'title' );
2827 this.$indicator
= $indicator
2828 .addClass( 'oo-ui-indicatorElement-indicator' )
2829 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2830 if ( this.indicatorTitle
!== null ) {
2831 this.$indicator
.attr( 'title', this.indicatorTitle
);
2834 this.updateThemeClasses();
2838 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2840 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2843 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2844 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2846 if ( this.indicator
!== indicator
) {
2847 if ( this.$indicator
) {
2848 if ( this.indicator
!== null ) {
2849 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2851 if ( indicator
!== null ) {
2852 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2855 this.indicator
= indicator
;
2858 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2859 this.updateThemeClasses();
2865 * Set the indicator title.
2867 * The title is displayed when a user moves the mouse over the indicator.
2869 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2870 * `null` for no indicator title
2873 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2875 ( typeof indicatorTitle
=== 'function' || ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ) ?
2876 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2878 if ( this.indicatorTitle
!== indicatorTitle
) {
2879 this.indicatorTitle
= indicatorTitle
;
2880 if ( this.$indicator
) {
2881 if ( this.indicatorTitle
!== null ) {
2882 this.$indicator
.attr( 'title', indicatorTitle
);
2884 this.$indicator
.removeAttr( 'title' );
2893 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2895 * @return {string} Symbolic name of indicator
2897 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2898 return this.indicator
;
2902 * Get the indicator title.
2904 * The title is displayed when a user moves the mouse over the indicator.
2906 * @return {string} Indicator title text
2908 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2909 return this.indicatorTitle
;
2913 * LabelElement is often mixed into other classes to generate a label, which
2914 * helps identify the function of an interface element.
2915 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2917 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2923 * @param {Object} [config] Configuration options
2924 * @cfg {jQuery} [$label] The label element created by the class. If this
2925 * configuration is omitted, the label element will use a generated `<span>`.
2926 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2927 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2928 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2929 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2931 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2932 // Configuration initialization
2933 config
= config
|| {};
2940 this.setLabel( config
.label
|| this.constructor.static.label
);
2941 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2946 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2951 * @event labelChange
2952 * @param {string} value
2955 /* Static Properties */
2958 * The label text. The label can be specified as a plaintext string, a function that will
2959 * produce a string in the future, or `null` for no label. The static value will
2960 * be overridden if a label is specified with the #label config option.
2964 * @property {string|Function|null}
2966 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2968 /* Static methods */
2971 * Highlight the first occurrence of the query in the given text
2973 * @param {string} text Text
2974 * @param {string} query Query to find
2975 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2976 * @return {jQuery} Text with the first match of the query
2977 * sub-string wrapped in highlighted span
2979 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2982 $result
= $( '<span>' );
2986 qLen
= query
.length
;
2987 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2988 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2993 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2996 if ( !query
.length
|| offset
=== -1 ) {
2997 $result
.text( text
);
3000 document
.createTextNode( text
.slice( 0, offset
) ),
3002 .addClass( 'oo-ui-labelElement-label-highlight' )
3003 .text( text
.slice( offset
, offset
+ query
.length
) ),
3004 document
.createTextNode( text
.slice( offset
+ query
.length
) )
3007 return $result
.contents();
3013 * Set the label element.
3015 * If an element is already set, it will be cleaned up before setting up the new element.
3017 * @param {jQuery} $label Element to use as label
3019 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
3020 if ( this.$label
) {
3021 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
3024 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
3025 this.setLabelContent( this.label
);
3031 * An empty string will result in the label being hidden. A string containing only whitespace will
3032 * be converted to a single ` `.
3034 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
3035 * text; or null for no label
3038 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
3039 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
3040 label
= ( ( typeof label
=== 'string' || label
instanceof jQuery
) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
3042 if ( this.label
!== label
) {
3043 if ( this.$label
) {
3044 this.setLabelContent( label
);
3047 this.emit( 'labelChange' );
3050 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
);
3056 * Set the label as plain text with a highlighted query
3058 * @param {string} text Text label to set
3059 * @param {string} query Substring of text to highlight
3060 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
3063 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
3064 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
3070 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
3071 * text; or null for no label
3073 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
3078 * Set the content of the label.
3080 * Do not call this method until after the label element has been set by #setLabelElement.
3083 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
3084 * text; or null for no label
3086 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
3087 if ( typeof label
=== 'string' ) {
3088 if ( label
.match( /^\s*$/ ) ) {
3089 // Convert whitespace only string to a single non-breaking space
3090 this.$label
.html( ' ' );
3092 this.$label
.text( label
);
3094 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
3095 this.$label
.html( label
.toString() );
3096 } else if ( label
instanceof jQuery
) {
3097 this.$label
.empty().append( label
);
3099 this.$label
.empty();
3104 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3105 * additional functionality to an element created by another class. The class provides
3106 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3107 * which are used to customize the look and feel of a widget to better describe its
3108 * importance and functionality.
3110 * The library currently contains the following styling flags for general use:
3112 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
3113 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
3114 * - **constructive**: Constructive styling is deprecated since v0.23.2 and equivalent to progressive.
3116 * The flags affect the appearance of the buttons:
3119 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3120 * var button1 = new OO.ui.ButtonWidget( {
3121 * label: 'Progressive',
3122 * flags: 'progressive'
3124 * var button2 = new OO.ui.ButtonWidget( {
3125 * label: 'Destructive',
3126 * flags: 'destructive'
3128 * $( 'body' ).append( button1.$element, button2.$element );
3130 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
3131 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
3133 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
3139 * @param {Object} [config] Configuration options
3140 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary') to apply.
3141 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
3142 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
3143 * @cfg {jQuery} [$flagged] The flagged element. By default,
3144 * the flagged functionality is applied to the element created by the class ($element).
3145 * If a different element is specified, the flagged functionality will be applied to it instead.
3147 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3148 // Configuration initialization
3149 config
= config
|| {};
3153 this.$flagged
= null;
3156 this.setFlags( config
.flags
);
3157 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3164 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3165 * parameter contains the name of each modified flag and indicates whether it was
3168 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3169 * that the flag was added, `false` that the flag was removed.
3175 * Set the flagged element.
3177 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3178 * If an element is already set, the method will remove the mixin’s effect on that element.
3180 * @param {jQuery} $flagged Element that should be flagged
3182 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3183 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3184 return 'oo-ui-flaggedElement-' + flag
;
3187 if ( this.$flagged
) {
3188 this.$flagged
.removeClass( classNames
);
3191 this.$flagged
= $flagged
.addClass( classNames
);
3195 * Check if the specified flag is set.
3197 * @param {string} flag Name of flag
3198 * @return {boolean} The flag is set
3200 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3201 // This may be called before the constructor, thus before this.flags is set
3202 return this.flags
&& ( flag
in this.flags
);
3206 * Get the names of all flags set.
3208 * @return {string[]} Flag names
3210 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3211 // This may be called before the constructor, thus before this.flags is set
3212 return Object
.keys( this.flags
|| {} );
3221 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3222 var flag
, className
,
3225 classPrefix
= 'oo-ui-flaggedElement-';
3227 for ( flag
in this.flags
) {
3228 className
= classPrefix
+ flag
;
3229 changes
[ flag
] = false;
3230 delete this.flags
[ flag
];
3231 remove
.push( className
);
3234 if ( this.$flagged
) {
3235 this.$flagged
.removeClass( remove
.join( ' ' ) );
3238 this.updateThemeClasses();
3239 this.emit( 'flag', changes
);
3245 * Add one or more flags.
3247 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3248 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3249 * be added (`true`) or removed (`false`).
3253 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3254 var i
, len
, flag
, className
,
3258 classPrefix
= 'oo-ui-flaggedElement-';
3260 if ( typeof flags
=== 'string' ) {
3261 className
= classPrefix
+ flags
;
3263 if ( !this.flags
[ flags
] ) {
3264 this.flags
[ flags
] = true;
3265 add
.push( className
);
3267 } else if ( Array
.isArray( flags
) ) {
3268 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3270 className
= classPrefix
+ flag
;
3272 if ( !this.flags
[ flag
] ) {
3273 changes
[ flag
] = true;
3274 this.flags
[ flag
] = true;
3275 add
.push( className
);
3278 } else if ( OO
.isPlainObject( flags
) ) {
3279 for ( flag
in flags
) {
3280 className
= classPrefix
+ flag
;
3281 if ( flags
[ flag
] ) {
3283 if ( !this.flags
[ flag
] ) {
3284 changes
[ flag
] = true;
3285 this.flags
[ flag
] = true;
3286 add
.push( className
);
3290 if ( this.flags
[ flag
] ) {
3291 changes
[ flag
] = false;
3292 delete this.flags
[ flag
];
3293 remove
.push( className
);
3299 if ( this.$flagged
) {
3301 .addClass( add
.join( ' ' ) )
3302 .removeClass( remove
.join( ' ' ) );
3305 this.updateThemeClasses();
3306 this.emit( 'flag', changes
);
3312 * TitledElement is mixed into other classes to provide a `title` attribute.
3313 * Titles are rendered by the browser and are made visible when the user moves
3314 * the mouse over the element. Titles are not visible on touch devices.
3317 * // TitledElement provides a 'title' attribute to the
3318 * // ButtonWidget class
3319 * var button = new OO.ui.ButtonWidget( {
3320 * label: 'Button with Title',
3321 * title: 'I am a button'
3323 * $( 'body' ).append( button.$element );
3329 * @param {Object} [config] Configuration options
3330 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3331 * If this config is omitted, the title functionality is applied to $element, the
3332 * element created by the class.
3333 * @cfg {string|Function} [title] The title text or a function that returns text. If
3334 * this config is omitted, the value of the {@link #static-title static title} property is used.
3336 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3337 // Configuration initialization
3338 config
= config
|| {};
3341 this.$titled
= null;
3345 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3346 this.setTitledElement( config
.$titled
|| this.$element
);
3351 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3353 /* Static Properties */
3356 * The title text, a function that returns text, or `null` for no title. The value of the static property
3357 * is overridden if the #title config option is used.
3361 * @property {string|Function|null}
3363 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3368 * Set the titled element.
3370 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3371 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3373 * @param {jQuery} $titled Element that should use the 'titled' functionality
3375 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3376 if ( this.$titled
) {
3377 this.$titled
.removeAttr( 'title' );
3380 this.$titled
= $titled
;
3389 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3392 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3393 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3394 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3396 if ( this.title
!== title
) {
3405 * Update the title attribute, in case of changes to title or accessKey.
3410 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3411 var title
= this.getTitle();
3412 if ( this.$titled
) {
3413 if ( title
!== null ) {
3414 // Only if this is an AccessKeyedElement
3415 if ( this.formatTitleWithAccessKey
) {
3416 title
= this.formatTitleWithAccessKey( title
);
3418 this.$titled
.attr( 'title', title
);
3420 this.$titled
.removeAttr( 'title' );
3429 * @return {string} Title string
3431 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3436 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3437 * Accesskeys allow an user to go to a specific element by using
3438 * a shortcut combination of a browser specific keys + the key
3442 * // AccessKeyedElement provides an 'accesskey' attribute to the
3443 * // ButtonWidget class
3444 * var button = new OO.ui.ButtonWidget( {
3445 * label: 'Button with Accesskey',
3448 * $( 'body' ).append( button.$element );
3454 * @param {Object} [config] Configuration options
3455 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3456 * If this config is omitted, the accesskey functionality is applied to $element, the
3457 * element created by the class.
3458 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3459 * this config is omitted, no accesskey will be added.
3461 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3462 // Configuration initialization
3463 config
= config
|| {};
3466 this.$accessKeyed
= null;
3467 this.accessKey
= null;
3470 this.setAccessKey( config
.accessKey
|| null );
3471 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3473 // If this is also a TitledElement and it initialized before we did, we may have
3474 // to update the title with the access key
3475 if ( this.updateTitle
) {
3482 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3484 /* Static Properties */
3487 * The access key, a function that returns a key, or `null` for no accesskey.
3491 * @property {string|Function|null}
3493 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3498 * Set the accesskeyed element.
3500 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3501 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3503 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3505 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3506 if ( this.$accessKeyed
) {
3507 this.$accessKeyed
.removeAttr( 'accesskey' );
3510 this.$accessKeyed
= $accessKeyed
;
3511 if ( this.accessKey
) {
3512 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3519 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3522 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3523 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3525 if ( this.accessKey
!== accessKey
) {
3526 if ( this.$accessKeyed
) {
3527 if ( accessKey
!== null ) {
3528 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3530 this.$accessKeyed
.removeAttr( 'accesskey' );
3533 this.accessKey
= accessKey
;
3535 // Only if this is a TitledElement
3536 if ( this.updateTitle
) {
3547 * @return {string} accessKey string
3549 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3550 return this.accessKey
;
3554 * Add information about the access key to the element's tooltip label.
3555 * (This is only public for hacky usage in FieldLayout.)
3557 * @param {string} title Tooltip label for `title` attribute
3560 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3563 if ( !this.$accessKeyed
) {
3564 // Not initialized yet; the constructor will call updateTitle() which will rerun this function
3567 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the single key
3568 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3569 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3571 accessKey
= this.getAccessKey();
3574 title
+= ' [' + accessKey
+ ']';
3580 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3581 * feels, and functionality can be customized via the class’s configuration options
3582 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3585 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3588 * // A button widget
3589 * var button = new OO.ui.ButtonWidget( {
3590 * label: 'Button with Icon',
3592 * iconTitle: 'Remove'
3594 * $( 'body' ).append( button.$element );
3596 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3599 * @extends OO.ui.Widget
3600 * @mixins OO.ui.mixin.ButtonElement
3601 * @mixins OO.ui.mixin.IconElement
3602 * @mixins OO.ui.mixin.IndicatorElement
3603 * @mixins OO.ui.mixin.LabelElement
3604 * @mixins OO.ui.mixin.TitledElement
3605 * @mixins OO.ui.mixin.FlaggedElement
3606 * @mixins OO.ui.mixin.TabIndexedElement
3607 * @mixins OO.ui.mixin.AccessKeyedElement
3610 * @param {Object} [config] Configuration options
3611 * @cfg {boolean} [active=false] Whether button should be shown as active
3612 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3613 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3614 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3616 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3617 // Configuration initialization
3618 config
= config
|| {};
3620 // Parent constructor
3621 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3623 // Mixin constructors
3624 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3625 OO
.ui
.mixin
.IconElement
.call( this, config
);
3626 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3627 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3628 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3629 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3630 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3631 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3636 this.noFollow
= false;
3639 this.connect( this, { disable
: 'onDisable' } );
3642 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3644 .addClass( 'oo-ui-buttonWidget' )
3645 .append( this.$button
);
3646 this.setActive( config
.active
);
3647 this.setHref( config
.href
);
3648 this.setTarget( config
.target
);
3649 this.setNoFollow( config
.noFollow
);
3654 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3655 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3656 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3657 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3658 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3659 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3660 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3661 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3662 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3664 /* Static Properties */
3670 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3676 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3681 * Get hyperlink location.
3683 * @return {string} Hyperlink location
3685 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3690 * Get hyperlink target.
3692 * @return {string} Hyperlink target
3694 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3699 * Get search engine traversal hint.
3701 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3703 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3704 return this.noFollow
;
3708 * Set hyperlink location.
3710 * @param {string|null} href Hyperlink location, null to remove
3712 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3713 href
= typeof href
=== 'string' ? href
: null;
3714 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3718 if ( href
!== this.href
) {
3727 * Update the `href` attribute, in case of changes to href or
3733 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3734 if ( this.href
!== null && !this.isDisabled() ) {
3735 this.$button
.attr( 'href', this.href
);
3737 this.$button
.removeAttr( 'href' );
3744 * Handle disable events.
3747 * @param {boolean} disabled Element is disabled
3749 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3754 * Set hyperlink target.
3756 * @param {string|null} target Hyperlink target, null to remove
3758 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3759 target
= typeof target
=== 'string' ? target
: null;
3761 if ( target
!== this.target
) {
3762 this.target
= target
;
3763 if ( target
!== null ) {
3764 this.$button
.attr( 'target', target
);
3766 this.$button
.removeAttr( 'target' );
3774 * Set search engine traversal hint.
3776 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3778 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3779 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3781 if ( noFollow
!== this.noFollow
) {
3782 this.noFollow
= noFollow
;
3784 this.$button
.attr( 'rel', 'nofollow' );
3786 this.$button
.removeAttr( 'rel' );
3793 // Override method visibility hints from ButtonElement
3804 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3805 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3806 * removed, and cleared from the group.
3809 * // Example: A ButtonGroupWidget with two buttons
3810 * var button1 = new OO.ui.PopupButtonWidget( {
3811 * label: 'Select a category',
3814 * $content: $( '<p>List of categories...</p>' ),
3819 * var button2 = new OO.ui.ButtonWidget( {
3822 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3823 * items: [button1, button2]
3825 * $( 'body' ).append( buttonGroup.$element );
3828 * @extends OO.ui.Widget
3829 * @mixins OO.ui.mixin.GroupElement
3832 * @param {Object} [config] Configuration options
3833 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3835 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3836 // Configuration initialization
3837 config
= config
|| {};
3839 // Parent constructor
3840 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3842 // Mixin constructors
3843 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3846 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3847 if ( Array
.isArray( config
.items
) ) {
3848 this.addItems( config
.items
);
3854 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3855 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3857 /* Static Properties */
3863 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
3872 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
3873 if ( !this.isDisabled() ) {
3874 if ( this.items
[ 0 ] ) {
3875 this.items
[ 0 ].focus();
3884 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
3889 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3890 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3891 * for a list of icons included in the library.
3894 * // An icon widget with a label
3895 * var myIcon = new OO.ui.IconWidget( {
3899 * // Create a label.
3900 * var iconLabel = new OO.ui.LabelWidget( {
3903 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3905 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3908 * @extends OO.ui.Widget
3909 * @mixins OO.ui.mixin.IconElement
3910 * @mixins OO.ui.mixin.TitledElement
3911 * @mixins OO.ui.mixin.FlaggedElement
3914 * @param {Object} [config] Configuration options
3916 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3917 // Configuration initialization
3918 config
= config
|| {};
3920 // Parent constructor
3921 OO
.ui
.IconWidget
.parent
.call( this, config
);
3923 // Mixin constructors
3924 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3925 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3926 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3929 this.$element
.addClass( 'oo-ui-iconWidget' );
3934 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3935 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3936 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3937 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3939 /* Static Properties */
3945 OO
.ui
.IconWidget
.static.tagName
= 'span';
3948 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3949 * attention to the status of an item or to clarify the function of a control. For a list of
3950 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3953 * // Example of an indicator widget
3954 * var indicator1 = new OO.ui.IndicatorWidget( {
3955 * indicator: 'alert'
3958 * // Create a fieldset layout to add a label
3959 * var fieldset = new OO.ui.FieldsetLayout();
3960 * fieldset.addItems( [
3961 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3963 * $( 'body' ).append( fieldset.$element );
3965 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3968 * @extends OO.ui.Widget
3969 * @mixins OO.ui.mixin.IndicatorElement
3970 * @mixins OO.ui.mixin.TitledElement
3973 * @param {Object} [config] Configuration options
3975 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
3976 // Configuration initialization
3977 config
= config
|| {};
3979 // Parent constructor
3980 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
3982 // Mixin constructors
3983 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
3984 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3987 this.$element
.addClass( 'oo-ui-indicatorWidget' );
3992 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
3993 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
3994 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
3996 /* Static Properties */
4002 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4005 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4006 * be configured with a `label` option that is set to a string, a label node, or a function:
4008 * - String: a plaintext string
4009 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4010 * label that includes a link or special styling, such as a gray color or additional graphical elements.
4011 * - Function: a function that will produce a string in the future. Functions are used
4012 * in cases where the value of the label is not currently defined.
4014 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
4015 * will come into focus when the label is clicked.
4018 * // Examples of LabelWidgets
4019 * var label1 = new OO.ui.LabelWidget( {
4020 * label: 'plaintext label'
4022 * var label2 = new OO.ui.LabelWidget( {
4023 * label: $( '<a href="default.html">jQuery label</a>' )
4025 * // Create a fieldset layout with fields for each example
4026 * var fieldset = new OO.ui.FieldsetLayout();
4027 * fieldset.addItems( [
4028 * new OO.ui.FieldLayout( label1 ),
4029 * new OO.ui.FieldLayout( label2 )
4031 * $( 'body' ).append( fieldset.$element );
4034 * @extends OO.ui.Widget
4035 * @mixins OO.ui.mixin.LabelElement
4036 * @mixins OO.ui.mixin.TitledElement
4039 * @param {Object} [config] Configuration options
4040 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4041 * Clicking the label will focus the specified input field.
4043 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4044 // Configuration initialization
4045 config
= config
|| {};
4047 // Parent constructor
4048 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4050 // Mixin constructors
4051 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
4052 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4055 this.input
= config
.input
;
4059 if ( this.input
.getInputId() ) {
4060 this.$element
.attr( 'for', this.input
.getInputId() );
4062 this.$label
.on( 'click', function () {
4063 this.input
.simulateLabelClick();
4068 this.$element
.addClass( 'oo-ui-labelWidget' );
4073 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4074 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4075 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4077 /* Static Properties */
4083 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4086 * PendingElement is a mixin that is used to create elements that notify users that something is happening
4087 * and that they should wait before proceeding. The pending state is visually represented with a pending
4088 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
4089 * field of a {@link OO.ui.TextInputWidget text input widget}.
4091 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
4092 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
4093 * in process dialogs.
4096 * function MessageDialog( config ) {
4097 * MessageDialog.parent.call( this, config );
4099 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4101 * MessageDialog.static.name = 'myMessageDialog';
4102 * MessageDialog.static.actions = [
4103 * { action: 'save', label: 'Done', flags: 'primary' },
4104 * { label: 'Cancel', flags: 'safe' }
4107 * MessageDialog.prototype.initialize = function () {
4108 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4109 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
4110 * 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>' );
4111 * this.$body.append( this.content.$element );
4113 * MessageDialog.prototype.getBodyHeight = function () {
4116 * MessageDialog.prototype.getActionProcess = function ( action ) {
4117 * var dialog = this;
4118 * if ( action === 'save' ) {
4119 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4120 * return new OO.ui.Process()
4122 * .next( function () {
4123 * dialog.getActions().get({actions: 'save'})[0].popPending();
4126 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4129 * var windowManager = new OO.ui.WindowManager();
4130 * $( 'body' ).append( windowManager.$element );
4132 * var dialog = new MessageDialog();
4133 * windowManager.addWindows( [ dialog ] );
4134 * windowManager.openWindow( dialog );
4140 * @param {Object} [config] Configuration options
4141 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4143 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4144 // Configuration initialization
4145 config
= config
|| {};
4149 this.$pending
= null;
4152 this.setPendingElement( config
.$pending
|| this.$element
);
4157 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4162 * Set the pending element (and clean up any existing one).
4164 * @param {jQuery} $pending The element to set to pending.
4166 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4167 if ( this.$pending
) {
4168 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4171 this.$pending
= $pending
;
4172 if ( this.pending
> 0 ) {
4173 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4178 * Check if an element is pending.
4180 * @return {boolean} Element is pending
4182 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4183 return !!this.pending
;
4187 * Increase the pending counter. The pending state will remain active until the counter is zero
4188 * (i.e., the number of calls to #pushPending and #popPending is the same).
4192 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4193 if ( this.pending
=== 0 ) {
4194 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4195 this.updateThemeClasses();
4203 * Decrease the pending counter. The pending state will remain active until the counter is zero
4204 * (i.e., the number of calls to #pushPending and #popPending is the same).
4208 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4209 if ( this.pending
=== 1 ) {
4210 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4211 this.updateThemeClasses();
4213 this.pending
= Math
.max( 0, this.pending
- 1 );
4219 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4220 * in the document (for example, in an OO.ui.Window's $overlay).
4222 * The elements's position is automatically calculated and maintained when window is resized or the
4223 * page is scrolled. If you reposition the container manually, you have to call #position to make
4224 * sure the element is still placed correctly.
4226 * As positioning is only possible when both the element and the container are attached to the DOM
4227 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4228 * the #toggle method to display a floating popup, for example.
4234 * @param {Object} [config] Configuration options
4235 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4236 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4237 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4238 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4239 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4240 * 'top': Align the top edge with $floatableContainer's top edge
4241 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4242 * 'center': Vertically align the center with $floatableContainer's center
4243 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4244 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4245 * 'after': Directly after $floatableContainer, algining f's start edge with fC's end edge
4246 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4247 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4248 * 'center': Horizontally align the center with $floatableContainer's center
4249 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4252 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4253 // Configuration initialization
4254 config
= config
|| {};
4257 this.$floatable
= null;
4258 this.$floatableContainer
= null;
4259 this.$floatableWindow
= null;
4260 this.$floatableClosestScrollable
= null;
4261 this.onFloatableScrollHandler
= this.position
.bind( this );
4262 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4265 this.setFloatableContainer( config
.$floatableContainer
);
4266 this.setFloatableElement( config
.$floatable
|| this.$element
);
4267 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4268 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4269 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ? true : !!config
.hideWhenOutOfView
;
4275 * Set floatable element.
4277 * If an element is already set, it will be cleaned up before setting up the new element.
4279 * @param {jQuery} $floatable Element to make floatable
4281 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4282 if ( this.$floatable
) {
4283 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4284 this.$floatable
.css( { left
: '', top
: '' } );
4287 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4292 * Set floatable container.
4294 * The element will be positioned relative to the specified container.
4296 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4298 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4299 this.$floatableContainer
= $floatableContainer
;
4300 if ( this.$floatable
) {
4306 * Change how the element is positioned vertically.
4308 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4310 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4311 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4312 throw new Error( 'Invalid value for vertical position: ' + position
);
4314 if ( this.verticalPosition
!== position
) {
4315 this.verticalPosition
= position
;
4316 if ( this.$floatable
) {
4323 * Change how the element is positioned horizontally.
4325 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4327 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4328 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4329 throw new Error( 'Invalid value for horizontal position: ' + position
);
4331 if ( this.horizontalPosition
!== position
) {
4332 this.horizontalPosition
= position
;
4333 if ( this.$floatable
) {
4340 * Toggle positioning.
4342 * Do not turn positioning on until after the element is attached to the DOM and visible.
4344 * @param {boolean} [positioning] Enable positioning, omit to toggle
4347 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4348 var closestScrollableOfContainer
;
4350 if ( !this.$floatable
|| !this.$floatableContainer
) {
4354 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4356 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4357 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4358 this.warnedUnattached
= true;
4361 if ( this.positioning
!== positioning
) {
4362 this.positioning
= positioning
;
4364 this.needsCustomPosition
=
4365 this.verticalPostion
!== 'below' ||
4366 this.horizontalPosition
!== 'start' ||
4367 !OO
.ui
.contains( this.$floatableContainer
[ 0 ], this.$floatable
[ 0 ] );
4369 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
4370 // If the scrollable is the root, we have to listen to scroll events
4371 // on the window because of browser inconsistencies.
4372 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4373 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
4376 if ( positioning
) {
4377 this.$floatableWindow
= $( this.getElementWindow() );
4378 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4380 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4381 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4383 // Initial position after visible
4386 if ( this.$floatableWindow
) {
4387 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4388 this.$floatableWindow
= null;
4391 if ( this.$floatableClosestScrollable
) {
4392 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4393 this.$floatableClosestScrollable
= null;
4396 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4404 * Check whether the bottom edge of the given element is within the viewport of the given container.
4407 * @param {jQuery} $element
4408 * @param {jQuery} $container
4411 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4412 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
, rightEdgeInBounds
,
4413 startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4414 direction
= $element
.css( 'direction' );
4416 elemRect
= $element
[ 0 ].getBoundingClientRect();
4417 if ( $container
[ 0 ] === window
) {
4418 viewportSpacing
= OO
.ui
.getViewportSpacing();
4422 right
: document
.documentElement
.clientWidth
,
4423 bottom
: document
.documentElement
.clientHeight
4425 contRect
.top
+= viewportSpacing
.top
;
4426 contRect
.left
+= viewportSpacing
.left
;
4427 contRect
.right
-= viewportSpacing
.right
;
4428 contRect
.bottom
-= viewportSpacing
.bottom
;
4430 contRect
= $container
[ 0 ].getBoundingClientRect();
4433 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4434 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4435 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4436 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4437 if ( direction
=== 'rtl' ) {
4438 startEdgeInBounds
= rightEdgeInBounds
;
4439 endEdgeInBounds
= leftEdgeInBounds
;
4441 startEdgeInBounds
= leftEdgeInBounds
;
4442 endEdgeInBounds
= rightEdgeInBounds
;
4445 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4448 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4451 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4454 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4458 // The other positioning values are all about being inside the container,
4459 // so in those cases all we care about is that any part of the container is visible.
4460 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4461 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4465 * Position the floatable below its container.
4467 * This should only be done when both of them are attached to the DOM and visible.
4471 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4472 if ( !this.positioning
) {
4477 // To continue, some things need to be true:
4478 // The element must actually be in the DOM
4479 this.isElementAttached() && (
4480 // The closest scrollable is the current window
4481 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4482 // OR is an element in the element's DOM
4483 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4486 // Abort early if important parts of the widget are no longer attached to the DOM
4490 if ( this.hideWhenOutOfView
&& !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
) ) {
4491 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4494 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4497 if ( !this.needsCustomPosition
) {
4501 this.$floatable
.css( this.computePosition() );
4503 // We updated the position, so re-evaluate the clipping state.
4504 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4505 // will not notice the need to update itself.)
4506 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4507 // it not listen to the right events in the right places?
4516 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4517 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4518 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4520 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4522 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4523 var isBody
, scrollableX
, scrollableY
, containerPos
,
4524 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4525 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4526 direction
= this.$floatableContainer
.css( 'direction' ),
4527 $offsetParent
= this.$floatable
.offsetParent();
4529 if ( $offsetParent
.is( 'html' ) ) {
4530 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4531 // <html> element, but they do work on the <body>
4532 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4534 isBody
= $offsetParent
.is( 'body' );
4535 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' || $offsetParent
.css( 'overflow-x' ) === 'auto';
4536 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' || $offsetParent
.css( 'overflow-y' ) === 'auto';
4538 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4539 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4540 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
4541 // or if it isn't scrollable
4542 scrollTop
= scrollableY
&& !isBody
? $offsetParent
.scrollTop() : 0;
4543 scrollLeft
= scrollableX
&& !isBody
? OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4545 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4546 // if the <body> has a margin
4547 containerPos
= isBody
?
4548 this.$floatableContainer
.offset() :
4549 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4550 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4551 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4552 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4553 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4555 if ( this.verticalPosition
=== 'below' ) {
4556 newPos
.top
= containerPos
.bottom
;
4557 } else if ( this.verticalPosition
=== 'above' ) {
4558 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4559 } else if ( this.verticalPosition
=== 'top' ) {
4560 newPos
.top
= containerPos
.top
;
4561 } else if ( this.verticalPosition
=== 'bottom' ) {
4562 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4563 } else if ( this.verticalPosition
=== 'center' ) {
4564 newPos
.top
= containerPos
.top
+
4565 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4568 if ( this.horizontalPosition
=== 'before' ) {
4569 newPos
.end
= containerPos
.start
;
4570 } else if ( this.horizontalPosition
=== 'after' ) {
4571 newPos
.start
= containerPos
.end
;
4572 } else if ( this.horizontalPosition
=== 'start' ) {
4573 newPos
.start
= containerPos
.start
;
4574 } else if ( this.horizontalPosition
=== 'end' ) {
4575 newPos
.end
= containerPos
.end
;
4576 } else if ( this.horizontalPosition
=== 'center' ) {
4577 newPos
.left
= containerPos
.left
+
4578 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4581 if ( newPos
.start
!== undefined ) {
4582 if ( direction
=== 'rtl' ) {
4583 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.start
;
4585 newPos
.left
= newPos
.start
;
4587 delete newPos
.start
;
4589 if ( newPos
.end
!== undefined ) {
4590 if ( direction
=== 'rtl' ) {
4591 newPos
.left
= newPos
.end
;
4593 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.end
;
4598 // Account for scroll position
4599 if ( newPos
.top
!== '' ) {
4600 newPos
.top
+= scrollTop
;
4602 if ( newPos
.bottom
!== '' ) {
4603 newPos
.bottom
-= scrollTop
;
4605 if ( newPos
.left
!== '' ) {
4606 newPos
.left
+= scrollLeft
;
4608 if ( newPos
.right
!== '' ) {
4609 newPos
.right
-= scrollLeft
;
4612 // Account for scrollbar gutter
4613 if ( newPos
.bottom
!== '' ) {
4614 newPos
.bottom
-= horizScrollbarHeight
;
4616 if ( direction
=== 'rtl' ) {
4617 if ( newPos
.left
!== '' ) {
4618 newPos
.left
-= vertScrollbarWidth
;
4621 if ( newPos
.right
!== '' ) {
4622 newPos
.right
-= vertScrollbarWidth
;
4630 * Element that can be automatically clipped to visible boundaries.
4632 * Whenever the element's natural height changes, you have to call
4633 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4634 * clipping correctly.
4636 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4637 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4638 * then #$clippable will be given a fixed reduced height and/or width and will be made
4639 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4640 * but you can build a static footer by setting #$clippableContainer to an element that contains
4641 * #$clippable and the footer.
4647 * @param {Object} [config] Configuration options
4648 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4649 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4650 * omit to use #$clippable
4652 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4653 // Configuration initialization
4654 config
= config
|| {};
4657 this.$clippable
= null;
4658 this.$clippableContainer
= null;
4659 this.clipping
= false;
4660 this.clippedHorizontally
= false;
4661 this.clippedVertically
= false;
4662 this.$clippableScrollableContainer
= null;
4663 this.$clippableScroller
= null;
4664 this.$clippableWindow
= null;
4665 this.idealWidth
= null;
4666 this.idealHeight
= null;
4667 this.onClippableScrollHandler
= this.clip
.bind( this );
4668 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4671 if ( config
.$clippableContainer
) {
4672 this.setClippableContainer( config
.$clippableContainer
);
4674 this.setClippableElement( config
.$clippable
|| this.$element
);
4680 * Set clippable element.
4682 * If an element is already set, it will be cleaned up before setting up the new element.
4684 * @param {jQuery} $clippable Element to make clippable
4686 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4687 if ( this.$clippable
) {
4688 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4689 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4690 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4693 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4698 * Set clippable container.
4700 * This is the container that will be measured when deciding whether to clip. When clipping,
4701 * #$clippable will be resized in order to keep the clippable container fully visible.
4703 * If the clippable container is unset, #$clippable will be used.
4705 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4707 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4708 this.$clippableContainer
= $clippableContainer
;
4709 if ( this.$clippable
) {
4717 * Do not turn clipping on until after the element is attached to the DOM and visible.
4719 * @param {boolean} [clipping] Enable clipping, omit to toggle
4722 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4723 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4725 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4726 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4727 this.warnedUnattached
= true;
4730 if ( this.clipping
!== clipping
) {
4731 this.clipping
= clipping
;
4733 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4734 // If the clippable container is the root, we have to listen to scroll events and check
4735 // jQuery.scrollTop on the window because of browser inconsistencies
4736 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4737 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4738 this.$clippableScrollableContainer
;
4739 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4740 this.$clippableWindow
= $( this.getElementWindow() )
4741 .on( 'resize', this.onClippableWindowResizeHandler
);
4742 // Initial clip after visible
4745 this.$clippable
.css( {
4753 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4755 this.$clippableScrollableContainer
= null;
4756 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4757 this.$clippableScroller
= null;
4758 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4759 this.$clippableWindow
= null;
4767 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4769 * @return {boolean} Element will be clipped to the visible area
4771 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4772 return this.clipping
;
4776 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4778 * @return {boolean} Part of the element is being clipped
4780 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4781 return this.clippedHorizontally
|| this.clippedVertically
;
4785 * Check if the right of the element is being clipped by the nearest scrollable container.
4787 * @return {boolean} Part of the element is being clipped
4789 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4790 return this.clippedHorizontally
;
4794 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4796 * @return {boolean} Part of the element is being clipped
4798 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4799 return this.clippedVertically
;
4803 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4805 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4806 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4808 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4809 this.idealWidth
= width
;
4810 this.idealHeight
= height
;
4812 if ( !this.clipping
) {
4813 // Update dimensions
4814 this.$clippable
.css( { width
: width
, height
: height
} );
4816 // While clipping, idealWidth and idealHeight are not considered
4820 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4821 * ClippableElement will clip the opposite side when reducing element's width.
4823 * Classes that mix in ClippableElement should override this to return 'right' if their
4824 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
4825 * If your class also mixes in FloatableElement, this is handled automatically.
4827 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4828 * always in pixels, even if they were unset or set to 'auto'.)
4830 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
4832 * @return {string} 'left' or 'right'
4834 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
4835 if ( this.computePosition
&& this.computePosition().right
!== '' ) {
4842 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4843 * ClippableElement will clip the opposite side when reducing element's width.
4845 * Classes that mix in ClippableElement should override this to return 'bottom' if their
4846 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
4847 * If your class also mixes in FloatableElement, this is handled automatically.
4849 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4850 * always in pixels, even if they were unset or set to 'auto'.)
4852 * When in doubt, 'top' is a sane fallback.
4854 * @return {string} 'top' or 'bottom'
4856 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
4857 if ( this.computePosition
&& this.computePosition().bottom
!== '' ) {
4864 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4865 * when the element's natural height changes.
4867 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4868 * overlapped by, the visible area of the nearest scrollable container.
4870 * Because calling clip() when the natural height changes isn't always possible, we also set
4871 * max-height when the element isn't being clipped. This means that if the element tries to grow
4872 * beyond the edge, something reasonable will happen before clip() is called.
4876 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
4877 var extraHeight
, extraWidth
, viewportSpacing
,
4878 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
4879 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
4880 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
4881 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
4882 // Extra tolerance so that the sloppy code below doesn't result in results that are off
4883 // by one or two pixels. (And also so that we have space to display drop shadows.)
4884 // Chosen by fair dice roll.
4887 if ( !this.clipping
) {
4888 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4892 function rectIntersection( a
, b
) {
4894 out
.top
= Math
.max( a
.top
, b
.top
);
4895 out
.left
= Math
.max( a
.left
, b
.left
);
4896 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
4897 out
.right
= Math
.min( a
.right
, b
.right
);
4901 viewportSpacing
= OO
.ui
.getViewportSpacing();
4903 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
4904 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
4905 // Dimensions of the browser window, rather than the element!
4909 right
: document
.documentElement
.clientWidth
,
4910 bottom
: document
.documentElement
.clientHeight
4912 viewportRect
.top
+= viewportSpacing
.top
;
4913 viewportRect
.left
+= viewportSpacing
.left
;
4914 viewportRect
.right
-= viewportSpacing
.right
;
4915 viewportRect
.bottom
-= viewportSpacing
.bottom
;
4917 $viewport
= this.$clippableScrollableContainer
;
4918 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
4919 // Convert into a plain object
4920 viewportRect
= $.extend( {}, viewportRect
);
4923 // Account for scrollbar gutter
4924 direction
= $viewport
.css( 'direction' );
4925 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
4926 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
4927 viewportRect
.bottom
-= horizScrollbarHeight
;
4928 if ( direction
=== 'rtl' ) {
4929 viewportRect
.left
+= vertScrollbarWidth
;
4931 viewportRect
.right
-= vertScrollbarWidth
;
4934 // Add arbitrary tolerance
4935 viewportRect
.top
+= buffer
;
4936 viewportRect
.left
+= buffer
;
4937 viewportRect
.right
-= buffer
;
4938 viewportRect
.bottom
-= buffer
;
4940 $item
= this.$clippableContainer
|| this.$clippable
;
4942 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
4943 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
4945 itemRect
= $item
[ 0 ].getBoundingClientRect();
4946 // Convert into a plain object
4947 itemRect
= $.extend( {}, itemRect
);
4949 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
4950 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
4951 if ( this.getHorizontalAnchorEdge() === 'right' ) {
4952 itemRect
.left
= viewportRect
.left
;
4954 itemRect
.right
= viewportRect
.right
;
4956 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
4957 itemRect
.top
= viewportRect
.top
;
4959 itemRect
.bottom
= viewportRect
.bottom
;
4962 availableRect
= rectIntersection( viewportRect
, itemRect
);
4964 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
4965 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
4966 // It should never be desirable to exceed the dimensions of the browser viewport... right?
4967 desiredWidth
= Math
.min( desiredWidth
,
4968 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
4969 desiredHeight
= Math
.min( desiredHeight
,
4970 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
4971 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
4972 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
4973 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
4974 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
4975 clipWidth
= allotedWidth
< naturalWidth
;
4976 clipHeight
= allotedHeight
< naturalHeight
;
4979 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
4980 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
4981 this.$clippable
.css( 'overflowX', 'scroll' );
4982 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
4983 this.$clippable
.css( {
4984 width
: Math
.max( 0, allotedWidth
),
4988 this.$clippable
.css( {
4990 width
: this.idealWidth
|| '',
4991 maxWidth
: Math
.max( 0, allotedWidth
)
4995 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
4996 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
4997 this.$clippable
.css( 'overflowY', 'scroll' );
4998 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
4999 this.$clippable
.css( {
5000 height
: Math
.max( 0, allotedHeight
),
5004 this.$clippable
.css( {
5006 height
: this.idealHeight
|| '',
5007 maxHeight
: Math
.max( 0, allotedHeight
)
5011 // If we stopped clipping in at least one of the dimensions
5012 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5013 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5016 this.clippedHorizontally
= clipWidth
;
5017 this.clippedVertically
= clipHeight
;
5023 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5024 * By default, each popup has an anchor that points toward its origin.
5025 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
5027 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5030 * // A popup widget.
5031 * var popup = new OO.ui.PopupWidget( {
5032 * $content: $( '<p>Hi there!</p>' ),
5037 * $( 'body' ).append( popup.$element );
5038 * // To display the popup, toggle the visibility to 'true'.
5039 * popup.toggle( true );
5041 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
5044 * @extends OO.ui.Widget
5045 * @mixins OO.ui.mixin.LabelElement
5046 * @mixins OO.ui.mixin.ClippableElement
5047 * @mixins OO.ui.mixin.FloatableElement
5050 * @param {Object} [config] Configuration options
5051 * @cfg {number} [width=320] Width of popup in pixels
5052 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
5053 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5054 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5055 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5056 * of $floatableContainer
5057 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5058 * of $floatableContainer
5059 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5060 * endwards (right/left) to the vertical center of $floatableContainer
5061 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5062 * startwards (left/right) to the vertical center of $floatableContainer
5063 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5064 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
5065 * as possible while still keeping the anchor within the popup;
5066 * if position is before/after, move the popup as far downwards as possible.
5067 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
5068 * as possible while still keeping the anchor within the popup;
5069 * if position in before/after, move the popup as far upwards as possible.
5070 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center
5071 * of the popup with the center of $floatableContainer.
5072 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5073 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5074 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5075 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5076 * desired direction to display the popup without clipping
5077 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5078 * See the [OOjs UI docs on MediaWiki][3] for an example.
5079 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
5080 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
5081 * @cfg {jQuery} [$content] Content to append to the popup's body
5082 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5083 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5084 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5085 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
5087 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
5088 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5090 * @cfg {boolean} [padded=false] Add padding to the popup's body
5092 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5093 // Configuration initialization
5094 config
= config
|| {};
5096 // Parent constructor
5097 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5099 // Properties (must be set before ClippableElement constructor call)
5100 this.$body
= $( '<div>' );
5101 this.$popup
= $( '<div>' );
5103 // Mixin constructors
5104 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5105 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
5106 $clippable
: this.$body
,
5107 $clippableContainer
: this.$popup
5109 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5112 this.$anchor
= $( '<div>' );
5113 // If undefined, will be computed lazily in computePosition()
5114 this.$container
= config
.$container
;
5115 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5116 this.autoClose
= !!config
.autoClose
;
5117 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
5118 this.transitionTimeout
= null;
5119 this.anchored
= false;
5120 this.width
= config
.width
!== undefined ? config
.width
: 320;
5121 this.height
= config
.height
!== undefined ? config
.height
: null;
5122 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
5123 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5126 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5127 this.setAlignment( config
.align
|| 'center' );
5128 this.setPosition( config
.position
|| 'below' );
5129 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5130 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5131 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5133 .addClass( 'oo-ui-popupWidget-popup' )
5134 .append( this.$body
);
5136 .addClass( 'oo-ui-popupWidget' )
5137 .append( this.$popup
, this.$anchor
);
5138 // Move content, which was added to #$element by OO.ui.Widget, to the body
5139 // FIXME This is gross, we should use '$body' or something for the config
5140 if ( config
.$content
instanceof jQuery
) {
5141 this.$body
.append( config
.$content
);
5144 if ( config
.padded
) {
5145 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5148 if ( config
.head
) {
5149 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
5150 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
5151 this.$head
= $( '<div>' )
5152 .addClass( 'oo-ui-popupWidget-head' )
5153 .append( this.$label
, this.closeButton
.$element
);
5154 this.$popup
.prepend( this.$head
);
5157 if ( config
.$footer
) {
5158 this.$footer
= $( '<div>' )
5159 .addClass( 'oo-ui-popupWidget-footer' )
5160 .append( config
.$footer
);
5161 this.$popup
.append( this.$footer
);
5164 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5165 // that reference properties not initialized at that time of parent class construction
5166 // TODO: Find a better way to handle post-constructor setup
5167 this.visible
= false;
5168 this.$element
.addClass( 'oo-ui-element-hidden' );
5173 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5174 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5175 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5176 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5183 * The popup is ready: it is visible and has been positioned and clipped.
5189 * Handles mouse down events.
5192 * @param {MouseEvent} e Mouse down event
5194 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
5197 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5199 this.toggle( false );
5204 * Bind mouse down listener.
5208 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
5209 // Capture clicks outside popup
5210 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
5214 * Handles close button click events.
5218 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5219 if ( this.isVisible() ) {
5220 this.toggle( false );
5225 * Unbind mouse down listener.
5229 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
5230 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
5234 * Handles key down events.
5237 * @param {KeyboardEvent} e Key down event
5239 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5241 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5244 this.toggle( false );
5246 e
.stopPropagation();
5251 * Bind key down listener.
5255 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
5256 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5260 * Unbind key down listener.
5264 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
5265 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5269 * Show, hide, or toggle the visibility of the anchor.
5271 * @param {boolean} [show] Show anchor, omit to toggle
5273 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5274 show
= show
=== undefined ? !this.anchored
: !!show
;
5276 if ( this.anchored
!== show
) {
5278 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5279 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5281 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5282 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5284 this.anchored
= show
;
5289 * Change which edge the anchor appears on.
5291 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5293 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5294 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5295 throw new Error( 'Invalid value for edge: ' + edge
);
5297 if ( this.anchorEdge
!== null ) {
5298 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5300 this.anchorEdge
= edge
;
5301 if ( this.anchored
) {
5302 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5307 * Check if the anchor is visible.
5309 * @return {boolean} Anchor is visible
5311 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5312 return this.anchored
;
5316 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5317 * `.toggle( true )` after its #$element is attached to the DOM.
5319 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5320 * it in the right place and with the right dimensions only work correctly while it is attached.
5321 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5322 * strictly enforced, so currently it only generates a warning in the browser console.
5327 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5328 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5329 show
= show
=== undefined ? !this.isVisible() : !!show
;
5331 change
= show
!== this.isVisible();
5333 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5334 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5335 this.warnedUnattached
= true;
5337 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5338 // Fall back to the parent node if the floatableContainer is not set
5339 this.setFloatableContainer( this.$element
.parent() );
5342 if ( change
&& show
&& this.autoFlip
) {
5343 // Reset auto-flipping before showing the popup again. It's possible we no longer need to flip
5344 // (e.g. if the user scrolled).
5345 this.isAutoFlipped
= false;
5349 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5352 this.togglePositioning( show
&& !!this.$floatableContainer
);
5355 if ( this.autoClose
) {
5356 this.bindMouseDownListener();
5357 this.bindKeyDownListener();
5359 this.updateDimensions();
5360 this.toggleClipping( true );
5362 if ( this.autoFlip
) {
5363 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5364 if ( this.isClippedVertically() ) {
5365 // If opening the popup in the normal direction causes it to be clipped, open
5366 // in the opposite one instead
5367 normalHeight
= this.$element
.height();
5368 this.isAutoFlipped
= !this.isAutoFlipped
;
5370 if ( this.isClippedVertically() ) {
5371 // If that also causes it to be clipped, open in whichever direction
5372 // we have more space
5373 oppositeHeight
= this.$element
.height();
5374 if ( oppositeHeight
< normalHeight
) {
5375 this.isAutoFlipped
= !this.isAutoFlipped
;
5381 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5382 if ( this.isClippedHorizontally() ) {
5383 // If opening the popup in the normal direction causes it to be clipped, open
5384 // in the opposite one instead
5385 normalWidth
= this.$element
.width();
5386 this.isAutoFlipped
= !this.isAutoFlipped
;
5387 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5388 // which causes positioning to be off. Toggle clipping back and fort to work around.
5389 this.toggleClipping( false );
5391 this.toggleClipping( true );
5392 if ( this.isClippedHorizontally() ) {
5393 // If that also causes it to be clipped, open in whichever direction
5394 // we have more space
5395 oppositeWidth
= this.$element
.width();
5396 if ( oppositeWidth
< normalWidth
) {
5397 this.isAutoFlipped
= !this.isAutoFlipped
;
5398 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5399 // which causes positioning to be off. Toggle clipping back and fort to work around.
5400 this.toggleClipping( false );
5402 this.toggleClipping( true );
5409 this.emit( 'ready' );
5411 this.toggleClipping( false );
5412 if ( this.autoClose
) {
5413 this.unbindMouseDownListener();
5414 this.unbindKeyDownListener();
5423 * Set the size of the popup.
5425 * Changing the size may also change the popup's position depending on the alignment.
5427 * @param {number} width Width in pixels
5428 * @param {number} height Height in pixels
5429 * @param {boolean} [transition=false] Use a smooth transition
5432 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5434 this.height
= height
!== undefined ? height
: null;
5435 if ( this.isVisible() ) {
5436 this.updateDimensions( transition
);
5441 * Update the size and position.
5443 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5444 * be called automatically.
5446 * @param {boolean} [transition=false] Use a smooth transition
5449 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5452 // Prevent transition from being interrupted
5453 clearTimeout( this.transitionTimeout
);
5455 // Enable transition
5456 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5462 // Prevent transitioning after transition is complete
5463 this.transitionTimeout
= setTimeout( function () {
5464 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5467 // Prevent transitioning immediately
5468 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5475 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5476 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
, anchorPos
,
5477 anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
, floatablePos
,
5478 offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5480 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5481 popupPositionOppositeMap
= {
5489 'force-left': 'backwards',
5490 'force-right': 'forwards'
5493 'force-left': 'forwards',
5494 'force-right': 'backwards'
5506 backwards
: this.anchored
? 'before' : 'end'
5514 if ( !this.$container
) {
5515 // Lazy-initialize $container if not specified in constructor
5516 this.$container
= $( this.getClosestScrollableElementContainer() );
5518 direction
= this.$container
.css( 'direction' );
5520 // Set height and width before we do anything else, since it might cause our measurements
5521 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5524 height
: this.height
!== null ? this.height
: 'auto'
5527 align
= alignMap
[ direction
][ this.align
] || this.align
;
5528 popupPosition
= this.popupPosition
;
5529 if ( this.isAutoFlipped
) {
5530 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5533 // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
5534 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5535 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5536 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5537 near
= vertical
? 'top' : 'left';
5538 far
= vertical
? 'bottom' : 'right';
5539 sizeProp
= vertical
? 'Height' : 'Width';
5540 popupSize
= vertical
? ( this.height
|| this.$popup
.height() ) : this.width
;
5542 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5543 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5544 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5547 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5548 // Find out which property FloatableElement used for positioning, and adjust that value
5549 positionProp
= vertical
?
5550 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5551 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5553 // Figure out where the near and far edges of the popup and $floatableContainer are
5554 floatablePos
= this.$floatableContainer
.offset();
5555 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5556 // Measure where the offsetParent is and compute our position based on that and parentPosition
5557 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5558 { top
: 0, left
: 0 } :
5559 this.$element
.offsetParent().offset();
5561 if ( positionProp
=== near
) {
5562 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5563 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5565 popupPos
[ far
] = offsetParentPos
[ near
] +
5566 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5567 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5570 if ( this.anchored
) {
5571 // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
5572 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5573 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5575 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
5576 // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
5577 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5578 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5579 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5580 // Not enough space for the anchor on the start side; pull the popup startwards
5581 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5582 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5583 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5584 // Not enough space for the anchor on the end side; pull the popup endwards
5585 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5586 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5588 positionAdjustment
= 0;
5591 positionAdjustment
= 0;
5594 // Check if the popup will go beyond the edge of this.$container
5595 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5596 { top
: 0, left
: 0 } :
5597 this.$container
.offset();
5598 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5599 if ( this.$container
[ 0 ] === document
.documentElement
) {
5600 viewportSpacing
= OO
.ui
.getViewportSpacing();
5601 containerPos
[ near
] += viewportSpacing
[ near
];
5602 containerPos
[ far
] -= viewportSpacing
[ far
];
5604 // Take into account how much the popup will move because of the adjustments we're going to make
5605 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5606 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5607 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5608 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5609 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5610 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5611 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5612 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5613 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5614 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5617 if ( this.anchored
) {
5618 // Adjust anchorOffset for positionAdjustment
5619 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5621 // Position the anchor
5622 anchorCss
[ start
] = anchorOffset
;
5623 this.$anchor
.css( anchorCss
);
5626 // Move the popup if needed
5627 parentPosition
[ positionProp
] += positionAdjustment
;
5629 return parentPosition
;
5633 * Set popup alignment
5635 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5636 * `backwards` or `forwards`.
5638 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5639 // Validate alignment
5640 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5643 this.align
= 'center';
5649 * Get popup alignment
5651 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5652 * `backwards` or `forwards`.
5654 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5659 * Change the positioning of the popup.
5661 * @param {string} position 'above', 'below', 'before' or 'after'
5663 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5664 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5667 this.popupPosition
= position
;
5672 * Get popup positioning.
5674 * @return {string} 'above', 'below', 'before' or 'after'
5676 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5677 return this.popupPosition
;
5681 * Set popup auto-flipping.
5683 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
5684 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5685 * desired direction to display the popup without clipping
5687 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
5688 autoFlip
= !!autoFlip
;
5690 if ( this.autoFlip
!== autoFlip
) {
5691 this.autoFlip
= autoFlip
;
5696 * Get an ID of the body element, this can be used as the
5697 * `aria-describedby` attribute for an input field.
5699 * @return {string} The ID of the body element
5701 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
5702 var id
= this.$body
.attr( 'id' );
5703 if ( id
=== undefined ) {
5704 id
= OO
.ui
.generateElementId();
5705 this.$body
.attr( 'id', id
);
5711 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5712 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5713 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5714 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5720 * @param {Object} [config] Configuration options
5721 * @cfg {Object} [popup] Configuration to pass to popup
5722 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5724 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5725 // Configuration initialization
5726 config
= config
|| {};
5729 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5732 $floatableContainer
: this.$element
5736 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5746 * @return {OO.ui.PopupWidget} Popup widget
5748 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5753 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5754 * which is used to display additional information or options.
5757 * // Example of a popup button.
5758 * var popupButton = new OO.ui.PopupButtonWidget( {
5759 * label: 'Popup button with options',
5762 * $content: $( '<p>Additional options here.</p>' ),
5764 * align: 'force-left'
5767 * // Append the button to the DOM.
5768 * $( 'body' ).append( popupButton.$element );
5771 * @extends OO.ui.ButtonWidget
5772 * @mixins OO.ui.mixin.PopupElement
5775 * @param {Object} [config] Configuration options
5776 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
5777 * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
5778 * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
5779 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
5781 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
5782 // Configuration initialization
5783 config
= config
|| {};
5785 // Parent constructor
5786 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
5788 // Mixin constructors
5789 OO
.ui
.mixin
.PopupElement
.call( this, config
);
5792 this.$overlay
= config
.$overlay
|| this.$element
;
5795 this.connect( this, { click
: 'onAction' } );
5799 .addClass( 'oo-ui-popupButtonWidget' )
5800 .attr( 'aria-haspopup', 'true' );
5802 .addClass( 'oo-ui-popupButtonWidget-popup' )
5803 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
5804 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
5805 this.$overlay
.append( this.popup
.$element
);
5810 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
5811 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
5816 * Handle the button action being triggered.
5820 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
5821 this.popup
.toggle();
5825 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
5827 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
5832 * @mixins OO.ui.mixin.GroupElement
5835 * @param {Object} [config] Configuration options
5837 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
5838 // Mixin constructors
5839 OO
.ui
.mixin
.GroupElement
.call( this, config
);
5844 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
5849 * Set the disabled state of the widget.
5851 * This will also update the disabled state of child widgets.
5853 * @param {boolean} disabled Disable widget
5856 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
5860 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
5861 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
5863 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
5865 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5866 this.items
[ i
].updateDisabled();
5874 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
5876 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
5877 * allows bidirectional communication.
5879 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
5887 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
5894 * Check if widget is disabled.
5896 * Checks parent if present, making disabled state inheritable.
5898 * @return {boolean} Widget is disabled
5900 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
5901 return this.disabled
||
5902 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
5906 * Set group element is in.
5908 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
5911 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
5913 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
5914 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
5916 // Initialize item disabled states
5917 this.updateDisabled();
5923 * OptionWidgets are special elements that can be selected and configured with data. The
5924 * data is often unique for each option, but it does not have to be. OptionWidgets are used
5925 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
5926 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
5928 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5931 * @extends OO.ui.Widget
5932 * @mixins OO.ui.mixin.ItemWidget
5933 * @mixins OO.ui.mixin.LabelElement
5934 * @mixins OO.ui.mixin.FlaggedElement
5935 * @mixins OO.ui.mixin.AccessKeyedElement
5938 * @param {Object} [config] Configuration options
5940 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
5941 // Configuration initialization
5942 config
= config
|| {};
5944 // Parent constructor
5945 OO
.ui
.OptionWidget
.parent
.call( this, config
);
5947 // Mixin constructors
5948 OO
.ui
.mixin
.ItemWidget
.call( this );
5949 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5950 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
5951 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
5954 this.selected
= false;
5955 this.highlighted
= false;
5956 this.pressed
= false;
5960 .data( 'oo-ui-optionWidget', this )
5961 // Allow programmatic focussing (and by accesskey), but not tabbing
5962 .attr( 'tabindex', '-1' )
5963 .attr( 'role', 'option' )
5964 .attr( 'aria-selected', 'false' )
5965 .addClass( 'oo-ui-optionWidget' )
5966 .append( this.$label
);
5971 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
5972 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
5973 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
5974 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
5975 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
5977 /* Static Properties */
5980 * Whether this option can be selected. See #setSelected.
5984 * @property {boolean}
5986 OO
.ui
.OptionWidget
.static.selectable
= true;
5989 * Whether this option can be highlighted. See #setHighlighted.
5993 * @property {boolean}
5995 OO
.ui
.OptionWidget
.static.highlightable
= true;
5998 * Whether this option can be pressed. See #setPressed.
6002 * @property {boolean}
6004 OO
.ui
.OptionWidget
.static.pressable
= true;
6007 * Whether this option will be scrolled into view when it is selected.
6011 * @property {boolean}
6013 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6018 * Check if the option can be selected.
6020 * @return {boolean} Item is selectable
6022 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6023 return this.constructor.static.selectable
&& !this.isDisabled() && this.isVisible();
6027 * Check if the option can be highlighted. A highlight indicates that the option
6028 * may be selected when a user presses enter or clicks. Disabled items cannot
6031 * @return {boolean} Item is highlightable
6033 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6034 return this.constructor.static.highlightable
&& !this.isDisabled() && this.isVisible();
6038 * Check if the option can be pressed. The pressed state occurs when a user mouses
6039 * down on an item, but has not yet let go of the mouse.
6041 * @return {boolean} Item is pressable
6043 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6044 return this.constructor.static.pressable
&& !this.isDisabled() && this.isVisible();
6048 * Check if the option is selected.
6050 * @return {boolean} Item is selected
6052 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6053 return this.selected
;
6057 * Check if the option is highlighted. A highlight indicates that the
6058 * item may be selected when a user presses enter or clicks.
6060 * @return {boolean} Item is highlighted
6062 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6063 return this.highlighted
;
6067 * Check if the option is pressed. The pressed state occurs when a user mouses
6068 * down on an item, but has not yet let go of the mouse. The item may appear
6069 * selected, but it will not be selected until the user releases the mouse.
6071 * @return {boolean} Item is pressed
6073 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6074 return this.pressed
;
6078 * Set the option’s selected state. In general, all modifications to the selection
6079 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6080 * method instead of this method.
6082 * @param {boolean} [state=false] Select option
6085 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6086 if ( this.constructor.static.selectable
) {
6087 this.selected
= !!state
;
6089 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6090 .attr( 'aria-selected', state
.toString() );
6091 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6092 this.scrollElementIntoView();
6094 this.updateThemeClasses();
6100 * Set the option’s highlighted state. In general, all programmatic
6101 * modifications to the highlight should be handled by the
6102 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6103 * method instead of this method.
6105 * @param {boolean} [state=false] Highlight option
6108 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6109 if ( this.constructor.static.highlightable
) {
6110 this.highlighted
= !!state
;
6111 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6112 this.updateThemeClasses();
6118 * Set the option’s pressed state. In general, all
6119 * programmatic modifications to the pressed state should be handled by the
6120 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6121 * method instead of this method.
6123 * @param {boolean} [state=false] Press option
6126 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6127 if ( this.constructor.static.pressable
) {
6128 this.pressed
= !!state
;
6129 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6130 this.updateThemeClasses();
6136 * Get text to match search strings against.
6138 * The default implementation returns the label text, but subclasses
6139 * can override this to provide more complex behavior.
6141 * @return {string|boolean} String to match search string against
6143 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6144 var label
= this.getLabel();
6145 return typeof label
=== 'string' ? label
: this.$label
.text();
6149 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
6150 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6151 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6154 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
6155 * information, please see the [OOjs UI documentation on MediaWiki][1].
6158 * // Example of a select widget with three options
6159 * var select = new OO.ui.SelectWidget( {
6161 * new OO.ui.OptionWidget( {
6163 * label: 'Option One',
6165 * new OO.ui.OptionWidget( {
6167 * label: 'Option Two',
6169 * new OO.ui.OptionWidget( {
6171 * label: 'Option Three',
6175 * $( 'body' ).append( select.$element );
6177 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6181 * @extends OO.ui.Widget
6182 * @mixins OO.ui.mixin.GroupWidget
6185 * @param {Object} [config] Configuration options
6186 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6187 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6188 * the [OOjs UI documentation on MediaWiki] [2] for examples.
6189 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6191 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6192 // Configuration initialization
6193 config
= config
|| {};
6195 // Parent constructor
6196 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6198 // Mixin constructors
6199 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
6202 this.pressed
= false;
6203 this.selecting
= null;
6204 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
6205 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
6206 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
6207 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
6208 this.keyPressBuffer
= '';
6209 this.keyPressBufferTimer
= null;
6210 this.blockMouseOverEvents
= 0;
6213 this.connect( this, {
6217 focusin
: this.onFocus
.bind( this ),
6218 mousedown
: this.onMouseDown
.bind( this ),
6219 mouseover
: this.onMouseOver
.bind( this ),
6220 mouseleave
: this.onMouseLeave
.bind( this )
6225 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
6226 .attr( 'role', 'listbox' );
6227 this.setFocusOwner( this.$element
);
6228 if ( Array
.isArray( config
.items
) ) {
6229 this.addItems( config
.items
);
6235 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6236 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6243 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6245 * @param {OO.ui.OptionWidget|null} item Highlighted item
6251 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6252 * pressed state of an option.
6254 * @param {OO.ui.OptionWidget|null} item Pressed item
6260 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
6262 * @param {OO.ui.OptionWidget|null} item Selected item
6267 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6268 * @param {OO.ui.OptionWidget} item Chosen item
6274 * An `add` event is emitted when options are added to the select with the #addItems method.
6276 * @param {OO.ui.OptionWidget[]} items Added items
6277 * @param {number} index Index of insertion point
6283 * A `remove` event is emitted when options are removed from the select with the #clearItems
6284 * or #removeItems methods.
6286 * @param {OO.ui.OptionWidget[]} items Removed items
6292 * Handle focus events
6295 * @param {jQuery.Event} event
6297 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6299 if ( event
.target
=== this.$element
[ 0 ] ) {
6300 // This widget was focussed, e.g. by the user tabbing to it.
6301 // The styles for focus state depend on one of the items being selected.
6302 if ( !this.getSelectedItem() ) {
6303 item
= this.findFirstSelectableItem();
6306 if ( event
.target
.tabIndex
=== -1 ) {
6307 // One of the options got focussed (and the event bubbled up here).
6308 // They can't be tabbed to, but they can be activated using accesskeys.
6309 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6310 item
= this.findTargetItem( event
);
6312 // There is something actually user-focusable in one of the labels of the options, and the
6313 // user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change the focus).
6319 if ( item
.constructor.static.highlightable
) {
6320 this.highlightItem( item
);
6322 this.selectItem( item
);
6326 if ( event
.target
!== this.$element
[ 0 ] ) {
6327 this.$focusOwner
.focus();
6332 * Handle mouse down events.
6335 * @param {jQuery.Event} e Mouse down event
6337 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6340 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6341 this.togglePressed( true );
6342 item
= this.findTargetItem( e
);
6343 if ( item
&& item
.isSelectable() ) {
6344 this.pressItem( item
);
6345 this.selecting
= item
;
6346 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
6347 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
6354 * Handle mouse up events.
6357 * @param {MouseEvent} e Mouse up event
6359 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
6362 this.togglePressed( false );
6363 if ( !this.selecting
) {
6364 item
= this.findTargetItem( e
);
6365 if ( item
&& item
.isSelectable() ) {
6366 this.selecting
= item
;
6369 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6370 this.pressItem( null );
6371 this.chooseItem( this.selecting
);
6372 this.selecting
= null;
6375 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
6376 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
6382 * Handle mouse move events.
6385 * @param {MouseEvent} e Mouse move event
6387 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
6390 if ( !this.isDisabled() && this.pressed
) {
6391 item
= this.findTargetItem( e
);
6392 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6393 this.pressItem( item
);
6394 this.selecting
= item
;
6400 * Handle mouse over events.
6403 * @param {jQuery.Event} e Mouse over event
6405 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6407 if ( this.blockMouseOverEvents
) {
6410 if ( !this.isDisabled() ) {
6411 item
= this.findTargetItem( e
);
6412 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6418 * Handle mouse leave events.
6421 * @param {jQuery.Event} e Mouse over event
6423 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6424 if ( !this.isDisabled() ) {
6425 this.highlightItem( null );
6431 * Handle key down events.
6434 * @param {KeyboardEvent} e Key down event
6436 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
6439 currentItem
= this.findHighlightedItem() || this.getSelectedItem();
6441 if ( !this.isDisabled() && this.isVisible() ) {
6442 switch ( e
.keyCode
) {
6443 case OO
.ui
.Keys
.ENTER
:
6444 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6445 // Was only highlighted, now let's select it. No-op if already selected.
6446 this.chooseItem( currentItem
);
6451 case OO
.ui
.Keys
.LEFT
:
6452 this.clearKeyPressBuffer();
6453 nextItem
= this.findRelativeSelectableItem( currentItem
, -1 );
6456 case OO
.ui
.Keys
.DOWN
:
6457 case OO
.ui
.Keys
.RIGHT
:
6458 this.clearKeyPressBuffer();
6459 nextItem
= this.findRelativeSelectableItem( currentItem
, 1 );
6462 case OO
.ui
.Keys
.ESCAPE
:
6463 case OO
.ui
.Keys
.TAB
:
6464 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6465 currentItem
.setHighlighted( false );
6467 this.unbindKeyDownListener();
6468 this.unbindKeyPressListener();
6469 // Don't prevent tabbing away / defocusing
6475 if ( nextItem
.constructor.static.highlightable
) {
6476 this.highlightItem( nextItem
);
6478 this.chooseItem( nextItem
);
6480 this.scrollItemIntoView( nextItem
);
6485 e
.stopPropagation();
6491 * Bind key down listener.
6495 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
6496 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
6500 * Unbind key down listener.
6504 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
6505 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
6509 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6511 * @param {OO.ui.OptionWidget} item Item to scroll into view
6513 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6515 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
6516 // and around 100-150 ms after it is finished.
6517 this.blockMouseOverEvents
++;
6518 item
.scrollElementIntoView().done( function () {
6519 setTimeout( function () {
6520 widget
.blockMouseOverEvents
--;
6526 * Clear the key-press buffer
6530 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6531 if ( this.keyPressBufferTimer
) {
6532 clearTimeout( this.keyPressBufferTimer
);
6533 this.keyPressBufferTimer
= null;
6535 this.keyPressBuffer
= '';
6539 * Handle key press events.
6542 * @param {KeyboardEvent} e Key press event
6544 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
6545 var c
, filter
, item
;
6547 if ( !e
.charCode
) {
6548 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6549 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6554 if ( String
.fromCodePoint
) {
6555 c
= String
.fromCodePoint( e
.charCode
);
6557 c
= String
.fromCharCode( e
.charCode
);
6560 if ( this.keyPressBufferTimer
) {
6561 clearTimeout( this.keyPressBufferTimer
);
6563 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6565 item
= this.findHighlightedItem() || this.getSelectedItem();
6567 if ( this.keyPressBuffer
=== c
) {
6568 // Common (if weird) special case: typing "xxxx" will cycle through all
6569 // the items beginning with "x".
6571 item
= this.findRelativeSelectableItem( item
, 1 );
6574 this.keyPressBuffer
+= c
;
6577 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6578 if ( !item
|| !filter( item
) ) {
6579 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6582 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6583 this.highlightItem( item
);
6585 this.chooseItem( item
);
6587 this.scrollItemIntoView( item
);
6591 e
.stopPropagation();
6595 * Get a matcher for the specific string
6598 * @param {string} s String to match against items
6599 * @param {boolean} [exact=false] Only accept exact matches
6600 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6602 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
6605 if ( s
.normalize
) {
6608 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
6609 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
6613 re
= new RegExp( re
, 'i' );
6614 return function ( item
) {
6615 var matchText
= item
.getMatchText();
6616 if ( matchText
.normalize
) {
6617 matchText
= matchText
.normalize();
6619 return re
.test( matchText
);
6624 * Bind key press listener.
6628 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
6629 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
6633 * Unbind key down listener.
6635 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6640 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
6641 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
6642 this.clearKeyPressBuffer();
6646 * Visibility change handler
6649 * @param {boolean} visible
6651 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6653 this.clearKeyPressBuffer();
6658 * Get the closest item to a jQuery.Event.
6661 * @param {jQuery.Event} e
6662 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6664 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
6665 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
6666 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
6669 return $option
.data( 'oo-ui-optionWidget' ) || null;
6673 * Get selected item.
6675 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6677 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
6680 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6681 if ( this.items
[ i
].isSelected() ) {
6682 return this.items
[ i
];
6689 * Find highlighted item.
6691 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6693 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
6696 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6697 if ( this.items
[ i
].isHighlighted() ) {
6698 return this.items
[ i
];
6705 * Get highlighted item.
6707 * @deprecated 0.23.1 Use {@link #findHighlightedItem} instead.
6708 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6710 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
6711 OO
.ui
.warnDeprecation( 'SelectWidget#getHighlightedItem: Deprecated function. Use findHighlightedItem instead. See T76630.' );
6712 return this.findHighlightedItem();
6716 * Toggle pressed state.
6718 * Press is a state that occurs when a user mouses down on an item, but
6719 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
6720 * until the user releases the mouse.
6722 * @param {boolean} pressed An option is being pressed
6724 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
6725 if ( pressed
=== undefined ) {
6726 pressed
= !this.pressed
;
6728 if ( pressed
!== this.pressed
) {
6730 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
6731 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
6732 this.pressed
= pressed
;
6737 * Highlight an option. If the `item` param is omitted, no options will be highlighted
6738 * and any existing highlight will be removed. The highlight is mutually exclusive.
6740 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
6744 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
6745 var i
, len
, highlighted
,
6748 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6749 highlighted
= this.items
[ i
] === item
;
6750 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
6751 this.items
[ i
].setHighlighted( highlighted
);
6757 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
6759 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
6761 this.emit( 'highlight', item
);
6768 * Fetch an item by its label.
6770 * @param {string} label Label of the item to select.
6771 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6772 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
6774 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
6776 len
= this.items
.length
,
6777 filter
= this.getItemMatcher( label
, true );
6779 for ( i
= 0; i
< len
; i
++ ) {
6780 item
= this.items
[ i
];
6781 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6788 filter
= this.getItemMatcher( label
, false );
6789 for ( i
= 0; i
< len
; i
++ ) {
6790 item
= this.items
[ i
];
6791 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6807 * Programmatically select an option by its label. If the item does not exist,
6808 * all options will be deselected.
6810 * @param {string} [label] Label of the item to select.
6811 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6815 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
6816 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
6817 if ( label
=== undefined || !itemFromLabel
) {
6818 return this.selectItem();
6820 return this.selectItem( itemFromLabel
);
6824 * Programmatically select an option by its data. If the `data` parameter is omitted,
6825 * or if the item does not exist, all options will be deselected.
6827 * @param {Object|string} [data] Value of the item to select, omit to deselect all
6831 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
6832 var itemFromData
= this.getItemFromData( data
);
6833 if ( data
=== undefined || !itemFromData
) {
6834 return this.selectItem();
6836 return this.selectItem( itemFromData
);
6840 * Programmatically select an option by its reference. If the `item` parameter is omitted,
6841 * all options will be deselected.
6843 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
6847 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
6848 var i
, len
, selected
,
6851 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6852 selected
= this.items
[ i
] === item
;
6853 if ( this.items
[ i
].isSelected() !== selected
) {
6854 this.items
[ i
].setSelected( selected
);
6859 if ( item
&& !item
.constructor.static.highlightable
) {
6861 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
6863 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
6866 this.emit( 'select', item
);
6875 * Press is a state that occurs when a user mouses down on an item, but has not
6876 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
6877 * releases the mouse.
6879 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
6883 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
6884 var i
, len
, pressed
,
6887 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6888 pressed
= this.items
[ i
] === item
;
6889 if ( this.items
[ i
].isPressed() !== pressed
) {
6890 this.items
[ i
].setPressed( pressed
);
6895 this.emit( 'press', item
);
6904 * Note that ‘choose’ should never be modified programmatically. A user can choose
6905 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
6906 * use the #selectItem method.
6908 * This method is identical to #selectItem, but may vary in subclasses that take additional action
6909 * when users choose an item with the keyboard or mouse.
6911 * @param {OO.ui.OptionWidget} item Item to choose
6915 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
6917 this.selectItem( item
);
6918 this.emit( 'choose', item
);
6925 * Find an option by its position relative to the specified item (or to the start of the option array,
6926 * if item is `null`). The direction in which to search through the option array is specified with a
6927 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6928 * `null` if there are no options in the array.
6930 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6931 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6932 * @param {Function} [filter] Only consider items for which this function returns
6933 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
6934 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
6936 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
6937 var currentIndex
, nextIndex
, i
,
6938 increase
= direction
> 0 ? 1 : -1,
6939 len
= this.items
.length
;
6941 if ( item
instanceof OO
.ui
.OptionWidget
) {
6942 currentIndex
= this.items
.indexOf( item
);
6943 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
6945 // If no item is selected and moving forward, start at the beginning.
6946 // If moving backward, start at the end.
6947 nextIndex
= direction
> 0 ? 0 : len
- 1;
6950 for ( i
= 0; i
< len
; i
++ ) {
6951 item
= this.items
[ nextIndex
];
6953 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
6954 ( !filter
|| filter( item
) )
6958 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
6964 * Get an option by its position relative to the specified item (or to the start of the option array,
6965 * if item is `null`). The direction in which to search through the option array is specified with a
6966 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6967 * `null` if there are no options in the array.
6969 * @deprecated 0.23.1 Use {@link #findRelativeSelectableItem} instead
6970 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6971 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6972 * @param {Function} [filter] Only consider items for which this function returns
6973 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
6974 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
6976 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
, filter
) {
6977 OO
.ui
.warnDeprecation( 'SelectWidget#getRelativeSelectableItem: Deprecated function. Use findRelativeSelectableItem instead. See T76630.' );
6978 return this.findRelativeSelectableItem( item
, direction
, filter
);
6982 * Find the next selectable item or `null` if there are no selectable items.
6983 * Disabled options and menu-section markers and breaks are not selectable.
6985 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
6987 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
6988 return this.findRelativeSelectableItem( null, 1 );
6992 * Get the next selectable item or `null` if there are no selectable items.
6993 * Disabled options and menu-section markers and breaks are not selectable.
6995 * @deprecated 0.23.1 Use {@link OO.ui.SelectWidget#findFirstSelectableItem} instead.
6996 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
6998 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
6999 OO
.ui
.warnDeprecation( 'SelectWidget#getFirstSelectableItem: Deprecated function. Use findFirstSelectableItem instead. See T76630.' );
7000 return this.findFirstSelectableItem();
7004 * Add an array of options to the select. Optionally, an index number can be used to
7005 * specify an insertion point.
7007 * @param {OO.ui.OptionWidget[]} items Items to add
7008 * @param {number} [index] Index to insert items after
7012 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7014 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7016 // Always provide an index, even if it was omitted
7017 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7023 * Remove the specified array of options from the select. Options will be detached
7024 * from the DOM, not removed, so they can be reused later. To remove all options from
7025 * the select, you may wish to use the #clearItems method instead.
7027 * @param {OO.ui.OptionWidget[]} items Items to remove
7031 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7034 // Deselect items being removed
7035 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7037 if ( item
.isSelected() ) {
7038 this.selectItem( null );
7043 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7045 this.emit( 'remove', items
);
7051 * Clear all options from the select. Options will be detached from the DOM, not removed,
7052 * so that they can be reused later. To remove a subset of options from the select, use
7053 * the #removeItems method.
7058 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7059 var items
= this.items
.slice();
7062 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7065 this.selectItem( null );
7067 this.emit( 'remove', items
);
7073 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7075 * Currently this is just used to set `aria-activedescendant` on it.
7078 * @param {jQuery} $focusOwner
7080 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7081 this.$focusOwner
= $focusOwner
;
7085 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7086 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
7087 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7088 * options. For more information about options and selects, please see the
7089 * [OOjs UI documentation on MediaWiki][1].
7092 * // Decorated options in a select widget
7093 * var select = new OO.ui.SelectWidget( {
7095 * new OO.ui.DecoratedOptionWidget( {
7097 * label: 'Option with icon',
7100 * new OO.ui.DecoratedOptionWidget( {
7102 * label: 'Option with indicator',
7107 * $( 'body' ).append( select.$element );
7109 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
7112 * @extends OO.ui.OptionWidget
7113 * @mixins OO.ui.mixin.IconElement
7114 * @mixins OO.ui.mixin.IndicatorElement
7117 * @param {Object} [config] Configuration options
7119 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7120 // Parent constructor
7121 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7123 // Mixin constructors
7124 OO
.ui
.mixin
.IconElement
.call( this, config
);
7125 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7129 .addClass( 'oo-ui-decoratedOptionWidget' )
7130 .prepend( this.$icon
)
7131 .append( this.$indicator
);
7136 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7137 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7138 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7141 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7142 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7143 * the [OOjs UI documentation on MediaWiki] [1] for more information.
7145 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
7148 * @extends OO.ui.DecoratedOptionWidget
7151 * @param {Object} [config] Configuration options
7153 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7154 // Parent constructor
7155 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7158 this.$element
.addClass( 'oo-ui-menuOptionWidget' );
7163 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7165 /* Static Properties */
7171 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7174 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
7175 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
7178 * var myDropdown = new OO.ui.DropdownWidget( {
7181 * new OO.ui.MenuSectionOptionWidget( {
7184 * new OO.ui.MenuOptionWidget( {
7186 * label: 'Welsh Corgi'
7188 * new OO.ui.MenuOptionWidget( {
7190 * label: 'Standard Poodle'
7192 * new OO.ui.MenuSectionOptionWidget( {
7195 * new OO.ui.MenuOptionWidget( {
7202 * $( 'body' ).append( myDropdown.$element );
7205 * @extends OO.ui.DecoratedOptionWidget
7208 * @param {Object} [config] Configuration options
7210 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7211 // Parent constructor
7212 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7215 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' )
7216 .removeAttr( 'role aria-selected' );
7221 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7223 /* Static Properties */
7229 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7235 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7238 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7239 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7240 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
7241 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7242 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7243 * and customized to be opened, closed, and displayed as needed.
7245 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7246 * mouse outside the menu.
7248 * Menus also have support for keyboard interaction:
7250 * - Enter/Return key: choose and select a menu option
7251 * - Up-arrow key: highlight the previous menu option
7252 * - Down-arrow key: highlight the next menu option
7253 * - Esc key: hide the menu
7255 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7257 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
7258 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
7261 * @extends OO.ui.SelectWidget
7262 * @mixins OO.ui.mixin.ClippableElement
7263 * @mixins OO.ui.mixin.FloatableElement
7266 * @param {Object} [config] Configuration options
7267 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
7268 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
7269 * and {@link OO.ui.mixin.LookupElement LookupElement}
7270 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7271 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
7272 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
7273 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
7274 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
7275 * that button, unless the button (or its parent widget) is passed in here.
7276 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7277 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7278 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7279 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7280 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7281 * @cfg {number} [width] Width of the menu
7283 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7284 // Configuration initialization
7285 config
= config
|| {};
7287 // Parent constructor
7288 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7290 // Mixin constructors
7291 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
7292 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7295 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7296 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7297 this.filterFromInput
= !!config
.filterFromInput
;
7298 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7299 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7300 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7301 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7302 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7303 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7304 this.width
= config
.width
;
7307 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7308 if ( config
.widget
) {
7309 this.setFocusOwner( config
.widget
.$tabIndexed
);
7312 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7313 // that reference properties not initialized at that time of parent class construction
7314 // TODO: Find a better way to handle post-constructor setup
7315 this.visible
= false;
7316 this.$element
.addClass( 'oo-ui-element-hidden' );
7321 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7322 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7323 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7330 * The menu is ready: it is visible and has been positioned and clipped.
7336 * Handles document mouse down events.
7339 * @param {MouseEvent} e Mouse down event
7341 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7345 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7350 this.toggle( false );
7357 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
7358 var currentItem
= this.findHighlightedItem() || this.getSelectedItem();
7360 if ( !this.isDisabled() && this.isVisible() ) {
7361 switch ( e
.keyCode
) {
7362 case OO
.ui
.Keys
.LEFT
:
7363 case OO
.ui
.Keys
.RIGHT
:
7364 // Do nothing if a text field is associated, arrow keys will be handled natively
7365 if ( !this.$input
) {
7366 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
7369 case OO
.ui
.Keys
.ESCAPE
:
7370 case OO
.ui
.Keys
.TAB
:
7371 if ( currentItem
) {
7372 currentItem
.setHighlighted( false );
7374 this.toggle( false );
7375 // Don't prevent tabbing away, prevent defocusing
7376 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7378 e
.stopPropagation();
7382 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
7389 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7390 * or after items were added/removed (always).
7394 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7395 var i
, item
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7396 firstItemFound
= false,
7398 len
= this.items
.length
,
7399 showAll
= !this.isVisible(),
7402 if ( this.$input
&& this.filterFromInput
) {
7403 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
7404 exactFilter
= this.getItemMatcher( this.$input
.val(), true );
7406 // Hide non-matching options, and also hide section headers if all options
7407 // in their section are hidden.
7408 for ( i
= 0; i
< len
; i
++ ) {
7409 item
= this.items
[ i
];
7410 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7412 // If the previous section was empty, hide its header
7413 section
.toggle( showAll
|| !sectionEmpty
);
7416 sectionEmpty
= true;
7417 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7418 visible
= showAll
|| filter( item
);
7419 exactMatch
= exactMatch
|| exactFilter( item
);
7420 anyVisible
= anyVisible
|| visible
;
7421 sectionEmpty
= sectionEmpty
&& !visible
;
7422 item
.toggle( visible
);
7423 if ( this.highlightOnFilter
&& visible
&& !firstItemFound
) {
7424 // Highlight the first item in the list
7425 this.highlightItem( item
);
7426 firstItemFound
= true;
7430 // Process the final section
7432 section
.toggle( showAll
|| !sectionEmpty
);
7435 if ( anyVisible
&& this.items
.length
&& !exactMatch
) {
7436 this.scrollItemIntoView( this.items
[ 0 ] );
7439 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7442 // Reevaluate clipping
7449 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
7450 if ( this.$input
) {
7451 this.$input
.on( 'keydown', this.onKeyDownHandler
);
7453 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
7460 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
7461 if ( this.$input
) {
7462 this.$input
.off( 'keydown', this.onKeyDownHandler
);
7464 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
7471 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
7472 if ( this.$input
) {
7473 if ( this.filterFromInput
) {
7474 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7475 this.updateItemVisibility();
7478 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
7485 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
7486 if ( this.$input
) {
7487 if ( this.filterFromInput
) {
7488 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7489 this.updateItemVisibility();
7492 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
7499 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
7501 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
7502 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
7504 * @param {OO.ui.OptionWidget} item Item to choose
7507 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7508 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7509 if ( this.hideOnChoose
) {
7510 this.toggle( false );
7518 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7520 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7522 this.updateItemVisibility();
7530 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7532 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7534 this.updateItemVisibility();
7542 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7544 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
7546 this.updateItemVisibility();
7552 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7553 * `.toggle( true )` after its #$element is attached to the DOM.
7555 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7556 * it in the right place and with the right dimensions only work correctly while it is attached.
7557 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7558 * strictly enforced, so currently it only generates a warning in the browser console.
7563 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7564 var change
, belowHeight
, aboveHeight
;
7566 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7567 change
= visible
!== this.isVisible();
7569 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7570 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7571 this.warnedUnattached
= true;
7575 if ( visible
&& ( this.width
|| this.$floatableContainer
) ) {
7576 this.setIdealSize( this.width
|| this.$floatableContainer
.width() );
7579 // Reset position before showing the popup again. It's possible we no longer need to flip
7580 // (e.g. if the user scrolled).
7581 this.setVerticalPosition( 'below' );
7586 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7590 this.bindKeyDownListener();
7591 this.bindKeyPressListener();
7593 this.togglePositioning( !!this.$floatableContainer
);
7594 this.toggleClipping( true );
7596 if ( this.isClippedVertically() ) {
7597 // If opening the menu downwards causes it to be clipped, flip it to open upwards instead
7598 belowHeight
= this.$element
.height();
7599 this.setVerticalPosition( 'above' );
7600 if ( this.isClippedVertically() ) {
7601 // If opening upwards also causes it to be clipped, flip it to open in whichever direction
7602 // we have more space
7603 aboveHeight
= this.$element
.height();
7604 if ( aboveHeight
< belowHeight
) {
7605 this.setVerticalPosition( 'below' );
7609 // Note that we do not flip the menu's opening direction if the clipping changes
7610 // later (e.g. after the user scrolls), that seems like it would be annoying
7612 this.$focusOwner
.attr( 'aria-expanded', 'true' );
7614 if ( this.getSelectedItem() ) {
7615 this.$focusOwner
.attr( 'aria-activedescendant', this.getSelectedItem().getElementId() );
7616 this.getSelectedItem().scrollElementIntoView( { duration
: 0 } );
7620 if ( this.autoHide
) {
7621 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7624 this.emit( 'ready' );
7626 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7627 this.unbindKeyDownListener();
7628 this.unbindKeyPressListener();
7629 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7630 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7631 this.togglePositioning( false );
7632 this.toggleClipping( false );
7640 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
7641 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
7642 * users can interact with it.
7644 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7645 * OO.ui.DropdownInputWidget instead.
7648 * // Example: A DropdownWidget with a menu that contains three options
7649 * var dropDown = new OO.ui.DropdownWidget( {
7650 * label: 'Dropdown menu: Select a menu option',
7653 * new OO.ui.MenuOptionWidget( {
7657 * new OO.ui.MenuOptionWidget( {
7661 * new OO.ui.MenuOptionWidget( {
7669 * $( 'body' ).append( dropDown.$element );
7671 * dropDown.getMenu().selectItemByData( 'b' );
7673 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
7675 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
7677 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
7680 * @extends OO.ui.Widget
7681 * @mixins OO.ui.mixin.IconElement
7682 * @mixins OO.ui.mixin.IndicatorElement
7683 * @mixins OO.ui.mixin.LabelElement
7684 * @mixins OO.ui.mixin.TitledElement
7685 * @mixins OO.ui.mixin.TabIndexedElement
7688 * @param {Object} [config] Configuration options
7689 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.MenuSelectWidget menu select widget}
7690 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
7691 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
7692 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
7693 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
7695 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
7696 // Configuration initialization
7697 config
= $.extend( { indicator
: 'down' }, config
);
7699 // Parent constructor
7700 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
7702 // Properties (must be set before TabIndexedElement constructor call)
7703 this.$handle
= this.$( '<span>' );
7704 this.$overlay
= config
.$overlay
|| this.$element
;
7706 // Mixin constructors
7707 OO
.ui
.mixin
.IconElement
.call( this, config
);
7708 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7709 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7710 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
7711 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
7714 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
7716 $floatableContainer
: this.$element
7721 click
: this.onClick
.bind( this ),
7722 keydown
: this.onKeyDown
.bind( this ),
7723 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
7724 keypress
: this.menu
.onKeyPressHandler
,
7725 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
7727 this.menu
.connect( this, {
7728 select
: 'onMenuSelect',
7729 toggle
: 'onMenuToggle'
7734 .addClass( 'oo-ui-dropdownWidget-handle' )
7737 'aria-owns': this.menu
.getElementId(),
7738 'aria-autocomplete': 'list'
7740 .append( this.$icon
, this.$label
, this.$indicator
);
7742 .addClass( 'oo-ui-dropdownWidget' )
7743 .append( this.$handle
);
7744 this.$overlay
.append( this.menu
.$element
);
7749 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
7750 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
7751 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
7752 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
7753 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
7754 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7761 * @return {OO.ui.MenuSelectWidget} Menu of widget
7763 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
7768 * Handles menu select events.
7771 * @param {OO.ui.MenuOptionWidget} item Selected menu item
7773 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
7777 this.setLabel( null );
7781 selectedLabel
= item
.getLabel();
7783 // If the label is a DOM element, clone it, because setLabel will append() it
7784 if ( selectedLabel
instanceof jQuery
) {
7785 selectedLabel
= selectedLabel
.clone();
7788 this.setLabel( selectedLabel
);
7792 * Handle menu toggle events.
7795 * @param {boolean} isVisible Open state of the menu
7797 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
7798 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
7801 this.$element
.hasClass( 'oo-ui-dropdownWidget-open' ).toString()
7806 * Handle mouse click events.
7809 * @param {jQuery.Event} e Mouse click event
7811 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
7812 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7819 * Handle key down events.
7822 * @param {jQuery.Event} e Key down event
7824 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
7826 !this.isDisabled() &&
7828 e
.which
=== OO
.ui
.Keys
.ENTER
||
7830 e
.which
=== OO
.ui
.Keys
.SPACE
&&
7831 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
7832 // Space only closes the menu is the user is not typing to search.
7833 this.menu
.keyPressBuffer
=== ''
7836 !this.menu
.isVisible() &&
7838 e
.which
=== OO
.ui
.Keys
.UP
||
7839 e
.which
=== OO
.ui
.Keys
.DOWN
7850 * RadioOptionWidget is an option widget that looks like a radio button.
7851 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
7852 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
7854 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
7857 * @extends OO.ui.OptionWidget
7860 * @param {Object} [config] Configuration options
7862 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
7863 // Configuration initialization
7864 config
= config
|| {};
7866 // Properties (must be done before parent constructor which calls #setDisabled)
7867 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
7869 // Parent constructor
7870 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
7873 // Remove implicit role, we're handling it ourselves
7874 this.radio
.$input
.attr( 'role', 'presentation' );
7876 .addClass( 'oo-ui-radioOptionWidget' )
7877 .attr( 'role', 'radio' )
7878 .attr( 'aria-checked', 'false' )
7879 .removeAttr( 'aria-selected' )
7880 .prepend( this.radio
.$element
);
7885 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
7887 /* Static Properties */
7893 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
7899 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
7905 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
7911 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
7918 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
7919 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
7921 this.radio
.setSelected( state
);
7923 .attr( 'aria-checked', state
.toString() )
7924 .removeAttr( 'aria-selected' );
7932 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
7933 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
7935 this.radio
.setDisabled( this.isDisabled() );
7941 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
7942 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
7943 * an interface for adding, removing and selecting options.
7944 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
7946 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7947 * OO.ui.RadioSelectInputWidget instead.
7950 * // A RadioSelectWidget with RadioOptions.
7951 * var option1 = new OO.ui.RadioOptionWidget( {
7953 * label: 'Selected radio option'
7956 * var option2 = new OO.ui.RadioOptionWidget( {
7958 * label: 'Unselected radio option'
7961 * var radioSelect=new OO.ui.RadioSelectWidget( {
7962 * items: [ option1, option2 ]
7965 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
7966 * radioSelect.selectItem( option1 );
7968 * $( 'body' ).append( radioSelect.$element );
7970 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
7974 * @extends OO.ui.SelectWidget
7975 * @mixins OO.ui.mixin.TabIndexedElement
7978 * @param {Object} [config] Configuration options
7980 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
7981 // Parent constructor
7982 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
7984 // Mixin constructors
7985 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
7989 focus
: this.bindKeyDownListener
.bind( this ),
7990 blur
: this.unbindKeyDownListener
.bind( this )
7995 .addClass( 'oo-ui-radioSelectWidget' )
7996 .attr( 'role', 'radiogroup' );
8001 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8002 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8005 * MultioptionWidgets are special elements that can be selected and configured with data. The
8006 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8007 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8008 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
8010 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Multioptions
8013 * @extends OO.ui.Widget
8014 * @mixins OO.ui.mixin.ItemWidget
8015 * @mixins OO.ui.mixin.LabelElement
8018 * @param {Object} [config] Configuration options
8019 * @cfg {boolean} [selected=false] Whether the option is initially selected
8021 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8022 // Configuration initialization
8023 config
= config
|| {};
8025 // Parent constructor
8026 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8028 // Mixin constructors
8029 OO
.ui
.mixin
.ItemWidget
.call( this );
8030 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8033 this.selected
= null;
8037 .addClass( 'oo-ui-multioptionWidget' )
8038 .append( this.$label
);
8039 this.setSelected( config
.selected
);
8044 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8045 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8046 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8053 * A change event is emitted when the selected state of the option changes.
8055 * @param {boolean} selected Whether the option is now selected
8061 * Check if the option is selected.
8063 * @return {boolean} Item is selected
8065 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8066 return this.selected
;
8070 * Set the option’s selected state. In general, all modifications to the selection
8071 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
8072 * method instead of this method.
8074 * @param {boolean} [state=false] Select option
8077 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8079 if ( this.selected
!== state
) {
8080 this.selected
= state
;
8081 this.emit( 'change', state
);
8082 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8088 * MultiselectWidget allows selecting multiple options from a list.
8090 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
8092 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
8096 * @extends OO.ui.Widget
8097 * @mixins OO.ui.mixin.GroupWidget
8100 * @param {Object} [config] Configuration options
8101 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8103 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8104 // Parent constructor
8105 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8107 // Configuration initialization
8108 config
= config
|| {};
8110 // Mixin constructors
8111 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8114 this.aggregate( { change
: 'select' } );
8115 // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
8116 // by GroupElement only when items are added/removed
8117 this.connect( this, { select
: [ 'emit', 'change' ] } );
8120 if ( config
.items
) {
8121 this.addItems( config
.items
);
8123 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8124 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8125 .append( this.$group
);
8130 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8131 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8138 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8144 * A select event is emitted when an item is selected or deselected.
8150 * Get options that are selected.
8152 * @return {OO.ui.MultioptionWidget[]} Selected options
8154 OO
.ui
.MultiselectWidget
.prototype.getSelectedItems = function () {
8155 return this.items
.filter( function ( item
) {
8156 return item
.isSelected();
8161 * Get the data of options that are selected.
8163 * @return {Object[]|string[]} Values of selected options
8165 OO
.ui
.MultiselectWidget
.prototype.getSelectedItemsData = function () {
8166 return this.getSelectedItems().map( function ( item
) {
8172 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8174 * @param {OO.ui.MultioptionWidget[]} items Items to select
8177 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8178 this.items
.forEach( function ( item
) {
8179 var selected
= items
.indexOf( item
) !== -1;
8180 item
.setSelected( selected
);
8186 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8188 * @param {Object[]|string[]} datas Values of items to select
8191 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8194 items
= datas
.map( function ( data
) {
8195 return widget
.getItemFromData( data
);
8197 this.selectItems( items
);
8202 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8203 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8204 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
8206 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
8209 * @extends OO.ui.MultioptionWidget
8212 * @param {Object} [config] Configuration options
8214 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8215 // Configuration initialization
8216 config
= config
|| {};
8218 // Properties (must be done before parent constructor which calls #setDisabled)
8219 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8221 // Parent constructor
8222 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8225 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8226 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8230 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8231 .prepend( this.checkbox
.$element
);
8236 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8238 /* Static Properties */
8244 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8249 * Handle checkbox selected state change.
8253 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8254 this.setSelected( this.checkbox
.isSelected() );
8260 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8261 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8262 this.checkbox
.setSelected( state
);
8269 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8270 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8271 this.checkbox
.setDisabled( this.isDisabled() );
8278 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8279 this.checkbox
.focus();
8283 * Handle key down events.
8286 * @param {jQuery.Event} e
8288 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8290 element
= this.getElementGroup(),
8293 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8294 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8295 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8296 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8306 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8307 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8308 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8309 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
8311 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8312 * OO.ui.CheckboxMultiselectInputWidget instead.
8315 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8316 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8319 * label: 'Selected checkbox'
8322 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
8324 * label: 'Unselected checkbox'
8327 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
8328 * items: [ option1, option2 ]
8331 * $( 'body' ).append( multiselect.$element );
8333 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
8336 * @extends OO.ui.MultiselectWidget
8339 * @param {Object} [config] Configuration options
8341 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8342 // Parent constructor
8343 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8346 this.$lastClicked
= null;
8349 this.$group
.on( 'click', this.onClick
.bind( this ) );
8353 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8358 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8363 * Get an option by its position relative to the specified item (or to the start of the option array,
8364 * if item is `null`). The direction in which to search through the option array is specified with a
8365 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8366 * `null` if there are no options in the array.
8368 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8369 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8370 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8372 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8373 var currentIndex
, nextIndex
, i
,
8374 increase
= direction
> 0 ? 1 : -1,
8375 len
= this.items
.length
;
8378 currentIndex
= this.items
.indexOf( item
);
8379 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8381 // If no item is selected and moving forward, start at the beginning.
8382 // If moving backward, start at the end.
8383 nextIndex
= direction
> 0 ? 0 : len
- 1;
8386 for ( i
= 0; i
< len
; i
++ ) {
8387 item
= this.items
[ nextIndex
];
8388 if ( item
&& !item
.isDisabled() ) {
8391 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8397 * Handle click events on checkboxes.
8399 * @param {jQuery.Event} e
8401 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8402 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8403 $lastClicked
= this.$lastClicked
,
8404 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8405 .not( '.oo-ui-widget-disabled' );
8407 // Allow selecting multiple options at once by Shift-clicking them
8408 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8409 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8410 lastClickedIndex
= $options
.index( $lastClicked
);
8411 nowClickedIndex
= $options
.index( $nowClicked
);
8412 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8413 // browser. In either case we don't need custom handling.
8414 if ( nowClickedIndex
!== lastClickedIndex
) {
8416 wasSelected
= items
[ nowClickedIndex
].isSelected();
8417 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8419 // This depends on the DOM order of the items and the order of the .items array being the same.
8420 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8421 if ( !items
[ i
].isDisabled() ) {
8422 items
[ i
].setSelected( !wasSelected
);
8425 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8426 // handling first, then set our value. The order in which events happen is different for
8427 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8428 // non-click actions that change the checkboxes.
8430 setTimeout( function () {
8431 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8432 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8438 if ( $nowClicked
.length
) {
8439 this.$lastClicked
= $nowClicked
;
8448 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8450 if ( !this.isDisabled() ) {
8451 item
= this.getRelativeFocusableItem( null, 1 );
8462 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8467 * Progress bars visually display the status of an operation, such as a download,
8468 * and can be either determinate or indeterminate:
8470 * - **determinate** process bars show the percent of an operation that is complete.
8472 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8473 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8474 * not use percentages.
8476 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8479 * // Examples of determinate and indeterminate progress bars.
8480 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8483 * var progressBar2 = new OO.ui.ProgressBarWidget();
8485 * // Create a FieldsetLayout to layout progress bars
8486 * var fieldset = new OO.ui.FieldsetLayout;
8487 * fieldset.addItems( [
8488 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
8489 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
8491 * $( 'body' ).append( fieldset.$element );
8494 * @extends OO.ui.Widget
8497 * @param {Object} [config] Configuration options
8498 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8499 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
8500 * By default, the progress bar is indeterminate.
8502 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8503 // Configuration initialization
8504 config
= config
|| {};
8506 // Parent constructor
8507 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8510 this.$bar
= $( '<div>' );
8511 this.progress
= null;
8514 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8515 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8518 role
: 'progressbar',
8520 'aria-valuemax': 100
8522 .addClass( 'oo-ui-progressBarWidget' )
8523 .append( this.$bar
);
8528 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8530 /* Static Properties */
8536 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8541 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8543 * @return {number|boolean} Progress percent
8545 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8546 return this.progress
;
8550 * Set the percent of the process completed or `false` for an indeterminate process.
8552 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8554 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8555 this.progress
= progress
;
8557 if ( progress
!== false ) {
8558 this.$bar
.css( 'width', this.progress
+ '%' );
8559 this.$element
.attr( 'aria-valuenow', this.progress
);
8561 this.$bar
.css( 'width', '' );
8562 this.$element
.removeAttr( 'aria-valuenow' );
8564 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8568 * InputWidget is the base class for all input widgets, which
8569 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8570 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8571 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8573 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8577 * @extends OO.ui.Widget
8578 * @mixins OO.ui.mixin.FlaggedElement
8579 * @mixins OO.ui.mixin.TabIndexedElement
8580 * @mixins OO.ui.mixin.TitledElement
8581 * @mixins OO.ui.mixin.AccessKeyedElement
8584 * @param {Object} [config] Configuration options
8585 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8586 * @cfg {string} [value=''] The value of the input.
8587 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8588 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8589 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8590 * before it is accepted.
8592 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8593 // Configuration initialization
8594 config
= config
|| {};
8596 // Parent constructor
8597 OO
.ui
.InputWidget
.parent
.call( this, config
);
8600 // See #reusePreInfuseDOM about config.$input
8601 this.$input
= config
.$input
|| this.getInputElement( config
);
8603 this.inputFilter
= config
.inputFilter
;
8605 // Mixin constructors
8606 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8607 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8608 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8609 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8612 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8616 .addClass( 'oo-ui-inputWidget-input' )
8617 .attr( 'name', config
.name
)
8618 .prop( 'disabled', this.isDisabled() );
8620 .addClass( 'oo-ui-inputWidget' )
8621 .append( this.$input
);
8622 this.setValue( config
.value
);
8624 this.setDir( config
.dir
);
8626 if ( config
.inputId
!== undefined ) {
8627 this.setInputId( config
.inputId
);
8633 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8634 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8635 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8636 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8637 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8639 /* Static Methods */
8644 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8645 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8646 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8647 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8654 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8655 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8656 if ( config
.$input
&& config
.$input
.length
) {
8657 state
.value
= config
.$input
.val();
8658 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8659 state
.focus
= config
.$input
.is( ':focus' );
8669 * A change event is emitted when the value of the input changes.
8671 * @param {string} value
8677 * Get input element.
8679 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
8680 * different circumstances. The element must have a `value` property (like form elements).
8683 * @param {Object} config Configuration options
8684 * @return {jQuery} Input element
8686 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
8687 return $( '<input>' );
8691 * Handle potentially value-changing events.
8694 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8696 OO
.ui
.InputWidget
.prototype.onEdit = function () {
8698 if ( !this.isDisabled() ) {
8699 // Allow the stack to clear so the value will be updated
8700 setTimeout( function () {
8701 widget
.setValue( widget
.$input
.val() );
8707 * Get the value of the input.
8709 * @return {string} Input value
8711 OO
.ui
.InputWidget
.prototype.getValue = function () {
8712 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8713 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8714 var value
= this.$input
.val();
8715 if ( this.value
!== value
) {
8716 this.setValue( value
);
8722 * Set the directionality of the input.
8724 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
8727 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
8728 this.$input
.prop( 'dir', dir
);
8733 * Set the value of the input.
8735 * @param {string} value New value
8739 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
8740 value
= this.cleanUpValue( value
);
8741 // Update the DOM if it has changed. Note that with cleanUpValue, it
8742 // is possible for the DOM value to change without this.value changing.
8743 if ( this.$input
.val() !== value
) {
8744 this.$input
.val( value
);
8746 if ( this.value
!== value
) {
8748 this.emit( 'change', this.value
);
8754 * Clean up incoming value.
8756 * Ensures value is a string, and converts undefined and null to empty string.
8759 * @param {string} value Original value
8760 * @return {string} Cleaned up value
8762 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
8763 if ( value
=== undefined || value
=== null ) {
8765 } else if ( this.inputFilter
) {
8766 return this.inputFilter( String( value
) );
8768 return String( value
);
8775 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
8776 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8777 if ( this.$input
) {
8778 this.$input
.prop( 'disabled', this.isDisabled() );
8784 * Set the 'id' attribute of the `<input>` element.
8786 * @param {string} id
8789 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
8790 this.$input
.attr( 'id', id
);
8797 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
8798 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8799 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
8800 this.setValue( state
.value
);
8802 if ( state
.focus
) {
8808 * Data widget intended for creating 'hidden'-type inputs.
8811 * @extends OO.ui.Widget
8814 * @param {Object} [config] Configuration options
8815 * @cfg {string} [value=''] The value of the input.
8816 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8818 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
8819 // Configuration initialization
8820 config
= $.extend( { value
: '', name
: '' }, config
);
8822 // Parent constructor
8823 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
8826 this.$element
.attr( {
8828 value
: config
.value
,
8831 this.$element
.removeAttr( 'aria-disabled' );
8836 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
8838 /* Static Properties */
8844 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
8847 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
8848 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
8849 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
8850 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
8851 * [OOjs UI documentation on MediaWiki] [1] for more information.
8854 * // A ButtonInputWidget rendered as an HTML button, the default.
8855 * var button = new OO.ui.ButtonInputWidget( {
8856 * label: 'Input button',
8860 * $( 'body' ).append( button.$element );
8862 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
8865 * @extends OO.ui.InputWidget
8866 * @mixins OO.ui.mixin.ButtonElement
8867 * @mixins OO.ui.mixin.IconElement
8868 * @mixins OO.ui.mixin.IndicatorElement
8869 * @mixins OO.ui.mixin.LabelElement
8870 * @mixins OO.ui.mixin.TitledElement
8873 * @param {Object} [config] Configuration options
8874 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
8875 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
8876 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
8877 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
8878 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
8880 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
8881 // Configuration initialization
8882 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
8884 // See InputWidget#reusePreInfuseDOM about config.$input
8885 if ( config
.$input
) {
8886 config
.$input
.empty();
8889 // Properties (must be set before parent constructor, which calls #setValue)
8890 this.useInputTag
= config
.useInputTag
;
8892 // Parent constructor
8893 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
8895 // Mixin constructors
8896 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
8897 OO
.ui
.mixin
.IconElement
.call( this, config
);
8898 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8899 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8900 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8903 if ( !config
.useInputTag
) {
8904 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
8906 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
8911 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
8912 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
8913 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
8914 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
8915 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
8916 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
8918 /* Static Properties */
8924 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
8932 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
8934 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
8935 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
8941 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
8943 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
8944 * text, or `null` for no label
8947 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
8948 if ( typeof label
=== 'function' ) {
8949 label
= OO
.ui
.resolveMsg( label
);
8952 if ( this.useInputTag
) {
8953 // Discard non-plaintext labels
8954 if ( typeof label
!== 'string' ) {
8958 this.$input
.val( label
);
8961 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
8965 * Set the value of the input.
8967 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
8968 * they do not support {@link #value values}.
8970 * @param {string} value New value
8973 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
8974 if ( !this.useInputTag
) {
8975 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
8983 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
8984 // Disable generating `<label>` elements for buttons. One would very rarely need additional label
8985 // for a button, and it's already a big clickable target, and it causes unexpected rendering.
8990 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
8991 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
8992 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
8993 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
8995 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
8998 * // An example of selected, unselected, and disabled checkbox inputs
8999 * var checkbox1=new OO.ui.CheckboxInputWidget( {
9003 * var checkbox2=new OO.ui.CheckboxInputWidget( {
9006 * var checkbox3=new OO.ui.CheckboxInputWidget( {
9010 * // Create a fieldset layout with fields for each checkbox.
9011 * var fieldset = new OO.ui.FieldsetLayout( {
9012 * label: 'Checkboxes'
9014 * fieldset.addItems( [
9015 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9016 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9017 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9019 * $( 'body' ).append( fieldset.$element );
9021 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9024 * @extends OO.ui.InputWidget
9027 * @param {Object} [config] Configuration options
9028 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
9030 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9031 // Configuration initialization
9032 config
= config
|| {};
9034 // Parent constructor
9035 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9039 .addClass( 'oo-ui-checkboxInputWidget' )
9040 // Required for pretty styling in WikimediaUI theme
9041 .append( $( '<span>' ) );
9042 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9047 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9049 /* Static Properties */
9055 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9057 /* Static Methods */
9062 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9063 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9064 state
.checked
= config
.$input
.prop( 'checked' );
9074 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9075 return $( '<input>' ).attr( 'type', 'checkbox' );
9081 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9083 if ( !this.isDisabled() ) {
9084 // Allow the stack to clear so the value will be updated
9085 setTimeout( function () {
9086 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9092 * Set selection state of this checkbox.
9094 * @param {boolean} state `true` for selected
9097 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
9099 if ( this.selected
!== state
) {
9100 this.selected
= state
;
9101 this.$input
.prop( 'checked', this.selected
);
9102 this.emit( 'change', this.selected
);
9108 * Check if this checkbox is selected.
9110 * @return {boolean} Checkbox is selected
9112 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9113 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9114 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9115 var selected
= this.$input
.prop( 'checked' );
9116 if ( this.selected
!== selected
) {
9117 this.setSelected( selected
);
9119 return this.selected
;
9125 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9126 if ( !this.isDisabled() ) {
9127 this.$input
.click();
9135 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9136 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9137 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9138 this.setSelected( state
.checked
);
9143 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9144 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9145 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
9146 * more information about input widgets.
9148 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9149 * are no options. If no `value` configuration option is provided, the first option is selected.
9150 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9152 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
9155 * // Example: A DropdownInputWidget with three options
9156 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9158 * { data: 'a', label: 'First' },
9159 * { data: 'b', label: 'Second'},
9160 * { data: 'c', label: 'Third' }
9163 * $( 'body' ).append( dropdownInput.$element );
9165 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9168 * @extends OO.ui.InputWidget
9171 * @param {Object} [config] Configuration options
9172 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9173 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9175 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9176 // Configuration initialization
9177 config
= config
|| {};
9179 // Properties (must be done before parent constructor which calls #setDisabled)
9180 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
9182 // Parent constructor
9183 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9186 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
9189 this.setOptions( config
.options
|| [] );
9190 // Set the value again, after we did setOptions(). The call from parent doesn't work because the
9191 // widget has no valid options when it happens.
9192 this.setValue( config
.value
);
9194 .addClass( 'oo-ui-dropdownInputWidget' )
9195 .append( this.dropdownWidget
.$element
);
9196 this.setTabIndexedElement( null );
9201 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9209 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9210 return $( '<select>' );
9214 * Handles menu select events.
9217 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9219 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9220 this.setValue( item
? item
.getData() : '' );
9226 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9228 value
= this.cleanUpValue( value
);
9229 // Only allow setting values that are actually present in the dropdown
9230 selected
= this.dropdownWidget
.getMenu().getItemFromData( value
) ||
9231 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9232 this.dropdownWidget
.getMenu().selectItem( selected
);
9233 value
= selected
? selected
.getData() : '';
9234 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9241 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9242 this.dropdownWidget
.setDisabled( state
);
9243 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9248 * Set the options available for this input.
9250 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9253 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9256 value
= this.getValue(),
9257 $optionsContainer
= this.$input
,
9260 this.dropdownWidget
.getMenu().clearItems();
9261 this.$input
.empty();
9263 // Rebuild the dropdown menu: our visible one and the hidden `<select>`
9264 options
.forEach( function ( opt
) {
9265 var optValue
, $optionNode
, optionWidget
;
9267 if ( opt
.optgroup
=== undefined ) {
9268 optValue
= widget
.cleanUpValue( opt
.data
);
9270 $optionNode
= $( '<option>' )
9271 .attr( 'value', optValue
)
9272 .text( opt
.label
!== undefined ? opt
.label
: optValue
);
9273 optionWidget
= new OO
.ui
.MenuOptionWidget( {
9275 label
: opt
.label
!== undefined ? opt
.label
: optValue
9278 $optionsContainer
.append( $optionNode
);
9279 optionWidgets
.push( optionWidget
);
9281 $optionNode
= $( '<optgroup>' )
9282 .attr( 'label', opt
.optgroup
);
9283 optionWidget
= new OO
.ui
.MenuSectionOptionWidget( {
9287 widget
.$input
.append( $optionNode
);
9288 $optionsContainer
= $optionNode
;
9289 optionWidgets
.push( optionWidget
);
9292 this.dropdownWidget
.getMenu().addItems( optionWidgets
);
9294 // Restore the previous value, or reset to something sensible
9295 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
9296 // Previous value is still available, ensure consistency with the dropdown
9297 this.setValue( value
);
9299 // No longer valid, reset
9300 if ( options
.length
) {
9301 this.setValue( options
[ 0 ].data
);
9311 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9312 this.dropdownWidget
.focus();
9319 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9320 this.dropdownWidget
.blur();
9325 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9326 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9327 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9328 * please see the [OOjs UI documentation on MediaWiki][1].
9330 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9333 * // An example of selected, unselected, and disabled radio inputs
9334 * var radio1 = new OO.ui.RadioInputWidget( {
9338 * var radio2 = new OO.ui.RadioInputWidget( {
9341 * var radio3 = new OO.ui.RadioInputWidget( {
9345 * // Create a fieldset layout with fields for each radio button.
9346 * var fieldset = new OO.ui.FieldsetLayout( {
9347 * label: 'Radio inputs'
9349 * fieldset.addItems( [
9350 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9351 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9352 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9354 * $( 'body' ).append( fieldset.$element );
9356 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9359 * @extends OO.ui.InputWidget
9362 * @param {Object} [config] Configuration options
9363 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9365 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9366 // Configuration initialization
9367 config
= config
|| {};
9369 // Parent constructor
9370 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9374 .addClass( 'oo-ui-radioInputWidget' )
9375 // Required for pretty styling in WikimediaUI theme
9376 .append( $( '<span>' ) );
9377 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9382 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9384 /* Static Properties */
9390 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9392 /* Static Methods */
9397 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9398 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9399 state
.checked
= config
.$input
.prop( 'checked' );
9409 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9410 return $( '<input>' ).attr( 'type', 'radio' );
9416 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9417 // RadioInputWidget doesn't track its state.
9421 * Set selection state of this radio button.
9423 * @param {boolean} state `true` for selected
9426 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9427 // RadioInputWidget doesn't track its state.
9428 this.$input
.prop( 'checked', state
);
9433 * Check if this radio button is selected.
9435 * @return {boolean} Radio is selected
9437 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
9438 return this.$input
.prop( 'checked' );
9444 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
9445 if ( !this.isDisabled() ) {
9446 this.$input
.click();
9454 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9455 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9456 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9457 this.setSelected( state
.checked
);
9462 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9463 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9464 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
9465 * more information about input widgets.
9467 * This and OO.ui.DropdownInputWidget support the same configuration options.
9470 * // Example: A RadioSelectInputWidget with three options
9471 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9473 * { data: 'a', label: 'First' },
9474 * { data: 'b', label: 'Second'},
9475 * { data: 'c', label: 'Third' }
9478 * $( 'body' ).append( radioSelectInput.$element );
9480 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9483 * @extends OO.ui.InputWidget
9486 * @param {Object} [config] Configuration options
9487 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9489 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
9490 // Configuration initialization
9491 config
= config
|| {};
9493 // Properties (must be done before parent constructor which calls #setDisabled)
9494 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
9496 // Parent constructor
9497 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
9500 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
9503 this.setOptions( config
.options
|| [] );
9505 .addClass( 'oo-ui-radioSelectInputWidget' )
9506 .append( this.radioSelectWidget
.$element
);
9507 this.setTabIndexedElement( null );
9512 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
9514 /* Static Methods */
9519 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9520 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9521 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9528 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9529 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9530 // Cannot reuse the `<input type=radio>` set
9531 delete config
.$input
;
9541 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
9542 return $( '<input>' ).attr( 'type', 'hidden' );
9546 * Handles menu select events.
9549 * @param {OO.ui.RadioOptionWidget} item Selected menu item
9551 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
9552 this.setValue( item
.getData() );
9558 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
9559 value
= this.cleanUpValue( value
);
9560 this.radioSelectWidget
.selectItemByData( value
);
9561 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9568 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
9569 this.radioSelectWidget
.setDisabled( state
);
9570 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9575 * Set the options available for this input.
9577 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9580 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
9582 value
= this.getValue(),
9585 // Rebuild the radioSelect menu
9586 this.radioSelectWidget
9588 .addItems( options
.map( function ( opt
) {
9589 var optValue
= widget
.cleanUpValue( opt
.data
);
9590 return new OO
.ui
.RadioOptionWidget( {
9592 label
: opt
.label
!== undefined ? opt
.label
: optValue
9596 // Restore the previous value, or reset to something sensible
9597 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
9598 // Previous value is still available, ensure consistency with the radioSelect
9599 this.setValue( value
);
9601 // No longer valid, reset
9602 if ( options
.length
) {
9603 this.setValue( options
[ 0 ].data
);
9613 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
9614 this.radioSelectWidget
.focus();
9621 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
9622 this.radioSelectWidget
.blur();
9627 * CheckboxMultiselectInputWidget is a
9628 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
9629 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
9630 * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on MediaWiki][1] for
9631 * more information about input widgets.
9634 * // Example: A CheckboxMultiselectInputWidget with three options
9635 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
9637 * { data: 'a', label: 'First' },
9638 * { data: 'b', label: 'Second'},
9639 * { data: 'c', label: 'Third' }
9642 * $( 'body' ).append( multiselectInput.$element );
9644 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9647 * @extends OO.ui.InputWidget
9650 * @param {Object} [config] Configuration options
9651 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
9653 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
9654 // Configuration initialization
9655 config
= config
|| {};
9657 // Properties (must be done before parent constructor which calls #setDisabled)
9658 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
9660 // Parent constructor
9661 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
9664 this.inputName
= config
.name
;
9668 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
9669 .append( this.checkboxMultiselectWidget
.$element
);
9670 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
9671 this.$input
.detach();
9672 this.setOptions( config
.options
|| [] );
9673 // Have to repeat this from parent, as we need options to be set up for this to make sense
9674 this.setValue( config
.value
);
9676 // setValue when checkboxMultiselectWidget changes
9677 this.checkboxMultiselectWidget
.on( 'change', function () {
9678 this.setValue( this.checkboxMultiselectWidget
.getSelectedItemsData() );
9684 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
9686 /* Static Methods */
9691 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9692 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9693 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9694 .toArray().map( function ( el
) { return el
.value
; } );
9701 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9702 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9703 // Cannot reuse the `<input type=checkbox>` set
9704 delete config
.$input
;
9714 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
9716 return $( '<unused>' );
9722 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
9723 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9724 .toArray().map( function ( el
) { return el
.value
; } );
9725 if ( this.value
!== value
) {
9726 this.setValue( value
);
9734 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
9735 value
= this.cleanUpValue( value
);
9736 this.checkboxMultiselectWidget
.selectItemsByData( value
);
9737 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9742 * Clean up incoming value.
9744 * @param {string[]} value Original value
9745 * @return {string[]} Cleaned up value
9747 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
9750 if ( !Array
.isArray( value
) ) {
9753 for ( i
= 0; i
< value
.length
; i
++ ) {
9755 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
9756 // Remove options that we don't have here
9757 if ( !this.checkboxMultiselectWidget
.getItemFromData( singleValue
) ) {
9760 cleanValue
.push( singleValue
);
9768 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
9769 this.checkboxMultiselectWidget
.setDisabled( state
);
9770 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9775 * Set the options available for this input.
9777 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
9780 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
9783 // Rebuild the checkboxMultiselectWidget menu
9784 this.checkboxMultiselectWidget
9786 .addItems( options
.map( function ( opt
) {
9787 var optValue
, item
, optDisabled
;
9789 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
9790 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
9791 item
= new OO
.ui
.CheckboxMultioptionWidget( {
9793 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
9794 disabled
: optDisabled
9796 // Set the 'name' and 'value' for form submission
9797 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
9798 item
.checkbox
.setValue( optValue
);
9802 // Re-set the value, checking the checkboxes as needed.
9803 // This will also get rid of any stale options that we just removed.
9804 this.setValue( this.getValue() );
9812 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
9813 this.checkboxMultiselectWidget
.focus();
9818 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
9819 * size of the field as well as its presentation. In addition, these widgets can be configured
9820 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
9821 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
9822 * which modifies incoming values rather than validating them.
9823 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9825 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9828 * // Example of a text input widget
9829 * var textInput = new OO.ui.TextInputWidget( {
9830 * value: 'Text input'
9832 * $( 'body' ).append( textInput.$element );
9834 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9837 * @extends OO.ui.InputWidget
9838 * @mixins OO.ui.mixin.IconElement
9839 * @mixins OO.ui.mixin.IndicatorElement
9840 * @mixins OO.ui.mixin.PendingElement
9841 * @mixins OO.ui.mixin.LabelElement
9844 * @param {Object} [config] Configuration options
9845 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
9846 * 'email', 'url' or 'number'.
9847 * @cfg {string} [placeholder] Placeholder text
9848 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
9849 * instruct the browser to focus this widget.
9850 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
9851 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
9852 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
9853 * the value or placeholder text: `'before'` or `'after'`
9854 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
9855 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
9856 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined` means
9857 * leaving it up to the browser).
9858 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
9859 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
9860 * (the value must contain only numbers); when RegExp, a regular expression that must match the
9861 * value for it to be considered valid; when Function, a function receiving the value as parameter
9862 * that must return true, or promise resolving to true, for it to be considered valid.
9864 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
9865 // Configuration initialization
9866 config
= $.extend( {
9868 labelPosition
: 'after'
9871 if ( config
.multiline
) {
9872 OO
.ui
.warnDeprecation( 'TextInputWidget: config.multiline is deprecated. Use the MultilineTextInputWidget instead. See T130434.' );
9873 return new OO
.ui
.MultilineTextInputWidget( config
);
9876 // Parent constructor
9877 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
9879 // Mixin constructors
9880 OO
.ui
.mixin
.IconElement
.call( this, config
);
9881 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9882 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
9883 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9886 this.type
= this.getSaneType( config
);
9887 this.readOnly
= false;
9888 this.required
= false;
9889 this.validate
= null;
9890 this.styleHeight
= null;
9891 this.scrollWidth
= null;
9893 this.setValidation( config
.validate
);
9894 this.setLabelPosition( config
.labelPosition
);
9898 keypress
: this.onKeyPress
.bind( this ),
9899 blur
: this.onBlur
.bind( this ),
9900 focus
: this.onFocus
.bind( this )
9902 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
9903 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
9904 this.on( 'labelChange', this.updatePosition
.bind( this ) );
9905 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
9909 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
9910 .append( this.$icon
, this.$indicator
);
9911 this.setReadOnly( !!config
.readOnly
);
9912 this.setRequired( !!config
.required
);
9913 if ( config
.placeholder
!== undefined ) {
9914 this.$input
.attr( 'placeholder', config
.placeholder
);
9916 if ( config
.maxLength
!== undefined ) {
9917 this.$input
.attr( 'maxlength', config
.maxLength
);
9919 if ( config
.autofocus
) {
9920 this.$input
.attr( 'autofocus', 'autofocus' );
9922 if ( config
.autocomplete
=== false ) {
9923 this.$input
.attr( 'autocomplete', 'off' );
9924 // Turning off autocompletion also disables "form caching" when the user navigates to a
9925 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
9927 beforeunload: function () {
9928 this.$input
.removeAttr( 'autocomplete' );
9930 pageshow: function () {
9931 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
9932 // whole page... it shouldn't hurt, though.
9933 this.$input
.attr( 'autocomplete', 'off' );
9937 if ( config
.spellcheck
!== undefined ) {
9938 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
9941 this.isWaitingToBeAttached
= true;
9942 this.installParentChangeDetector();
9948 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
9949 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
9950 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9951 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
9952 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
9954 /* Static Properties */
9956 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
9961 /* Static Methods */
9966 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9967 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9974 * An `enter` event is emitted when the user presses 'enter' inside the text box.
9982 * Handle icon mouse down events.
9985 * @param {jQuery.Event} e Mouse down event
9987 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
9988 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
9995 * Handle indicator mouse down events.
9998 * @param {jQuery.Event} e Mouse down event
10000 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10001 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10008 * Handle key press events.
10011 * @param {jQuery.Event} e Key press event
10012 * @fires enter If enter key is pressed
10014 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10015 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10016 this.emit( 'enter', e
);
10021 * Handle blur events.
10024 * @param {jQuery.Event} e Blur event
10026 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10027 this.setValidityFlag();
10031 * Handle focus events.
10034 * @param {jQuery.Event} e Focus event
10036 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10037 if ( this.isWaitingToBeAttached
) {
10038 // If we've received focus, then we must be attached to the document, and if
10039 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10040 this.onElementAttach();
10042 this.setValidityFlag( true );
10046 * Handle element attach events.
10049 * @param {jQuery.Event} e Element attach event
10051 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10052 this.isWaitingToBeAttached
= false;
10053 // Any previously calculated size is now probably invalid if we reattached elsewhere
10054 this.valCache
= null;
10055 this.positionLabel();
10059 * Handle debounced change events.
10061 * @param {string} value
10064 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10065 this.setValidityFlag();
10069 * Check if the input is {@link #readOnly read-only}.
10071 * @return {boolean}
10073 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10074 return this.readOnly
;
10078 * Set the {@link #readOnly read-only} state of the input.
10080 * @param {boolean} state Make input read-only
10083 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10084 this.readOnly
= !!state
;
10085 this.$input
.prop( 'readOnly', this.readOnly
);
10090 * Check if the input is {@link #required required}.
10092 * @return {boolean}
10094 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10095 return this.required
;
10099 * Set the {@link #required required} state of the input.
10101 * @param {boolean} state Make input required
10104 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10105 this.required
= !!state
;
10106 if ( this.required
) {
10108 .prop( 'required', true )
10109 .attr( 'aria-required', 'true' );
10110 if ( this.getIndicator() === null ) {
10111 this.setIndicator( 'required' );
10115 .prop( 'required', false )
10116 .removeAttr( 'aria-required' );
10117 if ( this.getIndicator() === 'required' ) {
10118 this.setIndicator( null );
10125 * Support function for making #onElementAttach work across browsers.
10127 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10128 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10130 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10131 * first time that the element gets attached to the documented.
10133 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10134 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10135 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
10138 if ( MutationObserver
) {
10139 // The new way. If only it wasn't so ugly.
10141 if ( this.isElementAttached() ) {
10142 // Widget is attached already, do nothing. This breaks the functionality of this function when
10143 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
10144 // would require observation of the whole document, which would hurt performance of other,
10145 // more important code.
10149 // Find topmost node in the tree
10150 topmostNode
= this.$element
[ 0 ];
10151 while ( topmostNode
.parentNode
) {
10152 topmostNode
= topmostNode
.parentNode
;
10155 // We have no way to detect the $element being attached somewhere without observing the entire
10156 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
10157 // parent node of $element, and instead detect when $element is removed from it (and thus
10158 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
10159 // doesn't get attached, we end up back here and create the parent.
10161 mutationObserver
= new MutationObserver( function ( mutations
) {
10162 var i
, j
, removedNodes
;
10163 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10164 removedNodes
= mutations
[ i
].removedNodes
;
10165 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10166 if ( removedNodes
[ j
] === topmostNode
) {
10167 setTimeout( onRemove
, 0 );
10174 onRemove = function () {
10175 // If the node was attached somewhere else, report it
10176 if ( widget
.isElementAttached() ) {
10177 widget
.onElementAttach();
10179 mutationObserver
.disconnect();
10180 widget
.installParentChangeDetector();
10183 // Create a fake parent and observe it
10184 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10185 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10187 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10188 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10189 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10197 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10198 if ( this.getSaneType( config
) === 'number' ) {
10199 return $( '<input>' )
10200 .attr( 'step', 'any' )
10201 .attr( 'type', 'number' );
10203 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10208 * Get sanitized value for 'type' for given config.
10210 * @param {Object} config Configuration options
10211 * @return {string|null}
10214 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10215 var allowedTypes
= [
10222 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10226 * Focus the input and select a specified range within the text.
10228 * @param {number} from Select from offset
10229 * @param {number} [to] Select to offset, defaults to from
10232 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10233 var isBackwards
, start
, end
,
10234 input
= this.$input
[ 0 ];
10238 isBackwards
= to
< from;
10239 start
= isBackwards
? to
: from;
10240 end
= isBackwards
? from : to
;
10245 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10247 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10248 // Rather than expensively check if the input is attached every time, just check
10249 // if it was the cause of an error being thrown. If not, rethrow the error.
10250 if ( this.getElementDocument().body
.contains( input
) ) {
10258 * Get an object describing the current selection range in a directional manner
10260 * @return {Object} Object containing 'from' and 'to' offsets
10262 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10263 var input
= this.$input
[ 0 ],
10264 start
= input
.selectionStart
,
10265 end
= input
.selectionEnd
,
10266 isBackwards
= input
.selectionDirection
=== 'backward';
10269 from: isBackwards
? end
: start
,
10270 to
: isBackwards
? start
: end
10275 * Get the length of the text input value.
10277 * This could differ from the length of #getValue if the
10278 * value gets filtered
10280 * @return {number} Input length
10282 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10283 return this.$input
[ 0 ].value
.length
;
10287 * Focus the input and select the entire text.
10291 OO
.ui
.TextInputWidget
.prototype.select = function () {
10292 return this.selectRange( 0, this.getInputLength() );
10296 * Focus the input and move the cursor to the start.
10300 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10301 return this.selectRange( 0 );
10305 * Focus the input and move the cursor to the end.
10309 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10310 return this.selectRange( this.getInputLength() );
10314 * Insert new content into the input.
10316 * @param {string} content Content to be inserted
10319 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10321 range
= this.getRange(),
10322 value
= this.getValue();
10324 start
= Math
.min( range
.from, range
.to
);
10325 end
= Math
.max( range
.from, range
.to
);
10327 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10328 this.selectRange( start
+ content
.length
);
10333 * Insert new content either side of a selection.
10335 * @param {string} pre Content to be inserted before the selection
10336 * @param {string} post Content to be inserted after the selection
10339 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10341 range
= this.getRange(),
10342 offset
= pre
.length
;
10344 start
= Math
.min( range
.from, range
.to
);
10345 end
= Math
.max( range
.from, range
.to
);
10347 this.selectRange( start
).insertContent( pre
);
10348 this.selectRange( offset
+ end
).insertContent( post
);
10350 this.selectRange( offset
+ start
, offset
+ end
);
10355 * Set the validation pattern.
10357 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10358 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10359 * value must contain only numbers).
10361 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10362 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10364 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
10365 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
10366 this.validate
= validate
;
10368 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
10373 * Sets the 'invalid' flag appropriately.
10375 * @param {boolean} [isValid] Optionally override validation result
10377 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
10379 setFlag = function ( valid
) {
10381 widget
.$input
.attr( 'aria-invalid', 'true' );
10383 widget
.$input
.removeAttr( 'aria-invalid' );
10385 widget
.setFlags( { invalid
: !valid
} );
10388 if ( isValid
!== undefined ) {
10389 setFlag( isValid
);
10391 this.getValidity().then( function () {
10400 * Get the validity of current value.
10402 * This method returns a promise that resolves if the value is valid and rejects if
10403 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
10405 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10407 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
10410 function rejectOrResolve( valid
) {
10412 return $.Deferred().resolve().promise();
10414 return $.Deferred().reject().promise();
10418 // Check browser validity and reject if it is invalid
10420 this.$input
[ 0 ].checkValidity
!== undefined &&
10421 this.$input
[ 0 ].checkValidity() === false
10423 return rejectOrResolve( false );
10426 // Run our checks if the browser thinks the field is valid
10427 if ( this.validate
instanceof Function
) {
10428 result
= this.validate( this.getValue() );
10429 if ( result
&& $.isFunction( result
.promise
) ) {
10430 return result
.promise().then( function ( valid
) {
10431 return rejectOrResolve( valid
);
10434 return rejectOrResolve( result
);
10437 return rejectOrResolve( this.getValue().match( this.validate
) );
10442 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10444 * @param {string} labelPosition Label position, 'before' or 'after'
10447 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10448 this.labelPosition
= labelPosition
;
10449 if ( this.label
) {
10450 // If there is no label and we only change the position, #updatePosition is a no-op,
10451 // but it takes really a lot of work to do nothing.
10452 this.updatePosition();
10458 * Update the position of the inline label.
10460 * This method is called by #setLabelPosition, and can also be called on its own if
10461 * something causes the label to be mispositioned.
10465 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10466 var after
= this.labelPosition
=== 'after';
10469 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10470 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10472 this.valCache
= null;
10473 this.scrollWidth
= null;
10474 this.positionLabel();
10480 * Position the label by setting the correct padding on the input.
10485 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
10486 var after
, rtl
, property
, newCss
;
10488 if ( this.isWaitingToBeAttached
) {
10489 // #onElementAttach will be called soon, which calls this method
10494 'padding-right': '',
10498 if ( this.label
) {
10499 this.$element
.append( this.$label
);
10501 this.$label
.detach();
10502 // Clear old values if present
10503 this.$input
.css( newCss
);
10507 after
= this.labelPosition
=== 'after';
10508 rtl
= this.$element
.css( 'direction' ) === 'rtl';
10509 property
= after
=== rtl
? 'padding-left' : 'padding-right';
10511 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
10512 // We have to clear the padding on the other side, in case the element direction changed
10513 this.$input
.css( newCss
);
10521 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10522 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10523 if ( state
.scrollTop
!== undefined ) {
10524 this.$input
.scrollTop( state
.scrollTop
);
10530 * @extends OO.ui.TextInputWidget
10533 * @param {Object} [config] Configuration options
10535 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
10536 config
= $.extend( {
10540 // Parent constructor
10541 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
10544 this.connect( this, {
10549 this.updateSearchIndicator();
10550 this.connect( this, {
10551 disable
: 'onDisable'
10557 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
10565 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
10572 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10573 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10574 // Clear the text field
10575 this.setValue( '' );
10582 * Update the 'clear' indicator displayed on type: 'search' text
10583 * fields, hiding it when the field is already empty or when it's not
10586 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
10587 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
10588 this.setIndicator( null );
10590 this.setIndicator( 'clear' );
10595 * Handle change events.
10599 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
10600 this.updateSearchIndicator();
10604 * Handle disable events.
10606 * @param {boolean} disabled Element is disabled
10609 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
10610 this.updateSearchIndicator();
10616 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
10617 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
10618 this.updateSearchIndicator();
10624 * @extends OO.ui.TextInputWidget
10627 * @param {Object} [config] Configuration options
10628 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
10629 * specifies minimum number of rows to display.
10630 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10631 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
10632 * Use the #maxRows config to specify a maximum number of displayed rows.
10633 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
10634 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
10636 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
10637 config
= $.extend( {
10640 config
.multiline
= false;
10641 // Parent constructor
10642 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
10645 this.multiline
= true;
10646 this.autosize
= !!config
.autosize
;
10647 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
10648 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
10650 // Clone for resizing
10651 if ( this.autosize
) {
10652 this.$clone
= this.$input
10654 .insertAfter( this.$input
)
10655 .attr( 'aria-hidden', 'true' )
10656 .addClass( 'oo-ui-element-hidden' );
10660 this.connect( this, {
10665 if ( this.multiline
&& config
.rows
) {
10666 this.$input
.attr( 'rows', config
.rows
);
10668 if ( this.autosize
) {
10669 this.isWaitingToBeAttached
= true;
10670 this.installParentChangeDetector();
10676 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
10678 /* Static Methods */
10683 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10684 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10685 state
.scrollTop
= config
.$input
.scrollTop();
10694 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
10695 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
10700 * Handle change events.
10704 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
10711 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
10712 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
10717 * Override TextInputWidget so it doesn't emit the 'enter' event.
10720 * @param {jQuery.Event} e Key press event
10722 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function () {
10727 * Automatically adjust the size of the text input.
10729 * This only affects multiline inputs that are {@link #autosize autosized}.
10734 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
10735 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
10736 idealHeight
, newHeight
, scrollWidth
, property
;
10738 if ( this.$input
.val() !== this.valCache
) {
10739 if ( this.autosize
) {
10741 .val( this.$input
.val() )
10742 .attr( 'rows', this.minRows
)
10743 // Set inline height property to 0 to measure scroll height
10744 .css( 'height', 0 );
10746 this.$clone
.removeClass( 'oo-ui-element-hidden' );
10748 this.valCache
= this.$input
.val();
10750 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
10752 // Remove inline height property to measure natural heights
10753 this.$clone
.css( 'height', '' );
10754 innerHeight
= this.$clone
.innerHeight();
10755 outerHeight
= this.$clone
.outerHeight();
10757 // Measure max rows height
10759 .attr( 'rows', this.maxRows
)
10760 .css( 'height', 'auto' )
10762 maxInnerHeight
= this.$clone
.innerHeight();
10764 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
10765 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
10766 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
10767 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
10769 this.$clone
.addClass( 'oo-ui-element-hidden' );
10771 // Only apply inline height when expansion beyond natural height is needed
10772 // Use the difference between the inner and outer height as a buffer
10773 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
10774 if ( newHeight
!== this.styleHeight
) {
10775 this.$input
.css( 'height', newHeight
);
10776 this.styleHeight
= newHeight
;
10777 this.emit( 'resize' );
10780 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
10781 if ( scrollWidth
!== this.scrollWidth
) {
10782 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
10784 this.$label
.css( { right
: '', left
: '' } );
10785 this.$indicator
.css( { right
: '', left
: '' } );
10787 if ( scrollWidth
) {
10788 this.$indicator
.css( property
, scrollWidth
);
10789 if ( this.labelPosition
=== 'after' ) {
10790 this.$label
.css( property
, scrollWidth
);
10794 this.scrollWidth
= scrollWidth
;
10795 this.positionLabel();
10805 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
10806 return $( '<textarea>' );
10810 * Check if the input supports multiple lines.
10812 * @return {boolean}
10814 OO
.ui
.MultilineTextInputWidget
.prototype.isMultiline = function () {
10815 return !!this.multiline
;
10819 * Check if the input automatically adjusts its size.
10821 * @return {boolean}
10823 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
10824 return !!this.autosize
;
10828 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
10829 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
10830 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
10832 * - by typing a value in the text input field. If the value exactly matches the value of a menu
10833 * option, that option will appear to be selected.
10834 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
10837 * After the user chooses an option, its `data` will be used as a new value for the widget.
10838 * A `label` also can be specified for each option: if given, it will be shown instead of the
10839 * `data` in the dropdown menu.
10841 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10843 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
10846 * // Example: A ComboBoxInputWidget.
10847 * var comboBox = new OO.ui.ComboBoxInputWidget( {
10848 * value: 'Option 1',
10850 * { data: 'Option 1' },
10851 * { data: 'Option 2' },
10852 * { data: 'Option 3' }
10855 * $( 'body' ).append( comboBox.$element );
10858 * // Example: A ComboBoxInputWidget with additional option labels.
10859 * var comboBox = new OO.ui.ComboBoxInputWidget( {
10860 * value: 'Option 1',
10863 * data: 'Option 1',
10864 * label: 'Option One'
10867 * data: 'Option 2',
10868 * label: 'Option Two'
10871 * data: 'Option 3',
10872 * label: 'Option Three'
10876 * $( 'body' ).append( comboBox.$element );
10878 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10881 * @extends OO.ui.TextInputWidget
10884 * @param {Object} [config] Configuration options
10885 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10886 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
10887 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
10888 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
10889 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
10890 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
10892 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
10893 // Configuration initialization
10894 config
= $.extend( {
10895 autocomplete
: false
10898 // ComboBoxInputWidget shouldn't support `multiline`
10899 config
.multiline
= false;
10901 // See InputWidget#reusePreInfuseDOM about `config.$input`
10902 if ( config
.$input
) {
10903 config
.$input
.removeAttr( 'list' );
10906 // Parent constructor
10907 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
10910 this.$overlay
= config
.$overlay
|| this.$element
;
10911 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
10912 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
10914 disabled
: this.disabled
10916 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
10920 $floatableContainer
: this.$element
,
10921 disabled
: this.isDisabled()
10927 this.connect( this, {
10928 change
: 'onInputChange',
10929 enter
: 'onInputEnter'
10931 this.dropdownButton
.connect( this, {
10932 click
: 'onDropdownButtonClick'
10934 this.menu
.connect( this, {
10935 choose
: 'onMenuChoose',
10936 add
: 'onMenuItemsChange',
10937 remove
: 'onMenuItemsChange',
10938 toggle
: 'onMenuToggle'
10942 this.$input
.attr( {
10944 'aria-owns': this.menu
.getElementId(),
10945 'aria-autocomplete': 'list'
10947 // Do not override options set via config.menu.items
10948 if ( config
.options
!== undefined ) {
10949 this.setOptions( config
.options
);
10951 this.$field
= $( '<div>' )
10952 .addClass( 'oo-ui-comboBoxInputWidget-field' )
10953 .append( this.$input
, this.dropdownButton
.$element
);
10955 .addClass( 'oo-ui-comboBoxInputWidget' )
10956 .append( this.$field
);
10957 this.$overlay
.append( this.menu
.$element
);
10958 this.onMenuItemsChange();
10963 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
10968 * Get the combobox's menu.
10970 * @return {OO.ui.MenuSelectWidget} Menu widget
10972 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
10977 * Get the combobox's text input widget.
10979 * @return {OO.ui.TextInputWidget} Text input widget
10981 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
10986 * Handle input change events.
10989 * @param {string} value New value
10991 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
10992 var match
= this.menu
.getItemFromData( value
);
10994 this.menu
.selectItem( match
);
10995 if ( this.menu
.findHighlightedItem() ) {
10996 this.menu
.highlightItem( match
);
10999 if ( !this.isDisabled() ) {
11000 this.menu
.toggle( true );
11005 * Handle input enter events.
11009 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11010 if ( !this.isDisabled() ) {
11011 this.menu
.toggle( false );
11016 * Handle button click events.
11020 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11021 this.menu
.toggle();
11026 * Handle menu choose events.
11029 * @param {OO.ui.OptionWidget} item Chosen item
11031 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11032 this.setValue( item
.getData() );
11036 * Handle menu item change events.
11040 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11041 var match
= this.menu
.getItemFromData( this.getValue() );
11042 this.menu
.selectItem( match
);
11043 if ( this.menu
.findHighlightedItem() ) {
11044 this.menu
.highlightItem( match
);
11046 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11050 * Handle menu toggle events.
11053 * @param {boolean} isVisible Open state of the menu
11055 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11056 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11062 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
11064 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
11066 if ( this.dropdownButton
) {
11067 this.dropdownButton
.setDisabled( this.isDisabled() );
11070 this.menu
.setDisabled( this.isDisabled() );
11077 * Set the options available for this input.
11079 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11082 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11085 .addItems( options
.map( function ( opt
) {
11086 return new OO
.ui
.MenuOptionWidget( {
11088 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11096 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11097 * which is a widget that is specified by reference before any optional configuration settings.
11099 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
11101 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11102 * A left-alignment is used for forms with many fields.
11103 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11104 * A right-alignment is used for long but familiar forms which users tab through,
11105 * verifying the current field with a quick glance at the label.
11106 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11107 * that users fill out from top to bottom.
11108 * - **inline**: The label is placed after the field-widget and aligned to the left.
11109 * An inline-alignment is best used with checkboxes or radio buttons.
11111 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
11112 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
11114 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
11117 * @extends OO.ui.Layout
11118 * @mixins OO.ui.mixin.LabelElement
11119 * @mixins OO.ui.mixin.TitledElement
11122 * @param {OO.ui.Widget} fieldWidget Field widget
11123 * @param {Object} [config] Configuration options
11124 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
11125 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
11126 * The array may contain strings or OO.ui.HtmlSnippet instances.
11127 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
11128 * The array may contain strings or OO.ui.HtmlSnippet instances.
11129 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
11130 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
11131 * For important messages, you are advised to use `notices`, as they are always shown.
11132 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
11133 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
11135 * @throws {Error} An error is thrown if no widget is specified
11137 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11138 // Allow passing positional parameters inside the config object
11139 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11140 config
= fieldWidget
;
11141 fieldWidget
= config
.fieldWidget
;
11144 // Make sure we have required constructor arguments
11145 if ( fieldWidget
=== undefined ) {
11146 throw new Error( 'Widget not found' );
11149 // Configuration initialization
11150 config
= $.extend( { align
: 'left' }, config
);
11152 // Parent constructor
11153 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11155 // Mixin constructors
11156 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
11157 $label
: $( '<label>' )
11159 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
11162 this.fieldWidget
= fieldWidget
;
11165 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11166 this.$messages
= $( '<ul>' );
11167 this.$header
= $( '<span>' );
11168 this.$body
= $( '<div>' );
11170 if ( config
.help
) {
11171 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
11172 $overlay
: config
.$overlay
,
11176 classes
: [ 'oo-ui-fieldLayout-help' ],
11180 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
11181 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
11183 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
11185 this.$help
= this.popupButtonWidget
.$element
;
11187 this.$help
= $( [] );
11191 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
11194 if ( config
.help
) {
11195 // Set the 'aria-describedby' attribute on the fieldWidget
11196 // Preference given to an input or a button
11198 this.fieldWidget
.$input
||
11199 this.fieldWidget
.$button
||
11200 this.fieldWidget
.$element
11202 'aria-describedby',
11203 this.popupButtonWidget
.getPopup().getBodyId()
11206 if ( this.fieldWidget
.getInputId() ) {
11207 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11209 this.$label
.on( 'click', function () {
11210 this.fieldWidget
.simulateLabelClick();
11215 .addClass( 'oo-ui-fieldLayout' )
11216 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11217 .append( this.$body
);
11218 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11219 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11220 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11222 .addClass( 'oo-ui-fieldLayout-field' )
11223 .append( this.fieldWidget
.$element
);
11225 this.setErrors( config
.errors
|| [] );
11226 this.setNotices( config
.notices
|| [] );
11227 this.setAlignment( config
.align
);
11228 // Call this again to take into account the widget's accessKey
11229 this.updateTitle();
11234 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11235 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11236 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
11241 * Handle field disable events.
11244 * @param {boolean} value Field is disabled
11246 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
11247 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11251 * Get the widget contained by the field.
11253 * @return {OO.ui.Widget} Field widget
11255 OO
.ui
.FieldLayout
.prototype.getField = function () {
11256 return this.fieldWidget
;
11260 * Return `true` if the given field widget can be used with `'inline'` alignment (see
11261 * #setAlignment). Return `false` if it can't or if this can't be determined.
11263 * @return {boolean}
11265 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11266 // This is very simplistic, but should be good enough.
11267 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11272 * @param {string} kind 'error' or 'notice'
11273 * @param {string|OO.ui.HtmlSnippet} text
11276 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11277 var $listItem
, $icon
, message
;
11278 $listItem
= $( '<li>' );
11279 if ( kind
=== 'error' ) {
11280 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11281 $listItem
.attr( 'role', 'alert' );
11282 } else if ( kind
=== 'notice' ) {
11283 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
11287 message
= new OO
.ui
.LabelWidget( { label
: text
} );
11289 .append( $icon
, message
.$element
)
11290 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
11295 * Set the field alignment mode.
11298 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
11301 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
11302 if ( value
!== this.align
) {
11303 // Default to 'left'
11304 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
11308 if ( value
=== 'inline' && !this.isFieldInline() ) {
11311 // Reorder elements
11312 if ( value
=== 'top' ) {
11313 this.$header
.append( this.$label
, this.$help
);
11314 this.$body
.append( this.$header
, this.$field
);
11315 } else if ( value
=== 'inline' ) {
11316 this.$header
.append( this.$label
, this.$help
);
11317 this.$body
.append( this.$field
, this.$header
);
11319 this.$header
.append( this.$label
);
11320 this.$body
.append( this.$header
, this.$help
, this.$field
);
11322 // Set classes. The following classes can be used here:
11323 // * oo-ui-fieldLayout-align-left
11324 // * oo-ui-fieldLayout-align-right
11325 // * oo-ui-fieldLayout-align-top
11326 // * oo-ui-fieldLayout-align-inline
11327 if ( this.align
) {
11328 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
11330 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
11331 this.align
= value
;
11338 * Set the list of error messages.
11340 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11341 * The array may contain strings or OO.ui.HtmlSnippet instances.
11344 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
11345 this.errors
= errors
.slice();
11346 this.updateMessages();
11351 * Set the list of notice messages.
11353 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11354 * The array may contain strings or OO.ui.HtmlSnippet instances.
11357 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
11358 this.notices
= notices
.slice();
11359 this.updateMessages();
11364 * Update the rendering of error and notice messages.
11368 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
11370 this.$messages
.empty();
11372 if ( this.errors
.length
|| this.notices
.length
) {
11373 this.$body
.after( this.$messages
);
11375 this.$messages
.remove();
11379 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
11380 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
11382 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
11383 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
11388 * Include information about the widget's accessKey in our title. TitledElement calls this method.
11389 * (This is a bit of a hack.)
11392 * @param {string} title Tooltip label for 'title' attribute
11395 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
11396 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
11397 return this.fieldWidget
.formatTitleWithAccessKey( title
);
11403 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
11404 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
11405 * is required and is specified before any optional configuration settings.
11407 * Labels can be aligned in one of four ways:
11409 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11410 * A left-alignment is used for forms with many fields.
11411 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11412 * A right-alignment is used for long but familiar forms which users tab through,
11413 * verifying the current field with a quick glance at the label.
11414 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11415 * that users fill out from top to bottom.
11416 * - **inline**: The label is placed after the field-widget and aligned to the left.
11417 * An inline-alignment is best used with checkboxes or radio buttons.
11419 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
11420 * text is specified.
11423 * // Example of an ActionFieldLayout
11424 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
11425 * new OO.ui.TextInputWidget( {
11426 * placeholder: 'Field widget'
11428 * new OO.ui.ButtonWidget( {
11432 * label: 'An ActionFieldLayout. This label is aligned top',
11434 * help: 'This is help text'
11438 * $( 'body' ).append( actionFieldLayout.$element );
11441 * @extends OO.ui.FieldLayout
11444 * @param {OO.ui.Widget} fieldWidget Field widget
11445 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
11446 * @param {Object} config
11448 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
11449 // Allow passing positional parameters inside the config object
11450 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11451 config
= fieldWidget
;
11452 fieldWidget
= config
.fieldWidget
;
11453 buttonWidget
= config
.buttonWidget
;
11456 // Parent constructor
11457 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
11460 this.buttonWidget
= buttonWidget
;
11461 this.$button
= $( '<span>' );
11462 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11466 .addClass( 'oo-ui-actionFieldLayout' );
11468 .addClass( 'oo-ui-actionFieldLayout-button' )
11469 .append( this.buttonWidget
.$element
);
11471 .addClass( 'oo-ui-actionFieldLayout-input' )
11472 .append( this.fieldWidget
.$element
);
11474 .append( this.$input
, this.$button
);
11479 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
11482 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
11483 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
11484 * configured with a label as well. For more information and examples,
11485 * please see the [OOjs UI documentation on MediaWiki][1].
11488 * // Example of a fieldset layout
11489 * var input1 = new OO.ui.TextInputWidget( {
11490 * placeholder: 'A text input field'
11493 * var input2 = new OO.ui.TextInputWidget( {
11494 * placeholder: 'A text input field'
11497 * var fieldset = new OO.ui.FieldsetLayout( {
11498 * label: 'Example of a fieldset layout'
11501 * fieldset.addItems( [
11502 * new OO.ui.FieldLayout( input1, {
11503 * label: 'Field One'
11505 * new OO.ui.FieldLayout( input2, {
11506 * label: 'Field Two'
11509 * $( 'body' ).append( fieldset.$element );
11511 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
11514 * @extends OO.ui.Layout
11515 * @mixins OO.ui.mixin.IconElement
11516 * @mixins OO.ui.mixin.LabelElement
11517 * @mixins OO.ui.mixin.GroupElement
11520 * @param {Object} [config] Configuration options
11521 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
11522 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
11523 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
11524 * For important messages, you are advised to use `notices`, as they are always shown.
11525 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
11526 * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
11528 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
11529 // Configuration initialization
11530 config
= config
|| {};
11532 // Parent constructor
11533 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
11535 // Mixin constructors
11536 OO
.ui
.mixin
.IconElement
.call( this, config
);
11537 OO
.ui
.mixin
.LabelElement
.call( this, config
);
11538 OO
.ui
.mixin
.GroupElement
.call( this, config
);
11541 this.$header
= $( '<legend>' );
11542 if ( config
.help
) {
11543 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
11544 $overlay
: config
.$overlay
,
11548 classes
: [ 'oo-ui-fieldsetLayout-help' ],
11552 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
11553 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
11555 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
11557 this.$help
= this.popupButtonWidget
.$element
;
11559 this.$help
= $( [] );
11564 .addClass( 'oo-ui-fieldsetLayout-header' )
11565 .append( this.$icon
, this.$label
, this.$help
);
11566 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
11568 .addClass( 'oo-ui-fieldsetLayout' )
11569 .prepend( this.$header
, this.$group
);
11570 if ( Array
.isArray( config
.items
) ) {
11571 this.addItems( config
.items
);
11577 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
11578 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
11579 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
11580 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
11582 /* Static Properties */
11588 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
11591 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
11592 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
11593 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
11594 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
11596 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
11597 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
11598 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
11599 * some fancier controls. Some controls have both regular and InputWidget variants, for example
11600 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
11601 * often have simplified APIs to match the capabilities of HTML forms.
11602 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
11604 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
11605 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11608 * // Example of a form layout that wraps a fieldset layout
11609 * var input1 = new OO.ui.TextInputWidget( {
11610 * placeholder: 'Username'
11612 * var input2 = new OO.ui.TextInputWidget( {
11613 * placeholder: 'Password',
11616 * var submit = new OO.ui.ButtonInputWidget( {
11620 * var fieldset = new OO.ui.FieldsetLayout( {
11621 * label: 'A form layout'
11623 * fieldset.addItems( [
11624 * new OO.ui.FieldLayout( input1, {
11625 * label: 'Username',
11628 * new OO.ui.FieldLayout( input2, {
11629 * label: 'Password',
11632 * new OO.ui.FieldLayout( submit )
11634 * var form = new OO.ui.FormLayout( {
11635 * items: [ fieldset ],
11636 * action: '/api/formhandler',
11639 * $( 'body' ).append( form.$element );
11642 * @extends OO.ui.Layout
11643 * @mixins OO.ui.mixin.GroupElement
11646 * @param {Object} [config] Configuration options
11647 * @cfg {string} [method] HTML form `method` attribute
11648 * @cfg {string} [action] HTML form `action` attribute
11649 * @cfg {string} [enctype] HTML form `enctype` attribute
11650 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
11652 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
11655 // Configuration initialization
11656 config
= config
|| {};
11658 // Parent constructor
11659 OO
.ui
.FormLayout
.parent
.call( this, config
);
11661 // Mixin constructors
11662 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11665 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
11667 // Make sure the action is safe
11668 action
= config
.action
;
11669 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
11670 action
= './' + action
;
11675 .addClass( 'oo-ui-formLayout' )
11677 method
: config
.method
,
11679 enctype
: config
.enctype
11681 if ( Array
.isArray( config
.items
) ) {
11682 this.addItems( config
.items
);
11688 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
11689 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
11694 * A 'submit' event is emitted when the form is submitted.
11699 /* Static Properties */
11705 OO
.ui
.FormLayout
.static.tagName
= 'form';
11710 * Handle form submit events.
11713 * @param {jQuery.Event} e Submit event
11716 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
11717 if ( this.emit( 'submit' ) ) {
11723 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
11724 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
11727 * // Example of a panel layout
11728 * var panel = new OO.ui.PanelLayout( {
11732 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
11734 * $( 'body' ).append( panel.$element );
11737 * @extends OO.ui.Layout
11740 * @param {Object} [config] Configuration options
11741 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
11742 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
11743 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
11744 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
11746 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
11747 // Configuration initialization
11748 config
= $.extend( {
11755 // Parent constructor
11756 OO
.ui
.PanelLayout
.parent
.call( this, config
);
11759 this.$element
.addClass( 'oo-ui-panelLayout' );
11760 if ( config
.scrollable
) {
11761 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
11763 if ( config
.padded
) {
11764 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
11766 if ( config
.expanded
) {
11767 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
11769 if ( config
.framed
) {
11770 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
11776 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
11781 * Focus the panel layout
11783 * The default implementation just focuses the first focusable element in the panel
11785 OO
.ui
.PanelLayout
.prototype.focus = function () {
11786 OO
.ui
.findFocusable( this.$element
).focus();
11790 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
11791 * items), with small margins between them. Convenient when you need to put a number of block-level
11792 * widgets on a single line next to each other.
11794 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
11797 * // HorizontalLayout with a text input and a label
11798 * var layout = new OO.ui.HorizontalLayout( {
11800 * new OO.ui.LabelWidget( { label: 'Label' } ),
11801 * new OO.ui.TextInputWidget( { value: 'Text' } )
11804 * $( 'body' ).append( layout.$element );
11807 * @extends OO.ui.Layout
11808 * @mixins OO.ui.mixin.GroupElement
11811 * @param {Object} [config] Configuration options
11812 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
11814 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
11815 // Configuration initialization
11816 config
= config
|| {};
11818 // Parent constructor
11819 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
11821 // Mixin constructors
11822 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11825 this.$element
.addClass( 'oo-ui-horizontalLayout' );
11826 if ( Array
.isArray( config
.items
) ) {
11827 this.addItems( config
.items
);
11833 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
11834 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
11838 //# sourceMappingURL=oojs-ui-core.js.map