3 * https://www.mediawiki.org/wiki/OOjs_UI
5 * Copyright 2011–2016 OOjs UI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2016-02-02T22:07:00Z
16 * Namespace for all classes, static methods and static properties.
48 * Constants for MouseEvent.which
52 OO
.ui
.MouseButtons
= {
64 * Generate a unique ID for element
66 * @return {String} [id]
68 OO
.ui
.generateElementId = function () {
70 return 'oojsui-' + OO
.ui
.elementId
;
74 * Check if an element is focusable.
75 * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
77 * @param {jQuery} element Element to test
80 OO
.ui
.isFocusableElement = function ( $element
) {
82 element
= $element
[ 0 ];
84 // Anything disabled is not focusable
85 if ( element
.disabled
) {
89 // Check if the element is visible
91 // This is quicker than calling $element.is( ':visible' )
92 $.expr
.filters
.visible( element
) &&
93 // Check that all parents are visible
94 !$element
.parents().addBack().filter( function () {
95 return $.css( this, 'visibility' ) === 'hidden';
101 // Check if the element is ContentEditable, which is the string 'true'
102 if ( element
.contentEditable
=== 'true' ) {
106 // Anything with a non-negative numeric tabIndex is focusable.
107 // Use .prop to avoid browser bugs
108 if ( $element
.prop( 'tabIndex' ) >= 0 ) {
112 // Some element types are naturally focusable
113 // (indexOf is much faster than regex in Chrome and about the
114 // same in FF: https://jsperf.com/regex-vs-indexof-array2)
115 nodeName
= element
.nodeName
.toLowerCase();
116 if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName
) !== -1 ) {
120 // Links and areas are focusable if they have an href
121 if ( ( nodeName
=== 'a' || nodeName
=== 'area' ) && $element
.attr( 'href' ) !== undefined ) {
129 * Find a focusable child
131 * @param {jQuery} $container Container to search in
132 * @param {boolean} [backwards] Search backwards
133 * @return {jQuery} Focusable child, an empty jQuery object if none found
135 OO
.ui
.findFocusable = function ( $container
, backwards
) {
136 var $focusable
= $( [] ),
137 // $focusableCandidates is a superset of things that
138 // could get matched by isFocusableElement
139 $focusableCandidates
= $container
140 .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
143 $focusableCandidates
= Array
.prototype.reverse
.call( $focusableCandidates
);
146 $focusableCandidates
.each( function () {
147 var $this = $( this );
148 if ( OO
.ui
.isFocusableElement( $this ) ) {
157 * Get the user's language and any fallback languages.
159 * These language codes are used to localize user interface elements in the user's language.
161 * In environments that provide a localization system, this function should be overridden to
162 * return the user's language(s). The default implementation returns English (en) only.
164 * @return {string[]} Language codes, in descending order of priority
166 OO
.ui
.getUserLanguages = function () {
171 * Get a value in an object keyed by language code.
173 * @param {Object.<string,Mixed>} obj Object keyed by language code
174 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
175 * @param {string} [fallback] Fallback code, used if no matching language can be found
176 * @return {Mixed} Local value
178 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
181 // Requested language
185 // Known user language
186 langs
= OO
.ui
.getUserLanguages();
187 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
194 if ( obj
[ fallback
] ) {
195 return obj
[ fallback
];
197 // First existing language
198 for ( lang
in obj
) {
206 * Check if a node is contained within another node
208 * Similar to jQuery#contains except a list of containers can be supplied
209 * and a boolean argument allows you to include the container in the match list
211 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
212 * @param {HTMLElement} contained Node to find
213 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
214 * @return {boolean} The node is in the list of target nodes
216 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
218 if ( !Array
.isArray( containers
) ) {
219 containers
= [ containers
];
221 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
222 if ( ( matchContainers
&& contained
=== containers
[ i
] ) || $.contains( containers
[ i
], contained
) ) {
230 * Return a function, that, as long as it continues to be invoked, will not
231 * be triggered. The function will be called after it stops being called for
232 * N milliseconds. If `immediate` is passed, trigger the function on the
233 * leading edge, instead of the trailing.
235 * Ported from: http://underscorejs.org/underscore.js
237 * @param {Function} func
238 * @param {number} wait
239 * @param {boolean} immediate
242 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
247 later = function () {
250 func
.apply( context
, args
);
253 if ( immediate
&& !timeout
) {
254 func
.apply( context
, args
);
256 clearTimeout( timeout
);
257 timeout
= setTimeout( later
, wait
);
262 * Proxy for `node.addEventListener( eventName, handler, true )`.
264 * @param {HTMLElement} node
265 * @param {string} eventName
266 * @param {Function} handler
269 OO
.ui
.addCaptureEventListener = function ( node
, eventName
, handler
) {
270 node
.addEventListener( eventName
, handler
, true );
274 * Proxy for `node.removeEventListener( eventName, handler, true )`.
276 * @param {HTMLElement} node
277 * @param {string} eventName
278 * @param {Function} handler
281 OO
.ui
.removeCaptureEventListener = function ( node
, eventName
, handler
) {
282 node
.removeEventListener( eventName
, handler
, true );
286 * Reconstitute a JavaScript object corresponding to a widget created by
287 * the PHP implementation.
289 * This is an alias for `OO.ui.Element.static.infuse()`.
291 * @param {string|HTMLElement|jQuery} idOrNode
292 * A DOM id (if a string) or node for the widget to infuse.
293 * @return {OO.ui.Element}
294 * The `OO.ui.Element` corresponding to this (infusable) document node.
296 OO
.ui
.infuse = function ( idOrNode
) {
297 return OO
.ui
.Element
.static.infuse( idOrNode
);
302 * Message store for the default implementation of OO.ui.msg
304 * Environments that provide a localization system should not use this, but should override
305 * OO.ui.msg altogether.
310 // Tool tip for a button that moves items in a list down one place
311 'ooui-outline-control-move-down': 'Move item down',
312 // Tool tip for a button that moves items in a list up one place
313 'ooui-outline-control-move-up': 'Move item up',
314 // Tool tip for a button that removes items from a list
315 'ooui-outline-control-remove': 'Remove item',
316 // Label for the toolbar group that contains a list of all other available tools
317 'ooui-toolbar-more': 'More',
318 // Label for the fake tool that expands the full list of tools in a toolbar group
319 'ooui-toolgroup-expand': 'More',
320 // Label for the fake tool that collapses the full list of tools in a toolbar group
321 'ooui-toolgroup-collapse': 'Fewer',
322 // Default label for the accept button of a confirmation dialog
323 'ooui-dialog-message-accept': 'OK',
324 // Default label for the reject button of a confirmation dialog
325 'ooui-dialog-message-reject': 'Cancel',
326 // Title for process dialog error description
327 'ooui-dialog-process-error': 'Something went wrong',
328 // Label for process dialog dismiss error button, visible when describing errors
329 'ooui-dialog-process-dismiss': 'Dismiss',
330 // Label for process dialog retry action button, visible when describing only recoverable errors
331 'ooui-dialog-process-retry': 'Try again',
332 // Label for process dialog retry action button, visible when describing only warnings
333 'ooui-dialog-process-continue': 'Continue',
334 // Label for the file selection widget's select file button
335 'ooui-selectfile-button-select': 'Select a file',
336 // Label for the file selection widget if file selection is not supported
337 'ooui-selectfile-not-supported': 'File selection is not supported',
338 // Label for the file selection widget when no file is currently selected
339 'ooui-selectfile-placeholder': 'No file is selected',
340 // Label for the file selection widget's drop target
341 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
345 * Get a localized message.
347 * In environments that provide a localization system, this function should be overridden to
348 * return the message translated in the user's language. The default implementation always returns
351 * After the message key, message parameters may optionally be passed. In the default implementation,
352 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
353 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
354 * they support unnamed, ordered message parameters.
356 * @param {string} key Message key
357 * @param {Mixed...} [params] Message parameters
358 * @return {string} Translated message with parameters substituted
360 OO
.ui
.msg = function ( key
) {
361 var message
= messages
[ key
],
362 params
= Array
.prototype.slice
.call( arguments
, 1 );
363 if ( typeof message
=== 'string' ) {
364 // Perform $1 substitution
365 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
366 var i
= parseInt( n
, 10 );
367 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
370 // Return placeholder if message not found
371 message
= '[' + key
+ ']';
378 * Package a message and arguments for deferred resolution.
380 * Use this when you are statically specifying a message and the message may not yet be present.
382 * @param {string} key Message key
383 * @param {Mixed...} [params] Message parameters
384 * @return {Function} Function that returns the resolved message when executed
386 OO
.ui
.deferMsg = function () {
387 var args
= arguments
;
389 return OO
.ui
.msg
.apply( OO
.ui
, args
);
396 * If the message is a function it will be executed, otherwise it will pass through directly.
398 * @param {Function|string} msg Deferred message, or message text
399 * @return {string} Resolved message
401 OO
.ui
.resolveMsg = function ( msg
) {
402 if ( $.isFunction( msg
) ) {
409 * @param {string} url
412 OO
.ui
.isSafeUrl = function ( url
) {
413 // Keep this function in sync with php/Tag.php
414 var i
, protocolWhitelist
;
416 function stringStartsWith( haystack
, needle
) {
417 return haystack
.substr( 0, needle
.length
) === needle
;
420 protocolWhitelist
= [
421 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
422 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
423 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
430 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
431 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
436 // This matches '//' too
437 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
440 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
452 * Namespace for OOjs UI mixins.
454 * Mixins are named according to the type of object they are intended to
455 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
456 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
457 * is intended to be mixed in to an instance of OO.ui.Widget.
465 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
466 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
467 * connected to them and can't be interacted with.
473 * @param {Object} [config] Configuration options
474 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
475 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
477 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
478 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
479 * @cfg {string} [text] Text to insert
480 * @cfg {Array} [content] An array of content elements to append (after #text).
481 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
482 * Instances of OO.ui.Element will have their $element appended.
483 * @cfg {jQuery} [$content] Content elements to append (after #text).
484 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
485 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
486 * Data can also be specified with the #setData method.
488 OO
.ui
.Element
= function OoUiElement( config
) {
489 // Configuration initialization
490 config
= config
|| {};
495 this.data
= config
.data
;
496 this.$element
= config
.$element
||
497 $( document
.createElement( this.getTagName() ) );
498 this.elementGroup
= null;
499 this.debouncedUpdateThemeClassesHandler
= OO
.ui
.debounce( this.debouncedUpdateThemeClasses
);
502 if ( Array
.isArray( config
.classes
) ) {
503 this.$element
.addClass( config
.classes
.join( ' ' ) );
506 this.$element
.attr( 'id', config
.id
);
509 this.$element
.text( config
.text
);
511 if ( config
.content
) {
512 // The `content` property treats plain strings as text; use an
513 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
514 // appropriate $element appended.
515 this.$element
.append( config
.content
.map( function ( v
) {
516 if ( typeof v
=== 'string' ) {
517 // Escape string so it is properly represented in HTML.
518 return document
.createTextNode( v
);
519 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
522 } else if ( v
instanceof OO
.ui
.Element
) {
528 if ( config
.$content
) {
529 // The `$content` property treats plain strings as HTML.
530 this.$element
.append( config
.$content
);
536 OO
.initClass( OO
.ui
.Element
);
538 /* Static Properties */
541 * The name of the HTML tag used by the element.
543 * The static value may be ignored if the #getTagName method is overridden.
549 OO
.ui
.Element
.static.tagName
= 'div';
554 * Reconstitute a JavaScript object corresponding to a widget created
555 * by the PHP implementation.
557 * @param {string|HTMLElement|jQuery} idOrNode
558 * A DOM id (if a string) or node for the widget to infuse.
559 * @return {OO.ui.Element}
560 * The `OO.ui.Element` corresponding to this (infusable) document node.
561 * For `Tag` objects emitted on the HTML side (used occasionally for content)
562 * the value returned is a newly-created Element wrapping around the existing
565 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
566 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, false );
567 // Verify that the type matches up.
568 // FIXME: uncomment after T89721 is fixed (see T90929)
570 if ( !( obj instanceof this['class'] ) ) {
571 throw new Error( 'Infusion type mismatch!' );
578 * Implementation helper for `infuse`; skips the type check and has an
579 * extra property so that only the top-level invocation touches the DOM.
581 * @param {string|HTMLElement|jQuery} idOrNode
582 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
583 * when the top-level widget of this infusion is inserted into DOM,
584 * replacing the original node; or false for top-level invocation.
585 * @return {OO.ui.Element}
587 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, domPromise
) {
588 // look for a cached result of a previous infusion.
589 var id
, $elem
, data
, cls
, parts
, parent
, obj
, top
, state
;
590 if ( typeof idOrNode
=== 'string' ) {
592 $elem
= $( document
.getElementById( id
) );
594 $elem
= $( idOrNode
);
595 id
= $elem
.attr( 'id' );
597 if ( !$elem
.length
) {
598 throw new Error( 'Widget not found: ' + id
);
600 data
= $elem
.data( 'ooui-infused' ) || $elem
[ 0 ].oouiInfused
;
603 if ( data
=== true ) {
604 throw new Error( 'Circular dependency! ' + id
);
608 data
= $elem
.attr( 'data-ooui' );
610 throw new Error( 'No infusion data found: ' + id
);
613 data
= $.parseJSON( data
);
617 if ( !( data
&& data
._
) ) {
618 throw new Error( 'No valid infusion data found: ' + id
);
620 if ( data
._
=== 'Tag' ) {
621 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
622 return new OO
.ui
.Element( { $element
: $elem
} );
624 parts
= data
._
.split( '.' );
625 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
626 if ( cls
=== undefined ) {
627 // The PHP output might be old and not including the "OO.ui" prefix
628 // TODO: Remove this back-compat after next major release
629 cls
= OO
.getProp
.apply( OO
, [ OO
.ui
].concat( parts
) );
630 if ( cls
=== undefined ) {
631 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
635 // Verify that we're creating an OO.ui.Element instance
638 while ( parent
!== undefined ) {
639 if ( parent
=== OO
.ui
.Element
) {
644 parent
= parent
.parent
;
647 if ( parent
!== OO
.ui
.Element
) {
648 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
651 if ( domPromise
=== false ) {
653 domPromise
= top
.promise();
655 $elem
.data( 'ooui-infused', true ); // prevent loops
656 data
.id
= id
; // implicit
657 data
= OO
.copy( data
, null, function deserialize( value
) {
658 if ( OO
.isPlainObject( value
) ) {
660 return OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, domPromise
);
663 return new OO
.ui
.HtmlSnippet( value
.html
);
667 // allow widgets to reuse parts of the DOM
668 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
669 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
670 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
672 // jscs:disable requireCapitalizedConstructors
673 obj
= new cls( data
);
674 // jscs:enable requireCapitalizedConstructors
675 // now replace old DOM with this new DOM.
677 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
678 // so only mutate the DOM if we need to.
679 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
680 $elem
.replaceWith( obj
.$element
);
681 // This element is now gone from the DOM, but if anyone is holding a reference to it,
682 // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
683 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
684 $elem
[ 0 ].oouiInfused
= obj
;
688 obj
.$element
.data( 'ooui-infused', obj
);
689 // set the 'data-ooui' attribute so we can identify infused widgets
690 obj
.$element
.attr( 'data-ooui', '' );
691 // restore dynamic state after the new element is inserted into DOM
692 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
697 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
699 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
700 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
701 * constructor, which will be given the enhanced config.
704 * @param {HTMLElement} node
705 * @param {Object} config
708 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
713 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
714 * (and its children) that represent an Element of the same class and the given configuration,
715 * generated by the PHP implementation.
717 * This method is called just before `node` is detached from the DOM. The return value of this
718 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
719 * is inserted into DOM to replace `node`.
722 * @param {HTMLElement} node
723 * @param {Object} config
726 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
731 * Get a jQuery function within a specific document.
734 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
735 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
737 * @return {Function} Bound jQuery function
739 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
740 function wrapper( selector
) {
741 return $( selector
, wrapper
.context
);
744 wrapper
.context
= this.getDocument( context
);
747 wrapper
.$iframe
= $iframe
;
754 * Get the document of an element.
757 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
758 * @return {HTMLDocument|null} Document object
760 OO
.ui
.Element
.static.getDocument = function ( obj
) {
761 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
762 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
763 // Empty jQuery selections might have a context
770 ( obj
.nodeType
=== 9 && obj
) ||
775 * Get the window of an element or document.
778 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
779 * @return {Window} Window object
781 OO
.ui
.Element
.static.getWindow = function ( obj
) {
782 var doc
= this.getDocument( obj
);
783 return doc
.defaultView
;
787 * Get the direction of an element or document.
790 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
791 * @return {string} Text direction, either 'ltr' or 'rtl'
793 OO
.ui
.Element
.static.getDir = function ( obj
) {
796 if ( obj
instanceof jQuery
) {
799 isDoc
= obj
.nodeType
=== 9;
800 isWin
= obj
.document
!== undefined;
801 if ( isDoc
|| isWin
) {
807 return $( obj
).css( 'direction' );
811 * Get the offset between two frames.
813 * TODO: Make this function not use recursion.
816 * @param {Window} from Window of the child frame
817 * @param {Window} [to=window] Window of the parent frame
818 * @param {Object} [offset] Offset to start with, used internally
819 * @return {Object} Offset object, containing left and top properties
821 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
822 var i
, len
, frames
, frame
, rect
;
828 offset
= { top
: 0, left
: 0 };
830 if ( from.parent
=== from ) {
834 // Get iframe element
835 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
836 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
837 if ( frames
[ i
].contentWindow
=== from ) {
843 // Recursively accumulate offset values
845 rect
= frame
.getBoundingClientRect();
846 offset
.left
+= rect
.left
;
847 offset
.top
+= rect
.top
;
849 this.getFrameOffset( from.parent
, offset
);
856 * Get the offset between two elements.
858 * The two elements may be in a different frame, but in that case the frame $element is in must
859 * be contained in the frame $anchor is in.
862 * @param {jQuery} $element Element whose position to get
863 * @param {jQuery} $anchor Element to get $element's position relative to
864 * @return {Object} Translated position coordinates, containing top and left properties
866 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
867 var iframe
, iframePos
,
868 pos
= $element
.offset(),
869 anchorPos
= $anchor
.offset(),
870 elementDocument
= this.getDocument( $element
),
871 anchorDocument
= this.getDocument( $anchor
);
873 // If $element isn't in the same document as $anchor, traverse up
874 while ( elementDocument
!== anchorDocument
) {
875 iframe
= elementDocument
.defaultView
.frameElement
;
877 throw new Error( '$element frame is not contained in $anchor frame' );
879 iframePos
= $( iframe
).offset();
880 pos
.left
+= iframePos
.left
;
881 pos
.top
+= iframePos
.top
;
882 elementDocument
= iframe
.ownerDocument
;
884 pos
.left
-= anchorPos
.left
;
885 pos
.top
-= anchorPos
.top
;
890 * Get element border sizes.
893 * @param {HTMLElement} el Element to measure
894 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
896 OO
.ui
.Element
.static.getBorders = function ( el
) {
897 var doc
= el
.ownerDocument
,
898 win
= doc
.defaultView
,
899 style
= win
.getComputedStyle( el
, null ),
901 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
902 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
903 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
904 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
915 * Get dimensions of an element or window.
918 * @param {HTMLElement|Window} el Element to measure
919 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
921 OO
.ui
.Element
.static.getDimensions = function ( el
) {
923 doc
= el
.ownerDocument
|| el
.document
,
924 win
= doc
.defaultView
;
926 if ( win
=== el
|| el
=== doc
.documentElement
) {
929 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
931 top
: $win
.scrollTop(),
932 left
: $win
.scrollLeft()
934 scrollbar
: { right
: 0, bottom
: 0 },
938 bottom
: $win
.innerHeight(),
939 right
: $win
.innerWidth()
945 borders
: this.getBorders( el
),
947 top
: $el
.scrollTop(),
948 left
: $el
.scrollLeft()
951 right
: $el
.innerWidth() - el
.clientWidth
,
952 bottom
: $el
.innerHeight() - el
.clientHeight
954 rect
: el
.getBoundingClientRect()
960 * Get scrollable object parent
962 * documentElement can't be used to get or set the scrollTop
963 * property on Blink. Changing and testing its value lets us
964 * use 'body' or 'documentElement' based on what is working.
966 * https://code.google.com/p/chromium/issues/detail?id=303131
969 * @param {HTMLElement} el Element to find scrollable parent for
970 * @return {HTMLElement} Scrollable parent
972 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
975 if ( OO
.ui
.scrollableElement
=== undefined ) {
976 body
= el
.ownerDocument
.body
;
977 scrollTop
= body
.scrollTop
;
980 if ( body
.scrollTop
=== 1 ) {
981 body
.scrollTop
= scrollTop
;
982 OO
.ui
.scrollableElement
= 'body';
984 OO
.ui
.scrollableElement
= 'documentElement';
988 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
992 * Get closest scrollable container.
994 * Traverses up until either a scrollable element or the root is reached, in which case the window
998 * @param {HTMLElement} el Element to find scrollable container for
999 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1000 * @return {HTMLElement} Closest scrollable container
1002 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1004 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1005 props
= [ 'overflow-x', 'overflow-y' ],
1006 $parent
= $( el
).parent();
1008 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1009 props
= [ 'overflow-' + dimension
];
1012 while ( $parent
.length
) {
1013 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1014 return $parent
[ 0 ];
1018 val
= $parent
.css( props
[ i
] );
1019 if ( val
=== 'auto' || val
=== 'scroll' ) {
1020 return $parent
[ 0 ];
1023 $parent
= $parent
.parent();
1025 return this.getDocument( el
).body
;
1029 * Scroll element into view.
1032 * @param {HTMLElement} el Element to scroll into view
1033 * @param {Object} [config] Configuration options
1034 * @param {string} [config.duration] jQuery animation duration value
1035 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1036 * to scroll in both directions
1037 * @param {Function} [config.complete] Function to call when scrolling completes
1039 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1040 var rel
, anim
, callback
, sc
, $sc
, eld
, scd
, $win
;
1042 // Configuration initialization
1043 config
= config
|| {};
1046 callback
= typeof config
.complete
=== 'function' && config
.complete
;
1047 sc
= this.getClosestScrollableContainer( el
, config
.direction
);
1049 eld
= this.getDimensions( el
);
1050 scd
= this.getDimensions( sc
);
1051 $win
= $( this.getWindow( el
) );
1053 // Compute the distances between the edges of el and the edges of the scroll viewport
1054 if ( $sc
.is( 'html, body' ) ) {
1055 // If the scrollable container is the root, this is easy
1058 bottom
: $win
.innerHeight() - eld
.rect
.bottom
,
1059 left
: eld
.rect
.left
,
1060 right
: $win
.innerWidth() - eld
.rect
.right
1063 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1065 top
: eld
.rect
.top
- ( scd
.rect
.top
+ scd
.borders
.top
),
1066 bottom
: scd
.rect
.bottom
- scd
.borders
.bottom
- scd
.scrollbar
.bottom
- eld
.rect
.bottom
,
1067 left
: eld
.rect
.left
- ( scd
.rect
.left
+ scd
.borders
.left
),
1068 right
: scd
.rect
.right
- scd
.borders
.right
- scd
.scrollbar
.right
- eld
.rect
.right
1072 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1073 if ( rel
.top
< 0 ) {
1074 anim
.scrollTop
= scd
.scroll
.top
+ rel
.top
;
1075 } else if ( rel
.top
> 0 && rel
.bottom
< 0 ) {
1076 anim
.scrollTop
= scd
.scroll
.top
+ Math
.min( rel
.top
, -rel
.bottom
);
1079 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1080 if ( rel
.left
< 0 ) {
1081 anim
.scrollLeft
= scd
.scroll
.left
+ rel
.left
;
1082 } else if ( rel
.left
> 0 && rel
.right
< 0 ) {
1083 anim
.scrollLeft
= scd
.scroll
.left
+ Math
.min( rel
.left
, -rel
.right
);
1086 if ( !$.isEmptyObject( anim
) ) {
1087 $sc
.stop( true ).animate( anim
, config
.duration
|| 'fast' );
1089 $sc
.queue( function ( next
) {
1102 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1103 * and reserve space for them, because it probably doesn't.
1105 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1106 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1107 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1108 * and then reattach (or show) them back.
1111 * @param {HTMLElement} el Element to reconsider the scrollbars on
1113 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1114 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1115 // Save scroll position
1116 scrollLeft
= el
.scrollLeft
;
1117 scrollTop
= el
.scrollTop
;
1118 // Detach all children
1119 while ( el
.firstChild
) {
1120 nodes
.push( el
.firstChild
);
1121 el
.removeChild( el
.firstChild
);
1124 void el
.offsetHeight
;
1125 // Reattach all children
1126 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1127 el
.appendChild( nodes
[ i
] );
1129 // Restore scroll position (no-op if scrollbars disappeared)
1130 el
.scrollLeft
= scrollLeft
;
1131 el
.scrollTop
= scrollTop
;
1137 * Toggle visibility of an element.
1139 * @param {boolean} [show] Make element visible, omit to toggle visibility
1143 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1144 show
= show
=== undefined ? !this.visible
: !!show
;
1146 if ( show
!== this.isVisible() ) {
1147 this.visible
= show
;
1148 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1149 this.emit( 'toggle', show
);
1156 * Check if element is visible.
1158 * @return {boolean} element is visible
1160 OO
.ui
.Element
.prototype.isVisible = function () {
1161 return this.visible
;
1167 * @return {Mixed} Element data
1169 OO
.ui
.Element
.prototype.getData = function () {
1176 * @param {Mixed} Element data
1179 OO
.ui
.Element
.prototype.setData = function ( data
) {
1185 * Check if element supports one or more methods.
1187 * @param {string|string[]} methods Method or list of methods to check
1188 * @return {boolean} All methods are supported
1190 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1194 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1195 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1196 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1201 return methods
.length
=== support
;
1205 * Update the theme-provided classes.
1207 * @localdoc This is called in element mixins and widget classes any time state changes.
1208 * Updating is debounced, minimizing overhead of changing multiple attributes and
1209 * guaranteeing that theme updates do not occur within an element's constructor
1211 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1212 this.debouncedUpdateThemeClassesHandler();
1217 * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
1218 * make them synchronous.
1220 OO
.ui
.Element
.prototype.debouncedUpdateThemeClasses = function () {
1221 OO
.ui
.theme
.updateElementClasses( this );
1225 * Get the HTML tag name.
1227 * Override this method to base the result on instance information.
1229 * @return {string} HTML tag name
1231 OO
.ui
.Element
.prototype.getTagName = function () {
1232 return this.constructor.static.tagName
;
1236 * Check if the element is attached to the DOM
1237 * @return {boolean} The element is attached to the DOM
1239 OO
.ui
.Element
.prototype.isElementAttached = function () {
1240 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1244 * Get the DOM document.
1246 * @return {HTMLDocument} Document object
1248 OO
.ui
.Element
.prototype.getElementDocument = function () {
1249 // Don't cache this in other ways either because subclasses could can change this.$element
1250 return OO
.ui
.Element
.static.getDocument( this.$element
);
1254 * Get the DOM window.
1256 * @return {Window} Window object
1258 OO
.ui
.Element
.prototype.getElementWindow = function () {
1259 return OO
.ui
.Element
.static.getWindow( this.$element
);
1263 * Get closest scrollable container.
1265 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1266 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1270 * Get group element is in.
1272 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1274 OO
.ui
.Element
.prototype.getElementGroup = function () {
1275 return this.elementGroup
;
1279 * Set group element is in.
1281 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1284 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1285 this.elementGroup
= group
;
1290 * Scroll element into view.
1292 * @param {Object} [config] Configuration options
1294 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1295 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1299 * Restore the pre-infusion dynamic state for this widget.
1301 * This method is called after #$element has been inserted into DOM. The parameter is the return
1302 * value of #gatherPreInfuseState.
1305 * @param {Object} state
1307 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1311 * Wraps an HTML snippet for use with configuration values which default
1312 * to strings. This bypasses the default html-escaping done to string
1318 * @param {string} [content] HTML content
1320 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1322 this.content
= content
;
1327 OO
.initClass( OO
.ui
.HtmlSnippet
);
1334 * @return {string} Unchanged HTML snippet.
1336 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1337 return this.content
;
1341 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1342 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1343 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1344 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1345 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1349 * @extends OO.ui.Element
1350 * @mixins OO.EventEmitter
1353 * @param {Object} [config] Configuration options
1355 OO
.ui
.Layout
= function OoUiLayout( config
) {
1356 // Configuration initialization
1357 config
= config
|| {};
1359 // Parent constructor
1360 OO
.ui
.Layout
.parent
.call( this, config
);
1362 // Mixin constructors
1363 OO
.EventEmitter
.call( this );
1366 this.$element
.addClass( 'oo-ui-layout' );
1371 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1372 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1375 * Widgets are compositions of one or more OOjs UI elements that users can both view
1376 * and interact with. All widgets can be configured and modified via a standard API,
1377 * and their state can change dynamically according to a model.
1381 * @extends OO.ui.Element
1382 * @mixins OO.EventEmitter
1385 * @param {Object} [config] Configuration options
1386 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1387 * appearance reflects this state.
1389 OO
.ui
.Widget
= function OoUiWidget( config
) {
1390 // Initialize config
1391 config
= $.extend( { disabled
: false }, config
);
1393 // Parent constructor
1394 OO
.ui
.Widget
.parent
.call( this, config
);
1396 // Mixin constructors
1397 OO
.EventEmitter
.call( this );
1400 this.disabled
= null;
1401 this.wasDisabled
= null;
1404 this.$element
.addClass( 'oo-ui-widget' );
1405 this.setDisabled( !!config
.disabled
);
1410 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1411 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1413 /* Static Properties */
1416 * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
1417 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1422 * @property {boolean}
1424 OO
.ui
.Widget
.static.supportsSimpleLabel
= false;
1431 * A 'disable' event is emitted when the disabled state of the widget changes
1432 * (i.e. on disable **and** enable).
1434 * @param {boolean} disabled Widget is disabled
1440 * A 'toggle' event is emitted when the visibility of the widget changes.
1442 * @param {boolean} visible Widget is visible
1448 * Check if the widget is disabled.
1450 * @return {boolean} Widget is disabled
1452 OO
.ui
.Widget
.prototype.isDisabled = function () {
1453 return this.disabled
;
1457 * Set the 'disabled' state of the widget.
1459 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1461 * @param {boolean} disabled Disable widget
1464 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1467 this.disabled
= !!disabled
;
1468 isDisabled
= this.isDisabled();
1469 if ( isDisabled
!== this.wasDisabled
) {
1470 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1471 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1472 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1473 this.emit( 'disable', isDisabled
);
1474 this.updateThemeClasses();
1476 this.wasDisabled
= isDisabled
;
1482 * Update the disabled state, in case of changes in parent widget.
1486 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1487 this.setDisabled( this.disabled
);
1498 * @param {Object} [config] Configuration options
1500 OO
.ui
.Theme
= function OoUiTheme( config
) {
1501 // Configuration initialization
1502 config
= config
|| {};
1507 OO
.initClass( OO
.ui
.Theme
);
1512 * Get a list of classes to be applied to a widget.
1514 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1515 * otherwise state transitions will not work properly.
1517 * @param {OO.ui.Element} element Element for which to get classes
1518 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1520 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1521 return { on
: [], off
: [] };
1525 * Update CSS classes provided by the theme.
1527 * For elements with theme logic hooks, this should be called any time there's a state change.
1529 * @param {OO.ui.Element} element Element for which to update classes
1530 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1532 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1533 var $elements
= $( [] ),
1534 classes
= this.getElementClasses( element
);
1536 if ( element
.$icon
) {
1537 $elements
= $elements
.add( element
.$icon
);
1539 if ( element
.$indicator
) {
1540 $elements
= $elements
.add( element
.$indicator
);
1544 .removeClass( classes
.off
.join( ' ' ) )
1545 .addClass( classes
.on
.join( ' ' ) );
1549 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1550 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1551 * order in which users will navigate through the focusable elements via the "tab" key.
1554 * // TabIndexedElement is mixed into the ButtonWidget class
1555 * // to provide a tabIndex property.
1556 * var button1 = new OO.ui.ButtonWidget( {
1560 * var button2 = new OO.ui.ButtonWidget( {
1564 * var button3 = new OO.ui.ButtonWidget( {
1568 * var button4 = new OO.ui.ButtonWidget( {
1572 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1578 * @param {Object} [config] Configuration options
1579 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1580 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1581 * functionality will be applied to it instead.
1582 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1583 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1584 * to remove the element from the tab-navigation flow.
1586 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1587 // Configuration initialization
1588 config
= $.extend( { tabIndex
: 0 }, config
);
1591 this.$tabIndexed
= null;
1592 this.tabIndex
= null;
1595 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1598 this.setTabIndex( config
.tabIndex
);
1599 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1604 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1609 * Set the element that should use the tabindex functionality.
1611 * This method is used to retarget a tabindex mixin so that its functionality applies
1612 * to the specified element. If an element is currently using the functionality, the mixin’s
1613 * effect on that element is removed before the new element is set up.
1615 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1618 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1619 var tabIndex
= this.tabIndex
;
1620 // Remove attributes from old $tabIndexed
1621 this.setTabIndex( null );
1622 // Force update of new $tabIndexed
1623 this.$tabIndexed
= $tabIndexed
;
1624 this.tabIndex
= tabIndex
;
1625 return this.updateTabIndex();
1629 * Set the value of the tabindex.
1631 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
1634 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1635 tabIndex
= typeof tabIndex
=== 'number' ? tabIndex
: null;
1637 if ( this.tabIndex
!== tabIndex
) {
1638 this.tabIndex
= tabIndex
;
1639 this.updateTabIndex();
1646 * Update the `tabindex` attribute, in case of changes to tab index or
1652 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1653 if ( this.$tabIndexed
) {
1654 if ( this.tabIndex
!== null ) {
1655 // Do not index over disabled elements
1656 this.$tabIndexed
.attr( {
1657 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1658 // Support: ChromeVox and NVDA
1659 // These do not seem to inherit aria-disabled from parent elements
1660 'aria-disabled': this.isDisabled().toString()
1663 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1670 * Handle disable events.
1673 * @param {boolean} disabled Element is disabled
1675 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1676 this.updateTabIndex();
1680 * Get the value of the tabindex.
1682 * @return {number|null} Tabindex value
1684 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
1685 return this.tabIndex
;
1689 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
1690 * interface element that can be configured with access keys for accessibility.
1691 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
1693 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
1698 * @param {Object} [config] Configuration options
1699 * @cfg {jQuery} [$button] The button element created by the class.
1700 * If this configuration is omitted, the button element will use a generated `<a>`.
1701 * @cfg {boolean} [framed=true] Render the button with a frame
1703 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
1704 // Configuration initialization
1705 config
= config
|| {};
1708 this.$button
= null;
1710 this.active
= false;
1711 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
1712 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
1713 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
1714 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
1715 this.onClickHandler
= this.onClick
.bind( this );
1716 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
1719 this.$element
.addClass( 'oo-ui-buttonElement' );
1720 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
1721 this.setButtonElement( config
.$button
|| $( '<a>' ) );
1726 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
1728 /* Static Properties */
1731 * Cancel mouse down events.
1733 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
1734 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
1735 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
1740 * @property {boolean}
1742 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
1747 * A 'click' event is emitted when the button element is clicked.
1755 * Set the button element.
1757 * This method is used to retarget a button mixin so that its functionality applies to
1758 * the specified button element instead of the one created by the class. If a button element
1759 * is already set, the method will remove the mixin’s effect on that element.
1761 * @param {jQuery} $button Element to use as button
1763 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
1764 if ( this.$button
) {
1766 .removeClass( 'oo-ui-buttonElement-button' )
1767 .removeAttr( 'role accesskey' )
1769 mousedown
: this.onMouseDownHandler
,
1770 keydown
: this.onKeyDownHandler
,
1771 click
: this.onClickHandler
,
1772 keypress
: this.onKeyPressHandler
1776 this.$button
= $button
1777 .addClass( 'oo-ui-buttonElement-button' )
1778 .attr( { role
: 'button' } )
1780 mousedown
: this.onMouseDownHandler
,
1781 keydown
: this.onKeyDownHandler
,
1782 click
: this.onClickHandler
,
1783 keypress
: this.onKeyPressHandler
1788 * Handles mouse down events.
1791 * @param {jQuery.Event} e Mouse down event
1793 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
1794 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
1797 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
1798 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
1799 // reliably remove the pressed class
1800 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
1801 // Prevent change of focus unless specifically configured otherwise
1802 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
1808 * Handles mouse up events.
1811 * @param {jQuery.Event} e Mouse up event
1813 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
1814 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
1817 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
1818 // Stop listening for mouseup, since we only needed this once
1819 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
1823 * Handles mouse click events.
1826 * @param {jQuery.Event} e Mouse click event
1829 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
1830 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
1831 if ( this.emit( 'click' ) ) {
1838 * Handles key down events.
1841 * @param {jQuery.Event} e Key down event
1843 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
1844 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
1847 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
1848 // Run the keyup handler no matter where the key is when the button is let go, so we can
1849 // reliably remove the pressed class
1850 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
1854 * Handles key up events.
1857 * @param {jQuery.Event} e Key up event
1859 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
1860 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
1863 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
1864 // Stop listening for keyup, since we only needed this once
1865 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
1869 * Handles key press events.
1872 * @param {jQuery.Event} e Key press event
1875 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
1876 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
1877 if ( this.emit( 'click' ) ) {
1884 * Check if button has a frame.
1886 * @return {boolean} Button is framed
1888 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
1893 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
1895 * @param {boolean} [framed] Make button framed, omit to toggle
1898 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
1899 framed
= framed
=== undefined ? !this.framed
: !!framed
;
1900 if ( framed
!== this.framed
) {
1901 this.framed
= framed
;
1903 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
1904 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
1905 this.updateThemeClasses();
1912 * Set the button's active state.
1914 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
1915 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
1916 * for other button types.
1918 * @param {boolean} value Make button active
1921 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
1922 this.active
= !!value
;
1923 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
1928 * Check if the button is active
1930 * @return {boolean} The button is active
1932 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
1937 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
1938 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
1939 * items from the group is done through the interface the class provides.
1940 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1942 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
1948 * @param {Object} [config] Configuration options
1949 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
1950 * is omitted, the group element will use a generated `<div>`.
1952 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
1953 // Configuration initialization
1954 config
= config
|| {};
1959 this.aggregateItemEvents
= {};
1962 this.setGroupElement( config
.$group
|| $( '<div>' ) );
1968 * Set the group element.
1970 * If an element is already set, items will be moved to the new element.
1972 * @param {jQuery} $group Element to use as group
1974 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
1977 this.$group
= $group
;
1978 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
1979 this.$group
.append( this.items
[ i
].$element
);
1984 * Check if a group contains no items.
1986 * @return {boolean} Group is empty
1988 OO
.ui
.mixin
.GroupElement
.prototype.isEmpty = function () {
1989 return !this.items
.length
;
1993 * Get all items in the group.
1995 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
1996 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
1999 * @return {OO.ui.Element[]} An array of items.
2001 OO
.ui
.mixin
.GroupElement
.prototype.getItems = function () {
2002 return this.items
.slice( 0 );
2006 * Get an item by its data.
2008 * Only the first item with matching data will be returned. To return all matching items,
2009 * use the #getItemsFromData method.
2011 * @param {Object} data Item data to search for
2012 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2014 OO
.ui
.mixin
.GroupElement
.prototype.getItemFromData = function ( data
) {
2016 hash
= OO
.getHash( data
);
2018 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2019 item
= this.items
[ i
];
2020 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2029 * Get items by their data.
2031 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2033 * @param {Object} data Item data to search for
2034 * @return {OO.ui.Element[]} Items with equivalent data
2036 OO
.ui
.mixin
.GroupElement
.prototype.getItemsFromData = function ( data
) {
2038 hash
= OO
.getHash( data
),
2041 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2042 item
= this.items
[ i
];
2043 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2052 * Aggregate the events emitted by the group.
2054 * When events are aggregated, the group will listen to all contained items for the event,
2055 * and then emit the event under a new name. The new event will contain an additional leading
2056 * parameter containing the item that emitted the original event. Other arguments emitted from
2057 * the original event are passed through.
2059 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
2060 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
2061 * A `null` value will remove aggregated events.
2063 * @throws {Error} An error is thrown if aggregation already exists.
2065 OO
.ui
.mixin
.GroupElement
.prototype.aggregate = function ( events
) {
2066 var i
, len
, item
, add
, remove
, itemEvent
, groupEvent
;
2068 for ( itemEvent
in events
) {
2069 groupEvent
= events
[ itemEvent
];
2071 // Remove existing aggregated event
2072 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2073 // Don't allow duplicate aggregations
2075 throw new Error( 'Duplicate item event aggregation for ' + itemEvent
);
2077 // Remove event aggregation from existing items
2078 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2079 item
= this.items
[ i
];
2080 if ( item
.connect
&& item
.disconnect
) {
2082 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2083 item
.disconnect( this, remove
);
2086 // Prevent future items from aggregating event
2087 delete this.aggregateItemEvents
[ itemEvent
];
2090 // Add new aggregate event
2092 // Make future items aggregate event
2093 this.aggregateItemEvents
[ itemEvent
] = groupEvent
;
2094 // Add event aggregation to existing items
2095 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2096 item
= this.items
[ i
];
2097 if ( item
.connect
&& item
.disconnect
) {
2099 add
[ itemEvent
] = [ 'emit', groupEvent
, item
];
2100 item
.connect( this, add
);
2108 * Add items to the group.
2110 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2111 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2113 * @param {OO.ui.Element[]} items An array of items to add to the group
2114 * @param {number} [index] Index of the insertion point
2117 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2118 var i
, len
, item
, event
, events
, currentIndex
,
2121 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2124 // Check if item exists then remove it first, effectively "moving" it
2125 currentIndex
= this.items
.indexOf( item
);
2126 if ( currentIndex
>= 0 ) {
2127 this.removeItems( [ item
] );
2128 // Adjust index to compensate for removal
2129 if ( currentIndex
< index
) {
2134 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
2136 for ( event
in this.aggregateItemEvents
) {
2137 events
[ event
] = [ 'emit', this.aggregateItemEvents
[ event
], item
];
2139 item
.connect( this, events
);
2141 item
.setElementGroup( this );
2142 itemElements
.push( item
.$element
.get( 0 ) );
2145 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2146 this.$group
.append( itemElements
);
2147 this.items
.push
.apply( this.items
, items
);
2148 } else if ( index
=== 0 ) {
2149 this.$group
.prepend( itemElements
);
2150 this.items
.unshift
.apply( this.items
, items
);
2152 this.items
[ index
].$element
.before( itemElements
);
2153 this.items
.splice
.apply( this.items
, [ index
, 0 ].concat( items
) );
2160 * Remove the specified items from a group.
2162 * Removed items are detached (not removed) from the DOM so that they may be reused.
2163 * To remove all items from a group, you may wish to use the #clearItems method instead.
2165 * @param {OO.ui.Element[]} items An array of items to remove
2168 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2169 var i
, len
, item
, index
, remove
, itemEvent
;
2171 // Remove specific items
2172 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2174 index
= this.items
.indexOf( item
);
2175 if ( index
!== -1 ) {
2177 item
.connect
&& item
.disconnect
&&
2178 !$.isEmptyObject( this.aggregateItemEvents
)
2181 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2182 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2184 item
.disconnect( this, remove
);
2186 item
.setElementGroup( null );
2187 this.items
.splice( index
, 1 );
2188 item
.$element
.detach();
2196 * Clear all items from the group.
2198 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2199 * To remove only a subset of items from a group, use the #removeItems method.
2203 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2204 var i
, len
, item
, remove
, itemEvent
;
2207 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2208 item
= this.items
[ i
];
2210 item
.connect
&& item
.disconnect
&&
2211 !$.isEmptyObject( this.aggregateItemEvents
)
2214 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
2215 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
2217 item
.disconnect( this, remove
);
2219 item
.setElementGroup( null );
2220 item
.$element
.detach();
2228 * IconElement is often mixed into other classes to generate an icon.
2229 * Icons are graphics, about the size of normal text. They are used to aid the user
2230 * in locating a control or to convey information in a space-efficient way. See the
2231 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2232 * included in the library.
2234 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2240 * @param {Object} [config] Configuration options
2241 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2242 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2243 * the icon element be set to an existing icon instead of the one generated by this class, set a
2244 * value using a jQuery selection. For example:
2246 * // Use a <div> tag instead of a <span>
2248 * // Use an existing icon element instead of the one generated by the class
2249 * $icon: this.$element
2250 * // Use an icon element from a child widget
2251 * $icon: this.childwidget.$element
2252 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2253 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2254 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2255 * by the user's language.
2257 * Example of an i18n map:
2259 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2260 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2261 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2262 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2263 * text. The icon title is displayed when users move the mouse over the icon.
2265 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2266 // Configuration initialization
2267 config
= config
|| {};
2272 this.iconTitle
= null;
2275 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2276 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2277 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2282 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2284 /* Static Properties */
2287 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2288 * for i18n purposes and contains a `default` icon name and additional names keyed by
2289 * language code. The `default` name is used when no icon is keyed by the user's language.
2291 * Example of an i18n map:
2293 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2295 * Note: the static property will be overridden if the #icon configuration is used.
2299 * @property {Object|string}
2301 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2304 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2305 * function that returns title text, or `null` for no title.
2307 * The static property will be overridden if the #iconTitle configuration is used.
2311 * @property {string|Function|null}
2313 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2318 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2319 * applies to the specified icon element instead of the one created by the class. If an icon
2320 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2321 * and mixin methods will no longer affect the element.
2323 * @param {jQuery} $icon Element to use as icon
2325 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2328 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2329 .removeAttr( 'title' );
2333 .addClass( 'oo-ui-iconElement-icon' )
2334 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2335 if ( this.iconTitle
!== null ) {
2336 this.$icon
.attr( 'title', this.iconTitle
);
2339 this.updateThemeClasses();
2343 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2344 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2347 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2348 * by language code, or `null` to remove the icon.
2351 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2352 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2353 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2355 if ( this.icon
!== icon
) {
2357 if ( this.icon
!== null ) {
2358 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2360 if ( icon
!== null ) {
2361 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2367 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2368 this.updateThemeClasses();
2374 * Set the icon title. Use `null` to remove the title.
2376 * @param {string|Function|null} iconTitle A text string used as the icon title,
2377 * a function that returns title text, or `null` for no title.
2380 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2381 iconTitle
= typeof iconTitle
=== 'function' ||
2382 ( typeof iconTitle
=== 'string' && iconTitle
.length
) ?
2383 OO
.ui
.resolveMsg( iconTitle
) : null;
2385 if ( this.iconTitle
!== iconTitle
) {
2386 this.iconTitle
= iconTitle
;
2388 if ( this.iconTitle
!== null ) {
2389 this.$icon
.attr( 'title', iconTitle
);
2391 this.$icon
.removeAttr( 'title' );
2400 * Get the symbolic name of the icon.
2402 * @return {string} Icon name
2404 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2409 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2411 * @return {string} Icon title text
2413 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2414 return this.iconTitle
;
2418 * IndicatorElement is often mixed into other classes to generate an indicator.
2419 * Indicators are small graphics that are generally used in two ways:
2421 * - To draw attention to the status of an item. For example, an indicator might be
2422 * used to show that an item in a list has errors that need to be resolved.
2423 * - To clarify the function of a control that acts in an exceptional way (a button
2424 * that opens a menu instead of performing an action directly, for example).
2426 * For a list of indicators included in the library, please see the
2427 * [OOjs UI documentation on MediaWiki] [1].
2429 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2435 * @param {Object} [config] Configuration options
2436 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2437 * configuration is omitted, the indicator element will use a generated `<span>`.
2438 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2439 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2441 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2442 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2443 * or a function that returns title text. The indicator title is displayed when users move
2444 * the mouse over the indicator.
2446 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2447 // Configuration initialization
2448 config
= config
|| {};
2451 this.$indicator
= null;
2452 this.indicator
= null;
2453 this.indicatorTitle
= null;
2456 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2457 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2458 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2463 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2465 /* Static Properties */
2468 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2469 * The static property will be overridden if the #indicator configuration is used.
2473 * @property {string|null}
2475 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2478 * A text string used as the indicator title, a function that returns title text, or `null`
2479 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2483 * @property {string|Function|null}
2485 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2490 * Set the indicator element.
2492 * If an element is already set, it will be cleaned up before setting up the new element.
2494 * @param {jQuery} $indicator Element to use as indicator
2496 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2497 if ( this.$indicator
) {
2499 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2500 .removeAttr( 'title' );
2503 this.$indicator
= $indicator
2504 .addClass( 'oo-ui-indicatorElement-indicator' )
2505 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2506 if ( this.indicatorTitle
!== null ) {
2507 this.$indicator
.attr( 'title', this.indicatorTitle
);
2510 this.updateThemeClasses();
2514 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2516 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2519 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2520 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2522 if ( this.indicator
!== indicator
) {
2523 if ( this.$indicator
) {
2524 if ( this.indicator
!== null ) {
2525 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2527 if ( indicator
!== null ) {
2528 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2531 this.indicator
= indicator
;
2534 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2535 this.updateThemeClasses();
2541 * Set the indicator title.
2543 * The title is displayed when a user moves the mouse over the indicator.
2545 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
2546 * `null` for no indicator title
2549 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2550 indicatorTitle
= typeof indicatorTitle
=== 'function' ||
2551 ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ?
2552 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2554 if ( this.indicatorTitle
!== indicatorTitle
) {
2555 this.indicatorTitle
= indicatorTitle
;
2556 if ( this.$indicator
) {
2557 if ( this.indicatorTitle
!== null ) {
2558 this.$indicator
.attr( 'title', indicatorTitle
);
2560 this.$indicator
.removeAttr( 'title' );
2569 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2571 * @return {string} Symbolic name of indicator
2573 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2574 return this.indicator
;
2578 * Get the indicator title.
2580 * The title is displayed when a user moves the mouse over the indicator.
2582 * @return {string} Indicator title text
2584 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2585 return this.indicatorTitle
;
2589 * LabelElement is often mixed into other classes to generate a label, which
2590 * helps identify the function of an interface element.
2591 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2593 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2599 * @param {Object} [config] Configuration options
2600 * @cfg {jQuery} [$label] The label element created by the class. If this
2601 * configuration is omitted, the label element will use a generated `<span>`.
2602 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2603 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2604 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2605 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2606 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
2607 * The label will be truncated to fit if necessary.
2609 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2610 // Configuration initialization
2611 config
= config
|| {};
2616 this.autoFitLabel
= config
.autoFitLabel
=== undefined || !!config
.autoFitLabel
;
2619 this.setLabel( config
.label
|| this.constructor.static.label
);
2620 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2625 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2630 * @event labelChange
2631 * @param {string} value
2634 /* Static Properties */
2637 * The label text. The label can be specified as a plaintext string, a function that will
2638 * produce a string in the future, or `null` for no label. The static value will
2639 * be overridden if a label is specified with the #label config option.
2643 * @property {string|Function|null}
2645 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2650 * Set the label element.
2652 * If an element is already set, it will be cleaned up before setting up the new element.
2654 * @param {jQuery} $label Element to use as label
2656 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2657 if ( this.$label
) {
2658 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2661 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2662 this.setLabelContent( this.label
);
2668 * An empty string will result in the label being hidden. A string containing only whitespace will
2669 * be converted to a single ` `.
2671 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2672 * text; or null for no label
2675 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2676 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2677 label
= ( ( typeof label
=== 'string' && label
.length
) || label
instanceof jQuery
|| label
instanceof OO
.ui
.HtmlSnippet
) ? label
: null;
2679 this.$element
.toggleClass( 'oo-ui-labelElement', !!label
);
2681 if ( this.label
!== label
) {
2682 if ( this.$label
) {
2683 this.setLabelContent( label
);
2686 this.emit( 'labelChange' );
2695 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2696 * text; or null for no label
2698 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2707 OO
.ui
.mixin
.LabelElement
.prototype.fitLabel = function () {
2708 if ( this.$label
&& this.$label
.autoEllipsis
&& this.autoFitLabel
) {
2709 this.$label
.autoEllipsis( { hasSpan
: false, tooltip
: true } );
2716 * Set the content of the label.
2718 * Do not call this method until after the label element has been set by #setLabelElement.
2721 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2722 * text; or null for no label
2724 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2725 if ( typeof label
=== 'string' ) {
2726 if ( label
.match( /^\s*$/ ) ) {
2727 // Convert whitespace only string to a single non-breaking space
2728 this.$label
.html( ' ' );
2730 this.$label
.text( label
);
2732 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2733 this.$label
.html( label
.toString() );
2734 } else if ( label
instanceof jQuery
) {
2735 this.$label
.empty().append( label
);
2737 this.$label
.empty();
2742 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
2743 * additional functionality to an element created by another class. The class provides
2744 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
2745 * which are used to customize the look and feel of a widget to better describe its
2746 * importance and functionality.
2748 * The library currently contains the following styling flags for general use:
2750 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
2751 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
2752 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
2754 * The flags affect the appearance of the buttons:
2757 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
2758 * var button1 = new OO.ui.ButtonWidget( {
2759 * label: 'Constructive',
2760 * flags: 'constructive'
2762 * var button2 = new OO.ui.ButtonWidget( {
2763 * label: 'Destructive',
2764 * flags: 'destructive'
2766 * var button3 = new OO.ui.ButtonWidget( {
2767 * label: 'Progressive',
2768 * flags: 'progressive'
2770 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
2772 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
2773 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
2775 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2781 * @param {Object} [config] Configuration options
2782 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
2783 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
2784 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2785 * @cfg {jQuery} [$flagged] The flagged element. By default,
2786 * the flagged functionality is applied to the element created by the class ($element).
2787 * If a different element is specified, the flagged functionality will be applied to it instead.
2789 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
2790 // Configuration initialization
2791 config
= config
|| {};
2795 this.$flagged
= null;
2798 this.setFlags( config
.flags
);
2799 this.setFlaggedElement( config
.$flagged
|| this.$element
);
2806 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
2807 * parameter contains the name of each modified flag and indicates whether it was
2810 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
2811 * that the flag was added, `false` that the flag was removed.
2817 * Set the flagged element.
2819 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
2820 * If an element is already set, the method will remove the mixin’s effect on that element.
2822 * @param {jQuery} $flagged Element that should be flagged
2824 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
2825 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
2826 return 'oo-ui-flaggedElement-' + flag
;
2829 if ( this.$flagged
) {
2830 this.$flagged
.removeClass( classNames
);
2833 this.$flagged
= $flagged
.addClass( classNames
);
2837 * Check if the specified flag is set.
2839 * @param {string} flag Name of flag
2840 * @return {boolean} The flag is set
2842 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
2843 // This may be called before the constructor, thus before this.flags is set
2844 return this.flags
&& ( flag
in this.flags
);
2848 * Get the names of all flags set.
2850 * @return {string[]} Flag names
2852 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
2853 // This may be called before the constructor, thus before this.flags is set
2854 return Object
.keys( this.flags
|| {} );
2863 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
2864 var flag
, className
,
2867 classPrefix
= 'oo-ui-flaggedElement-';
2869 for ( flag
in this.flags
) {
2870 className
= classPrefix
+ flag
;
2871 changes
[ flag
] = false;
2872 delete this.flags
[ flag
];
2873 remove
.push( className
);
2876 if ( this.$flagged
) {
2877 this.$flagged
.removeClass( remove
.join( ' ' ) );
2880 this.updateThemeClasses();
2881 this.emit( 'flag', changes
);
2887 * Add one or more flags.
2889 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
2890 * or an object keyed by flag name with a boolean value that indicates whether the flag should
2891 * be added (`true`) or removed (`false`).
2895 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
2896 var i
, len
, flag
, className
,
2900 classPrefix
= 'oo-ui-flaggedElement-';
2902 if ( typeof flags
=== 'string' ) {
2903 className
= classPrefix
+ flags
;
2905 if ( !this.flags
[ flags
] ) {
2906 this.flags
[ flags
] = true;
2907 add
.push( className
);
2909 } else if ( Array
.isArray( flags
) ) {
2910 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
2912 className
= classPrefix
+ flag
;
2914 if ( !this.flags
[ flag
] ) {
2915 changes
[ flag
] = true;
2916 this.flags
[ flag
] = true;
2917 add
.push( className
);
2920 } else if ( OO
.isPlainObject( flags
) ) {
2921 for ( flag
in flags
) {
2922 className
= classPrefix
+ flag
;
2923 if ( flags
[ flag
] ) {
2925 if ( !this.flags
[ flag
] ) {
2926 changes
[ flag
] = true;
2927 this.flags
[ flag
] = true;
2928 add
.push( className
);
2932 if ( this.flags
[ flag
] ) {
2933 changes
[ flag
] = false;
2934 delete this.flags
[ flag
];
2935 remove
.push( className
);
2941 if ( this.$flagged
) {
2943 .addClass( add
.join( ' ' ) )
2944 .removeClass( remove
.join( ' ' ) );
2947 this.updateThemeClasses();
2948 this.emit( 'flag', changes
);
2954 * TitledElement is mixed into other classes to provide a `title` attribute.
2955 * Titles are rendered by the browser and are made visible when the user moves
2956 * the mouse over the element. Titles are not visible on touch devices.
2959 * // TitledElement provides a 'title' attribute to the
2960 * // ButtonWidget class
2961 * var button = new OO.ui.ButtonWidget( {
2962 * label: 'Button with Title',
2963 * title: 'I am a button'
2965 * $( 'body' ).append( button.$element );
2971 * @param {Object} [config] Configuration options
2972 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
2973 * If this config is omitted, the title functionality is applied to $element, the
2974 * element created by the class.
2975 * @cfg {string|Function} [title] The title text or a function that returns text. If
2976 * this config is omitted, the value of the {@link #static-title static title} property is used.
2978 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
2979 // Configuration initialization
2980 config
= config
|| {};
2983 this.$titled
= null;
2987 this.setTitle( config
.title
|| this.constructor.static.title
);
2988 this.setTitledElement( config
.$titled
|| this.$element
);
2993 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
2995 /* Static Properties */
2998 * The title text, a function that returns text, or `null` for no title. The value of the static property
2999 * is overridden if the #title config option is used.
3003 * @property {string|Function|null}
3005 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3010 * Set the titled element.
3012 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3013 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3015 * @param {jQuery} $titled Element that should use the 'titled' functionality
3017 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3018 if ( this.$titled
) {
3019 this.$titled
.removeAttr( 'title' );
3022 this.$titled
= $titled
;
3024 this.$titled
.attr( 'title', this.title
);
3031 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3034 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3035 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3036 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3038 if ( this.title
!== title
) {
3039 if ( this.$titled
) {
3040 if ( title
!== null ) {
3041 this.$titled
.attr( 'title', title
);
3043 this.$titled
.removeAttr( 'title' );
3055 * @return {string} Title string
3057 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3062 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3063 * Accesskeys allow an user to go to a specific element by using
3064 * a shortcut combination of a browser specific keys + the key
3068 * // AccessKeyedElement provides an 'accesskey' attribute to the
3069 * // ButtonWidget class
3070 * var button = new OO.ui.ButtonWidget( {
3071 * label: 'Button with Accesskey',
3074 * $( 'body' ).append( button.$element );
3080 * @param {Object} [config] Configuration options
3081 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3082 * If this config is omitted, the accesskey functionality is applied to $element, the
3083 * element created by the class.
3084 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3085 * this config is omitted, no accesskey will be added.
3087 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3088 // Configuration initialization
3089 config
= config
|| {};
3092 this.$accessKeyed
= null;
3093 this.accessKey
= null;
3096 this.setAccessKey( config
.accessKey
|| null );
3097 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3102 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3104 /* Static Properties */
3107 * The access key, a function that returns a key, or `null` for no accesskey.
3111 * @property {string|Function|null}
3113 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3118 * Set the accesskeyed element.
3120 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3121 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3123 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3125 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3126 if ( this.$accessKeyed
) {
3127 this.$accessKeyed
.removeAttr( 'accesskey' );
3130 this.$accessKeyed
= $accessKeyed
;
3131 if ( this.accessKey
) {
3132 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3139 * @param {string|Function|null} accesskey Key, a function that returns a key, or `null` for no accesskey
3142 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3143 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3145 if ( this.accessKey
!== accessKey
) {
3146 if ( this.$accessKeyed
) {
3147 if ( accessKey
!== null ) {
3148 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3150 this.$accessKeyed
.removeAttr( 'accesskey' );
3153 this.accessKey
= accessKey
;
3162 * @return {string} accessKey string
3164 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3165 return this.accessKey
;
3169 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3170 * feels, and functionality can be customized via the class’s configuration options
3171 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3174 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3177 * // A button widget
3178 * var button = new OO.ui.ButtonWidget( {
3179 * label: 'Button with Icon',
3181 * iconTitle: 'Remove'
3183 * $( 'body' ).append( button.$element );
3185 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3188 * @extends OO.ui.Widget
3189 * @mixins OO.ui.mixin.ButtonElement
3190 * @mixins OO.ui.mixin.IconElement
3191 * @mixins OO.ui.mixin.IndicatorElement
3192 * @mixins OO.ui.mixin.LabelElement
3193 * @mixins OO.ui.mixin.TitledElement
3194 * @mixins OO.ui.mixin.FlaggedElement
3195 * @mixins OO.ui.mixin.TabIndexedElement
3196 * @mixins OO.ui.mixin.AccessKeyedElement
3199 * @param {Object} [config] Configuration options
3200 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3201 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3202 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3204 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3205 // Configuration initialization
3206 config
= config
|| {};
3208 // Parent constructor
3209 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3211 // Mixin constructors
3212 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3213 OO
.ui
.mixin
.IconElement
.call( this, config
);
3214 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3215 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3216 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3217 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3218 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3219 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3224 this.noFollow
= false;
3227 this.connect( this, { disable
: 'onDisable' } );
3230 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3232 .addClass( 'oo-ui-buttonWidget' )
3233 .append( this.$button
);
3234 this.setHref( config
.href
);
3235 this.setTarget( config
.target
);
3236 this.setNoFollow( config
.noFollow
);
3241 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3242 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3243 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3244 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3245 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3246 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3247 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3248 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3249 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3256 OO
.ui
.ButtonWidget
.prototype.onMouseDown = function ( e
) {
3257 if ( !this.isDisabled() ) {
3258 // Remove the tab-index while the button is down to prevent the button from stealing focus
3259 this.$button
.removeAttr( 'tabindex' );
3262 return OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown
.call( this, e
);
3268 OO
.ui
.ButtonWidget
.prototype.onMouseUp = function ( e
) {
3269 if ( !this.isDisabled() ) {
3270 // Restore the tab-index after the button is up to restore the button's accessibility
3271 this.$button
.attr( 'tabindex', this.tabIndex
);
3274 return OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp
.call( this, e
);
3278 * Get hyperlink location.
3280 * @return {string} Hyperlink location
3282 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3287 * Get hyperlink target.
3289 * @return {string} Hyperlink target
3291 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3296 * Get search engine traversal hint.
3298 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3300 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3301 return this.noFollow
;
3305 * Set hyperlink location.
3307 * @param {string|null} href Hyperlink location, null to remove
3309 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3310 href
= typeof href
=== 'string' ? href
: null;
3311 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3315 if ( href
!== this.href
) {
3324 * Update the `href` attribute, in case of changes to href or
3330 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3331 if ( this.href
!== null && !this.isDisabled() ) {
3332 this.$button
.attr( 'href', this.href
);
3334 this.$button
.removeAttr( 'href' );
3341 * Handle disable events.
3344 * @param {boolean} disabled Element is disabled
3346 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3351 * Set hyperlink target.
3353 * @param {string|null} target Hyperlink target, null to remove
3355 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3356 target
= typeof target
=== 'string' ? target
: null;
3358 if ( target
!== this.target
) {
3359 this.target
= target
;
3360 if ( target
!== null ) {
3361 this.$button
.attr( 'target', target
);
3363 this.$button
.removeAttr( 'target' );
3371 * Set search engine traversal hint.
3373 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3375 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3376 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3378 if ( noFollow
!== this.noFollow
) {
3379 this.noFollow
= noFollow
;
3381 this.$button
.attr( 'rel', 'nofollow' );
3383 this.$button
.removeAttr( 'rel' );
3391 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3392 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3393 * removed, and cleared from the group.
3396 * // Example: A ButtonGroupWidget with two buttons
3397 * var button1 = new OO.ui.PopupButtonWidget( {
3398 * label: 'Select a category',
3401 * $content: $( '<p>List of categories...</p>' ),
3406 * var button2 = new OO.ui.ButtonWidget( {
3409 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3410 * items: [button1, button2]
3412 * $( 'body' ).append( buttonGroup.$element );
3415 * @extends OO.ui.Widget
3416 * @mixins OO.ui.mixin.GroupElement
3419 * @param {Object} [config] Configuration options
3420 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3422 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3423 // Configuration initialization
3424 config
= config
|| {};
3426 // Parent constructor
3427 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3429 // Mixin constructors
3430 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3433 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3434 if ( Array
.isArray( config
.items
) ) {
3435 this.addItems( config
.items
);
3441 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3442 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3445 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3446 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3447 * for a list of icons included in the library.
3450 * // An icon widget with a label
3451 * var myIcon = new OO.ui.IconWidget( {
3455 * // Create a label.
3456 * var iconLabel = new OO.ui.LabelWidget( {
3459 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3461 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3464 * @extends OO.ui.Widget
3465 * @mixins OO.ui.mixin.IconElement
3466 * @mixins OO.ui.mixin.TitledElement
3467 * @mixins OO.ui.mixin.FlaggedElement
3470 * @param {Object} [config] Configuration options
3472 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3473 // Configuration initialization
3474 config
= config
|| {};
3476 // Parent constructor
3477 OO
.ui
.IconWidget
.parent
.call( this, config
);
3479 // Mixin constructors
3480 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3481 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3482 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3485 this.$element
.addClass( 'oo-ui-iconWidget' );
3490 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3491 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3492 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3493 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3495 /* Static Properties */
3497 OO
.ui
.IconWidget
.static.tagName
= 'span';
3500 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3501 * attention to the status of an item or to clarify the function of a control. For a list of
3502 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3505 * // Example of an indicator widget
3506 * var indicator1 = new OO.ui.IndicatorWidget( {
3507 * indicator: 'alert'
3510 * // Create a fieldset layout to add a label
3511 * var fieldset = new OO.ui.FieldsetLayout();
3512 * fieldset.addItems( [
3513 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3515 * $( 'body' ).append( fieldset.$element );
3517 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3520 * @extends OO.ui.Widget
3521 * @mixins OO.ui.mixin.IndicatorElement
3522 * @mixins OO.ui.mixin.TitledElement
3525 * @param {Object} [config] Configuration options
3527 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
3528 // Configuration initialization
3529 config
= config
|| {};
3531 // Parent constructor
3532 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
3534 // Mixin constructors
3535 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
3536 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3539 this.$element
.addClass( 'oo-ui-indicatorWidget' );
3544 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
3545 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
3546 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
3548 /* Static Properties */
3550 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
3553 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3554 * be configured with a `label` option that is set to a string, a label node, or a function:
3556 * - String: a plaintext string
3557 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3558 * label that includes a link or special styling, such as a gray color or additional graphical elements.
3559 * - Function: a function that will produce a string in the future. Functions are used
3560 * in cases where the value of the label is not currently defined.
3562 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3563 * will come into focus when the label is clicked.
3566 * // Examples of LabelWidgets
3567 * var label1 = new OO.ui.LabelWidget( {
3568 * label: 'plaintext label'
3570 * var label2 = new OO.ui.LabelWidget( {
3571 * label: $( '<a href="default.html">jQuery label</a>' )
3573 * // Create a fieldset layout with fields for each example
3574 * var fieldset = new OO.ui.FieldsetLayout();
3575 * fieldset.addItems( [
3576 * new OO.ui.FieldLayout( label1 ),
3577 * new OO.ui.FieldLayout( label2 )
3579 * $( 'body' ).append( fieldset.$element );
3582 * @extends OO.ui.Widget
3583 * @mixins OO.ui.mixin.LabelElement
3586 * @param {Object} [config] Configuration options
3587 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
3588 * Clicking the label will focus the specified input field.
3590 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
3591 // Configuration initialization
3592 config
= config
|| {};
3594 // Parent constructor
3595 OO
.ui
.LabelWidget
.parent
.call( this, config
);
3597 // Mixin constructors
3598 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
3599 OO
.ui
.mixin
.TitledElement
.call( this, config
);
3602 this.input
= config
.input
;
3605 if ( this.input
instanceof OO
.ui
.InputWidget
) {
3606 this.$element
.on( 'click', this.onClick
.bind( this ) );
3610 this.$element
.addClass( 'oo-ui-labelWidget' );
3615 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
3616 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
3617 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
3619 /* Static Properties */
3621 OO
.ui
.LabelWidget
.static.tagName
= 'span';
3626 * Handles label mouse click events.
3629 * @param {jQuery.Event} e Mouse click event
3631 OO
.ui
.LabelWidget
.prototype.onClick = function () {
3632 this.input
.simulateLabelClick();
3637 * PendingElement is a mixin that is used to create elements that notify users that something is happening
3638 * and that they should wait before proceeding. The pending state is visually represented with a pending
3639 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
3640 * field of a {@link OO.ui.TextInputWidget text input widget}.
3642 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
3643 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
3644 * in process dialogs.
3647 * function MessageDialog( config ) {
3648 * MessageDialog.parent.call( this, config );
3650 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
3652 * MessageDialog.static.actions = [
3653 * { action: 'save', label: 'Done', flags: 'primary' },
3654 * { label: 'Cancel', flags: 'safe' }
3657 * MessageDialog.prototype.initialize = function () {
3658 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
3659 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
3660 * 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>' );
3661 * this.$body.append( this.content.$element );
3663 * MessageDialog.prototype.getBodyHeight = function () {
3666 * MessageDialog.prototype.getActionProcess = function ( action ) {
3667 * var dialog = this;
3668 * if ( action === 'save' ) {
3669 * dialog.getActions().get({actions: 'save'})[0].pushPending();
3670 * return new OO.ui.Process()
3672 * .next( function () {
3673 * dialog.getActions().get({actions: 'save'})[0].popPending();
3676 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
3679 * var windowManager = new OO.ui.WindowManager();
3680 * $( 'body' ).append( windowManager.$element );
3682 * var dialog = new MessageDialog();
3683 * windowManager.addWindows( [ dialog ] );
3684 * windowManager.openWindow( dialog );
3690 * @param {Object} [config] Configuration options
3691 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
3693 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
3694 // Configuration initialization
3695 config
= config
|| {};
3699 this.$pending
= null;
3702 this.setPendingElement( config
.$pending
|| this.$element
);
3707 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
3712 * Set the pending element (and clean up any existing one).
3714 * @param {jQuery} $pending The element to set to pending.
3716 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
3717 if ( this.$pending
) {
3718 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3721 this.$pending
= $pending
;
3722 if ( this.pending
> 0 ) {
3723 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3728 * Check if an element is pending.
3730 * @return {boolean} Element is pending
3732 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
3733 return !!this.pending
;
3737 * Increase the pending counter. The pending state will remain active until the counter is zero
3738 * (i.e., the number of calls to #pushPending and #popPending is the same).
3742 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
3743 if ( this.pending
=== 0 ) {
3744 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
3745 this.updateThemeClasses();
3753 * Decrease the pending counter. The pending state will remain active until the counter is zero
3754 * (i.e., the number of calls to #pushPending and #popPending is the same).
3758 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
3759 if ( this.pending
=== 1 ) {
3760 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
3761 this.updateThemeClasses();
3763 this.pending
= Math
.max( 0, this.pending
- 1 );
3769 * Element that can be automatically clipped to visible boundaries.
3771 * Whenever the element's natural height changes, you have to call
3772 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
3773 * clipping correctly.
3775 * The dimensions of #$clippableContainer will be compared to the boundaries of the
3776 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
3777 * then #$clippable will be given a fixed reduced height and/or width and will be made
3778 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
3779 * but you can build a static footer by setting #$clippableContainer to an element that contains
3780 * #$clippable and the footer.
3786 * @param {Object} [config] Configuration options
3787 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
3788 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
3789 * omit to use #$clippable
3791 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
3792 // Configuration initialization
3793 config
= config
|| {};
3796 this.$clippable
= null;
3797 this.$clippableContainer
= null;
3798 this.clipping
= false;
3799 this.clippedHorizontally
= false;
3800 this.clippedVertically
= false;
3801 this.$clippableScrollableContainer
= null;
3802 this.$clippableScroller
= null;
3803 this.$clippableWindow
= null;
3804 this.idealWidth
= null;
3805 this.idealHeight
= null;
3806 this.onClippableScrollHandler
= this.clip
.bind( this );
3807 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
3810 if ( config
.$clippableContainer
) {
3811 this.setClippableContainer( config
.$clippableContainer
);
3813 this.setClippableElement( config
.$clippable
|| this.$element
);
3819 * Set clippable element.
3821 * If an element is already set, it will be cleaned up before setting up the new element.
3823 * @param {jQuery} $clippable Element to make clippable
3825 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
3826 if ( this.$clippable
) {
3827 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
3828 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
3829 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
3832 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
3837 * Set clippable container.
3839 * This is the container that will be measured when deciding whether to clip. When clipping,
3840 * #$clippable will be resized in order to keep the clippable container fully visible.
3842 * If the clippable container is unset, #$clippable will be used.
3844 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
3846 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
3847 this.$clippableContainer
= $clippableContainer
;
3848 if ( this.$clippable
) {
3856 * Do not turn clipping on until after the element is attached to the DOM and visible.
3858 * @param {boolean} [clipping] Enable clipping, omit to toggle
3861 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
3862 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
3864 if ( this.clipping
!== clipping
) {
3865 this.clipping
= clipping
;
3867 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
3868 // If the clippable container is the root, we have to listen to scroll events and check
3869 // jQuery.scrollTop on the window because of browser inconsistencies
3870 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
3871 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
3872 this.$clippableScrollableContainer
;
3873 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
3874 this.$clippableWindow
= $( this.getElementWindow() )
3875 .on( 'resize', this.onClippableWindowResizeHandler
);
3876 // Initial clip after visible
3879 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
3880 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
3882 this.$clippableScrollableContainer
= null;
3883 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
3884 this.$clippableScroller
= null;
3885 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
3886 this.$clippableWindow
= null;
3894 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
3896 * @return {boolean} Element will be clipped to the visible area
3898 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
3899 return this.clipping
;
3903 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
3905 * @return {boolean} Part of the element is being clipped
3907 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
3908 return this.clippedHorizontally
|| this.clippedVertically
;
3912 * Check if the right of the element is being clipped by the nearest scrollable container.
3914 * @return {boolean} Part of the element is being clipped
3916 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
3917 return this.clippedHorizontally
;
3921 * Check if the bottom of the element is being clipped by the nearest scrollable container.
3923 * @return {boolean} Part of the element is being clipped
3925 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
3926 return this.clippedVertically
;
3930 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
3932 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
3933 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
3935 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
3936 this.idealWidth
= width
;
3937 this.idealHeight
= height
;
3939 if ( !this.clipping
) {
3940 // Update dimensions
3941 this.$clippable
.css( { width
: width
, height
: height
} );
3943 // While clipping, idealWidth and idealHeight are not considered
3947 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
3948 * the element's natural height changes.
3950 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
3951 * overlapped by, the visible area of the nearest scrollable container.
3955 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
3956 var $container
, extraHeight
, extraWidth
, ccOffset
,
3957 $scrollableContainer
, scOffset
, scHeight
, scWidth
,
3958 ccWidth
, scrollerIsWindow
, scrollTop
, scrollLeft
,
3959 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
3960 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
3961 buffer
= 7; // Chosen by fair dice roll
3963 if ( !this.clipping
) {
3964 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
3968 $container
= this.$clippableContainer
|| this.$clippable
;
3969 extraHeight
= $container
.outerHeight() - this.$clippable
.outerHeight();
3970 extraWidth
= $container
.outerWidth() - this.$clippable
.outerWidth();
3971 ccOffset
= $container
.offset();
3972 $scrollableContainer
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
3973 this.$clippableWindow
: this.$clippableScrollableContainer
;
3974 scOffset
= $scrollableContainer
.offset() || { top
: 0, left
: 0 };
3975 scHeight
= $scrollableContainer
.innerHeight() - buffer
;
3976 scWidth
= $scrollableContainer
.innerWidth() - buffer
;
3977 ccWidth
= $container
.outerWidth() + buffer
;
3978 scrollerIsWindow
= this.$clippableScroller
[ 0 ] === this.$clippableWindow
[ 0 ];
3979 scrollTop
= scrollerIsWindow
? this.$clippableScroller
.scrollTop() : 0;
3980 scrollLeft
= scrollerIsWindow
? this.$clippableScroller
.scrollLeft() : 0;
3981 desiredWidth
= ccOffset
.left
< 0 ?
3982 ccWidth
+ ccOffset
.left
:
3983 ( scOffset
.left
+ scrollLeft
+ scWidth
) - ccOffset
.left
;
3984 desiredHeight
= ( scOffset
.top
+ scrollTop
+ scHeight
) - ccOffset
.top
;
3985 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
3986 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
3987 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
3988 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
3989 clipWidth
= allotedWidth
< naturalWidth
;
3990 clipHeight
= allotedHeight
< naturalHeight
;
3993 this.$clippable
.css( { overflowX
: 'scroll', width
: Math
.max( 0, allotedWidth
) } );
3995 this.$clippable
.css( { width
: this.idealWidth
? this.idealWidth
- extraWidth
: '', overflowX
: '' } );
3998 this.$clippable
.css( { overflowY
: 'scroll', height
: Math
.max( 0, allotedHeight
) } );
4000 this.$clippable
.css( { height
: this.idealHeight
? this.idealHeight
- extraHeight
: '', overflowY
: '' } );
4003 // If we stopped clipping in at least one of the dimensions
4004 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
4005 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4008 this.clippedHorizontally
= clipWidth
;
4009 this.clippedVertically
= clipHeight
;
4015 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4016 * By default, each popup has an anchor that points toward its origin.
4017 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4020 * // A popup widget.
4021 * var popup = new OO.ui.PopupWidget( {
4022 * $content: $( '<p>Hi there!</p>' ),
4027 * $( 'body' ).append( popup.$element );
4028 * // To display the popup, toggle the visibility to 'true'.
4029 * popup.toggle( true );
4031 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4034 * @extends OO.ui.Widget
4035 * @mixins OO.ui.mixin.LabelElement
4036 * @mixins OO.ui.mixin.ClippableElement
4039 * @param {Object} [config] Configuration options
4040 * @cfg {number} [width=320] Width of popup in pixels
4041 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4042 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4043 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
4044 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
4045 * popup is leaning towards the right of the screen.
4046 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
4047 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
4048 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
4049 * sentence in the given language.
4050 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4051 * See the [OOjs UI docs on MediaWiki][3] for an example.
4052 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4053 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4054 * @cfg {jQuery} [$content] Content to append to the popup's body
4055 * @cfg {jQuery} [$footer] Content to append to the popup's footer
4056 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4057 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4058 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4060 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4061 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
4063 * @cfg {boolean} [padded] Add padding to the popup's body
4065 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
4066 // Configuration initialization
4067 config
= config
|| {};
4069 // Parent constructor
4070 OO
.ui
.PopupWidget
.parent
.call( this, config
);
4072 // Properties (must be set before ClippableElement constructor call)
4073 this.$body
= $( '<div>' );
4074 this.$popup
= $( '<div>' );
4076 // Mixin constructors
4077 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4078 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
4079 $clippable
: this.$body
,
4080 $clippableContainer
: this.$popup
4084 this.$head
= $( '<div>' );
4085 this.$footer
= $( '<div>' );
4086 this.$anchor
= $( '<div>' );
4087 // If undefined, will be computed lazily in updateDimensions()
4088 this.$container
= config
.$container
;
4089 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
4090 this.autoClose
= !!config
.autoClose
;
4091 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
4092 this.transitionTimeout
= null;
4094 this.width
= config
.width
!== undefined ? config
.width
: 320;
4095 this.height
= config
.height
!== undefined ? config
.height
: null;
4096 this.setAlignment( config
.align
);
4097 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
4098 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
4099 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
4102 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
4105 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
4106 this.$body
.addClass( 'oo-ui-popupWidget-body' );
4107 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
4109 .addClass( 'oo-ui-popupWidget-head' )
4110 .append( this.$label
, this.closeButton
.$element
);
4111 this.$footer
.addClass( 'oo-ui-popupWidget-footer' );
4112 if ( !config
.head
) {
4113 this.$head
.addClass( 'oo-ui-element-hidden' );
4115 if ( !config
.$footer
) {
4116 this.$footer
.addClass( 'oo-ui-element-hidden' );
4119 .addClass( 'oo-ui-popupWidget-popup' )
4120 .append( this.$head
, this.$body
, this.$footer
);
4122 .addClass( 'oo-ui-popupWidget' )
4123 .append( this.$popup
, this.$anchor
);
4124 // Move content, which was added to #$element by OO.ui.Widget, to the body
4125 if ( config
.$content
instanceof jQuery
) {
4126 this.$body
.append( config
.$content
);
4128 if ( config
.$footer
instanceof jQuery
) {
4129 this.$footer
.append( config
.$footer
);
4131 if ( config
.padded
) {
4132 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
4135 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
4136 // that reference properties not initialized at that time of parent class construction
4137 // TODO: Find a better way to handle post-constructor setup
4138 this.visible
= false;
4139 this.$element
.addClass( 'oo-ui-element-hidden' );
4144 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
4145 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
4146 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
4151 * Handles mouse down events.
4154 * @param {MouseEvent} e Mouse down event
4156 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
4159 !$.contains( this.$element
[ 0 ], e
.target
) &&
4160 ( !this.$autoCloseIgnore
|| !this.$autoCloseIgnore
.has( e
.target
).length
)
4162 this.toggle( false );
4167 * Bind mouse down listener.
4171 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
4172 // Capture clicks outside popup
4173 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
4177 * Handles close button click events.
4181 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
4182 if ( this.isVisible() ) {
4183 this.toggle( false );
4188 * Unbind mouse down listener.
4192 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
4193 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
4197 * Handles key down events.
4200 * @param {KeyboardEvent} e Key down event
4202 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
4204 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
4207 this.toggle( false );
4209 e
.stopPropagation();
4214 * Bind key down listener.
4218 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
4219 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4223 * Unbind key down listener.
4227 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
4228 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
4232 * Show, hide, or toggle the visibility of the anchor.
4234 * @param {boolean} [show] Show anchor, omit to toggle
4236 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
4237 show
= show
=== undefined ? !this.anchored
: !!show
;
4239 if ( this.anchored
!== show
) {
4241 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
4243 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
4245 this.anchored
= show
;
4250 * Check if the anchor is visible.
4252 * @return {boolean} Anchor is visible
4254 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
4261 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
4263 show
= show
=== undefined ? !this.isVisible() : !!show
;
4265 change
= show
!== this.isVisible();
4268 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
4272 if ( this.autoClose
) {
4273 this.bindMouseDownListener();
4274 this.bindKeyDownListener();
4276 this.updateDimensions();
4277 this.toggleClipping( true );
4279 this.toggleClipping( false );
4280 if ( this.autoClose
) {
4281 this.unbindMouseDownListener();
4282 this.unbindKeyDownListener();
4291 * Set the size of the popup.
4293 * Changing the size may also change the popup's position depending on the alignment.
4295 * @param {number} width Width in pixels
4296 * @param {number} height Height in pixels
4297 * @param {boolean} [transition=false] Use a smooth transition
4300 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
4302 this.height
= height
!== undefined ? height
: null;
4303 if ( this.isVisible() ) {
4304 this.updateDimensions( transition
);
4309 * Update the size and position.
4311 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
4312 * be called automatically.
4314 * @param {boolean} [transition=false] Use a smooth transition
4317 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
4318 var popupOffset
, originOffset
, containerLeft
, containerWidth
, containerRight
,
4319 popupLeft
, popupRight
, overlapLeft
, overlapRight
, anchorWidth
,
4323 if ( !this.$container
) {
4324 // Lazy-initialize $container if not specified in constructor
4325 this.$container
= $( this.getClosestScrollableElementContainer() );
4328 // Set height and width before measuring things, since it might cause our measurements
4329 // to change (e.g. due to scrollbars appearing or disappearing)
4332 height
: this.height
!== null ? this.height
: 'auto'
4335 // If we are in RTL, we need to flip the alignment, unless it is center
4336 if ( align
=== 'forwards' || align
=== 'backwards' ) {
4337 if ( this.$container
.css( 'direction' ) === 'rtl' ) {
4338 align
= ( { forwards
: 'force-left', backwards
: 'force-right' } )[ this.align
];
4340 align
= ( { forwards
: 'force-right', backwards
: 'force-left' } )[ this.align
];
4345 // Compute initial popupOffset based on alignment
4346 popupOffset
= this.width
* ( { 'force-left': -1, center
: -0.5, 'force-right': 0 } )[ align
];
4348 // Figure out if this will cause the popup to go beyond the edge of the container
4349 originOffset
= this.$element
.offset().left
;
4350 containerLeft
= this.$container
.offset().left
;
4351 containerWidth
= this.$container
.innerWidth();
4352 containerRight
= containerLeft
+ containerWidth
;
4353 popupLeft
= popupOffset
- this.containerPadding
;
4354 popupRight
= popupOffset
+ this.containerPadding
+ this.width
+ this.containerPadding
;
4355 overlapLeft
= ( originOffset
+ popupLeft
) - containerLeft
;
4356 overlapRight
= containerRight
- ( originOffset
+ popupRight
);
4358 // Adjust offset to make the popup not go beyond the edge, if needed
4359 if ( overlapRight
< 0 ) {
4360 popupOffset
+= overlapRight
;
4361 } else if ( overlapLeft
< 0 ) {
4362 popupOffset
-= overlapLeft
;
4365 // Adjust offset to avoid anchor being rendered too close to the edge
4366 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
4367 // TODO: Find a measurement that works for CSS anchors and image anchors
4368 anchorWidth
= this.$anchor
[ 0 ].scrollWidth
* 2;
4369 if ( popupOffset
+ this.width
< anchorWidth
) {
4370 popupOffset
= anchorWidth
- this.width
;
4371 } else if ( -popupOffset
< anchorWidth
) {
4372 popupOffset
= -anchorWidth
;
4375 // Prevent transition from being interrupted
4376 clearTimeout( this.transitionTimeout
);
4378 // Enable transition
4379 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
4382 // Position body relative to anchor
4383 this.$popup
.css( 'margin-left', popupOffset
);
4386 // Prevent transitioning after transition is complete
4387 this.transitionTimeout
= setTimeout( function () {
4388 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4391 // Prevent transitioning immediately
4392 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
4395 // Reevaluate clipping state since we've relocated and resized the popup
4402 * Set popup alignment
4403 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4404 * `backwards` or `forwards`.
4406 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
4407 // Validate alignment and transform deprecated values
4408 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
4409 this.align
= { left
: 'force-right', right
: 'force-left' }[ align
] || align
;
4411 this.align
= 'center';
4416 * Get popup alignment
4417 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4418 * `backwards` or `forwards`.
4420 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
4425 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
4426 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
4427 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
4428 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
4434 * @param {Object} [config] Configuration options
4435 * @cfg {Object} [popup] Configuration to pass to popup
4436 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
4438 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
4439 // Configuration initialization
4440 config
= config
|| {};
4443 this.popup
= new OO
.ui
.PopupWidget( $.extend(
4444 { autoClose
: true },
4446 { $autoCloseIgnore
: this.$element
}
4455 * @return {OO.ui.PopupWidget} Popup widget
4457 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
4462 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
4463 * which is used to display additional information or options.
4466 * // Example of a popup button.
4467 * var popupButton = new OO.ui.PopupButtonWidget( {
4468 * label: 'Popup button with options',
4471 * $content: $( '<p>Additional options here.</p>' ),
4473 * align: 'force-left'
4476 * // Append the button to the DOM.
4477 * $( 'body' ).append( popupButton.$element );
4480 * @extends OO.ui.ButtonWidget
4481 * @mixins OO.ui.mixin.PopupElement
4484 * @param {Object} [config] Configuration options
4486 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
4487 // Parent constructor
4488 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
4490 // Mixin constructors
4491 OO
.ui
.mixin
.PopupElement
.call( this, config
);
4494 this.connect( this, { click
: 'onAction' } );
4498 .addClass( 'oo-ui-popupButtonWidget' )
4499 .attr( 'aria-haspopup', 'true' )
4500 .append( this.popup
.$element
);
4505 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
4506 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
4511 * Handle the button action being triggered.
4515 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
4516 this.popup
.toggle();
4520 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
4522 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
4527 * @extends OO.ui.mixin.GroupElement
4530 * @param {Object} [config] Configuration options
4532 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
4533 // Parent constructor
4534 OO
.ui
.mixin
.GroupWidget
.parent
.call( this, config
);
4539 OO
.inheritClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
4544 * Set the disabled state of the widget.
4546 * This will also update the disabled state of child widgets.
4548 * @param {boolean} disabled Disable widget
4551 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
4555 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
4556 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
4558 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
4560 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4561 this.items
[ i
].updateDisabled();
4569 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
4571 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
4572 * allows bidirectional communication.
4574 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
4582 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
4589 * Check if widget is disabled.
4591 * Checks parent if present, making disabled state inheritable.
4593 * @return {boolean} Widget is disabled
4595 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
4596 return this.disabled
||
4597 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
4601 * Set group element is in.
4603 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
4606 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
4608 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
4609 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
4611 // Initialize item disabled states
4612 this.updateDisabled();
4618 * OptionWidgets are special elements that can be selected and configured with data. The
4619 * data is often unique for each option, but it does not have to be. OptionWidgets are used
4620 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
4621 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
4623 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4626 * @extends OO.ui.Widget
4627 * @mixins OO.ui.mixin.LabelElement
4628 * @mixins OO.ui.mixin.FlaggedElement
4631 * @param {Object} [config] Configuration options
4633 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
4634 // Configuration initialization
4635 config
= config
|| {};
4637 // Parent constructor
4638 OO
.ui
.OptionWidget
.parent
.call( this, config
);
4640 // Mixin constructors
4641 OO
.ui
.mixin
.ItemWidget
.call( this );
4642 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4643 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
4646 this.selected
= false;
4647 this.highlighted
= false;
4648 this.pressed
= false;
4652 .data( 'oo-ui-optionWidget', this )
4653 .attr( 'role', 'option' )
4654 .attr( 'aria-selected', 'false' )
4655 .addClass( 'oo-ui-optionWidget' )
4656 .append( this.$label
);
4661 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
4662 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
4663 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
4664 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
4666 /* Static Properties */
4668 OO
.ui
.OptionWidget
.static.selectable
= true;
4670 OO
.ui
.OptionWidget
.static.highlightable
= true;
4672 OO
.ui
.OptionWidget
.static.pressable
= true;
4674 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
4679 * Check if the option can be selected.
4681 * @return {boolean} Item is selectable
4683 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
4684 return this.constructor.static.selectable
&& !this.isDisabled() && this.isVisible();
4688 * Check if the option can be highlighted. A highlight indicates that the option
4689 * may be selected when a user presses enter or clicks. Disabled items cannot
4692 * @return {boolean} Item is highlightable
4694 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
4695 return this.constructor.static.highlightable
&& !this.isDisabled() && this.isVisible();
4699 * Check if the option can be pressed. The pressed state occurs when a user mouses
4700 * down on an item, but has not yet let go of the mouse.
4702 * @return {boolean} Item is pressable
4704 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
4705 return this.constructor.static.pressable
&& !this.isDisabled() && this.isVisible();
4709 * Check if the option is selected.
4711 * @return {boolean} Item is selected
4713 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
4714 return this.selected
;
4718 * Check if the option is highlighted. A highlight indicates that the
4719 * item may be selected when a user presses enter or clicks.
4721 * @return {boolean} Item is highlighted
4723 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
4724 return this.highlighted
;
4728 * Check if the option is pressed. The pressed state occurs when a user mouses
4729 * down on an item, but has not yet let go of the mouse. The item may appear
4730 * selected, but it will not be selected until the user releases the mouse.
4732 * @return {boolean} Item is pressed
4734 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
4735 return this.pressed
;
4739 * Set the option’s selected state. In general, all modifications to the selection
4740 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
4741 * method instead of this method.
4743 * @param {boolean} [state=false] Select option
4746 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
4747 if ( this.constructor.static.selectable
) {
4748 this.selected
= !!state
;
4750 .toggleClass( 'oo-ui-optionWidget-selected', state
)
4751 .attr( 'aria-selected', state
.toString() );
4752 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
4753 this.scrollElementIntoView();
4755 this.updateThemeClasses();
4761 * Set the option’s highlighted state. In general, all programmatic
4762 * modifications to the highlight should be handled by the
4763 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
4764 * method instead of this method.
4766 * @param {boolean} [state=false] Highlight option
4769 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
4770 if ( this.constructor.static.highlightable
) {
4771 this.highlighted
= !!state
;
4772 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
4773 this.updateThemeClasses();
4779 * Set the option’s pressed state. In general, all
4780 * programmatic modifications to the pressed state should be handled by the
4781 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
4782 * method instead of this method.
4784 * @param {boolean} [state=false] Press option
4787 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
4788 if ( this.constructor.static.pressable
) {
4789 this.pressed
= !!state
;
4790 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
4791 this.updateThemeClasses();
4797 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
4798 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
4799 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
4802 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
4803 * information, please see the [OOjs UI documentation on MediaWiki][1].
4806 * // Example of a select widget with three options
4807 * var select = new OO.ui.SelectWidget( {
4809 * new OO.ui.OptionWidget( {
4811 * label: 'Option One',
4813 * new OO.ui.OptionWidget( {
4815 * label: 'Option Two',
4817 * new OO.ui.OptionWidget( {
4819 * label: 'Option Three',
4823 * $( 'body' ).append( select.$element );
4825 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4829 * @extends OO.ui.Widget
4830 * @mixins OO.ui.mixin.GroupWidget
4833 * @param {Object} [config] Configuration options
4834 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
4835 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
4836 * the [OOjs UI documentation on MediaWiki] [2] for examples.
4837 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4839 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
4840 // Configuration initialization
4841 config
= config
|| {};
4843 // Parent constructor
4844 OO
.ui
.SelectWidget
.parent
.call( this, config
);
4846 // Mixin constructors
4847 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
4850 this.pressed
= false;
4851 this.selecting
= null;
4852 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
4853 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
4854 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
4855 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
4856 this.keyPressBuffer
= '';
4857 this.keyPressBufferTimer
= null;
4860 this.connect( this, {
4864 mousedown
: this.onMouseDown
.bind( this ),
4865 mouseover
: this.onMouseOver
.bind( this ),
4866 mouseleave
: this.onMouseLeave
.bind( this )
4871 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
4872 .attr( 'role', 'listbox' );
4873 if ( Array
.isArray( config
.items
) ) {
4874 this.addItems( config
.items
);
4880 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
4882 // Need to mixin base class as well
4883 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupElement
);
4884 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
4887 OO
.ui
.SelectWidget
.static.passAllFilter = function () {
4896 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
4898 * @param {OO.ui.OptionWidget|null} item Highlighted item
4904 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
4905 * pressed state of an option.
4907 * @param {OO.ui.OptionWidget|null} item Pressed item
4913 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
4915 * @param {OO.ui.OptionWidget|null} item Selected item
4920 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
4921 * @param {OO.ui.OptionWidget} item Chosen item
4927 * An `add` event is emitted when options are added to the select with the #addItems method.
4929 * @param {OO.ui.OptionWidget[]} items Added items
4930 * @param {number} index Index of insertion point
4936 * A `remove` event is emitted when options are removed from the select with the #clearItems
4937 * or #removeItems methods.
4939 * @param {OO.ui.OptionWidget[]} items Removed items
4945 * Handle mouse down events.
4948 * @param {jQuery.Event} e Mouse down event
4950 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
4953 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
4954 this.togglePressed( true );
4955 item
= this.getTargetItem( e
);
4956 if ( item
&& item
.isSelectable() ) {
4957 this.pressItem( item
);
4958 this.selecting
= item
;
4959 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
4960 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
4967 * Handle mouse up events.
4970 * @param {jQuery.Event} e Mouse up event
4972 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
4975 this.togglePressed( false );
4976 if ( !this.selecting
) {
4977 item
= this.getTargetItem( e
);
4978 if ( item
&& item
.isSelectable() ) {
4979 this.selecting
= item
;
4982 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
4983 this.pressItem( null );
4984 this.chooseItem( this.selecting
);
4985 this.selecting
= null;
4988 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
4989 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
4995 * Handle mouse move events.
4998 * @param {jQuery.Event} e Mouse move event
5000 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
5003 if ( !this.isDisabled() && this.pressed
) {
5004 item
= this.getTargetItem( e
);
5005 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
5006 this.pressItem( item
);
5007 this.selecting
= item
;
5014 * Handle mouse over events.
5017 * @param {jQuery.Event} e Mouse over event
5019 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
5022 if ( !this.isDisabled() ) {
5023 item
= this.getTargetItem( e
);
5024 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
5030 * Handle mouse leave events.
5033 * @param {jQuery.Event} e Mouse over event
5035 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
5036 if ( !this.isDisabled() ) {
5037 this.highlightItem( null );
5043 * Handle key down events.
5046 * @param {jQuery.Event} e Key down event
5048 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
5051 currentItem
= this.getHighlightedItem() || this.getSelectedItem();
5053 if ( !this.isDisabled() && this.isVisible() ) {
5054 switch ( e
.keyCode
) {
5055 case OO
.ui
.Keys
.ENTER
:
5056 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5057 // Was only highlighted, now let's select it. No-op if already selected.
5058 this.chooseItem( currentItem
);
5063 case OO
.ui
.Keys
.LEFT
:
5064 this.clearKeyPressBuffer();
5065 nextItem
= this.getRelativeSelectableItem( currentItem
, -1 );
5068 case OO
.ui
.Keys
.DOWN
:
5069 case OO
.ui
.Keys
.RIGHT
:
5070 this.clearKeyPressBuffer();
5071 nextItem
= this.getRelativeSelectableItem( currentItem
, 1 );
5074 case OO
.ui
.Keys
.ESCAPE
:
5075 case OO
.ui
.Keys
.TAB
:
5076 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
5077 currentItem
.setHighlighted( false );
5079 this.unbindKeyDownListener();
5080 this.unbindKeyPressListener();
5081 // Don't prevent tabbing away / defocusing
5087 if ( nextItem
.constructor.static.highlightable
) {
5088 this.highlightItem( nextItem
);
5090 this.chooseItem( nextItem
);
5092 nextItem
.scrollElementIntoView();
5096 // Can't just return false, because e is not always a jQuery event
5098 e
.stopPropagation();
5104 * Bind key down listener.
5108 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
5109 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
5113 * Unbind key down listener.
5117 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
5118 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
5122 * Clear the key-press buffer
5126 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
5127 if ( this.keyPressBufferTimer
) {
5128 clearTimeout( this.keyPressBufferTimer
);
5129 this.keyPressBufferTimer
= null;
5131 this.keyPressBuffer
= '';
5135 * Handle key press events.
5138 * @param {jQuery.Event} e Key press event
5140 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
5141 var c
, filter
, item
;
5143 if ( !e
.charCode
) {
5144 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
5145 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
5150 if ( String
.fromCodePoint
) {
5151 c
= String
.fromCodePoint( e
.charCode
);
5153 c
= String
.fromCharCode( e
.charCode
);
5156 if ( this.keyPressBufferTimer
) {
5157 clearTimeout( this.keyPressBufferTimer
);
5159 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
5161 item
= this.getHighlightedItem() || this.getSelectedItem();
5163 if ( this.keyPressBuffer
=== c
) {
5164 // Common (if weird) special case: typing "xxxx" will cycle through all
5165 // the items beginning with "x".
5167 item
= this.getRelativeSelectableItem( item
, 1 );
5170 this.keyPressBuffer
+= c
;
5173 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
5174 if ( !item
|| !filter( item
) ) {
5175 item
= this.getRelativeSelectableItem( item
, 1, filter
);
5178 if ( item
.constructor.static.highlightable
) {
5179 this.highlightItem( item
);
5181 this.chooseItem( item
);
5183 item
.scrollElementIntoView();
5190 * Get a matcher for the specific string
5193 * @param {string} s String to match against items
5194 * @param {boolean} [exact=false] Only accept exact matches
5195 * @return {Function} function ( OO.ui.OptionItem ) => boolean
5197 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
5200 if ( s
.normalize
) {
5203 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
5204 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
5208 re
= new RegExp( re
, 'i' );
5209 return function ( item
) {
5210 var l
= item
.getLabel();
5211 if ( typeof l
!== 'string' ) {
5212 l
= item
.$label
.text();
5214 if ( l
.normalize
) {
5217 return re
.test( l
);
5222 * Bind key press listener.
5226 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
5227 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
5231 * Unbind key down listener.
5233 * If you override this, be sure to call this.clearKeyPressBuffer() from your
5238 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
5239 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
5240 this.clearKeyPressBuffer();
5244 * Visibility change handler
5247 * @param {boolean} visible
5249 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
5251 this.clearKeyPressBuffer();
5256 * Get the closest item to a jQuery.Event.
5259 * @param {jQuery.Event} e
5260 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
5262 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
5263 return $( e
.target
).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
5267 * Get selected item.
5269 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
5271 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
5274 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5275 if ( this.items
[ i
].isSelected() ) {
5276 return this.items
[ i
];
5283 * Get highlighted item.
5285 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
5287 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
5290 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5291 if ( this.items
[ i
].isHighlighted() ) {
5292 return this.items
[ i
];
5299 * Toggle pressed state.
5301 * Press is a state that occurs when a user mouses down on an item, but
5302 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
5303 * until the user releases the mouse.
5305 * @param {boolean} pressed An option is being pressed
5307 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
5308 if ( pressed
=== undefined ) {
5309 pressed
= !this.pressed
;
5311 if ( pressed
!== this.pressed
) {
5313 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
5314 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
5315 this.pressed
= pressed
;
5320 * Highlight an option. If the `item` param is omitted, no options will be highlighted
5321 * and any existing highlight will be removed. The highlight is mutually exclusive.
5323 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
5327 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
5328 var i
, len
, highlighted
,
5331 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5332 highlighted
= this.items
[ i
] === item
;
5333 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
5334 this.items
[ i
].setHighlighted( highlighted
);
5339 this.emit( 'highlight', item
);
5346 * Fetch an item by its label.
5348 * @param {string} label Label of the item to select.
5349 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5350 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
5352 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
5354 len
= this.items
.length
,
5355 filter
= this.getItemMatcher( label
, true );
5357 for ( i
= 0; i
< len
; i
++ ) {
5358 item
= this.items
[ i
];
5359 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5366 filter
= this.getItemMatcher( label
, false );
5367 for ( i
= 0; i
< len
; i
++ ) {
5368 item
= this.items
[ i
];
5369 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5385 * Programmatically select an option by its label. If the item does not exist,
5386 * all options will be deselected.
5388 * @param {string} [label] Label of the item to select.
5389 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5393 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
5394 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
5395 if ( label
=== undefined || !itemFromLabel
) {
5396 return this.selectItem();
5398 return this.selectItem( itemFromLabel
);
5402 * Programmatically select an option by its data. If the `data` parameter is omitted,
5403 * or if the item does not exist, all options will be deselected.
5405 * @param {Object|string} [data] Value of the item to select, omit to deselect all
5409 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
5410 var itemFromData
= this.getItemFromData( data
);
5411 if ( data
=== undefined || !itemFromData
) {
5412 return this.selectItem();
5414 return this.selectItem( itemFromData
);
5418 * Programmatically select an option by its reference. If the `item` parameter is omitted,
5419 * all options will be deselected.
5421 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
5425 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
5426 var i
, len
, selected
,
5429 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5430 selected
= this.items
[ i
] === item
;
5431 if ( this.items
[ i
].isSelected() !== selected
) {
5432 this.items
[ i
].setSelected( selected
);
5437 this.emit( 'select', item
);
5446 * Press is a state that occurs when a user mouses down on an item, but has not
5447 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
5448 * releases the mouse.
5450 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
5454 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
5455 var i
, len
, pressed
,
5458 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5459 pressed
= this.items
[ i
] === item
;
5460 if ( this.items
[ i
].isPressed() !== pressed
) {
5461 this.items
[ i
].setPressed( pressed
);
5466 this.emit( 'press', item
);
5475 * Note that ‘choose’ should never be modified programmatically. A user can choose
5476 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
5477 * use the #selectItem method.
5479 * This method is identical to #selectItem, but may vary in subclasses that take additional action
5480 * when users choose an item with the keyboard or mouse.
5482 * @param {OO.ui.OptionWidget} item Item to choose
5486 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
5488 this.selectItem( item
);
5489 this.emit( 'choose', item
);
5496 * Get an option by its position relative to the specified item (or to the start of the option array,
5497 * if item is `null`). The direction in which to search through the option array is specified with a
5498 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
5499 * `null` if there are no options in the array.
5501 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
5502 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
5503 * @param {Function} filter Only consider items for which this function returns
5504 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
5505 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
5507 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
, filter
) {
5508 var currentIndex
, nextIndex
, i
,
5509 increase
= direction
> 0 ? 1 : -1,
5510 len
= this.items
.length
;
5512 if ( !$.isFunction( filter
) ) {
5513 filter
= OO
.ui
.SelectWidget
.static.passAllFilter
;
5516 if ( item
instanceof OO
.ui
.OptionWidget
) {
5517 currentIndex
= this.items
.indexOf( item
);
5518 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
5520 // If no item is selected and moving forward, start at the beginning.
5521 // If moving backward, start at the end.
5522 nextIndex
= direction
> 0 ? 0 : len
- 1;
5525 for ( i
= 0; i
< len
; i
++ ) {
5526 item
= this.items
[ nextIndex
];
5527 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
5530 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
5536 * Get the next selectable item or `null` if there are no selectable items.
5537 * Disabled options and menu-section markers and breaks are not selectable.
5539 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
5541 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
5544 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5545 item
= this.items
[ i
];
5546 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() ) {
5555 * Add an array of options to the select. Optionally, an index number can be used to
5556 * specify an insertion point.
5558 * @param {OO.ui.OptionWidget[]} items Items to add
5559 * @param {number} [index] Index to insert items after
5563 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
5565 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
5567 // Always provide an index, even if it was omitted
5568 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
5574 * Remove the specified array of options from the select. Options will be detached
5575 * from the DOM, not removed, so they can be reused later. To remove all options from
5576 * the select, you may wish to use the #clearItems method instead.
5578 * @param {OO.ui.OptionWidget[]} items Items to remove
5582 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
5585 // Deselect items being removed
5586 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
5588 if ( item
.isSelected() ) {
5589 this.selectItem( null );
5594 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
5596 this.emit( 'remove', items
);
5602 * Clear all options from the select. Options will be detached from the DOM, not removed,
5603 * so that they can be reused later. To remove a subset of options from the select, use
5604 * the #removeItems method.
5609 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
5610 var items
= this.items
.slice();
5613 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
5616 this.selectItem( null );
5618 this.emit( 'remove', items
);
5624 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
5625 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
5626 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
5627 * options. For more information about options and selects, please see the
5628 * [OOjs UI documentation on MediaWiki][1].
5631 * // Decorated options in a select widget
5632 * var select = new OO.ui.SelectWidget( {
5634 * new OO.ui.DecoratedOptionWidget( {
5636 * label: 'Option with icon',
5639 * new OO.ui.DecoratedOptionWidget( {
5641 * label: 'Option with indicator',
5646 * $( 'body' ).append( select.$element );
5648 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5651 * @extends OO.ui.OptionWidget
5652 * @mixins OO.ui.mixin.IconElement
5653 * @mixins OO.ui.mixin.IndicatorElement
5656 * @param {Object} [config] Configuration options
5658 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
5659 // Parent constructor
5660 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
5662 // Mixin constructors
5663 OO
.ui
.mixin
.IconElement
.call( this, config
);
5664 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
5668 .addClass( 'oo-ui-decoratedOptionWidget' )
5669 .prepend( this.$icon
)
5670 .append( this.$indicator
);
5675 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
5676 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
5677 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
5680 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
5681 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
5682 * the [OOjs UI documentation on MediaWiki] [1] for more information.
5684 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
5687 * @extends OO.ui.DecoratedOptionWidget
5690 * @param {Object} [config] Configuration options
5692 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
5693 // Configuration initialization
5694 config
= $.extend( { icon
: 'check' }, config
);
5696 // Parent constructor
5697 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
5701 .attr( 'role', 'menuitem' )
5702 .addClass( 'oo-ui-menuOptionWidget' );
5707 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
5709 /* Static Properties */
5711 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
5714 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
5715 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
5718 * var myDropdown = new OO.ui.DropdownWidget( {
5721 * new OO.ui.MenuSectionOptionWidget( {
5724 * new OO.ui.MenuOptionWidget( {
5726 * label: 'Welsh Corgi'
5728 * new OO.ui.MenuOptionWidget( {
5730 * label: 'Standard Poodle'
5732 * new OO.ui.MenuSectionOptionWidget( {
5735 * new OO.ui.MenuOptionWidget( {
5742 * $( 'body' ).append( myDropdown.$element );
5745 * @extends OO.ui.DecoratedOptionWidget
5748 * @param {Object} [config] Configuration options
5750 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
5751 // Parent constructor
5752 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
5755 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' );
5760 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
5762 /* Static Properties */
5764 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
5766 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
5769 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
5770 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
5771 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
5772 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
5773 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
5774 * and customized to be opened, closed, and displayed as needed.
5776 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
5777 * mouse outside the menu.
5779 * Menus also have support for keyboard interaction:
5781 * - Enter/Return key: choose and select a menu option
5782 * - Up-arrow key: highlight the previous menu option
5783 * - Down-arrow key: highlight the next menu option
5784 * - Esc key: hide the menu
5786 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
5787 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5790 * @extends OO.ui.SelectWidget
5791 * @mixins OO.ui.mixin.ClippableElement
5794 * @param {Object} [config] Configuration options
5795 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
5796 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
5797 * and {@link OO.ui.mixin.LookupElement LookupElement}
5798 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
5799 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiSelectWidget CapsuleMultiSelectWidget}
5800 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
5801 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
5802 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
5803 * that button, unless the button (or its parent widget) is passed in here.
5804 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
5805 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
5807 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
5808 // Configuration initialization
5809 config
= config
|| {};
5811 // Parent constructor
5812 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
5814 // Mixin constructors
5815 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
5818 this.newItems
= null;
5819 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
5820 this.filterFromInput
= !!config
.filterFromInput
;
5821 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
5822 this.$widget
= config
.widget
? config
.widget
.$element
: null;
5823 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5824 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
5828 .addClass( 'oo-ui-menuSelectWidget' )
5829 .attr( 'role', 'menu' );
5831 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5832 // that reference properties not initialized at that time of parent class construction
5833 // TODO: Find a better way to handle post-constructor setup
5834 this.visible
= false;
5835 this.$element
.addClass( 'oo-ui-element-hidden' );
5840 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
5841 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
5846 * Handles document mouse down events.
5849 * @param {jQuery.Event} e Key down event
5851 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
5853 !OO
.ui
.contains( this.$element
[ 0 ], e
.target
, true ) &&
5854 ( !this.$widget
|| !OO
.ui
.contains( this.$widget
[ 0 ], e
.target
, true ) )
5856 this.toggle( false );
5863 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
5864 var currentItem
= this.getHighlightedItem() || this.getSelectedItem();
5866 if ( !this.isDisabled() && this.isVisible() ) {
5867 switch ( e
.keyCode
) {
5868 case OO
.ui
.Keys
.LEFT
:
5869 case OO
.ui
.Keys
.RIGHT
:
5870 // Do nothing if a text field is associated, arrow keys will be handled natively
5871 if ( !this.$input
) {
5872 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
5875 case OO
.ui
.Keys
.ESCAPE
:
5876 case OO
.ui
.Keys
.TAB
:
5877 if ( currentItem
) {
5878 currentItem
.setHighlighted( false );
5880 this.toggle( false );
5881 // Don't prevent tabbing away, prevent defocusing
5882 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
5884 e
.stopPropagation();
5888 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
5895 * Update menu item visibility after input changes.
5898 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
5900 len
= this.items
.length
,
5901 showAll
= !this.isVisible(),
5902 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
5904 for ( i
= 0; i
< len
; i
++ ) {
5905 item
= this.items
[ i
];
5906 if ( item
instanceof OO
.ui
.OptionWidget
) {
5907 item
.toggle( showAll
|| filter( item
) );
5911 // Reevaluate clipping
5918 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
5919 if ( this.$input
) {
5920 this.$input
.on( 'keydown', this.onKeyDownHandler
);
5922 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
5929 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
5930 if ( this.$input
) {
5931 this.$input
.off( 'keydown', this.onKeyDownHandler
);
5933 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
5940 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
5941 if ( this.$input
) {
5942 if ( this.filterFromInput
) {
5943 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
5946 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
5953 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
5954 if ( this.$input
) {
5955 if ( this.filterFromInput
) {
5956 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
5957 this.updateItemVisibility();
5960 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
5967 * When a user chooses an item, the menu is closed.
5969 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
5970 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
5971 * @param {OO.ui.OptionWidget} item Item to choose
5974 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
5975 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
5976 this.toggle( false );
5983 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
5987 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
5990 if ( !this.newItems
) {
5994 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
5996 if ( this.isVisible() ) {
5997 // Defer fitting label until item has been attached
6000 this.newItems
.push( item
);
6004 // Reevaluate clipping
6013 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
6015 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
6017 // Reevaluate clipping
6026 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
6028 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
6030 // Reevaluate clipping
6039 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
6042 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
6043 change
= visible
!== this.isVisible();
6046 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
6050 this.bindKeyDownListener();
6051 this.bindKeyPressListener();
6053 if ( this.newItems
&& this.newItems
.length
) {
6054 for ( i
= 0, len
= this.newItems
.length
; i
< len
; i
++ ) {
6055 this.newItems
[ i
].fitLabel();
6057 this.newItems
= null;
6059 this.toggleClipping( true );
6062 if ( this.autoHide
) {
6063 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6066 this.unbindKeyDownListener();
6067 this.unbindKeyPressListener();
6068 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
6069 this.toggleClipping( false );
6077 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
6078 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
6079 * users can interact with it.
6081 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6082 * OO.ui.DropdownInputWidget instead.
6085 * // Example: A DropdownWidget with a menu that contains three options
6086 * var dropDown = new OO.ui.DropdownWidget( {
6087 * label: 'Dropdown menu: Select a menu option',
6090 * new OO.ui.MenuOptionWidget( {
6094 * new OO.ui.MenuOptionWidget( {
6098 * new OO.ui.MenuOptionWidget( {
6106 * $( 'body' ).append( dropDown.$element );
6108 * dropDown.getMenu().selectItemByData( 'b' );
6110 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
6112 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
6114 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6117 * @extends OO.ui.Widget
6118 * @mixins OO.ui.mixin.IconElement
6119 * @mixins OO.ui.mixin.IndicatorElement
6120 * @mixins OO.ui.mixin.LabelElement
6121 * @mixins OO.ui.mixin.TitledElement
6122 * @mixins OO.ui.mixin.TabIndexedElement
6125 * @param {Object} [config] Configuration options
6126 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
6127 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
6128 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6129 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
6131 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
6132 // Configuration initialization
6133 config
= $.extend( { indicator
: 'down' }, config
);
6135 // Parent constructor
6136 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
6138 // Properties (must be set before TabIndexedElement constructor call)
6139 this.$handle
= this.$( '<span>' );
6140 this.$overlay
= config
.$overlay
|| this.$element
;
6142 // Mixin constructors
6143 OO
.ui
.mixin
.IconElement
.call( this, config
);
6144 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
6145 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6146 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
6147 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
6150 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend( {
6152 $container
: this.$element
6157 click
: this.onClick
.bind( this ),
6158 keypress
: this.onKeyPress
.bind( this )
6160 this.menu
.connect( this, { select
: 'onMenuSelect' } );
6164 .addClass( 'oo-ui-dropdownWidget-handle' )
6165 .append( this.$icon
, this.$label
, this.$indicator
);
6167 .addClass( 'oo-ui-dropdownWidget' )
6168 .append( this.$handle
);
6169 this.$overlay
.append( this.menu
.$element
);
6174 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
6175 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
6176 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
6177 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
6178 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
6179 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6186 * @return {OO.ui.MenuSelectWidget} Menu of widget
6188 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
6193 * Handles menu select events.
6196 * @param {OO.ui.MenuOptionWidget} item Selected menu item
6198 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
6202 this.setLabel( null );
6206 selectedLabel
= item
.getLabel();
6208 // If the label is a DOM element, clone it, because setLabel will append() it
6209 if ( selectedLabel
instanceof jQuery
) {
6210 selectedLabel
= selectedLabel
.clone();
6213 this.setLabel( selectedLabel
);
6217 * Handle mouse click events.
6220 * @param {jQuery.Event} e Mouse click event
6222 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
6223 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6230 * Handle key press events.
6233 * @param {jQuery.Event} e Key press event
6235 OO
.ui
.DropdownWidget
.prototype.onKeyPress = function ( e
) {
6236 if ( !this.isDisabled() &&
6237 ( ( e
.which
=== OO
.ui
.Keys
.SPACE
&& !this.menu
.isVisible() ) || e
.which
=== OO
.ui
.Keys
.ENTER
)
6245 * RadioOptionWidget is an option widget that looks like a radio button.
6246 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
6247 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6249 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6252 * @extends OO.ui.OptionWidget
6255 * @param {Object} [config] Configuration options
6257 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
6258 // Configuration initialization
6259 config
= config
|| {};
6261 // Properties (must be done before parent constructor which calls #setDisabled)
6262 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
6264 // Parent constructor
6265 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
6268 this.radio
.$input
.on( 'focus', this.onInputFocus
.bind( this ) );
6271 // Remove implicit role, we're handling it ourselves
6272 this.radio
.$input
.attr( 'role', 'presentation' );
6274 .addClass( 'oo-ui-radioOptionWidget' )
6275 .attr( 'role', 'radio' )
6276 .attr( 'aria-checked', 'false' )
6277 .removeAttr( 'aria-selected' )
6278 .prepend( this.radio
.$element
);
6283 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
6285 /* Static Properties */
6287 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
6289 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
6291 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
6293 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
6298 * @param {jQuery.Event} e Focus event
6301 OO
.ui
.RadioOptionWidget
.prototype.onInputFocus = function () {
6302 this.radio
.$input
.blur();
6303 this.$element
.parent().focus();
6309 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
6310 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
6312 this.radio
.setSelected( state
);
6314 .attr( 'aria-checked', state
.toString() )
6315 .removeAttr( 'aria-selected' );
6323 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
6324 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
6326 this.radio
.setDisabled( this.isDisabled() );
6332 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
6333 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
6334 * an interface for adding, removing and selecting options.
6335 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6337 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6338 * OO.ui.RadioSelectInputWidget instead.
6341 * // A RadioSelectWidget with RadioOptions.
6342 * var option1 = new OO.ui.RadioOptionWidget( {
6344 * label: 'Selected radio option'
6347 * var option2 = new OO.ui.RadioOptionWidget( {
6349 * label: 'Unselected radio option'
6352 * var radioSelect=new OO.ui.RadioSelectWidget( {
6353 * items: [ option1, option2 ]
6356 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
6357 * radioSelect.selectItem( option1 );
6359 * $( 'body' ).append( radioSelect.$element );
6361 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6365 * @extends OO.ui.SelectWidget
6366 * @mixins OO.ui.mixin.TabIndexedElement
6369 * @param {Object} [config] Configuration options
6371 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
6372 // Parent constructor
6373 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
6375 // Mixin constructors
6376 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
6380 focus
: this.bindKeyDownListener
.bind( this ),
6381 blur
: this.unbindKeyDownListener
.bind( this )
6386 .addClass( 'oo-ui-radioSelectWidget' )
6387 .attr( 'role', 'radiogroup' );
6392 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
6393 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6396 * Element that will stick under a specified container, even when it is inserted elsewhere in the
6397 * document (for example, in a OO.ui.Window's $overlay).
6399 * The elements's position is automatically calculated and maintained when window is resized or the
6400 * page is scrolled. If you reposition the container manually, you have to call #position to make
6401 * sure the element is still placed correctly.
6403 * As positioning is only possible when both the element and the container are attached to the DOM
6404 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
6405 * the #toggle method to display a floating popup, for example.
6411 * @param {Object} [config] Configuration options
6412 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
6413 * @cfg {jQuery} [$floatableContainer] Node to position below
6415 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
6416 // Configuration initialization
6417 config
= config
|| {};
6420 this.$floatable
= null;
6421 this.$floatableContainer
= null;
6422 this.$floatableWindow
= null;
6423 this.$floatableClosestScrollable
= null;
6424 this.onFloatableScrollHandler
= this.position
.bind( this );
6425 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
6428 this.setFloatableContainer( config
.$floatableContainer
);
6429 this.setFloatableElement( config
.$floatable
|| this.$element
);
6435 * Set floatable element.
6437 * If an element is already set, it will be cleaned up before setting up the new element.
6439 * @param {jQuery} $floatable Element to make floatable
6441 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
6442 if ( this.$floatable
) {
6443 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
6444 this.$floatable
.css( { left
: '', top
: '' } );
6447 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
6452 * Set floatable container.
6454 * The element will be always positioned under the specified container.
6456 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
6458 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
6459 this.$floatableContainer
= $floatableContainer
;
6460 if ( this.$floatable
) {
6466 * Toggle positioning.
6468 * Do not turn positioning on until after the element is attached to the DOM and visible.
6470 * @param {boolean} [positioning] Enable positioning, omit to toggle
6473 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
6474 var closestScrollableOfContainer
, closestScrollableOfFloatable
;
6476 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
6478 if ( this.positioning
!== positioning
) {
6479 this.positioning
= positioning
;
6481 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
6482 closestScrollableOfFloatable
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatable
[ 0 ] );
6483 if ( closestScrollableOfContainer
!== closestScrollableOfFloatable
) {
6484 // If the scrollable is the root, we have to listen to scroll events
6485 // on the window because of browser inconsistencies (or do we? someone should verify this)
6486 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
6487 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
6491 if ( positioning
) {
6492 this.$floatableWindow
= $( this.getElementWindow() );
6493 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
6495 if ( closestScrollableOfContainer
!== closestScrollableOfFloatable
) {
6496 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
6497 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
6500 // Initial position after visible
6503 if ( this.$floatableWindow
) {
6504 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
6505 this.$floatableWindow
= null;
6508 if ( this.$floatableClosestScrollable
) {
6509 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
6510 this.$floatableClosestScrollable
= null;
6513 this.$floatable
.css( { left
: '', top
: '' } );
6521 * Position the floatable below its container.
6523 * This should only be done when both of them are attached to the DOM and visible.
6527 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
6530 if ( !this.positioning
) {
6534 pos
= OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, this.$floatable
.offsetParent() );
6536 // Position under container
6537 pos
.top
+= this.$floatableContainer
.height();
6538 this.$floatable
.css( pos
);
6540 // We updated the position, so re-evaluate the clipping state.
6541 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
6542 // will not notice the need to update itself.)
6543 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
6544 // it not listen to the right events in the right places?
6553 * FloatingMenuSelectWidget is a menu that will stick under a specified
6554 * container, even when it is inserted elsewhere in the document (for example,
6555 * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
6556 * menu from being clipped too aggresively.
6558 * The menu's position is automatically calculated and maintained when the menu
6559 * is toggled or the window is resized.
6561 * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
6564 * @extends OO.ui.MenuSelectWidget
6565 * @mixins OO.ui.mixin.FloatableElement
6568 * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
6569 * Deprecated, omit this parameter and specify `$container` instead.
6570 * @param {Object} [config] Configuration options
6571 * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
6573 OO
.ui
.FloatingMenuSelectWidget
= function OoUiFloatingMenuSelectWidget( inputWidget
, config
) {
6574 // Allow 'inputWidget' parameter and config for backwards compatibility
6575 if ( OO
.isPlainObject( inputWidget
) && config
=== undefined ) {
6576 config
= inputWidget
;
6577 inputWidget
= config
.inputWidget
;
6580 // Configuration initialization
6581 config
= config
|| {};
6583 // Parent constructor
6584 OO
.ui
.FloatingMenuSelectWidget
.parent
.call( this, config
);
6586 // Properties (must be set before mixin constructors)
6587 this.inputWidget
= inputWidget
; // For backwards compatibility
6588 this.$container
= config
.$container
|| this.inputWidget
.$element
;
6590 // Mixins constructors
6591 OO
.ui
.mixin
.FloatableElement
.call( this, $.extend( {}, config
, { $floatableContainer
: this.$container
} ) );
6594 this.$element
.addClass( 'oo-ui-floatingMenuSelectWidget' );
6595 // For backwards compatibility
6596 this.$element
.addClass( 'oo-ui-textInputMenuSelectWidget' );
6601 OO
.inheritClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.MenuSelectWidget
);
6602 OO
.mixinClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
6604 // For backwards compatibility
6605 OO
.ui
.TextInputMenuSelectWidget
= OO
.ui
.FloatingMenuSelectWidget
;
6612 OO
.ui
.FloatingMenuSelectWidget
.prototype.toggle = function ( visible
) {
6614 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
6615 change
= visible
!== this.isVisible();
6617 if ( change
&& visible
) {
6618 // Make sure the width is set before the parent method runs.
6619 this.setIdealSize( this.$container
.width() );
6623 // This will call this.clip(), which is nonsensical since we're not positioned yet...
6624 OO
.ui
.FloatingMenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
6627 this.togglePositioning( this.isVisible() );
6634 * InputWidget is the base class for all input widgets, which
6635 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
6636 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
6637 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
6639 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
6643 * @extends OO.ui.Widget
6644 * @mixins OO.ui.mixin.FlaggedElement
6645 * @mixins OO.ui.mixin.TabIndexedElement
6646 * @mixins OO.ui.mixin.TitledElement
6647 * @mixins OO.ui.mixin.AccessKeyedElement
6650 * @param {Object} [config] Configuration options
6651 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
6652 * @cfg {string} [value=''] The value of the input.
6653 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
6654 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
6655 * before it is accepted.
6657 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
6658 // Configuration initialization
6659 config
= config
|| {};
6661 // Parent constructor
6662 OO
.ui
.InputWidget
.parent
.call( this, config
);
6665 this.$input
= this.getInputElement( config
);
6667 this.inputFilter
= config
.inputFilter
;
6669 // Mixin constructors
6670 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6671 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
6672 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
6673 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
6676 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
6680 .addClass( 'oo-ui-inputWidget-input' )
6681 .attr( 'name', config
.name
)
6682 .prop( 'disabled', this.isDisabled() );
6684 .addClass( 'oo-ui-inputWidget' )
6685 .append( this.$input
);
6686 this.setValue( config
.value
);
6687 this.setAccessKey( config
.accessKey
);
6689 this.setDir( config
.dir
);
6695 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
6696 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
6697 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
6698 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
6699 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6701 /* Static Properties */
6703 OO
.ui
.InputWidget
.static.supportsSimpleLabel
= true;
6705 /* Static Methods */
6710 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
6711 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
6712 // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
6713 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
6720 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
6721 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
6722 state
.value
= config
.$input
.val();
6723 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
6724 state
.focus
= config
.$input
.is( ':focus' );
6733 * A change event is emitted when the value of the input changes.
6735 * @param {string} value
6741 * Get input element.
6743 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
6744 * different circumstances. The element must have a `value` property (like form elements).
6747 * @param {Object} config Configuration options
6748 * @return {jQuery} Input element
6750 OO
.ui
.InputWidget
.prototype.getInputElement = function ( config
) {
6751 // See #reusePreInfuseDOM about config.$input
6752 return config
.$input
|| $( '<input>' );
6756 * Handle potentially value-changing events.
6759 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
6761 OO
.ui
.InputWidget
.prototype.onEdit = function () {
6763 if ( !this.isDisabled() ) {
6764 // Allow the stack to clear so the value will be updated
6765 setTimeout( function () {
6766 widget
.setValue( widget
.$input
.val() );
6772 * Get the value of the input.
6774 * @return {string} Input value
6776 OO
.ui
.InputWidget
.prototype.getValue = function () {
6777 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
6778 // it, and we won't know unless they're kind enough to trigger a 'change' event.
6779 var value
= this.$input
.val();
6780 if ( this.value
!== value
) {
6781 this.setValue( value
);
6787 * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
6789 * @deprecated since v0.13.1, use #setDir directly
6790 * @param {boolean} isRTL Directionality is right-to-left
6793 OO
.ui
.InputWidget
.prototype.setRTL = function ( isRTL
) {
6794 this.setDir( isRTL
? 'rtl' : 'ltr' );
6799 * Set the directionality of the input.
6801 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
6804 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
6805 this.$input
.prop( 'dir', dir
);
6810 * Set the value of the input.
6812 * @param {string} value New value
6816 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
6817 value
= this.cleanUpValue( value
);
6818 // Update the DOM if it has changed. Note that with cleanUpValue, it
6819 // is possible for the DOM value to change without this.value changing.
6820 if ( this.$input
.val() !== value
) {
6821 this.$input
.val( value
);
6823 if ( this.value
!== value
) {
6825 this.emit( 'change', this.value
);
6831 * Set the input's access key.
6832 * FIXME: This is the same code as in OO.ui.mixin.ButtonElement, maybe find a better place for it?
6834 * @param {string} accessKey Input's access key, use empty string to remove
6837 OO
.ui
.InputWidget
.prototype.setAccessKey = function ( accessKey
) {
6838 accessKey
= typeof accessKey
=== 'string' && accessKey
.length
? accessKey
: null;
6840 if ( this.accessKey
!== accessKey
) {
6841 if ( this.$input
) {
6842 if ( accessKey
!== null ) {
6843 this.$input
.attr( 'accesskey', accessKey
);
6845 this.$input
.removeAttr( 'accesskey' );
6848 this.accessKey
= accessKey
;
6855 * Clean up incoming value.
6857 * Ensures value is a string, and converts undefined and null to empty string.
6860 * @param {string} value Original value
6861 * @return {string} Cleaned up value
6863 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
6864 if ( value
=== undefined || value
=== null ) {
6866 } else if ( this.inputFilter
) {
6867 return this.inputFilter( String( value
) );
6869 return String( value
);
6874 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
6875 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
6878 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
6879 if ( !this.isDisabled() ) {
6880 if ( this.$input
.is( ':checkbox, :radio' ) ) {
6881 this.$input
.click();
6883 if ( this.$input
.is( ':input' ) ) {
6884 this.$input
[ 0 ].focus();
6892 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
6893 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
6894 if ( this.$input
) {
6895 this.$input
.prop( 'disabled', this.isDisabled() );
6905 OO
.ui
.InputWidget
.prototype.focus = function () {
6906 this.$input
[ 0 ].focus();
6915 OO
.ui
.InputWidget
.prototype.blur = function () {
6916 this.$input
[ 0 ].blur();
6923 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
6924 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
6925 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
6926 this.setValue( state
.value
);
6928 if ( state
.focus
) {
6934 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
6935 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
6936 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
6937 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
6938 * [OOjs UI documentation on MediaWiki] [1] for more information.
6941 * // A ButtonInputWidget rendered as an HTML button, the default.
6942 * var button = new OO.ui.ButtonInputWidget( {
6943 * label: 'Input button',
6947 * $( 'body' ).append( button.$element );
6949 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
6952 * @extends OO.ui.InputWidget
6953 * @mixins OO.ui.mixin.ButtonElement
6954 * @mixins OO.ui.mixin.IconElement
6955 * @mixins OO.ui.mixin.IndicatorElement
6956 * @mixins OO.ui.mixin.LabelElement
6957 * @mixins OO.ui.mixin.TitledElement
6960 * @param {Object} [config] Configuration options
6961 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
6962 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
6963 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
6964 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
6965 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
6967 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
6968 // Configuration initialization
6969 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
6971 // Properties (must be set before parent constructor, which calls #setValue)
6972 this.useInputTag
= config
.useInputTag
;
6974 // Parent constructor
6975 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
6977 // Mixin constructors
6978 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
6979 OO
.ui
.mixin
.IconElement
.call( this, config
);
6980 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
6981 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6982 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
6985 if ( !config
.useInputTag
) {
6986 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
6988 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
6993 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
6994 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
6995 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
6996 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
6997 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
6998 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
7000 /* Static Properties */
7003 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
7004 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
7006 OO
.ui
.ButtonInputWidget
.static.supportsSimpleLabel
= false;
7014 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
7016 // See InputWidget#reusePreInfuseDOM about config.$input
7017 if ( config
.$input
) {
7018 return config
.$input
.empty();
7020 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
7021 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
7027 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
7029 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
7030 * text, or `null` for no label
7033 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
7034 OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
7036 if ( this.useInputTag
) {
7037 if ( typeof label
=== 'function' ) {
7038 label
= OO
.ui
.resolveMsg( label
);
7040 if ( label
instanceof jQuery
) {
7041 label
= label
.text();
7046 this.$input
.val( label
);
7053 * Set the value of the input.
7055 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
7056 * they do not support {@link #value values}.
7058 * @param {string} value New value
7061 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
7062 if ( !this.useInputTag
) {
7063 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
7069 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
7070 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
7071 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
7072 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
7074 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7077 * // An example of selected, unselected, and disabled checkbox inputs
7078 * var checkbox1=new OO.ui.CheckboxInputWidget( {
7082 * var checkbox2=new OO.ui.CheckboxInputWidget( {
7085 * var checkbox3=new OO.ui.CheckboxInputWidget( {
7089 * // Create a fieldset layout with fields for each checkbox.
7090 * var fieldset = new OO.ui.FieldsetLayout( {
7091 * label: 'Checkboxes'
7093 * fieldset.addItems( [
7094 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
7095 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
7096 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
7098 * $( 'body' ).append( fieldset.$element );
7100 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7103 * @extends OO.ui.InputWidget
7106 * @param {Object} [config] Configuration options
7107 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
7109 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
7110 // Configuration initialization
7111 config
= config
|| {};
7113 // Parent constructor
7114 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
7118 .addClass( 'oo-ui-checkboxInputWidget' )
7119 // Required for pretty styling in MediaWiki theme
7120 .append( $( '<span>' ) );
7121 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
7126 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
7128 /* Static Methods */
7133 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7134 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7135 state
.checked
= config
.$input
.prop( 'checked' );
7145 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
7146 return $( '<input type="checkbox" />' );
7152 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
7154 if ( !this.isDisabled() ) {
7155 // Allow the stack to clear so the value will be updated
7156 setTimeout( function () {
7157 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
7163 * Set selection state of this checkbox.
7165 * @param {boolean} state `true` for selected
7168 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
7170 if ( this.selected
!== state
) {
7171 this.selected
= state
;
7172 this.$input
.prop( 'checked', this.selected
);
7173 this.emit( 'change', this.selected
);
7179 * Check if this checkbox is selected.
7181 * @return {boolean} Checkbox is selected
7183 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
7184 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7185 // it, and we won't know unless they're kind enough to trigger a 'change' event.
7186 var selected
= this.$input
.prop( 'checked' );
7187 if ( this.selected
!== selected
) {
7188 this.setSelected( selected
);
7190 return this.selected
;
7196 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
7197 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7198 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
7199 this.setSelected( state
.checked
);
7204 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
7205 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7206 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7207 * more information about input widgets.
7209 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
7210 * are no options. If no `value` configuration option is provided, the first option is selected.
7211 * If you need a state representing no value (no option being selected), use a DropdownWidget.
7213 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
7216 * // Example: A DropdownInputWidget with three options
7217 * var dropdownInput = new OO.ui.DropdownInputWidget( {
7219 * { data: 'a', label: 'First' },
7220 * { data: 'b', label: 'Second'},
7221 * { data: 'c', label: 'Third' }
7224 * $( 'body' ).append( dropdownInput.$element );
7226 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7229 * @extends OO.ui.InputWidget
7230 * @mixins OO.ui.mixin.TitledElement
7233 * @param {Object} [config] Configuration options
7234 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7235 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
7237 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
7238 // Configuration initialization
7239 config
= config
|| {};
7241 // Properties (must be done before parent constructor which calls #setDisabled)
7242 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
7244 // Parent constructor
7245 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
7247 // Mixin constructors
7248 OO
.ui
.mixin
.TitledElement
.call( this, config
);
7251 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
7254 this.setOptions( config
.options
|| [] );
7256 .addClass( 'oo-ui-dropdownInputWidget' )
7257 .append( this.dropdownWidget
.$element
);
7262 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
7263 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
7271 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function ( config
) {
7272 // See InputWidget#reusePreInfuseDOM about config.$input
7273 if ( config
.$input
) {
7274 return config
.$input
.addClass( 'oo-ui-element-hidden' );
7276 return $( '<input type="hidden">' );
7280 * Handles menu select events.
7283 * @param {OO.ui.MenuOptionWidget} item Selected menu item
7285 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
7286 this.setValue( item
.getData() );
7292 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
7293 value
= this.cleanUpValue( value
);
7294 this.dropdownWidget
.getMenu().selectItemByData( value
);
7295 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
7302 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
7303 this.dropdownWidget
.setDisabled( state
);
7304 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
7309 * Set the options available for this input.
7311 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
7314 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
7316 value
= this.getValue(),
7319 // Rebuild the dropdown menu
7320 this.dropdownWidget
.getMenu()
7322 .addItems( options
.map( function ( opt
) {
7323 var optValue
= widget
.cleanUpValue( opt
.data
);
7324 return new OO
.ui
.MenuOptionWidget( {
7326 label
: opt
.label
!== undefined ? opt
.label
: optValue
7330 // Restore the previous value, or reset to something sensible
7331 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
7332 // Previous value is still available, ensure consistency with the dropdown
7333 this.setValue( value
);
7335 // No longer valid, reset
7336 if ( options
.length
) {
7337 this.setValue( options
[ 0 ].data
);
7347 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
7348 this.dropdownWidget
.getMenu().toggle( true );
7355 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
7356 this.dropdownWidget
.getMenu().toggle( false );
7361 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
7362 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
7363 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
7364 * please see the [OOjs UI documentation on MediaWiki][1].
7366 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7369 * // An example of selected, unselected, and disabled radio inputs
7370 * var radio1 = new OO.ui.RadioInputWidget( {
7374 * var radio2 = new OO.ui.RadioInputWidget( {
7377 * var radio3 = new OO.ui.RadioInputWidget( {
7381 * // Create a fieldset layout with fields for each radio button.
7382 * var fieldset = new OO.ui.FieldsetLayout( {
7383 * label: 'Radio inputs'
7385 * fieldset.addItems( [
7386 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
7387 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
7388 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
7390 * $( 'body' ).append( fieldset.$element );
7392 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7395 * @extends OO.ui.InputWidget
7398 * @param {Object} [config] Configuration options
7399 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
7401 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
7402 // Configuration initialization
7403 config
= config
|| {};
7405 // Parent constructor
7406 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
7410 .addClass( 'oo-ui-radioInputWidget' )
7411 // Required for pretty styling in MediaWiki theme
7412 .append( $( '<span>' ) );
7413 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
7418 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
7420 /* Static Methods */
7425 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7426 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7427 state
.checked
= config
.$input
.prop( 'checked' );
7437 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
7438 return $( '<input type="radio" />' );
7444 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
7445 // RadioInputWidget doesn't track its state.
7449 * Set selection state of this radio button.
7451 * @param {boolean} state `true` for selected
7454 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
7455 // RadioInputWidget doesn't track its state.
7456 this.$input
.prop( 'checked', state
);
7461 * Check if this radio button is selected.
7463 * @return {boolean} Radio is selected
7465 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
7466 return this.$input
.prop( 'checked' );
7472 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
7473 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
7474 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
7475 this.setSelected( state
.checked
);
7480 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
7481 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7482 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7483 * more information about input widgets.
7485 * This and OO.ui.DropdownInputWidget support the same configuration options.
7488 * // Example: A RadioSelectInputWidget with three options
7489 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
7491 * { data: 'a', label: 'First' },
7492 * { data: 'b', label: 'Second'},
7493 * { data: 'c', label: 'Third' }
7496 * $( 'body' ).append( radioSelectInput.$element );
7498 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7501 * @extends OO.ui.InputWidget
7504 * @param {Object} [config] Configuration options
7505 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7507 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
7508 // Configuration initialization
7509 config
= config
|| {};
7511 // Properties (must be done before parent constructor which calls #setDisabled)
7512 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
7514 // Parent constructor
7515 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
7518 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
7521 this.setOptions( config
.options
|| [] );
7523 .addClass( 'oo-ui-radioSelectInputWidget' )
7524 .append( this.radioSelectWidget
.$element
);
7529 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
7531 /* Static Properties */
7533 OO
.ui
.RadioSelectInputWidget
.static.supportsSimpleLabel
= false;
7535 /* Static Methods */
7540 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7541 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7542 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
7552 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
7553 return $( '<input type="hidden">' );
7557 * Handles menu select events.
7560 * @param {OO.ui.RadioOptionWidget} item Selected menu item
7562 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
7563 this.setValue( item
.getData() );
7569 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
7570 value
= this.cleanUpValue( value
);
7571 this.radioSelectWidget
.selectItemByData( value
);
7572 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
7579 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
7580 this.radioSelectWidget
.setDisabled( state
);
7581 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
7586 * Set the options available for this input.
7588 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
7591 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
7593 value
= this.getValue(),
7596 // Rebuild the radioSelect menu
7597 this.radioSelectWidget
7599 .addItems( options
.map( function ( opt
) {
7600 var optValue
= widget
.cleanUpValue( opt
.data
);
7601 return new OO
.ui
.RadioOptionWidget( {
7603 label
: opt
.label
!== undefined ? opt
.label
: optValue
7607 // Restore the previous value, or reset to something sensible
7608 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
7609 // Previous value is still available, ensure consistency with the radioSelect
7610 this.setValue( value
);
7612 // No longer valid, reset
7613 if ( options
.length
) {
7614 this.setValue( options
[ 0 ].data
);
7622 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
7623 * size of the field as well as its presentation. In addition, these widgets can be configured
7624 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
7625 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
7626 * which modifies incoming values rather than validating them.
7627 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
7629 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7632 * // Example of a text input widget
7633 * var textInput = new OO.ui.TextInputWidget( {
7634 * value: 'Text input'
7636 * $( 'body' ).append( textInput.$element );
7638 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7641 * @extends OO.ui.InputWidget
7642 * @mixins OO.ui.mixin.IconElement
7643 * @mixins OO.ui.mixin.IndicatorElement
7644 * @mixins OO.ui.mixin.PendingElement
7645 * @mixins OO.ui.mixin.LabelElement
7648 * @param {Object} [config] Configuration options
7649 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
7650 * 'email' or 'url'. Ignored if `multiline` is true.
7652 * Some values of `type` result in additional behaviors:
7654 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
7655 * empties the text field
7656 * @cfg {string} [placeholder] Placeholder text
7657 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
7658 * instruct the browser to focus this widget.
7659 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
7660 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
7661 * @cfg {boolean} [multiline=false] Allow multiple lines of text
7662 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
7663 * specifies minimum number of rows to display.
7664 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
7665 * Use the #maxRows config to specify a maximum number of displayed rows.
7666 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
7667 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
7668 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
7669 * the value or placeholder text: `'before'` or `'after'`
7670 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
7671 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
7672 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
7673 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
7674 * (the value must contain only numbers); when RegExp, a regular expression that must match the
7675 * value for it to be considered valid; when Function, a function receiving the value as parameter
7676 * that must return true, or promise resolving to true, for it to be considered valid.
7678 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
7679 // Configuration initialization
7680 config
= $.extend( {
7682 labelPosition
: 'after'
7684 if ( config
.type
=== 'search' ) {
7685 if ( config
.icon
=== undefined ) {
7686 config
.icon
= 'search';
7688 // indicator: 'clear' is set dynamically later, depending on value
7690 if ( config
.required
) {
7691 if ( config
.indicator
=== undefined ) {
7692 config
.indicator
= 'required';
7696 // Parent constructor
7697 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
7699 // Mixin constructors
7700 OO
.ui
.mixin
.IconElement
.call( this, config
);
7701 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7702 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
7703 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7706 this.type
= this.getSaneType( config
);
7707 this.readOnly
= false;
7708 this.multiline
= !!config
.multiline
;
7709 this.autosize
= !!config
.autosize
;
7710 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
7711 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
7712 this.validate
= null;
7713 this.styleHeight
= null;
7714 this.scrollWidth
= null;
7716 // Clone for resizing
7717 if ( this.autosize
) {
7718 this.$clone
= this.$input
7720 .insertAfter( this.$input
)
7721 .attr( 'aria-hidden', 'true' )
7722 .addClass( 'oo-ui-element-hidden' );
7725 this.setValidation( config
.validate
);
7726 this.setLabelPosition( config
.labelPosition
);
7730 keypress
: this.onKeyPress
.bind( this ),
7731 blur
: this.onBlur
.bind( this )
7734 focus
: this.onElementAttach
.bind( this )
7736 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
7737 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
7738 this.on( 'labelChange', this.updatePosition
.bind( this ) );
7739 this.connect( this, {
7741 disable
: 'onDisable'
7746 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
7747 .append( this.$icon
, this.$indicator
);
7748 this.setReadOnly( !!config
.readOnly
);
7749 this.updateSearchIndicator();
7750 if ( config
.placeholder
) {
7751 this.$input
.attr( 'placeholder', config
.placeholder
);
7753 if ( config
.maxLength
!== undefined ) {
7754 this.$input
.attr( 'maxlength', config
.maxLength
);
7756 if ( config
.autofocus
) {
7757 this.$input
.attr( 'autofocus', 'autofocus' );
7759 if ( config
.required
) {
7760 this.$input
.attr( 'required', 'required' );
7761 this.$input
.attr( 'aria-required', 'true' );
7763 if ( config
.autocomplete
=== false ) {
7764 this.$input
.attr( 'autocomplete', 'off' );
7765 // Turning off autocompletion also disables "form caching" when the user navigates to a
7766 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
7768 beforeunload: function () {
7769 this.$input
.removeAttr( 'autocomplete' );
7771 pageshow: function () {
7772 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
7773 // whole page... it shouldn't hurt, though.
7774 this.$input
.attr( 'autocomplete', 'off' );
7778 if ( this.multiline
&& config
.rows
) {
7779 this.$input
.attr( 'rows', config
.rows
);
7781 if ( this.label
|| config
.autosize
) {
7782 this.installParentChangeDetector();
7788 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
7789 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
7790 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
7791 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
7792 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
7794 /* Static Properties */
7796 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
7801 /* Static Methods */
7806 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
7807 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
7808 if ( config
.multiline
) {
7809 state
.scrollTop
= config
.$input
.scrollTop();
7817 * An `enter` event is emitted when the user presses 'enter' inside the text box.
7819 * Not emitted if the input is multiline.
7825 * A `resize` event is emitted when autosize is set and the widget resizes
7833 * Handle icon mouse down events.
7836 * @param {jQuery.Event} e Mouse down event
7839 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
7840 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7841 this.$input
[ 0 ].focus();
7847 * Handle indicator mouse down events.
7850 * @param {jQuery.Event} e Mouse down event
7853 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
7854 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7855 if ( this.type
=== 'search' ) {
7856 // Clear the text field
7857 this.setValue( '' );
7859 this.$input
[ 0 ].focus();
7865 * Handle key press events.
7868 * @param {jQuery.Event} e Key press event
7869 * @fires enter If enter key is pressed and input is not multiline
7871 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
7872 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
7873 this.emit( 'enter', e
);
7878 * Handle blur events.
7881 * @param {jQuery.Event} e Blur event
7883 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
7884 this.setValidityFlag();
7888 * Handle element attach events.
7891 * @param {jQuery.Event} e Element attach event
7893 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
7894 // Any previously calculated size is now probably invalid if we reattached elsewhere
7895 this.valCache
= null;
7897 this.positionLabel();
7901 * Handle change events.
7903 * @param {string} value
7906 OO
.ui
.TextInputWidget
.prototype.onChange = function () {
7907 this.updateSearchIndicator();
7908 this.setValidityFlag();
7913 * Handle disable events.
7915 * @param {boolean} disabled Element is disabled
7918 OO
.ui
.TextInputWidget
.prototype.onDisable = function () {
7919 this.updateSearchIndicator();
7923 * Check if the input is {@link #readOnly read-only}.
7927 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
7928 return this.readOnly
;
7932 * Set the {@link #readOnly read-only} state of the input.
7934 * @param {boolean} state Make input read-only
7937 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
7938 this.readOnly
= !!state
;
7939 this.$input
.prop( 'readOnly', this.readOnly
);
7940 this.updateSearchIndicator();
7945 * Support function for making #onElementAttach work across browsers.
7947 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
7948 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
7950 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
7951 * first time that the element gets attached to the documented.
7953 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
7954 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
7955 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
7958 if ( MutationObserver
) {
7959 // The new way. If only it wasn't so ugly.
7961 if ( this.$element
.closest( 'html' ).length
) {
7962 // Widget is attached already, do nothing. This breaks the functionality of this function when
7963 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
7964 // would require observation of the whole document, which would hurt performance of other,
7965 // more important code.
7969 // Find topmost node in the tree
7970 topmostNode
= this.$element
[ 0 ];
7971 while ( topmostNode
.parentNode
) {
7972 topmostNode
= topmostNode
.parentNode
;
7975 // We have no way to detect the $element being attached somewhere without observing the entire
7976 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
7977 // parent node of $element, and instead detect when $element is removed from it (and thus
7978 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
7979 // doesn't get attached, we end up back here and create the parent.
7981 mutationObserver
= new MutationObserver( function ( mutations
) {
7982 var i
, j
, removedNodes
;
7983 for ( i
= 0; i
< mutations
.length
; i
++ ) {
7984 removedNodes
= mutations
[ i
].removedNodes
;
7985 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
7986 if ( removedNodes
[ j
] === topmostNode
) {
7987 setTimeout( onRemove
, 0 );
7994 onRemove = function () {
7995 // If the node was attached somewhere else, report it
7996 if ( widget
.$element
.closest( 'html' ).length
) {
7997 widget
.onElementAttach();
7999 mutationObserver
.disconnect();
8000 widget
.installParentChangeDetector();
8003 // Create a fake parent and observe it
8004 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
8005 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
8007 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
8008 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
8009 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
8014 * Automatically adjust the size of the text input.
8016 * This only affects #multiline inputs that are {@link #autosize autosized}.
8021 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
8022 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
8023 idealHeight
, newHeight
, scrollWidth
, property
;
8025 if ( this.multiline
&& this.$input
.val() !== this.valCache
) {
8026 if ( this.autosize
) {
8028 .val( this.$input
.val() )
8029 .attr( 'rows', this.minRows
)
8030 // Set inline height property to 0 to measure scroll height
8031 .css( 'height', 0 );
8033 this.$clone
.removeClass( 'oo-ui-element-hidden' );
8035 this.valCache
= this.$input
.val();
8037 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
8039 // Remove inline height property to measure natural heights
8040 this.$clone
.css( 'height', '' );
8041 innerHeight
= this.$clone
.innerHeight();
8042 outerHeight
= this.$clone
.outerHeight();
8044 // Measure max rows height
8046 .attr( 'rows', this.maxRows
)
8047 .css( 'height', 'auto' )
8049 maxInnerHeight
= this.$clone
.innerHeight();
8051 // Difference between reported innerHeight and scrollHeight with no scrollbars present
8052 // Equals 1 on Blink-based browsers and 0 everywhere else
8053 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
8054 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
8056 this.$clone
.addClass( 'oo-ui-element-hidden' );
8058 // Only apply inline height when expansion beyond natural height is needed
8059 // Use the difference between the inner and outer height as a buffer
8060 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
8061 if ( newHeight
!== this.styleHeight
) {
8062 this.$input
.css( 'height', newHeight
);
8063 this.styleHeight
= newHeight
;
8064 this.emit( 'resize' );
8067 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
8068 if ( scrollWidth
!== this.scrollWidth
) {
8069 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
8071 this.$label
.css( { right
: '', left
: '' } );
8072 this.$indicator
.css( { right
: '', left
: '' } );
8074 if ( scrollWidth
) {
8075 this.$indicator
.css( property
, scrollWidth
);
8076 if ( this.labelPosition
=== 'after' ) {
8077 this.$label
.css( property
, scrollWidth
);
8081 this.scrollWidth
= scrollWidth
;
8082 this.positionLabel();
8092 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
8093 return config
.multiline
?
8095 $( '<input type="' + this.getSaneType( config
) + '" />' );
8099 * Get sanitized value for 'type' for given config.
8101 * @param {Object} config Configuration options
8102 * @return {string|null}
8105 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
8106 var type
= [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config
.type
) !== -1 ?
8109 return config
.multiline
? 'multiline' : type
;
8113 * Check if the input supports multiple lines.
8117 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
8118 return !!this.multiline
;
8122 * Check if the input automatically adjusts its size.
8126 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
8127 return !!this.autosize
;
8131 * Focus the input and select a specified range within the text.
8133 * @param {number} from Select from offset
8134 * @param {number} [to] Select to offset, defaults to from
8137 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
8138 var isBackwards
, start
, end
,
8139 input
= this.$input
[ 0 ];
8143 isBackwards
= to
< from;
8144 start
= isBackwards
? to
: from;
8145 end
= isBackwards
? from : to
;
8149 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
8154 * Get an object describing the current selection range in a directional manner
8156 * @return {Object} Object containing 'from' and 'to' offsets
8158 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
8159 var input
= this.$input
[ 0 ],
8160 start
= input
.selectionStart
,
8161 end
= input
.selectionEnd
,
8162 isBackwards
= input
.selectionDirection
=== 'backward';
8165 from: isBackwards
? end
: start
,
8166 to
: isBackwards
? start
: end
8171 * Get the length of the text input value.
8173 * This could differ from the length of #getValue if the
8174 * value gets filtered
8176 * @return {number} Input length
8178 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
8179 return this.$input
[ 0 ].value
.length
;
8183 * Focus the input and select the entire text.
8187 OO
.ui
.TextInputWidget
.prototype.select = function () {
8188 return this.selectRange( 0, this.getInputLength() );
8192 * Focus the input and move the cursor to the start.
8196 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
8197 return this.selectRange( 0 );
8201 * Focus the input and move the cursor to the end.
8205 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
8206 return this.selectRange( this.getInputLength() );
8210 * Insert new content into the input.
8212 * @param {string} content Content to be inserted
8215 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
8217 range
= this.getRange(),
8218 value
= this.getValue();
8220 start
= Math
.min( range
.from, range
.to
);
8221 end
= Math
.max( range
.from, range
.to
);
8223 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
8224 this.selectRange( start
+ content
.length
);
8229 * Insert new content either side of a selection.
8231 * @param {string} pre Content to be inserted before the selection
8232 * @param {string} post Content to be inserted after the selection
8235 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
8237 range
= this.getRange(),
8238 offset
= pre
.length
;
8240 start
= Math
.min( range
.from, range
.to
);
8241 end
= Math
.max( range
.from, range
.to
);
8243 this.selectRange( start
).insertContent( pre
);
8244 this.selectRange( offset
+ end
).insertContent( post
);
8246 this.selectRange( offset
+ start
, offset
+ end
);
8251 * Set the validation pattern.
8253 * The validation pattern is either a regular expression, a function, or the symbolic name of a
8254 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
8255 * value must contain only numbers).
8257 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
8258 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
8260 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
8261 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
8262 this.validate
= validate
;
8264 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
8269 * Sets the 'invalid' flag appropriately.
8271 * @param {boolean} [isValid] Optionally override validation result
8273 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
8275 setFlag = function ( valid
) {
8277 widget
.$input
.attr( 'aria-invalid', 'true' );
8279 widget
.$input
.removeAttr( 'aria-invalid' );
8281 widget
.setFlags( { invalid
: !valid
} );
8284 if ( isValid
!== undefined ) {
8287 this.getValidity().then( function () {
8296 * Check if a value is valid.
8298 * This method returns a promise that resolves with a boolean `true` if the current value is
8299 * considered valid according to the supplied {@link #validate validation pattern}.
8302 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
8304 OO
.ui
.TextInputWidget
.prototype.isValid = function () {
8307 if ( this.validate
instanceof Function
) {
8308 result
= this.validate( this.getValue() );
8309 if ( result
&& $.isFunction( result
.promise
) ) {
8310 return result
.promise();
8312 return $.Deferred().resolve( !!result
).promise();
8315 return $.Deferred().resolve( !!this.getValue().match( this.validate
) ).promise();
8320 * Get the validity of current value.
8322 * This method returns a promise that resolves if the value is valid and rejects if
8323 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
8325 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
8327 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
8330 function rejectOrResolve( valid
) {
8332 return $.Deferred().resolve().promise();
8334 return $.Deferred().reject().promise();
8338 if ( this.validate
instanceof Function
) {
8339 result
= this.validate( this.getValue() );
8340 if ( result
&& $.isFunction( result
.promise
) ) {
8341 return result
.promise().then( function ( valid
) {
8342 return rejectOrResolve( valid
);
8345 return rejectOrResolve( result
);
8348 return rejectOrResolve( this.getValue().match( this.validate
) );
8353 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
8355 * @param {string} labelPosition Label position, 'before' or 'after'
8358 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
8359 this.labelPosition
= labelPosition
;
8360 this.updatePosition();
8365 * Update the position of the inline label.
8367 * This method is called by #setLabelPosition, and can also be called on its own if
8368 * something causes the label to be mispositioned.
8372 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
8373 var after
= this.labelPosition
=== 'after';
8376 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
8377 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
8379 this.valCache
= null;
8380 this.scrollWidth
= null;
8382 this.positionLabel();
8388 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
8389 * already empty or when it's not editable.
8391 OO
.ui
.TextInputWidget
.prototype.updateSearchIndicator = function () {
8392 if ( this.type
=== 'search' ) {
8393 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
8394 this.setIndicator( null );
8396 this.setIndicator( 'clear' );
8402 * Position the label by setting the correct padding on the input.
8407 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
8408 var after
, rtl
, property
;
8411 // Clear old values if present
8413 'padding-right': '',
8418 this.$element
.append( this.$label
);
8420 this.$label
.detach();
8424 after
= this.labelPosition
=== 'after';
8425 rtl
= this.$element
.css( 'direction' ) === 'rtl';
8426 property
= after
=== rtl
? 'padding-left' : 'padding-right';
8428 this.$input
.css( property
, this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 ) );
8436 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
8437 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8438 if ( state
.scrollTop
!== undefined ) {
8439 this.$input
.scrollTop( state
.scrollTop
);
8444 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
8445 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
8446 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
8448 * - by typing a value in the text input field. If the value exactly matches the value of a menu
8449 * option, that option will appear to be selected.
8450 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
8453 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8455 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
8458 * // Example: A ComboBoxInputWidget.
8459 * var comboBox = new OO.ui.ComboBoxInputWidget( {
8460 * label: 'ComboBoxInputWidget',
8461 * value: 'Option 1',
8464 * new OO.ui.MenuOptionWidget( {
8466 * label: 'Option One'
8468 * new OO.ui.MenuOptionWidget( {
8470 * label: 'Option Two'
8472 * new OO.ui.MenuOptionWidget( {
8474 * label: 'Option Three'
8476 * new OO.ui.MenuOptionWidget( {
8478 * label: 'Option Four'
8480 * new OO.ui.MenuOptionWidget( {
8482 * label: 'Option Five'
8487 * $( 'body' ).append( comboBox.$element );
8489 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
8492 * @extends OO.ui.TextInputWidget
8495 * @param {Object} [config] Configuration options
8496 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8497 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
8498 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
8499 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
8500 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
8502 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
8503 // Configuration initialization
8504 config
= $.extend( {
8507 // For backwards-compatibility with ComboBoxWidget config
8508 $.extend( config
, config
.input
);
8510 // Parent constructor
8511 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
8514 this.$overlay
= config
.$overlay
|| this.$element
;
8515 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend(
8519 $container
: this.$element
,
8520 disabled
: this.isDisabled()
8524 // For backwards-compatibility with ComboBoxWidget
8528 this.$indicator
.on( {
8529 click
: this.onIndicatorClick
.bind( this ),
8530 keypress
: this.onIndicatorKeyPress
.bind( this )
8532 this.connect( this, {
8533 change
: 'onInputChange',
8534 enter
: 'onInputEnter'
8536 this.menu
.connect( this, {
8537 choose
: 'onMenuChoose',
8538 add
: 'onMenuItemsChange',
8539 remove
: 'onMenuItemsChange'
8545 'aria-autocomplete': 'list'
8547 // Do not override options set via config.menu.items
8548 if ( config
.options
!== undefined ) {
8549 this.setOptions( config
.options
);
8551 // Extra class for backwards-compatibility with ComboBoxWidget
8552 this.$element
.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
8553 this.$overlay
.append( this.menu
.$element
);
8554 this.onMenuItemsChange();
8559 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
8564 * Get the combobox's menu.
8565 * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
8567 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
8572 * Get the combobox's text input widget.
8573 * @return {OO.ui.TextInputWidget} Text input widget
8575 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
8580 * Handle input change events.
8583 * @param {string} value New value
8585 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
8586 var match
= this.menu
.getItemFromData( value
);
8588 this.menu
.selectItem( match
);
8589 if ( this.menu
.getHighlightedItem() ) {
8590 this.menu
.highlightItem( match
);
8593 if ( !this.isDisabled() ) {
8594 this.menu
.toggle( true );
8599 * Handle mouse click events.
8602 * @param {jQuery.Event} e Mouse click event
8604 OO
.ui
.ComboBoxInputWidget
.prototype.onIndicatorClick = function ( e
) {
8605 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8607 this.$input
[ 0 ].focus();
8613 * Handle key press events.
8616 * @param {jQuery.Event} e Key press event
8618 OO
.ui
.ComboBoxInputWidget
.prototype.onIndicatorKeyPress = function ( e
) {
8619 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
8621 this.$input
[ 0 ].focus();
8627 * Handle input enter events.
8631 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
8632 if ( !this.isDisabled() ) {
8633 this.menu
.toggle( false );
8638 * Handle menu choose events.
8641 * @param {OO.ui.OptionWidget} item Chosen item
8643 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
8644 this.setValue( item
.getData() );
8648 * Handle menu item change events.
8652 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
8653 var match
= this.menu
.getItemFromData( this.getValue() );
8654 this.menu
.selectItem( match
);
8655 if ( this.menu
.getHighlightedItem() ) {
8656 this.menu
.highlightItem( match
);
8658 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
8664 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
8666 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8669 this.menu
.setDisabled( this.isDisabled() );
8676 * Set the options available for this input.
8678 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8681 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
8684 .addItems( options
.map( function ( opt
) {
8685 return new OO
.ui
.MenuOptionWidget( {
8687 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
8696 * @deprecated Use OO.ui.ComboBoxInputWidget instead.
8698 OO
.ui
.ComboBoxWidget
= OO
.ui
.ComboBoxInputWidget
;
8701 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
8702 * which is a widget that is specified by reference before any optional configuration settings.
8704 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
8706 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8707 * A left-alignment is used for forms with many fields.
8708 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8709 * A right-alignment is used for long but familiar forms which users tab through,
8710 * verifying the current field with a quick glance at the label.
8711 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8712 * that users fill out from top to bottom.
8713 * - **inline**: The label is placed after the field-widget and aligned to the left.
8714 * An inline-alignment is best used with checkboxes or radio buttons.
8716 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
8717 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
8719 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8721 * @extends OO.ui.Layout
8722 * @mixins OO.ui.mixin.LabelElement
8723 * @mixins OO.ui.mixin.TitledElement
8726 * @param {OO.ui.Widget} fieldWidget Field widget
8727 * @param {Object} [config] Configuration options
8728 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
8729 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
8730 * The array may contain strings or OO.ui.HtmlSnippet instances.
8731 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
8732 * The array may contain strings or OO.ui.HtmlSnippet instances.
8733 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
8734 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
8735 * For important messages, you are advised to use `notices`, as they are always shown.
8737 * @throws {Error} An error is thrown if no widget is specified
8739 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
8740 var hasInputWidget
, div
;
8742 // Allow passing positional parameters inside the config object
8743 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
8744 config
= fieldWidget
;
8745 fieldWidget
= config
.fieldWidget
;
8748 // Make sure we have required constructor arguments
8749 if ( fieldWidget
=== undefined ) {
8750 throw new Error( 'Widget not found' );
8753 hasInputWidget
= fieldWidget
.constructor.static.supportsSimpleLabel
;
8755 // Configuration initialization
8756 config
= $.extend( { align
: 'left' }, config
);
8758 // Parent constructor
8759 OO
.ui
.FieldLayout
.parent
.call( this, config
);
8761 // Mixin constructors
8762 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8763 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
8766 this.fieldWidget
= fieldWidget
;
8769 this.$field
= $( '<div>' );
8770 this.$messages
= $( '<ul>' );
8771 this.$body
= $( '<' + ( hasInputWidget
? 'label' : 'div' ) + '>' );
8773 if ( config
.help
) {
8774 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
8775 classes
: [ 'oo-ui-fieldLayout-help' ],
8781 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
8782 div
.html( config
.help
.toString() );
8784 div
.text( config
.help
);
8786 this.popupButtonWidget
.getPopup().$body
.append(
8787 div
.addClass( 'oo-ui-fieldLayout-help-content' )
8789 this.$help
= this.popupButtonWidget
.$element
;
8791 this.$help
= $( [] );
8795 if ( hasInputWidget
) {
8796 this.$label
.on( 'click', this.onLabelClick
.bind( this ) );
8798 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
8802 .addClass( 'oo-ui-fieldLayout' )
8803 .append( this.$help
, this.$body
);
8804 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
8805 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
8807 .addClass( 'oo-ui-fieldLayout-field' )
8808 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget
.isDisabled() )
8809 .append( this.fieldWidget
.$element
);
8811 this.setErrors( config
.errors
|| [] );
8812 this.setNotices( config
.notices
|| [] );
8813 this.setAlignment( config
.align
);
8818 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
8819 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
8820 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
8825 * Handle field disable events.
8828 * @param {boolean} value Field is disabled
8830 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
8831 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
8835 * Handle label mouse click events.
8838 * @param {jQuery.Event} e Mouse click event
8840 OO
.ui
.FieldLayout
.prototype.onLabelClick = function () {
8841 this.fieldWidget
.simulateLabelClick();
8846 * Get the widget contained by the field.
8848 * @return {OO.ui.Widget} Field widget
8850 OO
.ui
.FieldLayout
.prototype.getField = function () {
8851 return this.fieldWidget
;
8856 * @param {string} kind 'error' or 'notice'
8857 * @param {string|OO.ui.HtmlSnippet} text
8860 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
8861 var $listItem
, $icon
, message
;
8862 $listItem
= $( '<li>' );
8863 if ( kind
=== 'error' ) {
8864 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
8865 } else if ( kind
=== 'notice' ) {
8866 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
8870 message
= new OO
.ui
.LabelWidget( { label
: text
} );
8872 .append( $icon
, message
.$element
)
8873 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
8878 * Set the field alignment mode.
8881 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
8884 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
8885 if ( value
!== this.align
) {
8886 // Default to 'left'
8887 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
8891 if ( value
=== 'inline' ) {
8892 this.$body
.append( this.$field
, this.$label
);
8894 this.$body
.append( this.$label
, this.$field
);
8896 // Set classes. The following classes can be used here:
8897 // * oo-ui-fieldLayout-align-left
8898 // * oo-ui-fieldLayout-align-right
8899 // * oo-ui-fieldLayout-align-top
8900 // * oo-ui-fieldLayout-align-inline
8902 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
8904 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
8912 * Set the list of error messages.
8914 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
8915 * The array may contain strings or OO.ui.HtmlSnippet instances.
8918 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
8919 this.errors
= errors
.slice();
8920 this.updateMessages();
8925 * Set the list of notice messages.
8927 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
8928 * The array may contain strings or OO.ui.HtmlSnippet instances.
8931 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
8932 this.notices
= notices
.slice();
8933 this.updateMessages();
8938 * Update the rendering of error and notice messages.
8942 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
8944 this.$messages
.empty();
8946 if ( this.errors
.length
|| this.notices
.length
) {
8947 this.$body
.after( this.$messages
);
8949 this.$messages
.remove();
8953 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
8954 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
8956 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
8957 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
8962 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
8963 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
8964 * is required and is specified before any optional configuration settings.
8966 * Labels can be aligned in one of four ways:
8968 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8969 * A left-alignment is used for forms with many fields.
8970 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8971 * A right-alignment is used for long but familiar forms which users tab through,
8972 * verifying the current field with a quick glance at the label.
8973 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8974 * that users fill out from top to bottom.
8975 * - **inline**: The label is placed after the field-widget and aligned to the left.
8976 * An inline-alignment is best used with checkboxes or radio buttons.
8978 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
8979 * text is specified.
8982 * // Example of an ActionFieldLayout
8983 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
8984 * new OO.ui.TextInputWidget( {
8985 * placeholder: 'Field widget'
8987 * new OO.ui.ButtonWidget( {
8991 * label: 'An ActionFieldLayout. This label is aligned top',
8993 * help: 'This is help text'
8997 * $( 'body' ).append( actionFieldLayout.$element );
9000 * @extends OO.ui.FieldLayout
9003 * @param {OO.ui.Widget} fieldWidget Field widget
9004 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
9006 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
9007 // Allow passing positional parameters inside the config object
9008 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
9009 config
= fieldWidget
;
9010 fieldWidget
= config
.fieldWidget
;
9011 buttonWidget
= config
.buttonWidget
;
9014 // Parent constructor
9015 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
9018 this.buttonWidget
= buttonWidget
;
9019 this.$button
= $( '<div>' );
9020 this.$input
= $( '<div>' );
9024 .addClass( 'oo-ui-actionFieldLayout' );
9026 .addClass( 'oo-ui-actionFieldLayout-button' )
9027 .append( this.buttonWidget
.$element
);
9029 .addClass( 'oo-ui-actionFieldLayout-input' )
9030 .append( this.fieldWidget
.$element
);
9032 .append( this.$input
, this.$button
);
9037 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
9040 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
9041 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
9042 * configured with a label as well. For more information and examples,
9043 * please see the [OOjs UI documentation on MediaWiki][1].
9046 * // Example of a fieldset layout
9047 * var input1 = new OO.ui.TextInputWidget( {
9048 * placeholder: 'A text input field'
9051 * var input2 = new OO.ui.TextInputWidget( {
9052 * placeholder: 'A text input field'
9055 * var fieldset = new OO.ui.FieldsetLayout( {
9056 * label: 'Example of a fieldset layout'
9059 * fieldset.addItems( [
9060 * new OO.ui.FieldLayout( input1, {
9061 * label: 'Field One'
9063 * new OO.ui.FieldLayout( input2, {
9064 * label: 'Field Two'
9067 * $( 'body' ).append( fieldset.$element );
9069 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9072 * @extends OO.ui.Layout
9073 * @mixins OO.ui.mixin.IconElement
9074 * @mixins OO.ui.mixin.LabelElement
9075 * @mixins OO.ui.mixin.GroupElement
9078 * @param {Object} [config] Configuration options
9079 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
9081 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
9082 // Configuration initialization
9083 config
= config
|| {};
9085 // Parent constructor
9086 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
9088 // Mixin constructors
9089 OO
.ui
.mixin
.IconElement
.call( this, config
);
9090 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9091 OO
.ui
.mixin
.GroupElement
.call( this, config
);
9093 if ( config
.help
) {
9094 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
9095 classes
: [ 'oo-ui-fieldsetLayout-help' ],
9100 this.popupButtonWidget
.getPopup().$body
.append(
9102 .text( config
.help
)
9103 .addClass( 'oo-ui-fieldsetLayout-help-content' )
9105 this.$help
= this.popupButtonWidget
.$element
;
9107 this.$help
= $( [] );
9112 .addClass( 'oo-ui-fieldsetLayout' )
9113 .prepend( this.$help
, this.$icon
, this.$label
, this.$group
);
9114 if ( Array
.isArray( config
.items
) ) {
9115 this.addItems( config
.items
);
9121 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
9122 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
9123 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
9124 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
9127 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
9128 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
9129 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
9130 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9132 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
9133 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
9134 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
9135 * some fancier controls. Some controls have both regular and InputWidget variants, for example
9136 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
9137 * often have simplified APIs to match the capabilities of HTML forms.
9138 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
9140 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
9141 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9144 * // Example of a form layout that wraps a fieldset layout
9145 * var input1 = new OO.ui.TextInputWidget( {
9146 * placeholder: 'Username'
9148 * var input2 = new OO.ui.TextInputWidget( {
9149 * placeholder: 'Password',
9152 * var submit = new OO.ui.ButtonInputWidget( {
9156 * var fieldset = new OO.ui.FieldsetLayout( {
9157 * label: 'A form layout'
9159 * fieldset.addItems( [
9160 * new OO.ui.FieldLayout( input1, {
9161 * label: 'Username',
9164 * new OO.ui.FieldLayout( input2, {
9165 * label: 'Password',
9168 * new OO.ui.FieldLayout( submit )
9170 * var form = new OO.ui.FormLayout( {
9171 * items: [ fieldset ],
9172 * action: '/api/formhandler',
9175 * $( 'body' ).append( form.$element );
9178 * @extends OO.ui.Layout
9179 * @mixins OO.ui.mixin.GroupElement
9182 * @param {Object} [config] Configuration options
9183 * @cfg {string} [method] HTML form `method` attribute
9184 * @cfg {string} [action] HTML form `action` attribute
9185 * @cfg {string} [enctype] HTML form `enctype` attribute
9186 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
9188 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
9191 // Configuration initialization
9192 config
= config
|| {};
9194 // Parent constructor
9195 OO
.ui
.FormLayout
.parent
.call( this, config
);
9197 // Mixin constructors
9198 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
9201 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
9203 // Make sure the action is safe
9204 action
= config
.action
;
9205 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
9206 action
= './' + action
;
9211 .addClass( 'oo-ui-formLayout' )
9213 method
: config
.method
,
9215 enctype
: config
.enctype
9217 if ( Array
.isArray( config
.items
) ) {
9218 this.addItems( config
.items
);
9224 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
9225 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
9230 * A 'submit' event is emitted when the form is submitted.
9235 /* Static Properties */
9237 OO
.ui
.FormLayout
.static.tagName
= 'form';
9242 * Handle form submit events.
9245 * @param {jQuery.Event} e Submit event
9248 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
9249 if ( this.emit( 'submit' ) ) {
9255 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
9256 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
9259 * // Example of a panel layout
9260 * var panel = new OO.ui.PanelLayout( {
9264 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
9266 * $( 'body' ).append( panel.$element );
9269 * @extends OO.ui.Layout
9272 * @param {Object} [config] Configuration options
9273 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
9274 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
9275 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
9276 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
9278 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
9279 // Configuration initialization
9280 config
= $.extend( {
9287 // Parent constructor
9288 OO
.ui
.PanelLayout
.parent
.call( this, config
);
9291 this.$element
.addClass( 'oo-ui-panelLayout' );
9292 if ( config
.scrollable
) {
9293 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
9295 if ( config
.padded
) {
9296 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
9298 if ( config
.expanded
) {
9299 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
9301 if ( config
.framed
) {
9302 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
9308 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
9313 * Focus the panel layout
9315 * The default implementation just focuses the first focusable element in the panel
9317 OO
.ui
.PanelLayout
.prototype.focus = function () {
9318 OO
.ui
.findFocusable( this.$element
).focus();
9322 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
9323 * items), with small margins between them. Convenient when you need to put a number of block-level
9324 * widgets on a single line next to each other.
9326 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
9329 * // HorizontalLayout with a text input and a label
9330 * var layout = new OO.ui.HorizontalLayout( {
9332 * new OO.ui.LabelWidget( { label: 'Label' } ),
9333 * new OO.ui.TextInputWidget( { value: 'Text' } )
9336 * $( 'body' ).append( layout.$element );
9339 * @extends OO.ui.Layout
9340 * @mixins OO.ui.mixin.GroupElement
9343 * @param {Object} [config] Configuration options
9344 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
9346 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
9347 // Configuration initialization
9348 config
= config
|| {};
9350 // Parent constructor
9351 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
9353 // Mixin constructors
9354 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
9357 this.$element
.addClass( 'oo-ui-horizontalLayout' );
9358 if ( Array
.isArray( config
.items
) ) {
9359 this.addItems( config
.items
);
9365 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
9366 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
9372 * https://www.mediawiki.org/wiki/OOjs_UI
9374 * Copyright 2011–2016 OOjs UI Team and other contributors.
9375 * Released under the MIT license
9376 * http://oojs.mit-license.org
9378 * Date: 2016-02-02T22:07:00Z
9385 * DraggableElement is a mixin class used to create elements that can be clicked
9386 * and dragged by a mouse to a new position within a group. This class must be used
9387 * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
9388 * the draggable elements.
9395 OO
.ui
.mixin
.DraggableElement
= function OoUiMixinDraggableElement() {
9399 // Initialize and events
9401 .attr( 'draggable', true )
9402 .addClass( 'oo-ui-draggableElement' )
9404 dragstart
: this.onDragStart
.bind( this ),
9405 dragover
: this.onDragOver
.bind( this ),
9406 dragend
: this.onDragEnd
.bind( this ),
9407 drop
: this.onDrop
.bind( this )
9411 OO
.initClass( OO
.ui
.mixin
.DraggableElement
);
9418 * A dragstart event is emitted when the user clicks and begins dragging an item.
9419 * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
9424 * A dragend event is emitted when the user drags an item and releases the mouse,
9425 * thus terminating the drag operation.
9430 * A drop event is emitted when the user drags an item and then releases the mouse button
9431 * over a valid target.
9434 /* Static Properties */
9437 * @inheritdoc OO.ui.mixin.ButtonElement
9439 OO
.ui
.mixin
.DraggableElement
.static.cancelButtonMouseDownEvents
= false;
9444 * Respond to dragstart event.
9447 * @param {jQuery.Event} event jQuery event
9450 OO
.ui
.mixin
.DraggableElement
.prototype.onDragStart = function ( e
) {
9451 var dataTransfer
= e
.originalEvent
.dataTransfer
;
9452 // Define drop effect
9453 dataTransfer
.dropEffect
= 'none';
9454 dataTransfer
.effectAllowed
= 'move';
9456 // We must set up a dataTransfer data property or Firefox seems to
9457 // ignore the fact the element is draggable.
9459 dataTransfer
.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
9461 // The above is only for Firefox. Move on if it fails.
9463 // Add dragging class
9464 this.$element
.addClass( 'oo-ui-draggableElement-dragging' );
9466 this.emit( 'dragstart', this );
9471 * Respond to dragend event.
9476 OO
.ui
.mixin
.DraggableElement
.prototype.onDragEnd = function () {
9477 this.$element
.removeClass( 'oo-ui-draggableElement-dragging' );
9478 this.emit( 'dragend' );
9482 * Handle drop event.
9485 * @param {jQuery.Event} event jQuery event
9488 OO
.ui
.mixin
.DraggableElement
.prototype.onDrop = function ( e
) {
9490 this.emit( 'drop', e
);
9494 * In order for drag/drop to work, the dragover event must
9495 * return false and stop propogation.
9499 OO
.ui
.mixin
.DraggableElement
.prototype.onDragOver = function ( e
) {
9505 * Store it in the DOM so we can access from the widget drag event
9508 * @param {number} Item index
9510 OO
.ui
.mixin
.DraggableElement
.prototype.setIndex = function ( index
) {
9511 if ( this.index
!== index
) {
9513 this.$element
.data( 'index', index
);
9521 * @return {number} Item index
9523 OO
.ui
.mixin
.DraggableElement
.prototype.getIndex = function () {
9528 * DraggableGroupElement is a mixin class used to create a group element to
9529 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
9530 * The class is used with OO.ui.mixin.DraggableElement.
9534 * @mixins OO.ui.mixin.GroupElement
9537 * @param {Object} [config] Configuration options
9538 * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
9539 * should match the layout of the items. Items displayed in a single row
9540 * or in several rows should use horizontal orientation. The vertical orientation should only be
9541 * used when the items are displayed in a single column. Defaults to 'vertical'
9543 OO
.ui
.mixin
.DraggableGroupElement
= function OoUiMixinDraggableGroupElement( config
) {
9544 // Configuration initialization
9545 config
= config
|| {};
9547 // Parent constructor
9548 OO
.ui
.mixin
.GroupElement
.call( this, config
);
9551 this.orientation
= config
.orientation
|| 'vertical';
9552 this.dragItem
= null;
9553 this.itemDragOver
= null;
9555 this.sideInsertion
= '';
9559 dragstart
: 'itemDragStart',
9560 dragend
: 'itemDragEnd',
9563 this.connect( this, {
9564 itemDragStart
: 'onItemDragStart',
9565 itemDrop
: 'onItemDrop',
9566 itemDragEnd
: 'onItemDragEnd'
9569 dragover
: this.onDragOver
.bind( this ),
9570 dragleave
: this.onDragLeave
.bind( this )
9574 if ( Array
.isArray( config
.items
) ) {
9575 this.addItems( config
.items
);
9577 this.$placeholder
= $( '<div>' )
9578 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
9580 .addClass( 'oo-ui-draggableGroupElement' )
9581 .append( this.$status
)
9582 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation
=== 'horizontal' )
9583 .prepend( this.$placeholder
);
9587 OO
.mixinClass( OO
.ui
.mixin
.DraggableGroupElement
, OO
.ui
.mixin
.GroupElement
);
9592 * A 'reorder' event is emitted when the order of items in the group changes.
9595 * @param {OO.ui.mixin.DraggableElement} item Reordered item
9596 * @param {number} [newIndex] New index for the item
9602 * Respond to item drag start event
9605 * @param {OO.ui.mixin.DraggableElement} item Dragged item
9607 OO
.ui
.mixin
.DraggableGroupElement
.prototype.onItemDragStart = function ( item
) {
9610 // Map the index of each object
9611 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
9612 this.items
[ i
].setIndex( i
);
9615 if ( this.orientation
=== 'horizontal' ) {
9616 // Set the height of the indicator
9617 this.$placeholder
.css( {
9618 height
: item
.$element
.outerHeight(),
9622 // Set the width of the indicator
9623 this.$placeholder
.css( {
9625 width
: item
.$element
.outerWidth()
9628 this.setDragItem( item
);
9632 * Respond to item drag end event
9636 OO
.ui
.mixin
.DraggableGroupElement
.prototype.onItemDragEnd = function () {
9637 this.unsetDragItem();
9642 * Handle drop event and switch the order of the items accordingly
9645 * @param {OO.ui.mixin.DraggableElement} item Dropped item
9648 OO
.ui
.mixin
.DraggableGroupElement
.prototype.onItemDrop = function ( item
) {
9649 var toIndex
= item
.getIndex();
9650 // Check if the dropped item is from the current group
9651 // TODO: Figure out a way to configure a list of legally droppable
9652 // elements even if they are not yet in the list
9653 if ( this.getDragItem() ) {
9654 // If the insertion point is 'after', the insertion index
9655 // is shifted to the right (or to the left in RTL, hence 'after')
9656 if ( this.sideInsertion
=== 'after' ) {
9659 // Emit change event
9660 this.emit( 'reorder', this.getDragItem(), toIndex
);
9662 this.unsetDragItem();
9663 // Return false to prevent propogation
9668 * Handle dragleave event.
9672 OO
.ui
.mixin
.DraggableGroupElement
.prototype.onDragLeave = function () {
9673 // This means the item was dragged outside the widget
9676 .addClass( 'oo-ui-element-hidden' );
9680 * Respond to dragover event
9683 * @param {jQuery.Event} event Event details
9685 OO
.ui
.mixin
.DraggableGroupElement
.prototype.onDragOver = function ( e
) {
9686 var dragOverObj
, $optionWidget
, itemOffset
, itemMidpoint
, itemBoundingRect
,
9687 itemSize
, cssOutput
, dragPosition
, itemIndex
, itemPosition
,
9688 clientX
= e
.originalEvent
.clientX
,
9689 clientY
= e
.originalEvent
.clientY
;
9691 // Get the OptionWidget item we are dragging over
9692 dragOverObj
= this.getElementDocument().elementFromPoint( clientX
, clientY
);
9693 $optionWidget
= $( dragOverObj
).closest( '.oo-ui-draggableElement' );
9694 if ( $optionWidget
[ 0 ] ) {
9695 itemOffset
= $optionWidget
.offset();
9696 itemBoundingRect
= $optionWidget
[ 0 ].getBoundingClientRect();
9697 itemPosition
= $optionWidget
.position();
9698 itemIndex
= $optionWidget
.data( 'index' );
9703 this.isDragging() &&
9704 itemIndex
!== this.getDragItem().getIndex()
9706 if ( this.orientation
=== 'horizontal' ) {
9707 // Calculate where the mouse is relative to the item width
9708 itemSize
= itemBoundingRect
.width
;
9709 itemMidpoint
= itemBoundingRect
.left
+ itemSize
/ 2;
9710 dragPosition
= clientX
;
9711 // Which side of the item we hover over will dictate
9712 // where the placeholder will appear, on the left or
9715 left
: dragPosition
< itemMidpoint
? itemPosition
.left
: itemPosition
.left
+ itemSize
,
9716 top
: itemPosition
.top
9719 // Calculate where the mouse is relative to the item height
9720 itemSize
= itemBoundingRect
.height
;
9721 itemMidpoint
= itemBoundingRect
.top
+ itemSize
/ 2;
9722 dragPosition
= clientY
;
9723 // Which side of the item we hover over will dictate
9724 // where the placeholder will appear, on the top or
9727 top
: dragPosition
< itemMidpoint
? itemPosition
.top
: itemPosition
.top
+ itemSize
,
9728 left
: itemPosition
.left
9731 // Store whether we are before or after an item to rearrange
9732 // For horizontal layout, we need to account for RTL, as this is flipped
9733 if ( this.orientation
=== 'horizontal' && this.$element
.css( 'direction' ) === 'rtl' ) {
9734 this.sideInsertion
= dragPosition
< itemMidpoint
? 'after' : 'before';
9736 this.sideInsertion
= dragPosition
< itemMidpoint
? 'before' : 'after';
9738 // Add drop indicator between objects
9741 .removeClass( 'oo-ui-element-hidden' );
9743 // This means the item was dragged outside the widget
9746 .addClass( 'oo-ui-element-hidden' );
9753 * Set a dragged item
9755 * @param {OO.ui.mixin.DraggableElement} item Dragged item
9757 OO
.ui
.mixin
.DraggableGroupElement
.prototype.setDragItem = function ( item
) {
9758 this.dragItem
= item
;
9762 * Unset the current dragged item
9764 OO
.ui
.mixin
.DraggableGroupElement
.prototype.unsetDragItem = function () {
9765 this.dragItem
= null;
9766 this.itemDragOver
= null;
9767 this.$placeholder
.addClass( 'oo-ui-element-hidden' );
9768 this.sideInsertion
= '';
9772 * Get the item that is currently being dragged.
9774 * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
9776 OO
.ui
.mixin
.DraggableGroupElement
.prototype.getDragItem = function () {
9777 return this.dragItem
;
9781 * Check if an item in the group is currently being dragged.
9783 * @return {Boolean} Item is being dragged
9785 OO
.ui
.mixin
.DraggableGroupElement
.prototype.isDragging = function () {
9786 return this.getDragItem() !== null;
9790 * RequestManager is a mixin that manages the lifecycle of a promise-backed request for a widget, such as
9791 * the {@link OO.ui.mixin.LookupElement}.
9798 OO
.ui
.mixin
.RequestManager
= function OoUiMixinRequestManager() {
9799 this.requestCache
= {};
9800 this.requestQuery
= null;
9801 this.requestRequest
= null;
9806 OO
.initClass( OO
.ui
.mixin
.RequestManager
);
9809 * Get request results for the current query.
9811 * @return {jQuery.Promise} Promise object which will be passed response data as the first argument of
9812 * the done event. If the request was aborted to make way for a subsequent request, this promise
9813 * may not be rejected, depending on what jQuery feels like doing.
9815 OO
.ui
.mixin
.RequestManager
.prototype.getRequestData = function () {
9817 value
= this.getRequestQuery(),
9818 deferred
= $.Deferred(),
9821 this.abortRequest();
9822 if ( Object
.prototype.hasOwnProperty
.call( this.requestCache
, value
) ) {
9823 deferred
.resolve( this.requestCache
[ value
] );
9825 if ( this.pushPending
) {
9828 this.requestQuery
= value
;
9829 ourRequest
= this.requestRequest
= this.getRequest();
9831 .always( function () {
9832 // We need to pop pending even if this is an old request, otherwise
9833 // the widget will remain pending forever.
9834 // TODO: this assumes that an aborted request will fail or succeed soon after
9835 // being aborted, or at least eventually. It would be nice if we could popPending()
9836 // at abort time, but only if we knew that we hadn't already called popPending()
9837 // for that request.
9838 if ( widget
.popPending
) {
9839 widget
.popPending();
9842 .done( function ( response
) {
9843 // If this is an old request (and aborting it somehow caused it to still succeed),
9844 // ignore its success completely
9845 if ( ourRequest
=== widget
.requestRequest
) {
9846 widget
.requestQuery
= null;
9847 widget
.requestRequest
= null;
9848 widget
.requestCache
[ value
] = widget
.getRequestCacheDataFromResponse( response
);
9849 deferred
.resolve( widget
.requestCache
[ value
] );
9852 .fail( function () {
9853 // If this is an old request (or a request failing because it's being aborted),
9854 // ignore its failure completely
9855 if ( ourRequest
=== widget
.requestRequest
) {
9856 widget
.requestQuery
= null;
9857 widget
.requestRequest
= null;
9862 return deferred
.promise();
9866 * Abort the currently pending request, if any.
9870 OO
.ui
.mixin
.RequestManager
.prototype.abortRequest = function () {
9871 var oldRequest
= this.requestRequest
;
9873 // First unset this.requestRequest to the fail handler will notice
9874 // that the request is no longer current
9875 this.requestRequest
= null;
9876 this.requestQuery
= null;
9882 * Get the query to be made.
9887 * @return {string} query to be used
9889 OO
.ui
.mixin
.RequestManager
.prototype.getRequestQuery
= null;
9892 * Get a new request object of the current query value.
9897 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
9899 OO
.ui
.mixin
.RequestManager
.prototype.getRequest
= null;
9902 * Pre-process data returned by the request from #getRequest.
9904 * The return value of this function will be cached, and any further queries for the given value
9905 * will use the cache rather than doing API requests.
9910 * @param {Mixed} response Response from server
9911 * @return {Mixed} Cached result data
9913 OO
.ui
.mixin
.RequestManager
.prototype.getRequestCacheDataFromResponse
= null;
9916 * LookupElement is a mixin that creates a {@link OO.ui.FloatingMenuSelectWidget menu} of suggested values for
9917 * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
9918 * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
9919 * from the lookup menu, that value becomes the value of the input field.
9921 * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
9922 * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
9923 * re-enable lookups.
9925 * See the [OOjs UI demos][1] for an example.
9927 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
9933 * @param {Object} [config] Configuration options
9934 * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
9935 * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
9936 * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
9937 * By default, the lookup menu is not generated and displayed until the user begins to type.
9938 * @cfg {boolean} [highlightFirst=true] Whether the first lookup result should be highlighted (so, that the user can
9939 * take it over into the input with simply pressing return) automatically or not.
9941 OO
.ui
.mixin
.LookupElement
= function OoUiMixinLookupElement( config
) {
9942 // Configuration initialization
9943 config
= $.extend( { highlightFirst
: true }, config
);
9945 // Mixin constructors
9946 OO
.ui
.mixin
.RequestManager
.call( this, config
);
9949 this.$overlay
= config
.$overlay
|| this.$element
;
9950 this.lookupMenu
= new OO
.ui
.FloatingMenuSelectWidget( {
9953 $container
: config
.$container
|| this.$element
9956 this.allowSuggestionsWhenEmpty
= config
.allowSuggestionsWhenEmpty
|| false;
9958 this.lookupsDisabled
= false;
9959 this.lookupInputFocused
= false;
9960 this.lookupHighlightFirstItem
= config
.highlightFirst
;
9964 focus
: this.onLookupInputFocus
.bind( this ),
9965 blur
: this.onLookupInputBlur
.bind( this ),
9966 mousedown
: this.onLookupInputMouseDown
.bind( this )
9968 this.connect( this, { change
: 'onLookupInputChange' } );
9969 this.lookupMenu
.connect( this, {
9970 toggle
: 'onLookupMenuToggle',
9971 choose
: 'onLookupMenuItemChoose'
9975 this.$element
.addClass( 'oo-ui-lookupElement' );
9976 this.lookupMenu
.$element
.addClass( 'oo-ui-lookupElement-menu' );
9977 this.$overlay
.append( this.lookupMenu
.$element
);
9982 OO
.mixinClass( OO
.ui
.mixin
.LookupElement
, OO
.ui
.mixin
.RequestManager
);
9987 * Handle input focus event.
9990 * @param {jQuery.Event} e Input focus event
9992 OO
.ui
.mixin
.LookupElement
.prototype.onLookupInputFocus = function () {
9993 this.lookupInputFocused
= true;
9994 this.populateLookupMenu();
9998 * Handle input blur event.
10001 * @param {jQuery.Event} e Input blur event
10003 OO
.ui
.mixin
.LookupElement
.prototype.onLookupInputBlur = function () {
10004 this.closeLookupMenu();
10005 this.lookupInputFocused
= false;
10009 * Handle input mouse down event.
10012 * @param {jQuery.Event} e Input mouse down event
10014 OO
.ui
.mixin
.LookupElement
.prototype.onLookupInputMouseDown = function () {
10015 // Only open the menu if the input was already focused.
10016 // This way we allow the user to open the menu again after closing it with Esc
10017 // by clicking in the input. Opening (and populating) the menu when initially
10018 // clicking into the input is handled by the focus handler.
10019 if ( this.lookupInputFocused
&& !this.lookupMenu
.isVisible() ) {
10020 this.populateLookupMenu();
10025 * Handle input change event.
10028 * @param {string} value New input value
10030 OO
.ui
.mixin
.LookupElement
.prototype.onLookupInputChange = function () {
10031 if ( this.lookupInputFocused
) {
10032 this.populateLookupMenu();
10037 * Handle the lookup menu being shown/hidden.
10040 * @param {boolean} visible Whether the lookup menu is now visible.
10042 OO
.ui
.mixin
.LookupElement
.prototype.onLookupMenuToggle = function ( visible
) {
10044 // When the menu is hidden, abort any active request and clear the menu.
10045 // This has to be done here in addition to closeLookupMenu(), because
10046 // MenuSelectWidget will close itself when the user presses Esc.
10047 this.abortLookupRequest();
10048 this.lookupMenu
.clearItems();
10053 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
10056 * @param {OO.ui.MenuOptionWidget} item Selected item
10058 OO
.ui
.mixin
.LookupElement
.prototype.onLookupMenuItemChoose = function ( item
) {
10059 this.setValue( item
.getData() );
10066 * @return {OO.ui.FloatingMenuSelectWidget}
10068 OO
.ui
.mixin
.LookupElement
.prototype.getLookupMenu = function () {
10069 return this.lookupMenu
;
10073 * Disable or re-enable lookups.
10075 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
10077 * @param {boolean} disabled Disable lookups
10079 OO
.ui
.mixin
.LookupElement
.prototype.setLookupsDisabled = function ( disabled
) {
10080 this.lookupsDisabled
= !!disabled
;
10084 * Open the menu. If there are no entries in the menu, this does nothing.
10089 OO
.ui
.mixin
.LookupElement
.prototype.openLookupMenu = function () {
10090 if ( !this.lookupMenu
.isEmpty() ) {
10091 this.lookupMenu
.toggle( true );
10097 * Close the menu, empty it, and abort any pending request.
10102 OO
.ui
.mixin
.LookupElement
.prototype.closeLookupMenu = function () {
10103 this.lookupMenu
.toggle( false );
10104 this.abortLookupRequest();
10105 this.lookupMenu
.clearItems();
10110 * Request menu items based on the input's current value, and when they arrive,
10111 * populate the menu with these items and show the menu.
10113 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
10118 OO
.ui
.mixin
.LookupElement
.prototype.populateLookupMenu = function () {
10120 value
= this.getValue();
10122 if ( this.lookupsDisabled
|| this.isReadOnly() ) {
10126 // If the input is empty, clear the menu, unless suggestions when empty are allowed.
10127 if ( !this.allowSuggestionsWhenEmpty
&& value
=== '' ) {
10128 this.closeLookupMenu();
10129 // Skip population if there is already a request pending for the current value
10130 } else if ( value
!== this.lookupQuery
) {
10131 this.getLookupMenuItems()
10132 .done( function ( items
) {
10133 widget
.lookupMenu
.clearItems();
10134 if ( items
.length
) {
10138 widget
.initializeLookupMenuSelection();
10140 widget
.lookupMenu
.toggle( false );
10143 .fail( function () {
10144 widget
.lookupMenu
.clearItems();
10152 * Highlight the first selectable item in the menu, if configured.
10157 OO
.ui
.mixin
.LookupElement
.prototype.initializeLookupMenuSelection = function () {
10158 if ( this.lookupHighlightFirstItem
&& !this.lookupMenu
.getSelectedItem() ) {
10159 this.lookupMenu
.highlightItem( this.lookupMenu
.getFirstSelectableItem() );
10164 * Get lookup menu items for the current query.
10167 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
10168 * the done event. If the request was aborted to make way for a subsequent request, this promise
10169 * will not be rejected: it will remain pending forever.
10171 OO
.ui
.mixin
.LookupElement
.prototype.getLookupMenuItems = function () {
10172 return this.getRequestData().then( function ( data
) {
10173 return this.getLookupMenuOptionsFromData( data
);
10178 * Abort the currently pending lookup request, if any.
10182 OO
.ui
.mixin
.LookupElement
.prototype.abortLookupRequest = function () {
10183 this.abortRequest();
10187 * Get a new request object of the current lookup query value.
10192 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
10194 OO
.ui
.mixin
.LookupElement
.prototype.getLookupRequest
= null;
10197 * Pre-process data returned by the request from #getLookupRequest.
10199 * The return value of this function will be cached, and any further queries for the given value
10200 * will use the cache rather than doing API requests.
10205 * @param {Mixed} response Response from server
10206 * @return {Mixed} Cached result data
10208 OO
.ui
.mixin
.LookupElement
.prototype.getLookupCacheDataFromResponse
= null;
10211 * Get a list of menu option widgets from the (possibly cached) data returned by
10212 * #getLookupCacheDataFromResponse.
10217 * @param {Mixed} data Cached result data, usually an array
10218 * @return {OO.ui.MenuOptionWidget[]} Menu items
10220 OO
.ui
.mixin
.LookupElement
.prototype.getLookupMenuOptionsFromData
= null;
10223 * Set the read-only state of the widget.
10225 * This will also disable/enable the lookups functionality.
10227 * @param {boolean} readOnly Make input read-only
10230 OO
.ui
.mixin
.LookupElement
.prototype.setReadOnly = function ( readOnly
) {
10232 // Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
10233 OO
.ui
.TextInputWidget
.prototype.setReadOnly
.call( this, readOnly
);
10235 // During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor
10236 if ( this.isReadOnly() && this.lookupMenu
) {
10237 this.closeLookupMenu();
10244 * @inheritdoc OO.ui.mixin.RequestManager
10246 OO
.ui
.mixin
.LookupElement
.prototype.getRequestQuery = function () {
10247 return this.getValue();
10251 * @inheritdoc OO.ui.mixin.RequestManager
10253 OO
.ui
.mixin
.LookupElement
.prototype.getRequest = function () {
10254 return this.getLookupRequest();
10258 * @inheritdoc OO.ui.mixin.RequestManager
10260 OO
.ui
.mixin
.LookupElement
.prototype.getRequestCacheDataFromResponse = function ( response
) {
10261 return this.getLookupCacheDataFromResponse( response
);
10265 * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
10266 * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
10267 * rather extended to include the required content and functionality.
10269 * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
10270 * item is customized (with a label) using the #setupTabItem method. See
10271 * {@link OO.ui.IndexLayout IndexLayout} for an example.
10274 * @extends OO.ui.PanelLayout
10277 * @param {string} name Unique symbolic name of card
10278 * @param {Object} [config] Configuration options
10279 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for card's tab
10281 OO
.ui
.CardLayout
= function OoUiCardLayout( name
, config
) {
10282 // Allow passing positional parameters inside the config object
10283 if ( OO
.isPlainObject( name
) && config
=== undefined ) {
10285 name
= config
.name
;
10288 // Configuration initialization
10289 config
= $.extend( { scrollable
: true }, config
);
10291 // Parent constructor
10292 OO
.ui
.CardLayout
.parent
.call( this, config
);
10296 this.label
= config
.label
;
10297 this.tabItem
= null;
10298 this.active
= false;
10301 this.$element
.addClass( 'oo-ui-cardLayout' );
10306 OO
.inheritClass( OO
.ui
.CardLayout
, OO
.ui
.PanelLayout
);
10311 * An 'active' event is emitted when the card becomes active. Cards become active when they are
10312 * shown in a index layout that is configured to display only one card at a time.
10315 * @param {boolean} active Card is active
10321 * Get the symbolic name of the card.
10323 * @return {string} Symbolic name of card
10325 OO
.ui
.CardLayout
.prototype.getName = function () {
10330 * Check if card is active.
10332 * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
10333 * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
10335 * @return {boolean} Card is active
10337 OO
.ui
.CardLayout
.prototype.isActive = function () {
10338 return this.active
;
10344 * The tab item allows users to access the card from the index's tab
10345 * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
10347 * @return {OO.ui.TabOptionWidget|null} Tab option widget
10349 OO
.ui
.CardLayout
.prototype.getTabItem = function () {
10350 return this.tabItem
;
10354 * Set or unset the tab item.
10356 * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
10357 * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
10358 * level), use #setupTabItem instead of this method.
10360 * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
10363 OO
.ui
.CardLayout
.prototype.setTabItem = function ( tabItem
) {
10364 this.tabItem
= tabItem
|| null;
10366 this.setupTabItem();
10372 * Set up the tab item.
10374 * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
10375 * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
10376 * the #setTabItem method instead.
10378 * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
10381 OO
.ui
.CardLayout
.prototype.setupTabItem = function () {
10382 if ( this.label
) {
10383 this.tabItem
.setLabel( this.label
);
10389 * Set the card to its 'active' state.
10391 * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
10392 * CSS is applied to the tab item to reflect the card's active state. Outside of the index
10393 * context, setting the active state on a card does nothing.
10395 * @param {boolean} value Card is active
10398 OO
.ui
.CardLayout
.prototype.setActive = function ( active
) {
10401 if ( active
!== this.active
) {
10402 this.active
= active
;
10403 this.$element
.toggleClass( 'oo-ui-cardLayout-active', this.active
);
10404 this.emit( 'active', this.active
);
10409 * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
10410 * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
10411 * rather extended to include the required content and functionality.
10413 * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
10414 * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
10415 * {@link OO.ui.BookletLayout BookletLayout} for an example.
10418 * @extends OO.ui.PanelLayout
10421 * @param {string} name Unique symbolic name of page
10422 * @param {Object} [config] Configuration options
10424 OO
.ui
.PageLayout
= function OoUiPageLayout( name
, config
) {
10425 // Allow passing positional parameters inside the config object
10426 if ( OO
.isPlainObject( name
) && config
=== undefined ) {
10428 name
= config
.name
;
10431 // Configuration initialization
10432 config
= $.extend( { scrollable
: true }, config
);
10434 // Parent constructor
10435 OO
.ui
.PageLayout
.parent
.call( this, config
);
10439 this.outlineItem
= null;
10440 this.active
= false;
10443 this.$element
.addClass( 'oo-ui-pageLayout' );
10448 OO
.inheritClass( OO
.ui
.PageLayout
, OO
.ui
.PanelLayout
);
10453 * An 'active' event is emitted when the page becomes active. Pages become active when they are
10454 * shown in a booklet layout that is configured to display only one page at a time.
10457 * @param {boolean} active Page is active
10463 * Get the symbolic name of the page.
10465 * @return {string} Symbolic name of page
10467 OO
.ui
.PageLayout
.prototype.getName = function () {
10472 * Check if page is active.
10474 * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
10475 * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
10477 * @return {boolean} Page is active
10479 OO
.ui
.PageLayout
.prototype.isActive = function () {
10480 return this.active
;
10484 * Get outline item.
10486 * The outline item allows users to access the page from the booklet's outline
10487 * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
10489 * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
10491 OO
.ui
.PageLayout
.prototype.getOutlineItem = function () {
10492 return this.outlineItem
;
10496 * Set or unset the outline item.
10498 * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
10499 * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
10500 * level), use #setupOutlineItem instead of this method.
10502 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
10505 OO
.ui
.PageLayout
.prototype.setOutlineItem = function ( outlineItem
) {
10506 this.outlineItem
= outlineItem
|| null;
10507 if ( outlineItem
) {
10508 this.setupOutlineItem();
10514 * Set up the outline item.
10516 * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
10517 * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
10518 * the #setOutlineItem method instead.
10520 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
10523 OO
.ui
.PageLayout
.prototype.setupOutlineItem = function () {
10528 * Set the page to its 'active' state.
10530 * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
10531 * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
10532 * context, setting the active state on a page does nothing.
10534 * @param {boolean} value Page is active
10537 OO
.ui
.PageLayout
.prototype.setActive = function ( active
) {
10540 if ( active
!== this.active
) {
10541 this.active
= active
;
10542 this.$element
.toggleClass( 'oo-ui-pageLayout-active', active
);
10543 this.emit( 'active', this.active
);
10548 * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
10549 * at a time, though the stack layout can also be configured to show all contained panels, one after another,
10550 * by setting the #continuous option to 'true'.
10553 * // A stack layout with two panels, configured to be displayed continously
10554 * var myStack = new OO.ui.StackLayout( {
10556 * new OO.ui.PanelLayout( {
10557 * $content: $( '<p>Panel One</p>' ),
10561 * new OO.ui.PanelLayout( {
10562 * $content: $( '<p>Panel Two</p>' ),
10569 * $( 'body' ).append( myStack.$element );
10572 * @extends OO.ui.PanelLayout
10573 * @mixins OO.ui.mixin.GroupElement
10576 * @param {Object} [config] Configuration options
10577 * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
10578 * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
10580 OO
.ui
.StackLayout
= function OoUiStackLayout( config
) {
10581 // Configuration initialization
10582 config
= $.extend( { scrollable
: true }, config
);
10584 // Parent constructor
10585 OO
.ui
.StackLayout
.parent
.call( this, config
);
10587 // Mixin constructors
10588 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
10591 this.currentItem
= null;
10592 this.continuous
= !!config
.continuous
;
10595 this.$element
.addClass( 'oo-ui-stackLayout' );
10596 if ( this.continuous
) {
10597 this.$element
.addClass( 'oo-ui-stackLayout-continuous' );
10598 this.$element
.on( 'scroll', OO
.ui
.debounce( this.onScroll
.bind( this ), 250 ) );
10600 if ( Array
.isArray( config
.items
) ) {
10601 this.addItems( config
.items
);
10607 OO
.inheritClass( OO
.ui
.StackLayout
, OO
.ui
.PanelLayout
);
10608 OO
.mixinClass( OO
.ui
.StackLayout
, OO
.ui
.mixin
.GroupElement
);
10613 * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
10614 * {@link #clearItems cleared} or {@link #setItem displayed}.
10617 * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
10621 * When used in continuous mode, this event is emitted when the user scrolls down
10622 * far enough such that currentItem is no longer visible.
10624 * @event visibleItemChange
10625 * @param {OO.ui.PanelLayout} panel The next visible item in the layout
10631 * Handle scroll events from the layout element
10633 * @param {jQuery.Event} e
10634 * @fires visibleItemChange
10636 OO
.ui
.StackLayout
.prototype.onScroll = function () {
10638 len
= this.items
.length
,
10639 currentIndex
= this.items
.indexOf( this.currentItem
),
10640 newIndex
= currentIndex
,
10641 containerRect
= this.$element
[ 0 ].getBoundingClientRect();
10643 if ( !containerRect
|| ( !containerRect
.top
&& !containerRect
.bottom
) ) {
10644 // Can't get bounding rect, possibly not attached.
10648 function getRect( item
) {
10649 return item
.$element
[ 0 ].getBoundingClientRect();
10652 function isVisible( item
) {
10653 var rect
= getRect( item
);
10654 return rect
.bottom
> containerRect
.top
&& rect
.top
< containerRect
.bottom
;
10657 currentRect
= getRect( this.currentItem
);
10659 if ( currentRect
.bottom
< containerRect
.top
) {
10660 // Scrolled down past current item
10661 while ( ++newIndex
< len
) {
10662 if ( isVisible( this.items
[ newIndex
] ) ) {
10666 } else if ( currentRect
.top
> containerRect
.bottom
) {
10667 // Scrolled up past current item
10668 while ( --newIndex
>= 0 ) {
10669 if ( isVisible( this.items
[ newIndex
] ) ) {
10675 if ( newIndex
!== currentIndex
) {
10676 this.emit( 'visibleItemChange', this.items
[ newIndex
] );
10681 * Get the current panel.
10683 * @return {OO.ui.Layout|null}
10685 OO
.ui
.StackLayout
.prototype.getCurrentItem = function () {
10686 return this.currentItem
;
10690 * Unset the current item.
10693 * @param {OO.ui.StackLayout} layout
10696 OO
.ui
.StackLayout
.prototype.unsetCurrentItem = function () {
10697 var prevItem
= this.currentItem
;
10698 if ( prevItem
=== null ) {
10702 this.currentItem
= null;
10703 this.emit( 'set', null );
10707 * Add panel layouts to the stack layout.
10709 * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
10710 * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
10713 * @param {OO.ui.Layout[]} items Panels to add
10714 * @param {number} [index] Index of the insertion point
10717 OO
.ui
.StackLayout
.prototype.addItems = function ( items
, index
) {
10718 // Update the visibility
10719 this.updateHiddenState( items
, this.currentItem
);
10722 OO
.ui
.mixin
.GroupElement
.prototype.addItems
.call( this, items
, index
);
10724 if ( !this.currentItem
&& items
.length
) {
10725 this.setItem( items
[ 0 ] );
10732 * Remove the specified panels from the stack layout.
10734 * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
10735 * you may wish to use the #clearItems method instead.
10737 * @param {OO.ui.Layout[]} items Panels to remove
10741 OO
.ui
.StackLayout
.prototype.removeItems = function ( items
) {
10743 OO
.ui
.mixin
.GroupElement
.prototype.removeItems
.call( this, items
);
10745 if ( items
.indexOf( this.currentItem
) !== -1 ) {
10746 if ( this.items
.length
) {
10747 this.setItem( this.items
[ 0 ] );
10749 this.unsetCurrentItem();
10757 * Clear all panels from the stack layout.
10759 * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
10760 * a subset of panels, use the #removeItems method.
10765 OO
.ui
.StackLayout
.prototype.clearItems = function () {
10766 this.unsetCurrentItem();
10767 OO
.ui
.mixin
.GroupElement
.prototype.clearItems
.call( this );
10773 * Show the specified panel.
10775 * If another panel is currently displayed, it will be hidden.
10777 * @param {OO.ui.Layout} item Panel to show
10781 OO
.ui
.StackLayout
.prototype.setItem = function ( item
) {
10782 if ( item
!== this.currentItem
) {
10783 this.updateHiddenState( this.items
, item
);
10785 if ( this.items
.indexOf( item
) !== -1 ) {
10786 this.currentItem
= item
;
10787 this.emit( 'set', item
);
10789 this.unsetCurrentItem();
10797 * Update the visibility of all items in case of non-continuous view.
10799 * Ensure all items are hidden except for the selected one.
10800 * This method does nothing when the stack is continuous.
10803 * @param {OO.ui.Layout[]} items Item list iterate over
10804 * @param {OO.ui.Layout} [selectedItem] Selected item to show
10806 OO
.ui
.StackLayout
.prototype.updateHiddenState = function ( items
, selectedItem
) {
10809 if ( !this.continuous
) {
10810 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
10811 if ( !selectedItem
|| selectedItem
!== items
[ i
] ) {
10812 items
[ i
].$element
.addClass( 'oo-ui-element-hidden' );
10815 if ( selectedItem
) {
10816 selectedItem
.$element
.removeClass( 'oo-ui-element-hidden' );
10822 * MenuLayouts combine a menu and a content {@link OO.ui.PanelLayout panel}. The menu is positioned relative to the content (after, before, top, or bottom)
10823 * and its size is customized with the #menuSize config. The content area will fill all remaining space.
10826 * var menuLayout = new OO.ui.MenuLayout( {
10829 * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
10830 * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
10831 * select = new OO.ui.SelectWidget( {
10833 * new OO.ui.OptionWidget( {
10837 * new OO.ui.OptionWidget( {
10841 * new OO.ui.OptionWidget( {
10845 * new OO.ui.OptionWidget( {
10850 * } ).on( 'select', function ( item ) {
10851 * menuLayout.setMenuPosition( item.getData() );
10854 * menuLayout.$menu.append(
10855 * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
10857 * menuLayout.$content.append(
10858 * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
10860 * $( 'body' ).append( menuLayout.$element );
10862 * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
10863 * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
10864 * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
10867 * .oo-ui-menuLayout-menu {
10871 * .oo-ui-menuLayout-content {
10879 * @extends OO.ui.Layout
10882 * @param {Object} [config] Configuration options
10883 * @cfg {boolean} [showMenu=true] Show menu
10884 * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
10886 OO
.ui
.MenuLayout
= function OoUiMenuLayout( config
) {
10887 // Configuration initialization
10888 config
= $.extend( {
10890 menuPosition
: 'before'
10893 // Parent constructor
10894 OO
.ui
.MenuLayout
.parent
.call( this, config
);
10899 * @property {jQuery}
10901 this.$menu
= $( '<div>' );
10905 * @property {jQuery}
10907 this.$content
= $( '<div>' );
10911 .addClass( 'oo-ui-menuLayout-menu' );
10912 this.$content
.addClass( 'oo-ui-menuLayout-content' );
10914 .addClass( 'oo-ui-menuLayout' )
10915 .append( this.$content
, this.$menu
);
10916 this.setMenuPosition( config
.menuPosition
);
10917 this.toggleMenu( config
.showMenu
);
10922 OO
.inheritClass( OO
.ui
.MenuLayout
, OO
.ui
.Layout
);
10929 * @param {boolean} showMenu Show menu, omit to toggle
10932 OO
.ui
.MenuLayout
.prototype.toggleMenu = function ( showMenu
) {
10933 showMenu
= showMenu
=== undefined ? !this.showMenu
: !!showMenu
;
10935 if ( this.showMenu
!== showMenu
) {
10936 this.showMenu
= showMenu
;
10938 .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu
)
10939 .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu
);
10946 * Check if menu is visible
10948 * @return {boolean} Menu is visible
10950 OO
.ui
.MenuLayout
.prototype.isMenuVisible = function () {
10951 return this.showMenu
;
10955 * Set menu position.
10957 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
10958 * @throws {Error} If position value is not supported
10961 OO
.ui
.MenuLayout
.prototype.setMenuPosition = function ( position
) {
10962 this.$element
.removeClass( 'oo-ui-menuLayout-' + this.menuPosition
);
10963 this.menuPosition
= position
;
10964 this.$element
.addClass( 'oo-ui-menuLayout-' + position
);
10970 * Get menu position.
10972 * @return {string} Menu position
10974 OO
.ui
.MenuLayout
.prototype.getMenuPosition = function () {
10975 return this.menuPosition
;
10979 * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
10980 * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
10981 * through the pages and select which one to display. By default, only one page is
10982 * displayed at a time and the outline is hidden. When a user navigates to a new page,
10983 * the booklet layout automatically focuses on the first focusable element, unless the
10984 * default setting is changed. Optionally, booklets can be configured to show
10985 * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
10988 * // Example of a BookletLayout that contains two PageLayouts.
10990 * function PageOneLayout( name, config ) {
10991 * PageOneLayout.parent.call( this, name, config );
10992 * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
10994 * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
10995 * PageOneLayout.prototype.setupOutlineItem = function () {
10996 * this.outlineItem.setLabel( 'Page One' );
10999 * function PageTwoLayout( name, config ) {
11000 * PageTwoLayout.parent.call( this, name, config );
11001 * this.$element.append( '<p>Second page</p>' );
11003 * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
11004 * PageTwoLayout.prototype.setupOutlineItem = function () {
11005 * this.outlineItem.setLabel( 'Page Two' );
11008 * var page1 = new PageOneLayout( 'one' ),
11009 * page2 = new PageTwoLayout( 'two' );
11011 * var booklet = new OO.ui.BookletLayout( {
11015 * booklet.addPages ( [ page1, page2 ] );
11016 * $( 'body' ).append( booklet.$element );
11019 * @extends OO.ui.MenuLayout
11022 * @param {Object} [config] Configuration options
11023 * @cfg {boolean} [continuous=false] Show all pages, one after another
11024 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
11025 * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
11026 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
11028 OO
.ui
.BookletLayout
= function OoUiBookletLayout( config
) {
11029 // Configuration initialization
11030 config
= config
|| {};
11032 // Parent constructor
11033 OO
.ui
.BookletLayout
.parent
.call( this, config
);
11036 this.currentPageName
= null;
11038 this.ignoreFocus
= false;
11039 this.stackLayout
= new OO
.ui
.StackLayout( { continuous
: !!config
.continuous
} );
11040 this.$content
.append( this.stackLayout
.$element
);
11041 this.autoFocus
= config
.autoFocus
=== undefined || !!config
.autoFocus
;
11042 this.outlineVisible
= false;
11043 this.outlined
= !!config
.outlined
;
11044 if ( this.outlined
) {
11045 this.editable
= !!config
.editable
;
11046 this.outlineControlsWidget
= null;
11047 this.outlineSelectWidget
= new OO
.ui
.OutlineSelectWidget();
11048 this.outlinePanel
= new OO
.ui
.PanelLayout( { scrollable
: true } );
11049 this.$menu
.append( this.outlinePanel
.$element
);
11050 this.outlineVisible
= true;
11051 if ( this.editable
) {
11052 this.outlineControlsWidget
= new OO
.ui
.OutlineControlsWidget(
11053 this.outlineSelectWidget
11057 this.toggleMenu( this.outlined
);
11060 this.stackLayout
.connect( this, { set: 'onStackLayoutSet' } );
11061 if ( this.outlined
) {
11062 this.outlineSelectWidget
.connect( this, { select
: 'onOutlineSelectWidgetSelect' } );
11063 this.scrolling
= false;
11064 this.stackLayout
.connect( this, { visibleItemChange
: 'onStackLayoutVisibleItemChange' } );
11066 if ( this.autoFocus
) {
11067 // Event 'focus' does not bubble, but 'focusin' does
11068 this.stackLayout
.$element
.on( 'focusin', this.onStackLayoutFocus
.bind( this ) );
11072 this.$element
.addClass( 'oo-ui-bookletLayout' );
11073 this.stackLayout
.$element
.addClass( 'oo-ui-bookletLayout-stackLayout' );
11074 if ( this.outlined
) {
11075 this.outlinePanel
.$element
11076 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
11077 .append( this.outlineSelectWidget
.$element
);
11078 if ( this.editable
) {
11079 this.outlinePanel
.$element
11080 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
11081 .append( this.outlineControlsWidget
.$element
);
11088 OO
.inheritClass( OO
.ui
.BookletLayout
, OO
.ui
.MenuLayout
);
11093 * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
11095 * @param {OO.ui.PageLayout} page Current page
11099 * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
11102 * @param {OO.ui.PageLayout[]} page Added pages
11103 * @param {number} index Index pages were added at
11107 * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
11108 * {@link #removePages removed} from the booklet.
11111 * @param {OO.ui.PageLayout[]} pages Removed pages
11117 * Handle stack layout focus.
11120 * @param {jQuery.Event} e Focusin event
11122 OO
.ui
.BookletLayout
.prototype.onStackLayoutFocus = function ( e
) {
11125 // Find the page that an element was focused within
11126 $target
= $( e
.target
).closest( '.oo-ui-pageLayout' );
11127 for ( name
in this.pages
) {
11128 // Check for page match, exclude current page to find only page changes
11129 if ( this.pages
[ name
].$element
[ 0 ] === $target
[ 0 ] && name
!== this.currentPageName
) {
11130 this.setPage( name
);
11137 * Handle visibleItemChange events from the stackLayout
11139 * The next visible page is set as the current page by selecting it
11142 * @param {OO.ui.PageLayout} page The next visible page in the layout
11144 OO
.ui
.BookletLayout
.prototype.onStackLayoutVisibleItemChange = function ( page
) {
11145 // Set a flag to so that the resulting call to #onStackLayoutSet doesn't
11146 // try and scroll the item into view again.
11147 this.scrolling
= true;
11148 this.outlineSelectWidget
.selectItemByData( page
.getName() );
11149 this.scrolling
= false;
11153 * Handle stack layout set events.
11156 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
11158 OO
.ui
.BookletLayout
.prototype.onStackLayoutSet = function ( page
) {
11160 if ( !this.scrolling
&& page
) {
11161 page
.scrollElementIntoView( { complete: function () {
11162 if ( layout
.autoFocus
) {
11170 * Focus the first input in the current page.
11172 * If no page is selected, the first selectable page will be selected.
11173 * If the focus is already in an element on the current page, nothing will happen.
11174 * @param {number} [itemIndex] A specific item to focus on
11176 OO
.ui
.BookletLayout
.prototype.focus = function ( itemIndex
) {
11178 items
= this.stackLayout
.getItems();
11180 if ( itemIndex
!== undefined && items
[ itemIndex
] ) {
11181 page
= items
[ itemIndex
];
11183 page
= this.stackLayout
.getCurrentItem();
11186 if ( !page
&& this.outlined
) {
11187 this.selectFirstSelectablePage();
11188 page
= this.stackLayout
.getCurrentItem();
11193 // Only change the focus if is not already in the current page
11194 if ( !OO
.ui
.contains( page
.$element
[ 0 ], this.getElementDocument().activeElement
, true ) ) {
11200 * Find the first focusable input in the booklet layout and focus
11203 OO
.ui
.BookletLayout
.prototype.focusFirstFocusable = function () {
11204 OO
.ui
.findFocusable( this.stackLayout
.$element
).focus();
11208 * Handle outline widget select events.
11211 * @param {OO.ui.OptionWidget|null} item Selected item
11213 OO
.ui
.BookletLayout
.prototype.onOutlineSelectWidgetSelect = function ( item
) {
11215 this.setPage( item
.getData() );
11220 * Check if booklet has an outline.
11222 * @return {boolean} Booklet has an outline
11224 OO
.ui
.BookletLayout
.prototype.isOutlined = function () {
11225 return this.outlined
;
11229 * Check if booklet has editing controls.
11231 * @return {boolean} Booklet is editable
11233 OO
.ui
.BookletLayout
.prototype.isEditable = function () {
11234 return this.editable
;
11238 * Check if booklet has a visible outline.
11240 * @return {boolean} Outline is visible
11242 OO
.ui
.BookletLayout
.prototype.isOutlineVisible = function () {
11243 return this.outlined
&& this.outlineVisible
;
11247 * Hide or show the outline.
11249 * @param {boolean} [show] Show outline, omit to invert current state
11252 OO
.ui
.BookletLayout
.prototype.toggleOutline = function ( show
) {
11253 if ( this.outlined
) {
11254 show
= show
=== undefined ? !this.outlineVisible
: !!show
;
11255 this.outlineVisible
= show
;
11256 this.toggleMenu( show
);
11263 * Get the page closest to the specified page.
11265 * @param {OO.ui.PageLayout} page Page to use as a reference point
11266 * @return {OO.ui.PageLayout|null} Page closest to the specified page
11268 OO
.ui
.BookletLayout
.prototype.getClosestPage = function ( page
) {
11269 var next
, prev
, level
,
11270 pages
= this.stackLayout
.getItems(),
11271 index
= pages
.indexOf( page
);
11273 if ( index
!== -1 ) {
11274 next
= pages
[ index
+ 1 ];
11275 prev
= pages
[ index
- 1 ];
11276 // Prefer adjacent pages at the same level
11277 if ( this.outlined
) {
11278 level
= this.outlineSelectWidget
.getItemFromData( page
.getName() ).getLevel();
11281 level
=== this.outlineSelectWidget
.getItemFromData( prev
.getName() ).getLevel()
11287 level
=== this.outlineSelectWidget
.getItemFromData( next
.getName() ).getLevel()
11293 return prev
|| next
|| null;
11297 * Get the outline widget.
11299 * If the booklet is not outlined, the method will return `null`.
11301 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
11303 OO
.ui
.BookletLayout
.prototype.getOutline = function () {
11304 return this.outlineSelectWidget
;
11308 * Get the outline controls widget.
11310 * If the outline is not editable, the method will return `null`.
11312 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
11314 OO
.ui
.BookletLayout
.prototype.getOutlineControls = function () {
11315 return this.outlineControlsWidget
;
11319 * Get a page by its symbolic name.
11321 * @param {string} name Symbolic name of page
11322 * @return {OO.ui.PageLayout|undefined} Page, if found
11324 OO
.ui
.BookletLayout
.prototype.getPage = function ( name
) {
11325 return this.pages
[ name
];
11329 * Get the current page.
11331 * @return {OO.ui.PageLayout|undefined} Current page, if found
11333 OO
.ui
.BookletLayout
.prototype.getCurrentPage = function () {
11334 var name
= this.getCurrentPageName();
11335 return name
? this.getPage( name
) : undefined;
11339 * Get the symbolic name of the current page.
11341 * @return {string|null} Symbolic name of the current page
11343 OO
.ui
.BookletLayout
.prototype.getCurrentPageName = function () {
11344 return this.currentPageName
;
11348 * Add pages to the booklet layout
11350 * When pages are added with the same names as existing pages, the existing pages will be
11351 * automatically removed before the new pages are added.
11353 * @param {OO.ui.PageLayout[]} pages Pages to add
11354 * @param {number} index Index of the insertion point
11358 OO
.ui
.BookletLayout
.prototype.addPages = function ( pages
, index
) {
11359 var i
, len
, name
, page
, item
, currentIndex
,
11360 stackLayoutPages
= this.stackLayout
.getItems(),
11364 // Remove pages with same names
11365 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
11367 name
= page
.getName();
11369 if ( Object
.prototype.hasOwnProperty
.call( this.pages
, name
) ) {
11370 // Correct the insertion index
11371 currentIndex
= stackLayoutPages
.indexOf( this.pages
[ name
] );
11372 if ( currentIndex
!== -1 && currentIndex
+ 1 < index
) {
11375 remove
.push( this.pages
[ name
] );
11378 if ( remove
.length
) {
11379 this.removePages( remove
);
11383 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
11385 name
= page
.getName();
11386 this.pages
[ page
.getName() ] = page
;
11387 if ( this.outlined
) {
11388 item
= new OO
.ui
.OutlineOptionWidget( { data
: name
} );
11389 page
.setOutlineItem( item
);
11390 items
.push( item
);
11394 if ( this.outlined
&& items
.length
) {
11395 this.outlineSelectWidget
.addItems( items
, index
);
11396 this.selectFirstSelectablePage();
11398 this.stackLayout
.addItems( pages
, index
);
11399 this.emit( 'add', pages
, index
);
11405 * Remove the specified pages from the booklet layout.
11407 * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
11409 * @param {OO.ui.PageLayout[]} pages An array of pages to remove
11413 OO
.ui
.BookletLayout
.prototype.removePages = function ( pages
) {
11414 var i
, len
, name
, page
,
11417 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
11419 name
= page
.getName();
11420 delete this.pages
[ name
];
11421 if ( this.outlined
) {
11422 items
.push( this.outlineSelectWidget
.getItemFromData( name
) );
11423 page
.setOutlineItem( null );
11426 if ( this.outlined
&& items
.length
) {
11427 this.outlineSelectWidget
.removeItems( items
);
11428 this.selectFirstSelectablePage();
11430 this.stackLayout
.removeItems( pages
);
11431 this.emit( 'remove', pages
);
11437 * Clear all pages from the booklet layout.
11439 * To remove only a subset of pages from the booklet, use the #removePages method.
11444 OO
.ui
.BookletLayout
.prototype.clearPages = function () {
11446 pages
= this.stackLayout
.getItems();
11449 this.currentPageName
= null;
11450 if ( this.outlined
) {
11451 this.outlineSelectWidget
.clearItems();
11452 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
11453 pages
[ i
].setOutlineItem( null );
11456 this.stackLayout
.clearItems();
11458 this.emit( 'remove', pages
);
11464 * Set the current page by symbolic name.
11467 * @param {string} name Symbolic name of page
11469 OO
.ui
.BookletLayout
.prototype.setPage = function ( name
) {
11472 page
= this.pages
[ name
],
11473 previousPage
= this.currentPageName
&& this.pages
[ this.currentPageName
];
11475 if ( name
!== this.currentPageName
) {
11476 if ( this.outlined
) {
11477 selectedItem
= this.outlineSelectWidget
.getSelectedItem();
11478 if ( selectedItem
&& selectedItem
.getData() !== name
) {
11479 this.outlineSelectWidget
.selectItemByData( name
);
11483 if ( previousPage
) {
11484 previousPage
.setActive( false );
11485 // Blur anything focused if the next page doesn't have anything focusable.
11486 // This is not needed if the next page has something focusable (because once it is focused
11487 // this blur happens automatically). If the layout is non-continuous, this check is
11488 // meaningless because the next page is not visible yet and thus can't hold focus.
11491 this.stackLayout
.continuous
&&
11492 OO
.ui
.findFocusable( page
.$element
).length
!== 0
11494 $focused
= previousPage
.$element
.find( ':focus' );
11495 if ( $focused
.length
) {
11496 $focused
[ 0 ].blur();
11500 this.currentPageName
= name
;
11501 page
.setActive( true );
11502 this.stackLayout
.setItem( page
);
11503 if ( !this.stackLayout
.continuous
&& previousPage
) {
11504 // This should not be necessary, since any inputs on the previous page should have been
11505 // blurred when it was hidden, but browsers are not very consistent about this.
11506 $focused
= previousPage
.$element
.find( ':focus' );
11507 if ( $focused
.length
) {
11508 $focused
[ 0 ].blur();
11511 this.emit( 'set', page
);
11517 * Select the first selectable page.
11521 OO
.ui
.BookletLayout
.prototype.selectFirstSelectablePage = function () {
11522 if ( !this.outlineSelectWidget
.getSelectedItem() ) {
11523 this.outlineSelectWidget
.selectItem( this.outlineSelectWidget
.getFirstSelectableItem() );
11530 * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
11531 * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
11532 * select which one to display. By default, only one card is displayed at a time. When a user
11533 * navigates to a new card, the index layout automatically focuses on the first focusable element,
11534 * unless the default setting is changed.
11536 * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
11539 * // Example of a IndexLayout that contains two CardLayouts.
11541 * function CardOneLayout( name, config ) {
11542 * CardOneLayout.parent.call( this, name, config );
11543 * this.$element.append( '<p>First card</p>' );
11545 * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
11546 * CardOneLayout.prototype.setupTabItem = function () {
11547 * this.tabItem.setLabel( 'Card one' );
11550 * var card1 = new CardOneLayout( 'one' ),
11551 * card2 = new CardLayout( 'two', { label: 'Card two' } );
11553 * card2.$element.append( '<p>Second card</p>' );
11555 * var index = new OO.ui.IndexLayout();
11557 * index.addCards ( [ card1, card2 ] );
11558 * $( 'body' ).append( index.$element );
11561 * @extends OO.ui.MenuLayout
11564 * @param {Object} [config] Configuration options
11565 * @cfg {boolean} [continuous=false] Show all cards, one after another
11566 * @cfg {boolean} [expanded=true] Expand the content panel to fill the entire parent element.
11567 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
11569 OO
.ui
.IndexLayout
= function OoUiIndexLayout( config
) {
11570 // Configuration initialization
11571 config
= $.extend( {}, config
, { menuPosition
: 'top' } );
11573 // Parent constructor
11574 OO
.ui
.IndexLayout
.parent
.call( this, config
);
11577 this.currentCardName
= null;
11579 this.ignoreFocus
= false;
11580 this.stackLayout
= new OO
.ui
.StackLayout( {
11581 continuous
: !!config
.continuous
,
11582 expanded
: config
.expanded
11584 this.$content
.append( this.stackLayout
.$element
);
11585 this.autoFocus
= config
.autoFocus
=== undefined || !!config
.autoFocus
;
11587 this.tabSelectWidget
= new OO
.ui
.TabSelectWidget();
11588 this.tabPanel
= new OO
.ui
.PanelLayout();
11589 this.$menu
.append( this.tabPanel
.$element
);
11591 this.toggleMenu( true );
11594 this.stackLayout
.connect( this, { set: 'onStackLayoutSet' } );
11595 this.tabSelectWidget
.connect( this, { select
: 'onTabSelectWidgetSelect' } );
11596 if ( this.autoFocus
) {
11597 // Event 'focus' does not bubble, but 'focusin' does
11598 this.stackLayout
.$element
.on( 'focusin', this.onStackLayoutFocus
.bind( this ) );
11602 this.$element
.addClass( 'oo-ui-indexLayout' );
11603 this.stackLayout
.$element
.addClass( 'oo-ui-indexLayout-stackLayout' );
11604 this.tabPanel
.$element
11605 .addClass( 'oo-ui-indexLayout-tabPanel' )
11606 .append( this.tabSelectWidget
.$element
);
11611 OO
.inheritClass( OO
.ui
.IndexLayout
, OO
.ui
.MenuLayout
);
11616 * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
11618 * @param {OO.ui.CardLayout} card Current card
11622 * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
11625 * @param {OO.ui.CardLayout[]} card Added cards
11626 * @param {number} index Index cards were added at
11630 * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
11631 * {@link #removeCards removed} from the index.
11634 * @param {OO.ui.CardLayout[]} cards Removed cards
11640 * Handle stack layout focus.
11643 * @param {jQuery.Event} e Focusin event
11645 OO
.ui
.IndexLayout
.prototype.onStackLayoutFocus = function ( e
) {
11648 // Find the card that an element was focused within
11649 $target
= $( e
.target
).closest( '.oo-ui-cardLayout' );
11650 for ( name
in this.cards
) {
11651 // Check for card match, exclude current card to find only card changes
11652 if ( this.cards
[ name
].$element
[ 0 ] === $target
[ 0 ] && name
!== this.currentCardName
) {
11653 this.setCard( name
);
11660 * Handle stack layout set events.
11663 * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
11665 OO
.ui
.IndexLayout
.prototype.onStackLayoutSet = function ( card
) {
11668 card
.scrollElementIntoView( { complete: function () {
11669 if ( layout
.autoFocus
) {
11677 * Focus the first input in the current card.
11679 * If no card is selected, the first selectable card will be selected.
11680 * If the focus is already in an element on the current card, nothing will happen.
11681 * @param {number} [itemIndex] A specific item to focus on
11683 OO
.ui
.IndexLayout
.prototype.focus = function ( itemIndex
) {
11685 items
= this.stackLayout
.getItems();
11687 if ( itemIndex
!== undefined && items
[ itemIndex
] ) {
11688 card
= items
[ itemIndex
];
11690 card
= this.stackLayout
.getCurrentItem();
11694 this.selectFirstSelectableCard();
11695 card
= this.stackLayout
.getCurrentItem();
11700 // Only change the focus if is not already in the current page
11701 if ( !OO
.ui
.contains( card
.$element
[ 0 ], this.getElementDocument().activeElement
, true ) ) {
11707 * Find the first focusable input in the index layout and focus
11710 OO
.ui
.IndexLayout
.prototype.focusFirstFocusable = function () {
11711 OO
.ui
.findFocusable( this.stackLayout
.$element
).focus();
11715 * Handle tab widget select events.
11718 * @param {OO.ui.OptionWidget|null} item Selected item
11720 OO
.ui
.IndexLayout
.prototype.onTabSelectWidgetSelect = function ( item
) {
11722 this.setCard( item
.getData() );
11727 * Get the card closest to the specified card.
11729 * @param {OO.ui.CardLayout} card Card to use as a reference point
11730 * @return {OO.ui.CardLayout|null} Card closest to the specified card
11732 OO
.ui
.IndexLayout
.prototype.getClosestCard = function ( card
) {
11733 var next
, prev
, level
,
11734 cards
= this.stackLayout
.getItems(),
11735 index
= cards
.indexOf( card
);
11737 if ( index
!== -1 ) {
11738 next
= cards
[ index
+ 1 ];
11739 prev
= cards
[ index
- 1 ];
11740 // Prefer adjacent cards at the same level
11741 level
= this.tabSelectWidget
.getItemFromData( card
.getName() ).getLevel();
11744 level
=== this.tabSelectWidget
.getItemFromData( prev
.getName() ).getLevel()
11750 level
=== this.tabSelectWidget
.getItemFromData( next
.getName() ).getLevel()
11755 return prev
|| next
|| null;
11759 * Get the tabs widget.
11761 * @return {OO.ui.TabSelectWidget} Tabs widget
11763 OO
.ui
.IndexLayout
.prototype.getTabs = function () {
11764 return this.tabSelectWidget
;
11768 * Get a card by its symbolic name.
11770 * @param {string} name Symbolic name of card
11771 * @return {OO.ui.CardLayout|undefined} Card, if found
11773 OO
.ui
.IndexLayout
.prototype.getCard = function ( name
) {
11774 return this.cards
[ name
];
11778 * Get the current card.
11780 * @return {OO.ui.CardLayout|undefined} Current card, if found
11782 OO
.ui
.IndexLayout
.prototype.getCurrentCard = function () {
11783 var name
= this.getCurrentCardName();
11784 return name
? this.getCard( name
) : undefined;
11788 * Get the symbolic name of the current card.
11790 * @return {string|null} Symbolic name of the current card
11792 OO
.ui
.IndexLayout
.prototype.getCurrentCardName = function () {
11793 return this.currentCardName
;
11797 * Add cards to the index layout
11799 * When cards are added with the same names as existing cards, the existing cards will be
11800 * automatically removed before the new cards are added.
11802 * @param {OO.ui.CardLayout[]} cards Cards to add
11803 * @param {number} index Index of the insertion point
11807 OO
.ui
.IndexLayout
.prototype.addCards = function ( cards
, index
) {
11808 var i
, len
, name
, card
, item
, currentIndex
,
11809 stackLayoutCards
= this.stackLayout
.getItems(),
11813 // Remove cards with same names
11814 for ( i
= 0, len
= cards
.length
; i
< len
; i
++ ) {
11816 name
= card
.getName();
11818 if ( Object
.prototype.hasOwnProperty
.call( this.cards
, name
) ) {
11819 // Correct the insertion index
11820 currentIndex
= stackLayoutCards
.indexOf( this.cards
[ name
] );
11821 if ( currentIndex
!== -1 && currentIndex
+ 1 < index
) {
11824 remove
.push( this.cards
[ name
] );
11827 if ( remove
.length
) {
11828 this.removeCards( remove
);
11832 for ( i
= 0, len
= cards
.length
; i
< len
; i
++ ) {
11834 name
= card
.getName();
11835 this.cards
[ card
.getName() ] = card
;
11836 item
= new OO
.ui
.TabOptionWidget( { data
: name
} );
11837 card
.setTabItem( item
);
11838 items
.push( item
);
11841 if ( items
.length
) {
11842 this.tabSelectWidget
.addItems( items
, index
);
11843 this.selectFirstSelectableCard();
11845 this.stackLayout
.addItems( cards
, index
);
11846 this.emit( 'add', cards
, index
);
11852 * Remove the specified cards from the index layout.
11854 * To remove all cards from the index, you may wish to use the #clearCards method instead.
11856 * @param {OO.ui.CardLayout[]} cards An array of cards to remove
11860 OO
.ui
.IndexLayout
.prototype.removeCards = function ( cards
) {
11861 var i
, len
, name
, card
,
11864 for ( i
= 0, len
= cards
.length
; i
< len
; i
++ ) {
11866 name
= card
.getName();
11867 delete this.cards
[ name
];
11868 items
.push( this.tabSelectWidget
.getItemFromData( name
) );
11869 card
.setTabItem( null );
11871 if ( items
.length
) {
11872 this.tabSelectWidget
.removeItems( items
);
11873 this.selectFirstSelectableCard();
11875 this.stackLayout
.removeItems( cards
);
11876 this.emit( 'remove', cards
);
11882 * Clear all cards from the index layout.
11884 * To remove only a subset of cards from the index, use the #removeCards method.
11889 OO
.ui
.IndexLayout
.prototype.clearCards = function () {
11891 cards
= this.stackLayout
.getItems();
11894 this.currentCardName
= null;
11895 this.tabSelectWidget
.clearItems();
11896 for ( i
= 0, len
= cards
.length
; i
< len
; i
++ ) {
11897 cards
[ i
].setTabItem( null );
11899 this.stackLayout
.clearItems();
11901 this.emit( 'remove', cards
);
11907 * Set the current card by symbolic name.
11910 * @param {string} name Symbolic name of card
11912 OO
.ui
.IndexLayout
.prototype.setCard = function ( name
) {
11915 card
= this.cards
[ name
],
11916 previousCard
= this.currentCardName
&& this.cards
[ this.currentCardName
];
11918 if ( name
!== this.currentCardName
) {
11919 selectedItem
= this.tabSelectWidget
.getSelectedItem();
11920 if ( selectedItem
&& selectedItem
.getData() !== name
) {
11921 this.tabSelectWidget
.selectItemByData( name
);
11924 if ( previousCard
) {
11925 previousCard
.setActive( false );
11926 // Blur anything focused if the next card doesn't have anything focusable.
11927 // This is not needed if the next card has something focusable (because once it is focused
11928 // this blur happens automatically). If the layout is non-continuous, this check is
11929 // meaningless because the next card is not visible yet and thus can't hold focus.
11932 this.stackLayout
.continuous
&&
11933 OO
.ui
.findFocusable( card
.$element
).length
!== 0
11935 $focused
= previousCard
.$element
.find( ':focus' );
11936 if ( $focused
.length
) {
11937 $focused
[ 0 ].blur();
11941 this.currentCardName
= name
;
11942 card
.setActive( true );
11943 this.stackLayout
.setItem( card
);
11944 if ( !this.stackLayout
.continuous
&& previousCard
) {
11945 // This should not be necessary, since any inputs on the previous card should have been
11946 // blurred when it was hidden, but browsers are not very consistent about this.
11947 $focused
= previousCard
.$element
.find( ':focus' );
11948 if ( $focused
.length
) {
11949 $focused
[ 0 ].blur();
11952 this.emit( 'set', card
);
11958 * Select the first selectable card.
11962 OO
.ui
.IndexLayout
.prototype.selectFirstSelectableCard = function () {
11963 if ( !this.tabSelectWidget
.getSelectedItem() ) {
11964 this.tabSelectWidget
.selectItem( this.tabSelectWidget
.getFirstSelectableItem() );
11971 * ToggleWidget implements basic behavior of widgets with an on/off state.
11972 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
11976 * @extends OO.ui.Widget
11979 * @param {Object} [config] Configuration options
11980 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
11981 * By default, the toggle is in the 'off' state.
11983 OO
.ui
.ToggleWidget
= function OoUiToggleWidget( config
) {
11984 // Configuration initialization
11985 config
= config
|| {};
11987 // Parent constructor
11988 OO
.ui
.ToggleWidget
.parent
.call( this, config
);
11994 this.$element
.addClass( 'oo-ui-toggleWidget' );
11995 this.setValue( !!config
.value
);
12000 OO
.inheritClass( OO
.ui
.ToggleWidget
, OO
.ui
.Widget
);
12007 * A change event is emitted when the on/off state of the toggle changes.
12009 * @param {boolean} value Value representing the new state of the toggle
12015 * Get the value representing the toggle’s state.
12017 * @return {boolean} The on/off state of the toggle
12019 OO
.ui
.ToggleWidget
.prototype.getValue = function () {
12024 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
12026 * @param {boolean} value The state of the toggle
12030 OO
.ui
.ToggleWidget
.prototype.setValue = function ( value
) {
12032 if ( this.value
!== value
) {
12033 this.value
= value
;
12034 this.emit( 'change', value
);
12035 this.$element
.toggleClass( 'oo-ui-toggleWidget-on', value
);
12036 this.$element
.toggleClass( 'oo-ui-toggleWidget-off', !value
);
12037 this.$element
.attr( 'aria-checked', value
.toString() );
12043 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
12044 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
12045 * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
12046 * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
12047 * and {@link OO.ui.mixin.LabelElement labels}. Please see
12048 * the [OOjs UI documentation][1] on MediaWiki for more information.
12051 * // Toggle buttons in the 'off' and 'on' state.
12052 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
12053 * label: 'Toggle Button off'
12055 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
12056 * label: 'Toggle Button on',
12059 * // Append the buttons to the DOM.
12060 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
12062 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
12065 * @extends OO.ui.ToggleWidget
12066 * @mixins OO.ui.mixin.ButtonElement
12067 * @mixins OO.ui.mixin.IconElement
12068 * @mixins OO.ui.mixin.IndicatorElement
12069 * @mixins OO.ui.mixin.LabelElement
12070 * @mixins OO.ui.mixin.TitledElement
12071 * @mixins OO.ui.mixin.FlaggedElement
12072 * @mixins OO.ui.mixin.TabIndexedElement
12075 * @param {Object} [config] Configuration options
12076 * @cfg {boolean} [value=false] The toggle button’s initial on/off
12077 * state. By default, the button is in the 'off' state.
12079 OO
.ui
.ToggleButtonWidget
= function OoUiToggleButtonWidget( config
) {
12080 // Configuration initialization
12081 config
= config
|| {};
12083 // Parent constructor
12084 OO
.ui
.ToggleButtonWidget
.parent
.call( this, config
);
12086 // Mixin constructors
12087 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
12088 OO
.ui
.mixin
.IconElement
.call( this, config
);
12089 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
12090 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12091 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
12092 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
12093 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
12096 this.connect( this, { click
: 'onAction' } );
12099 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
12101 .addClass( 'oo-ui-toggleButtonWidget' )
12102 .append( this.$button
);
12107 OO
.inheritClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.ToggleWidget
);
12108 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
12109 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.IconElement
);
12110 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
12111 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.LabelElement
);
12112 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.TitledElement
);
12113 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
12114 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
12119 * Handle the button action being triggered.
12123 OO
.ui
.ToggleButtonWidget
.prototype.onAction = function () {
12124 this.setValue( !this.value
);
12130 OO
.ui
.ToggleButtonWidget
.prototype.setValue = function ( value
) {
12132 if ( value
!== this.value
) {
12133 // Might be called from parent constructor before ButtonElement constructor
12134 if ( this.$button
) {
12135 this.$button
.attr( 'aria-pressed', value
.toString() );
12137 this.setActive( value
);
12141 OO
.ui
.ToggleButtonWidget
.parent
.prototype.setValue
.call( this, value
);
12149 OO
.ui
.ToggleButtonWidget
.prototype.setButtonElement = function ( $button
) {
12150 if ( this.$button
) {
12151 this.$button
.removeAttr( 'aria-pressed' );
12153 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement
.call( this, $button
);
12154 this.$button
.attr( 'aria-pressed', this.value
.toString() );
12158 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
12159 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
12160 * visually by a slider in the leftmost position.
12163 * // Toggle switches in the 'off' and 'on' position.
12164 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
12165 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
12169 * // Create a FieldsetLayout to layout and label switches
12170 * var fieldset = new OO.ui.FieldsetLayout( {
12171 * label: 'Toggle switches'
12173 * fieldset.addItems( [
12174 * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
12175 * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
12177 * $( 'body' ).append( fieldset.$element );
12180 * @extends OO.ui.ToggleWidget
12181 * @mixins OO.ui.mixin.TabIndexedElement
12184 * @param {Object} [config] Configuration options
12185 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
12186 * By default, the toggle switch is in the 'off' position.
12188 OO
.ui
.ToggleSwitchWidget
= function OoUiToggleSwitchWidget( config
) {
12189 // Parent constructor
12190 OO
.ui
.ToggleSwitchWidget
.parent
.call( this, config
);
12192 // Mixin constructors
12193 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
12196 this.dragging
= false;
12197 this.dragStart
= null;
12198 this.sliding
= false;
12199 this.$glow
= $( '<span>' );
12200 this.$grip
= $( '<span>' );
12203 this.$element
.on( {
12204 click
: this.onClick
.bind( this ),
12205 keypress
: this.onKeyPress
.bind( this )
12209 this.$glow
.addClass( 'oo-ui-toggleSwitchWidget-glow' );
12210 this.$grip
.addClass( 'oo-ui-toggleSwitchWidget-grip' );
12212 .addClass( 'oo-ui-toggleSwitchWidget' )
12213 .attr( 'role', 'checkbox' )
12214 .append( this.$glow
, this.$grip
);
12219 OO
.inheritClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.ToggleWidget
);
12220 OO
.mixinClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.mixin
.TabIndexedElement
);
12225 * Handle mouse click events.
12228 * @param {jQuery.Event} e Mouse click event
12230 OO
.ui
.ToggleSwitchWidget
.prototype.onClick = function ( e
) {
12231 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
12232 this.setValue( !this.value
);
12238 * Handle key press events.
12241 * @param {jQuery.Event} e Key press event
12243 OO
.ui
.ToggleSwitchWidget
.prototype.onKeyPress = function ( e
) {
12244 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
12245 this.setValue( !this.value
);
12251 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
12252 * Controls include moving items up and down, removing items, and adding different kinds of items.
12254 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
12257 * @extends OO.ui.Widget
12258 * @mixins OO.ui.mixin.GroupElement
12259 * @mixins OO.ui.mixin.IconElement
12262 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
12263 * @param {Object} [config] Configuration options
12264 * @cfg {Object} [abilities] List of abilties
12265 * @cfg {boolean} [abilities.move=true] Allow moving movable items
12266 * @cfg {boolean} [abilities.remove=true] Allow removing removable items
12268 OO
.ui
.OutlineControlsWidget
= function OoUiOutlineControlsWidget( outline
, config
) {
12269 // Allow passing positional parameters inside the config object
12270 if ( OO
.isPlainObject( outline
) && config
=== undefined ) {
12272 outline
= config
.outline
;
12275 // Configuration initialization
12276 config
= $.extend( { icon
: 'add' }, config
);
12278 // Parent constructor
12279 OO
.ui
.OutlineControlsWidget
.parent
.call( this, config
);
12281 // Mixin constructors
12282 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12283 OO
.ui
.mixin
.IconElement
.call( this, config
);
12286 this.outline
= outline
;
12287 this.$movers
= $( '<div>' );
12288 this.upButton
= new OO
.ui
.ButtonWidget( {
12291 title
: OO
.ui
.msg( 'ooui-outline-control-move-up' )
12293 this.downButton
= new OO
.ui
.ButtonWidget( {
12296 title
: OO
.ui
.msg( 'ooui-outline-control-move-down' )
12298 this.removeButton
= new OO
.ui
.ButtonWidget( {
12301 title
: OO
.ui
.msg( 'ooui-outline-control-remove' )
12303 this.abilities
= { move: true, remove
: true };
12306 outline
.connect( this, {
12307 select
: 'onOutlineChange',
12308 add
: 'onOutlineChange',
12309 remove
: 'onOutlineChange'
12311 this.upButton
.connect( this, { click
: [ 'emit', 'move', -1 ] } );
12312 this.downButton
.connect( this, { click
: [ 'emit', 'move', 1 ] } );
12313 this.removeButton
.connect( this, { click
: [ 'emit', 'remove' ] } );
12316 this.$element
.addClass( 'oo-ui-outlineControlsWidget' );
12317 this.$group
.addClass( 'oo-ui-outlineControlsWidget-items' );
12319 .addClass( 'oo-ui-outlineControlsWidget-movers' )
12320 .append( this.removeButton
.$element
, this.upButton
.$element
, this.downButton
.$element
);
12321 this.$element
.append( this.$icon
, this.$group
, this.$movers
);
12322 this.setAbilities( config
.abilities
|| {} );
12327 OO
.inheritClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.Widget
);
12328 OO
.mixinClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.mixin
.GroupElement
);
12329 OO
.mixinClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.mixin
.IconElement
);
12335 * @param {number} places Number of places to move
12347 * @param {Object} abilities List of abilties
12348 * @param {boolean} [abilities.move] Allow moving movable items
12349 * @param {boolean} [abilities.remove] Allow removing removable items
12351 OO
.ui
.OutlineControlsWidget
.prototype.setAbilities = function ( abilities
) {
12354 for ( ability
in this.abilities
) {
12355 if ( abilities
[ ability
] !== undefined ) {
12356 this.abilities
[ ability
] = !!abilities
[ ability
];
12360 this.onOutlineChange();
12365 * Handle outline change events.
12367 OO
.ui
.OutlineControlsWidget
.prototype.onOutlineChange = function () {
12368 var i
, len
, firstMovable
, lastMovable
,
12369 items
= this.outline
.getItems(),
12370 selectedItem
= this.outline
.getSelectedItem(),
12371 movable
= this.abilities
.move && selectedItem
&& selectedItem
.isMovable(),
12372 removable
= this.abilities
.remove
&& selectedItem
&& selectedItem
.isRemovable();
12376 len
= items
.length
;
12377 while ( ++i
< len
) {
12378 if ( items
[ i
].isMovable() ) {
12379 firstMovable
= items
[ i
];
12385 if ( items
[ i
].isMovable() ) {
12386 lastMovable
= items
[ i
];
12391 this.upButton
.setDisabled( !movable
|| selectedItem
=== firstMovable
);
12392 this.downButton
.setDisabled( !movable
|| selectedItem
=== lastMovable
);
12393 this.removeButton
.setDisabled( !removable
);
12397 * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
12399 * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
12400 * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
12404 * @extends OO.ui.DecoratedOptionWidget
12407 * @param {Object} [config] Configuration options
12408 * @cfg {number} [level] Indentation level
12409 * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
12411 OO
.ui
.OutlineOptionWidget
= function OoUiOutlineOptionWidget( config
) {
12412 // Configuration initialization
12413 config
= config
|| {};
12415 // Parent constructor
12416 OO
.ui
.OutlineOptionWidget
.parent
.call( this, config
);
12420 this.movable
= !!config
.movable
;
12421 this.removable
= !!config
.removable
;
12424 this.$element
.addClass( 'oo-ui-outlineOptionWidget' );
12425 this.setLevel( config
.level
);
12430 OO
.inheritClass( OO
.ui
.OutlineOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
12432 /* Static Properties */
12434 OO
.ui
.OutlineOptionWidget
.static.highlightable
= false;
12436 OO
.ui
.OutlineOptionWidget
.static.scrollIntoViewOnSelect
= true;
12438 OO
.ui
.OutlineOptionWidget
.static.levelClass
= 'oo-ui-outlineOptionWidget-level-';
12440 OO
.ui
.OutlineOptionWidget
.static.levels
= 3;
12445 * Check if item is movable.
12447 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
12449 * @return {boolean} Item is movable
12451 OO
.ui
.OutlineOptionWidget
.prototype.isMovable = function () {
12452 return this.movable
;
12456 * Check if item is removable.
12458 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
12460 * @return {boolean} Item is removable
12462 OO
.ui
.OutlineOptionWidget
.prototype.isRemovable = function () {
12463 return this.removable
;
12467 * Get indentation level.
12469 * @return {number} Indentation level
12471 OO
.ui
.OutlineOptionWidget
.prototype.getLevel = function () {
12478 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
12480 * @param {boolean} movable Item is movable
12483 OO
.ui
.OutlineOptionWidget
.prototype.setMovable = function ( movable
) {
12484 this.movable
= !!movable
;
12485 this.updateThemeClasses();
12490 * Set removability.
12492 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
12494 * @param {boolean} removable Item is removable
12497 OO
.ui
.OutlineOptionWidget
.prototype.setRemovable = function ( removable
) {
12498 this.removable
= !!removable
;
12499 this.updateThemeClasses();
12504 * Set indentation level.
12506 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
12509 OO
.ui
.OutlineOptionWidget
.prototype.setLevel = function ( level
) {
12510 var levels
= this.constructor.static.levels
,
12511 levelClass
= this.constructor.static.levelClass
,
12514 this.level
= level
? Math
.max( 0, Math
.min( levels
- 1, level
) ) : 0;
12516 if ( this.level
=== i
) {
12517 this.$element
.addClass( levelClass
+ i
);
12519 this.$element
.removeClass( levelClass
+ i
);
12522 this.updateThemeClasses();
12528 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
12529 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
12531 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
12534 * @extends OO.ui.SelectWidget
12535 * @mixins OO.ui.mixin.TabIndexedElement
12538 * @param {Object} [config] Configuration options
12540 OO
.ui
.OutlineSelectWidget
= function OoUiOutlineSelectWidget( config
) {
12541 // Parent constructor
12542 OO
.ui
.OutlineSelectWidget
.parent
.call( this, config
);
12544 // Mixin constructors
12545 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
12548 this.$element
.on( {
12549 focus
: this.bindKeyDownListener
.bind( this ),
12550 blur
: this.unbindKeyDownListener
.bind( this )
12554 this.$element
.addClass( 'oo-ui-outlineSelectWidget' );
12559 OO
.inheritClass( OO
.ui
.OutlineSelectWidget
, OO
.ui
.SelectWidget
);
12560 OO
.mixinClass( OO
.ui
.OutlineSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
12563 * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
12564 * can be selected and configured with data. The class is
12565 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
12566 * [OOjs UI documentation on MediaWiki] [1] for more information.
12568 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
12571 * @extends OO.ui.DecoratedOptionWidget
12572 * @mixins OO.ui.mixin.ButtonElement
12573 * @mixins OO.ui.mixin.TabIndexedElement
12574 * @mixins OO.ui.mixin.TitledElement
12577 * @param {Object} [config] Configuration options
12579 OO
.ui
.ButtonOptionWidget
= function OoUiButtonOptionWidget( config
) {
12580 // Configuration initialization
12581 config
= config
|| {};
12583 // Parent constructor
12584 OO
.ui
.ButtonOptionWidget
.parent
.call( this, config
);
12586 // Mixin constructors
12587 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
12588 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
12589 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, {
12590 $tabIndexed
: this.$button
,
12595 this.$element
.addClass( 'oo-ui-buttonOptionWidget' );
12596 this.$button
.append( this.$element
.contents() );
12597 this.$element
.append( this.$button
);
12602 OO
.inheritClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
12603 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.mixin
.ButtonElement
);
12604 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.mixin
.TitledElement
);
12605 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.mixin
.TabIndexedElement
);
12607 /* Static Properties */
12609 // Allow button mouse down events to pass through so they can be handled by the parent select widget
12610 OO
.ui
.ButtonOptionWidget
.static.cancelButtonMouseDownEvents
= false;
12612 OO
.ui
.ButtonOptionWidget
.static.highlightable
= false;
12619 OO
.ui
.ButtonOptionWidget
.prototype.setSelected = function ( state
) {
12620 OO
.ui
.ButtonOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
12622 if ( this.constructor.static.selectable
) {
12623 this.setActive( state
);
12630 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
12631 * button options and is used together with
12632 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
12633 * highlighting, choosing, and selecting mutually exclusive options. Please see
12634 * the [OOjs UI documentation on MediaWiki] [1] for more information.
12637 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
12638 * var option1 = new OO.ui.ButtonOptionWidget( {
12640 * label: 'Option 1',
12641 * title: 'Button option 1'
12644 * var option2 = new OO.ui.ButtonOptionWidget( {
12646 * label: 'Option 2',
12647 * title: 'Button option 2'
12650 * var option3 = new OO.ui.ButtonOptionWidget( {
12652 * label: 'Option 3',
12653 * title: 'Button option 3'
12656 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
12657 * items: [ option1, option2, option3 ]
12659 * $( 'body' ).append( buttonSelect.$element );
12661 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12664 * @extends OO.ui.SelectWidget
12665 * @mixins OO.ui.mixin.TabIndexedElement
12668 * @param {Object} [config] Configuration options
12670 OO
.ui
.ButtonSelectWidget
= function OoUiButtonSelectWidget( config
) {
12671 // Parent constructor
12672 OO
.ui
.ButtonSelectWidget
.parent
.call( this, config
);
12674 // Mixin constructors
12675 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
12678 this.$element
.on( {
12679 focus
: this.bindKeyDownListener
.bind( this ),
12680 blur
: this.unbindKeyDownListener
.bind( this )
12684 this.$element
.addClass( 'oo-ui-buttonSelectWidget' );
12689 OO
.inheritClass( OO
.ui
.ButtonSelectWidget
, OO
.ui
.SelectWidget
);
12690 OO
.mixinClass( OO
.ui
.ButtonSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
12693 * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
12695 * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
12696 * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
12700 * @extends OO.ui.OptionWidget
12703 * @param {Object} [config] Configuration options
12705 OO
.ui
.TabOptionWidget
= function OoUiTabOptionWidget( config
) {
12706 // Configuration initialization
12707 config
= config
|| {};
12709 // Parent constructor
12710 OO
.ui
.TabOptionWidget
.parent
.call( this, config
);
12713 this.$element
.addClass( 'oo-ui-tabOptionWidget' );
12718 OO
.inheritClass( OO
.ui
.TabOptionWidget
, OO
.ui
.OptionWidget
);
12720 /* Static Properties */
12722 OO
.ui
.TabOptionWidget
.static.highlightable
= false;
12725 * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
12727 * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
12730 * @extends OO.ui.SelectWidget
12731 * @mixins OO.ui.mixin.TabIndexedElement
12734 * @param {Object} [config] Configuration options
12736 OO
.ui
.TabSelectWidget
= function OoUiTabSelectWidget( config
) {
12737 // Parent constructor
12738 OO
.ui
.TabSelectWidget
.parent
.call( this, config
);
12740 // Mixin constructors
12741 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
12744 this.$element
.on( {
12745 focus
: this.bindKeyDownListener
.bind( this ),
12746 blur
: this.unbindKeyDownListener
.bind( this )
12750 this.$element
.addClass( 'oo-ui-tabSelectWidget' );
12755 OO
.inheritClass( OO
.ui
.TabSelectWidget
, OO
.ui
.SelectWidget
);
12756 OO
.mixinClass( OO
.ui
.TabSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
12759 * CapsuleItemWidgets are used within a {@link OO.ui.CapsuleMultiSelectWidget
12760 * CapsuleMultiSelectWidget} to display the selected items.
12763 * @extends OO.ui.Widget
12764 * @mixins OO.ui.mixin.ItemWidget
12765 * @mixins OO.ui.mixin.IndicatorElement
12766 * @mixins OO.ui.mixin.LabelElement
12767 * @mixins OO.ui.mixin.FlaggedElement
12768 * @mixins OO.ui.mixin.TabIndexedElement
12771 * @param {Object} [config] Configuration options
12773 OO
.ui
.CapsuleItemWidget
= function OoUiCapsuleItemWidget( config
) {
12774 // Configuration initialization
12775 config
= config
|| {};
12777 // Parent constructor
12778 OO
.ui
.CapsuleItemWidget
.parent
.call( this, config
);
12780 // Properties (must be set before mixin constructor calls)
12781 this.$indicator
= $( '<span>' );
12783 // Mixin constructors
12784 OO
.ui
.mixin
.ItemWidget
.call( this );
12785 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$indicator
, indicator
: 'clear' } ) );
12786 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12787 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
12788 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$indicator
} ) );
12791 this.$indicator
.on( {
12792 keydown
: this.onCloseKeyDown
.bind( this ),
12793 click
: this.onCloseClick
.bind( this )
12798 .addClass( 'oo-ui-capsuleItemWidget' )
12799 .append( this.$indicator
, this.$label
);
12804 OO
.inheritClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.Widget
);
12805 OO
.mixinClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.mixin
.ItemWidget
);
12806 OO
.mixinClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.mixin
.IndicatorElement
);
12807 OO
.mixinClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.mixin
.LabelElement
);
12808 OO
.mixinClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.mixin
.FlaggedElement
);
12809 OO
.mixinClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.mixin
.TabIndexedElement
);
12814 * Handle close icon clicks
12815 * @param {jQuery.Event} event
12817 OO
.ui
.CapsuleItemWidget
.prototype.onCloseClick = function () {
12818 var element
= this.getElementGroup();
12820 if ( !this.isDisabled() && element
&& $.isFunction( element
.removeItems
) ) {
12821 element
.removeItems( [ this ] );
12827 * Handle close keyboard events
12828 * @param {jQuery.Event} event Key down event
12830 OO
.ui
.CapsuleItemWidget
.prototype.onCloseKeyDown = function ( e
) {
12831 if ( !this.isDisabled() && $.isFunction( this.getElementGroup().removeItems
) ) {
12832 switch ( e
.which
) {
12833 case OO
.ui
.Keys
.ENTER
:
12834 case OO
.ui
.Keys
.BACKSPACE
:
12835 case OO
.ui
.Keys
.SPACE
:
12836 this.getElementGroup().removeItems( [ this ] );
12843 * CapsuleMultiSelectWidgets are something like a {@link OO.ui.ComboBoxInputWidget combo box widget}
12844 * that allows for selecting multiple values.
12846 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
12849 * // Example: A CapsuleMultiSelectWidget.
12850 * var capsule = new OO.ui.CapsuleMultiSelectWidget( {
12851 * label: 'CapsuleMultiSelectWidget',
12852 * selected: [ 'Option 1', 'Option 3' ],
12855 * new OO.ui.MenuOptionWidget( {
12856 * data: 'Option 1',
12857 * label: 'Option One'
12859 * new OO.ui.MenuOptionWidget( {
12860 * data: 'Option 2',
12861 * label: 'Option Two'
12863 * new OO.ui.MenuOptionWidget( {
12864 * data: 'Option 3',
12865 * label: 'Option Three'
12867 * new OO.ui.MenuOptionWidget( {
12868 * data: 'Option 4',
12869 * label: 'Option Four'
12871 * new OO.ui.MenuOptionWidget( {
12872 * data: 'Option 5',
12873 * label: 'Option Five'
12878 * $( 'body' ).append( capsule.$element );
12880 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12883 * @extends OO.ui.Widget
12884 * @mixins OO.ui.mixin.TabIndexedElement
12885 * @mixins OO.ui.mixin.GroupElement
12888 * @param {Object} [config] Configuration options
12889 * @cfg {boolean} [allowArbitrary=false] Allow data items to be added even if not present in the menu.
12890 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
12891 * @cfg {Object} [popup] Configuration options to pass to the {@link OO.ui.PopupWidget popup widget}.
12892 * If specified, this popup will be shown instead of the menu (but the menu
12893 * will still be used for item labels and allowArbitrary=false). The widgets
12894 * in the popup should use this.addItemsFromData() or this.addItems() as necessary.
12895 * @cfg {jQuery} [$overlay] Render the menu or popup into a separate layer.
12896 * This configuration is useful in cases where the expanded menu is larger than
12897 * its containing `<div>`. The specified overlay layer is usually on top of
12898 * the containing `<div>` and has a larger area. By default, the menu uses
12899 * relative positioning.
12901 OO
.ui
.CapsuleMultiSelectWidget
= function OoUiCapsuleMultiSelectWidget( config
) {
12904 // Configuration initialization
12905 config
= config
|| {};
12907 // Parent constructor
12908 OO
.ui
.CapsuleMultiSelectWidget
.parent
.call( this, config
);
12910 // Properties (must be set before mixin constructor calls)
12911 this.$input
= config
.popup
? null : $( '<input>' );
12912 this.$handle
= $( '<div>' );
12914 // Mixin constructors
12915 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12916 if ( config
.popup
) {
12917 config
.popup
= $.extend( {}, config
.popup
, {
12921 OO
.ui
.mixin
.PopupElement
.call( this, config
);
12922 $tabFocus
= $( '<span>' );
12923 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: $tabFocus
} ) );
12927 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
12929 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
12930 OO
.ui
.mixin
.IconElement
.call( this, config
);
12933 this.$content
= $( '<div>' );
12934 this.allowArbitrary
= !!config
.allowArbitrary
;
12935 this.$overlay
= config
.$overlay
|| this.$element
;
12936 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend(
12939 $input
: this.$input
,
12940 $container
: this.$element
,
12941 filterFromInput
: true,
12942 disabled
: this.isDisabled()
12948 if ( this.popup
) {
12950 focus
: this.onFocusForPopup
.bind( this )
12952 this.popup
.$element
.on( 'focusout', this.onPopupFocusOut
.bind( this ) );
12953 if ( this.popup
.$autoCloseIgnore
) {
12954 this.popup
.$autoCloseIgnore
.on( 'focusout', this.onPopupFocusOut
.bind( this ) );
12956 this.popup
.connect( this, {
12957 toggle: function ( visible
) {
12958 $tabFocus
.toggle( !visible
);
12963 focus
: this.onInputFocus
.bind( this ),
12964 blur
: this.onInputBlur
.bind( this ),
12965 'propertychange change click mouseup keydown keyup input cut paste select focus':
12966 OO
.ui
.debounce( this.updateInputSize
.bind( this ) ),
12967 keydown
: this.onKeyDown
.bind( this ),
12968 keypress
: this.onKeyPress
.bind( this )
12971 this.menu
.connect( this, {
12972 choose
: 'onMenuChoose',
12973 add
: 'onMenuItemsChange',
12974 remove
: 'onMenuItemsChange'
12977 mousedown
: this.onMouseDown
.bind( this )
12981 if ( this.$input
) {
12982 this.$input
.prop( 'disabled', this.isDisabled() );
12983 this.$input
.attr( {
12985 'aria-autocomplete': 'list'
12987 this.updateInputSize();
12989 if ( config
.data
) {
12990 this.setItemsFromData( config
.data
);
12992 this.$content
.addClass( 'oo-ui-capsuleMultiSelectWidget-content' )
12993 .append( this.$group
);
12994 this.$group
.addClass( 'oo-ui-capsuleMultiSelectWidget-group' );
12995 this.$handle
.addClass( 'oo-ui-capsuleMultiSelectWidget-handle' )
12996 .append( this.$indicator
, this.$icon
, this.$content
);
12997 this.$element
.addClass( 'oo-ui-capsuleMultiSelectWidget' )
12998 .append( this.$handle
);
12999 if ( this.popup
) {
13000 this.$content
.append( $tabFocus
);
13001 this.$overlay
.append( this.popup
.$element
);
13003 this.$content
.append( this.$input
);
13004 this.$overlay
.append( this.menu
.$element
);
13006 this.onMenuItemsChange();
13011 OO
.inheritClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.Widget
);
13012 OO
.mixinClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.mixin
.GroupElement
);
13013 OO
.mixinClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.mixin
.PopupElement
);
13014 OO
.mixinClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
13015 OO
.mixinClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.mixin
.IndicatorElement
);
13016 OO
.mixinClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.mixin
.IconElement
);
13023 * A change event is emitted when the set of selected items changes.
13025 * @param {Mixed[]} datas Data of the now-selected items
13031 * Construct a OO.ui.CapsuleItemWidget (or a subclass thereof) from given label and data.
13034 * @param {Mixed} data Custom data of any type.
13035 * @param {string} label The label text.
13036 * @return {OO.ui.CapsuleItemWidget}
13038 OO
.ui
.CapsuleMultiSelectWidget
.prototype.createItemWidget = function ( data
, label
) {
13039 return new OO
.ui
.CapsuleItemWidget( { data
: data
, label
: label
} );
13043 * Get the data of the items in the capsule
13044 * @return {Mixed[]}
13046 OO
.ui
.CapsuleMultiSelectWidget
.prototype.getItemsData = function () {
13047 return $.map( this.getItems(), function ( e
) { return e
.data
; } );
13051 * Set the items in the capsule by providing data
13053 * @param {Mixed[]} datas
13054 * @return {OO.ui.CapsuleMultiSelectWidget}
13056 OO
.ui
.CapsuleMultiSelectWidget
.prototype.setItemsFromData = function ( datas
) {
13059 items
= this.getItems();
13061 $.each( datas
, function ( i
, data
) {
13063 item
= menu
.getItemFromData( data
);
13066 label
= item
.label
;
13067 } else if ( widget
.allowArbitrary
) {
13068 label
= String( data
);
13074 for ( j
= 0; j
< items
.length
; j
++ ) {
13075 if ( items
[ j
].data
=== data
&& items
[ j
].label
=== label
) {
13077 items
.splice( j
, 1 );
13082 item
= widget
.createItemWidget( data
, label
);
13084 widget
.addItems( [ item
], i
);
13087 if ( items
.length
) {
13088 widget
.removeItems( items
);
13095 * Add items to the capsule by providing their data
13097 * @param {Mixed[]} datas
13098 * @return {OO.ui.CapsuleMultiSelectWidget}
13100 OO
.ui
.CapsuleMultiSelectWidget
.prototype.addItemsFromData = function ( datas
) {
13105 $.each( datas
, function ( i
, data
) {
13108 if ( !widget
.getItemFromData( data
) ) {
13109 item
= menu
.getItemFromData( data
);
13111 items
.push( widget
.createItemWidget( data
, item
.label
) );
13112 } else if ( widget
.allowArbitrary
) {
13113 items
.push( widget
.createItemWidget( data
, String( data
) ) );
13118 if ( items
.length
) {
13119 this.addItems( items
);
13126 * Remove items by data
13128 * @param {Mixed[]} datas
13129 * @return {OO.ui.CapsuleMultiSelectWidget}
13131 OO
.ui
.CapsuleMultiSelectWidget
.prototype.removeItemsFromData = function ( datas
) {
13135 $.each( datas
, function ( i
, data
) {
13136 var item
= widget
.getItemFromData( data
);
13138 items
.push( item
);
13142 if ( items
.length
) {
13143 this.removeItems( items
);
13152 OO
.ui
.CapsuleMultiSelectWidget
.prototype.addItems = function ( items
) {
13154 oldItems
= this.items
.slice();
13156 OO
.ui
.mixin
.GroupElement
.prototype.addItems
.call( this, items
);
13158 if ( this.items
.length
!== oldItems
.length
) {
13162 for ( i
= 0, l
= oldItems
.length
; same
&& i
< l
; i
++ ) {
13163 same
= same
&& this.items
[ i
] === oldItems
[ i
];
13167 this.emit( 'change', this.getItemsData() );
13168 this.menu
.position();
13177 OO
.ui
.CapsuleMultiSelectWidget
.prototype.removeItems = function ( items
) {
13179 oldItems
= this.items
.slice();
13181 OO
.ui
.mixin
.GroupElement
.prototype.removeItems
.call( this, items
);
13183 if ( this.items
.length
!== oldItems
.length
) {
13187 for ( i
= 0, l
= oldItems
.length
; same
&& i
< l
; i
++ ) {
13188 same
= same
&& this.items
[ i
] === oldItems
[ i
];
13192 this.emit( 'change', this.getItemsData() );
13193 this.menu
.position();
13202 OO
.ui
.CapsuleMultiSelectWidget
.prototype.clearItems = function () {
13203 if ( this.items
.length
) {
13204 OO
.ui
.mixin
.GroupElement
.prototype.clearItems
.call( this );
13205 this.emit( 'change', this.getItemsData() );
13206 this.menu
.position();
13212 * Get the capsule widget's menu.
13213 * @return {OO.ui.MenuSelectWidget} Menu widget
13215 OO
.ui
.CapsuleMultiSelectWidget
.prototype.getMenu = function () {
13220 * Handle focus events
13223 * @param {jQuery.Event} event
13225 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onInputFocus = function () {
13226 if ( !this.isDisabled() ) {
13227 this.menu
.toggle( true );
13232 * Handle blur events
13235 * @param {jQuery.Event} event
13237 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onInputBlur = function () {
13238 if ( this.allowArbitrary
&& this.$input
.val().trim() !== '' ) {
13239 this.addItemsFromData( [ this.$input
.val() ] );
13245 * Handle focus events
13248 * @param {jQuery.Event} event
13250 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onFocusForPopup = function () {
13251 if ( !this.isDisabled() ) {
13252 this.popup
.setSize( this.$handle
.width() );
13253 this.popup
.toggle( true );
13254 this.popup
.$element
.find( '*' )
13255 .filter( function () { return OO
.ui
.isFocusableElement( $( this ), true ); } )
13262 * Handles popup focus out events.
13265 * @param {Event} e Focus out event
13267 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onPopupFocusOut = function () {
13268 var widget
= this.popup
;
13270 setTimeout( function () {
13272 widget
.isVisible() &&
13273 !OO
.ui
.contains( widget
.$element
[ 0 ], document
.activeElement
, true ) &&
13274 ( !widget
.$autoCloseIgnore
|| !widget
.$autoCloseIgnore
.has( document
.activeElement
).length
)
13276 widget
.toggle( false );
13282 * Handle mouse down events.
13285 * @param {jQuery.Event} e Mouse down event
13287 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onMouseDown = function ( e
) {
13288 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
13292 this.updateInputSize();
13297 * Handle key press events.
13300 * @param {jQuery.Event} e Key press event
13302 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onKeyPress = function ( e
) {
13305 if ( !this.isDisabled() ) {
13306 if ( e
.which
=== OO
.ui
.Keys
.ESCAPE
) {
13311 if ( !this.popup
) {
13312 this.menu
.toggle( true );
13313 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
13314 item
= this.menu
.getItemFromLabel( this.$input
.val(), true );
13316 this.addItemsFromData( [ item
.data
] );
13318 } else if ( this.allowArbitrary
&& this.$input
.val().trim() !== '' ) {
13319 this.addItemsFromData( [ this.$input
.val() ] );
13325 // Make sure the input gets resized.
13326 setTimeout( this.updateInputSize
.bind( this ), 0 );
13332 * Handle key down events.
13335 * @param {jQuery.Event} e Key down event
13337 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onKeyDown = function ( e
) {
13338 if ( !this.isDisabled() ) {
13339 // 'keypress' event is not triggered for Backspace
13340 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.$input
.val() === '' ) {
13341 if ( this.items
.length
) {
13342 this.removeItems( this.items
.slice( -1 ) );
13350 * Update the dimensions of the text input field to encompass all available area.
13353 * @param {jQuery.Event} e Event of some sort
13355 OO
.ui
.CapsuleMultiSelectWidget
.prototype.updateInputSize = function () {
13356 var $lastItem
, direction
, contentWidth
, currentWidth
, bestWidth
;
13357 if ( !this.isDisabled() ) {
13358 this.$input
.css( 'width', '1em' );
13359 $lastItem
= this.$group
.children().last();
13360 direction
= OO
.ui
.Element
.static.getDir( this.$handle
);
13361 contentWidth
= this.$input
[ 0 ].scrollWidth
;
13362 currentWidth
= this.$input
.width();
13364 if ( contentWidth
< currentWidth
) {
13365 // All is fine, don't perform expensive calculations
13369 if ( !$lastItem
.length
) {
13370 bestWidth
= this.$content
.innerWidth();
13372 bestWidth
= direction
=== 'ltr' ?
13373 this.$content
.innerWidth() - $lastItem
.position().left
- $lastItem
.outerWidth() :
13374 $lastItem
.position().left
;
13376 // Some safety margin for sanity, because I *really* don't feel like finding out where the few
13377 // pixels this is off by are coming from.
13379 if ( contentWidth
> bestWidth
) {
13380 // This will result in the input getting shifted to the next line
13381 bestWidth
= this.$content
.innerWidth() - 10;
13383 this.$input
.width( Math
.floor( bestWidth
) );
13385 this.menu
.position();
13390 * Handle menu choose events.
13393 * @param {OO.ui.OptionWidget} item Chosen item
13395 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onMenuChoose = function ( item
) {
13396 if ( item
&& item
.isVisible() ) {
13397 this.addItemsFromData( [ item
.getData() ] );
13403 * Handle menu item change events.
13407 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onMenuItemsChange = function () {
13408 this.setItemsFromData( this.getItemsData() );
13409 this.$element
.toggleClass( 'oo-ui-capsuleMultiSelectWidget-empty', this.menu
.isEmpty() );
13413 * Clear the input field
13416 OO
.ui
.CapsuleMultiSelectWidget
.prototype.clearInput = function () {
13417 if ( this.$input
) {
13418 this.$input
.val( '' );
13419 this.updateInputSize();
13421 if ( this.popup
) {
13422 this.popup
.toggle( false );
13424 this.menu
.toggle( false );
13425 this.menu
.selectItem();
13426 this.menu
.highlightItem();
13432 OO
.ui
.CapsuleMultiSelectWidget
.prototype.setDisabled = function ( disabled
) {
13436 OO
.ui
.CapsuleMultiSelectWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13438 if ( this.$input
) {
13439 this.$input
.prop( 'disabled', this.isDisabled() );
13442 this.menu
.setDisabled( this.isDisabled() );
13444 if ( this.popup
) {
13445 this.popup
.setDisabled( this.isDisabled() );
13448 if ( this.items
) {
13449 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13450 this.items
[ i
].updateDisabled();
13460 * @return {OO.ui.CapsuleMultiSelectWidget}
13462 OO
.ui
.CapsuleMultiSelectWidget
.prototype.focus = function () {
13463 if ( !this.isDisabled() ) {
13464 if ( this.popup
) {
13465 this.popup
.setSize( this.$handle
.width() );
13466 this.popup
.toggle( true );
13467 this.popup
.$element
.find( '*' )
13468 .filter( function () { return OO
.ui
.isFocusableElement( $( this ), true ); } )
13472 this.updateInputSize();
13473 this.menu
.toggle( true );
13474 this.$input
.focus();
13481 * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
13482 * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
13483 * OO.ui.mixin.IndicatorElement indicators}.
13484 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13487 * // Example of a file select widget
13488 * var selectFile = new OO.ui.SelectFileWidget();
13489 * $( 'body' ).append( selectFile.$element );
13491 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
13494 * @extends OO.ui.Widget
13495 * @mixins OO.ui.mixin.IconElement
13496 * @mixins OO.ui.mixin.IndicatorElement
13497 * @mixins OO.ui.mixin.PendingElement
13498 * @mixins OO.ui.mixin.LabelElement
13501 * @param {Object} [config] Configuration options
13502 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
13503 * @cfg {string} [placeholder] Text to display when no file is selected.
13504 * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
13505 * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
13506 * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be true.
13507 * @cfg {boolean} [dragDropUI=false] Deprecated alias for showDropTarget
13509 OO
.ui
.SelectFileWidget
= function OoUiSelectFileWidget( config
) {
13512 // TODO: Remove in next release
13513 if ( config
&& config
.dragDropUI
) {
13514 config
.showDropTarget
= true;
13517 // Configuration initialization
13518 config
= $.extend( {
13520 placeholder
: OO
.ui
.msg( 'ooui-selectfile-placeholder' ),
13521 notsupported
: OO
.ui
.msg( 'ooui-selectfile-not-supported' ),
13523 showDropTarget
: false
13526 // Parent constructor
13527 OO
.ui
.SelectFileWidget
.parent
.call( this, config
);
13529 // Mixin constructors
13530 OO
.ui
.mixin
.IconElement
.call( this, config
);
13531 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
13532 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$info
} ) );
13533 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { autoFitLabel
: true } ) );
13536 this.$info
= $( '<span>' );
13539 this.showDropTarget
= config
.showDropTarget
;
13540 this.isSupported
= this.constructor.static.isSupported();
13541 this.currentFile
= null;
13542 if ( Array
.isArray( config
.accept
) ) {
13543 this.accept
= config
.accept
;
13545 this.accept
= null;
13547 this.placeholder
= config
.placeholder
;
13548 this.notsupported
= config
.notsupported
;
13549 this.onFileSelectedHandler
= this.onFileSelected
.bind( this );
13551 this.selectButton
= new OO
.ui
.ButtonWidget( {
13552 classes
: [ 'oo-ui-selectFileWidget-selectButton' ],
13553 label
: OO
.ui
.msg( 'ooui-selectfile-button-select' ),
13554 disabled
: this.disabled
|| !this.isSupported
13557 this.clearButton
= new OO
.ui
.ButtonWidget( {
13558 classes
: [ 'oo-ui-selectFileWidget-clearButton' ],
13561 disabled
: this.disabled
13565 this.selectButton
.$button
.on( {
13566 keypress
: this.onKeyPress
.bind( this )
13568 this.clearButton
.connect( this, {
13569 click
: 'onClearClick'
13571 if ( config
.droppable
) {
13572 dragHandler
= this.onDragEnterOrOver
.bind( this );
13573 this.$element
.on( {
13574 dragenter
: dragHandler
,
13575 dragover
: dragHandler
,
13576 dragleave
: this.onDragLeave
.bind( this ),
13577 drop
: this.onDrop
.bind( this )
13584 this.$label
.addClass( 'oo-ui-selectFileWidget-label' );
13586 .addClass( 'oo-ui-selectFileWidget-info' )
13587 .append( this.$icon
, this.$label
, this.clearButton
.$element
, this.$indicator
);
13589 .addClass( 'oo-ui-selectFileWidget' )
13590 .append( this.$info
, this.selectButton
.$element
);
13591 if ( config
.droppable
&& config
.showDropTarget
) {
13592 this.$dropTarget
= $( '<div>' )
13593 .addClass( 'oo-ui-selectFileWidget-dropTarget' )
13594 .text( OO
.ui
.msg( 'ooui-selectfile-dragdrop-placeholder' ) )
13596 click
: this.onDropTargetClick
.bind( this )
13598 this.$element
.prepend( this.$dropTarget
);
13604 OO
.inheritClass( OO
.ui
.SelectFileWidget
, OO
.ui
.Widget
);
13605 OO
.mixinClass( OO
.ui
.SelectFileWidget
, OO
.ui
.mixin
.IconElement
);
13606 OO
.mixinClass( OO
.ui
.SelectFileWidget
, OO
.ui
.mixin
.IndicatorElement
);
13607 OO
.mixinClass( OO
.ui
.SelectFileWidget
, OO
.ui
.mixin
.PendingElement
);
13608 OO
.mixinClass( OO
.ui
.SelectFileWidget
, OO
.ui
.mixin
.LabelElement
);
13610 /* Static Properties */
13613 * Check if this widget is supported
13616 * @return {boolean}
13618 OO
.ui
.SelectFileWidget
.static.isSupported = function () {
13620 if ( OO
.ui
.SelectFileWidget
.static.isSupportedCache
=== null ) {
13621 $input
= $( '<input type="file">' );
13622 OO
.ui
.SelectFileWidget
.static.isSupportedCache
= $input
[ 0 ].files
!== undefined;
13624 return OO
.ui
.SelectFileWidget
.static.isSupportedCache
;
13627 OO
.ui
.SelectFileWidget
.static.isSupportedCache
= null;
13634 * A change event is emitted when the on/off state of the toggle changes.
13636 * @param {File|null} value New value
13642 * Get the current value of the field
13644 * @return {File|null}
13646 OO
.ui
.SelectFileWidget
.prototype.getValue = function () {
13647 return this.currentFile
;
13651 * Set the current value of the field
13653 * @param {File|null} file File to select
13655 OO
.ui
.SelectFileWidget
.prototype.setValue = function ( file
) {
13656 if ( this.currentFile
!== file
) {
13657 this.currentFile
= file
;
13659 this.emit( 'change', this.currentFile
);
13664 * Focus the widget.
13666 * Focusses the select file button.
13670 OO
.ui
.SelectFileWidget
.prototype.focus = function () {
13671 this.selectButton
.$button
[ 0 ].focus();
13676 * Update the user interface when a file is selected or unselected
13680 OO
.ui
.SelectFileWidget
.prototype.updateUI = function () {
13682 if ( !this.isSupported
) {
13683 this.$element
.addClass( 'oo-ui-selectFileWidget-notsupported' );
13684 this.$element
.removeClass( 'oo-ui-selectFileWidget-empty' );
13685 this.setLabel( this.notsupported
);
13687 this.$element
.addClass( 'oo-ui-selectFileWidget-supported' );
13688 if ( this.currentFile
) {
13689 this.$element
.removeClass( 'oo-ui-selectFileWidget-empty' );
13691 $label
= $label
.add(
13693 .addClass( 'oo-ui-selectFileWidget-fileName' )
13694 .text( this.currentFile
.name
)
13696 if ( this.currentFile
.type
!== '' ) {
13697 $label
= $label
.add(
13699 .addClass( 'oo-ui-selectFileWidget-fileType' )
13700 .text( this.currentFile
.type
)
13703 this.setLabel( $label
);
13705 this.$element
.addClass( 'oo-ui-selectFileWidget-empty' );
13706 this.setLabel( this.placeholder
);
13712 * Add the input to the widget
13716 OO
.ui
.SelectFileWidget
.prototype.addInput = function () {
13717 if ( this.$input
) {
13718 this.$input
.remove();
13721 if ( !this.isSupported
) {
13722 this.$input
= null;
13726 this.$input
= $( '<input type="file">' );
13727 this.$input
.on( 'change', this.onFileSelectedHandler
);
13728 this.$input
.attr( {
13731 if ( this.accept
) {
13732 this.$input
.attr( 'accept', this.accept
.join( ', ' ) );
13734 this.selectButton
.$button
.append( this.$input
);
13738 * Determine if we should accept this file
13741 * @param {string} File MIME type
13742 * @return {boolean}
13744 OO
.ui
.SelectFileWidget
.prototype.isAllowedType = function ( mimeType
) {
13747 if ( !this.accept
|| !mimeType
) {
13751 for ( i
= 0; i
< this.accept
.length
; i
++ ) {
13752 mimeTest
= this.accept
[ i
];
13753 if ( mimeTest
=== mimeType
) {
13755 } else if ( mimeTest
.substr( -2 ) === '/*' ) {
13756 mimeTest
= mimeTest
.substr( 0, mimeTest
.length
- 1 );
13757 if ( mimeType
.substr( 0, mimeTest
.length
) === mimeTest
) {
13767 * Handle file selection from the input
13770 * @param {jQuery.Event} e
13772 OO
.ui
.SelectFileWidget
.prototype.onFileSelected = function ( e
) {
13773 var file
= OO
.getProp( e
.target
, 'files', 0 ) || null;
13775 if ( file
&& !this.isAllowedType( file
.type
) ) {
13779 this.setValue( file
);
13784 * Handle clear button click events.
13788 OO
.ui
.SelectFileWidget
.prototype.onClearClick = function () {
13789 this.setValue( null );
13794 * Handle key press events.
13797 * @param {jQuery.Event} e Key press event
13799 OO
.ui
.SelectFileWidget
.prototype.onKeyPress = function ( e
) {
13800 if ( this.isSupported
&& !this.isDisabled() && this.$input
&&
13801 ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
13803 this.$input
.click();
13809 * Handle drop target click events.
13812 * @param {jQuery.Event} e Key press event
13814 OO
.ui
.SelectFileWidget
.prototype.onDropTargetClick = function () {
13815 if ( this.isSupported
&& !this.isDisabled() && this.$input
) {
13816 this.$input
.click();
13822 * Handle drag enter and over events
13825 * @param {jQuery.Event} e Drag event
13827 OO
.ui
.SelectFileWidget
.prototype.onDragEnterOrOver = function ( e
) {
13829 droppableFile
= false,
13830 dt
= e
.originalEvent
.dataTransfer
;
13832 e
.preventDefault();
13833 e
.stopPropagation();
13835 if ( this.isDisabled() || !this.isSupported
) {
13836 this.$element
.removeClass( 'oo-ui-selectFileWidget-canDrop' );
13837 dt
.dropEffect
= 'none';
13841 // DataTransferItem and File both have a type property, but in Chrome files
13842 // have no information at this point.
13843 itemOrFile
= OO
.getProp( dt
, 'items', 0 ) || OO
.getProp( dt
, 'files', 0 );
13844 if ( itemOrFile
) {
13845 if ( this.isAllowedType( itemOrFile
.type
) ) {
13846 droppableFile
= true;
13848 // dt.types is Array-like, but not an Array
13849 } else if ( Array
.prototype.indexOf
.call( OO
.getProp( dt
, 'types' ) || [], 'Files' ) !== -1 ) {
13850 // File information is not available at this point for security so just assume
13851 // it is acceptable for now.
13852 // https://bugzilla.mozilla.org/show_bug.cgi?id=640534
13853 droppableFile
= true;
13856 this.$element
.toggleClass( 'oo-ui-selectFileWidget-canDrop', droppableFile
);
13857 if ( !droppableFile
) {
13858 dt
.dropEffect
= 'none';
13865 * Handle drag leave events
13868 * @param {jQuery.Event} e Drag event
13870 OO
.ui
.SelectFileWidget
.prototype.onDragLeave = function () {
13871 this.$element
.removeClass( 'oo-ui-selectFileWidget-canDrop' );
13875 * Handle drop events
13878 * @param {jQuery.Event} e Drop event
13880 OO
.ui
.SelectFileWidget
.prototype.onDrop = function ( e
) {
13882 dt
= e
.originalEvent
.dataTransfer
;
13884 e
.preventDefault();
13885 e
.stopPropagation();
13886 this.$element
.removeClass( 'oo-ui-selectFileWidget-canDrop' );
13888 if ( this.isDisabled() || !this.isSupported
) {
13892 file
= OO
.getProp( dt
, 'files', 0 );
13893 if ( file
&& !this.isAllowedType( file
.type
) ) {
13897 this.setValue( file
);
13906 OO
.ui
.SelectFileWidget
.prototype.setDisabled = function ( disabled
) {
13907 OO
.ui
.SelectFileWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13908 if ( this.selectButton
) {
13909 this.selectButton
.setDisabled( disabled
);
13911 if ( this.clearButton
) {
13912 this.clearButton
.setDisabled( disabled
);
13918 * Progress bars visually display the status of an operation, such as a download,
13919 * and can be either determinate or indeterminate:
13921 * - **determinate** process bars show the percent of an operation that is complete.
13923 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
13924 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
13925 * not use percentages.
13927 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
13930 * // Examples of determinate and indeterminate progress bars.
13931 * var progressBar1 = new OO.ui.ProgressBarWidget( {
13934 * var progressBar2 = new OO.ui.ProgressBarWidget();
13936 * // Create a FieldsetLayout to layout progress bars
13937 * var fieldset = new OO.ui.FieldsetLayout;
13938 * fieldset.addItems( [
13939 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
13940 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
13942 * $( 'body' ).append( fieldset.$element );
13945 * @extends OO.ui.Widget
13948 * @param {Object} [config] Configuration options
13949 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
13950 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
13951 * By default, the progress bar is indeterminate.
13953 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
13954 // Configuration initialization
13955 config
= config
|| {};
13957 // Parent constructor
13958 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
13961 this.$bar
= $( '<div>' );
13962 this.progress
= null;
13965 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
13966 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
13969 role
: 'progressbar',
13970 'aria-valuemin': 0,
13971 'aria-valuemax': 100
13973 .addClass( 'oo-ui-progressBarWidget' )
13974 .append( this.$bar
);
13979 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
13981 /* Static Properties */
13983 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
13988 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
13990 * @return {number|boolean} Progress percent
13992 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
13993 return this.progress
;
13997 * Set the percent of the process completed or `false` for an indeterminate process.
13999 * @param {number|boolean} progress Progress percent or `false` for indeterminate
14001 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
14002 this.progress
= progress
;
14004 if ( progress
!== false ) {
14005 this.$bar
.css( 'width', this.progress
+ '%' );
14006 this.$element
.attr( 'aria-valuenow', this.progress
);
14008 this.$bar
.css( 'width', '' );
14009 this.$element
.removeAttr( 'aria-valuenow' );
14011 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress
);
14015 * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
14016 * and a menu of search results, which is displayed beneath the query
14017 * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
14018 * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
14019 * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
14021 * Each time the query is changed, the search result menu is cleared and repopulated. Please see
14022 * the [OOjs UI demos][1] for an example.
14024 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
14027 * @extends OO.ui.Widget
14030 * @param {Object} [config] Configuration options
14031 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
14032 * @cfg {string} [value] Initial query value
14034 OO
.ui
.SearchWidget
= function OoUiSearchWidget( config
) {
14035 // Configuration initialization
14036 config
= config
|| {};
14038 // Parent constructor
14039 OO
.ui
.SearchWidget
.parent
.call( this, config
);
14042 this.query
= new OO
.ui
.TextInputWidget( {
14044 placeholder
: config
.placeholder
,
14045 value
: config
.value
14047 this.results
= new OO
.ui
.SelectWidget();
14048 this.$query
= $( '<div>' );
14049 this.$results
= $( '<div>' );
14052 this.query
.connect( this, {
14053 change
: 'onQueryChange',
14054 enter
: 'onQueryEnter'
14056 this.query
.$input
.on( 'keydown', this.onQueryKeydown
.bind( this ) );
14060 .addClass( 'oo-ui-searchWidget-query' )
14061 .append( this.query
.$element
);
14063 .addClass( 'oo-ui-searchWidget-results' )
14064 .append( this.results
.$element
);
14066 .addClass( 'oo-ui-searchWidget' )
14067 .append( this.$results
, this.$query
);
14072 OO
.inheritClass( OO
.ui
.SearchWidget
, OO
.ui
.Widget
);
14077 * Handle query key down events.
14080 * @param {jQuery.Event} e Key down event
14082 OO
.ui
.SearchWidget
.prototype.onQueryKeydown = function ( e
) {
14083 var highlightedItem
, nextItem
,
14084 dir
= e
.which
=== OO
.ui
.Keys
.DOWN
? 1 : ( e
.which
=== OO
.ui
.Keys
.UP
? -1 : 0 );
14087 highlightedItem
= this.results
.getHighlightedItem();
14088 if ( !highlightedItem
) {
14089 highlightedItem
= this.results
.getSelectedItem();
14091 nextItem
= this.results
.getRelativeSelectableItem( highlightedItem
, dir
);
14092 this.results
.highlightItem( nextItem
);
14093 nextItem
.scrollElementIntoView();
14098 * Handle select widget select events.
14100 * Clears existing results. Subclasses should repopulate items according to new query.
14103 * @param {string} value New value
14105 OO
.ui
.SearchWidget
.prototype.onQueryChange = function () {
14107 this.results
.clearItems();
14111 * Handle select widget enter key events.
14113 * Chooses highlighted item.
14116 * @param {string} value New value
14118 OO
.ui
.SearchWidget
.prototype.onQueryEnter = function () {
14119 var highlightedItem
= this.results
.getHighlightedItem();
14120 if ( highlightedItem
) {
14121 this.results
.chooseItem( highlightedItem
);
14126 * Get the query input.
14128 * @return {OO.ui.TextInputWidget} Query input
14130 OO
.ui
.SearchWidget
.prototype.getQuery = function () {
14135 * Get the search results menu.
14137 * @return {OO.ui.SelectWidget} Menu of search results
14139 OO
.ui
.SearchWidget
.prototype.getResults = function () {
14140 return this.results
;
14144 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
14145 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
14146 * (to adjust the value in increments) to allow the user to enter a number.
14149 * // Example: A NumberInputWidget.
14150 * var numberInput = new OO.ui.NumberInputWidget( {
14151 * label: 'NumberInputWidget',
14152 * input: { value: 5, min: 1, max: 10 }
14154 * $( 'body' ).append( numberInput.$element );
14157 * @extends OO.ui.Widget
14160 * @param {Object} [config] Configuration options
14161 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
14162 * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
14163 * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
14164 * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
14165 * @cfg {number} [min=-Infinity] Minimum allowed value
14166 * @cfg {number} [max=Infinity] Maximum allowed value
14167 * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
14168 * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
14170 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
14171 // Configuration initialization
14172 config
= $.extend( {
14180 // Parent constructor
14181 OO
.ui
.NumberInputWidget
.parent
.call( this, config
);
14184 this.input
= new OO
.ui
.TextInputWidget( $.extend(
14186 disabled
: this.isDisabled()
14190 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
14192 disabled
: this.isDisabled(),
14195 config
.minusButton
,
14197 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
14201 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
14203 disabled
: this.isDisabled(),
14208 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
14214 this.input
.connect( this, {
14215 change
: this.emit
.bind( this, 'change' ),
14216 enter
: this.emit
.bind( this, 'enter' )
14218 this.input
.$input
.on( {
14219 keydown
: this.onKeyDown
.bind( this ),
14220 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
14222 this.plusButton
.connect( this, {
14223 click
: [ 'onButtonClick', +1 ]
14225 this.minusButton
.connect( this, {
14226 click
: [ 'onButtonClick', -1 ]
14230 this.setIsInteger( !!config
.isInteger
);
14231 this.setRange( config
.min
, config
.max
);
14232 this.setStep( config
.step
, config
.pageStep
);
14234 this.$field
= $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
14236 this.minusButton
.$element
,
14237 this.input
.$element
,
14238 this.plusButton
.$element
14240 this.$element
.addClass( 'oo-ui-numberInputWidget' ).append( this.$field
);
14241 this.input
.setValidation( this.validateNumber
.bind( this ) );
14246 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.Widget
);
14251 * A `change` event is emitted when the value of the input changes.
14257 * An `enter` event is emitted when the user presses 'enter' inside the text box.
14265 * Set whether only integers are allowed
14266 * @param {boolean} flag
14268 OO
.ui
.NumberInputWidget
.prototype.setIsInteger = function ( flag
) {
14269 this.isInteger
= !!flag
;
14270 this.input
.setValidityFlag();
14274 * Get whether only integers are allowed
14275 * @return {boolean} Flag value
14277 OO
.ui
.NumberInputWidget
.prototype.getIsInteger = function () {
14278 return this.isInteger
;
14282 * Set the range of allowed values
14283 * @param {number} min Minimum allowed value
14284 * @param {number} max Maximum allowed value
14286 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
14288 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
14292 this.input
.setValidityFlag();
14296 * Get the current range
14297 * @return {number[]} Minimum and maximum values
14299 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
14300 return [ this.min
, this.max
];
14304 * Set the stepping deltas
14305 * @param {number} step Normal step
14306 * @param {number|null} pageStep Page step. If null, 10 * step will be used.
14308 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( step
, pageStep
) {
14310 throw new Error( 'Step value must be positive' );
14312 if ( pageStep
=== null ) {
14313 pageStep
= step
* 10;
14314 } else if ( pageStep
<= 0 ) {
14315 throw new Error( 'Page step value must be positive' );
14318 this.pageStep
= pageStep
;
14322 * Get the current stepping values
14323 * @return {number[]} Step and page step
14325 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
14326 return [ this.step
, this.pageStep
];
14330 * Get the current value of the widget
14333 OO
.ui
.NumberInputWidget
.prototype.getValue = function () {
14334 return this.input
.getValue();
14338 * Get the current value of the widget as a number
14339 * @return {number} May be NaN, or an invalid number
14341 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
14342 return +this.input
.getValue();
14346 * Set the value of the widget
14347 * @param {string} value Invalid values are allowed
14349 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
14350 this.input
.setValue( value
);
14354 * Adjust the value of the widget
14355 * @param {number} delta Adjustment amount
14357 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
14358 var n
, v
= this.getNumericValue();
14361 if ( isNaN( delta
) || !isFinite( delta
) ) {
14362 throw new Error( 'Delta must be a finite number' );
14365 if ( isNaN( v
) ) {
14369 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
14370 if ( this.isInteger
) {
14371 n
= Math
.round( n
);
14376 this.setValue( n
);
14383 * @param {string} value Field value
14384 * @return {boolean}
14386 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
14388 if ( isNaN( n
) || !isFinite( n
) ) {
14392 /*jshint bitwise: false */
14393 if ( this.isInteger
&& ( n
| 0 ) !== n
) {
14396 /*jshint bitwise: true */
14398 if ( n
< this.min
|| n
> this.max
) {
14406 * Handle mouse click events.
14409 * @param {number} dir +1 or -1
14411 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
14412 this.adjustValue( dir
* this.step
);
14416 * Handle mouse wheel events.
14419 * @param {jQuery.Event} event
14421 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
14424 // Standard 'wheel' event
14425 if ( event
.originalEvent
.deltaMode
!== undefined ) {
14426 this.sawWheelEvent
= true;
14428 if ( event
.originalEvent
.deltaY
) {
14429 delta
= -event
.originalEvent
.deltaY
;
14430 } else if ( event
.originalEvent
.deltaX
) {
14431 delta
= event
.originalEvent
.deltaX
;
14434 // Non-standard events
14435 if ( !this.sawWheelEvent
) {
14436 if ( event
.originalEvent
.wheelDeltaX
) {
14437 delta
= -event
.originalEvent
.wheelDeltaX
;
14438 } else if ( event
.originalEvent
.wheelDeltaY
) {
14439 delta
= event
.originalEvent
.wheelDeltaY
;
14440 } else if ( event
.originalEvent
.wheelDelta
) {
14441 delta
= event
.originalEvent
.wheelDelta
;
14442 } else if ( event
.originalEvent
.detail
) {
14443 delta
= -event
.originalEvent
.detail
;
14448 delta
= delta
< 0 ? -1 : 1;
14449 this.adjustValue( delta
* this.step
);
14456 * Handle key down events.
14459 * @param {jQuery.Event} e Key down event
14461 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
14462 if ( !this.isDisabled() ) {
14463 switch ( e
.which
) {
14464 case OO
.ui
.Keys
.UP
:
14465 this.adjustValue( this.step
);
14467 case OO
.ui
.Keys
.DOWN
:
14468 this.adjustValue( -this.step
);
14470 case OO
.ui
.Keys
.PAGEUP
:
14471 this.adjustValue( this.pageStep
);
14473 case OO
.ui
.Keys
.PAGEDOWN
:
14474 this.adjustValue( -this.pageStep
);
14483 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
14485 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
14487 if ( this.input
) {
14488 this.input
.setDisabled( this.isDisabled() );
14490 if ( this.minusButton
) {
14491 this.minusButton
.setDisabled( this.isDisabled() );
14493 if ( this.plusButton
) {
14494 this.plusButton
.setDisabled( this.isDisabled() );
14504 * https://www.mediawiki.org/wiki/OOjs_UI
14506 * Copyright 2011–2016 OOjs UI Team and other contributors.
14507 * Released under the MIT license
14508 * http://oojs.mit-license.org
14510 * Date: 2016-02-02T22:07:00Z
14512 ( function ( OO
) {
14517 * Toolbars are complex interface components that permit users to easily access a variety
14518 * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
14519 * part of the toolbar, but not configured as tools.
14521 * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
14522 * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
14523 * image’), and an icon.
14525 * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
14526 * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
14527 * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
14528 * any order, but each can only appear once in the toolbar.
14530 * The toolbar can be synchronized with the state of the external "application", like a text
14531 * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
14532 * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
14533 * tool would be disabled while the user is not editing a table). A state change is signalled by
14534 * emitting the {@link #event-updateState 'updateState' event}, which calls Tools'
14535 * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
14537 * The following is an example of a basic toolbar.
14540 * // Example of a toolbar
14541 * // Create the toolbar
14542 * var toolFactory = new OO.ui.ToolFactory();
14543 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
14544 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
14546 * // We will be placing status text in this element when tools are used
14547 * var $area = $( '<p>' ).text( 'Toolbar example' );
14549 * // Define the tools that we're going to place in our toolbar
14551 * // Create a class inheriting from OO.ui.Tool
14552 * function SearchTool() {
14553 * SearchTool.parent.apply( this, arguments );
14555 * OO.inheritClass( SearchTool, OO.ui.Tool );
14556 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
14557 * // of 'icon' and 'title' (displayed icon and text).
14558 * SearchTool.static.name = 'search';
14559 * SearchTool.static.icon = 'search';
14560 * SearchTool.static.title = 'Search...';
14561 * // Defines the action that will happen when this tool is selected (clicked).
14562 * SearchTool.prototype.onSelect = function () {
14563 * $area.text( 'Search tool clicked!' );
14564 * // Never display this tool as "active" (selected).
14565 * this.setActive( false );
14567 * SearchTool.prototype.onUpdateState = function () {};
14568 * // Make this tool available in our toolFactory and thus our toolbar
14569 * toolFactory.register( SearchTool );
14571 * // Register two more tools, nothing interesting here
14572 * function SettingsTool() {
14573 * SettingsTool.parent.apply( this, arguments );
14575 * OO.inheritClass( SettingsTool, OO.ui.Tool );
14576 * SettingsTool.static.name = 'settings';
14577 * SettingsTool.static.icon = 'settings';
14578 * SettingsTool.static.title = 'Change settings';
14579 * SettingsTool.prototype.onSelect = function () {
14580 * $area.text( 'Settings tool clicked!' );
14581 * this.setActive( false );
14583 * SettingsTool.prototype.onUpdateState = function () {};
14584 * toolFactory.register( SettingsTool );
14586 * // Register two more tools, nothing interesting here
14587 * function StuffTool() {
14588 * StuffTool.parent.apply( this, arguments );
14590 * OO.inheritClass( StuffTool, OO.ui.Tool );
14591 * StuffTool.static.name = 'stuff';
14592 * StuffTool.static.icon = 'ellipsis';
14593 * StuffTool.static.title = 'More stuff';
14594 * StuffTool.prototype.onSelect = function () {
14595 * $area.text( 'More stuff tool clicked!' );
14596 * this.setActive( false );
14598 * StuffTool.prototype.onUpdateState = function () {};
14599 * toolFactory.register( StuffTool );
14601 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
14602 * // little popup window (a PopupWidget).
14603 * function HelpTool( toolGroup, config ) {
14604 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
14609 * this.popup.$body.append( '<p>I am helpful!</p>' );
14611 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
14612 * HelpTool.static.name = 'help';
14613 * HelpTool.static.icon = 'help';
14614 * HelpTool.static.title = 'Help';
14615 * toolFactory.register( HelpTool );
14617 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
14618 * // used once (but not all defined tools must be used).
14621 * // 'bar' tool groups display tools' icons only, side-by-side.
14623 * include: [ 'search', 'help' ]
14626 * // 'list' tool groups display both the titles and icons, in a dropdown list.
14628 * indicator: 'down',
14630 * include: [ 'settings', 'stuff' ]
14632 * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
14633 * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
14634 * // since it's more complicated to use. (See the next example snippet on this page.)
14637 * // Create some UI around the toolbar and place it in the document
14638 * var frame = new OO.ui.PanelLayout( {
14642 * var contentFrame = new OO.ui.PanelLayout( {
14646 * frame.$element.append(
14647 * toolbar.$element,
14648 * contentFrame.$element.append( $area )
14650 * $( 'body' ).append( frame.$element );
14652 * // Here is where the toolbar is actually built. This must be done after inserting it into the
14654 * toolbar.initialize();
14655 * toolbar.emit( 'updateState' );
14657 * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
14658 * {@link #event-updateState 'updateState' event}.
14661 * // Create the toolbar
14662 * var toolFactory = new OO.ui.ToolFactory();
14663 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
14664 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
14666 * // We will be placing status text in this element when tools are used
14667 * var $area = $( '<p>' ).text( 'Toolbar example' );
14669 * // Define the tools that we're going to place in our toolbar
14671 * // Create a class inheriting from OO.ui.Tool
14672 * function SearchTool() {
14673 * SearchTool.parent.apply( this, arguments );
14675 * OO.inheritClass( SearchTool, OO.ui.Tool );
14676 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
14677 * // of 'icon' and 'title' (displayed icon and text).
14678 * SearchTool.static.name = 'search';
14679 * SearchTool.static.icon = 'search';
14680 * SearchTool.static.title = 'Search...';
14681 * // Defines the action that will happen when this tool is selected (clicked).
14682 * SearchTool.prototype.onSelect = function () {
14683 * $area.text( 'Search tool clicked!' );
14684 * // Never display this tool as "active" (selected).
14685 * this.setActive( false );
14687 * SearchTool.prototype.onUpdateState = function () {};
14688 * // Make this tool available in our toolFactory and thus our toolbar
14689 * toolFactory.register( SearchTool );
14691 * // Register two more tools, nothing interesting here
14692 * function SettingsTool() {
14693 * SettingsTool.parent.apply( this, arguments );
14694 * this.reallyActive = false;
14696 * OO.inheritClass( SettingsTool, OO.ui.Tool );
14697 * SettingsTool.static.name = 'settings';
14698 * SettingsTool.static.icon = 'settings';
14699 * SettingsTool.static.title = 'Change settings';
14700 * SettingsTool.prototype.onSelect = function () {
14701 * $area.text( 'Settings tool clicked!' );
14702 * // Toggle the active state on each click
14703 * this.reallyActive = !this.reallyActive;
14704 * this.setActive( this.reallyActive );
14705 * // To update the menu label
14706 * this.toolbar.emit( 'updateState' );
14708 * SettingsTool.prototype.onUpdateState = function () {};
14709 * toolFactory.register( SettingsTool );
14711 * // Register two more tools, nothing interesting here
14712 * function StuffTool() {
14713 * StuffTool.parent.apply( this, arguments );
14714 * this.reallyActive = false;
14716 * OO.inheritClass( StuffTool, OO.ui.Tool );
14717 * StuffTool.static.name = 'stuff';
14718 * StuffTool.static.icon = 'ellipsis';
14719 * StuffTool.static.title = 'More stuff';
14720 * StuffTool.prototype.onSelect = function () {
14721 * $area.text( 'More stuff tool clicked!' );
14722 * // Toggle the active state on each click
14723 * this.reallyActive = !this.reallyActive;
14724 * this.setActive( this.reallyActive );
14725 * // To update the menu label
14726 * this.toolbar.emit( 'updateState' );
14728 * StuffTool.prototype.onUpdateState = function () {};
14729 * toolFactory.register( StuffTool );
14731 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
14732 * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
14733 * function HelpTool( toolGroup, config ) {
14734 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
14739 * this.popup.$body.append( '<p>I am helpful!</p>' );
14741 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
14742 * HelpTool.static.name = 'help';
14743 * HelpTool.static.icon = 'help';
14744 * HelpTool.static.title = 'Help';
14745 * toolFactory.register( HelpTool );
14747 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
14748 * // used once (but not all defined tools must be used).
14751 * // 'bar' tool groups display tools' icons only, side-by-side.
14753 * include: [ 'search', 'help' ]
14756 * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
14757 * // Menu label indicates which items are selected.
14759 * indicator: 'down',
14760 * include: [ 'settings', 'stuff' ]
14764 * // Create some UI around the toolbar and place it in the document
14765 * var frame = new OO.ui.PanelLayout( {
14769 * var contentFrame = new OO.ui.PanelLayout( {
14773 * frame.$element.append(
14774 * toolbar.$element,
14775 * contentFrame.$element.append( $area )
14777 * $( 'body' ).append( frame.$element );
14779 * // Here is where the toolbar is actually built. This must be done after inserting it into the
14781 * toolbar.initialize();
14782 * toolbar.emit( 'updateState' );
14785 * @extends OO.ui.Element
14786 * @mixins OO.EventEmitter
14787 * @mixins OO.ui.mixin.GroupElement
14790 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
14791 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
14792 * @param {Object} [config] Configuration options
14793 * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
14794 * in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
14796 * @cfg {boolean} [shadow] Add a shadow below the toolbar.
14798 OO
.ui
.Toolbar
= function OoUiToolbar( toolFactory
, toolGroupFactory
, config
) {
14799 // Allow passing positional parameters inside the config object
14800 if ( OO
.isPlainObject( toolFactory
) && config
=== undefined ) {
14801 config
= toolFactory
;
14802 toolFactory
= config
.toolFactory
;
14803 toolGroupFactory
= config
.toolGroupFactory
;
14806 // Configuration initialization
14807 config
= config
|| {};
14809 // Parent constructor
14810 OO
.ui
.Toolbar
.parent
.call( this, config
);
14812 // Mixin constructors
14813 OO
.EventEmitter
.call( this );
14814 OO
.ui
.mixin
.GroupElement
.call( this, config
);
14817 this.toolFactory
= toolFactory
;
14818 this.toolGroupFactory
= toolGroupFactory
;
14821 this.$bar
= $( '<div>' );
14822 this.$actions
= $( '<div>' );
14823 this.initialized
= false;
14824 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
14828 .add( this.$bar
).add( this.$group
).add( this.$actions
)
14829 .on( 'mousedown keydown', this.onPointerDown
.bind( this ) );
14832 this.$group
.addClass( 'oo-ui-toolbar-tools' );
14833 if ( config
.actions
) {
14834 this.$bar
.append( this.$actions
.addClass( 'oo-ui-toolbar-actions' ) );
14837 .addClass( 'oo-ui-toolbar-bar' )
14838 .append( this.$group
, '<div style="clear:both"></div>' );
14839 if ( config
.shadow
) {
14840 this.$bar
.append( '<div class="oo-ui-toolbar-shadow"></div>' );
14842 this.$element
.addClass( 'oo-ui-toolbar' ).append( this.$bar
);
14847 OO
.inheritClass( OO
.ui
.Toolbar
, OO
.ui
.Element
);
14848 OO
.mixinClass( OO
.ui
.Toolbar
, OO
.EventEmitter
);
14849 OO
.mixinClass( OO
.ui
.Toolbar
, OO
.ui
.mixin
.GroupElement
);
14854 * @event updateState
14856 * An 'updateState' event must be emitted on the Toolbar (by calling `toolbar.emit( 'updateState' )`)
14857 * every time the state of the application using the toolbar changes, and an update to the state of
14858 * tools is required.
14860 * @param {Mixed...} data Application-defined parameters
14866 * Get the tool factory.
14868 * @return {OO.ui.ToolFactory} Tool factory
14870 OO
.ui
.Toolbar
.prototype.getToolFactory = function () {
14871 return this.toolFactory
;
14875 * Get the toolgroup factory.
14877 * @return {OO.Factory} Toolgroup factory
14879 OO
.ui
.Toolbar
.prototype.getToolGroupFactory = function () {
14880 return this.toolGroupFactory
;
14884 * Handles mouse down events.
14887 * @param {jQuery.Event} e Mouse down event
14889 OO
.ui
.Toolbar
.prototype.onPointerDown = function ( e
) {
14890 var $closestWidgetToEvent
= $( e
.target
).closest( '.oo-ui-widget' ),
14891 $closestWidgetToToolbar
= this.$element
.closest( '.oo-ui-widget' );
14892 if ( !$closestWidgetToEvent
.length
|| $closestWidgetToEvent
[ 0 ] === $closestWidgetToToolbar
[ 0 ] ) {
14898 * Handle window resize event.
14901 * @param {jQuery.Event} e Window resize event
14903 OO
.ui
.Toolbar
.prototype.onWindowResize = function () {
14904 this.$element
.toggleClass(
14905 'oo-ui-toolbar-narrow',
14906 this.$bar
.width() <= this.narrowThreshold
14911 * Sets up handles and preloads required information for the toolbar to work.
14912 * This must be called after it is attached to a visible document and before doing anything else.
14914 OO
.ui
.Toolbar
.prototype.initialize = function () {
14915 if ( !this.initialized
) {
14916 this.initialized
= true;
14917 this.narrowThreshold
= this.$group
.width() + this.$actions
.width();
14918 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler
);
14919 this.onWindowResize();
14924 * Set up the toolbar.
14926 * The toolbar is set up with a list of toolgroup configurations that specify the type of
14927 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
14928 * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
14929 * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
14931 * @param {Object.<string,Array>} groups List of toolgroup configurations
14932 * @param {Array|string} [groups.include] Tools to include in the toolgroup
14933 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
14934 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
14935 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
14937 OO
.ui
.Toolbar
.prototype.setup = function ( groups
) {
14938 var i
, len
, type
, group
,
14940 defaultType
= 'bar';
14942 // Cleanup previous groups
14945 // Build out new groups
14946 for ( i
= 0, len
= groups
.length
; i
< len
; i
++ ) {
14947 group
= groups
[ i
];
14948 if ( group
.include
=== '*' ) {
14949 // Apply defaults to catch-all groups
14950 if ( group
.type
=== undefined ) {
14951 group
.type
= 'list';
14953 if ( group
.label
=== undefined ) {
14954 group
.label
= OO
.ui
.msg( 'ooui-toolbar-more' );
14957 // Check type has been registered
14958 type
= this.getToolGroupFactory().lookup( group
.type
) ? group
.type
: defaultType
;
14960 this.getToolGroupFactory().create( type
, this, group
)
14963 this.addItems( items
);
14967 * Remove all tools and toolgroups from the toolbar.
14969 OO
.ui
.Toolbar
.prototype.reset = function () {
14974 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
14975 this.items
[ i
].destroy();
14981 * Destroy the toolbar.
14983 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
14984 * this method whenever you are done using a toolbar.
14986 OO
.ui
.Toolbar
.prototype.destroy = function () {
14987 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler
);
14989 this.$element
.remove();
14993 * Check if the tool is available.
14995 * Available tools are ones that have not yet been added to the toolbar.
14997 * @param {string} name Symbolic name of tool
14998 * @return {boolean} Tool is available
15000 OO
.ui
.Toolbar
.prototype.isToolAvailable = function ( name
) {
15001 return !this.tools
[ name
];
15005 * Prevent tool from being used again.
15007 * @param {OO.ui.Tool} tool Tool to reserve
15009 OO
.ui
.Toolbar
.prototype.reserveTool = function ( tool
) {
15010 this.tools
[ tool
.getName() ] = tool
;
15014 * Allow tool to be used again.
15016 * @param {OO.ui.Tool} tool Tool to release
15018 OO
.ui
.Toolbar
.prototype.releaseTool = function ( tool
) {
15019 delete this.tools
[ tool
.getName() ];
15023 * Get accelerator label for tool.
15025 * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
15026 * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
15027 * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
15029 * @param {string} name Symbolic name of tool
15030 * @return {string|undefined} Tool accelerator label if available
15032 OO
.ui
.Toolbar
.prototype.getToolAccelerator = function () {
15037 * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
15038 * Each tool is configured with a static name, title, and icon and is customized with the command to carry
15039 * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
15040 * which creates the tools on demand.
15042 * Every Tool subclass must implement two methods:
15044 * - {@link #onUpdateState}
15045 * - {@link #onSelect}
15047 * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
15048 * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
15049 * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
15051 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
15052 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
15056 * @extends OO.ui.Widget
15057 * @mixins OO.ui.mixin.IconElement
15058 * @mixins OO.ui.mixin.FlaggedElement
15059 * @mixins OO.ui.mixin.TabIndexedElement
15062 * @param {OO.ui.ToolGroup} toolGroup
15063 * @param {Object} [config] Configuration options
15064 * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
15065 * the {@link #static-title static title} property is used.
15067 * The title is used in different ways depending on the type of toolgroup that contains the tool. The
15068 * title is used as a tooltip if the tool is part of a {@link OO.ui.BarToolGroup bar} toolgroup, or as the label text if the tool is
15069 * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
15071 * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
15072 * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
15073 * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
15075 OO
.ui
.Tool
= function OoUiTool( toolGroup
, config
) {
15076 // Allow passing positional parameters inside the config object
15077 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
15078 config
= toolGroup
;
15079 toolGroup
= config
.toolGroup
;
15082 // Configuration initialization
15083 config
= config
|| {};
15085 // Parent constructor
15086 OO
.ui
.Tool
.parent
.call( this, config
);
15089 this.toolGroup
= toolGroup
;
15090 this.toolbar
= this.toolGroup
.getToolbar();
15091 this.active
= false;
15092 this.$title
= $( '<span>' );
15093 this.$accel
= $( '<span>' );
15094 this.$link
= $( '<a>' );
15097 // Mixin constructors
15098 OO
.ui
.mixin
.IconElement
.call( this, config
);
15099 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
15100 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$link
} ) );
15103 this.toolbar
.connect( this, { updateState
: 'onUpdateState' } );
15106 this.$title
.addClass( 'oo-ui-tool-title' );
15108 .addClass( 'oo-ui-tool-accel' )
15110 // This may need to be changed if the key names are ever localized,
15111 // but for now they are essentially written in English
15116 .addClass( 'oo-ui-tool-link' )
15117 .append( this.$icon
, this.$title
, this.$accel
)
15118 .attr( 'role', 'button' );
15120 .data( 'oo-ui-tool', this )
15122 'oo-ui-tool ' + 'oo-ui-tool-name-' +
15123 this.constructor.static.name
.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
15125 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel
)
15126 .append( this.$link
);
15127 this.setTitle( config
.title
|| this.constructor.static.title
);
15132 OO
.inheritClass( OO
.ui
.Tool
, OO
.ui
.Widget
);
15133 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.mixin
.IconElement
);
15134 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.mixin
.FlaggedElement
);
15135 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.mixin
.TabIndexedElement
);
15137 /* Static Properties */
15143 OO
.ui
.Tool
.static.tagName
= 'span';
15146 * Symbolic name of tool.
15148 * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
15149 * also be used when adding tools to toolgroups.
15154 * @property {string}
15156 OO
.ui
.Tool
.static.name
= '';
15159 * Symbolic name of the group.
15161 * The group name is used to associate tools with each other so that they can be selected later by
15162 * a {@link OO.ui.ToolGroup toolgroup}.
15167 * @property {string}
15169 OO
.ui
.Tool
.static.group
= '';
15172 * Tool title text or a function that returns title text. The value of the static property is overridden if the #title config option is used.
15177 * @property {string|Function}
15179 OO
.ui
.Tool
.static.title
= '';
15182 * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
15183 * Normally only the icon is displayed, or only the label if no icon is given.
15187 * @property {boolean}
15189 OO
.ui
.Tool
.static.displayBothIconAndLabel
= false;
15192 * Add tool to catch-all groups automatically.
15194 * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
15195 * can be included in a toolgroup using the wildcard selector, an asterisk (*).
15199 * @property {boolean}
15201 OO
.ui
.Tool
.static.autoAddToCatchall
= true;
15204 * Add tool to named groups automatically.
15206 * By default, tools that are configured with a static ‘group’ property are added
15207 * to that group and will be selected when the symbolic name of the group is specified (e.g., when
15208 * toolgroups include tools by group name).
15211 * @property {boolean}
15214 OO
.ui
.Tool
.static.autoAddToGroup
= true;
15217 * Check if this tool is compatible with given data.
15219 * This is a stub that can be overridden to provide support for filtering tools based on an
15220 * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
15221 * must also call this method so that the compatibility check can be performed.
15225 * @param {Mixed} data Data to check
15226 * @return {boolean} Tool can be used with data
15228 OO
.ui
.Tool
.static.isCompatibleWith = function () {
15235 * Handle the toolbar state being updated. This method is called when the
15236 * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
15237 * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
15238 * depending on application state (usually by calling #setDisabled to enable or disable the tool,
15239 * or #setActive to mark is as currently in-use or not).
15241 * This is an abstract method that must be overridden in a concrete subclass.
15247 OO
.ui
.Tool
.prototype.onUpdateState
= null;
15250 * Handle the tool being selected. This method is called when the user triggers this tool,
15251 * usually by clicking on its label/icon.
15253 * This is an abstract method that must be overridden in a concrete subclass.
15259 OO
.ui
.Tool
.prototype.onSelect
= null;
15262 * Check if the tool is active.
15264 * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
15265 * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
15267 * @return {boolean} Tool is active
15269 OO
.ui
.Tool
.prototype.isActive = function () {
15270 return this.active
;
15274 * Make the tool appear active or inactive.
15276 * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
15277 * appear pressed or not.
15279 * @param {boolean} state Make tool appear active
15281 OO
.ui
.Tool
.prototype.setActive = function ( state
) {
15282 this.active
= !!state
;
15283 if ( this.active
) {
15284 this.$element
.addClass( 'oo-ui-tool-active' );
15286 this.$element
.removeClass( 'oo-ui-tool-active' );
15291 * Set the tool #title.
15293 * @param {string|Function} title Title text or a function that returns text
15296 OO
.ui
.Tool
.prototype.setTitle = function ( title
) {
15297 this.title
= OO
.ui
.resolveMsg( title
);
15298 this.updateTitle();
15303 * Get the tool #title.
15305 * @return {string} Title text
15307 OO
.ui
.Tool
.prototype.getTitle = function () {
15312 * Get the tool's symbolic name.
15314 * @return {string} Symbolic name of tool
15316 OO
.ui
.Tool
.prototype.getName = function () {
15317 return this.constructor.static.name
;
15321 * Update the title.
15323 OO
.ui
.Tool
.prototype.updateTitle = function () {
15324 var titleTooltips
= this.toolGroup
.constructor.static.titleTooltips
,
15325 accelTooltips
= this.toolGroup
.constructor.static.accelTooltips
,
15326 accel
= this.toolbar
.getToolAccelerator( this.constructor.static.name
),
15329 this.$title
.text( this.title
);
15330 this.$accel
.text( accel
);
15332 if ( titleTooltips
&& typeof this.title
=== 'string' && this.title
.length
) {
15333 tooltipParts
.push( this.title
);
15335 if ( accelTooltips
&& typeof accel
=== 'string' && accel
.length
) {
15336 tooltipParts
.push( accel
);
15338 if ( tooltipParts
.length
) {
15339 this.$link
.attr( 'title', tooltipParts
.join( ' ' ) );
15341 this.$link
.removeAttr( 'title' );
15348 * Destroying the tool removes all event handlers and the tool’s DOM elements.
15349 * Call this method whenever you are done using a tool.
15351 OO
.ui
.Tool
.prototype.destroy = function () {
15352 this.toolbar
.disconnect( this );
15353 this.$element
.remove();
15357 * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
15358 * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
15359 * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
15360 * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
15362 * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
15363 * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
15364 * The options `exclude`, `promote`, and `demote` support the same formats.
15366 * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
15367 * please see the [OOjs UI documentation on MediaWiki][1].
15369 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
15373 * @extends OO.ui.Widget
15374 * @mixins OO.ui.mixin.GroupElement
15377 * @param {OO.ui.Toolbar} toolbar
15378 * @param {Object} [config] Configuration options
15379 * @cfg {Array|string} [include] List of tools to include in the toolgroup, see above.
15380 * @cfg {Array|string} [exclude] List of tools to exclude from the toolgroup, see above.
15381 * @cfg {Array|string} [promote] List of tools to promote to the beginning of the toolgroup, see above.
15382 * @cfg {Array|string} [demote] List of tools to demote to the end of the toolgroup, see above.
15383 * This setting is particularly useful when tools have been added to the toolgroup
15384 * en masse (e.g., via the catch-all selector).
15386 OO
.ui
.ToolGroup
= function OoUiToolGroup( toolbar
, config
) {
15387 // Allow passing positional parameters inside the config object
15388 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
15390 toolbar
= config
.toolbar
;
15393 // Configuration initialization
15394 config
= config
|| {};
15396 // Parent constructor
15397 OO
.ui
.ToolGroup
.parent
.call( this, config
);
15399 // Mixin constructors
15400 OO
.ui
.mixin
.GroupElement
.call( this, config
);
15403 this.toolbar
= toolbar
;
15405 this.pressed
= null;
15406 this.autoDisabled
= false;
15407 this.include
= config
.include
|| [];
15408 this.exclude
= config
.exclude
|| [];
15409 this.promote
= config
.promote
|| [];
15410 this.demote
= config
.demote
|| [];
15411 this.onCapturedMouseKeyUpHandler
= this.onCapturedMouseKeyUp
.bind( this );
15414 this.$element
.on( {
15415 mousedown
: this.onMouseKeyDown
.bind( this ),
15416 mouseup
: this.onMouseKeyUp
.bind( this ),
15417 keydown
: this.onMouseKeyDown
.bind( this ),
15418 keyup
: this.onMouseKeyUp
.bind( this ),
15419 focus
: this.onMouseOverFocus
.bind( this ),
15420 blur
: this.onMouseOutBlur
.bind( this ),
15421 mouseover
: this.onMouseOverFocus
.bind( this ),
15422 mouseout
: this.onMouseOutBlur
.bind( this )
15424 this.toolbar
.getToolFactory().connect( this, { register
: 'onToolFactoryRegister' } );
15425 this.aggregate( { disable
: 'itemDisable' } );
15426 this.connect( this, { itemDisable
: 'updateDisabled' } );
15429 this.$group
.addClass( 'oo-ui-toolGroup-tools' );
15431 .addClass( 'oo-ui-toolGroup' )
15432 .append( this.$group
);
15438 OO
.inheritClass( OO
.ui
.ToolGroup
, OO
.ui
.Widget
);
15439 OO
.mixinClass( OO
.ui
.ToolGroup
, OO
.ui
.mixin
.GroupElement
);
15447 /* Static Properties */
15450 * Show labels in tooltips.
15454 * @property {boolean}
15456 OO
.ui
.ToolGroup
.static.titleTooltips
= false;
15459 * Show acceleration labels in tooltips.
15461 * Note: The OOjs UI library does not include an accelerator system, but does contain
15462 * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
15463 * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
15464 * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
15468 * @property {boolean}
15470 OO
.ui
.ToolGroup
.static.accelTooltips
= false;
15473 * Automatically disable the toolgroup when all tools are disabled
15477 * @property {boolean}
15479 OO
.ui
.ToolGroup
.static.autoDisable
= true;
15486 OO
.ui
.ToolGroup
.prototype.isDisabled = function () {
15487 return this.autoDisabled
|| OO
.ui
.ToolGroup
.parent
.prototype.isDisabled
.apply( this, arguments
);
15493 OO
.ui
.ToolGroup
.prototype.updateDisabled = function () {
15494 var i
, item
, allDisabled
= true;
15496 if ( this.constructor.static.autoDisable
) {
15497 for ( i
= this.items
.length
- 1; i
>= 0; i
-- ) {
15498 item
= this.items
[ i
];
15499 if ( !item
.isDisabled() ) {
15500 allDisabled
= false;
15504 this.autoDisabled
= allDisabled
;
15506 OO
.ui
.ToolGroup
.parent
.prototype.updateDisabled
.apply( this, arguments
);
15510 * Handle mouse down and key down events.
15513 * @param {jQuery.Event} e Mouse down or key down event
15515 OO
.ui
.ToolGroup
.prototype.onMouseKeyDown = function ( e
) {
15517 !this.isDisabled() &&
15518 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
15520 this.pressed
= this.getTargetTool( e
);
15521 if ( this.pressed
) {
15522 this.pressed
.setActive( true );
15523 this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler
, true );
15524 this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler
, true );
15531 * Handle captured mouse up and key up events.
15534 * @param {Event} e Mouse up or key up event
15536 OO
.ui
.ToolGroup
.prototype.onCapturedMouseKeyUp = function ( e
) {
15537 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler
, true );
15538 this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler
, true );
15539 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
15540 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
15541 this.onMouseKeyUp( e
);
15545 * Handle mouse up and key up events.
15548 * @param {jQuery.Event} e Mouse up or key up event
15550 OO
.ui
.ToolGroup
.prototype.onMouseKeyUp = function ( e
) {
15551 var tool
= this.getTargetTool( e
);
15554 !this.isDisabled() && this.pressed
&& this.pressed
=== tool
&&
15555 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
15557 this.pressed
.onSelect();
15558 this.pressed
= null;
15562 this.pressed
= null;
15566 * Handle mouse over and focus events.
15569 * @param {jQuery.Event} e Mouse over or focus event
15571 OO
.ui
.ToolGroup
.prototype.onMouseOverFocus = function ( e
) {
15572 var tool
= this.getTargetTool( e
);
15574 if ( this.pressed
&& this.pressed
=== tool
) {
15575 this.pressed
.setActive( true );
15580 * Handle mouse out and blur events.
15583 * @param {jQuery.Event} e Mouse out or blur event
15585 OO
.ui
.ToolGroup
.prototype.onMouseOutBlur = function ( e
) {
15586 var tool
= this.getTargetTool( e
);
15588 if ( this.pressed
&& this.pressed
=== tool
) {
15589 this.pressed
.setActive( false );
15594 * Get the closest tool to a jQuery.Event.
15596 * Only tool links are considered, which prevents other elements in the tool such as popups from
15597 * triggering tool group interactions.
15600 * @param {jQuery.Event} e
15601 * @return {OO.ui.Tool|null} Tool, `null` if none was found
15603 OO
.ui
.ToolGroup
.prototype.getTargetTool = function ( e
) {
15605 $item
= $( e
.target
).closest( '.oo-ui-tool-link' );
15607 if ( $item
.length
) {
15608 tool
= $item
.parent().data( 'oo-ui-tool' );
15611 return tool
&& !tool
.isDisabled() ? tool
: null;
15615 * Handle tool registry register events.
15617 * If a tool is registered after the group is created, we must repopulate the list to account for:
15619 * - a tool being added that may be included
15620 * - a tool already included being overridden
15623 * @param {string} name Symbolic name of tool
15625 OO
.ui
.ToolGroup
.prototype.onToolFactoryRegister = function () {
15630 * Get the toolbar that contains the toolgroup.
15632 * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
15634 OO
.ui
.ToolGroup
.prototype.getToolbar = function () {
15635 return this.toolbar
;
15639 * Add and remove tools based on configuration.
15641 OO
.ui
.ToolGroup
.prototype.populate = function () {
15642 var i
, len
, name
, tool
,
15643 toolFactory
= this.toolbar
.getToolFactory(),
15647 list
= this.toolbar
.getToolFactory().getTools(
15648 this.include
, this.exclude
, this.promote
, this.demote
15651 // Build a list of needed tools
15652 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
15656 toolFactory
.lookup( name
) &&
15657 // Tool is available or is already in this group
15658 ( this.toolbar
.isToolAvailable( name
) || this.tools
[ name
] )
15660 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
15661 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
15662 this.toolbar
.tools
[ name
] = true;
15663 tool
= this.tools
[ name
];
15665 // Auto-initialize tools on first use
15666 this.tools
[ name
] = tool
= toolFactory
.create( name
, this );
15667 tool
.updateTitle();
15669 this.toolbar
.reserveTool( tool
);
15671 names
[ name
] = true;
15674 // Remove tools that are no longer needed
15675 for ( name
in this.tools
) {
15676 if ( !names
[ name
] ) {
15677 this.tools
[ name
].destroy();
15678 this.toolbar
.releaseTool( this.tools
[ name
] );
15679 remove
.push( this.tools
[ name
] );
15680 delete this.tools
[ name
];
15683 if ( remove
.length
) {
15684 this.removeItems( remove
);
15686 // Update emptiness state
15687 if ( add
.length
) {
15688 this.$element
.removeClass( 'oo-ui-toolGroup-empty' );
15690 this.$element
.addClass( 'oo-ui-toolGroup-empty' );
15692 // Re-add tools (moving existing ones to new locations)
15693 this.addItems( add
);
15694 // Disabled state may depend on items
15695 this.updateDisabled();
15699 * Destroy toolgroup.
15701 OO
.ui
.ToolGroup
.prototype.destroy = function () {
15705 this.toolbar
.getToolFactory().disconnect( this );
15706 for ( name
in this.tools
) {
15707 this.toolbar
.releaseTool( this.tools
[ name
] );
15708 this.tools
[ name
].disconnect( this ).destroy();
15709 delete this.tools
[ name
];
15711 this.$element
.remove();
15715 * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
15716 * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
15717 * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
15719 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
15721 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
15724 * @extends OO.Factory
15727 OO
.ui
.ToolFactory
= function OoUiToolFactory() {
15728 // Parent constructor
15729 OO
.ui
.ToolFactory
.parent
.call( this );
15734 OO
.inheritClass( OO
.ui
.ToolFactory
, OO
.Factory
);
15739 * Get tools from the factory
15741 * @param {Array|string} [include] Included tools, see #extract for format
15742 * @param {Array|string} [exclude] Excluded tools, see #extract for format
15743 * @param {Array|string} [promote] Promoted tools, see #extract for format
15744 * @param {Array|string} [demote] Demoted tools, see #extract for format
15745 * @return {string[]} List of tools
15747 OO
.ui
.ToolFactory
.prototype.getTools = function ( include
, exclude
, promote
, demote
) {
15748 var i
, len
, included
, promoted
, demoted
,
15752 // Collect included and not excluded tools
15753 included
= OO
.simpleArrayDifference( this.extract( include
), this.extract( exclude
) );
15756 promoted
= this.extract( promote
, used
);
15757 demoted
= this.extract( demote
, used
);
15760 for ( i
= 0, len
= included
.length
; i
< len
; i
++ ) {
15761 if ( !used
[ included
[ i
] ] ) {
15762 auto
.push( included
[ i
] );
15766 return promoted
.concat( auto
).concat( demoted
);
15770 * Get a flat list of names from a list of names or groups.
15772 * Normally, `collection` is an array of tool specifications. Tools can be specified in the
15775 * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
15776 * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
15777 * tool to a group, use OO.ui.Tool.static.group.)
15779 * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
15780 * catch-all selector `'*'`.
15782 * If `used` is passed, tool names that appear as properties in this object will be considered
15783 * already assigned, and will not be returned even if specified otherwise. The tool names extracted
15784 * by this function call will be added as new properties in the object.
15787 * @param {Array|string} collection List of tools, see above
15788 * @param {Object} [used] Object containing information about used tools, see above
15789 * @return {string[]} List of extracted tool names
15791 OO
.ui
.ToolFactory
.prototype.extract = function ( collection
, used
) {
15792 var i
, len
, item
, name
, tool
,
15795 if ( collection
=== '*' ) {
15796 for ( name
in this.registry
) {
15797 tool
= this.registry
[ name
];
15799 // Only add tools by group name when auto-add is enabled
15800 tool
.static.autoAddToCatchall
&&
15801 // Exclude already used tools
15802 ( !used
|| !used
[ name
] )
15804 names
.push( name
);
15806 used
[ name
] = true;
15810 } else if ( Array
.isArray( collection
) ) {
15811 for ( i
= 0, len
= collection
.length
; i
< len
; i
++ ) {
15812 item
= collection
[ i
];
15813 // Allow plain strings as shorthand for named tools
15814 if ( typeof item
=== 'string' ) {
15815 item
= { name
: item
};
15817 if ( OO
.isPlainObject( item
) ) {
15818 if ( item
.group
) {
15819 for ( name
in this.registry
) {
15820 tool
= this.registry
[ name
];
15822 // Include tools with matching group
15823 tool
.static.group
=== item
.group
&&
15824 // Only add tools by group name when auto-add is enabled
15825 tool
.static.autoAddToGroup
&&
15826 // Exclude already used tools
15827 ( !used
|| !used
[ name
] )
15829 names
.push( name
);
15831 used
[ name
] = true;
15835 // Include tools with matching name and exclude already used tools
15836 } else if ( item
.name
&& ( !used
|| !used
[ item
.name
] ) ) {
15837 names
.push( item
.name
);
15839 used
[ item
.name
] = true;
15849 * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
15850 * specify a symbolic name and be registered with the factory. The following classes are registered by
15853 * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
15854 * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
15855 * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
15857 * See {@link OO.ui.Toolbar toolbars} for an example.
15859 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
15861 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
15863 * @extends OO.Factory
15866 OO
.ui
.ToolGroupFactory
= function OoUiToolGroupFactory() {
15867 var i
, l
, defaultClasses
;
15868 // Parent constructor
15869 OO
.Factory
.call( this );
15871 defaultClasses
= this.constructor.static.getDefaultClasses();
15873 // Register default toolgroups
15874 for ( i
= 0, l
= defaultClasses
.length
; i
< l
; i
++ ) {
15875 this.register( defaultClasses
[ i
] );
15881 OO
.inheritClass( OO
.ui
.ToolGroupFactory
, OO
.Factory
);
15883 /* Static Methods */
15886 * Get a default set of classes to be registered on construction.
15888 * @return {Function[]} Default classes
15890 OO
.ui
.ToolGroupFactory
.static.getDefaultClasses = function () {
15892 OO
.ui
.BarToolGroup
,
15893 OO
.ui
.ListToolGroup
,
15894 OO
.ui
.MenuToolGroup
15899 * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
15900 * with a static name, title, and icon, as well with as any popup configurations. Unlike other tools, popup tools do not require that developers specify
15901 * an #onSelect or #onUpdateState method, as these methods have been implemented already.
15903 * // Example of a popup tool. When selected, a popup tool displays
15904 * // a popup window.
15905 * function HelpTool( toolGroup, config ) {
15906 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
15911 * this.popup.$body.append( '<p>I am helpful!</p>' );
15913 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
15914 * HelpTool.static.name = 'help';
15915 * HelpTool.static.icon = 'help';
15916 * HelpTool.static.title = 'Help';
15917 * toolFactory.register( HelpTool );
15919 * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
15920 * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
15922 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
15926 * @extends OO.ui.Tool
15927 * @mixins OO.ui.mixin.PopupElement
15930 * @param {OO.ui.ToolGroup} toolGroup
15931 * @param {Object} [config] Configuration options
15933 OO
.ui
.PopupTool
= function OoUiPopupTool( toolGroup
, config
) {
15934 // Allow passing positional parameters inside the config object
15935 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
15936 config
= toolGroup
;
15937 toolGroup
= config
.toolGroup
;
15940 // Parent constructor
15941 OO
.ui
.PopupTool
.parent
.call( this, toolGroup
, config
);
15943 // Mixin constructors
15944 OO
.ui
.mixin
.PopupElement
.call( this, config
);
15948 .addClass( 'oo-ui-popupTool' )
15949 .append( this.popup
.$element
);
15954 OO
.inheritClass( OO
.ui
.PopupTool
, OO
.ui
.Tool
);
15955 OO
.mixinClass( OO
.ui
.PopupTool
, OO
.ui
.mixin
.PopupElement
);
15960 * Handle the tool being selected.
15964 OO
.ui
.PopupTool
.prototype.onSelect = function () {
15965 if ( !this.isDisabled() ) {
15966 this.popup
.toggle();
15968 this.setActive( false );
15973 * Handle the toolbar state being updated.
15977 OO
.ui
.PopupTool
.prototype.onUpdateState = function () {
15978 this.setActive( false );
15982 * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
15983 * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
15984 * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
15985 * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
15986 * when the ToolGroupTool is selected.
15988 * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
15990 * function SettingsTool() {
15991 * SettingsTool.parent.apply( this, arguments );
15993 * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
15994 * SettingsTool.static.name = 'settings';
15995 * SettingsTool.static.title = 'Change settings';
15996 * SettingsTool.static.groupConfig = {
15997 * icon: 'settings',
15998 * label: 'ToolGroupTool',
15999 * include: [ 'setting1', 'setting2' ]
16001 * toolFactory.register( SettingsTool );
16003 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
16005 * Please note that this implementation is subject to change per [T74159] [2].
16007 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
16008 * [2]: https://phabricator.wikimedia.org/T74159
16012 * @extends OO.ui.Tool
16015 * @param {OO.ui.ToolGroup} toolGroup
16016 * @param {Object} [config] Configuration options
16018 OO
.ui
.ToolGroupTool
= function OoUiToolGroupTool( toolGroup
, config
) {
16019 // Allow passing positional parameters inside the config object
16020 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
16021 config
= toolGroup
;
16022 toolGroup
= config
.toolGroup
;
16025 // Parent constructor
16026 OO
.ui
.ToolGroupTool
.parent
.call( this, toolGroup
, config
);
16029 this.innerToolGroup
= this.createGroup( this.constructor.static.groupConfig
);
16032 this.innerToolGroup
.connect( this, { disable
: 'onToolGroupDisable' } );
16035 this.$link
.remove();
16037 .addClass( 'oo-ui-toolGroupTool' )
16038 .append( this.innerToolGroup
.$element
);
16043 OO
.inheritClass( OO
.ui
.ToolGroupTool
, OO
.ui
.Tool
);
16045 /* Static Properties */
16048 * Toolgroup configuration.
16050 * The toolgroup configuration consists of the tools to include, as well as an icon and label
16051 * to use for the bar item. Tools can be included by symbolic name, group, or with the
16052 * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
16054 * @property {Object.<string,Array>}
16056 OO
.ui
.ToolGroupTool
.static.groupConfig
= {};
16061 * Handle the tool being selected.
16065 OO
.ui
.ToolGroupTool
.prototype.onSelect = function () {
16066 this.innerToolGroup
.setActive( !this.innerToolGroup
.active
);
16071 * Synchronize disabledness state of the tool with the inner toolgroup.
16074 * @param {boolean} disabled Element is disabled
16076 OO
.ui
.ToolGroupTool
.prototype.onToolGroupDisable = function ( disabled
) {
16077 this.setDisabled( disabled
);
16081 * Handle the toolbar state being updated.
16085 OO
.ui
.ToolGroupTool
.prototype.onUpdateState = function () {
16086 this.setActive( false );
16090 * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
16092 * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
16093 * more information.
16094 * @return {OO.ui.ListToolGroup}
16096 OO
.ui
.ToolGroupTool
.prototype.createGroup = function ( group
) {
16097 if ( group
.include
=== '*' ) {
16098 // Apply defaults to catch-all groups
16099 if ( group
.label
=== undefined ) {
16100 group
.label
= OO
.ui
.msg( 'ooui-toolbar-more' );
16104 return this.toolbar
.getToolGroupFactory().create( 'list', this.toolbar
, group
);
16108 * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
16109 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
16110 * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
16111 * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
16114 * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
16118 * // Example of a BarToolGroup with two tools
16119 * var toolFactory = new OO.ui.ToolFactory();
16120 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
16121 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
16123 * // We will be placing status text in this element when tools are used
16124 * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
16126 * // Define the tools that we're going to place in our toolbar
16128 * // Create a class inheriting from OO.ui.Tool
16129 * function SearchTool() {
16130 * SearchTool.parent.apply( this, arguments );
16132 * OO.inheritClass( SearchTool, OO.ui.Tool );
16133 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
16134 * // of 'icon' and 'title' (displayed icon and text).
16135 * SearchTool.static.name = 'search';
16136 * SearchTool.static.icon = 'search';
16137 * SearchTool.static.title = 'Search...';
16138 * // Defines the action that will happen when this tool is selected (clicked).
16139 * SearchTool.prototype.onSelect = function () {
16140 * $area.text( 'Search tool clicked!' );
16141 * // Never display this tool as "active" (selected).
16142 * this.setActive( false );
16144 * SearchTool.prototype.onUpdateState = function () {};
16145 * // Make this tool available in our toolFactory and thus our toolbar
16146 * toolFactory.register( SearchTool );
16148 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
16149 * // little popup window (a PopupWidget).
16150 * function HelpTool( toolGroup, config ) {
16151 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
16156 * this.popup.$body.append( '<p>I am helpful!</p>' );
16158 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
16159 * HelpTool.static.name = 'help';
16160 * HelpTool.static.icon = 'help';
16161 * HelpTool.static.title = 'Help';
16162 * toolFactory.register( HelpTool );
16164 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
16165 * // used once (but not all defined tools must be used).
16168 * // 'bar' tool groups display tools by icon only
16170 * include: [ 'search', 'help' ]
16174 * // Create some UI around the toolbar and place it in the document
16175 * var frame = new OO.ui.PanelLayout( {
16179 * var contentFrame = new OO.ui.PanelLayout( {
16183 * frame.$element.append(
16184 * toolbar.$element,
16185 * contentFrame.$element.append( $area )
16187 * $( 'body' ).append( frame.$element );
16189 * // Here is where the toolbar is actually built. This must be done after inserting it into the
16191 * toolbar.initialize();
16193 * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
16194 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
16196 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
16199 * @extends OO.ui.ToolGroup
16202 * @param {OO.ui.Toolbar} toolbar
16203 * @param {Object} [config] Configuration options
16205 OO
.ui
.BarToolGroup
= function OoUiBarToolGroup( toolbar
, config
) {
16206 // Allow passing positional parameters inside the config object
16207 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
16209 toolbar
= config
.toolbar
;
16212 // Parent constructor
16213 OO
.ui
.BarToolGroup
.parent
.call( this, toolbar
, config
);
16216 this.$element
.addClass( 'oo-ui-barToolGroup' );
16221 OO
.inheritClass( OO
.ui
.BarToolGroup
, OO
.ui
.ToolGroup
);
16223 /* Static Properties */
16225 OO
.ui
.BarToolGroup
.static.titleTooltips
= true;
16227 OO
.ui
.BarToolGroup
.static.accelTooltips
= true;
16229 OO
.ui
.BarToolGroup
.static.name
= 'bar';
16232 * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
16233 * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
16234 * optional icon and label. This class can be used for other base classes that also use this functionality.
16238 * @extends OO.ui.ToolGroup
16239 * @mixins OO.ui.mixin.IconElement
16240 * @mixins OO.ui.mixin.IndicatorElement
16241 * @mixins OO.ui.mixin.LabelElement
16242 * @mixins OO.ui.mixin.TitledElement
16243 * @mixins OO.ui.mixin.ClippableElement
16244 * @mixins OO.ui.mixin.TabIndexedElement
16247 * @param {OO.ui.Toolbar} toolbar
16248 * @param {Object} [config] Configuration options
16249 * @cfg {string} [header] Text to display at the top of the popup
16251 OO
.ui
.PopupToolGroup
= function OoUiPopupToolGroup( toolbar
, config
) {
16252 // Allow passing positional parameters inside the config object
16253 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
16255 toolbar
= config
.toolbar
;
16258 // Configuration initialization
16259 config
= config
|| {};
16261 // Parent constructor
16262 OO
.ui
.PopupToolGroup
.parent
.call( this, toolbar
, config
);
16265 this.active
= false;
16266 this.dragging
= false;
16267 this.onBlurHandler
= this.onBlur
.bind( this );
16268 this.$handle
= $( '<span>' );
16270 // Mixin constructors
16271 OO
.ui
.mixin
.IconElement
.call( this, config
);
16272 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
16273 OO
.ui
.mixin
.LabelElement
.call( this, config
);
16274 OO
.ui
.mixin
.TitledElement
.call( this, config
);
16275 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
16276 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
16280 keydown
: this.onHandleMouseKeyDown
.bind( this ),
16281 keyup
: this.onHandleMouseKeyUp
.bind( this ),
16282 mousedown
: this.onHandleMouseKeyDown
.bind( this ),
16283 mouseup
: this.onHandleMouseKeyUp
.bind( this )
16288 .addClass( 'oo-ui-popupToolGroup-handle' )
16289 .append( this.$icon
, this.$label
, this.$indicator
);
16290 // If the pop-up should have a header, add it to the top of the toolGroup.
16291 // Note: If this feature is useful for other widgets, we could abstract it into an
16292 // OO.ui.HeaderedElement mixin constructor.
16293 if ( config
.header
!== undefined ) {
16295 .prepend( $( '<span>' )
16296 .addClass( 'oo-ui-popupToolGroup-header' )
16297 .text( config
.header
)
16301 .addClass( 'oo-ui-popupToolGroup' )
16302 .prepend( this.$handle
);
16307 OO
.inheritClass( OO
.ui
.PopupToolGroup
, OO
.ui
.ToolGroup
);
16308 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.IconElement
);
16309 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.IndicatorElement
);
16310 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.LabelElement
);
16311 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.TitledElement
);
16312 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.ClippableElement
);
16313 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.TabIndexedElement
);
16320 OO
.ui
.PopupToolGroup
.prototype.setDisabled = function () {
16322 OO
.ui
.PopupToolGroup
.parent
.prototype.setDisabled
.apply( this, arguments
);
16324 if ( this.isDisabled() && this.isElementAttached() ) {
16325 this.setActive( false );
16330 * Handle focus being lost.
16332 * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
16335 * @param {jQuery.Event} e Mouse up or key up event
16337 OO
.ui
.PopupToolGroup
.prototype.onBlur = function ( e
) {
16338 // Only deactivate when clicking outside the dropdown element
16339 if ( $( e
.target
).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element
[ 0 ] ) {
16340 this.setActive( false );
16347 OO
.ui
.PopupToolGroup
.prototype.onMouseKeyUp = function ( e
) {
16348 // Only close toolgroup when a tool was actually selected
16350 !this.isDisabled() && this.pressed
&& this.pressed
=== this.getTargetTool( e
) &&
16351 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
16353 this.setActive( false );
16355 return OO
.ui
.PopupToolGroup
.parent
.prototype.onMouseKeyUp
.call( this, e
);
16359 * Handle mouse up and key up events.
16362 * @param {jQuery.Event} e Mouse up or key up event
16364 OO
.ui
.PopupToolGroup
.prototype.onHandleMouseKeyUp = function ( e
) {
16366 !this.isDisabled() &&
16367 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
16374 * Handle mouse down and key down events.
16377 * @param {jQuery.Event} e Mouse down or key down event
16379 OO
.ui
.PopupToolGroup
.prototype.onHandleMouseKeyDown = function ( e
) {
16381 !this.isDisabled() &&
16382 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
16384 this.setActive( !this.active
);
16390 * Switch into 'active' mode.
16392 * When active, the popup is visible. A mouseup event anywhere in the document will trigger
16395 OO
.ui
.PopupToolGroup
.prototype.setActive = function ( value
) {
16396 var containerWidth
, containerLeft
;
16398 if ( this.active
!== value
) {
16399 this.active
= value
;
16401 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler
, true );
16402 this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler
, true );
16404 this.$clippable
.css( 'left', '' );
16405 // Try anchoring the popup to the left first
16406 this.$element
.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
16407 this.toggleClipping( true );
16408 if ( this.isClippedHorizontally() ) {
16409 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
16410 this.toggleClipping( false );
16412 .removeClass( 'oo-ui-popupToolGroup-left' )
16413 .addClass( 'oo-ui-popupToolGroup-right' );
16414 this.toggleClipping( true );
16416 if ( this.isClippedHorizontally() ) {
16417 // Anchoring to the right also caused the popup to clip, so just make it fill the container
16418 containerWidth
= this.$clippableScrollableContainer
.width();
16419 containerLeft
= this.$clippableScrollableContainer
.offset().left
;
16421 this.toggleClipping( false );
16422 this.$element
.removeClass( 'oo-ui-popupToolGroup-right' );
16424 this.$clippable
.css( {
16425 left
: -( this.$element
.offset().left
- containerLeft
),
16426 width
: containerWidth
16430 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler
, true );
16431 this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler
, true );
16432 this.$element
.removeClass(
16433 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
16435 this.toggleClipping( false );
16441 * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
16442 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
16443 * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
16444 * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
16445 * with a label, icon, indicator, header, and title.
16447 * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
16448 * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
16449 * users to collapse the list again.
16451 * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
16452 * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
16453 * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
16456 * // Example of a ListToolGroup
16457 * var toolFactory = new OO.ui.ToolFactory();
16458 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
16459 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
16461 * // Configure and register two tools
16462 * function SettingsTool() {
16463 * SettingsTool.parent.apply( this, arguments );
16465 * OO.inheritClass( SettingsTool, OO.ui.Tool );
16466 * SettingsTool.static.name = 'settings';
16467 * SettingsTool.static.icon = 'settings';
16468 * SettingsTool.static.title = 'Change settings';
16469 * SettingsTool.prototype.onSelect = function () {
16470 * this.setActive( false );
16472 * SettingsTool.prototype.onUpdateState = function () {};
16473 * toolFactory.register( SettingsTool );
16474 * // Register two more tools, nothing interesting here
16475 * function StuffTool() {
16476 * StuffTool.parent.apply( this, arguments );
16478 * OO.inheritClass( StuffTool, OO.ui.Tool );
16479 * StuffTool.static.name = 'stuff';
16480 * StuffTool.static.icon = 'search';
16481 * StuffTool.static.title = 'Change the world';
16482 * StuffTool.prototype.onSelect = function () {
16483 * this.setActive( false );
16485 * StuffTool.prototype.onUpdateState = function () {};
16486 * toolFactory.register( StuffTool );
16489 * // Configurations for list toolgroup.
16491 * label: 'ListToolGroup',
16492 * indicator: 'down',
16493 * icon: 'ellipsis',
16494 * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
16495 * header: 'This is the header',
16496 * include: [ 'settings', 'stuff' ],
16497 * allowCollapse: ['stuff']
16501 * // Create some UI around the toolbar and place it in the document
16502 * var frame = new OO.ui.PanelLayout( {
16506 * frame.$element.append(
16509 * $( 'body' ).append( frame.$element );
16510 * // Build the toolbar. This must be done after the toolbar has been appended to the document.
16511 * toolbar.initialize();
16513 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
16515 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
16518 * @extends OO.ui.PopupToolGroup
16521 * @param {OO.ui.Toolbar} toolbar
16522 * @param {Object} [config] Configuration options
16523 * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
16524 * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
16525 * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
16526 * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
16527 * To open a collapsible list in its expanded state, set #expanded to 'true'.
16528 * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
16529 * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
16530 * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
16531 * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
16532 * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
16534 OO
.ui
.ListToolGroup
= function OoUiListToolGroup( toolbar
, config
) {
16535 // Allow passing positional parameters inside the config object
16536 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
16538 toolbar
= config
.toolbar
;
16541 // Configuration initialization
16542 config
= config
|| {};
16544 // Properties (must be set before parent constructor, which calls #populate)
16545 this.allowCollapse
= config
.allowCollapse
;
16546 this.forceExpand
= config
.forceExpand
;
16547 this.expanded
= config
.expanded
!== undefined ? config
.expanded
: false;
16548 this.collapsibleTools
= [];
16550 // Parent constructor
16551 OO
.ui
.ListToolGroup
.parent
.call( this, toolbar
, config
);
16554 this.$element
.addClass( 'oo-ui-listToolGroup' );
16559 OO
.inheritClass( OO
.ui
.ListToolGroup
, OO
.ui
.PopupToolGroup
);
16561 /* Static Properties */
16563 OO
.ui
.ListToolGroup
.static.name
= 'list';
16570 OO
.ui
.ListToolGroup
.prototype.populate = function () {
16571 var i
, len
, allowCollapse
= [];
16573 OO
.ui
.ListToolGroup
.parent
.prototype.populate
.call( this );
16575 // Update the list of collapsible tools
16576 if ( this.allowCollapse
!== undefined ) {
16577 allowCollapse
= this.allowCollapse
;
16578 } else if ( this.forceExpand
!== undefined ) {
16579 allowCollapse
= OO
.simpleArrayDifference( Object
.keys( this.tools
), this.forceExpand
);
16582 this.collapsibleTools
= [];
16583 for ( i
= 0, len
= allowCollapse
.length
; i
< len
; i
++ ) {
16584 if ( this.tools
[ allowCollapse
[ i
] ] !== undefined ) {
16585 this.collapsibleTools
.push( this.tools
[ allowCollapse
[ i
] ] );
16589 // Keep at the end, even when tools are added
16590 this.$group
.append( this.getExpandCollapseTool().$element
);
16592 this.getExpandCollapseTool().toggle( this.collapsibleTools
.length
!== 0 );
16593 this.updateCollapsibleState();
16596 OO
.ui
.ListToolGroup
.prototype.getExpandCollapseTool = function () {
16597 var ExpandCollapseTool
;
16598 if ( this.expandCollapseTool
=== undefined ) {
16599 ExpandCollapseTool = function () {
16600 ExpandCollapseTool
.parent
.apply( this, arguments
);
16603 OO
.inheritClass( ExpandCollapseTool
, OO
.ui
.Tool
);
16605 ExpandCollapseTool
.prototype.onSelect = function () {
16606 this.toolGroup
.expanded
= !this.toolGroup
.expanded
;
16607 this.toolGroup
.updateCollapsibleState();
16608 this.setActive( false );
16610 ExpandCollapseTool
.prototype.onUpdateState = function () {
16611 // Do nothing. Tool interface requires an implementation of this function.
16614 ExpandCollapseTool
.static.name
= 'more-fewer';
16616 this.expandCollapseTool
= new ExpandCollapseTool( this );
16618 return this.expandCollapseTool
;
16624 OO
.ui
.ListToolGroup
.prototype.onMouseKeyUp = function ( e
) {
16625 // Do not close the popup when the user wants to show more/fewer tools
16627 $( e
.target
).closest( '.oo-ui-tool-name-more-fewer' ).length
&&
16628 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
16630 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
16631 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
16632 return OO
.ui
.ListToolGroup
.parent
.parent
.prototype.onMouseKeyUp
.call( this, e
);
16634 return OO
.ui
.ListToolGroup
.parent
.prototype.onMouseKeyUp
.call( this, e
);
16638 OO
.ui
.ListToolGroup
.prototype.updateCollapsibleState = function () {
16641 this.getExpandCollapseTool()
16642 .setIcon( this.expanded
? 'collapse' : 'expand' )
16643 .setTitle( OO
.ui
.msg( this.expanded
? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
16645 for ( i
= 0, len
= this.collapsibleTools
.length
; i
< len
; i
++ ) {
16646 this.collapsibleTools
[ i
].toggle( this.expanded
);
16651 * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
16652 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
16653 * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
16654 * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
16655 * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
16656 * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
16658 * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
16662 * // Example of a MenuToolGroup
16663 * var toolFactory = new OO.ui.ToolFactory();
16664 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
16665 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
16667 * // We will be placing status text in this element when tools are used
16668 * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
16670 * // Define the tools that we're going to place in our toolbar
16672 * function SettingsTool() {
16673 * SettingsTool.parent.apply( this, arguments );
16674 * this.reallyActive = false;
16676 * OO.inheritClass( SettingsTool, OO.ui.Tool );
16677 * SettingsTool.static.name = 'settings';
16678 * SettingsTool.static.icon = 'settings';
16679 * SettingsTool.static.title = 'Change settings';
16680 * SettingsTool.prototype.onSelect = function () {
16681 * $area.text( 'Settings tool clicked!' );
16682 * // Toggle the active state on each click
16683 * this.reallyActive = !this.reallyActive;
16684 * this.setActive( this.reallyActive );
16685 * // To update the menu label
16686 * this.toolbar.emit( 'updateState' );
16688 * SettingsTool.prototype.onUpdateState = function () {};
16689 * toolFactory.register( SettingsTool );
16691 * function StuffTool() {
16692 * StuffTool.parent.apply( this, arguments );
16693 * this.reallyActive = false;
16695 * OO.inheritClass( StuffTool, OO.ui.Tool );
16696 * StuffTool.static.name = 'stuff';
16697 * StuffTool.static.icon = 'ellipsis';
16698 * StuffTool.static.title = 'More stuff';
16699 * StuffTool.prototype.onSelect = function () {
16700 * $area.text( 'More stuff tool clicked!' );
16701 * // Toggle the active state on each click
16702 * this.reallyActive = !this.reallyActive;
16703 * this.setActive( this.reallyActive );
16704 * // To update the menu label
16705 * this.toolbar.emit( 'updateState' );
16707 * StuffTool.prototype.onUpdateState = function () {};
16708 * toolFactory.register( StuffTool );
16710 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
16711 * // used once (but not all defined tools must be used).
16715 * header: 'This is the (optional) header',
16716 * title: 'This is the (optional) title',
16717 * indicator: 'down',
16718 * include: [ 'settings', 'stuff' ]
16722 * // Create some UI around the toolbar and place it in the document
16723 * var frame = new OO.ui.PanelLayout( {
16727 * var contentFrame = new OO.ui.PanelLayout( {
16731 * frame.$element.append(
16732 * toolbar.$element,
16733 * contentFrame.$element.append( $area )
16735 * $( 'body' ).append( frame.$element );
16737 * // Here is where the toolbar is actually built. This must be done after inserting it into the
16739 * toolbar.initialize();
16740 * toolbar.emit( 'updateState' );
16742 * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
16743 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
16745 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
16748 * @extends OO.ui.PopupToolGroup
16751 * @param {OO.ui.Toolbar} toolbar
16752 * @param {Object} [config] Configuration options
16754 OO
.ui
.MenuToolGroup
= function OoUiMenuToolGroup( toolbar
, config
) {
16755 // Allow passing positional parameters inside the config object
16756 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
16758 toolbar
= config
.toolbar
;
16761 // Configuration initialization
16762 config
= config
|| {};
16764 // Parent constructor
16765 OO
.ui
.MenuToolGroup
.parent
.call( this, toolbar
, config
);
16768 this.toolbar
.connect( this, { updateState
: 'onUpdateState' } );
16771 this.$element
.addClass( 'oo-ui-menuToolGroup' );
16776 OO
.inheritClass( OO
.ui
.MenuToolGroup
, OO
.ui
.PopupToolGroup
);
16778 /* Static Properties */
16780 OO
.ui
.MenuToolGroup
.static.name
= 'menu';
16785 * Handle the toolbar state being updated.
16787 * When the state changes, the title of each active item in the menu will be joined together and
16788 * used as a label for the group. The label will be empty if none of the items are active.
16792 OO
.ui
.MenuToolGroup
.prototype.onUpdateState = function () {
16796 for ( name
in this.tools
) {
16797 if ( this.tools
[ name
].isActive() ) {
16798 labelTexts
.push( this.tools
[ name
].getTitle() );
16802 this.setLabel( labelTexts
.join( ', ' ) || ' ' );
16809 * https://www.mediawiki.org/wiki/OOjs_UI
16811 * Copyright 2011–2016 OOjs UI Team and other contributors.
16812 * Released under the MIT license
16813 * http://oojs.mit-license.org
16815 * Date: 2016-02-02T22:07:00Z
16817 ( function ( OO
) {
16822 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
16823 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
16826 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
16827 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
16830 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
16833 * @extends OO.ui.ButtonWidget
16834 * @mixins OO.ui.mixin.PendingElement
16837 * @param {Object} [config] Configuration options
16838 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
16839 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
16840 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
16841 * for more information about setting modes.
16842 * @cfg {boolean} [framed=false] Render the action button with a frame
16844 OO
.ui
.ActionWidget
= function OoUiActionWidget( config
) {
16845 // Configuration initialization
16846 config
= $.extend( { framed
: false }, config
);
16848 // Parent constructor
16849 OO
.ui
.ActionWidget
.parent
.call( this, config
);
16851 // Mixin constructors
16852 OO
.ui
.mixin
.PendingElement
.call( this, config
);
16855 this.action
= config
.action
|| '';
16856 this.modes
= config
.modes
|| [];
16861 this.$element
.addClass( 'oo-ui-actionWidget' );
16866 OO
.inheritClass( OO
.ui
.ActionWidget
, OO
.ui
.ButtonWidget
);
16867 OO
.mixinClass( OO
.ui
.ActionWidget
, OO
.ui
.mixin
.PendingElement
);
16872 * A resize event is emitted when the size of the widget changes.
16880 * Check if the action is configured to be available in the specified `mode`.
16882 * @param {string} mode Name of mode
16883 * @return {boolean} The action is configured with the mode
16885 OO
.ui
.ActionWidget
.prototype.hasMode = function ( mode
) {
16886 return this.modes
.indexOf( mode
) !== -1;
16890 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
16894 OO
.ui
.ActionWidget
.prototype.getAction = function () {
16895 return this.action
;
16899 * Get the symbolic name of the mode or modes for which the action is configured to be available.
16901 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
16902 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
16905 * @return {string[]}
16907 OO
.ui
.ActionWidget
.prototype.getModes = function () {
16908 return this.modes
.slice();
16912 * Emit a resize event if the size has changed.
16917 OO
.ui
.ActionWidget
.prototype.propagateResize = function () {
16920 if ( this.isElementAttached() ) {
16921 width
= this.$element
.width();
16922 height
= this.$element
.height();
16924 if ( width
!== this.width
|| height
!== this.height
) {
16925 this.width
= width
;
16926 this.height
= height
;
16927 this.emit( 'resize' );
16937 OO
.ui
.ActionWidget
.prototype.setIcon = function () {
16939 OO
.ui
.mixin
.IconElement
.prototype.setIcon
.apply( this, arguments
);
16940 this.propagateResize();
16948 OO
.ui
.ActionWidget
.prototype.setLabel = function () {
16950 OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.apply( this, arguments
);
16951 this.propagateResize();
16959 OO
.ui
.ActionWidget
.prototype.setFlags = function () {
16961 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags
.apply( this, arguments
);
16962 this.propagateResize();
16970 OO
.ui
.ActionWidget
.prototype.clearFlags = function () {
16972 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags
.apply( this, arguments
);
16973 this.propagateResize();
16979 * Toggle the visibility of the action button.
16981 * @param {boolean} [show] Show button, omit to toggle visibility
16984 OO
.ui
.ActionWidget
.prototype.toggle = function () {
16986 OO
.ui
.ActionWidget
.parent
.prototype.toggle
.apply( this, arguments
);
16987 this.propagateResize();
16993 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
16994 * Actions can be made available for specific contexts (modes) and circumstances
16995 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
16997 * ActionSets contain two types of actions:
16999 * - Special: Special actions are the first visible actions with special flags, such as 'safe' and 'primary', the default special flags. Additional special flags can be configured in subclasses with the static #specialFlags property.
17000 * - Other: Other actions include all non-special visible actions.
17002 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
17005 * // Example: An action set used in a process dialog
17006 * function MyProcessDialog( config ) {
17007 * MyProcessDialog.parent.call( this, config );
17009 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
17010 * MyProcessDialog.static.title = 'An action set in a process dialog';
17011 * // An action set that uses modes ('edit' and 'help' mode, in this example).
17012 * MyProcessDialog.static.actions = [
17013 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
17014 * { action: 'help', modes: 'edit', label: 'Help' },
17015 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
17016 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
17019 * MyProcessDialog.prototype.initialize = function () {
17020 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
17021 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
17022 * this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode.</p>' );
17023 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
17024 * this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode.</p>' );
17025 * this.stackLayout = new OO.ui.StackLayout( {
17026 * items: [ this.panel1, this.panel2 ]
17028 * this.$body.append( this.stackLayout.$element );
17030 * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
17031 * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
17032 * .next( function () {
17033 * this.actions.setMode( 'edit' );
17036 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
17037 * if ( action === 'help' ) {
17038 * this.actions.setMode( 'help' );
17039 * this.stackLayout.setItem( this.panel2 );
17040 * } else if ( action === 'back' ) {
17041 * this.actions.setMode( 'edit' );
17042 * this.stackLayout.setItem( this.panel1 );
17043 * } else if ( action === 'continue' ) {
17044 * var dialog = this;
17045 * return new OO.ui.Process( function () {
17049 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
17051 * MyProcessDialog.prototype.getBodyHeight = function () {
17052 * return this.panel1.$element.outerHeight( true );
17054 * var windowManager = new OO.ui.WindowManager();
17055 * $( 'body' ).append( windowManager.$element );
17056 * var dialog = new MyProcessDialog( {
17059 * windowManager.addWindows( [ dialog ] );
17060 * windowManager.openWindow( dialog );
17062 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
17066 * @mixins OO.EventEmitter
17069 * @param {Object} [config] Configuration options
17071 OO
.ui
.ActionSet
= function OoUiActionSet( config
) {
17072 // Configuration initialization
17073 config
= config
|| {};
17075 // Mixin constructors
17076 OO
.EventEmitter
.call( this );
17080 this.categories
= {
17081 actions
: 'getAction',
17085 this.categorized
= {};
17088 this.organized
= false;
17089 this.changing
= false;
17090 this.changed
= false;
17095 OO
.mixinClass( OO
.ui
.ActionSet
, OO
.EventEmitter
);
17097 /* Static Properties */
17100 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
17101 * header of a {@link OO.ui.ProcessDialog process dialog}.
17102 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
17104 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
17109 * @property {string}
17111 OO
.ui
.ActionSet
.static.specialFlags
= [ 'safe', 'primary' ];
17118 * A 'click' event is emitted when an action is clicked.
17120 * @param {OO.ui.ActionWidget} action Action that was clicked
17126 * A 'resize' event is emitted when an action widget is resized.
17128 * @param {OO.ui.ActionWidget} action Action that was resized
17134 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
17136 * @param {OO.ui.ActionWidget[]} added Actions added
17142 * A 'remove' event is emitted when actions are {@link #method-remove removed}
17143 * or {@link #clear cleared}.
17145 * @param {OO.ui.ActionWidget[]} added Actions removed
17151 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
17152 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
17159 * Handle action change events.
17164 OO
.ui
.ActionSet
.prototype.onActionChange = function () {
17165 this.organized
= false;
17166 if ( this.changing
) {
17167 this.changed
= true;
17169 this.emit( 'change' );
17174 * Check if an action is one of the special actions.
17176 * @param {OO.ui.ActionWidget} action Action to check
17177 * @return {boolean} Action is special
17179 OO
.ui
.ActionSet
.prototype.isSpecial = function ( action
) {
17182 for ( flag
in this.special
) {
17183 if ( action
=== this.special
[ flag
] ) {
17192 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
17195 * @param {Object} [filters] Filters to use, omit to get all actions
17196 * @param {string|string[]} [filters.actions] Actions that action widgets must have
17197 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
17198 * @param {string|string[]} [filters.modes] Modes that action widgets must have
17199 * @param {boolean} [filters.visible] Action widgets must be visible
17200 * @param {boolean} [filters.disabled] Action widgets must be disabled
17201 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
17203 OO
.ui
.ActionSet
.prototype.get = function ( filters
) {
17204 var i
, len
, list
, category
, actions
, index
, match
, matches
;
17209 // Collect category candidates
17211 for ( category
in this.categorized
) {
17212 list
= filters
[ category
];
17214 if ( !Array
.isArray( list
) ) {
17217 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
17218 actions
= this.categorized
[ category
][ list
[ i
] ];
17219 if ( Array
.isArray( actions
) ) {
17220 matches
.push
.apply( matches
, actions
);
17225 // Remove by boolean filters
17226 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
17227 match
= matches
[ i
];
17229 ( filters
.visible
!== undefined && match
.isVisible() !== filters
.visible
) ||
17230 ( filters
.disabled
!== undefined && match
.isDisabled() !== filters
.disabled
)
17232 matches
.splice( i
, 1 );
17237 // Remove duplicates
17238 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
17239 match
= matches
[ i
];
17240 index
= matches
.lastIndexOf( match
);
17241 while ( index
!== i
) {
17242 matches
.splice( index
, 1 );
17244 index
= matches
.lastIndexOf( match
);
17249 return this.list
.slice();
17253 * Get 'special' actions.
17255 * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
17256 * Special flags can be configured in subclasses by changing the static #specialFlags property.
17258 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
17260 OO
.ui
.ActionSet
.prototype.getSpecial = function () {
17262 return $.extend( {}, this.special
);
17266 * Get 'other' actions.
17268 * Other actions include all non-special visible action widgets.
17270 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
17272 OO
.ui
.ActionSet
.prototype.getOthers = function () {
17274 return this.others
.slice();
17278 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
17279 * to be available in the specified mode will be made visible. All other actions will be hidden.
17281 * @param {string} mode The mode. Only actions configured to be available in the specified
17282 * mode will be made visible.
17287 OO
.ui
.ActionSet
.prototype.setMode = function ( mode
) {
17288 var i
, len
, action
;
17290 this.changing
= true;
17291 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
17292 action
= this.list
[ i
];
17293 action
.toggle( action
.hasMode( mode
) );
17296 this.organized
= false;
17297 this.changing
= false;
17298 this.emit( 'change' );
17304 * Set the abilities of the specified actions.
17306 * Action widgets that are configured with the specified actions will be enabled
17307 * or disabled based on the boolean values specified in the `actions`
17310 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
17311 * values that indicate whether or not the action should be enabled.
17314 OO
.ui
.ActionSet
.prototype.setAbilities = function ( actions
) {
17315 var i
, len
, action
, item
;
17317 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
17318 item
= this.list
[ i
];
17319 action
= item
.getAction();
17320 if ( actions
[ action
] !== undefined ) {
17321 item
.setDisabled( !actions
[ action
] );
17329 * Executes a function once per action.
17331 * When making changes to multiple actions, use this method instead of iterating over the actions
17332 * manually to defer emitting a #change event until after all actions have been changed.
17334 * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get
17335 * @param {Function} callback Callback to run for each action; callback is invoked with three
17336 * arguments: the action, the action's index, the list of actions being iterated over
17339 OO
.ui
.ActionSet
.prototype.forEach = function ( filter
, callback
) {
17340 this.changed
= false;
17341 this.changing
= true;
17342 this.get( filter
).forEach( callback
);
17343 this.changing
= false;
17344 if ( this.changed
) {
17345 this.emit( 'change' );
17352 * Add action widgets to the action set.
17354 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
17359 OO
.ui
.ActionSet
.prototype.add = function ( actions
) {
17360 var i
, len
, action
;
17362 this.changing
= true;
17363 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
17364 action
= actions
[ i
];
17365 action
.connect( this, {
17366 click
: [ 'emit', 'click', action
],
17367 resize
: [ 'emit', 'resize', action
],
17368 toggle
: [ 'onActionChange' ]
17370 this.list
.push( action
);
17372 this.organized
= false;
17373 this.emit( 'add', actions
);
17374 this.changing
= false;
17375 this.emit( 'change' );
17381 * Remove action widgets from the set.
17383 * To remove all actions, you may wish to use the #clear method instead.
17385 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
17390 OO
.ui
.ActionSet
.prototype.remove = function ( actions
) {
17391 var i
, len
, index
, action
;
17393 this.changing
= true;
17394 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
17395 action
= actions
[ i
];
17396 index
= this.list
.indexOf( action
);
17397 if ( index
!== -1 ) {
17398 action
.disconnect( this );
17399 this.list
.splice( index
, 1 );
17402 this.organized
= false;
17403 this.emit( 'remove', actions
);
17404 this.changing
= false;
17405 this.emit( 'change' );
17411 * Remove all action widets from the set.
17413 * To remove only specified actions, use the {@link #method-remove remove} method instead.
17419 OO
.ui
.ActionSet
.prototype.clear = function () {
17420 var i
, len
, action
,
17421 removed
= this.list
.slice();
17423 this.changing
= true;
17424 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
17425 action
= this.list
[ i
];
17426 action
.disconnect( this );
17431 this.organized
= false;
17432 this.emit( 'remove', removed
);
17433 this.changing
= false;
17434 this.emit( 'change' );
17440 * Organize actions.
17442 * This is called whenever organized information is requested. It will only reorganize the actions
17443 * if something has changed since the last time it ran.
17448 OO
.ui
.ActionSet
.prototype.organize = function () {
17449 var i
, iLen
, j
, jLen
, flag
, action
, category
, list
, item
, special
,
17450 specialFlags
= this.constructor.static.specialFlags
;
17452 if ( !this.organized
) {
17453 this.categorized
= {};
17456 for ( i
= 0, iLen
= this.list
.length
; i
< iLen
; i
++ ) {
17457 action
= this.list
[ i
];
17458 if ( action
.isVisible() ) {
17459 // Populate categories
17460 for ( category
in this.categories
) {
17461 if ( !this.categorized
[ category
] ) {
17462 this.categorized
[ category
] = {};
17464 list
= action
[ this.categories
[ category
] ]();
17465 if ( !Array
.isArray( list
) ) {
17468 for ( j
= 0, jLen
= list
.length
; j
< jLen
; j
++ ) {
17470 if ( !this.categorized
[ category
][ item
] ) {
17471 this.categorized
[ category
][ item
] = [];
17473 this.categorized
[ category
][ item
].push( action
);
17476 // Populate special/others
17478 for ( j
= 0, jLen
= specialFlags
.length
; j
< jLen
; j
++ ) {
17479 flag
= specialFlags
[ j
];
17480 if ( !this.special
[ flag
] && action
.hasFlag( flag
) ) {
17481 this.special
[ flag
] = action
;
17487 this.others
.push( action
);
17491 this.organized
= true;
17498 * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
17499 * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
17500 * appearance and functionality of the error interface.
17502 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
17503 * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
17504 * that initiated the failed process will be disabled.
17506 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
17509 * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
17511 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
17516 * @param {string|jQuery} message Description of error
17517 * @param {Object} [config] Configuration options
17518 * @cfg {boolean} [recoverable=true] Error is recoverable.
17519 * By default, errors are recoverable, and users can try the process again.
17520 * @cfg {boolean} [warning=false] Error is a warning.
17521 * If the error is a warning, the error interface will include a
17522 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
17523 * is not triggered a second time if the user chooses to continue.
17525 OO
.ui
.Error
= function OoUiError( message
, config
) {
17526 // Allow passing positional parameters inside the config object
17527 if ( OO
.isPlainObject( message
) && config
=== undefined ) {
17529 message
= config
.message
;
17532 // Configuration initialization
17533 config
= config
|| {};
17536 this.message
= message
instanceof jQuery
? message
: String( message
);
17537 this.recoverable
= config
.recoverable
=== undefined || !!config
.recoverable
;
17538 this.warning
= !!config
.warning
;
17543 OO
.initClass( OO
.ui
.Error
);
17548 * Check if the error is recoverable.
17550 * If the error is recoverable, users are able to try the process again.
17552 * @return {boolean} Error is recoverable
17554 OO
.ui
.Error
.prototype.isRecoverable = function () {
17555 return this.recoverable
;
17559 * Check if the error is a warning.
17561 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
17563 * @return {boolean} Error is warning
17565 OO
.ui
.Error
.prototype.isWarning = function () {
17566 return this.warning
;
17570 * Get error message as DOM nodes.
17572 * @return {jQuery} Error message in DOM nodes
17574 OO
.ui
.Error
.prototype.getMessage = function () {
17575 return this.message
instanceof jQuery
?
17576 this.message
.clone() :
17577 $( '<div>' ).text( this.message
).contents();
17581 * Get the error message text.
17583 * @return {string} Error message
17585 OO
.ui
.Error
.prototype.getMessageText = function () {
17586 return this.message
instanceof jQuery
? this.message
.text() : this.message
;
17590 * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
17593 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
17594 * - **promise**: the process will continue to the next step when the promise is successfully resolved
17595 * or stop if the promise is rejected.
17596 * - **function**: the process will execute the function. The process will stop if the function returns
17597 * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
17598 * will wait for that number of milliseconds before proceeding.
17600 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
17601 * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
17602 * its remaining steps will not be performed.
17607 * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
17608 * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
17609 * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
17610 * a number or promise.
17611 * @return {Object} Step object, with `callback` and `context` properties
17613 OO
.ui
.Process = function ( step
, context
) {
17618 if ( step
!== undefined ) {
17619 this.next( step
, context
);
17625 OO
.initClass( OO
.ui
.Process
);
17630 * Start the process.
17632 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
17633 * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
17634 * and any remaining steps are not performed.
17636 OO
.ui
.Process
.prototype.execute = function () {
17637 var i
, len
, promise
;
17640 * Continue execution.
17643 * @param {Array} step A function and the context it should be called in
17644 * @return {Function} Function that continues the process
17646 function proceed( step
) {
17647 return function () {
17648 // Execute step in the correct context
17650 result
= step
.callback
.call( step
.context
);
17652 if ( result
=== false ) {
17653 // Use rejected promise for boolean false results
17654 return $.Deferred().reject( [] ).promise();
17656 if ( typeof result
=== 'number' ) {
17657 if ( result
< 0 ) {
17658 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
17660 // Use a delayed promise for numbers, expecting them to be in milliseconds
17661 deferred
= $.Deferred();
17662 setTimeout( deferred
.resolve
, result
);
17663 return deferred
.promise();
17665 if ( result
instanceof OO
.ui
.Error
) {
17666 // Use rejected promise for error
17667 return $.Deferred().reject( [ result
] ).promise();
17669 if ( Array
.isArray( result
) && result
.length
&& result
[ 0 ] instanceof OO
.ui
.Error
) {
17670 // Use rejected promise for list of errors
17671 return $.Deferred().reject( result
).promise();
17673 // Duck-type the object to see if it can produce a promise
17674 if ( result
&& $.isFunction( result
.promise
) ) {
17675 // Use a promise generated from the result
17676 return result
.promise();
17678 // Use resolved promise for other results
17679 return $.Deferred().resolve().promise();
17683 if ( this.steps
.length
) {
17684 // Generate a chain reaction of promises
17685 promise
= proceed( this.steps
[ 0 ] )();
17686 for ( i
= 1, len
= this.steps
.length
; i
< len
; i
++ ) {
17687 promise
= promise
.then( proceed( this.steps
[ i
] ) );
17690 promise
= $.Deferred().resolve().promise();
17697 * Create a process step.
17700 * @param {number|jQuery.Promise|Function} step
17702 * - Number of milliseconds to wait before proceeding
17703 * - Promise that must be resolved before proceeding
17704 * - Function to execute
17705 * - If the function returns a boolean false the process will stop
17706 * - If the function returns a promise, the process will continue to the next
17707 * step when the promise is resolved or stop if the promise is rejected
17708 * - If the function returns a number, the process will wait for that number of
17709 * milliseconds before proceeding
17710 * @param {Object} [context=null] Execution context of the function. The context is
17711 * ignored if the step is a number or promise.
17712 * @return {Object} Step object, with `callback` and `context` properties
17714 OO
.ui
.Process
.prototype.createStep = function ( step
, context
) {
17715 if ( typeof step
=== 'number' || $.isFunction( step
.promise
) ) {
17717 callback: function () {
17723 if ( $.isFunction( step
) ) {
17729 throw new Error( 'Cannot create process step: number, promise or function expected' );
17733 * Add step to the beginning of the process.
17735 * @inheritdoc #createStep
17736 * @return {OO.ui.Process} this
17739 OO
.ui
.Process
.prototype.first = function ( step
, context
) {
17740 this.steps
.unshift( this.createStep( step
, context
) );
17745 * Add step to the end of the process.
17747 * @inheritdoc #createStep
17748 * @return {OO.ui.Process} this
17751 OO
.ui
.Process
.prototype.next = function ( step
, context
) {
17752 this.steps
.push( this.createStep( step
, context
) );
17757 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
17758 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
17759 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
17760 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
17761 * pertinent data and reused.
17763 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
17764 * `opened`, and `closing`, which represent the primary stages of the cycle:
17766 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
17767 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
17769 * - an `opening` event is emitted with an `opening` promise
17770 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
17771 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
17772 * window and its result executed
17773 * - a `setup` progress notification is emitted from the `opening` promise
17774 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
17775 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
17776 * window and its result executed
17777 * - a `ready` progress notification is emitted from the `opening` promise
17778 * - the `opening` promise is resolved with an `opened` promise
17780 * **Opened**: the window is now open.
17782 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
17783 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
17784 * to close the window.
17786 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
17787 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
17788 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
17789 * window and its result executed
17790 * - a `hold` progress notification is emitted from the `closing` promise
17791 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
17792 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
17793 * window and its result executed
17794 * - a `teardown` progress notification is emitted from the `closing` promise
17795 * - the `closing` promise is resolved. The window is now closed
17797 * See the [OOjs UI documentation on MediaWiki][1] for more information.
17799 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
17802 * @extends OO.ui.Element
17803 * @mixins OO.EventEmitter
17806 * @param {Object} [config] Configuration options
17807 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
17808 * Note that window classes that are instantiated with a factory must have
17809 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
17810 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
17812 OO
.ui
.WindowManager
= function OoUiWindowManager( config
) {
17813 // Configuration initialization
17814 config
= config
|| {};
17816 // Parent constructor
17817 OO
.ui
.WindowManager
.parent
.call( this, config
);
17819 // Mixin constructors
17820 OO
.EventEmitter
.call( this );
17823 this.factory
= config
.factory
;
17824 this.modal
= config
.modal
=== undefined || !!config
.modal
;
17826 this.opening
= null;
17827 this.opened
= null;
17828 this.closing
= null;
17829 this.preparingToOpen
= null;
17830 this.preparingToClose
= null;
17831 this.currentWindow
= null;
17832 this.globalEvents
= false;
17833 this.$ariaHidden
= null;
17834 this.onWindowResizeTimeout
= null;
17835 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
17836 this.afterWindowResizeHandler
= this.afterWindowResize
.bind( this );
17840 .addClass( 'oo-ui-windowManager' )
17841 .toggleClass( 'oo-ui-windowManager-modal', this.modal
);
17846 OO
.inheritClass( OO
.ui
.WindowManager
, OO
.ui
.Element
);
17847 OO
.mixinClass( OO
.ui
.WindowManager
, OO
.EventEmitter
);
17852 * An 'opening' event is emitted when the window begins to be opened.
17855 * @param {OO.ui.Window} win Window that's being opened
17856 * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
17857 * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
17858 * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
17859 * @param {Object} data Window opening data
17863 * A 'closing' event is emitted when the window begins to be closed.
17866 * @param {OO.ui.Window} win Window that's being closed
17867 * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
17868 * is closed successfully. The promise emits `hold` and `teardown` notifications when those
17869 * processes are complete. When the `closing` promise is resolved, the first argument of its value
17870 * is the closing data.
17871 * @param {Object} data Window closing data
17875 * A 'resize' event is emitted when a window is resized.
17878 * @param {OO.ui.Window} win Window that was resized
17881 /* Static Properties */
17884 * Map of the symbolic name of each window size and its CSS properties.
17888 * @property {Object}
17890 OO
.ui
.WindowManager
.static.sizes
= {
17904 // These can be non-numeric because they are never used in calculations
17911 * Symbolic name of the default window size.
17913 * The default size is used if the window's requested size is not recognized.
17917 * @property {string}
17919 OO
.ui
.WindowManager
.static.defaultSize
= 'medium';
17924 * Handle window resize events.
17927 * @param {jQuery.Event} e Window resize event
17929 OO
.ui
.WindowManager
.prototype.onWindowResize = function () {
17930 clearTimeout( this.onWindowResizeTimeout
);
17931 this.onWindowResizeTimeout
= setTimeout( this.afterWindowResizeHandler
, 200 );
17935 * Handle window resize events.
17938 * @param {jQuery.Event} e Window resize event
17940 OO
.ui
.WindowManager
.prototype.afterWindowResize = function () {
17941 if ( this.currentWindow
) {
17942 this.updateWindowSize( this.currentWindow
);
17947 * Check if window is opening.
17949 * @return {boolean} Window is opening
17951 OO
.ui
.WindowManager
.prototype.isOpening = function ( win
) {
17952 return win
=== this.currentWindow
&& !!this.opening
&& this.opening
.state() === 'pending';
17956 * Check if window is closing.
17958 * @return {boolean} Window is closing
17960 OO
.ui
.WindowManager
.prototype.isClosing = function ( win
) {
17961 return win
=== this.currentWindow
&& !!this.closing
&& this.closing
.state() === 'pending';
17965 * Check if window is opened.
17967 * @return {boolean} Window is opened
17969 OO
.ui
.WindowManager
.prototype.isOpened = function ( win
) {
17970 return win
=== this.currentWindow
&& !!this.opened
&& this.opened
.state() === 'pending';
17974 * Check if a window is being managed.
17976 * @param {OO.ui.Window} win Window to check
17977 * @return {boolean} Window is being managed
17979 OO
.ui
.WindowManager
.prototype.hasWindow = function ( win
) {
17982 for ( name
in this.windows
) {
17983 if ( this.windows
[ name
] === win
) {
17992 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
17994 * @param {OO.ui.Window} win Window being opened
17995 * @param {Object} [data] Window opening data
17996 * @return {number} Milliseconds to wait
17998 OO
.ui
.WindowManager
.prototype.getSetupDelay = function () {
18003 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
18005 * @param {OO.ui.Window} win Window being opened
18006 * @param {Object} [data] Window opening data
18007 * @return {number} Milliseconds to wait
18009 OO
.ui
.WindowManager
.prototype.getReadyDelay = function () {
18014 * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
18016 * @param {OO.ui.Window} win Window being closed
18017 * @param {Object} [data] Window closing data
18018 * @return {number} Milliseconds to wait
18020 OO
.ui
.WindowManager
.prototype.getHoldDelay = function () {
18025 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
18026 * executing the ‘teardown’ process.
18028 * @param {OO.ui.Window} win Window being closed
18029 * @param {Object} [data] Window closing data
18030 * @return {number} Milliseconds to wait
18032 OO
.ui
.WindowManager
.prototype.getTeardownDelay = function () {
18033 return this.modal
? 250 : 0;
18037 * Get a window by its symbolic name.
18039 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
18040 * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
18041 * for more information about using factories.
18042 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
18044 * @param {string} name Symbolic name of the window
18045 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
18046 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
18047 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
18049 OO
.ui
.WindowManager
.prototype.getWindow = function ( name
) {
18050 var deferred
= $.Deferred(),
18051 win
= this.windows
[ name
];
18053 if ( !( win
instanceof OO
.ui
.Window
) ) {
18054 if ( this.factory
) {
18055 if ( !this.factory
.lookup( name
) ) {
18056 deferred
.reject( new OO
.ui
.Error(
18057 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
18060 win
= this.factory
.create( name
);
18061 this.addWindows( [ win
] );
18062 deferred
.resolve( win
);
18065 deferred
.reject( new OO
.ui
.Error(
18066 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
18070 deferred
.resolve( win
);
18073 return deferred
.promise();
18077 * Get current window.
18079 * @return {OO.ui.Window|null} Currently opening/opened/closing window
18081 OO
.ui
.WindowManager
.prototype.getCurrentWindow = function () {
18082 return this.currentWindow
;
18088 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
18089 * @param {Object} [data] Window opening data
18090 * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
18091 * See {@link #event-opening 'opening' event} for more information about `opening` promises.
18094 OO
.ui
.WindowManager
.prototype.openWindow = function ( win
, data
) {
18095 var manager
= this,
18096 opening
= $.Deferred();
18098 // Argument handling
18099 if ( typeof win
=== 'string' ) {
18100 return this.getWindow( win
).then( function ( win
) {
18101 return manager
.openWindow( win
, data
);
18106 if ( !this.hasWindow( win
) ) {
18107 opening
.reject( new OO
.ui
.Error(
18108 'Cannot open window: window is not attached to manager'
18110 } else if ( this.preparingToOpen
|| this.opening
|| this.opened
) {
18111 opening
.reject( new OO
.ui
.Error(
18112 'Cannot open window: another window is opening or open'
18117 if ( opening
.state() !== 'rejected' ) {
18118 // If a window is currently closing, wait for it to complete
18119 this.preparingToOpen
= $.when( this.closing
);
18120 // Ensure handlers get called after preparingToOpen is set
18121 this.preparingToOpen
.done( function () {
18122 if ( manager
.modal
) {
18123 manager
.toggleGlobalEvents( true );
18124 manager
.toggleAriaIsolation( true );
18126 manager
.currentWindow
= win
;
18127 manager
.opening
= opening
;
18128 manager
.preparingToOpen
= null;
18129 manager
.emit( 'opening', win
, opening
, data
);
18130 setTimeout( function () {
18131 win
.setup( data
).then( function () {
18132 manager
.updateWindowSize( win
);
18133 manager
.opening
.notify( { state
: 'setup' } );
18134 setTimeout( function () {
18135 win
.ready( data
).then( function () {
18136 manager
.opening
.notify( { state
: 'ready' } );
18137 manager
.opening
= null;
18138 manager
.opened
= $.Deferred();
18139 opening
.resolve( manager
.opened
.promise(), data
);
18141 manager
.opening
= null;
18142 manager
.opened
= $.Deferred();
18144 manager
.closeWindow( win
);
18146 }, manager
.getReadyDelay() );
18148 manager
.opening
= null;
18149 manager
.opened
= $.Deferred();
18151 manager
.closeWindow( win
);
18153 }, manager
.getSetupDelay() );
18157 return opening
.promise();
18163 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
18164 * @param {Object} [data] Window closing data
18165 * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
18166 * See {@link #event-closing 'closing' event} for more information about closing promises.
18167 * @throws {Error} An error is thrown if the window is not managed by the window manager.
18170 OO
.ui
.WindowManager
.prototype.closeWindow = function ( win
, data
) {
18171 var manager
= this,
18172 closing
= $.Deferred(),
18175 // Argument handling
18176 if ( typeof win
=== 'string' ) {
18177 win
= this.windows
[ win
];
18178 } else if ( !this.hasWindow( win
) ) {
18184 closing
.reject( new OO
.ui
.Error(
18185 'Cannot close window: window is not attached to manager'
18187 } else if ( win
!== this.currentWindow
) {
18188 closing
.reject( new OO
.ui
.Error(
18189 'Cannot close window: window already closed with different data'
18191 } else if ( this.preparingToClose
|| this.closing
) {
18192 closing
.reject( new OO
.ui
.Error(
18193 'Cannot close window: window already closing with different data'
18198 if ( closing
.state() !== 'rejected' ) {
18199 // If the window is currently opening, close it when it's done
18200 this.preparingToClose
= $.when( this.opening
);
18201 // Ensure handlers get called after preparingToClose is set
18202 this.preparingToClose
.always( function () {
18203 manager
.closing
= closing
;
18204 manager
.preparingToClose
= null;
18205 manager
.emit( 'closing', win
, closing
, data
);
18206 opened
= manager
.opened
;
18207 manager
.opened
= null;
18208 opened
.resolve( closing
.promise(), data
);
18209 setTimeout( function () {
18210 win
.hold( data
).then( function () {
18211 closing
.notify( { state
: 'hold' } );
18212 setTimeout( function () {
18213 win
.teardown( data
).then( function () {
18214 closing
.notify( { state
: 'teardown' } );
18215 if ( manager
.modal
) {
18216 manager
.toggleGlobalEvents( false );
18217 manager
.toggleAriaIsolation( false );
18219 manager
.closing
= null;
18220 manager
.currentWindow
= null;
18221 closing
.resolve( data
);
18223 }, manager
.getTeardownDelay() );
18225 }, manager
.getHoldDelay() );
18229 return closing
.promise();
18233 * Add windows to the window manager.
18235 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
18236 * See the [OOjs ui documentation on MediaWiki] [2] for examples.
18237 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
18239 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
18240 * by reference, symbolic name, or explicitly defined symbolic names.
18241 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
18242 * explicit nor a statically configured symbolic name.
18244 OO
.ui
.WindowManager
.prototype.addWindows = function ( windows
) {
18245 var i
, len
, win
, name
, list
;
18247 if ( Array
.isArray( windows
) ) {
18248 // Convert to map of windows by looking up symbolic names from static configuration
18250 for ( i
= 0, len
= windows
.length
; i
< len
; i
++ ) {
18251 name
= windows
[ i
].constructor.static.name
;
18252 if ( typeof name
!== 'string' ) {
18253 throw new Error( 'Cannot add window' );
18255 list
[ name
] = windows
[ i
];
18257 } else if ( OO
.isPlainObject( windows
) ) {
18262 for ( name
in list
) {
18263 win
= list
[ name
];
18264 this.windows
[ name
] = win
.toggle( false );
18265 this.$element
.append( win
.$element
);
18266 win
.setManager( this );
18271 * Remove the specified windows from the windows manager.
18273 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
18274 * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
18275 * longer listens to events, use the #destroy method.
18277 * @param {string[]} names Symbolic names of windows to remove
18278 * @return {jQuery.Promise} Promise resolved when window is closed and removed
18279 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
18281 OO
.ui
.WindowManager
.prototype.removeWindows = function ( names
) {
18282 var i
, len
, win
, name
, cleanupWindow
,
18285 cleanup = function ( name
, win
) {
18286 delete manager
.windows
[ name
];
18287 win
.$element
.detach();
18290 for ( i
= 0, len
= names
.length
; i
< len
; i
++ ) {
18292 win
= this.windows
[ name
];
18294 throw new Error( 'Cannot remove window' );
18296 cleanupWindow
= cleanup
.bind( null, name
, win
);
18297 promises
.push( this.closeWindow( name
).then( cleanupWindow
, cleanupWindow
) );
18300 return $.when
.apply( $, promises
);
18304 * Remove all windows from the window manager.
18306 * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
18307 * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
18308 * To remove just a subset of windows, use the #removeWindows method.
18310 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
18312 OO
.ui
.WindowManager
.prototype.clearWindows = function () {
18313 return this.removeWindows( Object
.keys( this.windows
) );
18317 * Set dialog size. In general, this method should not be called directly.
18319 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
18323 OO
.ui
.WindowManager
.prototype.updateWindowSize = function ( win
) {
18326 // Bypass for non-current, and thus invisible, windows
18327 if ( win
!== this.currentWindow
) {
18331 isFullscreen
= win
.getSize() === 'full';
18333 this.$element
.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen
);
18334 this.$element
.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen
);
18335 win
.setDimensions( win
.getSizeProperties() );
18337 this.emit( 'resize', win
);
18343 * Bind or unbind global events for scrolling.
18346 * @param {boolean} [on] Bind global events
18349 OO
.ui
.WindowManager
.prototype.toggleGlobalEvents = function ( on
) {
18350 var scrollWidth
, bodyMargin
,
18351 $body
= $( this.getElementDocument().body
),
18352 // We could have multiple window managers open so only modify
18353 // the body css at the bottom of the stack
18354 stackDepth
= $body
.data( 'windowManagerGlobalEvents' ) || 0 ;
18356 on
= on
=== undefined ? !!this.globalEvents
: !!on
;
18359 if ( !this.globalEvents
) {
18360 $( this.getElementWindow() ).on( {
18361 // Start listening for top-level window dimension changes
18362 'orientationchange resize': this.onWindowResizeHandler
18364 if ( stackDepth
=== 0 ) {
18365 scrollWidth
= window
.innerWidth
- document
.documentElement
.clientWidth
;
18366 bodyMargin
= parseFloat( $body
.css( 'margin-right' ) ) || 0;
18368 overflow
: 'hidden',
18369 'margin-right': bodyMargin
+ scrollWidth
18373 this.globalEvents
= true;
18375 } else if ( this.globalEvents
) {
18376 $( this.getElementWindow() ).off( {
18377 // Stop listening for top-level window dimension changes
18378 'orientationchange resize': this.onWindowResizeHandler
18381 if ( stackDepth
=== 0 ) {
18387 this.globalEvents
= false;
18389 $body
.data( 'windowManagerGlobalEvents', stackDepth
);
18395 * Toggle screen reader visibility of content other than the window manager.
18398 * @param {boolean} [isolate] Make only the window manager visible to screen readers
18401 OO
.ui
.WindowManager
.prototype.toggleAriaIsolation = function ( isolate
) {
18402 isolate
= isolate
=== undefined ? !this.$ariaHidden
: !!isolate
;
18405 if ( !this.$ariaHidden
) {
18406 // Hide everything other than the window manager from screen readers
18407 this.$ariaHidden
= $( 'body' )
18409 .not( this.$element
.parentsUntil( 'body' ).last() )
18410 .attr( 'aria-hidden', '' );
18412 } else if ( this.$ariaHidden
) {
18413 // Restore screen reader visibility
18414 this.$ariaHidden
.removeAttr( 'aria-hidden' );
18415 this.$ariaHidden
= null;
18422 * Destroy the window manager.
18424 * Destroying the window manager ensures that it will no longer listen to events. If you would like to
18425 * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
18428 OO
.ui
.WindowManager
.prototype.destroy = function () {
18429 this.toggleGlobalEvents( false );
18430 this.toggleAriaIsolation( false );
18431 this.clearWindows();
18432 this.$element
.remove();
18436 * A window is a container for elements that are in a child frame. They are used with
18437 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
18438 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
18439 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
18440 * the window manager will choose a sensible fallback.
18442 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
18443 * different processes are executed:
18445 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
18446 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
18449 * - {@link #getSetupProcess} method is called and its result executed
18450 * - {@link #getReadyProcess} method is called and its result executed
18452 * **opened**: The window is now open
18454 * **closing**: The closing stage begins when the window manager's
18455 * {@link OO.ui.WindowManager#closeWindow closeWindow}
18456 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
18458 * - {@link #getHoldProcess} method is called and its result executed
18459 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
18461 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
18462 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
18463 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
18464 * processing can complete. Always assume window processes are executed asynchronously.
18466 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
18468 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
18472 * @extends OO.ui.Element
18473 * @mixins OO.EventEmitter
18476 * @param {Object} [config] Configuration options
18477 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
18478 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
18480 OO
.ui
.Window
= function OoUiWindow( config
) {
18481 // Configuration initialization
18482 config
= config
|| {};
18484 // Parent constructor
18485 OO
.ui
.Window
.parent
.call( this, config
);
18487 // Mixin constructors
18488 OO
.EventEmitter
.call( this );
18491 this.manager
= null;
18492 this.size
= config
.size
|| this.constructor.static.size
;
18493 this.$frame
= $( '<div>' );
18494 this.$overlay
= $( '<div>' );
18495 this.$content
= $( '<div>' );
18497 this.$focusTrapBefore
= $( '<div>' ).prop( 'tabIndex', 0 );
18498 this.$focusTrapAfter
= $( '<div>' ).prop( 'tabIndex', 0 );
18499 this.$focusTraps
= this.$focusTrapBefore
.add( this.$focusTrapAfter
);
18502 this.$overlay
.addClass( 'oo-ui-window-overlay' );
18504 .addClass( 'oo-ui-window-content' )
18505 .attr( 'tabindex', 0 );
18507 .addClass( 'oo-ui-window-frame' )
18508 .append( this.$focusTrapBefore
, this.$content
, this.$focusTrapAfter
);
18511 .addClass( 'oo-ui-window' )
18512 .append( this.$frame
, this.$overlay
);
18514 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
18515 // that reference properties not initialized at that time of parent class construction
18516 // TODO: Find a better way to handle post-constructor setup
18517 this.visible
= false;
18518 this.$element
.addClass( 'oo-ui-element-hidden' );
18523 OO
.inheritClass( OO
.ui
.Window
, OO
.ui
.Element
);
18524 OO
.mixinClass( OO
.ui
.Window
, OO
.EventEmitter
);
18526 /* Static Properties */
18529 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
18531 * The static size is used if no #size is configured during construction.
18535 * @property {string}
18537 OO
.ui
.Window
.static.size
= 'medium';
18542 * Handle mouse down events.
18545 * @param {jQuery.Event} e Mouse down event
18547 OO
.ui
.Window
.prototype.onMouseDown = function ( e
) {
18548 // Prevent clicking on the click-block from stealing focus
18549 if ( e
.target
=== this.$element
[ 0 ] ) {
18555 * Check if the window has been initialized.
18557 * Initialization occurs when a window is added to a manager.
18559 * @return {boolean} Window has been initialized
18561 OO
.ui
.Window
.prototype.isInitialized = function () {
18562 return !!this.manager
;
18566 * Check if the window is visible.
18568 * @return {boolean} Window is visible
18570 OO
.ui
.Window
.prototype.isVisible = function () {
18571 return this.visible
;
18575 * Check if the window is opening.
18577 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
18580 * @return {boolean} Window is opening
18582 OO
.ui
.Window
.prototype.isOpening = function () {
18583 return this.manager
.isOpening( this );
18587 * Check if the window is closing.
18589 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
18591 * @return {boolean} Window is closing
18593 OO
.ui
.Window
.prototype.isClosing = function () {
18594 return this.manager
.isClosing( this );
18598 * Check if the window is opened.
18600 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
18602 * @return {boolean} Window is opened
18604 OO
.ui
.Window
.prototype.isOpened = function () {
18605 return this.manager
.isOpened( this );
18609 * Get the window manager.
18611 * All windows must be attached to a window manager, which is used to open
18612 * and close the window and control its presentation.
18614 * @return {OO.ui.WindowManager} Manager of window
18616 OO
.ui
.Window
.prototype.getManager = function () {
18617 return this.manager
;
18621 * Get the symbolic name of the window size (e.g., `small` or `medium`).
18623 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
18625 OO
.ui
.Window
.prototype.getSize = function () {
18626 var viewport
= OO
.ui
.Element
.static.getDimensions( this.getElementWindow() ),
18627 sizes
= this.manager
.constructor.static.sizes
,
18630 if ( !sizes
[ size
] ) {
18631 size
= this.manager
.constructor.static.defaultSize
;
18633 if ( size
!== 'full' && viewport
.rect
.right
- viewport
.rect
.left
< sizes
[ size
].width
) {
18641 * Get the size properties associated with the current window size
18643 * @return {Object} Size properties
18645 OO
.ui
.Window
.prototype.getSizeProperties = function () {
18646 return this.manager
.constructor.static.sizes
[ this.getSize() ];
18650 * Disable transitions on window's frame for the duration of the callback function, then enable them
18654 * @param {Function} callback Function to call while transitions are disabled
18656 OO
.ui
.Window
.prototype.withoutSizeTransitions = function ( callback
) {
18657 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
18658 // Disable transitions first, otherwise we'll get values from when the window was animating.
18660 styleObj
= this.$frame
[ 0 ].style
;
18661 oldTransition
= styleObj
.transition
|| styleObj
.OTransition
|| styleObj
.MsTransition
||
18662 styleObj
.MozTransition
|| styleObj
.WebkitTransition
;
18663 styleObj
.transition
= styleObj
.OTransition
= styleObj
.MsTransition
=
18664 styleObj
.MozTransition
= styleObj
.WebkitTransition
= 'none';
18666 // Force reflow to make sure the style changes done inside callback really are not transitioned
18667 this.$frame
.height();
18668 styleObj
.transition
= styleObj
.OTransition
= styleObj
.MsTransition
=
18669 styleObj
.MozTransition
= styleObj
.WebkitTransition
= oldTransition
;
18673 * Get the height of the full window contents (i.e., the window head, body and foot together).
18675 * What consistitutes the head, body, and foot varies depending on the window type.
18676 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
18677 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
18678 * and special actions in the head, and dialog content in the body.
18680 * To get just the height of the dialog body, use the #getBodyHeight method.
18682 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
18684 OO
.ui
.Window
.prototype.getContentHeight = function () {
18687 bodyStyleObj
= this.$body
[ 0 ].style
,
18688 frameStyleObj
= this.$frame
[ 0 ].style
;
18690 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
18691 // Disable transitions first, otherwise we'll get values from when the window was animating.
18692 this.withoutSizeTransitions( function () {
18693 var oldHeight
= frameStyleObj
.height
,
18694 oldPosition
= bodyStyleObj
.position
;
18695 frameStyleObj
.height
= '1px';
18696 // Force body to resize to new width
18697 bodyStyleObj
.position
= 'relative';
18698 bodyHeight
= win
.getBodyHeight();
18699 frameStyleObj
.height
= oldHeight
;
18700 bodyStyleObj
.position
= oldPosition
;
18704 // Add buffer for border
18705 ( this.$frame
.outerHeight() - this.$frame
.innerHeight() ) +
18706 // Use combined heights of children
18707 ( this.$head
.outerHeight( true ) + bodyHeight
+ this.$foot
.outerHeight( true ) )
18712 * Get the height of the window body.
18714 * To get the height of the full window contents (the window body, head, and foot together),
18715 * use #getContentHeight.
18717 * When this function is called, the window will temporarily have been resized
18718 * to height=1px, so .scrollHeight measurements can be taken accurately.
18720 * @return {number} Height of the window body in pixels
18722 OO
.ui
.Window
.prototype.getBodyHeight = function () {
18723 return this.$body
[ 0 ].scrollHeight
;
18727 * Get the directionality of the frame (right-to-left or left-to-right).
18729 * @return {string} Directionality: `'ltr'` or `'rtl'`
18731 OO
.ui
.Window
.prototype.getDir = function () {
18732 return OO
.ui
.Element
.static.getDir( this.$content
) || 'ltr';
18736 * Get the 'setup' process.
18738 * The setup process is used to set up a window for use in a particular context,
18739 * based on the `data` argument. This method is called during the opening phase of the window’s
18742 * Override this method to add additional steps to the ‘setup’ process the parent method provides
18743 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
18744 * of OO.ui.Process.
18746 * To add window content that persists between openings, you may wish to use the #initialize method
18749 * @param {Object} [data] Window opening data
18750 * @return {OO.ui.Process} Setup process
18752 OO
.ui
.Window
.prototype.getSetupProcess = function () {
18753 return new OO
.ui
.Process();
18757 * Get the ‘ready’ process.
18759 * The ready process is used to ready a window for use in a particular
18760 * context, based on the `data` argument. This method is called during the opening phase of
18761 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
18763 * Override this method to add additional steps to the ‘ready’ process the parent method
18764 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
18765 * methods of OO.ui.Process.
18767 * @param {Object} [data] Window opening data
18768 * @return {OO.ui.Process} Ready process
18770 OO
.ui
.Window
.prototype.getReadyProcess = function () {
18771 return new OO
.ui
.Process();
18775 * Get the 'hold' process.
18777 * The hold proccess is used to keep a window from being used in a particular context,
18778 * based on the `data` argument. This method is called during the closing phase of the window’s
18781 * Override this method to add additional steps to the 'hold' process the parent method provides
18782 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
18783 * of OO.ui.Process.
18785 * @param {Object} [data] Window closing data
18786 * @return {OO.ui.Process} Hold process
18788 OO
.ui
.Window
.prototype.getHoldProcess = function () {
18789 return new OO
.ui
.Process();
18793 * Get the ‘teardown’ process.
18795 * The teardown process is used to teardown a window after use. During teardown,
18796 * user interactions within the window are conveyed and the window is closed, based on the `data`
18797 * argument. This method is called during the closing phase of the window’s lifecycle.
18799 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
18800 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
18801 * of OO.ui.Process.
18803 * @param {Object} [data] Window closing data
18804 * @return {OO.ui.Process} Teardown process
18806 OO
.ui
.Window
.prototype.getTeardownProcess = function () {
18807 return new OO
.ui
.Process();
18811 * Set the window manager.
18813 * This will cause the window to initialize. Calling it more than once will cause an error.
18815 * @param {OO.ui.WindowManager} manager Manager for this window
18816 * @throws {Error} An error is thrown if the method is called more than once
18819 OO
.ui
.Window
.prototype.setManager = function ( manager
) {
18820 if ( this.manager
) {
18821 throw new Error( 'Cannot set window manager, window already has a manager' );
18824 this.manager
= manager
;
18831 * Set the window size by symbolic name (e.g., 'small' or 'medium')
18833 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
18837 OO
.ui
.Window
.prototype.setSize = function ( size
) {
18844 * Update the window size.
18846 * @throws {Error} An error is thrown if the window is not attached to a window manager
18849 OO
.ui
.Window
.prototype.updateSize = function () {
18850 if ( !this.manager
) {
18851 throw new Error( 'Cannot update window size, must be attached to a manager' );
18854 this.manager
.updateWindowSize( this );
18860 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
18861 * when the window is opening. In general, setDimensions should not be called directly.
18863 * To set the size of the window, use the #setSize method.
18865 * @param {Object} dim CSS dimension properties
18866 * @param {string|number} [dim.width] Width
18867 * @param {string|number} [dim.minWidth] Minimum width
18868 * @param {string|number} [dim.maxWidth] Maximum width
18869 * @param {string|number} [dim.width] Height, omit to set based on height of contents
18870 * @param {string|number} [dim.minWidth] Minimum height
18871 * @param {string|number} [dim.maxWidth] Maximum height
18874 OO
.ui
.Window
.prototype.setDimensions = function ( dim
) {
18877 styleObj
= this.$frame
[ 0 ].style
;
18879 // Calculate the height we need to set using the correct width
18880 if ( dim
.height
=== undefined ) {
18881 this.withoutSizeTransitions( function () {
18882 var oldWidth
= styleObj
.width
;
18883 win
.$frame
.css( 'width', dim
.width
|| '' );
18884 height
= win
.getContentHeight();
18885 styleObj
.width
= oldWidth
;
18888 height
= dim
.height
;
18892 width
: dim
.width
|| '',
18893 minWidth
: dim
.minWidth
|| '',
18894 maxWidth
: dim
.maxWidth
|| '',
18895 height
: height
|| '',
18896 minHeight
: dim
.minHeight
|| '',
18897 maxHeight
: dim
.maxHeight
|| ''
18904 * Initialize window contents.
18906 * Before the window is opened for the first time, #initialize is called so that content that
18907 * persists between openings can be added to the window.
18909 * To set up a window with new content each time the window opens, use #getSetupProcess.
18911 * @throws {Error} An error is thrown if the window is not attached to a window manager
18914 OO
.ui
.Window
.prototype.initialize = function () {
18915 if ( !this.manager
) {
18916 throw new Error( 'Cannot initialize window, must be attached to a manager' );
18920 this.$head
= $( '<div>' );
18921 this.$body
= $( '<div>' );
18922 this.$foot
= $( '<div>' );
18923 this.$document
= $( this.getElementDocument() );
18926 this.$element
.on( 'mousedown', this.onMouseDown
.bind( this ) );
18929 this.$head
.addClass( 'oo-ui-window-head' );
18930 this.$body
.addClass( 'oo-ui-window-body' );
18931 this.$foot
.addClass( 'oo-ui-window-foot' );
18932 this.$content
.append( this.$head
, this.$body
, this.$foot
);
18938 * Called when someone tries to focus the hidden element at the end of the dialog.
18939 * Sends focus back to the start of the dialog.
18941 * @param {jQuery.Event} event Focus event
18943 OO
.ui
.Window
.prototype.onFocusTrapFocused = function ( event
) {
18944 if ( this.$focusTrapBefore
.is( event
.target
) ) {
18945 OO
.ui
.findFocusable( this.$content
, true ).focus();
18947 // this.$content is the part of the focus cycle, and is the first focusable element
18948 this.$content
.focus();
18955 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
18956 * method, which returns a promise resolved when the window is done opening.
18958 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
18960 * @param {Object} [data] Window opening data
18961 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
18962 * if the window fails to open. When the promise is resolved successfully, the first argument of the
18963 * value is a new promise, which is resolved when the window begins closing.
18964 * @throws {Error} An error is thrown if the window is not attached to a window manager
18966 OO
.ui
.Window
.prototype.open = function ( data
) {
18967 if ( !this.manager
) {
18968 throw new Error( 'Cannot open window, must be attached to a manager' );
18971 return this.manager
.openWindow( this, data
);
18975 * Close the window.
18977 * This method is a wrapper around a call to the window
18978 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
18979 * which returns a closing promise resolved when the window is done closing.
18981 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
18982 * phase of the window’s lifecycle and can be used to specify closing behavior each time
18983 * the window closes.
18985 * @param {Object} [data] Window closing data
18986 * @return {jQuery.Promise} Promise resolved when window is closed
18987 * @throws {Error} An error is thrown if the window is not attached to a window manager
18989 OO
.ui
.Window
.prototype.close = function ( data
) {
18990 if ( !this.manager
) {
18991 throw new Error( 'Cannot close window, must be attached to a manager' );
18994 return this.manager
.closeWindow( this, data
);
19000 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
19001 * by other systems.
19003 * @param {Object} [data] Window opening data
19004 * @return {jQuery.Promise} Promise resolved when window is setup
19006 OO
.ui
.Window
.prototype.setup = function ( data
) {
19009 this.toggle( true );
19011 this.focusTrapHandler
= OO
.ui
.bind( this.onFocusTrapFocused
, this );
19012 this.$focusTraps
.on( 'focus', this.focusTrapHandler
);
19014 return this.getSetupProcess( data
).execute().then( function () {
19015 // Force redraw by asking the browser to measure the elements' widths
19016 win
.$element
.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
19017 win
.$content
.addClass( 'oo-ui-window-content-setup' ).width();
19024 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
19025 * by other systems.
19027 * @param {Object} [data] Window opening data
19028 * @return {jQuery.Promise} Promise resolved when window is ready
19030 OO
.ui
.Window
.prototype.ready = function ( data
) {
19033 this.$content
.focus();
19034 return this.getReadyProcess( data
).execute().then( function () {
19035 // Force redraw by asking the browser to measure the elements' widths
19036 win
.$element
.addClass( 'oo-ui-window-ready' ).width();
19037 win
.$content
.addClass( 'oo-ui-window-content-ready' ).width();
19044 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
19045 * by other systems.
19047 * @param {Object} [data] Window closing data
19048 * @return {jQuery.Promise} Promise resolved when window is held
19050 OO
.ui
.Window
.prototype.hold = function ( data
) {
19053 return this.getHoldProcess( data
).execute().then( function () {
19054 // Get the focused element within the window's content
19055 var $focus
= win
.$content
.find( OO
.ui
.Element
.static.getDocument( win
.$content
).activeElement
);
19057 // Blur the focused element
19058 if ( $focus
.length
) {
19059 $focus
[ 0 ].blur();
19062 // Force redraw by asking the browser to measure the elements' widths
19063 win
.$element
.removeClass( 'oo-ui-window-ready' ).width();
19064 win
.$content
.removeClass( 'oo-ui-window-content-ready' ).width();
19071 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
19072 * by other systems.
19074 * @param {Object} [data] Window closing data
19075 * @return {jQuery.Promise} Promise resolved when window is torn down
19077 OO
.ui
.Window
.prototype.teardown = function ( data
) {
19080 return this.getTeardownProcess( data
).execute().then( function () {
19081 // Force redraw by asking the browser to measure the elements' widths
19082 win
.$element
.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
19083 win
.$content
.removeClass( 'oo-ui-window-content-setup' ).width();
19084 win
.$focusTraps
.off( 'focus', win
.focusTrapHandler
);
19085 win
.toggle( false );
19090 * The Dialog class serves as the base class for the other types of dialogs.
19091 * Unless extended to include controls, the rendered dialog box is a simple window
19092 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
19093 * which opens, closes, and controls the presentation of the window. See the
19094 * [OOjs UI documentation on MediaWiki] [1] for more information.
19097 * // A simple dialog window.
19098 * function MyDialog( config ) {
19099 * MyDialog.parent.call( this, config );
19101 * OO.inheritClass( MyDialog, OO.ui.Dialog );
19102 * MyDialog.prototype.initialize = function () {
19103 * MyDialog.parent.prototype.initialize.call( this );
19104 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
19105 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
19106 * this.$body.append( this.content.$element );
19108 * MyDialog.prototype.getBodyHeight = function () {
19109 * return this.content.$element.outerHeight( true );
19111 * var myDialog = new MyDialog( {
19114 * // Create and append a window manager, which opens and closes the window.
19115 * var windowManager = new OO.ui.WindowManager();
19116 * $( 'body' ).append( windowManager.$element );
19117 * windowManager.addWindows( [ myDialog ] );
19118 * // Open the window!
19119 * windowManager.openWindow( myDialog );
19121 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
19125 * @extends OO.ui.Window
19126 * @mixins OO.ui.mixin.PendingElement
19129 * @param {Object} [config] Configuration options
19131 OO
.ui
.Dialog
= function OoUiDialog( config
) {
19132 // Parent constructor
19133 OO
.ui
.Dialog
.parent
.call( this, config
);
19135 // Mixin constructors
19136 OO
.ui
.mixin
.PendingElement
.call( this );
19139 this.actions
= new OO
.ui
.ActionSet();
19140 this.attachedActions
= [];
19141 this.currentAction
= null;
19142 this.onDialogKeyDownHandler
= this.onDialogKeyDown
.bind( this );
19145 this.actions
.connect( this, {
19146 click
: 'onActionClick',
19147 resize
: 'onActionResize',
19148 change
: 'onActionsChange'
19153 .addClass( 'oo-ui-dialog' )
19154 .attr( 'role', 'dialog' );
19159 OO
.inheritClass( OO
.ui
.Dialog
, OO
.ui
.Window
);
19160 OO
.mixinClass( OO
.ui
.Dialog
, OO
.ui
.mixin
.PendingElement
);
19162 /* Static Properties */
19165 * Symbolic name of dialog.
19167 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
19168 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
19170 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
19175 * @property {string}
19177 OO
.ui
.Dialog
.static.name
= '';
19180 * The dialog title.
19182 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
19183 * that will produce a Label node or string. The title can also be specified with data passed to the
19184 * constructor (see #getSetupProcess). In this case, the static value will be overridden.
19189 * @property {jQuery|string|Function}
19191 OO
.ui
.Dialog
.static.title
= '';
19194 * An array of configured {@link OO.ui.ActionWidget action widgets}.
19196 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
19197 * value will be overridden.
19199 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
19203 * @property {Object[]}
19205 OO
.ui
.Dialog
.static.actions
= [];
19208 * Close the dialog when the 'Esc' key is pressed.
19213 * @property {boolean}
19215 OO
.ui
.Dialog
.static.escapable
= true;
19220 * Handle frame document key down events.
19223 * @param {jQuery.Event} e Key down event
19225 OO
.ui
.Dialog
.prototype.onDialogKeyDown = function ( e
) {
19226 if ( e
.which
=== OO
.ui
.Keys
.ESCAPE
) {
19227 this.executeAction( '' );
19228 e
.preventDefault();
19229 e
.stopPropagation();
19234 * Handle action resized events.
19237 * @param {OO.ui.ActionWidget} action Action that was resized
19239 OO
.ui
.Dialog
.prototype.onActionResize = function () {
19240 // Override in subclass
19244 * Handle action click events.
19247 * @param {OO.ui.ActionWidget} action Action that was clicked
19249 OO
.ui
.Dialog
.prototype.onActionClick = function ( action
) {
19250 if ( !this.isPending() ) {
19251 this.executeAction( action
.getAction() );
19256 * Handle actions change event.
19260 OO
.ui
.Dialog
.prototype.onActionsChange = function () {
19261 this.detachActions();
19262 if ( !this.isClosing() ) {
19263 this.attachActions();
19268 * Get the set of actions used by the dialog.
19270 * @return {OO.ui.ActionSet}
19272 OO
.ui
.Dialog
.prototype.getActions = function () {
19273 return this.actions
;
19277 * Get a process for taking action.
19279 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
19280 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
19281 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
19283 * @param {string} [action] Symbolic name of action
19284 * @return {OO.ui.Process} Action process
19286 OO
.ui
.Dialog
.prototype.getActionProcess = function ( action
) {
19287 return new OO
.ui
.Process()
19288 .next( function () {
19290 // An empty action always closes the dialog without data, which should always be
19291 // safe and make no changes
19300 * @param {Object} [data] Dialog opening data
19301 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
19302 * the {@link #static-title static title}
19303 * @param {Object[]} [data.actions] List of configuration options for each
19304 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
19306 OO
.ui
.Dialog
.prototype.getSetupProcess = function ( data
) {
19310 return OO
.ui
.Dialog
.parent
.prototype.getSetupProcess
.call( this, data
)
19311 .next( function () {
19312 var config
= this.constructor.static,
19313 actions
= data
.actions
!== undefined ? data
.actions
: config
.actions
;
19315 this.title
.setLabel(
19316 data
.title
!== undefined ? data
.title
: this.constructor.static.title
19318 this.actions
.add( this.getActionWidgets( actions
) );
19320 if ( this.constructor.static.escapable
) {
19321 this.$element
.on( 'keydown', this.onDialogKeyDownHandler
);
19329 OO
.ui
.Dialog
.prototype.getTeardownProcess = function ( data
) {
19331 return OO
.ui
.Dialog
.parent
.prototype.getTeardownProcess
.call( this, data
)
19332 .first( function () {
19333 if ( this.constructor.static.escapable
) {
19334 this.$element
.off( 'keydown', this.onDialogKeyDownHandler
);
19337 this.actions
.clear();
19338 this.currentAction
= null;
19345 OO
.ui
.Dialog
.prototype.initialize = function () {
19349 OO
.ui
.Dialog
.parent
.prototype.initialize
.call( this );
19351 titleId
= OO
.ui
.generateElementId();
19354 this.title
= new OO
.ui
.LabelWidget( {
19359 this.$content
.addClass( 'oo-ui-dialog-content' );
19360 this.$element
.attr( 'aria-labelledby', titleId
);
19361 this.setPendingElement( this.$head
);
19365 * Get action widgets from a list of configs
19367 * @param {Object[]} actions Action widget configs
19368 * @return {OO.ui.ActionWidget[]} Action widgets
19370 OO
.ui
.Dialog
.prototype.getActionWidgets = function ( actions
) {
19371 var i
, len
, widgets
= [];
19372 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
19374 new OO
.ui
.ActionWidget( actions
[ i
] )
19381 * Attach action actions.
19385 OO
.ui
.Dialog
.prototype.attachActions = function () {
19386 // Remember the list of potentially attached actions
19387 this.attachedActions
= this.actions
.get();
19391 * Detach action actions.
19396 OO
.ui
.Dialog
.prototype.detachActions = function () {
19399 // Detach all actions that may have been previously attached
19400 for ( i
= 0, len
= this.attachedActions
.length
; i
< len
; i
++ ) {
19401 this.attachedActions
[ i
].$element
.detach();
19403 this.attachedActions
= [];
19407 * Execute an action.
19409 * @param {string} action Symbolic name of action to execute
19410 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
19412 OO
.ui
.Dialog
.prototype.executeAction = function ( action
) {
19413 this.pushPending();
19414 this.currentAction
= action
;
19415 return this.getActionProcess( action
).execute()
19416 .always( this.popPending
.bind( this ) );
19420 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
19421 * consists of a header that contains the dialog title, a body with the message, and a footer that
19422 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
19423 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
19425 * There are two basic types of message dialogs, confirmation and alert:
19427 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
19428 * more details about the consequences.
19429 * - **alert**: the dialog title describes which event occurred and the message provides more information
19430 * about why the event occurred.
19432 * The MessageDialog class specifies two actions: ‘accept’, the primary
19433 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
19434 * passing along the selected action.
19436 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
19439 * // Example: Creating and opening a message dialog window.
19440 * var messageDialog = new OO.ui.MessageDialog();
19442 * // Create and append a window manager.
19443 * var windowManager = new OO.ui.WindowManager();
19444 * $( 'body' ).append( windowManager.$element );
19445 * windowManager.addWindows( [ messageDialog ] );
19446 * // Open the window.
19447 * windowManager.openWindow( messageDialog, {
19448 * title: 'Basic message dialog',
19449 * message: 'This is the message'
19452 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
19455 * @extends OO.ui.Dialog
19458 * @param {Object} [config] Configuration options
19460 OO
.ui
.MessageDialog
= function OoUiMessageDialog( config
) {
19461 // Parent constructor
19462 OO
.ui
.MessageDialog
.parent
.call( this, config
);
19465 this.verticalActionLayout
= null;
19468 this.$element
.addClass( 'oo-ui-messageDialog' );
19473 OO
.inheritClass( OO
.ui
.MessageDialog
, OO
.ui
.Dialog
);
19475 /* Static Properties */
19477 OO
.ui
.MessageDialog
.static.name
= 'message';
19479 OO
.ui
.MessageDialog
.static.size
= 'small';
19481 OO
.ui
.MessageDialog
.static.verbose
= false;
19486 * The title of a confirmation dialog describes what a progressive action will do. The
19487 * title of an alert dialog describes which event occurred.
19491 * @property {jQuery|string|Function|null}
19493 OO
.ui
.MessageDialog
.static.title
= null;
19496 * The message displayed in the dialog body.
19498 * A confirmation message describes the consequences of a progressive action. An alert
19499 * message describes why an event occurred.
19503 * @property {jQuery|string|Function|null}
19505 OO
.ui
.MessageDialog
.static.message
= null;
19507 // Note that OO.ui.alert() and OO.ui.confirm() rely on these.
19508 OO
.ui
.MessageDialog
.static.actions
= [
19509 { action
: 'accept', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-accept' ), flags
: 'primary' },
19510 { action
: 'reject', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-reject' ), flags
: 'safe' }
19518 OO
.ui
.MessageDialog
.prototype.setManager = function ( manager
) {
19519 OO
.ui
.MessageDialog
.parent
.prototype.setManager
.call( this, manager
);
19522 this.manager
.connect( this, {
19532 OO
.ui
.MessageDialog
.prototype.onActionResize = function ( action
) {
19534 return OO
.ui
.MessageDialog
.parent
.prototype.onActionResize
.call( this, action
);
19538 * Handle window resized events.
19542 OO
.ui
.MessageDialog
.prototype.onResize = function () {
19544 dialog
.fitActions();
19545 // Wait for CSS transition to finish and do it again :(
19546 setTimeout( function () {
19547 dialog
.fitActions();
19552 * Toggle action layout between vertical and horizontal.
19555 * @param {boolean} [value] Layout actions vertically, omit to toggle
19558 OO
.ui
.MessageDialog
.prototype.toggleVerticalActionLayout = function ( value
) {
19559 value
= value
=== undefined ? !this.verticalActionLayout
: !!value
;
19561 if ( value
!== this.verticalActionLayout
) {
19562 this.verticalActionLayout
= value
;
19564 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value
)
19565 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value
);
19574 OO
.ui
.MessageDialog
.prototype.getActionProcess = function ( action
) {
19576 return new OO
.ui
.Process( function () {
19577 this.close( { action
: action
} );
19580 return OO
.ui
.MessageDialog
.parent
.prototype.getActionProcess
.call( this, action
);
19586 * @param {Object} [data] Dialog opening data
19587 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
19588 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
19589 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
19590 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
19593 OO
.ui
.MessageDialog
.prototype.getSetupProcess = function ( data
) {
19597 return OO
.ui
.MessageDialog
.parent
.prototype.getSetupProcess
.call( this, data
)
19598 .next( function () {
19599 this.title
.setLabel(
19600 data
.title
!== undefined ? data
.title
: this.constructor.static.title
19602 this.message
.setLabel(
19603 data
.message
!== undefined ? data
.message
: this.constructor.static.message
19605 this.message
.$element
.toggleClass(
19606 'oo-ui-messageDialog-message-verbose',
19607 data
.verbose
!== undefined ? data
.verbose
: this.constructor.static.verbose
19615 OO
.ui
.MessageDialog
.prototype.getReadyProcess = function ( data
) {
19619 return OO
.ui
.MessageDialog
.parent
.prototype.getReadyProcess
.call( this, data
)
19620 .next( function () {
19621 // Focus the primary action button
19622 var actions
= this.actions
.get();
19623 actions
= actions
.filter( function ( action
) {
19624 return action
.getFlags().indexOf( 'primary' ) > -1;
19626 if ( actions
.length
> 0 ) {
19627 actions
[ 0 ].$button
.focus();
19635 OO
.ui
.MessageDialog
.prototype.getBodyHeight = function () {
19636 var bodyHeight
, oldOverflow
,
19637 $scrollable
= this.container
.$element
;
19639 oldOverflow
= $scrollable
[ 0 ].style
.overflow
;
19640 $scrollable
[ 0 ].style
.overflow
= 'hidden';
19642 OO
.ui
.Element
.static.reconsiderScrollbars( $scrollable
[ 0 ] );
19644 bodyHeight
= this.text
.$element
.outerHeight( true );
19645 $scrollable
[ 0 ].style
.overflow
= oldOverflow
;
19653 OO
.ui
.MessageDialog
.prototype.setDimensions = function ( dim
) {
19654 var $scrollable
= this.container
.$element
;
19655 OO
.ui
.MessageDialog
.parent
.prototype.setDimensions
.call( this, dim
);
19657 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
19658 // Need to do it after transition completes (250ms), add 50ms just in case.
19659 setTimeout( function () {
19660 var oldOverflow
= $scrollable
[ 0 ].style
.overflow
;
19661 $scrollable
[ 0 ].style
.overflow
= 'hidden';
19663 OO
.ui
.Element
.static.reconsiderScrollbars( $scrollable
[ 0 ] );
19665 $scrollable
[ 0 ].style
.overflow
= oldOverflow
;
19674 OO
.ui
.MessageDialog
.prototype.initialize = function () {
19676 OO
.ui
.MessageDialog
.parent
.prototype.initialize
.call( this );
19679 this.$actions
= $( '<div>' );
19680 this.container
= new OO
.ui
.PanelLayout( {
19681 scrollable
: true, classes
: [ 'oo-ui-messageDialog-container' ]
19683 this.text
= new OO
.ui
.PanelLayout( {
19684 padded
: true, expanded
: false, classes
: [ 'oo-ui-messageDialog-text' ]
19686 this.message
= new OO
.ui
.LabelWidget( {
19687 classes
: [ 'oo-ui-messageDialog-message' ]
19691 this.title
.$element
.addClass( 'oo-ui-messageDialog-title' );
19692 this.$content
.addClass( 'oo-ui-messageDialog-content' );
19693 this.container
.$element
.append( this.text
.$element
);
19694 this.text
.$element
.append( this.title
.$element
, this.message
.$element
);
19695 this.$body
.append( this.container
.$element
);
19696 this.$actions
.addClass( 'oo-ui-messageDialog-actions' );
19697 this.$foot
.append( this.$actions
);
19703 OO
.ui
.MessageDialog
.prototype.attachActions = function () {
19704 var i
, len
, other
, special
, others
;
19707 OO
.ui
.MessageDialog
.parent
.prototype.attachActions
.call( this );
19709 special
= this.actions
.getSpecial();
19710 others
= this.actions
.getOthers();
19712 if ( special
.safe
) {
19713 this.$actions
.append( special
.safe
.$element
);
19714 special
.safe
.toggleFramed( false );
19716 if ( others
.length
) {
19717 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
19718 other
= others
[ i
];
19719 this.$actions
.append( other
.$element
);
19720 other
.toggleFramed( false );
19723 if ( special
.primary
) {
19724 this.$actions
.append( special
.primary
.$element
);
19725 special
.primary
.toggleFramed( false );
19728 if ( !this.isOpening() ) {
19729 // If the dialog is currently opening, this will be called automatically soon.
19730 // This also calls #fitActions.
19736 * Fit action actions into columns or rows.
19738 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
19742 OO
.ui
.MessageDialog
.prototype.fitActions = function () {
19743 var i
, len
, action
,
19744 previous
= this.verticalActionLayout
,
19745 actions
= this.actions
.get();
19748 this.toggleVerticalActionLayout( false );
19749 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
19750 action
= actions
[ i
];
19751 if ( action
.$element
.innerWidth() < action
.$label
.outerWidth( true ) ) {
19752 this.toggleVerticalActionLayout( true );
19757 // Move the body out of the way of the foot
19758 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
19760 if ( this.verticalActionLayout
!== previous
) {
19761 // We changed the layout, window height might need to be updated.
19767 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
19768 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
19769 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
19770 * relevant. The ProcessDialog class is always extended and customized with the actions and content
19771 * required for each process.
19773 * The process dialog box consists of a header that visually represents the ‘working’ state of long
19774 * processes with an animation. The header contains the dialog title as well as
19775 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
19776 * a ‘primary’ action on the right (e.g., ‘Done’).
19778 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
19779 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
19782 * // Example: Creating and opening a process dialog window.
19783 * function MyProcessDialog( config ) {
19784 * MyProcessDialog.parent.call( this, config );
19786 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
19788 * MyProcessDialog.static.title = 'Process dialog';
19789 * MyProcessDialog.static.actions = [
19790 * { action: 'save', label: 'Done', flags: 'primary' },
19791 * { label: 'Cancel', flags: 'safe' }
19794 * MyProcessDialog.prototype.initialize = function () {
19795 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
19796 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
19797 * this.content.$element.append( '<p>This is a process dialog window. The header contains the title and two buttons: \'Cancel\' (a safe action) on the left and \'Done\' (a primary action) on the right.</p>' );
19798 * this.$body.append( this.content.$element );
19800 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
19801 * var dialog = this;
19803 * return new OO.ui.Process( function () {
19804 * dialog.close( { action: action } );
19807 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
19810 * var windowManager = new OO.ui.WindowManager();
19811 * $( 'body' ).append( windowManager.$element );
19813 * var dialog = new MyProcessDialog();
19814 * windowManager.addWindows( [ dialog ] );
19815 * windowManager.openWindow( dialog );
19817 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
19821 * @extends OO.ui.Dialog
19824 * @param {Object} [config] Configuration options
19826 OO
.ui
.ProcessDialog
= function OoUiProcessDialog( config
) {
19827 // Parent constructor
19828 OO
.ui
.ProcessDialog
.parent
.call( this, config
);
19831 this.fitOnOpen
= false;
19834 this.$element
.addClass( 'oo-ui-processDialog' );
19839 OO
.inheritClass( OO
.ui
.ProcessDialog
, OO
.ui
.Dialog
);
19844 * Handle dismiss button click events.
19850 OO
.ui
.ProcessDialog
.prototype.onDismissErrorButtonClick = function () {
19855 * Handle retry button click events.
19857 * Hides errors and then tries again.
19861 OO
.ui
.ProcessDialog
.prototype.onRetryButtonClick = function () {
19863 this.executeAction( this.currentAction
);
19869 OO
.ui
.ProcessDialog
.prototype.onActionResize = function ( action
) {
19870 if ( this.actions
.isSpecial( action
) ) {
19873 return OO
.ui
.ProcessDialog
.parent
.prototype.onActionResize
.call( this, action
);
19879 OO
.ui
.ProcessDialog
.prototype.initialize = function () {
19881 OO
.ui
.ProcessDialog
.parent
.prototype.initialize
.call( this );
19884 this.$navigation
= $( '<div>' );
19885 this.$location
= $( '<div>' );
19886 this.$safeActions
= $( '<div>' );
19887 this.$primaryActions
= $( '<div>' );
19888 this.$otherActions
= $( '<div>' );
19889 this.dismissButton
= new OO
.ui
.ButtonWidget( {
19890 label
: OO
.ui
.msg( 'ooui-dialog-process-dismiss' )
19892 this.retryButton
= new OO
.ui
.ButtonWidget();
19893 this.$errors
= $( '<div>' );
19894 this.$errorsTitle
= $( '<div>' );
19897 this.dismissButton
.connect( this, { click
: 'onDismissErrorButtonClick' } );
19898 this.retryButton
.connect( this, { click
: 'onRetryButtonClick' } );
19901 this.title
.$element
.addClass( 'oo-ui-processDialog-title' );
19903 .append( this.title
.$element
)
19904 .addClass( 'oo-ui-processDialog-location' );
19905 this.$safeActions
.addClass( 'oo-ui-processDialog-actions-safe' );
19906 this.$primaryActions
.addClass( 'oo-ui-processDialog-actions-primary' );
19907 this.$otherActions
.addClass( 'oo-ui-processDialog-actions-other' );
19909 .addClass( 'oo-ui-processDialog-errors-title' )
19910 .text( OO
.ui
.msg( 'ooui-dialog-process-error' ) );
19912 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
19913 .append( this.$errorsTitle
, this.dismissButton
.$element
, this.retryButton
.$element
);
19915 .addClass( 'oo-ui-processDialog-content' )
19916 .append( this.$errors
);
19918 .addClass( 'oo-ui-processDialog-navigation' )
19919 .append( this.$safeActions
, this.$location
, this.$primaryActions
);
19920 this.$head
.append( this.$navigation
);
19921 this.$foot
.append( this.$otherActions
);
19927 OO
.ui
.ProcessDialog
.prototype.getActionWidgets = function ( actions
) {
19928 var i
, len
, widgets
= [];
19929 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
19931 new OO
.ui
.ActionWidget( $.extend( { framed
: true }, actions
[ i
] ) )
19940 OO
.ui
.ProcessDialog
.prototype.attachActions = function () {
19941 var i
, len
, other
, special
, others
;
19944 OO
.ui
.ProcessDialog
.parent
.prototype.attachActions
.call( this );
19946 special
= this.actions
.getSpecial();
19947 others
= this.actions
.getOthers();
19948 if ( special
.primary
) {
19949 this.$primaryActions
.append( special
.primary
.$element
);
19951 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
19952 other
= others
[ i
];
19953 this.$otherActions
.append( other
.$element
);
19955 if ( special
.safe
) {
19956 this.$safeActions
.append( special
.safe
.$element
);
19960 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
19966 OO
.ui
.ProcessDialog
.prototype.executeAction = function ( action
) {
19967 var process
= this;
19968 return OO
.ui
.ProcessDialog
.parent
.prototype.executeAction
.call( this, action
)
19969 .fail( function ( errors
) {
19970 process
.showErrors( errors
|| [] );
19977 OO
.ui
.ProcessDialog
.prototype.setDimensions = function () {
19979 OO
.ui
.ProcessDialog
.parent
.prototype.setDimensions
.apply( this, arguments
);
19985 * Fit label between actions.
19990 OO
.ui
.ProcessDialog
.prototype.fitLabel = function () {
19991 var safeWidth
, primaryWidth
, biggerWidth
, labelWidth
, navigationWidth
, leftWidth
, rightWidth
,
19992 size
= this.getSizeProperties();
19994 if ( typeof size
.width
!== 'number' ) {
19995 if ( this.isOpened() ) {
19996 navigationWidth
= this.$head
.width() - 20;
19997 } else if ( this.isOpening() ) {
19998 if ( !this.fitOnOpen
) {
19999 // Size is relative and the dialog isn't open yet, so wait.
20000 this.manager
.opening
.done( this.fitLabel
.bind( this ) );
20001 this.fitOnOpen
= true;
20008 navigationWidth
= size
.width
- 20;
20011 safeWidth
= this.$safeActions
.is( ':visible' ) ? this.$safeActions
.width() : 0;
20012 primaryWidth
= this.$primaryActions
.is( ':visible' ) ? this.$primaryActions
.width() : 0;
20013 biggerWidth
= Math
.max( safeWidth
, primaryWidth
);
20015 labelWidth
= this.title
.$element
.width();
20017 if ( 2 * biggerWidth
+ labelWidth
< navigationWidth
) {
20018 // We have enough space to center the label
20019 leftWidth
= rightWidth
= biggerWidth
;
20021 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
20022 if ( this.getDir() === 'ltr' ) {
20023 leftWidth
= safeWidth
;
20024 rightWidth
= primaryWidth
;
20026 leftWidth
= primaryWidth
;
20027 rightWidth
= safeWidth
;
20031 this.$location
.css( { paddingLeft
: leftWidth
, paddingRight
: rightWidth
} );
20037 * Handle errors that occurred during accept or reject processes.
20040 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
20042 OO
.ui
.ProcessDialog
.prototype.showErrors = function ( errors
) {
20043 var i
, len
, $item
, actions
,
20046 recoverable
= true,
20049 if ( errors
instanceof OO
.ui
.Error
) {
20050 errors
= [ errors
];
20053 for ( i
= 0, len
= errors
.length
; i
< len
; i
++ ) {
20054 if ( !errors
[ i
].isRecoverable() ) {
20055 recoverable
= false;
20057 if ( errors
[ i
].isWarning() ) {
20060 $item
= $( '<div>' )
20061 .addClass( 'oo-ui-processDialog-error' )
20062 .append( errors
[ i
].getMessage() );
20063 items
.push( $item
[ 0 ] );
20065 this.$errorItems
= $( items
);
20066 if ( recoverable
) {
20067 abilities
[ this.currentAction
] = true;
20068 // Copy the flags from the first matching action
20069 actions
= this.actions
.get( { actions
: this.currentAction
} );
20070 if ( actions
.length
) {
20071 this.retryButton
.clearFlags().setFlags( actions
[ 0 ].getFlags() );
20074 abilities
[ this.currentAction
] = false;
20075 this.actions
.setAbilities( abilities
);
20078 this.retryButton
.setLabel( OO
.ui
.msg( 'ooui-dialog-process-continue' ) );
20080 this.retryButton
.setLabel( OO
.ui
.msg( 'ooui-dialog-process-retry' ) );
20082 this.retryButton
.toggle( recoverable
);
20083 this.$errorsTitle
.after( this.$errorItems
);
20084 this.$errors
.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
20092 OO
.ui
.ProcessDialog
.prototype.hideErrors = function () {
20093 this.$errors
.addClass( 'oo-ui-element-hidden' );
20094 if ( this.$errorItems
) {
20095 this.$errorItems
.remove();
20096 this.$errorItems
= null;
20103 OO
.ui
.ProcessDialog
.prototype.getTeardownProcess = function ( data
) {
20105 return OO
.ui
.ProcessDialog
.parent
.prototype.getTeardownProcess
.call( this, data
)
20106 .first( function () {
20107 // Make sure to hide errors
20109 this.fitOnOpen
= false;
20118 * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
20122 * @return {OO.ui.WindowManager}
20124 OO
.ui
.getWindowManager = function () {
20125 if ( !OO
.ui
.windowManager
) {
20126 OO
.ui
.windowManager
= new OO
.ui
.WindowManager();
20127 $( 'body' ).append( OO
.ui
.windowManager
.$element
);
20128 OO
.ui
.windowManager
.addWindows( {
20129 messageDialog
: new OO
.ui
.MessageDialog()
20132 return OO
.ui
.windowManager
;
20136 * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
20137 * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
20138 * has only one action button, labelled "OK", clicking it will simply close the dialog.
20140 * A window manager is created automatically when this function is called for the first time.
20143 * OO.ui.alert( 'Something happened!' ).done( function () {
20144 * console.log( 'User closed the dialog.' );
20147 * @param {jQuery|string} text Message text to display
20148 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
20149 * @return {jQuery.Promise} Promise resolved when the user closes the dialog
20151 OO
.ui
.alert = function ( text
, options
) {
20152 return OO
.ui
.getWindowManager().openWindow( 'messageDialog', $.extend( {
20155 actions
: [ OO
.ui
.MessageDialog
.static.actions
[ 0 ] ]
20156 }, options
) ).then( function ( opened
) {
20157 return opened
.then( function ( closing
) {
20158 return closing
.then( function () {
20159 return $.Deferred().resolve();
20166 * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
20167 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
20168 * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
20169 * (labelled "Cancel").
20171 * A window manager is created automatically when this function is called for the first time.
20174 * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
20175 * if ( confirmed ) {
20176 * console.log( 'User clicked "OK"!' );
20178 * console.log( 'User clicked "Cancel" or closed the dialog.' );
20182 * @param {jQuery|string} text Message text to display
20183 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
20184 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
20185 * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
20188 OO
.ui
.confirm = function ( text
, options
) {
20189 return OO
.ui
.getWindowManager().openWindow( 'messageDialog', $.extend( {
20192 }, options
) ).then( function ( opened
) {
20193 return opened
.then( function ( closing
) {
20194 return closing
.then( function ( data
) {
20195 return $.Deferred().resolve( !!( data
&& data
.action
=== 'accept' ) );