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-01-26T21:14:23Z
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
, '#' ) ) {
448 * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
452 * @return {OO.ui.WindowManager}
454 OO
.ui
.getWindowManager = function () {
455 if ( !OO
.ui
.windowManager
) {
456 OO
.ui
.windowManager
= new OO
.ui
.WindowManager();
457 $( 'body' ).append( OO
.ui
.windowManager
.$element
);
458 OO
.ui
.windowManager
.addWindows( {
459 messageDialog
: new OO
.ui
.MessageDialog()
462 return OO
.ui
.windowManager
;
466 * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
467 * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
468 * has only one action button, labelled "OK", clicking it will simply close the dialog.
470 * A window manager is created automatically when this function is called for the first time.
473 * OO.ui.alert( 'Something happened!' ).done( function () {
474 * console.log( 'User closed the dialog.' );
477 * @param {jQuery|string} text Message text to display
478 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
479 * @return {jQuery.Promise} Promise resolved when the user closes the dialog
481 OO
.ui
.alert = function ( text
, options
) {
482 return OO
.ui
.getWindowManager().openWindow( 'messageDialog', $.extend( {
485 actions
: [ OO
.ui
.MessageDialog
.static.actions
[ 0 ] ]
486 }, options
) ).then( function ( opened
) {
487 return opened
.then( function ( closing
) {
488 return closing
.then( function () {
489 return $.Deferred().resolve();
496 * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
497 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
498 * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
499 * (labelled "Cancel").
501 * A window manager is created automatically when this function is called for the first time.
504 * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
506 * console.log( 'User clicked "OK"!' );
508 * console.log( 'User clicked "Cancel" or closed the dialog.' );
512 * @param {jQuery|string} text Message text to display
513 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
514 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
515 * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
518 OO
.ui
.confirm = function ( text
, options
) {
519 return OO
.ui
.getWindowManager().openWindow( 'messageDialog', $.extend( {
522 }, options
) ).then( function ( opened
) {
523 return opened
.then( function ( closing
) {
524 return closing
.then( function ( data
) {
525 return $.Deferred().resolve( !!( data
&& data
.action
=== 'accept' ) );
536 * Namespace for OOjs UI mixins.
538 * Mixins are named according to the type of object they are intended to
539 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
540 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
541 * is intended to be mixed in to an instance of OO.ui.Widget.
549 * PendingElement is a mixin that is used to create elements that notify users that something is happening
550 * and that they should wait before proceeding. The pending state is visually represented with a pending
551 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
552 * field of a {@link OO.ui.TextInputWidget text input widget}.
554 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
555 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
556 * in process dialogs.
559 * function MessageDialog( config ) {
560 * MessageDialog.parent.call( this, config );
562 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
564 * MessageDialog.static.actions = [
565 * { action: 'save', label: 'Done', flags: 'primary' },
566 * { label: 'Cancel', flags: 'safe' }
569 * MessageDialog.prototype.initialize = function () {
570 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
571 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
572 * 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>' );
573 * this.$body.append( this.content.$element );
575 * MessageDialog.prototype.getBodyHeight = function () {
578 * MessageDialog.prototype.getActionProcess = function ( action ) {
580 * if ( action === 'save' ) {
581 * dialog.getActions().get({actions: 'save'})[0].pushPending();
582 * return new OO.ui.Process()
584 * .next( function () {
585 * dialog.getActions().get({actions: 'save'})[0].popPending();
588 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
591 * var windowManager = new OO.ui.WindowManager();
592 * $( 'body' ).append( windowManager.$element );
594 * var dialog = new MessageDialog();
595 * windowManager.addWindows( [ dialog ] );
596 * windowManager.openWindow( dialog );
602 * @param {Object} [config] Configuration options
603 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
605 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
606 // Configuration initialization
607 config
= config
|| {};
611 this.$pending
= null;
614 this.setPendingElement( config
.$pending
|| this.$element
);
619 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
624 * Set the pending element (and clean up any existing one).
626 * @param {jQuery} $pending The element to set to pending.
628 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
629 if ( this.$pending
) {
630 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
633 this.$pending
= $pending
;
634 if ( this.pending
> 0 ) {
635 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
640 * Check if an element is pending.
642 * @return {boolean} Element is pending
644 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
645 return !!this.pending
;
649 * Increase the pending counter. The pending state will remain active until the counter is zero
650 * (i.e., the number of calls to #pushPending and #popPending is the same).
654 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
655 if ( this.pending
=== 0 ) {
656 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
657 this.updateThemeClasses();
665 * Decrease the pending counter. The pending state will remain active until the counter is zero
666 * (i.e., the number of calls to #pushPending and #popPending is the same).
670 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
671 if ( this.pending
=== 1 ) {
672 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
673 this.updateThemeClasses();
675 this.pending
= Math
.max( 0, this.pending
- 1 );
681 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
682 * Actions can be made available for specific contexts (modes) and circumstances
683 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
685 * ActionSets contain two types of actions:
687 * - 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.
688 * - Other: Other actions include all non-special visible actions.
690 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
693 * // Example: An action set used in a process dialog
694 * function MyProcessDialog( config ) {
695 * MyProcessDialog.parent.call( this, config );
697 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
698 * MyProcessDialog.static.title = 'An action set in a process dialog';
699 * // An action set that uses modes ('edit' and 'help' mode, in this example).
700 * MyProcessDialog.static.actions = [
701 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
702 * { action: 'help', modes: 'edit', label: 'Help' },
703 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
704 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
707 * MyProcessDialog.prototype.initialize = function () {
708 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
709 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
710 * 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>' );
711 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
712 * 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>' );
713 * this.stackLayout = new OO.ui.StackLayout( {
714 * items: [ this.panel1, this.panel2 ]
716 * this.$body.append( this.stackLayout.$element );
718 * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
719 * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
720 * .next( function () {
721 * this.actions.setMode( 'edit' );
724 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
725 * if ( action === 'help' ) {
726 * this.actions.setMode( 'help' );
727 * this.stackLayout.setItem( this.panel2 );
728 * } else if ( action === 'back' ) {
729 * this.actions.setMode( 'edit' );
730 * this.stackLayout.setItem( this.panel1 );
731 * } else if ( action === 'continue' ) {
733 * return new OO.ui.Process( function () {
737 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
739 * MyProcessDialog.prototype.getBodyHeight = function () {
740 * return this.panel1.$element.outerHeight( true );
742 * var windowManager = new OO.ui.WindowManager();
743 * $( 'body' ).append( windowManager.$element );
744 * var dialog = new MyProcessDialog( {
747 * windowManager.addWindows( [ dialog ] );
748 * windowManager.openWindow( dialog );
750 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
754 * @mixins OO.EventEmitter
757 * @param {Object} [config] Configuration options
759 OO
.ui
.ActionSet
= function OoUiActionSet( config
) {
760 // Configuration initialization
761 config
= config
|| {};
763 // Mixin constructors
764 OO
.EventEmitter
.call( this );
769 actions
: 'getAction',
773 this.categorized
= {};
776 this.organized
= false;
777 this.changing
= false;
778 this.changed
= false;
783 OO
.mixinClass( OO
.ui
.ActionSet
, OO
.EventEmitter
);
785 /* Static Properties */
788 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
789 * header of a {@link OO.ui.ProcessDialog process dialog}.
790 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
792 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
799 OO
.ui
.ActionSet
.static.specialFlags
= [ 'safe', 'primary' ];
806 * A 'click' event is emitted when an action is clicked.
808 * @param {OO.ui.ActionWidget} action Action that was clicked
814 * A 'resize' event is emitted when an action widget is resized.
816 * @param {OO.ui.ActionWidget} action Action that was resized
822 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
824 * @param {OO.ui.ActionWidget[]} added Actions added
830 * A 'remove' event is emitted when actions are {@link #method-remove removed}
831 * or {@link #clear cleared}.
833 * @param {OO.ui.ActionWidget[]} added Actions removed
839 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
840 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
847 * Handle action change events.
852 OO
.ui
.ActionSet
.prototype.onActionChange = function () {
853 this.organized
= false;
854 if ( this.changing
) {
857 this.emit( 'change' );
862 * Check if an action is one of the special actions.
864 * @param {OO.ui.ActionWidget} action Action to check
865 * @return {boolean} Action is special
867 OO
.ui
.ActionSet
.prototype.isSpecial = function ( action
) {
870 for ( flag
in this.special
) {
871 if ( action
=== this.special
[ flag
] ) {
880 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
883 * @param {Object} [filters] Filters to use, omit to get all actions
884 * @param {string|string[]} [filters.actions] Actions that action widgets must have
885 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
886 * @param {string|string[]} [filters.modes] Modes that action widgets must have
887 * @param {boolean} [filters.visible] Action widgets must be visible
888 * @param {boolean} [filters.disabled] Action widgets must be disabled
889 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
891 OO
.ui
.ActionSet
.prototype.get = function ( filters
) {
892 var i
, len
, list
, category
, actions
, index
, match
, matches
;
897 // Collect category candidates
899 for ( category
in this.categorized
) {
900 list
= filters
[ category
];
902 if ( !Array
.isArray( list
) ) {
905 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
906 actions
= this.categorized
[ category
][ list
[ i
] ];
907 if ( Array
.isArray( actions
) ) {
908 matches
.push
.apply( matches
, actions
);
913 // Remove by boolean filters
914 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
915 match
= matches
[ i
];
917 ( filters
.visible
!== undefined && match
.isVisible() !== filters
.visible
) ||
918 ( filters
.disabled
!== undefined && match
.isDisabled() !== filters
.disabled
)
920 matches
.splice( i
, 1 );
926 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
927 match
= matches
[ i
];
928 index
= matches
.lastIndexOf( match
);
929 while ( index
!== i
) {
930 matches
.splice( index
, 1 );
932 index
= matches
.lastIndexOf( match
);
937 return this.list
.slice();
941 * Get 'special' actions.
943 * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
944 * Special flags can be configured in subclasses by changing the static #specialFlags property.
946 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
948 OO
.ui
.ActionSet
.prototype.getSpecial = function () {
950 return $.extend( {}, this.special
);
954 * Get 'other' actions.
956 * Other actions include all non-special visible action widgets.
958 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
960 OO
.ui
.ActionSet
.prototype.getOthers = function () {
962 return this.others
.slice();
966 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
967 * to be available in the specified mode will be made visible. All other actions will be hidden.
969 * @param {string} mode The mode. Only actions configured to be available in the specified
970 * mode will be made visible.
975 OO
.ui
.ActionSet
.prototype.setMode = function ( mode
) {
978 this.changing
= true;
979 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
980 action
= this.list
[ i
];
981 action
.toggle( action
.hasMode( mode
) );
984 this.organized
= false;
985 this.changing
= false;
986 this.emit( 'change' );
992 * Set the abilities of the specified actions.
994 * Action widgets that are configured with the specified actions will be enabled
995 * or disabled based on the boolean values specified in the `actions`
998 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
999 * values that indicate whether or not the action should be enabled.
1002 OO
.ui
.ActionSet
.prototype.setAbilities = function ( actions
) {
1003 var i
, len
, action
, item
;
1005 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
1006 item
= this.list
[ i
];
1007 action
= item
.getAction();
1008 if ( actions
[ action
] !== undefined ) {
1009 item
.setDisabled( !actions
[ action
] );
1017 * Executes a function once per action.
1019 * When making changes to multiple actions, use this method instead of iterating over the actions
1020 * manually to defer emitting a #change event until after all actions have been changed.
1022 * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get
1023 * @param {Function} callback Callback to run for each action; callback is invoked with three
1024 * arguments: the action, the action's index, the list of actions being iterated over
1027 OO
.ui
.ActionSet
.prototype.forEach = function ( filter
, callback
) {
1028 this.changed
= false;
1029 this.changing
= true;
1030 this.get( filter
).forEach( callback
);
1031 this.changing
= false;
1032 if ( this.changed
) {
1033 this.emit( 'change' );
1040 * Add action widgets to the action set.
1042 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
1047 OO
.ui
.ActionSet
.prototype.add = function ( actions
) {
1050 this.changing
= true;
1051 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
1052 action
= actions
[ i
];
1053 action
.connect( this, {
1054 click
: [ 'emit', 'click', action
],
1055 resize
: [ 'emit', 'resize', action
],
1056 toggle
: [ 'onActionChange' ]
1058 this.list
.push( action
);
1060 this.organized
= false;
1061 this.emit( 'add', actions
);
1062 this.changing
= false;
1063 this.emit( 'change' );
1069 * Remove action widgets from the set.
1071 * To remove all actions, you may wish to use the #clear method instead.
1073 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
1078 OO
.ui
.ActionSet
.prototype.remove = function ( actions
) {
1079 var i
, len
, index
, action
;
1081 this.changing
= true;
1082 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
1083 action
= actions
[ i
];
1084 index
= this.list
.indexOf( action
);
1085 if ( index
!== -1 ) {
1086 action
.disconnect( this );
1087 this.list
.splice( index
, 1 );
1090 this.organized
= false;
1091 this.emit( 'remove', actions
);
1092 this.changing
= false;
1093 this.emit( 'change' );
1099 * Remove all action widets from the set.
1101 * To remove only specified actions, use the {@link #method-remove remove} method instead.
1107 OO
.ui
.ActionSet
.prototype.clear = function () {
1109 removed
= this.list
.slice();
1111 this.changing
= true;
1112 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
1113 action
= this.list
[ i
];
1114 action
.disconnect( this );
1119 this.organized
= false;
1120 this.emit( 'remove', removed
);
1121 this.changing
= false;
1122 this.emit( 'change' );
1130 * This is called whenever organized information is requested. It will only reorganize the actions
1131 * if something has changed since the last time it ran.
1136 OO
.ui
.ActionSet
.prototype.organize = function () {
1137 var i
, iLen
, j
, jLen
, flag
, action
, category
, list
, item
, special
,
1138 specialFlags
= this.constructor.static.specialFlags
;
1140 if ( !this.organized
) {
1141 this.categorized
= {};
1144 for ( i
= 0, iLen
= this.list
.length
; i
< iLen
; i
++ ) {
1145 action
= this.list
[ i
];
1146 if ( action
.isVisible() ) {
1147 // Populate categories
1148 for ( category
in this.categories
) {
1149 if ( !this.categorized
[ category
] ) {
1150 this.categorized
[ category
] = {};
1152 list
= action
[ this.categories
[ category
] ]();
1153 if ( !Array
.isArray( list
) ) {
1156 for ( j
= 0, jLen
= list
.length
; j
< jLen
; j
++ ) {
1158 if ( !this.categorized
[ category
][ item
] ) {
1159 this.categorized
[ category
][ item
] = [];
1161 this.categorized
[ category
][ item
].push( action
);
1164 // Populate special/others
1166 for ( j
= 0, jLen
= specialFlags
.length
; j
< jLen
; j
++ ) {
1167 flag
= specialFlags
[ j
];
1168 if ( !this.special
[ flag
] && action
.hasFlag( flag
) ) {
1169 this.special
[ flag
] = action
;
1175 this.others
.push( action
);
1179 this.organized
= true;
1186 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
1187 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
1188 * connected to them and can't be interacted with.
1194 * @param {Object} [config] Configuration options
1195 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
1196 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
1198 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
1199 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
1200 * @cfg {string} [text] Text to insert
1201 * @cfg {Array} [content] An array of content elements to append (after #text).
1202 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
1203 * Instances of OO.ui.Element will have their $element appended.
1204 * @cfg {jQuery} [$content] Content elements to append (after #text).
1205 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
1206 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
1207 * Data can also be specified with the #setData method.
1209 OO
.ui
.Element
= function OoUiElement( config
) {
1210 // Configuration initialization
1211 config
= config
|| {};
1215 this.visible
= true;
1216 this.data
= config
.data
;
1217 this.$element
= config
.$element
||
1218 $( document
.createElement( this.getTagName() ) );
1219 this.elementGroup
= null;
1220 this.debouncedUpdateThemeClassesHandler
= OO
.ui
.debounce( this.debouncedUpdateThemeClasses
);
1223 if ( Array
.isArray( config
.classes
) ) {
1224 this.$element
.addClass( config
.classes
.join( ' ' ) );
1227 this.$element
.attr( 'id', config
.id
);
1229 if ( config
.text
) {
1230 this.$element
.text( config
.text
);
1232 if ( config
.content
) {
1233 // The `content` property treats plain strings as text; use an
1234 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
1235 // appropriate $element appended.
1236 this.$element
.append( config
.content
.map( function ( v
) {
1237 if ( typeof v
=== 'string' ) {
1238 // Escape string so it is properly represented in HTML.
1239 return document
.createTextNode( v
);
1240 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
1242 return v
.toString();
1243 } else if ( v
instanceof OO
.ui
.Element
) {
1249 if ( config
.$content
) {
1250 // The `$content` property treats plain strings as HTML.
1251 this.$element
.append( config
.$content
);
1257 OO
.initClass( OO
.ui
.Element
);
1259 /* Static Properties */
1262 * The name of the HTML tag used by the element.
1264 * The static value may be ignored if the #getTagName method is overridden.
1268 * @property {string}
1270 OO
.ui
.Element
.static.tagName
= 'div';
1272 /* Static Methods */
1275 * Reconstitute a JavaScript object corresponding to a widget created
1276 * by the PHP implementation.
1278 * @param {string|HTMLElement|jQuery} idOrNode
1279 * A DOM id (if a string) or node for the widget to infuse.
1280 * @return {OO.ui.Element}
1281 * The `OO.ui.Element` corresponding to this (infusable) document node.
1282 * For `Tag` objects emitted on the HTML side (used occasionally for content)
1283 * the value returned is a newly-created Element wrapping around the existing
1286 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
1287 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, false );
1288 // Verify that the type matches up.
1289 // FIXME: uncomment after T89721 is fixed (see T90929)
1291 if ( !( obj instanceof this['class'] ) ) {
1292 throw new Error( 'Infusion type mismatch!' );
1299 * Implementation helper for `infuse`; skips the type check and has an
1300 * extra property so that only the top-level invocation touches the DOM.
1302 * @param {string|HTMLElement|jQuery} idOrNode
1303 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
1304 * when the top-level widget of this infusion is inserted into DOM,
1305 * replacing the original node; or false for top-level invocation.
1306 * @return {OO.ui.Element}
1308 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, domPromise
) {
1309 // look for a cached result of a previous infusion.
1310 var id
, $elem
, data
, cls
, parts
, parent
, obj
, top
, state
;
1311 if ( typeof idOrNode
=== 'string' ) {
1313 $elem
= $( document
.getElementById( id
) );
1315 $elem
= $( idOrNode
);
1316 id
= $elem
.attr( 'id' );
1318 if ( !$elem
.length
) {
1319 throw new Error( 'Widget not found: ' + id
);
1321 data
= $elem
.data( 'ooui-infused' ) || $elem
[ 0 ].oouiInfused
;
1324 if ( data
=== true ) {
1325 throw new Error( 'Circular dependency! ' + id
);
1329 data
= $elem
.attr( 'data-ooui' );
1331 throw new Error( 'No infusion data found: ' + id
);
1334 data
= $.parseJSON( data
);
1338 if ( !( data
&& data
._
) ) {
1339 throw new Error( 'No valid infusion data found: ' + id
);
1341 if ( data
._
=== 'Tag' ) {
1342 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
1343 return new OO
.ui
.Element( { $element
: $elem
} );
1345 parts
= data
._
.split( '.' );
1346 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
1347 if ( cls
=== undefined ) {
1348 // The PHP output might be old and not including the "OO.ui" prefix
1349 // TODO: Remove this back-compat after next major release
1350 cls
= OO
.getProp
.apply( OO
, [ OO
.ui
].concat( parts
) );
1351 if ( cls
=== undefined ) {
1352 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
1356 // Verify that we're creating an OO.ui.Element instance
1357 parent
= cls
.parent
;
1359 while ( parent
!== undefined ) {
1360 if ( parent
=== OO
.ui
.Element
) {
1365 parent
= parent
.parent
;
1368 if ( parent
!== OO
.ui
.Element
) {
1369 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
1372 if ( domPromise
=== false ) {
1374 domPromise
= top
.promise();
1376 $elem
.data( 'ooui-infused', true ); // prevent loops
1377 data
.id
= id
; // implicit
1378 data
= OO
.copy( data
, null, function deserialize( value
) {
1379 if ( OO
.isPlainObject( value
) ) {
1381 return OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, domPromise
);
1384 return new OO
.ui
.HtmlSnippet( value
.html
);
1388 // allow widgets to reuse parts of the DOM
1389 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
1390 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
1391 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
1393 // jscs:disable requireCapitalizedConstructors
1394 obj
= new cls( data
);
1395 // jscs:enable requireCapitalizedConstructors
1396 // now replace old DOM with this new DOM.
1398 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
1399 // so only mutate the DOM if we need to.
1400 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
1401 $elem
.replaceWith( obj
.$element
);
1402 // This element is now gone from the DOM, but if anyone is holding a reference to it,
1403 // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
1404 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
1405 $elem
[ 0 ].oouiInfused
= obj
;
1409 obj
.$element
.data( 'ooui-infused', obj
);
1410 // set the 'data-ooui' attribute so we can identify infused widgets
1411 obj
.$element
.attr( 'data-ooui', '' );
1412 // restore dynamic state after the new element is inserted into DOM
1413 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
1418 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
1420 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
1421 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
1422 * constructor, which will be given the enhanced config.
1425 * @param {HTMLElement} node
1426 * @param {Object} config
1429 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
1434 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
1435 * (and its children) that represent an Element of the same class and the given configuration,
1436 * generated by the PHP implementation.
1438 * This method is called just before `node` is detached from the DOM. The return value of this
1439 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
1440 * is inserted into DOM to replace `node`.
1443 * @param {HTMLElement} node
1444 * @param {Object} config
1447 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
1452 * Get a jQuery function within a specific document.
1455 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
1456 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
1458 * @return {Function} Bound jQuery function
1460 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
1461 function wrapper( selector
) {
1462 return $( selector
, wrapper
.context
);
1465 wrapper
.context
= this.getDocument( context
);
1468 wrapper
.$iframe
= $iframe
;
1475 * Get the document of an element.
1478 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1479 * @return {HTMLDocument|null} Document object
1481 OO
.ui
.Element
.static.getDocument = function ( obj
) {
1482 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1483 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
1484 // Empty jQuery selections might have a context
1487 obj
.ownerDocument
||
1491 ( obj
.nodeType
=== 9 && obj
) ||
1496 * Get the window of an element or document.
1499 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1500 * @return {Window} Window object
1502 OO
.ui
.Element
.static.getWindow = function ( obj
) {
1503 var doc
= this.getDocument( obj
);
1504 return doc
.defaultView
;
1508 * Get the direction of an element or document.
1511 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1512 * @return {string} Text direction, either 'ltr' or 'rtl'
1514 OO
.ui
.Element
.static.getDir = function ( obj
) {
1517 if ( obj
instanceof jQuery
) {
1520 isDoc
= obj
.nodeType
=== 9;
1521 isWin
= obj
.document
!== undefined;
1522 if ( isDoc
|| isWin
) {
1528 return $( obj
).css( 'direction' );
1532 * Get the offset between two frames.
1534 * TODO: Make this function not use recursion.
1537 * @param {Window} from Window of the child frame
1538 * @param {Window} [to=window] Window of the parent frame
1539 * @param {Object} [offset] Offset to start with, used internally
1540 * @return {Object} Offset object, containing left and top properties
1542 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
1543 var i
, len
, frames
, frame
, rect
;
1549 offset
= { top
: 0, left
: 0 };
1551 if ( from.parent
=== from ) {
1555 // Get iframe element
1556 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
1557 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
1558 if ( frames
[ i
].contentWindow
=== from ) {
1559 frame
= frames
[ i
];
1564 // Recursively accumulate offset values
1566 rect
= frame
.getBoundingClientRect();
1567 offset
.left
+= rect
.left
;
1568 offset
.top
+= rect
.top
;
1569 if ( from !== to
) {
1570 this.getFrameOffset( from.parent
, offset
);
1577 * Get the offset between two elements.
1579 * The two elements may be in a different frame, but in that case the frame $element is in must
1580 * be contained in the frame $anchor is in.
1583 * @param {jQuery} $element Element whose position to get
1584 * @param {jQuery} $anchor Element to get $element's position relative to
1585 * @return {Object} Translated position coordinates, containing top and left properties
1587 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1588 var iframe
, iframePos
,
1589 pos
= $element
.offset(),
1590 anchorPos
= $anchor
.offset(),
1591 elementDocument
= this.getDocument( $element
),
1592 anchorDocument
= this.getDocument( $anchor
);
1594 // If $element isn't in the same document as $anchor, traverse up
1595 while ( elementDocument
!== anchorDocument
) {
1596 iframe
= elementDocument
.defaultView
.frameElement
;
1598 throw new Error( '$element frame is not contained in $anchor frame' );
1600 iframePos
= $( iframe
).offset();
1601 pos
.left
+= iframePos
.left
;
1602 pos
.top
+= iframePos
.top
;
1603 elementDocument
= iframe
.ownerDocument
;
1605 pos
.left
-= anchorPos
.left
;
1606 pos
.top
-= anchorPos
.top
;
1611 * Get element border sizes.
1614 * @param {HTMLElement} el Element to measure
1615 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1617 OO
.ui
.Element
.static.getBorders = function ( el
) {
1618 var doc
= el
.ownerDocument
,
1619 win
= doc
.defaultView
,
1620 style
= win
.getComputedStyle( el
, null ),
1622 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1623 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1624 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1625 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1636 * Get dimensions of an element or window.
1639 * @param {HTMLElement|Window} el Element to measure
1640 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1642 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1644 doc
= el
.ownerDocument
|| el
.document
,
1645 win
= doc
.defaultView
;
1647 if ( win
=== el
|| el
=== doc
.documentElement
) {
1650 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1652 top
: $win
.scrollTop(),
1653 left
: $win
.scrollLeft()
1655 scrollbar
: { right
: 0, bottom
: 0 },
1659 bottom
: $win
.innerHeight(),
1660 right
: $win
.innerWidth()
1666 borders
: this.getBorders( el
),
1668 top
: $el
.scrollTop(),
1669 left
: $el
.scrollLeft()
1672 right
: $el
.innerWidth() - el
.clientWidth
,
1673 bottom
: $el
.innerHeight() - el
.clientHeight
1675 rect
: el
.getBoundingClientRect()
1681 * Get scrollable object parent
1683 * documentElement can't be used to get or set the scrollTop
1684 * property on Blink. Changing and testing its value lets us
1685 * use 'body' or 'documentElement' based on what is working.
1687 * https://code.google.com/p/chromium/issues/detail?id=303131
1690 * @param {HTMLElement} el Element to find scrollable parent for
1691 * @return {HTMLElement} Scrollable parent
1693 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1694 var scrollTop
, body
;
1696 if ( OO
.ui
.scrollableElement
=== undefined ) {
1697 body
= el
.ownerDocument
.body
;
1698 scrollTop
= body
.scrollTop
;
1701 if ( body
.scrollTop
=== 1 ) {
1702 body
.scrollTop
= scrollTop
;
1703 OO
.ui
.scrollableElement
= 'body';
1705 OO
.ui
.scrollableElement
= 'documentElement';
1709 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1713 * Get closest scrollable container.
1715 * Traverses up until either a scrollable element or the root is reached, in which case the window
1719 * @param {HTMLElement} el Element to find scrollable container for
1720 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1721 * @return {HTMLElement} Closest scrollable container
1723 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1725 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1726 props
= [ 'overflow-x', 'overflow-y' ],
1727 $parent
= $( el
).parent();
1729 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1730 props
= [ 'overflow-' + dimension
];
1733 while ( $parent
.length
) {
1734 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1735 return $parent
[ 0 ];
1739 val
= $parent
.css( props
[ i
] );
1740 if ( val
=== 'auto' || val
=== 'scroll' ) {
1741 return $parent
[ 0 ];
1744 $parent
= $parent
.parent();
1746 return this.getDocument( el
).body
;
1750 * Scroll element into view.
1753 * @param {HTMLElement} el Element to scroll into view
1754 * @param {Object} [config] Configuration options
1755 * @param {string} [config.duration] jQuery animation duration value
1756 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1757 * to scroll in both directions
1758 * @param {Function} [config.complete] Function to call when scrolling completes
1760 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1761 var rel
, anim
, callback
, sc
, $sc
, eld
, scd
, $win
;
1763 // Configuration initialization
1764 config
= config
|| {};
1767 callback
= typeof config
.complete
=== 'function' && config
.complete
;
1768 sc
= this.getClosestScrollableContainer( el
, config
.direction
);
1770 eld
= this.getDimensions( el
);
1771 scd
= this.getDimensions( sc
);
1772 $win
= $( this.getWindow( el
) );
1774 // Compute the distances between the edges of el and the edges of the scroll viewport
1775 if ( $sc
.is( 'html, body' ) ) {
1776 // If the scrollable container is the root, this is easy
1779 bottom
: $win
.innerHeight() - eld
.rect
.bottom
,
1780 left
: eld
.rect
.left
,
1781 right
: $win
.innerWidth() - eld
.rect
.right
1784 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1786 top
: eld
.rect
.top
- ( scd
.rect
.top
+ scd
.borders
.top
),
1787 bottom
: scd
.rect
.bottom
- scd
.borders
.bottom
- scd
.scrollbar
.bottom
- eld
.rect
.bottom
,
1788 left
: eld
.rect
.left
- ( scd
.rect
.left
+ scd
.borders
.left
),
1789 right
: scd
.rect
.right
- scd
.borders
.right
- scd
.scrollbar
.right
- eld
.rect
.right
1793 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1794 if ( rel
.top
< 0 ) {
1795 anim
.scrollTop
= scd
.scroll
.top
+ rel
.top
;
1796 } else if ( rel
.top
> 0 && rel
.bottom
< 0 ) {
1797 anim
.scrollTop
= scd
.scroll
.top
+ Math
.min( rel
.top
, -rel
.bottom
);
1800 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1801 if ( rel
.left
< 0 ) {
1802 anim
.scrollLeft
= scd
.scroll
.left
+ rel
.left
;
1803 } else if ( rel
.left
> 0 && rel
.right
< 0 ) {
1804 anim
.scrollLeft
= scd
.scroll
.left
+ Math
.min( rel
.left
, -rel
.right
);
1807 if ( !$.isEmptyObject( anim
) ) {
1808 $sc
.stop( true ).animate( anim
, config
.duration
|| 'fast' );
1810 $sc
.queue( function ( next
) {
1823 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1824 * and reserve space for them, because it probably doesn't.
1826 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1827 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1828 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1829 * and then reattach (or show) them back.
1832 * @param {HTMLElement} el Element to reconsider the scrollbars on
1834 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1835 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1836 // Save scroll position
1837 scrollLeft
= el
.scrollLeft
;
1838 scrollTop
= el
.scrollTop
;
1839 // Detach all children
1840 while ( el
.firstChild
) {
1841 nodes
.push( el
.firstChild
);
1842 el
.removeChild( el
.firstChild
);
1845 void el
.offsetHeight
;
1846 // Reattach all children
1847 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1848 el
.appendChild( nodes
[ i
] );
1850 // Restore scroll position (no-op if scrollbars disappeared)
1851 el
.scrollLeft
= scrollLeft
;
1852 el
.scrollTop
= scrollTop
;
1858 * Toggle visibility of an element.
1860 * @param {boolean} [show] Make element visible, omit to toggle visibility
1864 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1865 show
= show
=== undefined ? !this.visible
: !!show
;
1867 if ( show
!== this.isVisible() ) {
1868 this.visible
= show
;
1869 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1870 this.emit( 'toggle', show
);
1877 * Check if element is visible.
1879 * @return {boolean} element is visible
1881 OO
.ui
.Element
.prototype.isVisible = function () {
1882 return this.visible
;
1888 * @return {Mixed} Element data
1890 OO
.ui
.Element
.prototype.getData = function () {
1897 * @param {Mixed} Element data
1900 OO
.ui
.Element
.prototype.setData = function ( data
) {
1906 * Check if element supports one or more methods.
1908 * @param {string|string[]} methods Method or list of methods to check
1909 * @return {boolean} All methods are supported
1911 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1915 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1916 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1917 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1922 return methods
.length
=== support
;
1926 * Update the theme-provided classes.
1928 * @localdoc This is called in element mixins and widget classes any time state changes.
1929 * Updating is debounced, minimizing overhead of changing multiple attributes and
1930 * guaranteeing that theme updates do not occur within an element's constructor
1932 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1933 this.debouncedUpdateThemeClassesHandler();
1938 * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
1939 * make them synchronous.
1941 OO
.ui
.Element
.prototype.debouncedUpdateThemeClasses = function () {
1942 OO
.ui
.theme
.updateElementClasses( this );
1946 * Get the HTML tag name.
1948 * Override this method to base the result on instance information.
1950 * @return {string} HTML tag name
1952 OO
.ui
.Element
.prototype.getTagName = function () {
1953 return this.constructor.static.tagName
;
1957 * Check if the element is attached to the DOM
1958 * @return {boolean} The element is attached to the DOM
1960 OO
.ui
.Element
.prototype.isElementAttached = function () {
1961 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1965 * Get the DOM document.
1967 * @return {HTMLDocument} Document object
1969 OO
.ui
.Element
.prototype.getElementDocument = function () {
1970 // Don't cache this in other ways either because subclasses could can change this.$element
1971 return OO
.ui
.Element
.static.getDocument( this.$element
);
1975 * Get the DOM window.
1977 * @return {Window} Window object
1979 OO
.ui
.Element
.prototype.getElementWindow = function () {
1980 return OO
.ui
.Element
.static.getWindow( this.$element
);
1984 * Get closest scrollable container.
1986 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1987 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1991 * Get group element is in.
1993 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1995 OO
.ui
.Element
.prototype.getElementGroup = function () {
1996 return this.elementGroup
;
2000 * Set group element is in.
2002 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
2005 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
2006 this.elementGroup
= group
;
2011 * Scroll element into view.
2013 * @param {Object} [config] Configuration options
2015 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
2016 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
2020 * Restore the pre-infusion dynamic state for this widget.
2022 * This method is called after #$element has been inserted into DOM. The parameter is the return
2023 * value of #gatherPreInfuseState.
2026 * @param {Object} state
2028 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
2032 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
2033 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
2034 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
2035 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
2036 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
2040 * @extends OO.ui.Element
2041 * @mixins OO.EventEmitter
2044 * @param {Object} [config] Configuration options
2046 OO
.ui
.Layout
= function OoUiLayout( config
) {
2047 // Configuration initialization
2048 config
= config
|| {};
2050 // Parent constructor
2051 OO
.ui
.Layout
.parent
.call( this, config
);
2053 // Mixin constructors
2054 OO
.EventEmitter
.call( this );
2057 this.$element
.addClass( 'oo-ui-layout' );
2062 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
2063 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
2066 * Widgets are compositions of one or more OOjs UI elements that users can both view
2067 * and interact with. All widgets can be configured and modified via a standard API,
2068 * and their state can change dynamically according to a model.
2072 * @extends OO.ui.Element
2073 * @mixins OO.EventEmitter
2076 * @param {Object} [config] Configuration options
2077 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
2078 * appearance reflects this state.
2080 OO
.ui
.Widget
= function OoUiWidget( config
) {
2081 // Initialize config
2082 config
= $.extend( { disabled
: false }, config
);
2084 // Parent constructor
2085 OO
.ui
.Widget
.parent
.call( this, config
);
2087 // Mixin constructors
2088 OO
.EventEmitter
.call( this );
2091 this.disabled
= null;
2092 this.wasDisabled
= null;
2095 this.$element
.addClass( 'oo-ui-widget' );
2096 this.setDisabled( !!config
.disabled
);
2101 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
2102 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
2104 /* Static Properties */
2107 * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
2108 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
2113 * @property {boolean}
2115 OO
.ui
.Widget
.static.supportsSimpleLabel
= false;
2122 * A 'disable' event is emitted when the disabled state of the widget changes
2123 * (i.e. on disable **and** enable).
2125 * @param {boolean} disabled Widget is disabled
2131 * A 'toggle' event is emitted when the visibility of the widget changes.
2133 * @param {boolean} visible Widget is visible
2139 * Check if the widget is disabled.
2141 * @return {boolean} Widget is disabled
2143 OO
.ui
.Widget
.prototype.isDisabled = function () {
2144 return this.disabled
;
2148 * Set the 'disabled' state of the widget.
2150 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
2152 * @param {boolean} disabled Disable widget
2155 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
2158 this.disabled
= !!disabled
;
2159 isDisabled
= this.isDisabled();
2160 if ( isDisabled
!== this.wasDisabled
) {
2161 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
2162 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
2163 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
2164 this.emit( 'disable', isDisabled
);
2165 this.updateThemeClasses();
2167 this.wasDisabled
= isDisabled
;
2173 * Update the disabled state, in case of changes in parent widget.
2177 OO
.ui
.Widget
.prototype.updateDisabled = function () {
2178 this.setDisabled( this.disabled
);
2183 * A window is a container for elements that are in a child frame. They are used with
2184 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
2185 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
2186 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
2187 * the window manager will choose a sensible fallback.
2189 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
2190 * different processes are executed:
2192 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
2193 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
2196 * - {@link #getSetupProcess} method is called and its result executed
2197 * - {@link #getReadyProcess} method is called and its result executed
2199 * **opened**: The window is now open
2201 * **closing**: The closing stage begins when the window manager's
2202 * {@link OO.ui.WindowManager#closeWindow closeWindow}
2203 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
2205 * - {@link #getHoldProcess} method is called and its result executed
2206 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
2208 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
2209 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
2210 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
2211 * processing can complete. Always assume window processes are executed asynchronously.
2213 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2215 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
2219 * @extends OO.ui.Element
2220 * @mixins OO.EventEmitter
2223 * @param {Object} [config] Configuration options
2224 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
2225 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
2227 OO
.ui
.Window
= function OoUiWindow( config
) {
2228 // Configuration initialization
2229 config
= config
|| {};
2231 // Parent constructor
2232 OO
.ui
.Window
.parent
.call( this, config
);
2234 // Mixin constructors
2235 OO
.EventEmitter
.call( this );
2238 this.manager
= null;
2239 this.size
= config
.size
|| this.constructor.static.size
;
2240 this.$frame
= $( '<div>' );
2241 this.$overlay
= $( '<div>' );
2242 this.$content
= $( '<div>' );
2244 this.$focusTrapBefore
= $( '<div>' ).prop( 'tabIndex', 0 );
2245 this.$focusTrapAfter
= $( '<div>' ).prop( 'tabIndex', 0 );
2246 this.$focusTraps
= this.$focusTrapBefore
.add( this.$focusTrapAfter
);
2249 this.$overlay
.addClass( 'oo-ui-window-overlay' );
2251 .addClass( 'oo-ui-window-content' )
2252 .attr( 'tabindex', 0 );
2254 .addClass( 'oo-ui-window-frame' )
2255 .append( this.$focusTrapBefore
, this.$content
, this.$focusTrapAfter
);
2258 .addClass( 'oo-ui-window' )
2259 .append( this.$frame
, this.$overlay
);
2261 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
2262 // that reference properties not initialized at that time of parent class construction
2263 // TODO: Find a better way to handle post-constructor setup
2264 this.visible
= false;
2265 this.$element
.addClass( 'oo-ui-element-hidden' );
2270 OO
.inheritClass( OO
.ui
.Window
, OO
.ui
.Element
);
2271 OO
.mixinClass( OO
.ui
.Window
, OO
.EventEmitter
);
2273 /* Static Properties */
2276 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
2278 * The static size is used if no #size is configured during construction.
2282 * @property {string}
2284 OO
.ui
.Window
.static.size
= 'medium';
2289 * Handle mouse down events.
2292 * @param {jQuery.Event} e Mouse down event
2294 OO
.ui
.Window
.prototype.onMouseDown = function ( e
) {
2295 // Prevent clicking on the click-block from stealing focus
2296 if ( e
.target
=== this.$element
[ 0 ] ) {
2302 * Check if the window has been initialized.
2304 * Initialization occurs when a window is added to a manager.
2306 * @return {boolean} Window has been initialized
2308 OO
.ui
.Window
.prototype.isInitialized = function () {
2309 return !!this.manager
;
2313 * Check if the window is visible.
2315 * @return {boolean} Window is visible
2317 OO
.ui
.Window
.prototype.isVisible = function () {
2318 return this.visible
;
2322 * Check if the window is opening.
2324 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
2327 * @return {boolean} Window is opening
2329 OO
.ui
.Window
.prototype.isOpening = function () {
2330 return this.manager
.isOpening( this );
2334 * Check if the window is closing.
2336 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
2338 * @return {boolean} Window is closing
2340 OO
.ui
.Window
.prototype.isClosing = function () {
2341 return this.manager
.isClosing( this );
2345 * Check if the window is opened.
2347 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
2349 * @return {boolean} Window is opened
2351 OO
.ui
.Window
.prototype.isOpened = function () {
2352 return this.manager
.isOpened( this );
2356 * Get the window manager.
2358 * All windows must be attached to a window manager, which is used to open
2359 * and close the window and control its presentation.
2361 * @return {OO.ui.WindowManager} Manager of window
2363 OO
.ui
.Window
.prototype.getManager = function () {
2364 return this.manager
;
2368 * Get the symbolic name of the window size (e.g., `small` or `medium`).
2370 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
2372 OO
.ui
.Window
.prototype.getSize = function () {
2373 var viewport
= OO
.ui
.Element
.static.getDimensions( this.getElementWindow() ),
2374 sizes
= this.manager
.constructor.static.sizes
,
2377 if ( !sizes
[ size
] ) {
2378 size
= this.manager
.constructor.static.defaultSize
;
2380 if ( size
!== 'full' && viewport
.rect
.right
- viewport
.rect
.left
< sizes
[ size
].width
) {
2388 * Get the size properties associated with the current window size
2390 * @return {Object} Size properties
2392 OO
.ui
.Window
.prototype.getSizeProperties = function () {
2393 return this.manager
.constructor.static.sizes
[ this.getSize() ];
2397 * Disable transitions on window's frame for the duration of the callback function, then enable them
2401 * @param {Function} callback Function to call while transitions are disabled
2403 OO
.ui
.Window
.prototype.withoutSizeTransitions = function ( callback
) {
2404 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2405 // Disable transitions first, otherwise we'll get values from when the window was animating.
2407 styleObj
= this.$frame
[ 0 ].style
;
2408 oldTransition
= styleObj
.transition
|| styleObj
.OTransition
|| styleObj
.MsTransition
||
2409 styleObj
.MozTransition
|| styleObj
.WebkitTransition
;
2410 styleObj
.transition
= styleObj
.OTransition
= styleObj
.MsTransition
=
2411 styleObj
.MozTransition
= styleObj
.WebkitTransition
= 'none';
2413 // Force reflow to make sure the style changes done inside callback really are not transitioned
2414 this.$frame
.height();
2415 styleObj
.transition
= styleObj
.OTransition
= styleObj
.MsTransition
=
2416 styleObj
.MozTransition
= styleObj
.WebkitTransition
= oldTransition
;
2420 * Get the height of the full window contents (i.e., the window head, body and foot together).
2422 * What consistitutes the head, body, and foot varies depending on the window type.
2423 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
2424 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
2425 * and special actions in the head, and dialog content in the body.
2427 * To get just the height of the dialog body, use the #getBodyHeight method.
2429 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
2431 OO
.ui
.Window
.prototype.getContentHeight = function () {
2434 bodyStyleObj
= this.$body
[ 0 ].style
,
2435 frameStyleObj
= this.$frame
[ 0 ].style
;
2437 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2438 // Disable transitions first, otherwise we'll get values from when the window was animating.
2439 this.withoutSizeTransitions( function () {
2440 var oldHeight
= frameStyleObj
.height
,
2441 oldPosition
= bodyStyleObj
.position
;
2442 frameStyleObj
.height
= '1px';
2443 // Force body to resize to new width
2444 bodyStyleObj
.position
= 'relative';
2445 bodyHeight
= win
.getBodyHeight();
2446 frameStyleObj
.height
= oldHeight
;
2447 bodyStyleObj
.position
= oldPosition
;
2451 // Add buffer for border
2452 ( this.$frame
.outerHeight() - this.$frame
.innerHeight() ) +
2453 // Use combined heights of children
2454 ( this.$head
.outerHeight( true ) + bodyHeight
+ this.$foot
.outerHeight( true ) )
2459 * Get the height of the window body.
2461 * To get the height of the full window contents (the window body, head, and foot together),
2462 * use #getContentHeight.
2464 * When this function is called, the window will temporarily have been resized
2465 * to height=1px, so .scrollHeight measurements can be taken accurately.
2467 * @return {number} Height of the window body in pixels
2469 OO
.ui
.Window
.prototype.getBodyHeight = function () {
2470 return this.$body
[ 0 ].scrollHeight
;
2474 * Get the directionality of the frame (right-to-left or left-to-right).
2476 * @return {string} Directionality: `'ltr'` or `'rtl'`
2478 OO
.ui
.Window
.prototype.getDir = function () {
2479 return OO
.ui
.Element
.static.getDir( this.$content
) || 'ltr';
2483 * Get the 'setup' process.
2485 * The setup process is used to set up a window for use in a particular context,
2486 * based on the `data` argument. This method is called during the opening phase of the window’s
2489 * Override this method to add additional steps to the ‘setup’ process the parent method provides
2490 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2493 * To add window content that persists between openings, you may wish to use the #initialize method
2496 * @param {Object} [data] Window opening data
2497 * @return {OO.ui.Process} Setup process
2499 OO
.ui
.Window
.prototype.getSetupProcess = function () {
2500 return new OO
.ui
.Process();
2504 * Get the ‘ready’ process.
2506 * The ready process is used to ready a window for use in a particular
2507 * context, based on the `data` argument. This method is called during the opening phase of
2508 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
2510 * Override this method to add additional steps to the ‘ready’ process the parent method
2511 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2512 * methods of OO.ui.Process.
2514 * @param {Object} [data] Window opening data
2515 * @return {OO.ui.Process} Ready process
2517 OO
.ui
.Window
.prototype.getReadyProcess = function () {
2518 return new OO
.ui
.Process();
2522 * Get the 'hold' process.
2524 * The hold proccess is used to keep a window from being used in a particular context,
2525 * based on the `data` argument. This method is called during the closing phase of the window’s
2528 * Override this method to add additional steps to the 'hold' process the parent method provides
2529 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2532 * @param {Object} [data] Window closing data
2533 * @return {OO.ui.Process} Hold process
2535 OO
.ui
.Window
.prototype.getHoldProcess = function () {
2536 return new OO
.ui
.Process();
2540 * Get the ‘teardown’ process.
2542 * The teardown process is used to teardown a window after use. During teardown,
2543 * user interactions within the window are conveyed and the window is closed, based on the `data`
2544 * argument. This method is called during the closing phase of the window’s lifecycle.
2546 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2547 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2550 * @param {Object} [data] Window closing data
2551 * @return {OO.ui.Process} Teardown process
2553 OO
.ui
.Window
.prototype.getTeardownProcess = function () {
2554 return new OO
.ui
.Process();
2558 * Set the window manager.
2560 * This will cause the window to initialize. Calling it more than once will cause an error.
2562 * @param {OO.ui.WindowManager} manager Manager for this window
2563 * @throws {Error} An error is thrown if the method is called more than once
2566 OO
.ui
.Window
.prototype.setManager = function ( manager
) {
2567 if ( this.manager
) {
2568 throw new Error( 'Cannot set window manager, window already has a manager' );
2571 this.manager
= manager
;
2578 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2580 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2584 OO
.ui
.Window
.prototype.setSize = function ( size
) {
2591 * Update the window size.
2593 * @throws {Error} An error is thrown if the window is not attached to a window manager
2596 OO
.ui
.Window
.prototype.updateSize = function () {
2597 if ( !this.manager
) {
2598 throw new Error( 'Cannot update window size, must be attached to a manager' );
2601 this.manager
.updateWindowSize( this );
2607 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2608 * when the window is opening. In general, setDimensions should not be called directly.
2610 * To set the size of the window, use the #setSize method.
2612 * @param {Object} dim CSS dimension properties
2613 * @param {string|number} [dim.width] Width
2614 * @param {string|number} [dim.minWidth] Minimum width
2615 * @param {string|number} [dim.maxWidth] Maximum width
2616 * @param {string|number} [dim.width] Height, omit to set based on height of contents
2617 * @param {string|number} [dim.minWidth] Minimum height
2618 * @param {string|number} [dim.maxWidth] Maximum height
2621 OO
.ui
.Window
.prototype.setDimensions = function ( dim
) {
2624 styleObj
= this.$frame
[ 0 ].style
;
2626 // Calculate the height we need to set using the correct width
2627 if ( dim
.height
=== undefined ) {
2628 this.withoutSizeTransitions( function () {
2629 var oldWidth
= styleObj
.width
;
2630 win
.$frame
.css( 'width', dim
.width
|| '' );
2631 height
= win
.getContentHeight();
2632 styleObj
.width
= oldWidth
;
2635 height
= dim
.height
;
2639 width
: dim
.width
|| '',
2640 minWidth
: dim
.minWidth
|| '',
2641 maxWidth
: dim
.maxWidth
|| '',
2642 height
: height
|| '',
2643 minHeight
: dim
.minHeight
|| '',
2644 maxHeight
: dim
.maxHeight
|| ''
2651 * Initialize window contents.
2653 * Before the window is opened for the first time, #initialize is called so that content that
2654 * persists between openings can be added to the window.
2656 * To set up a window with new content each time the window opens, use #getSetupProcess.
2658 * @throws {Error} An error is thrown if the window is not attached to a window manager
2661 OO
.ui
.Window
.prototype.initialize = function () {
2662 if ( !this.manager
) {
2663 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2667 this.$head
= $( '<div>' );
2668 this.$body
= $( '<div>' );
2669 this.$foot
= $( '<div>' );
2670 this.$document
= $( this.getElementDocument() );
2673 this.$element
.on( 'mousedown', this.onMouseDown
.bind( this ) );
2676 this.$head
.addClass( 'oo-ui-window-head' );
2677 this.$body
.addClass( 'oo-ui-window-body' );
2678 this.$foot
.addClass( 'oo-ui-window-foot' );
2679 this.$content
.append( this.$head
, this.$body
, this.$foot
);
2685 * Called when someone tries to focus the hidden element at the end of the dialog.
2686 * Sends focus back to the start of the dialog.
2688 * @param {jQuery.Event} event Focus event
2690 OO
.ui
.Window
.prototype.onFocusTrapFocused = function ( event
) {
2691 if ( this.$focusTrapBefore
.is( event
.target
) ) {
2692 OO
.ui
.findFocusable( this.$content
, true ).focus();
2694 // this.$content is the part of the focus cycle, and is the first focusable element
2695 this.$content
.focus();
2702 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2703 * method, which returns a promise resolved when the window is done opening.
2705 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2707 * @param {Object} [data] Window opening data
2708 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2709 * if the window fails to open. When the promise is resolved successfully, the first argument of the
2710 * value is a new promise, which is resolved when the window begins closing.
2711 * @throws {Error} An error is thrown if the window is not attached to a window manager
2713 OO
.ui
.Window
.prototype.open = function ( data
) {
2714 if ( !this.manager
) {
2715 throw new Error( 'Cannot open window, must be attached to a manager' );
2718 return this.manager
.openWindow( this, data
);
2724 * This method is a wrapper around a call to the window
2725 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2726 * which returns a closing promise resolved when the window is done closing.
2728 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2729 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2730 * the window closes.
2732 * @param {Object} [data] Window closing data
2733 * @return {jQuery.Promise} Promise resolved when window is closed
2734 * @throws {Error} An error is thrown if the window is not attached to a window manager
2736 OO
.ui
.Window
.prototype.close = function ( data
) {
2737 if ( !this.manager
) {
2738 throw new Error( 'Cannot close window, must be attached to a manager' );
2741 return this.manager
.closeWindow( this, data
);
2747 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2750 * @param {Object} [data] Window opening data
2751 * @return {jQuery.Promise} Promise resolved when window is setup
2753 OO
.ui
.Window
.prototype.setup = function ( data
) {
2756 this.toggle( true );
2758 this.focusTrapHandler
= OO
.ui
.bind( this.onFocusTrapFocused
, this );
2759 this.$focusTraps
.on( 'focus', this.focusTrapHandler
);
2761 return this.getSetupProcess( data
).execute().then( function () {
2762 // Force redraw by asking the browser to measure the elements' widths
2763 win
.$element
.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2764 win
.$content
.addClass( 'oo-ui-window-content-setup' ).width();
2771 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2774 * @param {Object} [data] Window opening data
2775 * @return {jQuery.Promise} Promise resolved when window is ready
2777 OO
.ui
.Window
.prototype.ready = function ( data
) {
2780 this.$content
.focus();
2781 return this.getReadyProcess( data
).execute().then( function () {
2782 // Force redraw by asking the browser to measure the elements' widths
2783 win
.$element
.addClass( 'oo-ui-window-ready' ).width();
2784 win
.$content
.addClass( 'oo-ui-window-content-ready' ).width();
2791 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2794 * @param {Object} [data] Window closing data
2795 * @return {jQuery.Promise} Promise resolved when window is held
2797 OO
.ui
.Window
.prototype.hold = function ( data
) {
2800 return this.getHoldProcess( data
).execute().then( function () {
2801 // Get the focused element within the window's content
2802 var $focus
= win
.$content
.find( OO
.ui
.Element
.static.getDocument( win
.$content
).activeElement
);
2804 // Blur the focused element
2805 if ( $focus
.length
) {
2809 // Force redraw by asking the browser to measure the elements' widths
2810 win
.$element
.removeClass( 'oo-ui-window-ready' ).width();
2811 win
.$content
.removeClass( 'oo-ui-window-content-ready' ).width();
2818 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2821 * @param {Object} [data] Window closing data
2822 * @return {jQuery.Promise} Promise resolved when window is torn down
2824 OO
.ui
.Window
.prototype.teardown = function ( data
) {
2827 return this.getTeardownProcess( data
).execute().then( function () {
2828 // Force redraw by asking the browser to measure the elements' widths
2829 win
.$element
.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2830 win
.$content
.removeClass( 'oo-ui-window-content-setup' ).width();
2831 win
.$focusTraps
.off( 'focus', win
.focusTrapHandler
);
2832 win
.toggle( false );
2837 * The Dialog class serves as the base class for the other types of dialogs.
2838 * Unless extended to include controls, the rendered dialog box is a simple window
2839 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2840 * which opens, closes, and controls the presentation of the window. See the
2841 * [OOjs UI documentation on MediaWiki] [1] for more information.
2844 * // A simple dialog window.
2845 * function MyDialog( config ) {
2846 * MyDialog.parent.call( this, config );
2848 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2849 * MyDialog.prototype.initialize = function () {
2850 * MyDialog.parent.prototype.initialize.call( this );
2851 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2852 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2853 * this.$body.append( this.content.$element );
2855 * MyDialog.prototype.getBodyHeight = function () {
2856 * return this.content.$element.outerHeight( true );
2858 * var myDialog = new MyDialog( {
2861 * // Create and append a window manager, which opens and closes the window.
2862 * var windowManager = new OO.ui.WindowManager();
2863 * $( 'body' ).append( windowManager.$element );
2864 * windowManager.addWindows( [ myDialog ] );
2865 * // Open the window!
2866 * windowManager.openWindow( myDialog );
2868 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2872 * @extends OO.ui.Window
2873 * @mixins OO.ui.mixin.PendingElement
2876 * @param {Object} [config] Configuration options
2878 OO
.ui
.Dialog
= function OoUiDialog( config
) {
2879 // Parent constructor
2880 OO
.ui
.Dialog
.parent
.call( this, config
);
2882 // Mixin constructors
2883 OO
.ui
.mixin
.PendingElement
.call( this );
2886 this.actions
= new OO
.ui
.ActionSet();
2887 this.attachedActions
= [];
2888 this.currentAction
= null;
2889 this.onDialogKeyDownHandler
= this.onDialogKeyDown
.bind( this );
2892 this.actions
.connect( this, {
2893 click
: 'onActionClick',
2894 resize
: 'onActionResize',
2895 change
: 'onActionsChange'
2900 .addClass( 'oo-ui-dialog' )
2901 .attr( 'role', 'dialog' );
2906 OO
.inheritClass( OO
.ui
.Dialog
, OO
.ui
.Window
);
2907 OO
.mixinClass( OO
.ui
.Dialog
, OO
.ui
.mixin
.PendingElement
);
2909 /* Static Properties */
2912 * Symbolic name of dialog.
2914 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2915 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2917 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2922 * @property {string}
2924 OO
.ui
.Dialog
.static.name
= '';
2929 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
2930 * that will produce a Label node or string. The title can also be specified with data passed to the
2931 * constructor (see #getSetupProcess). In this case, the static value will be overridden.
2936 * @property {jQuery|string|Function}
2938 OO
.ui
.Dialog
.static.title
= '';
2941 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2943 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2944 * value will be overridden.
2946 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2950 * @property {Object[]}
2952 OO
.ui
.Dialog
.static.actions
= [];
2955 * Close the dialog when the 'Esc' key is pressed.
2960 * @property {boolean}
2962 OO
.ui
.Dialog
.static.escapable
= true;
2967 * Handle frame document key down events.
2970 * @param {jQuery.Event} e Key down event
2972 OO
.ui
.Dialog
.prototype.onDialogKeyDown = function ( e
) {
2973 if ( e
.which
=== OO
.ui
.Keys
.ESCAPE
) {
2974 this.executeAction( '' );
2976 e
.stopPropagation();
2981 * Handle action resized events.
2984 * @param {OO.ui.ActionWidget} action Action that was resized
2986 OO
.ui
.Dialog
.prototype.onActionResize = function () {
2987 // Override in subclass
2991 * Handle action click events.
2994 * @param {OO.ui.ActionWidget} action Action that was clicked
2996 OO
.ui
.Dialog
.prototype.onActionClick = function ( action
) {
2997 if ( !this.isPending() ) {
2998 this.executeAction( action
.getAction() );
3003 * Handle actions change event.
3007 OO
.ui
.Dialog
.prototype.onActionsChange = function () {
3008 this.detachActions();
3009 if ( !this.isClosing() ) {
3010 this.attachActions();
3015 * Get the set of actions used by the dialog.
3017 * @return {OO.ui.ActionSet}
3019 OO
.ui
.Dialog
.prototype.getActions = function () {
3020 return this.actions
;
3024 * Get a process for taking action.
3026 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
3027 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
3028 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
3030 * @param {string} [action] Symbolic name of action
3031 * @return {OO.ui.Process} Action process
3033 OO
.ui
.Dialog
.prototype.getActionProcess = function ( action
) {
3034 return new OO
.ui
.Process()
3035 .next( function () {
3037 // An empty action always closes the dialog without data, which should always be
3038 // safe and make no changes
3047 * @param {Object} [data] Dialog opening data
3048 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
3049 * the {@link #static-title static title}
3050 * @param {Object[]} [data.actions] List of configuration options for each
3051 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
3053 OO
.ui
.Dialog
.prototype.getSetupProcess = function ( data
) {
3057 return OO
.ui
.Dialog
.parent
.prototype.getSetupProcess
.call( this, data
)
3058 .next( function () {
3059 var config
= this.constructor.static,
3060 actions
= data
.actions
!== undefined ? data
.actions
: config
.actions
;
3062 this.title
.setLabel(
3063 data
.title
!== undefined ? data
.title
: this.constructor.static.title
3065 this.actions
.add( this.getActionWidgets( actions
) );
3067 if ( this.constructor.static.escapable
) {
3068 this.$element
.on( 'keydown', this.onDialogKeyDownHandler
);
3076 OO
.ui
.Dialog
.prototype.getTeardownProcess = function ( data
) {
3078 return OO
.ui
.Dialog
.parent
.prototype.getTeardownProcess
.call( this, data
)
3079 .first( function () {
3080 if ( this.constructor.static.escapable
) {
3081 this.$element
.off( 'keydown', this.onDialogKeyDownHandler
);
3084 this.actions
.clear();
3085 this.currentAction
= null;
3092 OO
.ui
.Dialog
.prototype.initialize = function () {
3096 OO
.ui
.Dialog
.parent
.prototype.initialize
.call( this );
3098 titleId
= OO
.ui
.generateElementId();
3101 this.title
= new OO
.ui
.LabelWidget( {
3106 this.$content
.addClass( 'oo-ui-dialog-content' );
3107 this.$element
.attr( 'aria-labelledby', titleId
);
3108 this.setPendingElement( this.$head
);
3112 * Get action widgets from a list of configs
3114 * @param {Object[]} actions Action widget configs
3115 * @return {OO.ui.ActionWidget[]} Action widgets
3117 OO
.ui
.Dialog
.prototype.getActionWidgets = function ( actions
) {
3118 var i
, len
, widgets
= [];
3119 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
3121 new OO
.ui
.ActionWidget( actions
[ i
] )
3128 * Attach action actions.
3132 OO
.ui
.Dialog
.prototype.attachActions = function () {
3133 // Remember the list of potentially attached actions
3134 this.attachedActions
= this.actions
.get();
3138 * Detach action actions.
3143 OO
.ui
.Dialog
.prototype.detachActions = function () {
3146 // Detach all actions that may have been previously attached
3147 for ( i
= 0, len
= this.attachedActions
.length
; i
< len
; i
++ ) {
3148 this.attachedActions
[ i
].$element
.detach();
3150 this.attachedActions
= [];
3154 * Execute an action.
3156 * @param {string} action Symbolic name of action to execute
3157 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
3159 OO
.ui
.Dialog
.prototype.executeAction = function ( action
) {
3161 this.currentAction
= action
;
3162 return this.getActionProcess( action
).execute()
3163 .always( this.popPending
.bind( this ) );
3167 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
3168 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
3169 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
3170 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
3171 * pertinent data and reused.
3173 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
3174 * `opened`, and `closing`, which represent the primary stages of the cycle:
3176 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
3177 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
3179 * - an `opening` event is emitted with an `opening` promise
3180 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
3181 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
3182 * window and its result executed
3183 * - a `setup` progress notification is emitted from the `opening` promise
3184 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
3185 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
3186 * window and its result executed
3187 * - a `ready` progress notification is emitted from the `opening` promise
3188 * - the `opening` promise is resolved with an `opened` promise
3190 * **Opened**: the window is now open.
3192 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
3193 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
3194 * to close the window.
3196 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
3197 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
3198 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
3199 * window and its result executed
3200 * - a `hold` progress notification is emitted from the `closing` promise
3201 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
3202 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
3203 * window and its result executed
3204 * - a `teardown` progress notification is emitted from the `closing` promise
3205 * - the `closing` promise is resolved. The window is now closed
3207 * See the [OOjs UI documentation on MediaWiki][1] for more information.
3209 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3212 * @extends OO.ui.Element
3213 * @mixins OO.EventEmitter
3216 * @param {Object} [config] Configuration options
3217 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
3218 * Note that window classes that are instantiated with a factory must have
3219 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
3220 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
3222 OO
.ui
.WindowManager
= function OoUiWindowManager( config
) {
3223 // Configuration initialization
3224 config
= config
|| {};
3226 // Parent constructor
3227 OO
.ui
.WindowManager
.parent
.call( this, config
);
3229 // Mixin constructors
3230 OO
.EventEmitter
.call( this );
3233 this.factory
= config
.factory
;
3234 this.modal
= config
.modal
=== undefined || !!config
.modal
;
3236 this.opening
= null;
3238 this.closing
= null;
3239 this.preparingToOpen
= null;
3240 this.preparingToClose
= null;
3241 this.currentWindow
= null;
3242 this.globalEvents
= false;
3243 this.$ariaHidden
= null;
3244 this.onWindowResizeTimeout
= null;
3245 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
3246 this.afterWindowResizeHandler
= this.afterWindowResize
.bind( this );
3250 .addClass( 'oo-ui-windowManager' )
3251 .toggleClass( 'oo-ui-windowManager-modal', this.modal
);
3256 OO
.inheritClass( OO
.ui
.WindowManager
, OO
.ui
.Element
);
3257 OO
.mixinClass( OO
.ui
.WindowManager
, OO
.EventEmitter
);
3262 * An 'opening' event is emitted when the window begins to be opened.
3265 * @param {OO.ui.Window} win Window that's being opened
3266 * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
3267 * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
3268 * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
3269 * @param {Object} data Window opening data
3273 * A 'closing' event is emitted when the window begins to be closed.
3276 * @param {OO.ui.Window} win Window that's being closed
3277 * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
3278 * is closed successfully. The promise emits `hold` and `teardown` notifications when those
3279 * processes are complete. When the `closing` promise is resolved, the first argument of its value
3280 * is the closing data.
3281 * @param {Object} data Window closing data
3285 * A 'resize' event is emitted when a window is resized.
3288 * @param {OO.ui.Window} win Window that was resized
3291 /* Static Properties */
3294 * Map of the symbolic name of each window size and its CSS properties.
3298 * @property {Object}
3300 OO
.ui
.WindowManager
.static.sizes
= {
3314 // These can be non-numeric because they are never used in calculations
3321 * Symbolic name of the default window size.
3323 * The default size is used if the window's requested size is not recognized.
3327 * @property {string}
3329 OO
.ui
.WindowManager
.static.defaultSize
= 'medium';
3334 * Handle window resize events.
3337 * @param {jQuery.Event} e Window resize event
3339 OO
.ui
.WindowManager
.prototype.onWindowResize = function () {
3340 clearTimeout( this.onWindowResizeTimeout
);
3341 this.onWindowResizeTimeout
= setTimeout( this.afterWindowResizeHandler
, 200 );
3345 * Handle window resize events.
3348 * @param {jQuery.Event} e Window resize event
3350 OO
.ui
.WindowManager
.prototype.afterWindowResize = function () {
3351 if ( this.currentWindow
) {
3352 this.updateWindowSize( this.currentWindow
);
3357 * Check if window is opening.
3359 * @return {boolean} Window is opening
3361 OO
.ui
.WindowManager
.prototype.isOpening = function ( win
) {
3362 return win
=== this.currentWindow
&& !!this.opening
&& this.opening
.state() === 'pending';
3366 * Check if window is closing.
3368 * @return {boolean} Window is closing
3370 OO
.ui
.WindowManager
.prototype.isClosing = function ( win
) {
3371 return win
=== this.currentWindow
&& !!this.closing
&& this.closing
.state() === 'pending';
3375 * Check if window is opened.
3377 * @return {boolean} Window is opened
3379 OO
.ui
.WindowManager
.prototype.isOpened = function ( win
) {
3380 return win
=== this.currentWindow
&& !!this.opened
&& this.opened
.state() === 'pending';
3384 * Check if a window is being managed.
3386 * @param {OO.ui.Window} win Window to check
3387 * @return {boolean} Window is being managed
3389 OO
.ui
.WindowManager
.prototype.hasWindow = function ( win
) {
3392 for ( name
in this.windows
) {
3393 if ( this.windows
[ name
] === win
) {
3402 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
3404 * @param {OO.ui.Window} win Window being opened
3405 * @param {Object} [data] Window opening data
3406 * @return {number} Milliseconds to wait
3408 OO
.ui
.WindowManager
.prototype.getSetupDelay = function () {
3413 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
3415 * @param {OO.ui.Window} win Window being opened
3416 * @param {Object} [data] Window opening data
3417 * @return {number} Milliseconds to wait
3419 OO
.ui
.WindowManager
.prototype.getReadyDelay = function () {
3424 * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
3426 * @param {OO.ui.Window} win Window being closed
3427 * @param {Object} [data] Window closing data
3428 * @return {number} Milliseconds to wait
3430 OO
.ui
.WindowManager
.prototype.getHoldDelay = function () {
3435 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
3436 * executing the ‘teardown’ process.
3438 * @param {OO.ui.Window} win Window being closed
3439 * @param {Object} [data] Window closing data
3440 * @return {number} Milliseconds to wait
3442 OO
.ui
.WindowManager
.prototype.getTeardownDelay = function () {
3443 return this.modal
? 250 : 0;
3447 * Get a window by its symbolic name.
3449 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
3450 * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
3451 * for more information about using factories.
3452 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3454 * @param {string} name Symbolic name of the window
3455 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
3456 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
3457 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
3459 OO
.ui
.WindowManager
.prototype.getWindow = function ( name
) {
3460 var deferred
= $.Deferred(),
3461 win
= this.windows
[ name
];
3463 if ( !( win
instanceof OO
.ui
.Window
) ) {
3464 if ( this.factory
) {
3465 if ( !this.factory
.lookup( name
) ) {
3466 deferred
.reject( new OO
.ui
.Error(
3467 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
3470 win
= this.factory
.create( name
);
3471 this.addWindows( [ win
] );
3472 deferred
.resolve( win
);
3475 deferred
.reject( new OO
.ui
.Error(
3476 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
3480 deferred
.resolve( win
);
3483 return deferred
.promise();
3487 * Get current window.
3489 * @return {OO.ui.Window|null} Currently opening/opened/closing window
3491 OO
.ui
.WindowManager
.prototype.getCurrentWindow = function () {
3492 return this.currentWindow
;
3498 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
3499 * @param {Object} [data] Window opening data
3500 * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
3501 * See {@link #event-opening 'opening' event} for more information about `opening` promises.
3504 OO
.ui
.WindowManager
.prototype.openWindow = function ( win
, data
) {
3506 opening
= $.Deferred();
3508 // Argument handling
3509 if ( typeof win
=== 'string' ) {
3510 return this.getWindow( win
).then( function ( win
) {
3511 return manager
.openWindow( win
, data
);
3516 if ( !this.hasWindow( win
) ) {
3517 opening
.reject( new OO
.ui
.Error(
3518 'Cannot open window: window is not attached to manager'
3520 } else if ( this.preparingToOpen
|| this.opening
|| this.opened
) {
3521 opening
.reject( new OO
.ui
.Error(
3522 'Cannot open window: another window is opening or open'
3527 if ( opening
.state() !== 'rejected' ) {
3528 // If a window is currently closing, wait for it to complete
3529 this.preparingToOpen
= $.when( this.closing
);
3530 // Ensure handlers get called after preparingToOpen is set
3531 this.preparingToOpen
.done( function () {
3532 if ( manager
.modal
) {
3533 manager
.toggleGlobalEvents( true );
3534 manager
.toggleAriaIsolation( true );
3536 manager
.currentWindow
= win
;
3537 manager
.opening
= opening
;
3538 manager
.preparingToOpen
= null;
3539 manager
.emit( 'opening', win
, opening
, data
);
3540 setTimeout( function () {
3541 win
.setup( data
).then( function () {
3542 manager
.updateWindowSize( win
);
3543 manager
.opening
.notify( { state
: 'setup' } );
3544 setTimeout( function () {
3545 win
.ready( data
).then( function () {
3546 manager
.opening
.notify( { state
: 'ready' } );
3547 manager
.opening
= null;
3548 manager
.opened
= $.Deferred();
3549 opening
.resolve( manager
.opened
.promise(), data
);
3551 manager
.opening
= null;
3552 manager
.opened
= $.Deferred();
3554 manager
.closeWindow( win
);
3556 }, manager
.getReadyDelay() );
3558 manager
.opening
= null;
3559 manager
.opened
= $.Deferred();
3561 manager
.closeWindow( win
);
3563 }, manager
.getSetupDelay() );
3567 return opening
.promise();
3573 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
3574 * @param {Object} [data] Window closing data
3575 * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
3576 * See {@link #event-closing 'closing' event} for more information about closing promises.
3577 * @throws {Error} An error is thrown if the window is not managed by the window manager.
3580 OO
.ui
.WindowManager
.prototype.closeWindow = function ( win
, data
) {
3582 closing
= $.Deferred(),
3585 // Argument handling
3586 if ( typeof win
=== 'string' ) {
3587 win
= this.windows
[ win
];
3588 } else if ( !this.hasWindow( win
) ) {
3594 closing
.reject( new OO
.ui
.Error(
3595 'Cannot close window: window is not attached to manager'
3597 } else if ( win
!== this.currentWindow
) {
3598 closing
.reject( new OO
.ui
.Error(
3599 'Cannot close window: window already closed with different data'
3601 } else if ( this.preparingToClose
|| this.closing
) {
3602 closing
.reject( new OO
.ui
.Error(
3603 'Cannot close window: window already closing with different data'
3608 if ( closing
.state() !== 'rejected' ) {
3609 // If the window is currently opening, close it when it's done
3610 this.preparingToClose
= $.when( this.opening
);
3611 // Ensure handlers get called after preparingToClose is set
3612 this.preparingToClose
.always( function () {
3613 manager
.closing
= closing
;
3614 manager
.preparingToClose
= null;
3615 manager
.emit( 'closing', win
, closing
, data
);
3616 opened
= manager
.opened
;
3617 manager
.opened
= null;
3618 opened
.resolve( closing
.promise(), data
);
3619 setTimeout( function () {
3620 win
.hold( data
).then( function () {
3621 closing
.notify( { state
: 'hold' } );
3622 setTimeout( function () {
3623 win
.teardown( data
).then( function () {
3624 closing
.notify( { state
: 'teardown' } );
3625 if ( manager
.modal
) {
3626 manager
.toggleGlobalEvents( false );
3627 manager
.toggleAriaIsolation( false );
3629 manager
.closing
= null;
3630 manager
.currentWindow
= null;
3631 closing
.resolve( data
);
3633 }, manager
.getTeardownDelay() );
3635 }, manager
.getHoldDelay() );
3639 return closing
.promise();
3643 * Add windows to the window manager.
3645 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
3646 * See the [OOjs ui documentation on MediaWiki] [2] for examples.
3647 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3649 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
3650 * by reference, symbolic name, or explicitly defined symbolic names.
3651 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
3652 * explicit nor a statically configured symbolic name.
3654 OO
.ui
.WindowManager
.prototype.addWindows = function ( windows
) {
3655 var i
, len
, win
, name
, list
;
3657 if ( Array
.isArray( windows
) ) {
3658 // Convert to map of windows by looking up symbolic names from static configuration
3660 for ( i
= 0, len
= windows
.length
; i
< len
; i
++ ) {
3661 name
= windows
[ i
].constructor.static.name
;
3662 if ( typeof name
!== 'string' ) {
3663 throw new Error( 'Cannot add window' );
3665 list
[ name
] = windows
[ i
];
3667 } else if ( OO
.isPlainObject( windows
) ) {
3672 for ( name
in list
) {
3674 this.windows
[ name
] = win
.toggle( false );
3675 this.$element
.append( win
.$element
);
3676 win
.setManager( this );
3681 * Remove the specified windows from the windows manager.
3683 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
3684 * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
3685 * longer listens to events, use the #destroy method.
3687 * @param {string[]} names Symbolic names of windows to remove
3688 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3689 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
3691 OO
.ui
.WindowManager
.prototype.removeWindows = function ( names
) {
3692 var i
, len
, win
, name
, cleanupWindow
,
3695 cleanup = function ( name
, win
) {
3696 delete manager
.windows
[ name
];
3697 win
.$element
.detach();
3700 for ( i
= 0, len
= names
.length
; i
< len
; i
++ ) {
3702 win
= this.windows
[ name
];
3704 throw new Error( 'Cannot remove window' );
3706 cleanupWindow
= cleanup
.bind( null, name
, win
);
3707 promises
.push( this.closeWindow( name
).then( cleanupWindow
, cleanupWindow
) );
3710 return $.when
.apply( $, promises
);
3714 * Remove all windows from the window manager.
3716 * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
3717 * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
3718 * To remove just a subset of windows, use the #removeWindows method.
3720 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3722 OO
.ui
.WindowManager
.prototype.clearWindows = function () {
3723 return this.removeWindows( Object
.keys( this.windows
) );
3727 * Set dialog size. In general, this method should not be called directly.
3729 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3733 OO
.ui
.WindowManager
.prototype.updateWindowSize = function ( win
) {
3736 // Bypass for non-current, and thus invisible, windows
3737 if ( win
!== this.currentWindow
) {
3741 isFullscreen
= win
.getSize() === 'full';
3743 this.$element
.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen
);
3744 this.$element
.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen
);
3745 win
.setDimensions( win
.getSizeProperties() );
3747 this.emit( 'resize', win
);
3753 * Bind or unbind global events for scrolling.
3756 * @param {boolean} [on] Bind global events
3759 OO
.ui
.WindowManager
.prototype.toggleGlobalEvents = function ( on
) {
3760 var scrollWidth
, bodyMargin
,
3761 $body
= $( this.getElementDocument().body
),
3762 // We could have multiple window managers open so only modify
3763 // the body css at the bottom of the stack
3764 stackDepth
= $body
.data( 'windowManagerGlobalEvents' ) || 0 ;
3766 on
= on
=== undefined ? !!this.globalEvents
: !!on
;
3769 if ( !this.globalEvents
) {
3770 $( this.getElementWindow() ).on( {
3771 // Start listening for top-level window dimension changes
3772 'orientationchange resize': this.onWindowResizeHandler
3774 if ( stackDepth
=== 0 ) {
3775 scrollWidth
= window
.innerWidth
- document
.documentElement
.clientWidth
;
3776 bodyMargin
= parseFloat( $body
.css( 'margin-right' ) ) || 0;
3779 'margin-right': bodyMargin
+ scrollWidth
3783 this.globalEvents
= true;
3785 } else if ( this.globalEvents
) {
3786 $( this.getElementWindow() ).off( {
3787 // Stop listening for top-level window dimension changes
3788 'orientationchange resize': this.onWindowResizeHandler
3791 if ( stackDepth
=== 0 ) {
3797 this.globalEvents
= false;
3799 $body
.data( 'windowManagerGlobalEvents', stackDepth
);
3805 * Toggle screen reader visibility of content other than the window manager.
3808 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3811 OO
.ui
.WindowManager
.prototype.toggleAriaIsolation = function ( isolate
) {
3812 isolate
= isolate
=== undefined ? !this.$ariaHidden
: !!isolate
;
3815 if ( !this.$ariaHidden
) {
3816 // Hide everything other than the window manager from screen readers
3817 this.$ariaHidden
= $( 'body' )
3819 .not( this.$element
.parentsUntil( 'body' ).last() )
3820 .attr( 'aria-hidden', '' );
3822 } else if ( this.$ariaHidden
) {
3823 // Restore screen reader visibility
3824 this.$ariaHidden
.removeAttr( 'aria-hidden' );
3825 this.$ariaHidden
= null;
3832 * Destroy the window manager.
3834 * Destroying the window manager ensures that it will no longer listen to events. If you would like to
3835 * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
3838 OO
.ui
.WindowManager
.prototype.destroy = function () {
3839 this.toggleGlobalEvents( false );
3840 this.toggleAriaIsolation( false );
3841 this.clearWindows();
3842 this.$element
.remove();
3846 * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
3847 * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
3848 * appearance and functionality of the error interface.
3850 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
3851 * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
3852 * that initiated the failed process will be disabled.
3854 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
3857 * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
3859 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
3864 * @param {string|jQuery} message Description of error
3865 * @param {Object} [config] Configuration options
3866 * @cfg {boolean} [recoverable=true] Error is recoverable.
3867 * By default, errors are recoverable, and users can try the process again.
3868 * @cfg {boolean} [warning=false] Error is a warning.
3869 * If the error is a warning, the error interface will include a
3870 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
3871 * is not triggered a second time if the user chooses to continue.
3873 OO
.ui
.Error
= function OoUiError( message
, config
) {
3874 // Allow passing positional parameters inside the config object
3875 if ( OO
.isPlainObject( message
) && config
=== undefined ) {
3877 message
= config
.message
;
3880 // Configuration initialization
3881 config
= config
|| {};
3884 this.message
= message
instanceof jQuery
? message
: String( message
);
3885 this.recoverable
= config
.recoverable
=== undefined || !!config
.recoverable
;
3886 this.warning
= !!config
.warning
;
3891 OO
.initClass( OO
.ui
.Error
);
3896 * Check if the error is recoverable.
3898 * If the error is recoverable, users are able to try the process again.
3900 * @return {boolean} Error is recoverable
3902 OO
.ui
.Error
.prototype.isRecoverable = function () {
3903 return this.recoverable
;
3907 * Check if the error is a warning.
3909 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
3911 * @return {boolean} Error is warning
3913 OO
.ui
.Error
.prototype.isWarning = function () {
3914 return this.warning
;
3918 * Get error message as DOM nodes.
3920 * @return {jQuery} Error message in DOM nodes
3922 OO
.ui
.Error
.prototype.getMessage = function () {
3923 return this.message
instanceof jQuery
?
3924 this.message
.clone() :
3925 $( '<div>' ).text( this.message
).contents();
3929 * Get the error message text.
3931 * @return {string} Error message
3933 OO
.ui
.Error
.prototype.getMessageText = function () {
3934 return this.message
instanceof jQuery
? this.message
.text() : this.message
;
3938 * Wraps an HTML snippet for use with configuration values which default
3939 * to strings. This bypasses the default html-escaping done to string
3945 * @param {string} [content] HTML content
3947 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
3949 this.content
= content
;
3954 OO
.initClass( OO
.ui
.HtmlSnippet
);
3961 * @return {string} Unchanged HTML snippet.
3963 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
3964 return this.content
;
3968 * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
3971 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
3972 * - **promise**: the process will continue to the next step when the promise is successfully resolved
3973 * or stop if the promise is rejected.
3974 * - **function**: the process will execute the function. The process will stop if the function returns
3975 * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
3976 * will wait for that number of milliseconds before proceeding.
3978 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
3979 * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
3980 * its remaining steps will not be performed.
3985 * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
3986 * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
3987 * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
3988 * a number or promise.
3989 * @return {Object} Step object, with `callback` and `context` properties
3991 OO
.ui
.Process = function ( step
, context
) {
3996 if ( step
!== undefined ) {
3997 this.next( step
, context
);
4003 OO
.initClass( OO
.ui
.Process
);
4008 * Start the process.
4010 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
4011 * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
4012 * and any remaining steps are not performed.
4014 OO
.ui
.Process
.prototype.execute = function () {
4015 var i
, len
, promise
;
4018 * Continue execution.
4021 * @param {Array} step A function and the context it should be called in
4022 * @return {Function} Function that continues the process
4024 function proceed( step
) {
4025 return function () {
4026 // Execute step in the correct context
4028 result
= step
.callback
.call( step
.context
);
4030 if ( result
=== false ) {
4031 // Use rejected promise for boolean false results
4032 return $.Deferred().reject( [] ).promise();
4034 if ( typeof result
=== 'number' ) {
4036 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
4038 // Use a delayed promise for numbers, expecting them to be in milliseconds
4039 deferred
= $.Deferred();
4040 setTimeout( deferred
.resolve
, result
);
4041 return deferred
.promise();
4043 if ( result
instanceof OO
.ui
.Error
) {
4044 // Use rejected promise for error
4045 return $.Deferred().reject( [ result
] ).promise();
4047 if ( Array
.isArray( result
) && result
.length
&& result
[ 0 ] instanceof OO
.ui
.Error
) {
4048 // Use rejected promise for list of errors
4049 return $.Deferred().reject( result
).promise();
4051 // Duck-type the object to see if it can produce a promise
4052 if ( result
&& $.isFunction( result
.promise
) ) {
4053 // Use a promise generated from the result
4054 return result
.promise();
4056 // Use resolved promise for other results
4057 return $.Deferred().resolve().promise();
4061 if ( this.steps
.length
) {
4062 // Generate a chain reaction of promises
4063 promise
= proceed( this.steps
[ 0 ] )();
4064 for ( i
= 1, len
= this.steps
.length
; i
< len
; i
++ ) {
4065 promise
= promise
.then( proceed( this.steps
[ i
] ) );
4068 promise
= $.Deferred().resolve().promise();
4075 * Create a process step.
4078 * @param {number|jQuery.Promise|Function} step
4080 * - Number of milliseconds to wait before proceeding
4081 * - Promise that must be resolved before proceeding
4082 * - Function to execute
4083 * - If the function returns a boolean false the process will stop
4084 * - If the function returns a promise, the process will continue to the next
4085 * step when the promise is resolved or stop if the promise is rejected
4086 * - If the function returns a number, the process will wait for that number of
4087 * milliseconds before proceeding
4088 * @param {Object} [context=null] Execution context of the function. The context is
4089 * ignored if the step is a number or promise.
4090 * @return {Object} Step object, with `callback` and `context` properties
4092 OO
.ui
.Process
.prototype.createStep = function ( step
, context
) {
4093 if ( typeof step
=== 'number' || $.isFunction( step
.promise
) ) {
4095 callback: function () {
4101 if ( $.isFunction( step
) ) {
4107 throw new Error( 'Cannot create process step: number, promise or function expected' );
4111 * Add step to the beginning of the process.
4113 * @inheritdoc #createStep
4114 * @return {OO.ui.Process} this
4117 OO
.ui
.Process
.prototype.first = function ( step
, context
) {
4118 this.steps
.unshift( this.createStep( step
, context
) );
4123 * Add step to the end of the process.
4125 * @inheritdoc #createStep
4126 * @return {OO.ui.Process} this
4129 OO
.ui
.Process
.prototype.next = function ( step
, context
) {
4130 this.steps
.push( this.createStep( step
, context
) );
4135 * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
4136 * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
4137 * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
4139 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
4141 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
4144 * @extends OO.Factory
4147 OO
.ui
.ToolFactory
= function OoUiToolFactory() {
4148 // Parent constructor
4149 OO
.ui
.ToolFactory
.parent
.call( this );
4154 OO
.inheritClass( OO
.ui
.ToolFactory
, OO
.Factory
);
4159 * Get tools from the factory
4161 * @param {Array|string} [include] Included tools, see #extract for format
4162 * @param {Array|string} [exclude] Excluded tools, see #extract for format
4163 * @param {Array|string} [promote] Promoted tools, see #extract for format
4164 * @param {Array|string} [demote] Demoted tools, see #extract for format
4165 * @return {string[]} List of tools
4167 OO
.ui
.ToolFactory
.prototype.getTools = function ( include
, exclude
, promote
, demote
) {
4168 var i
, len
, included
, promoted
, demoted
,
4172 // Collect included and not excluded tools
4173 included
= OO
.simpleArrayDifference( this.extract( include
), this.extract( exclude
) );
4176 promoted
= this.extract( promote
, used
);
4177 demoted
= this.extract( demote
, used
);
4180 for ( i
= 0, len
= included
.length
; i
< len
; i
++ ) {
4181 if ( !used
[ included
[ i
] ] ) {
4182 auto
.push( included
[ i
] );
4186 return promoted
.concat( auto
).concat( demoted
);
4190 * Get a flat list of names from a list of names or groups.
4192 * Normally, `collection` is an array of tool specifications. Tools can be specified in the
4195 * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
4196 * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
4197 * tool to a group, use OO.ui.Tool.static.group.)
4199 * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
4200 * catch-all selector `'*'`.
4202 * If `used` is passed, tool names that appear as properties in this object will be considered
4203 * already assigned, and will not be returned even if specified otherwise. The tool names extracted
4204 * by this function call will be added as new properties in the object.
4207 * @param {Array|string} collection List of tools, see above
4208 * @param {Object} [used] Object containing information about used tools, see above
4209 * @return {string[]} List of extracted tool names
4211 OO
.ui
.ToolFactory
.prototype.extract = function ( collection
, used
) {
4212 var i
, len
, item
, name
, tool
,
4215 if ( collection
=== '*' ) {
4216 for ( name
in this.registry
) {
4217 tool
= this.registry
[ name
];
4219 // Only add tools by group name when auto-add is enabled
4220 tool
.static.autoAddToCatchall
&&
4221 // Exclude already used tools
4222 ( !used
|| !used
[ name
] )
4226 used
[ name
] = true;
4230 } else if ( Array
.isArray( collection
) ) {
4231 for ( i
= 0, len
= collection
.length
; i
< len
; i
++ ) {
4232 item
= collection
[ i
];
4233 // Allow plain strings as shorthand for named tools
4234 if ( typeof item
=== 'string' ) {
4235 item
= { name
: item
};
4237 if ( OO
.isPlainObject( item
) ) {
4239 for ( name
in this.registry
) {
4240 tool
= this.registry
[ name
];
4242 // Include tools with matching group
4243 tool
.static.group
=== item
.group
&&
4244 // Only add tools by group name when auto-add is enabled
4245 tool
.static.autoAddToGroup
&&
4246 // Exclude already used tools
4247 ( !used
|| !used
[ name
] )
4251 used
[ name
] = true;
4255 // Include tools with matching name and exclude already used tools
4256 } else if ( item
.name
&& ( !used
|| !used
[ item
.name
] ) ) {
4257 names
.push( item
.name
);
4259 used
[ item
.name
] = true;
4269 * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
4270 * specify a symbolic name and be registered with the factory. The following classes are registered by
4273 * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
4274 * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
4275 * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
4277 * See {@link OO.ui.Toolbar toolbars} for an example.
4279 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
4281 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
4283 * @extends OO.Factory
4286 OO
.ui
.ToolGroupFactory
= function OoUiToolGroupFactory() {
4287 var i
, l
, defaultClasses
;
4288 // Parent constructor
4289 OO
.Factory
.call( this );
4291 defaultClasses
= this.constructor.static.getDefaultClasses();
4293 // Register default toolgroups
4294 for ( i
= 0, l
= defaultClasses
.length
; i
< l
; i
++ ) {
4295 this.register( defaultClasses
[ i
] );
4301 OO
.inheritClass( OO
.ui
.ToolGroupFactory
, OO
.Factory
);
4303 /* Static Methods */
4306 * Get a default set of classes to be registered on construction.
4308 * @return {Function[]} Default classes
4310 OO
.ui
.ToolGroupFactory
.static.getDefaultClasses = function () {
4313 OO
.ui
.ListToolGroup
,
4325 * @param {Object} [config] Configuration options
4327 OO
.ui
.Theme
= function OoUiTheme( config
) {
4328 // Configuration initialization
4329 config
= config
|| {};
4334 OO
.initClass( OO
.ui
.Theme
);
4339 * Get a list of classes to be applied to a widget.
4341 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
4342 * otherwise state transitions will not work properly.
4344 * @param {OO.ui.Element} element Element for which to get classes
4345 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4347 OO
.ui
.Theme
.prototype.getElementClasses = function () {
4348 return { on
: [], off
: [] };
4352 * Update CSS classes provided by the theme.
4354 * For elements with theme logic hooks, this should be called any time there's a state change.
4356 * @param {OO.ui.Element} element Element for which to update classes
4357 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4359 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
4360 var $elements
= $( [] ),
4361 classes
= this.getElementClasses( element
);
4363 if ( element
.$icon
) {
4364 $elements
= $elements
.add( element
.$icon
);
4366 if ( element
.$indicator
) {
4367 $elements
= $elements
.add( element
.$indicator
);
4371 .removeClass( classes
.off
.join( ' ' ) )
4372 .addClass( classes
.on
.join( ' ' ) );
4376 * RequestManager is a mixin that manages the lifecycle of a promise-backed request for a widget, such as
4377 * the {@link OO.ui.mixin.LookupElement}.
4384 OO
.ui
.mixin
.RequestManager
= function OoUiMixinRequestManager() {
4385 this.requestCache
= {};
4386 this.requestQuery
= null;
4387 this.requestRequest
= null;
4392 OO
.initClass( OO
.ui
.mixin
.RequestManager
);
4395 * Get request results for the current query.
4397 * @return {jQuery.Promise} Promise object which will be passed response data as the first argument of
4398 * the done event. If the request was aborted to make way for a subsequent request, this promise
4399 * may not be rejected, depending on what jQuery feels like doing.
4401 OO
.ui
.mixin
.RequestManager
.prototype.getRequestData = function () {
4403 value
= this.getRequestQuery(),
4404 deferred
= $.Deferred(),
4407 this.abortRequest();
4408 if ( Object
.prototype.hasOwnProperty
.call( this.requestCache
, value
) ) {
4409 deferred
.resolve( this.requestCache
[ value
] );
4411 if ( this.pushPending
) {
4414 this.requestQuery
= value
;
4415 ourRequest
= this.requestRequest
= this.getRequest();
4417 .always( function () {
4418 // We need to pop pending even if this is an old request, otherwise
4419 // the widget will remain pending forever.
4420 // TODO: this assumes that an aborted request will fail or succeed soon after
4421 // being aborted, or at least eventually. It would be nice if we could popPending()
4422 // at abort time, but only if we knew that we hadn't already called popPending()
4423 // for that request.
4424 if ( widget
.popPending
) {
4425 widget
.popPending();
4428 .done( function ( response
) {
4429 // If this is an old request (and aborting it somehow caused it to still succeed),
4430 // ignore its success completely
4431 if ( ourRequest
=== widget
.requestRequest
) {
4432 widget
.requestQuery
= null;
4433 widget
.requestRequest
= null;
4434 widget
.requestCache
[ value
] = widget
.getRequestCacheDataFromResponse( response
);
4435 deferred
.resolve( widget
.requestCache
[ value
] );
4438 .fail( function () {
4439 // If this is an old request (or a request failing because it's being aborted),
4440 // ignore its failure completely
4441 if ( ourRequest
=== widget
.requestRequest
) {
4442 widget
.requestQuery
= null;
4443 widget
.requestRequest
= null;
4448 return deferred
.promise();
4452 * Abort the currently pending request, if any.
4456 OO
.ui
.mixin
.RequestManager
.prototype.abortRequest = function () {
4457 var oldRequest
= this.requestRequest
;
4459 // First unset this.requestRequest to the fail handler will notice
4460 // that the request is no longer current
4461 this.requestRequest
= null;
4462 this.requestQuery
= null;
4468 * Get the query to be made.
4473 * @return {string} query to be used
4475 OO
.ui
.mixin
.RequestManager
.prototype.getRequestQuery
= null;
4478 * Get a new request object of the current query value.
4483 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
4485 OO
.ui
.mixin
.RequestManager
.prototype.getRequest
= null;
4488 * Pre-process data returned by the request from #getRequest.
4490 * The return value of this function will be cached, and any further queries for the given value
4491 * will use the cache rather than doing API requests.
4496 * @param {Mixed} response Response from server
4497 * @return {Mixed} Cached result data
4499 OO
.ui
.mixin
.RequestManager
.prototype.getRequestCacheDataFromResponse
= null;
4502 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
4503 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
4504 * order in which users will navigate through the focusable elements via the "tab" key.
4507 * // TabIndexedElement is mixed into the ButtonWidget class
4508 * // to provide a tabIndex property.
4509 * var button1 = new OO.ui.ButtonWidget( {
4513 * var button2 = new OO.ui.ButtonWidget( {
4517 * var button3 = new OO.ui.ButtonWidget( {
4521 * var button4 = new OO.ui.ButtonWidget( {
4525 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
4531 * @param {Object} [config] Configuration options
4532 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
4533 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
4534 * functionality will be applied to it instead.
4535 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
4536 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
4537 * to remove the element from the tab-navigation flow.
4539 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
4540 // Configuration initialization
4541 config
= $.extend( { tabIndex
: 0 }, config
);
4544 this.$tabIndexed
= null;
4545 this.tabIndex
= null;
4548 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
4551 this.setTabIndex( config
.tabIndex
);
4552 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
4557 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
4562 * Set the element that should use the tabindex functionality.
4564 * This method is used to retarget a tabindex mixin so that its functionality applies
4565 * to the specified element. If an element is currently using the functionality, the mixin’s
4566 * effect on that element is removed before the new element is set up.
4568 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
4571 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
4572 var tabIndex
= this.tabIndex
;
4573 // Remove attributes from old $tabIndexed
4574 this.setTabIndex( null );
4575 // Force update of new $tabIndexed
4576 this.$tabIndexed
= $tabIndexed
;
4577 this.tabIndex
= tabIndex
;
4578 return this.updateTabIndex();
4582 * Set the value of the tabindex.
4584 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
4587 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
4588 tabIndex
= typeof tabIndex
=== 'number' ? tabIndex
: null;
4590 if ( this.tabIndex
!== tabIndex
) {
4591 this.tabIndex
= tabIndex
;
4592 this.updateTabIndex();
4599 * Update the `tabindex` attribute, in case of changes to tab index or
4605 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
4606 if ( this.$tabIndexed
) {
4607 if ( this.tabIndex
!== null ) {
4608 // Do not index over disabled elements
4609 this.$tabIndexed
.attr( {
4610 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
4611 // Support: ChromeVox and NVDA
4612 // These do not seem to inherit aria-disabled from parent elements
4613 'aria-disabled': this.isDisabled().toString()
4616 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
4623 * Handle disable events.
4626 * @param {boolean} disabled Element is disabled
4628 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
4629 this.updateTabIndex();
4633 * Get the value of the tabindex.
4635 * @return {number|null} Tabindex value
4637 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
4638 return this.tabIndex
;
4642 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
4643 * interface element that can be configured with access keys for accessibility.
4644 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
4646 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
4651 * @param {Object} [config] Configuration options
4652 * @cfg {jQuery} [$button] The button element created by the class.
4653 * If this configuration is omitted, the button element will use a generated `<a>`.
4654 * @cfg {boolean} [framed=true] Render the button with a frame
4656 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
4657 // Configuration initialization
4658 config
= config
|| {};
4661 this.$button
= null;
4663 this.active
= false;
4664 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
4665 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
4666 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
4667 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
4668 this.onClickHandler
= this.onClick
.bind( this );
4669 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
4672 this.$element
.addClass( 'oo-ui-buttonElement' );
4673 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
4674 this.setButtonElement( config
.$button
|| $( '<a>' ) );
4679 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
4681 /* Static Properties */
4684 * Cancel mouse down events.
4686 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
4687 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
4688 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
4693 * @property {boolean}
4695 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
4700 * A 'click' event is emitted when the button element is clicked.
4708 * Set the button element.
4710 * This method is used to retarget a button mixin so that its functionality applies to
4711 * the specified button element instead of the one created by the class. If a button element
4712 * is already set, the method will remove the mixin’s effect on that element.
4714 * @param {jQuery} $button Element to use as button
4716 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
4717 if ( this.$button
) {
4719 .removeClass( 'oo-ui-buttonElement-button' )
4720 .removeAttr( 'role accesskey' )
4722 mousedown
: this.onMouseDownHandler
,
4723 keydown
: this.onKeyDownHandler
,
4724 click
: this.onClickHandler
,
4725 keypress
: this.onKeyPressHandler
4729 this.$button
= $button
4730 .addClass( 'oo-ui-buttonElement-button' )
4731 .attr( { role
: 'button' } )
4733 mousedown
: this.onMouseDownHandler
,
4734 keydown
: this.onKeyDownHandler
,
4735 click
: this.onClickHandler
,
4736 keypress
: this.onKeyPressHandler
4741 * Handles mouse down events.
4744 * @param {jQuery.Event} e Mouse down event
4746 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
4747 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
4750 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
4751 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
4752 // reliably remove the pressed class
4753 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
4754 // Prevent change of focus unless specifically configured otherwise
4755 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
4761 * Handles mouse up events.
4764 * @param {jQuery.Event} e Mouse up event
4766 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
4767 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
4770 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
4771 // Stop listening for mouseup, since we only needed this once
4772 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
4776 * Handles mouse click events.
4779 * @param {jQuery.Event} e Mouse click event
4782 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
4783 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
4784 if ( this.emit( 'click' ) ) {
4791 * Handles key down events.
4794 * @param {jQuery.Event} e Key down event
4796 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
4797 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
4800 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
4801 // Run the keyup handler no matter where the key is when the button is let go, so we can
4802 // reliably remove the pressed class
4803 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
4807 * Handles key up events.
4810 * @param {jQuery.Event} e Key up event
4812 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
4813 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
4816 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
4817 // Stop listening for keyup, since we only needed this once
4818 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
4822 * Handles key press events.
4825 * @param {jQuery.Event} e Key press event
4828 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
4829 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
4830 if ( this.emit( 'click' ) ) {
4837 * Check if button has a frame.
4839 * @return {boolean} Button is framed
4841 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
4846 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
4848 * @param {boolean} [framed] Make button framed, omit to toggle
4851 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
4852 framed
= framed
=== undefined ? !this.framed
: !!framed
;
4853 if ( framed
!== this.framed
) {
4854 this.framed
= framed
;
4856 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
4857 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
4858 this.updateThemeClasses();
4865 * Set the button's active state.
4867 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
4868 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
4869 * for other button types.
4871 * @param {boolean} value Make button active
4874 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
4875 this.active
= !!value
;
4876 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
4881 * Check if the button is active
4883 * @return {boolean} The button is active
4885 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
4890 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4891 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4892 * items from the group is done through the interface the class provides.
4893 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4895 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4901 * @param {Object} [config] Configuration options
4902 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
4903 * is omitted, the group element will use a generated `<div>`.
4905 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
4906 // Configuration initialization
4907 config
= config
|| {};
4912 this.aggregateItemEvents
= {};
4915 this.setGroupElement( config
.$group
|| $( '<div>' ) );
4921 * Set the group element.
4923 * If an element is already set, items will be moved to the new element.
4925 * @param {jQuery} $group Element to use as group
4927 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
4930 this.$group
= $group
;
4931 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4932 this.$group
.append( this.items
[ i
].$element
);
4937 * Check if a group contains no items.
4939 * @return {boolean} Group is empty
4941 OO
.ui
.mixin
.GroupElement
.prototype.isEmpty = function () {
4942 return !this.items
.length
;
4946 * Get all items in the group.
4948 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
4949 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
4952 * @return {OO.ui.Element[]} An array of items.
4954 OO
.ui
.mixin
.GroupElement
.prototype.getItems = function () {
4955 return this.items
.slice( 0 );
4959 * Get an item by its data.
4961 * Only the first item with matching data will be returned. To return all matching items,
4962 * use the #getItemsFromData method.
4964 * @param {Object} data Item data to search for
4965 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4967 OO
.ui
.mixin
.GroupElement
.prototype.getItemFromData = function ( data
) {
4969 hash
= OO
.getHash( data
);
4971 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4972 item
= this.items
[ i
];
4973 if ( hash
=== OO
.getHash( item
.getData() ) ) {
4982 * Get items by their data.
4984 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
4986 * @param {Object} data Item data to search for
4987 * @return {OO.ui.Element[]} Items with equivalent data
4989 OO
.ui
.mixin
.GroupElement
.prototype.getItemsFromData = function ( data
) {
4991 hash
= OO
.getHash( data
),
4994 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4995 item
= this.items
[ i
];
4996 if ( hash
=== OO
.getHash( item
.getData() ) ) {
5005 * Aggregate the events emitted by the group.
5007 * When events are aggregated, the group will listen to all contained items for the event,
5008 * and then emit the event under a new name. The new event will contain an additional leading
5009 * parameter containing the item that emitted the original event. Other arguments emitted from
5010 * the original event are passed through.
5012 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
5013 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
5014 * A `null` value will remove aggregated events.
5016 * @throws {Error} An error is thrown if aggregation already exists.
5018 OO
.ui
.mixin
.GroupElement
.prototype.aggregate = function ( events
) {
5019 var i
, len
, item
, add
, remove
, itemEvent
, groupEvent
;
5021 for ( itemEvent
in events
) {
5022 groupEvent
= events
[ itemEvent
];
5024 // Remove existing aggregated event
5025 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
5026 // Don't allow duplicate aggregations
5028 throw new Error( 'Duplicate item event aggregation for ' + itemEvent
);
5030 // Remove event aggregation from existing items
5031 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5032 item
= this.items
[ i
];
5033 if ( item
.connect
&& item
.disconnect
) {
5035 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
5036 item
.disconnect( this, remove
);
5039 // Prevent future items from aggregating event
5040 delete this.aggregateItemEvents
[ itemEvent
];
5043 // Add new aggregate event
5045 // Make future items aggregate event
5046 this.aggregateItemEvents
[ itemEvent
] = groupEvent
;
5047 // Add event aggregation to existing items
5048 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5049 item
= this.items
[ i
];
5050 if ( item
.connect
&& item
.disconnect
) {
5052 add
[ itemEvent
] = [ 'emit', groupEvent
, item
];
5053 item
.connect( this, add
);
5061 * Add items to the group.
5063 * Items will be added to the end of the group array unless the optional `index` parameter specifies
5064 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
5066 * @param {OO.ui.Element[]} items An array of items to add to the group
5067 * @param {number} [index] Index of the insertion point
5070 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
5071 var i
, len
, item
, event
, events
, currentIndex
,
5074 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
5077 // Check if item exists then remove it first, effectively "moving" it
5078 currentIndex
= this.items
.indexOf( item
);
5079 if ( currentIndex
>= 0 ) {
5080 this.removeItems( [ item
] );
5081 // Adjust index to compensate for removal
5082 if ( currentIndex
< index
) {
5087 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
5089 for ( event
in this.aggregateItemEvents
) {
5090 events
[ event
] = [ 'emit', this.aggregateItemEvents
[ event
], item
];
5092 item
.connect( this, events
);
5094 item
.setElementGroup( this );
5095 itemElements
.push( item
.$element
.get( 0 ) );
5098 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
5099 this.$group
.append( itemElements
);
5100 this.items
.push
.apply( this.items
, items
);
5101 } else if ( index
=== 0 ) {
5102 this.$group
.prepend( itemElements
);
5103 this.items
.unshift
.apply( this.items
, items
);
5105 this.items
[ index
].$element
.before( itemElements
);
5106 this.items
.splice
.apply( this.items
, [ index
, 0 ].concat( items
) );
5113 * Remove the specified items from a group.
5115 * Removed items are detached (not removed) from the DOM so that they may be reused.
5116 * To remove all items from a group, you may wish to use the #clearItems method instead.
5118 * @param {OO.ui.Element[]} items An array of items to remove
5121 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
5122 var i
, len
, item
, index
, remove
, itemEvent
;
5124 // Remove specific items
5125 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
5127 index
= this.items
.indexOf( item
);
5128 if ( index
!== -1 ) {
5130 item
.connect
&& item
.disconnect
&&
5131 !$.isEmptyObject( this.aggregateItemEvents
)
5134 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
5135 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
5137 item
.disconnect( this, remove
);
5139 item
.setElementGroup( null );
5140 this.items
.splice( index
, 1 );
5141 item
.$element
.detach();
5149 * Clear all items from the group.
5151 * Cleared items are detached from the DOM, not removed, so that they may be reused.
5152 * To remove only a subset of items from a group, use the #removeItems method.
5156 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
5157 var i
, len
, item
, remove
, itemEvent
;
5160 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5161 item
= this.items
[ i
];
5163 item
.connect
&& item
.disconnect
&&
5164 !$.isEmptyObject( this.aggregateItemEvents
)
5167 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
5168 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
5170 item
.disconnect( this, remove
);
5172 item
.setElementGroup( null );
5173 item
.$element
.detach();
5181 * DraggableElement is a mixin class used to create elements that can be clicked
5182 * and dragged by a mouse to a new position within a group. This class must be used
5183 * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
5184 * the draggable elements.
5191 OO
.ui
.mixin
.DraggableElement
= function OoUiMixinDraggableElement() {
5195 // Initialize and events
5197 .attr( 'draggable', true )
5198 .addClass( 'oo-ui-draggableElement' )
5200 dragstart
: this.onDragStart
.bind( this ),
5201 dragover
: this.onDragOver
.bind( this ),
5202 dragend
: this.onDragEnd
.bind( this ),
5203 drop
: this.onDrop
.bind( this )
5207 OO
.initClass( OO
.ui
.mixin
.DraggableElement
);
5214 * A dragstart event is emitted when the user clicks and begins dragging an item.
5215 * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
5220 * A dragend event is emitted when the user drags an item and releases the mouse,
5221 * thus terminating the drag operation.
5226 * A drop event is emitted when the user drags an item and then releases the mouse button
5227 * over a valid target.
5230 /* Static Properties */
5233 * @inheritdoc OO.ui.mixin.ButtonElement
5235 OO
.ui
.mixin
.DraggableElement
.static.cancelButtonMouseDownEvents
= false;
5240 * Respond to dragstart event.
5243 * @param {jQuery.Event} event jQuery event
5246 OO
.ui
.mixin
.DraggableElement
.prototype.onDragStart = function ( e
) {
5247 var dataTransfer
= e
.originalEvent
.dataTransfer
;
5248 // Define drop effect
5249 dataTransfer
.dropEffect
= 'none';
5250 dataTransfer
.effectAllowed
= 'move';
5252 // We must set up a dataTransfer data property or Firefox seems to
5253 // ignore the fact the element is draggable.
5255 dataTransfer
.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
5257 // The above is only for Firefox. Move on if it fails.
5259 // Add dragging class
5260 this.$element
.addClass( 'oo-ui-draggableElement-dragging' );
5262 this.emit( 'dragstart', this );
5267 * Respond to dragend event.
5272 OO
.ui
.mixin
.DraggableElement
.prototype.onDragEnd = function () {
5273 this.$element
.removeClass( 'oo-ui-draggableElement-dragging' );
5274 this.emit( 'dragend' );
5278 * Handle drop event.
5281 * @param {jQuery.Event} event jQuery event
5284 OO
.ui
.mixin
.DraggableElement
.prototype.onDrop = function ( e
) {
5286 this.emit( 'drop', e
);
5290 * In order for drag/drop to work, the dragover event must
5291 * return false and stop propogation.
5295 OO
.ui
.mixin
.DraggableElement
.prototype.onDragOver = function ( e
) {
5301 * Store it in the DOM so we can access from the widget drag event
5304 * @param {number} Item index
5306 OO
.ui
.mixin
.DraggableElement
.prototype.setIndex = function ( index
) {
5307 if ( this.index
!== index
) {
5309 this.$element
.data( 'index', index
);
5317 * @return {number} Item index
5319 OO
.ui
.mixin
.DraggableElement
.prototype.getIndex = function () {
5324 * DraggableGroupElement is a mixin class used to create a group element to
5325 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
5326 * The class is used with OO.ui.mixin.DraggableElement.
5330 * @mixins OO.ui.mixin.GroupElement
5333 * @param {Object} [config] Configuration options
5334 * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
5335 * should match the layout of the items. Items displayed in a single row
5336 * or in several rows should use horizontal orientation. The vertical orientation should only be
5337 * used when the items are displayed in a single column. Defaults to 'vertical'
5339 OO
.ui
.mixin
.DraggableGroupElement
= function OoUiMixinDraggableGroupElement( config
) {
5340 // Configuration initialization
5341 config
= config
|| {};
5343 // Parent constructor
5344 OO
.ui
.mixin
.GroupElement
.call( this, config
);
5347 this.orientation
= config
.orientation
|| 'vertical';
5348 this.dragItem
= null;
5349 this.itemDragOver
= null;
5351 this.sideInsertion
= '';
5355 dragstart
: 'itemDragStart',
5356 dragend
: 'itemDragEnd',
5359 this.connect( this, {
5360 itemDragStart
: 'onItemDragStart',
5361 itemDrop
: 'onItemDrop',
5362 itemDragEnd
: 'onItemDragEnd'
5365 dragover
: this.onDragOver
.bind( this ),
5366 dragleave
: this.onDragLeave
.bind( this )
5370 if ( Array
.isArray( config
.items
) ) {
5371 this.addItems( config
.items
);
5373 this.$placeholder
= $( '<div>' )
5374 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
5376 .addClass( 'oo-ui-draggableGroupElement' )
5377 .append( this.$status
)
5378 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation
=== 'horizontal' )
5379 .prepend( this.$placeholder
);
5383 OO
.mixinClass( OO
.ui
.mixin
.DraggableGroupElement
, OO
.ui
.mixin
.GroupElement
);
5388 * A 'reorder' event is emitted when the order of items in the group changes.
5391 * @param {OO.ui.mixin.DraggableElement} item Reordered item
5392 * @param {number} [newIndex] New index for the item
5398 * Respond to item drag start event
5401 * @param {OO.ui.mixin.DraggableElement} item Dragged item
5403 OO
.ui
.mixin
.DraggableGroupElement
.prototype.onItemDragStart = function ( item
) {
5406 // Map the index of each object
5407 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5408 this.items
[ i
].setIndex( i
);
5411 if ( this.orientation
=== 'horizontal' ) {
5412 // Set the height of the indicator
5413 this.$placeholder
.css( {
5414 height
: item
.$element
.outerHeight(),
5418 // Set the width of the indicator
5419 this.$placeholder
.css( {
5421 width
: item
.$element
.outerWidth()
5424 this.setDragItem( item
);
5428 * Respond to item drag end event
5432 OO
.ui
.mixin
.DraggableGroupElement
.prototype.onItemDragEnd = function () {
5433 this.unsetDragItem();
5438 * Handle drop event and switch the order of the items accordingly
5441 * @param {OO.ui.mixin.DraggableElement} item Dropped item
5444 OO
.ui
.mixin
.DraggableGroupElement
.prototype.onItemDrop = function ( item
) {
5445 var toIndex
= item
.getIndex();
5446 // Check if the dropped item is from the current group
5447 // TODO: Figure out a way to configure a list of legally droppable
5448 // elements even if they are not yet in the list
5449 if ( this.getDragItem() ) {
5450 // If the insertion point is 'after', the insertion index
5451 // is shifted to the right (or to the left in RTL, hence 'after')
5452 if ( this.sideInsertion
=== 'after' ) {
5455 // Emit change event
5456 this.emit( 'reorder', this.getDragItem(), toIndex
);
5458 this.unsetDragItem();
5459 // Return false to prevent propogation
5464 * Handle dragleave event.
5468 OO
.ui
.mixin
.DraggableGroupElement
.prototype.onDragLeave = function () {
5469 // This means the item was dragged outside the widget
5472 .addClass( 'oo-ui-element-hidden' );
5476 * Respond to dragover event
5479 * @param {jQuery.Event} event Event details
5481 OO
.ui
.mixin
.DraggableGroupElement
.prototype.onDragOver = function ( e
) {
5482 var dragOverObj
, $optionWidget
, itemOffset
, itemMidpoint
, itemBoundingRect
,
5483 itemSize
, cssOutput
, dragPosition
, itemIndex
, itemPosition
,
5484 clientX
= e
.originalEvent
.clientX
,
5485 clientY
= e
.originalEvent
.clientY
;
5487 // Get the OptionWidget item we are dragging over
5488 dragOverObj
= this.getElementDocument().elementFromPoint( clientX
, clientY
);
5489 $optionWidget
= $( dragOverObj
).closest( '.oo-ui-draggableElement' );
5490 if ( $optionWidget
[ 0 ] ) {
5491 itemOffset
= $optionWidget
.offset();
5492 itemBoundingRect
= $optionWidget
[ 0 ].getBoundingClientRect();
5493 itemPosition
= $optionWidget
.position();
5494 itemIndex
= $optionWidget
.data( 'index' );
5499 this.isDragging() &&
5500 itemIndex
!== this.getDragItem().getIndex()
5502 if ( this.orientation
=== 'horizontal' ) {
5503 // Calculate where the mouse is relative to the item width
5504 itemSize
= itemBoundingRect
.width
;
5505 itemMidpoint
= itemBoundingRect
.left
+ itemSize
/ 2;
5506 dragPosition
= clientX
;
5507 // Which side of the item we hover over will dictate
5508 // where the placeholder will appear, on the left or
5511 left
: dragPosition
< itemMidpoint
? itemPosition
.left
: itemPosition
.left
+ itemSize
,
5512 top
: itemPosition
.top
5515 // Calculate where the mouse is relative to the item height
5516 itemSize
= itemBoundingRect
.height
;
5517 itemMidpoint
= itemBoundingRect
.top
+ itemSize
/ 2;
5518 dragPosition
= clientY
;
5519 // Which side of the item we hover over will dictate
5520 // where the placeholder will appear, on the top or
5523 top
: dragPosition
< itemMidpoint
? itemPosition
.top
: itemPosition
.top
+ itemSize
,
5524 left
: itemPosition
.left
5527 // Store whether we are before or after an item to rearrange
5528 // For horizontal layout, we need to account for RTL, as this is flipped
5529 if ( this.orientation
=== 'horizontal' && this.$element
.css( 'direction' ) === 'rtl' ) {
5530 this.sideInsertion
= dragPosition
< itemMidpoint
? 'after' : 'before';
5532 this.sideInsertion
= dragPosition
< itemMidpoint
? 'before' : 'after';
5534 // Add drop indicator between objects
5537 .removeClass( 'oo-ui-element-hidden' );
5539 // This means the item was dragged outside the widget
5542 .addClass( 'oo-ui-element-hidden' );
5549 * Set a dragged item
5551 * @param {OO.ui.mixin.DraggableElement} item Dragged item
5553 OO
.ui
.mixin
.DraggableGroupElement
.prototype.setDragItem = function ( item
) {
5554 this.dragItem
= item
;
5558 * Unset the current dragged item
5560 OO
.ui
.mixin
.DraggableGroupElement
.prototype.unsetDragItem = function () {
5561 this.dragItem
= null;
5562 this.itemDragOver
= null;
5563 this.$placeholder
.addClass( 'oo-ui-element-hidden' );
5564 this.sideInsertion
= '';
5568 * Get the item that is currently being dragged.
5570 * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
5572 OO
.ui
.mixin
.DraggableGroupElement
.prototype.getDragItem = function () {
5573 return this.dragItem
;
5577 * Check if an item in the group is currently being dragged.
5579 * @return {Boolean} Item is being dragged
5581 OO
.ui
.mixin
.DraggableGroupElement
.prototype.isDragging = function () {
5582 return this.getDragItem() !== null;
5586 * IconElement is often mixed into other classes to generate an icon.
5587 * Icons are graphics, about the size of normal text. They are used to aid the user
5588 * in locating a control or to convey information in a space-efficient way. See the
5589 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
5590 * included in the library.
5592 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5598 * @param {Object} [config] Configuration options
5599 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
5600 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
5601 * the icon element be set to an existing icon instead of the one generated by this class, set a
5602 * value using a jQuery selection. For example:
5604 * // Use a <div> tag instead of a <span>
5606 * // Use an existing icon element instead of the one generated by the class
5607 * $icon: this.$element
5608 * // Use an icon element from a child widget
5609 * $icon: this.childwidget.$element
5610 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
5611 * symbolic names. A map is used for i18n purposes and contains a `default` icon
5612 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
5613 * by the user's language.
5615 * Example of an i18n map:
5617 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5618 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
5619 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5620 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
5621 * text. The icon title is displayed when users move the mouse over the icon.
5623 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
5624 // Configuration initialization
5625 config
= config
|| {};
5630 this.iconTitle
= null;
5633 this.setIcon( config
.icon
|| this.constructor.static.icon
);
5634 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
5635 this.setIconElement( config
.$icon
|| $( '<span>' ) );
5640 OO
.initClass( OO
.ui
.mixin
.IconElement
);
5642 /* Static Properties */
5645 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
5646 * for i18n purposes and contains a `default` icon name and additional names keyed by
5647 * language code. The `default` name is used when no icon is keyed by the user's language.
5649 * Example of an i18n map:
5651 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5653 * Note: the static property will be overridden if the #icon configuration is used.
5657 * @property {Object|string}
5659 OO
.ui
.mixin
.IconElement
.static.icon
= null;
5662 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
5663 * function that returns title text, or `null` for no title.
5665 * The static property will be overridden if the #iconTitle configuration is used.
5669 * @property {string|Function|null}
5671 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
5676 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
5677 * applies to the specified icon element instead of the one created by the class. If an icon
5678 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
5679 * and mixin methods will no longer affect the element.
5681 * @param {jQuery} $icon Element to use as icon
5683 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
5686 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
5687 .removeAttr( 'title' );
5691 .addClass( 'oo-ui-iconElement-icon' )
5692 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
5693 if ( this.iconTitle
!== null ) {
5694 this.$icon
.attr( 'title', this.iconTitle
);
5697 this.updateThemeClasses();
5701 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
5702 * The icon parameter can also be set to a map of icon names. See the #icon config setting
5705 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
5706 * by language code, or `null` to remove the icon.
5709 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
5710 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
5711 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
5713 if ( this.icon
!== icon
) {
5715 if ( this.icon
!== null ) {
5716 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
5718 if ( icon
!== null ) {
5719 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
5725 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
5726 this.updateThemeClasses();
5732 * Set the icon title. Use `null` to remove the title.
5734 * @param {string|Function|null} iconTitle A text string used as the icon title,
5735 * a function that returns title text, or `null` for no title.
5738 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
5739 iconTitle
= typeof iconTitle
=== 'function' ||
5740 ( typeof iconTitle
=== 'string' && iconTitle
.length
) ?
5741 OO
.ui
.resolveMsg( iconTitle
) : null;
5743 if ( this.iconTitle
!== iconTitle
) {
5744 this.iconTitle
= iconTitle
;
5746 if ( this.iconTitle
!== null ) {
5747 this.$icon
.attr( 'title', iconTitle
);
5749 this.$icon
.removeAttr( 'title' );
5758 * Get the symbolic name of the icon.
5760 * @return {string} Icon name
5762 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
5767 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
5769 * @return {string} Icon title text
5771 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
5772 return this.iconTitle
;
5776 * IndicatorElement is often mixed into other classes to generate an indicator.
5777 * Indicators are small graphics that are generally used in two ways:
5779 * - To draw attention to the status of an item. For example, an indicator might be
5780 * used to show that an item in a list has errors that need to be resolved.
5781 * - To clarify the function of a control that acts in an exceptional way (a button
5782 * that opens a menu instead of performing an action directly, for example).
5784 * For a list of indicators included in the library, please see the
5785 * [OOjs UI documentation on MediaWiki] [1].
5787 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5793 * @param {Object} [config] Configuration options
5794 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
5795 * configuration is omitted, the indicator element will use a generated `<span>`.
5796 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5797 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
5799 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5800 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
5801 * or a function that returns title text. The indicator title is displayed when users move
5802 * the mouse over the indicator.
5804 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
5805 // Configuration initialization
5806 config
= config
|| {};
5809 this.$indicator
= null;
5810 this.indicator
= null;
5811 this.indicatorTitle
= null;
5814 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
5815 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
5816 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
5821 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
5823 /* Static Properties */
5826 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5827 * The static property will be overridden if the #indicator configuration is used.
5831 * @property {string|null}
5833 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
5836 * A text string used as the indicator title, a function that returns title text, or `null`
5837 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
5841 * @property {string|Function|null}
5843 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
5848 * Set the indicator element.
5850 * If an element is already set, it will be cleaned up before setting up the new element.
5852 * @param {jQuery} $indicator Element to use as indicator
5854 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
5855 if ( this.$indicator
) {
5857 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
5858 .removeAttr( 'title' );
5861 this.$indicator
= $indicator
5862 .addClass( 'oo-ui-indicatorElement-indicator' )
5863 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
5864 if ( this.indicatorTitle
!== null ) {
5865 this.$indicator
.attr( 'title', this.indicatorTitle
);
5868 this.updateThemeClasses();
5872 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
5874 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
5877 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
5878 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
5880 if ( this.indicator
!== indicator
) {
5881 if ( this.$indicator
) {
5882 if ( this.indicator
!== null ) {
5883 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
5885 if ( indicator
!== null ) {
5886 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
5889 this.indicator
= indicator
;
5892 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
5893 this.updateThemeClasses();
5899 * Set the indicator title.
5901 * The title is displayed when a user moves the mouse over the indicator.
5903 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
5904 * `null` for no indicator title
5907 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
5908 indicatorTitle
= typeof indicatorTitle
=== 'function' ||
5909 ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ?
5910 OO
.ui
.resolveMsg( indicatorTitle
) : null;
5912 if ( this.indicatorTitle
!== indicatorTitle
) {
5913 this.indicatorTitle
= indicatorTitle
;
5914 if ( this.$indicator
) {
5915 if ( this.indicatorTitle
!== null ) {
5916 this.$indicator
.attr( 'title', indicatorTitle
);
5918 this.$indicator
.removeAttr( 'title' );
5927 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5929 * @return {string} Symbolic name of indicator
5931 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
5932 return this.indicator
;
5936 * Get the indicator title.
5938 * The title is displayed when a user moves the mouse over the indicator.
5940 * @return {string} Indicator title text
5942 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
5943 return this.indicatorTitle
;
5947 * LabelElement is often mixed into other classes to generate a label, which
5948 * helps identify the function of an interface element.
5949 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5951 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5957 * @param {Object} [config] Configuration options
5958 * @cfg {jQuery} [$label] The label element created by the class. If this
5959 * configuration is omitted, the label element will use a generated `<span>`.
5960 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
5961 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
5962 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
5963 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5964 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5965 * The label will be truncated to fit if necessary.
5967 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
5968 // Configuration initialization
5969 config
= config
|| {};
5974 this.autoFitLabel
= config
.autoFitLabel
=== undefined || !!config
.autoFitLabel
;
5977 this.setLabel( config
.label
|| this.constructor.static.label
);
5978 this.setLabelElement( config
.$label
|| $( '<span>' ) );
5983 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
5988 * @event labelChange
5989 * @param {string} value
5992 /* Static Properties */
5995 * The label text. The label can be specified as a plaintext string, a function that will
5996 * produce a string in the future, or `null` for no label. The static value will
5997 * be overridden if a label is specified with the #label config option.
6001 * @property {string|Function|null}
6003 OO
.ui
.mixin
.LabelElement
.static.label
= null;
6008 * Set the label element.
6010 * If an element is already set, it will be cleaned up before setting up the new element.
6012 * @param {jQuery} $label Element to use as label
6014 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
6015 if ( this.$label
) {
6016 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
6019 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
6020 this.setLabelContent( this.label
);
6026 * An empty string will result in the label being hidden. A string containing only whitespace will
6027 * be converted to a single ` `.
6029 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
6030 * text; or null for no label
6033 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
6034 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
6035 label
= ( ( typeof label
=== 'string' && label
.length
) || label
instanceof jQuery
|| label
instanceof OO
.ui
.HtmlSnippet
) ? label
: null;
6037 this.$element
.toggleClass( 'oo-ui-labelElement', !!label
);
6039 if ( this.label
!== label
) {
6040 if ( this.$label
) {
6041 this.setLabelContent( label
);
6044 this.emit( 'labelChange' );
6053 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
6054 * text; or null for no label
6056 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
6065 OO
.ui
.mixin
.LabelElement
.prototype.fitLabel = function () {
6066 if ( this.$label
&& this.$label
.autoEllipsis
&& this.autoFitLabel
) {
6067 this.$label
.autoEllipsis( { hasSpan
: false, tooltip
: true } );
6074 * Set the content of the label.
6076 * Do not call this method until after the label element has been set by #setLabelElement.
6079 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
6080 * text; or null for no label
6082 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
6083 if ( typeof label
=== 'string' ) {
6084 if ( label
.match( /^\s*$/ ) ) {
6085 // Convert whitespace only string to a single non-breaking space
6086 this.$label
.html( ' ' );
6088 this.$label
.text( label
);
6090 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
6091 this.$label
.html( label
.toString() );
6092 } else if ( label
instanceof jQuery
) {
6093 this.$label
.empty().append( label
);
6095 this.$label
.empty();
6100 * LookupElement is a mixin that creates a {@link OO.ui.FloatingMenuSelectWidget menu} of suggested values for
6101 * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
6102 * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
6103 * from the lookup menu, that value becomes the value of the input field.
6105 * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
6106 * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
6107 * re-enable lookups.
6109 * See the [OOjs UI demos][1] for an example.
6111 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
6117 * @param {Object} [config] Configuration options
6118 * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
6119 * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
6120 * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
6121 * By default, the lookup menu is not generated and displayed until the user begins to type.
6122 * @cfg {boolean} [highlightFirst=true] Whether the first lookup result should be highlighted (so, that the user can
6123 * take it over into the input with simply pressing return) automatically or not.
6125 OO
.ui
.mixin
.LookupElement
= function OoUiMixinLookupElement( config
) {
6126 // Configuration initialization
6127 config
= $.extend( { highlightFirst
: true }, config
);
6129 // Mixin constructors
6130 OO
.ui
.mixin
.RequestManager
.call( this, config
);
6133 this.$overlay
= config
.$overlay
|| this.$element
;
6134 this.lookupMenu
= new OO
.ui
.FloatingMenuSelectWidget( {
6137 $container
: config
.$container
|| this.$element
6140 this.allowSuggestionsWhenEmpty
= config
.allowSuggestionsWhenEmpty
|| false;
6142 this.lookupsDisabled
= false;
6143 this.lookupInputFocused
= false;
6144 this.lookupHighlightFirstItem
= config
.highlightFirst
;
6148 focus
: this.onLookupInputFocus
.bind( this ),
6149 blur
: this.onLookupInputBlur
.bind( this ),
6150 mousedown
: this.onLookupInputMouseDown
.bind( this )
6152 this.connect( this, { change
: 'onLookupInputChange' } );
6153 this.lookupMenu
.connect( this, {
6154 toggle
: 'onLookupMenuToggle',
6155 choose
: 'onLookupMenuItemChoose'
6159 this.$element
.addClass( 'oo-ui-lookupElement' );
6160 this.lookupMenu
.$element
.addClass( 'oo-ui-lookupElement-menu' );
6161 this.$overlay
.append( this.lookupMenu
.$element
);
6166 OO
.mixinClass( OO
.ui
.mixin
.LookupElement
, OO
.ui
.mixin
.RequestManager
);
6171 * Handle input focus event.
6174 * @param {jQuery.Event} e Input focus event
6176 OO
.ui
.mixin
.LookupElement
.prototype.onLookupInputFocus = function () {
6177 this.lookupInputFocused
= true;
6178 this.populateLookupMenu();
6182 * Handle input blur event.
6185 * @param {jQuery.Event} e Input blur event
6187 OO
.ui
.mixin
.LookupElement
.prototype.onLookupInputBlur = function () {
6188 this.closeLookupMenu();
6189 this.lookupInputFocused
= false;
6193 * Handle input mouse down event.
6196 * @param {jQuery.Event} e Input mouse down event
6198 OO
.ui
.mixin
.LookupElement
.prototype.onLookupInputMouseDown = function () {
6199 // Only open the menu if the input was already focused.
6200 // This way we allow the user to open the menu again after closing it with Esc
6201 // by clicking in the input. Opening (and populating) the menu when initially
6202 // clicking into the input is handled by the focus handler.
6203 if ( this.lookupInputFocused
&& !this.lookupMenu
.isVisible() ) {
6204 this.populateLookupMenu();
6209 * Handle input change event.
6212 * @param {string} value New input value
6214 OO
.ui
.mixin
.LookupElement
.prototype.onLookupInputChange = function () {
6215 if ( this.lookupInputFocused
) {
6216 this.populateLookupMenu();
6221 * Handle the lookup menu being shown/hidden.
6224 * @param {boolean} visible Whether the lookup menu is now visible.
6226 OO
.ui
.mixin
.LookupElement
.prototype.onLookupMenuToggle = function ( visible
) {
6228 // When the menu is hidden, abort any active request and clear the menu.
6229 // This has to be done here in addition to closeLookupMenu(), because
6230 // MenuSelectWidget will close itself when the user presses Esc.
6231 this.abortLookupRequest();
6232 this.lookupMenu
.clearItems();
6237 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
6240 * @param {OO.ui.MenuOptionWidget} item Selected item
6242 OO
.ui
.mixin
.LookupElement
.prototype.onLookupMenuItemChoose = function ( item
) {
6243 this.setValue( item
.getData() );
6250 * @return {OO.ui.FloatingMenuSelectWidget}
6252 OO
.ui
.mixin
.LookupElement
.prototype.getLookupMenu = function () {
6253 return this.lookupMenu
;
6257 * Disable or re-enable lookups.
6259 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
6261 * @param {boolean} disabled Disable lookups
6263 OO
.ui
.mixin
.LookupElement
.prototype.setLookupsDisabled = function ( disabled
) {
6264 this.lookupsDisabled
= !!disabled
;
6268 * Open the menu. If there are no entries in the menu, this does nothing.
6273 OO
.ui
.mixin
.LookupElement
.prototype.openLookupMenu = function () {
6274 if ( !this.lookupMenu
.isEmpty() ) {
6275 this.lookupMenu
.toggle( true );
6281 * Close the menu, empty it, and abort any pending request.
6286 OO
.ui
.mixin
.LookupElement
.prototype.closeLookupMenu = function () {
6287 this.lookupMenu
.toggle( false );
6288 this.abortLookupRequest();
6289 this.lookupMenu
.clearItems();
6294 * Request menu items based on the input's current value, and when they arrive,
6295 * populate the menu with these items and show the menu.
6297 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
6302 OO
.ui
.mixin
.LookupElement
.prototype.populateLookupMenu = function () {
6304 value
= this.getValue();
6306 if ( this.lookupsDisabled
|| this.isReadOnly() ) {
6310 // If the input is empty, clear the menu, unless suggestions when empty are allowed.
6311 if ( !this.allowSuggestionsWhenEmpty
&& value
=== '' ) {
6312 this.closeLookupMenu();
6313 // Skip population if there is already a request pending for the current value
6314 } else if ( value
!== this.lookupQuery
) {
6315 this.getLookupMenuItems()
6316 .done( function ( items
) {
6317 widget
.lookupMenu
.clearItems();
6318 if ( items
.length
) {
6322 widget
.initializeLookupMenuSelection();
6324 widget
.lookupMenu
.toggle( false );
6327 .fail( function () {
6328 widget
.lookupMenu
.clearItems();
6336 * Highlight the first selectable item in the menu, if configured.
6341 OO
.ui
.mixin
.LookupElement
.prototype.initializeLookupMenuSelection = function () {
6342 if ( this.lookupHighlightFirstItem
&& !this.lookupMenu
.getSelectedItem() ) {
6343 this.lookupMenu
.highlightItem( this.lookupMenu
.getFirstSelectableItem() );
6348 * Get lookup menu items for the current query.
6351 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
6352 * the done event. If the request was aborted to make way for a subsequent request, this promise
6353 * will not be rejected: it will remain pending forever.
6355 OO
.ui
.mixin
.LookupElement
.prototype.getLookupMenuItems = function () {
6356 return this.getRequestData().then( function ( data
) {
6357 return this.getLookupMenuOptionsFromData( data
);
6362 * Abort the currently pending lookup request, if any.
6366 OO
.ui
.mixin
.LookupElement
.prototype.abortLookupRequest = function () {
6367 this.abortRequest();
6371 * Get a new request object of the current lookup query value.
6376 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
6378 OO
.ui
.mixin
.LookupElement
.prototype.getLookupRequest
= null;
6381 * Pre-process data returned by the request from #getLookupRequest.
6383 * The return value of this function will be cached, and any further queries for the given value
6384 * will use the cache rather than doing API requests.
6389 * @param {Mixed} response Response from server
6390 * @return {Mixed} Cached result data
6392 OO
.ui
.mixin
.LookupElement
.prototype.getLookupCacheDataFromResponse
= null;
6395 * Get a list of menu option widgets from the (possibly cached) data returned by
6396 * #getLookupCacheDataFromResponse.
6401 * @param {Mixed} data Cached result data, usually an array
6402 * @return {OO.ui.MenuOptionWidget[]} Menu items
6404 OO
.ui
.mixin
.LookupElement
.prototype.getLookupMenuOptionsFromData
= null;
6407 * Set the read-only state of the widget.
6409 * This will also disable/enable the lookups functionality.
6411 * @param {boolean} readOnly Make input read-only
6414 OO
.ui
.mixin
.LookupElement
.prototype.setReadOnly = function ( readOnly
) {
6416 // Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
6417 OO
.ui
.TextInputWidget
.prototype.setReadOnly
.call( this, readOnly
);
6419 // During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor
6420 if ( this.isReadOnly() && this.lookupMenu
) {
6421 this.closeLookupMenu();
6428 * @inheritdoc OO.ui.mixin.RequestManager
6430 OO
.ui
.mixin
.LookupElement
.prototype.getRequestQuery = function () {
6431 return this.getValue();
6435 * @inheritdoc OO.ui.mixin.RequestManager
6437 OO
.ui
.mixin
.LookupElement
.prototype.getRequest = function () {
6438 return this.getLookupRequest();
6442 * @inheritdoc OO.ui.mixin.RequestManager
6444 OO
.ui
.mixin
.LookupElement
.prototype.getRequestCacheDataFromResponse = function ( response
) {
6445 return this.getLookupCacheDataFromResponse( response
);
6449 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
6450 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
6451 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
6452 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
6458 * @param {Object} [config] Configuration options
6459 * @cfg {Object} [popup] Configuration to pass to popup
6460 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6462 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
6463 // Configuration initialization
6464 config
= config
|| {};
6467 this.popup
= new OO
.ui
.PopupWidget( $.extend(
6468 { autoClose
: true },
6470 { $autoCloseIgnore
: this.$element
}
6479 * @return {OO.ui.PopupWidget} Popup widget
6481 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
6486 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
6487 * additional functionality to an element created by another class. The class provides
6488 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
6489 * which are used to customize the look and feel of a widget to better describe its
6490 * importance and functionality.
6492 * The library currently contains the following styling flags for general use:
6494 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
6495 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
6496 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
6498 * The flags affect the appearance of the buttons:
6501 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
6502 * var button1 = new OO.ui.ButtonWidget( {
6503 * label: 'Constructive',
6504 * flags: 'constructive'
6506 * var button2 = new OO.ui.ButtonWidget( {
6507 * label: 'Destructive',
6508 * flags: 'destructive'
6510 * var button3 = new OO.ui.ButtonWidget( {
6511 * label: 'Progressive',
6512 * flags: 'progressive'
6514 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
6516 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
6517 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6519 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6525 * @param {Object} [config] Configuration options
6526 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
6527 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
6528 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6529 * @cfg {jQuery} [$flagged] The flagged element. By default,
6530 * the flagged functionality is applied to the element created by the class ($element).
6531 * If a different element is specified, the flagged functionality will be applied to it instead.
6533 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
6534 // Configuration initialization
6535 config
= config
|| {};
6539 this.$flagged
= null;
6542 this.setFlags( config
.flags
);
6543 this.setFlaggedElement( config
.$flagged
|| this.$element
);
6550 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
6551 * parameter contains the name of each modified flag and indicates whether it was
6554 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
6555 * that the flag was added, `false` that the flag was removed.
6561 * Set the flagged element.
6563 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
6564 * If an element is already set, the method will remove the mixin’s effect on that element.
6566 * @param {jQuery} $flagged Element that should be flagged
6568 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
6569 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
6570 return 'oo-ui-flaggedElement-' + flag
;
6573 if ( this.$flagged
) {
6574 this.$flagged
.removeClass( classNames
);
6577 this.$flagged
= $flagged
.addClass( classNames
);
6581 * Check if the specified flag is set.
6583 * @param {string} flag Name of flag
6584 * @return {boolean} The flag is set
6586 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
6587 // This may be called before the constructor, thus before this.flags is set
6588 return this.flags
&& ( flag
in this.flags
);
6592 * Get the names of all flags set.
6594 * @return {string[]} Flag names
6596 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
6597 // This may be called before the constructor, thus before this.flags is set
6598 return Object
.keys( this.flags
|| {} );
6607 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
6608 var flag
, className
,
6611 classPrefix
= 'oo-ui-flaggedElement-';
6613 for ( flag
in this.flags
) {
6614 className
= classPrefix
+ flag
;
6615 changes
[ flag
] = false;
6616 delete this.flags
[ flag
];
6617 remove
.push( className
);
6620 if ( this.$flagged
) {
6621 this.$flagged
.removeClass( remove
.join( ' ' ) );
6624 this.updateThemeClasses();
6625 this.emit( 'flag', changes
);
6631 * Add one or more flags.
6633 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
6634 * or an object keyed by flag name with a boolean value that indicates whether the flag should
6635 * be added (`true`) or removed (`false`).
6639 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
6640 var i
, len
, flag
, className
,
6644 classPrefix
= 'oo-ui-flaggedElement-';
6646 if ( typeof flags
=== 'string' ) {
6647 className
= classPrefix
+ flags
;
6649 if ( !this.flags
[ flags
] ) {
6650 this.flags
[ flags
] = true;
6651 add
.push( className
);
6653 } else if ( Array
.isArray( flags
) ) {
6654 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
6656 className
= classPrefix
+ flag
;
6658 if ( !this.flags
[ flag
] ) {
6659 changes
[ flag
] = true;
6660 this.flags
[ flag
] = true;
6661 add
.push( className
);
6664 } else if ( OO
.isPlainObject( flags
) ) {
6665 for ( flag
in flags
) {
6666 className
= classPrefix
+ flag
;
6667 if ( flags
[ flag
] ) {
6669 if ( !this.flags
[ flag
] ) {
6670 changes
[ flag
] = true;
6671 this.flags
[ flag
] = true;
6672 add
.push( className
);
6676 if ( this.flags
[ flag
] ) {
6677 changes
[ flag
] = false;
6678 delete this.flags
[ flag
];
6679 remove
.push( className
);
6685 if ( this.$flagged
) {
6687 .addClass( add
.join( ' ' ) )
6688 .removeClass( remove
.join( ' ' ) );
6691 this.updateThemeClasses();
6692 this.emit( 'flag', changes
);
6698 * TitledElement is mixed into other classes to provide a `title` attribute.
6699 * Titles are rendered by the browser and are made visible when the user moves
6700 * the mouse over the element. Titles are not visible on touch devices.
6703 * // TitledElement provides a 'title' attribute to the
6704 * // ButtonWidget class
6705 * var button = new OO.ui.ButtonWidget( {
6706 * label: 'Button with Title',
6707 * title: 'I am a button'
6709 * $( 'body' ).append( button.$element );
6715 * @param {Object} [config] Configuration options
6716 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
6717 * If this config is omitted, the title functionality is applied to $element, the
6718 * element created by the class.
6719 * @cfg {string|Function} [title] The title text or a function that returns text. If
6720 * this config is omitted, the value of the {@link #static-title static title} property is used.
6722 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
6723 // Configuration initialization
6724 config
= config
|| {};
6727 this.$titled
= null;
6731 this.setTitle( config
.title
|| this.constructor.static.title
);
6732 this.setTitledElement( config
.$titled
|| this.$element
);
6737 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
6739 /* Static Properties */
6742 * The title text, a function that returns text, or `null` for no title. The value of the static property
6743 * is overridden if the #title config option is used.
6747 * @property {string|Function|null}
6749 OO
.ui
.mixin
.TitledElement
.static.title
= null;
6754 * Set the titled element.
6756 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
6757 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
6759 * @param {jQuery} $titled Element that should use the 'titled' functionality
6761 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
6762 if ( this.$titled
) {
6763 this.$titled
.removeAttr( 'title' );
6766 this.$titled
= $titled
;
6768 this.$titled
.attr( 'title', this.title
);
6775 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
6778 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
6779 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
6780 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
6782 if ( this.title
!== title
) {
6783 if ( this.$titled
) {
6784 if ( title
!== null ) {
6785 this.$titled
.attr( 'title', title
);
6787 this.$titled
.removeAttr( 'title' );
6799 * @return {string} Title string
6801 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
6806 * Element that can be automatically clipped to visible boundaries.
6808 * Whenever the element's natural height changes, you have to call
6809 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
6810 * clipping correctly.
6812 * The dimensions of #$clippableContainer will be compared to the boundaries of the
6813 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
6814 * then #$clippable will be given a fixed reduced height and/or width and will be made
6815 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
6816 * but you can build a static footer by setting #$clippableContainer to an element that contains
6817 * #$clippable and the footer.
6823 * @param {Object} [config] Configuration options
6824 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
6825 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
6826 * omit to use #$clippable
6828 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
6829 // Configuration initialization
6830 config
= config
|| {};
6833 this.$clippable
= null;
6834 this.$clippableContainer
= null;
6835 this.clipping
= false;
6836 this.clippedHorizontally
= false;
6837 this.clippedVertically
= false;
6838 this.$clippableScrollableContainer
= null;
6839 this.$clippableScroller
= null;
6840 this.$clippableWindow
= null;
6841 this.idealWidth
= null;
6842 this.idealHeight
= null;
6843 this.onClippableScrollHandler
= this.clip
.bind( this );
6844 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
6847 if ( config
.$clippableContainer
) {
6848 this.setClippableContainer( config
.$clippableContainer
);
6850 this.setClippableElement( config
.$clippable
|| this.$element
);
6856 * Set clippable element.
6858 * If an element is already set, it will be cleaned up before setting up the new element.
6860 * @param {jQuery} $clippable Element to make clippable
6862 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
6863 if ( this.$clippable
) {
6864 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
6865 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
6866 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
6869 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
6874 * Set clippable container.
6876 * This is the container that will be measured when deciding whether to clip. When clipping,
6877 * #$clippable will be resized in order to keep the clippable container fully visible.
6879 * If the clippable container is unset, #$clippable will be used.
6881 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
6883 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
6884 this.$clippableContainer
= $clippableContainer
;
6885 if ( this.$clippable
) {
6893 * Do not turn clipping on until after the element is attached to the DOM and visible.
6895 * @param {boolean} [clipping] Enable clipping, omit to toggle
6898 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
6899 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
6901 if ( this.clipping
!== clipping
) {
6902 this.clipping
= clipping
;
6904 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
6905 // If the clippable container is the root, we have to listen to scroll events and check
6906 // jQuery.scrollTop on the window because of browser inconsistencies
6907 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
6908 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
6909 this.$clippableScrollableContainer
;
6910 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
6911 this.$clippableWindow
= $( this.getElementWindow() )
6912 .on( 'resize', this.onClippableWindowResizeHandler
);
6913 // Initial clip after visible
6916 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
6917 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
6919 this.$clippableScrollableContainer
= null;
6920 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
6921 this.$clippableScroller
= null;
6922 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
6923 this.$clippableWindow
= null;
6931 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
6933 * @return {boolean} Element will be clipped to the visible area
6935 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
6936 return this.clipping
;
6940 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
6942 * @return {boolean} Part of the element is being clipped
6944 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
6945 return this.clippedHorizontally
|| this.clippedVertically
;
6949 * Check if the right of the element is being clipped by the nearest scrollable container.
6951 * @return {boolean} Part of the element is being clipped
6953 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
6954 return this.clippedHorizontally
;
6958 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6960 * @return {boolean} Part of the element is being clipped
6962 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
6963 return this.clippedVertically
;
6967 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6969 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6970 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6972 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
6973 this.idealWidth
= width
;
6974 this.idealHeight
= height
;
6976 if ( !this.clipping
) {
6977 // Update dimensions
6978 this.$clippable
.css( { width
: width
, height
: height
} );
6980 // While clipping, idealWidth and idealHeight are not considered
6984 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6985 * the element's natural height changes.
6987 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6988 * overlapped by, the visible area of the nearest scrollable container.
6992 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
6993 var $container
, extraHeight
, extraWidth
, ccOffset
,
6994 $scrollableContainer
, scOffset
, scHeight
, scWidth
,
6995 ccWidth
, scrollerIsWindow
, scrollTop
, scrollLeft
,
6996 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
6997 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
6998 buffer
= 7; // Chosen by fair dice roll
7000 if ( !this.clipping
) {
7001 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
7005 $container
= this.$clippableContainer
|| this.$clippable
;
7006 extraHeight
= $container
.outerHeight() - this.$clippable
.outerHeight();
7007 extraWidth
= $container
.outerWidth() - this.$clippable
.outerWidth();
7008 ccOffset
= $container
.offset();
7009 $scrollableContainer
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
7010 this.$clippableWindow
: this.$clippableScrollableContainer
;
7011 scOffset
= $scrollableContainer
.offset() || { top
: 0, left
: 0 };
7012 scHeight
= $scrollableContainer
.innerHeight() - buffer
;
7013 scWidth
= $scrollableContainer
.innerWidth() - buffer
;
7014 ccWidth
= $container
.outerWidth() + buffer
;
7015 scrollerIsWindow
= this.$clippableScroller
[ 0 ] === this.$clippableWindow
[ 0 ];
7016 scrollTop
= scrollerIsWindow
? this.$clippableScroller
.scrollTop() : 0;
7017 scrollLeft
= scrollerIsWindow
? this.$clippableScroller
.scrollLeft() : 0;
7018 desiredWidth
= ccOffset
.left
< 0 ?
7019 ccWidth
+ ccOffset
.left
:
7020 ( scOffset
.left
+ scrollLeft
+ scWidth
) - ccOffset
.left
;
7021 desiredHeight
= ( scOffset
.top
+ scrollTop
+ scHeight
) - ccOffset
.top
;
7022 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
7023 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
7024 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
7025 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
7026 clipWidth
= allotedWidth
< naturalWidth
;
7027 clipHeight
= allotedHeight
< naturalHeight
;
7030 this.$clippable
.css( { overflowX
: 'scroll', width
: Math
.max( 0, allotedWidth
) } );
7032 this.$clippable
.css( { width
: this.idealWidth
? this.idealWidth
- extraWidth
: '', overflowX
: '' } );
7035 this.$clippable
.css( { overflowY
: 'scroll', height
: Math
.max( 0, allotedHeight
) } );
7037 this.$clippable
.css( { height
: this.idealHeight
? this.idealHeight
- extraHeight
: '', overflowY
: '' } );
7040 // If we stopped clipping in at least one of the dimensions
7041 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
7042 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
7045 this.clippedHorizontally
= clipWidth
;
7046 this.clippedVertically
= clipHeight
;
7052 * Element that will stick under a specified container, even when it is inserted elsewhere in the
7053 * document (for example, in a OO.ui.Window's $overlay).
7055 * The elements's position is automatically calculated and maintained when window is resized or the
7056 * page is scrolled. If you reposition the container manually, you have to call #position to make
7057 * sure the element is still placed correctly.
7059 * As positioning is only possible when both the element and the container are attached to the DOM
7060 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
7061 * the #toggle method to display a floating popup, for example.
7067 * @param {Object} [config] Configuration options
7068 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
7069 * @cfg {jQuery} [$floatableContainer] Node to position below
7071 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
7072 // Configuration initialization
7073 config
= config
|| {};
7076 this.$floatable
= null;
7077 this.$floatableContainer
= null;
7078 this.$floatableWindow
= null;
7079 this.$floatableClosestScrollable
= null;
7080 this.onFloatableScrollHandler
= this.position
.bind( this );
7081 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
7084 this.setFloatableContainer( config
.$floatableContainer
);
7085 this.setFloatableElement( config
.$floatable
|| this.$element
);
7091 * Set floatable element.
7093 * If an element is already set, it will be cleaned up before setting up the new element.
7095 * @param {jQuery} $floatable Element to make floatable
7097 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
7098 if ( this.$floatable
) {
7099 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
7100 this.$floatable
.css( { left
: '', top
: '' } );
7103 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
7108 * Set floatable container.
7110 * The element will be always positioned under the specified container.
7112 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
7114 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
7115 this.$floatableContainer
= $floatableContainer
;
7116 if ( this.$floatable
) {
7122 * Toggle positioning.
7124 * Do not turn positioning on until after the element is attached to the DOM and visible.
7126 * @param {boolean} [positioning] Enable positioning, omit to toggle
7129 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
7130 var closestScrollableOfContainer
, closestScrollableOfFloatable
;
7132 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
7134 if ( this.positioning
!== positioning
) {
7135 this.positioning
= positioning
;
7137 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
7138 closestScrollableOfFloatable
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatable
[ 0 ] );
7139 if ( closestScrollableOfContainer
!== closestScrollableOfFloatable
) {
7140 // If the scrollable is the root, we have to listen to scroll events
7141 // on the window because of browser inconsistencies (or do we? someone should verify this)
7142 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
7143 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
7147 if ( positioning
) {
7148 this.$floatableWindow
= $( this.getElementWindow() );
7149 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
7151 if ( closestScrollableOfContainer
!== closestScrollableOfFloatable
) {
7152 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
7153 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
7156 // Initial position after visible
7159 if ( this.$floatableWindow
) {
7160 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
7161 this.$floatableWindow
= null;
7164 if ( this.$floatableClosestScrollable
) {
7165 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
7166 this.$floatableClosestScrollable
= null;
7169 this.$floatable
.css( { left
: '', top
: '' } );
7177 * Position the floatable below its container.
7179 * This should only be done when both of them are attached to the DOM and visible.
7183 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
7186 if ( !this.positioning
) {
7190 pos
= OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, this.$floatable
.offsetParent() );
7192 // Position under container
7193 pos
.top
+= this.$floatableContainer
.height();
7194 this.$floatable
.css( pos
);
7196 // We updated the position, so re-evaluate the clipping state.
7197 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
7198 // will not notice the need to update itself.)
7199 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
7200 // it not listen to the right events in the right places?
7209 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
7210 * Accesskeys allow an user to go to a specific element by using
7211 * a shortcut combination of a browser specific keys + the key
7215 * // AccessKeyedElement provides an 'accesskey' attribute to the
7216 * // ButtonWidget class
7217 * var button = new OO.ui.ButtonWidget( {
7218 * label: 'Button with Accesskey',
7221 * $( 'body' ).append( button.$element );
7227 * @param {Object} [config] Configuration options
7228 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
7229 * If this config is omitted, the accesskey functionality is applied to $element, the
7230 * element created by the class.
7231 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
7232 * this config is omitted, no accesskey will be added.
7234 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
7235 // Configuration initialization
7236 config
= config
|| {};
7239 this.$accessKeyed
= null;
7240 this.accessKey
= null;
7243 this.setAccessKey( config
.accessKey
|| null );
7244 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
7249 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
7251 /* Static Properties */
7254 * The access key, a function that returns a key, or `null` for no accesskey.
7258 * @property {string|Function|null}
7260 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
7265 * Set the accesskeyed element.
7267 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
7268 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
7270 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
7272 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
7273 if ( this.$accessKeyed
) {
7274 this.$accessKeyed
.removeAttr( 'accesskey' );
7277 this.$accessKeyed
= $accessKeyed
;
7278 if ( this.accessKey
) {
7279 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
7286 * @param {string|Function|null} accesskey Key, a function that returns a key, or `null` for no accesskey
7289 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
7290 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
7292 if ( this.accessKey
!== accessKey
) {
7293 if ( this.$accessKeyed
) {
7294 if ( accessKey
!== null ) {
7295 this.$accessKeyed
.attr( 'accesskey', accessKey
);
7297 this.$accessKeyed
.removeAttr( 'accesskey' );
7300 this.accessKey
= accessKey
;
7309 * @return {string} accessKey string
7311 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
7312 return this.accessKey
;
7316 * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
7317 * Each tool is configured with a static name, title, and icon and is customized with the command to carry
7318 * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
7319 * which creates the tools on demand.
7321 * Every Tool subclass must implement two methods:
7323 * - {@link #onUpdateState}
7324 * - {@link #onSelect}
7326 * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
7327 * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
7328 * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
7330 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
7331 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
7335 * @extends OO.ui.Widget
7336 * @mixins OO.ui.mixin.IconElement
7337 * @mixins OO.ui.mixin.FlaggedElement
7338 * @mixins OO.ui.mixin.TabIndexedElement
7341 * @param {OO.ui.ToolGroup} toolGroup
7342 * @param {Object} [config] Configuration options
7343 * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
7344 * the {@link #static-title static title} property is used.
7346 * The title is used in different ways depending on the type of toolgroup that contains the tool. The
7347 * 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
7348 * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
7350 * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
7351 * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
7352 * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
7354 OO
.ui
.Tool
= function OoUiTool( toolGroup
, config
) {
7355 // Allow passing positional parameters inside the config object
7356 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
7358 toolGroup
= config
.toolGroup
;
7361 // Configuration initialization
7362 config
= config
|| {};
7364 // Parent constructor
7365 OO
.ui
.Tool
.parent
.call( this, config
);
7368 this.toolGroup
= toolGroup
;
7369 this.toolbar
= this.toolGroup
.getToolbar();
7370 this.active
= false;
7371 this.$title
= $( '<span>' );
7372 this.$accel
= $( '<span>' );
7373 this.$link
= $( '<a>' );
7376 // Mixin constructors
7377 OO
.ui
.mixin
.IconElement
.call( this, config
);
7378 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
7379 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$link
} ) );
7382 this.toolbar
.connect( this, { updateState
: 'onUpdateState' } );
7385 this.$title
.addClass( 'oo-ui-tool-title' );
7387 .addClass( 'oo-ui-tool-accel' )
7389 // This may need to be changed if the key names are ever localized,
7390 // but for now they are essentially written in English
7395 .addClass( 'oo-ui-tool-link' )
7396 .append( this.$icon
, this.$title
, this.$accel
)
7397 .attr( 'role', 'button' );
7399 .data( 'oo-ui-tool', this )
7401 'oo-ui-tool ' + 'oo-ui-tool-name-' +
7402 this.constructor.static.name
.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
7404 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel
)
7405 .append( this.$link
);
7406 this.setTitle( config
.title
|| this.constructor.static.title
);
7411 OO
.inheritClass( OO
.ui
.Tool
, OO
.ui
.Widget
);
7412 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.mixin
.IconElement
);
7413 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.mixin
.FlaggedElement
);
7414 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.mixin
.TabIndexedElement
);
7416 /* Static Properties */
7422 OO
.ui
.Tool
.static.tagName
= 'span';
7425 * Symbolic name of tool.
7427 * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
7428 * also be used when adding tools to toolgroups.
7433 * @property {string}
7435 OO
.ui
.Tool
.static.name
= '';
7438 * Symbolic name of the group.
7440 * The group name is used to associate tools with each other so that they can be selected later by
7441 * a {@link OO.ui.ToolGroup toolgroup}.
7446 * @property {string}
7448 OO
.ui
.Tool
.static.group
= '';
7451 * 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.
7456 * @property {string|Function}
7458 OO
.ui
.Tool
.static.title
= '';
7461 * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
7462 * Normally only the icon is displayed, or only the label if no icon is given.
7466 * @property {boolean}
7468 OO
.ui
.Tool
.static.displayBothIconAndLabel
= false;
7471 * Add tool to catch-all groups automatically.
7473 * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
7474 * can be included in a toolgroup using the wildcard selector, an asterisk (*).
7478 * @property {boolean}
7480 OO
.ui
.Tool
.static.autoAddToCatchall
= true;
7483 * Add tool to named groups automatically.
7485 * By default, tools that are configured with a static ‘group’ property are added
7486 * to that group and will be selected when the symbolic name of the group is specified (e.g., when
7487 * toolgroups include tools by group name).
7490 * @property {boolean}
7493 OO
.ui
.Tool
.static.autoAddToGroup
= true;
7496 * Check if this tool is compatible with given data.
7498 * This is a stub that can be overridden to provide support for filtering tools based on an
7499 * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
7500 * must also call this method so that the compatibility check can be performed.
7504 * @param {Mixed} data Data to check
7505 * @return {boolean} Tool can be used with data
7507 OO
.ui
.Tool
.static.isCompatibleWith = function () {
7514 * Handle the toolbar state being updated. This method is called when the
7515 * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
7516 * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
7517 * depending on application state (usually by calling #setDisabled to enable or disable the tool,
7518 * or #setActive to mark is as currently in-use or not).
7520 * This is an abstract method that must be overridden in a concrete subclass.
7526 OO
.ui
.Tool
.prototype.onUpdateState
= null;
7529 * Handle the tool being selected. This method is called when the user triggers this tool,
7530 * usually by clicking on its label/icon.
7532 * This is an abstract method that must be overridden in a concrete subclass.
7538 OO
.ui
.Tool
.prototype.onSelect
= null;
7541 * Check if the tool is active.
7543 * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
7544 * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
7546 * @return {boolean} Tool is active
7548 OO
.ui
.Tool
.prototype.isActive = function () {
7553 * Make the tool appear active or inactive.
7555 * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
7556 * appear pressed or not.
7558 * @param {boolean} state Make tool appear active
7560 OO
.ui
.Tool
.prototype.setActive = function ( state
) {
7561 this.active
= !!state
;
7562 if ( this.active
) {
7563 this.$element
.addClass( 'oo-ui-tool-active' );
7565 this.$element
.removeClass( 'oo-ui-tool-active' );
7570 * Set the tool #title.
7572 * @param {string|Function} title Title text or a function that returns text
7575 OO
.ui
.Tool
.prototype.setTitle = function ( title
) {
7576 this.title
= OO
.ui
.resolveMsg( title
);
7582 * Get the tool #title.
7584 * @return {string} Title text
7586 OO
.ui
.Tool
.prototype.getTitle = function () {
7591 * Get the tool's symbolic name.
7593 * @return {string} Symbolic name of tool
7595 OO
.ui
.Tool
.prototype.getName = function () {
7596 return this.constructor.static.name
;
7602 OO
.ui
.Tool
.prototype.updateTitle = function () {
7603 var titleTooltips
= this.toolGroup
.constructor.static.titleTooltips
,
7604 accelTooltips
= this.toolGroup
.constructor.static.accelTooltips
,
7605 accel
= this.toolbar
.getToolAccelerator( this.constructor.static.name
),
7608 this.$title
.text( this.title
);
7609 this.$accel
.text( accel
);
7611 if ( titleTooltips
&& typeof this.title
=== 'string' && this.title
.length
) {
7612 tooltipParts
.push( this.title
);
7614 if ( accelTooltips
&& typeof accel
=== 'string' && accel
.length
) {
7615 tooltipParts
.push( accel
);
7617 if ( tooltipParts
.length
) {
7618 this.$link
.attr( 'title', tooltipParts
.join( ' ' ) );
7620 this.$link
.removeAttr( 'title' );
7627 * Destroying the tool removes all event handlers and the tool’s DOM elements.
7628 * Call this method whenever you are done using a tool.
7630 OO
.ui
.Tool
.prototype.destroy = function () {
7631 this.toolbar
.disconnect( this );
7632 this.$element
.remove();
7636 * Toolbars are complex interface components that permit users to easily access a variety
7637 * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
7638 * part of the toolbar, but not configured as tools.
7640 * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
7641 * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
7642 * image’), and an icon.
7644 * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
7645 * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
7646 * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
7647 * any order, but each can only appear once in the toolbar.
7649 * The toolbar can be synchronized with the state of the external "application", like a text
7650 * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
7651 * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
7652 * tool would be disabled while the user is not editing a table). A state change is signalled by
7653 * emitting the {@link #event-updateState 'updateState' event}, which calls Tools'
7654 * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
7656 * The following is an example of a basic toolbar.
7659 * // Example of a toolbar
7660 * // Create the toolbar
7661 * var toolFactory = new OO.ui.ToolFactory();
7662 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
7663 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
7665 * // We will be placing status text in this element when tools are used
7666 * var $area = $( '<p>' ).text( 'Toolbar example' );
7668 * // Define the tools that we're going to place in our toolbar
7670 * // Create a class inheriting from OO.ui.Tool
7671 * function SearchTool() {
7672 * SearchTool.parent.apply( this, arguments );
7674 * OO.inheritClass( SearchTool, OO.ui.Tool );
7675 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
7676 * // of 'icon' and 'title' (displayed icon and text).
7677 * SearchTool.static.name = 'search';
7678 * SearchTool.static.icon = 'search';
7679 * SearchTool.static.title = 'Search...';
7680 * // Defines the action that will happen when this tool is selected (clicked).
7681 * SearchTool.prototype.onSelect = function () {
7682 * $area.text( 'Search tool clicked!' );
7683 * // Never display this tool as "active" (selected).
7684 * this.setActive( false );
7686 * SearchTool.prototype.onUpdateState = function () {};
7687 * // Make this tool available in our toolFactory and thus our toolbar
7688 * toolFactory.register( SearchTool );
7690 * // Register two more tools, nothing interesting here
7691 * function SettingsTool() {
7692 * SettingsTool.parent.apply( this, arguments );
7694 * OO.inheritClass( SettingsTool, OO.ui.Tool );
7695 * SettingsTool.static.name = 'settings';
7696 * SettingsTool.static.icon = 'settings';
7697 * SettingsTool.static.title = 'Change settings';
7698 * SettingsTool.prototype.onSelect = function () {
7699 * $area.text( 'Settings tool clicked!' );
7700 * this.setActive( false );
7702 * SettingsTool.prototype.onUpdateState = function () {};
7703 * toolFactory.register( SettingsTool );
7705 * // Register two more tools, nothing interesting here
7706 * function StuffTool() {
7707 * StuffTool.parent.apply( this, arguments );
7709 * OO.inheritClass( StuffTool, OO.ui.Tool );
7710 * StuffTool.static.name = 'stuff';
7711 * StuffTool.static.icon = 'ellipsis';
7712 * StuffTool.static.title = 'More stuff';
7713 * StuffTool.prototype.onSelect = function () {
7714 * $area.text( 'More stuff tool clicked!' );
7715 * this.setActive( false );
7717 * StuffTool.prototype.onUpdateState = function () {};
7718 * toolFactory.register( StuffTool );
7720 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
7721 * // little popup window (a PopupWidget).
7722 * function HelpTool( toolGroup, config ) {
7723 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
7728 * this.popup.$body.append( '<p>I am helpful!</p>' );
7730 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
7731 * HelpTool.static.name = 'help';
7732 * HelpTool.static.icon = 'help';
7733 * HelpTool.static.title = 'Help';
7734 * toolFactory.register( HelpTool );
7736 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7737 * // used once (but not all defined tools must be used).
7740 * // 'bar' tool groups display tools' icons only, side-by-side.
7742 * include: [ 'search', 'help' ]
7745 * // 'list' tool groups display both the titles and icons, in a dropdown list.
7747 * indicator: 'down',
7749 * include: [ 'settings', 'stuff' ]
7751 * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
7752 * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
7753 * // since it's more complicated to use. (See the next example snippet on this page.)
7756 * // Create some UI around the toolbar and place it in the document
7757 * var frame = new OO.ui.PanelLayout( {
7761 * var contentFrame = new OO.ui.PanelLayout( {
7765 * frame.$element.append(
7767 * contentFrame.$element.append( $area )
7769 * $( 'body' ).append( frame.$element );
7771 * // Here is where the toolbar is actually built. This must be done after inserting it into the
7773 * toolbar.initialize();
7774 * toolbar.emit( 'updateState' );
7776 * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
7777 * {@link #event-updateState 'updateState' event}.
7780 * // Create the toolbar
7781 * var toolFactory = new OO.ui.ToolFactory();
7782 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
7783 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
7785 * // We will be placing status text in this element when tools are used
7786 * var $area = $( '<p>' ).text( 'Toolbar example' );
7788 * // Define the tools that we're going to place in our toolbar
7790 * // Create a class inheriting from OO.ui.Tool
7791 * function SearchTool() {
7792 * SearchTool.parent.apply( this, arguments );
7794 * OO.inheritClass( SearchTool, OO.ui.Tool );
7795 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
7796 * // of 'icon' and 'title' (displayed icon and text).
7797 * SearchTool.static.name = 'search';
7798 * SearchTool.static.icon = 'search';
7799 * SearchTool.static.title = 'Search...';
7800 * // Defines the action that will happen when this tool is selected (clicked).
7801 * SearchTool.prototype.onSelect = function () {
7802 * $area.text( 'Search tool clicked!' );
7803 * // Never display this tool as "active" (selected).
7804 * this.setActive( false );
7806 * SearchTool.prototype.onUpdateState = function () {};
7807 * // Make this tool available in our toolFactory and thus our toolbar
7808 * toolFactory.register( SearchTool );
7810 * // Register two more tools, nothing interesting here
7811 * function SettingsTool() {
7812 * SettingsTool.parent.apply( this, arguments );
7813 * this.reallyActive = false;
7815 * OO.inheritClass( SettingsTool, OO.ui.Tool );
7816 * SettingsTool.static.name = 'settings';
7817 * SettingsTool.static.icon = 'settings';
7818 * SettingsTool.static.title = 'Change settings';
7819 * SettingsTool.prototype.onSelect = function () {
7820 * $area.text( 'Settings tool clicked!' );
7821 * // Toggle the active state on each click
7822 * this.reallyActive = !this.reallyActive;
7823 * this.setActive( this.reallyActive );
7824 * // To update the menu label
7825 * this.toolbar.emit( 'updateState' );
7827 * SettingsTool.prototype.onUpdateState = function () {};
7828 * toolFactory.register( SettingsTool );
7830 * // Register two more tools, nothing interesting here
7831 * function StuffTool() {
7832 * StuffTool.parent.apply( this, arguments );
7833 * this.reallyActive = false;
7835 * OO.inheritClass( StuffTool, OO.ui.Tool );
7836 * StuffTool.static.name = 'stuff';
7837 * StuffTool.static.icon = 'ellipsis';
7838 * StuffTool.static.title = 'More stuff';
7839 * StuffTool.prototype.onSelect = function () {
7840 * $area.text( 'More stuff tool clicked!' );
7841 * // Toggle the active state on each click
7842 * this.reallyActive = !this.reallyActive;
7843 * this.setActive( this.reallyActive );
7844 * // To update the menu label
7845 * this.toolbar.emit( 'updateState' );
7847 * StuffTool.prototype.onUpdateState = function () {};
7848 * toolFactory.register( StuffTool );
7850 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
7851 * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
7852 * function HelpTool( toolGroup, config ) {
7853 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
7858 * this.popup.$body.append( '<p>I am helpful!</p>' );
7860 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
7861 * HelpTool.static.name = 'help';
7862 * HelpTool.static.icon = 'help';
7863 * HelpTool.static.title = 'Help';
7864 * toolFactory.register( HelpTool );
7866 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7867 * // used once (but not all defined tools must be used).
7870 * // 'bar' tool groups display tools' icons only, side-by-side.
7872 * include: [ 'search', 'help' ]
7875 * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
7876 * // Menu label indicates which items are selected.
7878 * indicator: 'down',
7879 * include: [ 'settings', 'stuff' ]
7883 * // Create some UI around the toolbar and place it in the document
7884 * var frame = new OO.ui.PanelLayout( {
7888 * var contentFrame = new OO.ui.PanelLayout( {
7892 * frame.$element.append(
7894 * contentFrame.$element.append( $area )
7896 * $( 'body' ).append( frame.$element );
7898 * // Here is where the toolbar is actually built. This must be done after inserting it into the
7900 * toolbar.initialize();
7901 * toolbar.emit( 'updateState' );
7904 * @extends OO.ui.Element
7905 * @mixins OO.EventEmitter
7906 * @mixins OO.ui.mixin.GroupElement
7909 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
7910 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
7911 * @param {Object} [config] Configuration options
7912 * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
7913 * in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
7915 * @cfg {boolean} [shadow] Add a shadow below the toolbar.
7917 OO
.ui
.Toolbar
= function OoUiToolbar( toolFactory
, toolGroupFactory
, config
) {
7918 // Allow passing positional parameters inside the config object
7919 if ( OO
.isPlainObject( toolFactory
) && config
=== undefined ) {
7920 config
= toolFactory
;
7921 toolFactory
= config
.toolFactory
;
7922 toolGroupFactory
= config
.toolGroupFactory
;
7925 // Configuration initialization
7926 config
= config
|| {};
7928 // Parent constructor
7929 OO
.ui
.Toolbar
.parent
.call( this, config
);
7931 // Mixin constructors
7932 OO
.EventEmitter
.call( this );
7933 OO
.ui
.mixin
.GroupElement
.call( this, config
);
7936 this.toolFactory
= toolFactory
;
7937 this.toolGroupFactory
= toolGroupFactory
;
7940 this.$bar
= $( '<div>' );
7941 this.$actions
= $( '<div>' );
7942 this.initialized
= false;
7943 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
7947 .add( this.$bar
).add( this.$group
).add( this.$actions
)
7948 .on( 'mousedown keydown', this.onPointerDown
.bind( this ) );
7951 this.$group
.addClass( 'oo-ui-toolbar-tools' );
7952 if ( config
.actions
) {
7953 this.$bar
.append( this.$actions
.addClass( 'oo-ui-toolbar-actions' ) );
7956 .addClass( 'oo-ui-toolbar-bar' )
7957 .append( this.$group
, '<div style="clear:both"></div>' );
7958 if ( config
.shadow
) {
7959 this.$bar
.append( '<div class="oo-ui-toolbar-shadow"></div>' );
7961 this.$element
.addClass( 'oo-ui-toolbar' ).append( this.$bar
);
7966 OO
.inheritClass( OO
.ui
.Toolbar
, OO
.ui
.Element
);
7967 OO
.mixinClass( OO
.ui
.Toolbar
, OO
.EventEmitter
);
7968 OO
.mixinClass( OO
.ui
.Toolbar
, OO
.ui
.mixin
.GroupElement
);
7973 * @event updateState
7975 * An 'updateState' event must be emitted on the Toolbar (by calling `toolbar.emit( 'updateState' )`)
7976 * every time the state of the application using the toolbar changes, and an update to the state of
7977 * tools is required.
7979 * @param {Mixed...} data Application-defined parameters
7985 * Get the tool factory.
7987 * @return {OO.ui.ToolFactory} Tool factory
7989 OO
.ui
.Toolbar
.prototype.getToolFactory = function () {
7990 return this.toolFactory
;
7994 * Get the toolgroup factory.
7996 * @return {OO.Factory} Toolgroup factory
7998 OO
.ui
.Toolbar
.prototype.getToolGroupFactory = function () {
7999 return this.toolGroupFactory
;
8003 * Handles mouse down events.
8006 * @param {jQuery.Event} e Mouse down event
8008 OO
.ui
.Toolbar
.prototype.onPointerDown = function ( e
) {
8009 var $closestWidgetToEvent
= $( e
.target
).closest( '.oo-ui-widget' ),
8010 $closestWidgetToToolbar
= this.$element
.closest( '.oo-ui-widget' );
8011 if ( !$closestWidgetToEvent
.length
|| $closestWidgetToEvent
[ 0 ] === $closestWidgetToToolbar
[ 0 ] ) {
8017 * Handle window resize event.
8020 * @param {jQuery.Event} e Window resize event
8022 OO
.ui
.Toolbar
.prototype.onWindowResize = function () {
8023 this.$element
.toggleClass(
8024 'oo-ui-toolbar-narrow',
8025 this.$bar
.width() <= this.narrowThreshold
8030 * Sets up handles and preloads required information for the toolbar to work.
8031 * This must be called after it is attached to a visible document and before doing anything else.
8033 OO
.ui
.Toolbar
.prototype.initialize = function () {
8034 if ( !this.initialized
) {
8035 this.initialized
= true;
8036 this.narrowThreshold
= this.$group
.width() + this.$actions
.width();
8037 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler
);
8038 this.onWindowResize();
8043 * Set up the toolbar.
8045 * The toolbar is set up with a list of toolgroup configurations that specify the type of
8046 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
8047 * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
8048 * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
8050 * @param {Object.<string,Array>} groups List of toolgroup configurations
8051 * @param {Array|string} [groups.include] Tools to include in the toolgroup
8052 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
8053 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
8054 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
8056 OO
.ui
.Toolbar
.prototype.setup = function ( groups
) {
8057 var i
, len
, type
, group
,
8059 defaultType
= 'bar';
8061 // Cleanup previous groups
8064 // Build out new groups
8065 for ( i
= 0, len
= groups
.length
; i
< len
; i
++ ) {
8066 group
= groups
[ i
];
8067 if ( group
.include
=== '*' ) {
8068 // Apply defaults to catch-all groups
8069 if ( group
.type
=== undefined ) {
8070 group
.type
= 'list';
8072 if ( group
.label
=== undefined ) {
8073 group
.label
= OO
.ui
.msg( 'ooui-toolbar-more' );
8076 // Check type has been registered
8077 type
= this.getToolGroupFactory().lookup( group
.type
) ? group
.type
: defaultType
;
8079 this.getToolGroupFactory().create( type
, this, group
)
8082 this.addItems( items
);
8086 * Remove all tools and toolgroups from the toolbar.
8088 OO
.ui
.Toolbar
.prototype.reset = function () {
8093 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
8094 this.items
[ i
].destroy();
8100 * Destroy the toolbar.
8102 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
8103 * this method whenever you are done using a toolbar.
8105 OO
.ui
.Toolbar
.prototype.destroy = function () {
8106 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler
);
8108 this.$element
.remove();
8112 * Check if the tool is available.
8114 * Available tools are ones that have not yet been added to the toolbar.
8116 * @param {string} name Symbolic name of tool
8117 * @return {boolean} Tool is available
8119 OO
.ui
.Toolbar
.prototype.isToolAvailable = function ( name
) {
8120 return !this.tools
[ name
];
8124 * Prevent tool from being used again.
8126 * @param {OO.ui.Tool} tool Tool to reserve
8128 OO
.ui
.Toolbar
.prototype.reserveTool = function ( tool
) {
8129 this.tools
[ tool
.getName() ] = tool
;
8133 * Allow tool to be used again.
8135 * @param {OO.ui.Tool} tool Tool to release
8137 OO
.ui
.Toolbar
.prototype.releaseTool = function ( tool
) {
8138 delete this.tools
[ tool
.getName() ];
8142 * Get accelerator label for tool.
8144 * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
8145 * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
8146 * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
8148 * @param {string} name Symbolic name of tool
8149 * @return {string|undefined} Tool accelerator label if available
8151 OO
.ui
.Toolbar
.prototype.getToolAccelerator = function () {
8156 * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
8157 * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
8158 * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
8159 * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
8161 * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
8162 * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
8163 * The options `exclude`, `promote`, and `demote` support the same formats.
8165 * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
8166 * please see the [OOjs UI documentation on MediaWiki][1].
8168 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
8172 * @extends OO.ui.Widget
8173 * @mixins OO.ui.mixin.GroupElement
8176 * @param {OO.ui.Toolbar} toolbar
8177 * @param {Object} [config] Configuration options
8178 * @cfg {Array|string} [include] List of tools to include in the toolgroup, see above.
8179 * @cfg {Array|string} [exclude] List of tools to exclude from the toolgroup, see above.
8180 * @cfg {Array|string} [promote] List of tools to promote to the beginning of the toolgroup, see above.
8181 * @cfg {Array|string} [demote] List of tools to demote to the end of the toolgroup, see above.
8182 * This setting is particularly useful when tools have been added to the toolgroup
8183 * en masse (e.g., via the catch-all selector).
8185 OO
.ui
.ToolGroup
= function OoUiToolGroup( toolbar
, config
) {
8186 // Allow passing positional parameters inside the config object
8187 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
8189 toolbar
= config
.toolbar
;
8192 // Configuration initialization
8193 config
= config
|| {};
8195 // Parent constructor
8196 OO
.ui
.ToolGroup
.parent
.call( this, config
);
8198 // Mixin constructors
8199 OO
.ui
.mixin
.GroupElement
.call( this, config
);
8202 this.toolbar
= toolbar
;
8204 this.pressed
= null;
8205 this.autoDisabled
= false;
8206 this.include
= config
.include
|| [];
8207 this.exclude
= config
.exclude
|| [];
8208 this.promote
= config
.promote
|| [];
8209 this.demote
= config
.demote
|| [];
8210 this.onCapturedMouseKeyUpHandler
= this.onCapturedMouseKeyUp
.bind( this );
8214 mousedown
: this.onMouseKeyDown
.bind( this ),
8215 mouseup
: this.onMouseKeyUp
.bind( this ),
8216 keydown
: this.onMouseKeyDown
.bind( this ),
8217 keyup
: this.onMouseKeyUp
.bind( this ),
8218 focus
: this.onMouseOverFocus
.bind( this ),
8219 blur
: this.onMouseOutBlur
.bind( this ),
8220 mouseover
: this.onMouseOverFocus
.bind( this ),
8221 mouseout
: this.onMouseOutBlur
.bind( this )
8223 this.toolbar
.getToolFactory().connect( this, { register
: 'onToolFactoryRegister' } );
8224 this.aggregate( { disable
: 'itemDisable' } );
8225 this.connect( this, { itemDisable
: 'updateDisabled' } );
8228 this.$group
.addClass( 'oo-ui-toolGroup-tools' );
8230 .addClass( 'oo-ui-toolGroup' )
8231 .append( this.$group
);
8237 OO
.inheritClass( OO
.ui
.ToolGroup
, OO
.ui
.Widget
);
8238 OO
.mixinClass( OO
.ui
.ToolGroup
, OO
.ui
.mixin
.GroupElement
);
8246 /* Static Properties */
8249 * Show labels in tooltips.
8253 * @property {boolean}
8255 OO
.ui
.ToolGroup
.static.titleTooltips
= false;
8258 * Show acceleration labels in tooltips.
8260 * Note: The OOjs UI library does not include an accelerator system, but does contain
8261 * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
8262 * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
8263 * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
8267 * @property {boolean}
8269 OO
.ui
.ToolGroup
.static.accelTooltips
= false;
8272 * Automatically disable the toolgroup when all tools are disabled
8276 * @property {boolean}
8278 OO
.ui
.ToolGroup
.static.autoDisable
= true;
8285 OO
.ui
.ToolGroup
.prototype.isDisabled = function () {
8286 return this.autoDisabled
|| OO
.ui
.ToolGroup
.parent
.prototype.isDisabled
.apply( this, arguments
);
8292 OO
.ui
.ToolGroup
.prototype.updateDisabled = function () {
8293 var i
, item
, allDisabled
= true;
8295 if ( this.constructor.static.autoDisable
) {
8296 for ( i
= this.items
.length
- 1; i
>= 0; i
-- ) {
8297 item
= this.items
[ i
];
8298 if ( !item
.isDisabled() ) {
8299 allDisabled
= false;
8303 this.autoDisabled
= allDisabled
;
8305 OO
.ui
.ToolGroup
.parent
.prototype.updateDisabled
.apply( this, arguments
);
8309 * Handle mouse down and key down events.
8312 * @param {jQuery.Event} e Mouse down or key down event
8314 OO
.ui
.ToolGroup
.prototype.onMouseKeyDown = function ( e
) {
8316 !this.isDisabled() &&
8317 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
8319 this.pressed
= this.getTargetTool( e
);
8320 if ( this.pressed
) {
8321 this.pressed
.setActive( true );
8322 this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler
, true );
8323 this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler
, true );
8330 * Handle captured mouse up and key up events.
8333 * @param {Event} e Mouse up or key up event
8335 OO
.ui
.ToolGroup
.prototype.onCapturedMouseKeyUp = function ( e
) {
8336 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler
, true );
8337 this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler
, true );
8338 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
8339 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
8340 this.onMouseKeyUp( e
);
8344 * Handle mouse up and key up events.
8347 * @param {jQuery.Event} e Mouse up or key up event
8349 OO
.ui
.ToolGroup
.prototype.onMouseKeyUp = function ( e
) {
8350 var tool
= this.getTargetTool( e
);
8353 !this.isDisabled() && this.pressed
&& this.pressed
=== tool
&&
8354 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
8356 this.pressed
.onSelect();
8357 this.pressed
= null;
8361 this.pressed
= null;
8365 * Handle mouse over and focus events.
8368 * @param {jQuery.Event} e Mouse over or focus event
8370 OO
.ui
.ToolGroup
.prototype.onMouseOverFocus = function ( e
) {
8371 var tool
= this.getTargetTool( e
);
8373 if ( this.pressed
&& this.pressed
=== tool
) {
8374 this.pressed
.setActive( true );
8379 * Handle mouse out and blur events.
8382 * @param {jQuery.Event} e Mouse out or blur event
8384 OO
.ui
.ToolGroup
.prototype.onMouseOutBlur = function ( e
) {
8385 var tool
= this.getTargetTool( e
);
8387 if ( this.pressed
&& this.pressed
=== tool
) {
8388 this.pressed
.setActive( false );
8393 * Get the closest tool to a jQuery.Event.
8395 * Only tool links are considered, which prevents other elements in the tool such as popups from
8396 * triggering tool group interactions.
8399 * @param {jQuery.Event} e
8400 * @return {OO.ui.Tool|null} Tool, `null` if none was found
8402 OO
.ui
.ToolGroup
.prototype.getTargetTool = function ( e
) {
8404 $item
= $( e
.target
).closest( '.oo-ui-tool-link' );
8406 if ( $item
.length
) {
8407 tool
= $item
.parent().data( 'oo-ui-tool' );
8410 return tool
&& !tool
.isDisabled() ? tool
: null;
8414 * Handle tool registry register events.
8416 * If a tool is registered after the group is created, we must repopulate the list to account for:
8418 * - a tool being added that may be included
8419 * - a tool already included being overridden
8422 * @param {string} name Symbolic name of tool
8424 OO
.ui
.ToolGroup
.prototype.onToolFactoryRegister = function () {
8429 * Get the toolbar that contains the toolgroup.
8431 * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
8433 OO
.ui
.ToolGroup
.prototype.getToolbar = function () {
8434 return this.toolbar
;
8438 * Add and remove tools based on configuration.
8440 OO
.ui
.ToolGroup
.prototype.populate = function () {
8441 var i
, len
, name
, tool
,
8442 toolFactory
= this.toolbar
.getToolFactory(),
8446 list
= this.toolbar
.getToolFactory().getTools(
8447 this.include
, this.exclude
, this.promote
, this.demote
8450 // Build a list of needed tools
8451 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
8455 toolFactory
.lookup( name
) &&
8456 // Tool is available or is already in this group
8457 ( this.toolbar
.isToolAvailable( name
) || this.tools
[ name
] )
8459 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
8460 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
8461 this.toolbar
.tools
[ name
] = true;
8462 tool
= this.tools
[ name
];
8464 // Auto-initialize tools on first use
8465 this.tools
[ name
] = tool
= toolFactory
.create( name
, this );
8468 this.toolbar
.reserveTool( tool
);
8470 names
[ name
] = true;
8473 // Remove tools that are no longer needed
8474 for ( name
in this.tools
) {
8475 if ( !names
[ name
] ) {
8476 this.tools
[ name
].destroy();
8477 this.toolbar
.releaseTool( this.tools
[ name
] );
8478 remove
.push( this.tools
[ name
] );
8479 delete this.tools
[ name
];
8482 if ( remove
.length
) {
8483 this.removeItems( remove
);
8485 // Update emptiness state
8487 this.$element
.removeClass( 'oo-ui-toolGroup-empty' );
8489 this.$element
.addClass( 'oo-ui-toolGroup-empty' );
8491 // Re-add tools (moving existing ones to new locations)
8492 this.addItems( add
);
8493 // Disabled state may depend on items
8494 this.updateDisabled();
8498 * Destroy toolgroup.
8500 OO
.ui
.ToolGroup
.prototype.destroy = function () {
8504 this.toolbar
.getToolFactory().disconnect( this );
8505 for ( name
in this.tools
) {
8506 this.toolbar
.releaseTool( this.tools
[ name
] );
8507 this.tools
[ name
].disconnect( this ).destroy();
8508 delete this.tools
[ name
];
8510 this.$element
.remove();
8514 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
8515 * consists of a header that contains the dialog title, a body with the message, and a footer that
8516 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
8517 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
8519 * There are two basic types of message dialogs, confirmation and alert:
8521 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
8522 * more details about the consequences.
8523 * - **alert**: the dialog title describes which event occurred and the message provides more information
8524 * about why the event occurred.
8526 * The MessageDialog class specifies two actions: ‘accept’, the primary
8527 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
8528 * passing along the selected action.
8530 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
8533 * // Example: Creating and opening a message dialog window.
8534 * var messageDialog = new OO.ui.MessageDialog();
8536 * // Create and append a window manager.
8537 * var windowManager = new OO.ui.WindowManager();
8538 * $( 'body' ).append( windowManager.$element );
8539 * windowManager.addWindows( [ messageDialog ] );
8540 * // Open the window.
8541 * windowManager.openWindow( messageDialog, {
8542 * title: 'Basic message dialog',
8543 * message: 'This is the message'
8546 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
8549 * @extends OO.ui.Dialog
8552 * @param {Object} [config] Configuration options
8554 OO
.ui
.MessageDialog
= function OoUiMessageDialog( config
) {
8555 // Parent constructor
8556 OO
.ui
.MessageDialog
.parent
.call( this, config
);
8559 this.verticalActionLayout
= null;
8562 this.$element
.addClass( 'oo-ui-messageDialog' );
8567 OO
.inheritClass( OO
.ui
.MessageDialog
, OO
.ui
.Dialog
);
8569 /* Static Properties */
8571 OO
.ui
.MessageDialog
.static.name
= 'message';
8573 OO
.ui
.MessageDialog
.static.size
= 'small';
8575 OO
.ui
.MessageDialog
.static.verbose
= false;
8580 * The title of a confirmation dialog describes what a progressive action will do. The
8581 * title of an alert dialog describes which event occurred.
8585 * @property {jQuery|string|Function|null}
8587 OO
.ui
.MessageDialog
.static.title
= null;
8590 * The message displayed in the dialog body.
8592 * A confirmation message describes the consequences of a progressive action. An alert
8593 * message describes why an event occurred.
8597 * @property {jQuery|string|Function|null}
8599 OO
.ui
.MessageDialog
.static.message
= null;
8601 // Note that OO.ui.alert() and OO.ui.confirm() rely on these.
8602 OO
.ui
.MessageDialog
.static.actions
= [
8603 { action
: 'accept', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-accept' ), flags
: 'primary' },
8604 { action
: 'reject', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-reject' ), flags
: 'safe' }
8612 OO
.ui
.MessageDialog
.prototype.setManager = function ( manager
) {
8613 OO
.ui
.MessageDialog
.parent
.prototype.setManager
.call( this, manager
);
8616 this.manager
.connect( this, {
8626 OO
.ui
.MessageDialog
.prototype.onActionResize = function ( action
) {
8628 return OO
.ui
.MessageDialog
.parent
.prototype.onActionResize
.call( this, action
);
8632 * Handle window resized events.
8636 OO
.ui
.MessageDialog
.prototype.onResize = function () {
8638 dialog
.fitActions();
8639 // Wait for CSS transition to finish and do it again :(
8640 setTimeout( function () {
8641 dialog
.fitActions();
8646 * Toggle action layout between vertical and horizontal.
8649 * @param {boolean} [value] Layout actions vertically, omit to toggle
8652 OO
.ui
.MessageDialog
.prototype.toggleVerticalActionLayout = function ( value
) {
8653 value
= value
=== undefined ? !this.verticalActionLayout
: !!value
;
8655 if ( value
!== this.verticalActionLayout
) {
8656 this.verticalActionLayout
= value
;
8658 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value
)
8659 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value
);
8668 OO
.ui
.MessageDialog
.prototype.getActionProcess = function ( action
) {
8670 return new OO
.ui
.Process( function () {
8671 this.close( { action
: action
} );
8674 return OO
.ui
.MessageDialog
.parent
.prototype.getActionProcess
.call( this, action
);
8680 * @param {Object} [data] Dialog opening data
8681 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
8682 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
8683 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
8684 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
8687 OO
.ui
.MessageDialog
.prototype.getSetupProcess = function ( data
) {
8691 return OO
.ui
.MessageDialog
.parent
.prototype.getSetupProcess
.call( this, data
)
8692 .next( function () {
8693 this.title
.setLabel(
8694 data
.title
!== undefined ? data
.title
: this.constructor.static.title
8696 this.message
.setLabel(
8697 data
.message
!== undefined ? data
.message
: this.constructor.static.message
8699 this.message
.$element
.toggleClass(
8700 'oo-ui-messageDialog-message-verbose',
8701 data
.verbose
!== undefined ? data
.verbose
: this.constructor.static.verbose
8709 OO
.ui
.MessageDialog
.prototype.getReadyProcess = function ( data
) {
8713 return OO
.ui
.MessageDialog
.parent
.prototype.getReadyProcess
.call( this, data
)
8714 .next( function () {
8715 // Focus the primary action button
8716 var actions
= this.actions
.get();
8717 actions
= actions
.filter( function ( action
) {
8718 return action
.getFlags().indexOf( 'primary' ) > -1;
8720 if ( actions
.length
> 0 ) {
8721 actions
[ 0 ].$button
.focus();
8729 OO
.ui
.MessageDialog
.prototype.getBodyHeight = function () {
8730 var bodyHeight
, oldOverflow
,
8731 $scrollable
= this.container
.$element
;
8733 oldOverflow
= $scrollable
[ 0 ].style
.overflow
;
8734 $scrollable
[ 0 ].style
.overflow
= 'hidden';
8736 OO
.ui
.Element
.static.reconsiderScrollbars( $scrollable
[ 0 ] );
8738 bodyHeight
= this.text
.$element
.outerHeight( true );
8739 $scrollable
[ 0 ].style
.overflow
= oldOverflow
;
8747 OO
.ui
.MessageDialog
.prototype.setDimensions = function ( dim
) {
8748 var $scrollable
= this.container
.$element
;
8749 OO
.ui
.MessageDialog
.parent
.prototype.setDimensions
.call( this, dim
);
8751 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
8752 // Need to do it after transition completes (250ms), add 50ms just in case.
8753 setTimeout( function () {
8754 var oldOverflow
= $scrollable
[ 0 ].style
.overflow
;
8755 $scrollable
[ 0 ].style
.overflow
= 'hidden';
8757 OO
.ui
.Element
.static.reconsiderScrollbars( $scrollable
[ 0 ] );
8759 $scrollable
[ 0 ].style
.overflow
= oldOverflow
;
8768 OO
.ui
.MessageDialog
.prototype.initialize = function () {
8770 OO
.ui
.MessageDialog
.parent
.prototype.initialize
.call( this );
8773 this.$actions
= $( '<div>' );
8774 this.container
= new OO
.ui
.PanelLayout( {
8775 scrollable
: true, classes
: [ 'oo-ui-messageDialog-container' ]
8777 this.text
= new OO
.ui
.PanelLayout( {
8778 padded
: true, expanded
: false, classes
: [ 'oo-ui-messageDialog-text' ]
8780 this.message
= new OO
.ui
.LabelWidget( {
8781 classes
: [ 'oo-ui-messageDialog-message' ]
8785 this.title
.$element
.addClass( 'oo-ui-messageDialog-title' );
8786 this.$content
.addClass( 'oo-ui-messageDialog-content' );
8787 this.container
.$element
.append( this.text
.$element
);
8788 this.text
.$element
.append( this.title
.$element
, this.message
.$element
);
8789 this.$body
.append( this.container
.$element
);
8790 this.$actions
.addClass( 'oo-ui-messageDialog-actions' );
8791 this.$foot
.append( this.$actions
);
8797 OO
.ui
.MessageDialog
.prototype.attachActions = function () {
8798 var i
, len
, other
, special
, others
;
8801 OO
.ui
.MessageDialog
.parent
.prototype.attachActions
.call( this );
8803 special
= this.actions
.getSpecial();
8804 others
= this.actions
.getOthers();
8806 if ( special
.safe
) {
8807 this.$actions
.append( special
.safe
.$element
);
8808 special
.safe
.toggleFramed( false );
8810 if ( others
.length
) {
8811 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
8812 other
= others
[ i
];
8813 this.$actions
.append( other
.$element
);
8814 other
.toggleFramed( false );
8817 if ( special
.primary
) {
8818 this.$actions
.append( special
.primary
.$element
);
8819 special
.primary
.toggleFramed( false );
8822 if ( !this.isOpening() ) {
8823 // If the dialog is currently opening, this will be called automatically soon.
8824 // This also calls #fitActions.
8830 * Fit action actions into columns or rows.
8832 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
8836 OO
.ui
.MessageDialog
.prototype.fitActions = function () {
8838 previous
= this.verticalActionLayout
,
8839 actions
= this.actions
.get();
8842 this.toggleVerticalActionLayout( false );
8843 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
8844 action
= actions
[ i
];
8845 if ( action
.$element
.innerWidth() < action
.$label
.outerWidth( true ) ) {
8846 this.toggleVerticalActionLayout( true );
8851 // Move the body out of the way of the foot
8852 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
8854 if ( this.verticalActionLayout
!== previous
) {
8855 // We changed the layout, window height might need to be updated.
8861 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
8862 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
8863 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
8864 * relevant. The ProcessDialog class is always extended and customized with the actions and content
8865 * required for each process.
8867 * The process dialog box consists of a header that visually represents the ‘working’ state of long
8868 * processes with an animation. The header contains the dialog title as well as
8869 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
8870 * a ‘primary’ action on the right (e.g., ‘Done’).
8872 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
8873 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
8876 * // Example: Creating and opening a process dialog window.
8877 * function MyProcessDialog( config ) {
8878 * MyProcessDialog.parent.call( this, config );
8880 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
8882 * MyProcessDialog.static.title = 'Process dialog';
8883 * MyProcessDialog.static.actions = [
8884 * { action: 'save', label: 'Done', flags: 'primary' },
8885 * { label: 'Cancel', flags: 'safe' }
8888 * MyProcessDialog.prototype.initialize = function () {
8889 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
8890 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
8891 * 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>' );
8892 * this.$body.append( this.content.$element );
8894 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
8895 * var dialog = this;
8897 * return new OO.ui.Process( function () {
8898 * dialog.close( { action: action } );
8901 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
8904 * var windowManager = new OO.ui.WindowManager();
8905 * $( 'body' ).append( windowManager.$element );
8907 * var dialog = new MyProcessDialog();
8908 * windowManager.addWindows( [ dialog ] );
8909 * windowManager.openWindow( dialog );
8911 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
8915 * @extends OO.ui.Dialog
8918 * @param {Object} [config] Configuration options
8920 OO
.ui
.ProcessDialog
= function OoUiProcessDialog( config
) {
8921 // Parent constructor
8922 OO
.ui
.ProcessDialog
.parent
.call( this, config
);
8925 this.fitOnOpen
= false;
8928 this.$element
.addClass( 'oo-ui-processDialog' );
8933 OO
.inheritClass( OO
.ui
.ProcessDialog
, OO
.ui
.Dialog
);
8938 * Handle dismiss button click events.
8944 OO
.ui
.ProcessDialog
.prototype.onDismissErrorButtonClick = function () {
8949 * Handle retry button click events.
8951 * Hides errors and then tries again.
8955 OO
.ui
.ProcessDialog
.prototype.onRetryButtonClick = function () {
8957 this.executeAction( this.currentAction
);
8963 OO
.ui
.ProcessDialog
.prototype.onActionResize = function ( action
) {
8964 if ( this.actions
.isSpecial( action
) ) {
8967 return OO
.ui
.ProcessDialog
.parent
.prototype.onActionResize
.call( this, action
);
8973 OO
.ui
.ProcessDialog
.prototype.initialize = function () {
8975 OO
.ui
.ProcessDialog
.parent
.prototype.initialize
.call( this );
8978 this.$navigation
= $( '<div>' );
8979 this.$location
= $( '<div>' );
8980 this.$safeActions
= $( '<div>' );
8981 this.$primaryActions
= $( '<div>' );
8982 this.$otherActions
= $( '<div>' );
8983 this.dismissButton
= new OO
.ui
.ButtonWidget( {
8984 label
: OO
.ui
.msg( 'ooui-dialog-process-dismiss' )
8986 this.retryButton
= new OO
.ui
.ButtonWidget();
8987 this.$errors
= $( '<div>' );
8988 this.$errorsTitle
= $( '<div>' );
8991 this.dismissButton
.connect( this, { click
: 'onDismissErrorButtonClick' } );
8992 this.retryButton
.connect( this, { click
: 'onRetryButtonClick' } );
8995 this.title
.$element
.addClass( 'oo-ui-processDialog-title' );
8997 .append( this.title
.$element
)
8998 .addClass( 'oo-ui-processDialog-location' );
8999 this.$safeActions
.addClass( 'oo-ui-processDialog-actions-safe' );
9000 this.$primaryActions
.addClass( 'oo-ui-processDialog-actions-primary' );
9001 this.$otherActions
.addClass( 'oo-ui-processDialog-actions-other' );
9003 .addClass( 'oo-ui-processDialog-errors-title' )
9004 .text( OO
.ui
.msg( 'ooui-dialog-process-error' ) );
9006 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
9007 .append( this.$errorsTitle
, this.dismissButton
.$element
, this.retryButton
.$element
);
9009 .addClass( 'oo-ui-processDialog-content' )
9010 .append( this.$errors
);
9012 .addClass( 'oo-ui-processDialog-navigation' )
9013 .append( this.$safeActions
, this.$location
, this.$primaryActions
);
9014 this.$head
.append( this.$navigation
);
9015 this.$foot
.append( this.$otherActions
);
9021 OO
.ui
.ProcessDialog
.prototype.getActionWidgets = function ( actions
) {
9022 var i
, len
, widgets
= [];
9023 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
9025 new OO
.ui
.ActionWidget( $.extend( { framed
: true }, actions
[ i
] ) )
9034 OO
.ui
.ProcessDialog
.prototype.attachActions = function () {
9035 var i
, len
, other
, special
, others
;
9038 OO
.ui
.ProcessDialog
.parent
.prototype.attachActions
.call( this );
9040 special
= this.actions
.getSpecial();
9041 others
= this.actions
.getOthers();
9042 if ( special
.primary
) {
9043 this.$primaryActions
.append( special
.primary
.$element
);
9045 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
9046 other
= others
[ i
];
9047 this.$otherActions
.append( other
.$element
);
9049 if ( special
.safe
) {
9050 this.$safeActions
.append( special
.safe
.$element
);
9054 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
9060 OO
.ui
.ProcessDialog
.prototype.executeAction = function ( action
) {
9062 return OO
.ui
.ProcessDialog
.parent
.prototype.executeAction
.call( this, action
)
9063 .fail( function ( errors
) {
9064 process
.showErrors( errors
|| [] );
9071 OO
.ui
.ProcessDialog
.prototype.setDimensions = function () {
9073 OO
.ui
.ProcessDialog
.parent
.prototype.setDimensions
.apply( this, arguments
);
9079 * Fit label between actions.
9084 OO
.ui
.ProcessDialog
.prototype.fitLabel = function () {
9085 var safeWidth
, primaryWidth
, biggerWidth
, labelWidth
, navigationWidth
, leftWidth
, rightWidth
,
9086 size
= this.getSizeProperties();
9088 if ( typeof size
.width
!== 'number' ) {
9089 if ( this.isOpened() ) {
9090 navigationWidth
= this.$head
.width() - 20;
9091 } else if ( this.isOpening() ) {
9092 if ( !this.fitOnOpen
) {
9093 // Size is relative and the dialog isn't open yet, so wait.
9094 this.manager
.opening
.done( this.fitLabel
.bind( this ) );
9095 this.fitOnOpen
= true;
9102 navigationWidth
= size
.width
- 20;
9105 safeWidth
= this.$safeActions
.is( ':visible' ) ? this.$safeActions
.width() : 0;
9106 primaryWidth
= this.$primaryActions
.is( ':visible' ) ? this.$primaryActions
.width() : 0;
9107 biggerWidth
= Math
.max( safeWidth
, primaryWidth
);
9109 labelWidth
= this.title
.$element
.width();
9111 if ( 2 * biggerWidth
+ labelWidth
< navigationWidth
) {
9112 // We have enough space to center the label
9113 leftWidth
= rightWidth
= biggerWidth
;
9115 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
9116 if ( this.getDir() === 'ltr' ) {
9117 leftWidth
= safeWidth
;
9118 rightWidth
= primaryWidth
;
9120 leftWidth
= primaryWidth
;
9121 rightWidth
= safeWidth
;
9125 this.$location
.css( { paddingLeft
: leftWidth
, paddingRight
: rightWidth
} );
9131 * Handle errors that occurred during accept or reject processes.
9134 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
9136 OO
.ui
.ProcessDialog
.prototype.showErrors = function ( errors
) {
9137 var i
, len
, $item
, actions
,
9143 if ( errors
instanceof OO
.ui
.Error
) {
9144 errors
= [ errors
];
9147 for ( i
= 0, len
= errors
.length
; i
< len
; i
++ ) {
9148 if ( !errors
[ i
].isRecoverable() ) {
9149 recoverable
= false;
9151 if ( errors
[ i
].isWarning() ) {
9154 $item
= $( '<div>' )
9155 .addClass( 'oo-ui-processDialog-error' )
9156 .append( errors
[ i
].getMessage() );
9157 items
.push( $item
[ 0 ] );
9159 this.$errorItems
= $( items
);
9160 if ( recoverable
) {
9161 abilities
[ this.currentAction
] = true;
9162 // Copy the flags from the first matching action
9163 actions
= this.actions
.get( { actions
: this.currentAction
} );
9164 if ( actions
.length
) {
9165 this.retryButton
.clearFlags().setFlags( actions
[ 0 ].getFlags() );
9168 abilities
[ this.currentAction
] = false;
9169 this.actions
.setAbilities( abilities
);
9172 this.retryButton
.setLabel( OO
.ui
.msg( 'ooui-dialog-process-continue' ) );
9174 this.retryButton
.setLabel( OO
.ui
.msg( 'ooui-dialog-process-retry' ) );
9176 this.retryButton
.toggle( recoverable
);
9177 this.$errorsTitle
.after( this.$errorItems
);
9178 this.$errors
.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
9186 OO
.ui
.ProcessDialog
.prototype.hideErrors = function () {
9187 this.$errors
.addClass( 'oo-ui-element-hidden' );
9188 if ( this.$errorItems
) {
9189 this.$errorItems
.remove();
9190 this.$errorItems
= null;
9197 OO
.ui
.ProcessDialog
.prototype.getTeardownProcess = function ( data
) {
9199 return OO
.ui
.ProcessDialog
.parent
.prototype.getTeardownProcess
.call( this, data
)
9200 .first( function () {
9201 // Make sure to hide errors
9203 this.fitOnOpen
= false;
9208 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
9209 * which is a widget that is specified by reference before any optional configuration settings.
9211 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
9213 * - **left**: The label is placed before the field-widget and aligned with the left margin.
9214 * A left-alignment is used for forms with many fields.
9215 * - **right**: The label is placed before the field-widget and aligned to the right margin.
9216 * A right-alignment is used for long but familiar forms which users tab through,
9217 * verifying the current field with a quick glance at the label.
9218 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9219 * that users fill out from top to bottom.
9220 * - **inline**: The label is placed after the field-widget and aligned to the left.
9221 * An inline-alignment is best used with checkboxes or radio buttons.
9223 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
9224 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
9226 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9228 * @extends OO.ui.Layout
9229 * @mixins OO.ui.mixin.LabelElement
9230 * @mixins OO.ui.mixin.TitledElement
9233 * @param {OO.ui.Widget} fieldWidget Field widget
9234 * @param {Object} [config] Configuration options
9235 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
9236 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
9237 * The array may contain strings or OO.ui.HtmlSnippet instances.
9238 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
9239 * The array may contain strings or OO.ui.HtmlSnippet instances.
9240 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
9241 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
9242 * For important messages, you are advised to use `notices`, as they are always shown.
9244 * @throws {Error} An error is thrown if no widget is specified
9246 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
9247 var hasInputWidget
, div
;
9249 // Allow passing positional parameters inside the config object
9250 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
9251 config
= fieldWidget
;
9252 fieldWidget
= config
.fieldWidget
;
9255 // Make sure we have required constructor arguments
9256 if ( fieldWidget
=== undefined ) {
9257 throw new Error( 'Widget not found' );
9260 hasInputWidget
= fieldWidget
.constructor.static.supportsSimpleLabel
;
9262 // Configuration initialization
9263 config
= $.extend( { align
: 'left' }, config
);
9265 // Parent constructor
9266 OO
.ui
.FieldLayout
.parent
.call( this, config
);
9268 // Mixin constructors
9269 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9270 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
9273 this.fieldWidget
= fieldWidget
;
9276 this.$field
= $( '<div>' );
9277 this.$messages
= $( '<ul>' );
9278 this.$body
= $( '<' + ( hasInputWidget
? 'label' : 'div' ) + '>' );
9280 if ( config
.help
) {
9281 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
9282 classes
: [ 'oo-ui-fieldLayout-help' ],
9288 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
9289 div
.html( config
.help
.toString() );
9291 div
.text( config
.help
);
9293 this.popupButtonWidget
.getPopup().$body
.append(
9294 div
.addClass( 'oo-ui-fieldLayout-help-content' )
9296 this.$help
= this.popupButtonWidget
.$element
;
9298 this.$help
= $( [] );
9302 if ( hasInputWidget
) {
9303 this.$label
.on( 'click', this.onLabelClick
.bind( this ) );
9305 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
9309 .addClass( 'oo-ui-fieldLayout' )
9310 .append( this.$help
, this.$body
);
9311 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
9312 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
9314 .addClass( 'oo-ui-fieldLayout-field' )
9315 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget
.isDisabled() )
9316 .append( this.fieldWidget
.$element
);
9318 this.setErrors( config
.errors
|| [] );
9319 this.setNotices( config
.notices
|| [] );
9320 this.setAlignment( config
.align
);
9325 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
9326 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
9327 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
9332 * Handle field disable events.
9335 * @param {boolean} value Field is disabled
9337 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
9338 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
9342 * Handle label mouse click events.
9345 * @param {jQuery.Event} e Mouse click event
9347 OO
.ui
.FieldLayout
.prototype.onLabelClick = function () {
9348 this.fieldWidget
.simulateLabelClick();
9353 * Get the widget contained by the field.
9355 * @return {OO.ui.Widget} Field widget
9357 OO
.ui
.FieldLayout
.prototype.getField = function () {
9358 return this.fieldWidget
;
9363 * @param {string} kind 'error' or 'notice'
9364 * @param {string|OO.ui.HtmlSnippet} text
9367 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
9368 var $listItem
, $icon
, message
;
9369 $listItem
= $( '<li>' );
9370 if ( kind
=== 'error' ) {
9371 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
9372 } else if ( kind
=== 'notice' ) {
9373 $icon
= new OO
.ui
.IconWidget( { icon
: 'info' } ).$element
;
9377 message
= new OO
.ui
.LabelWidget( { label
: text
} );
9379 .append( $icon
, message
.$element
)
9380 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
9385 * Set the field alignment mode.
9388 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
9391 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
9392 if ( value
!== this.align
) {
9393 // Default to 'left'
9394 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
9398 if ( value
=== 'inline' ) {
9399 this.$body
.append( this.$field
, this.$label
);
9401 this.$body
.append( this.$label
, this.$field
);
9403 // Set classes. The following classes can be used here:
9404 // * oo-ui-fieldLayout-align-left
9405 // * oo-ui-fieldLayout-align-right
9406 // * oo-ui-fieldLayout-align-top
9407 // * oo-ui-fieldLayout-align-inline
9409 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
9411 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
9419 * Set the list of error messages.
9421 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
9422 * The array may contain strings or OO.ui.HtmlSnippet instances.
9425 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
9426 this.errors
= errors
.slice();
9427 this.updateMessages();
9432 * Set the list of notice messages.
9434 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
9435 * The array may contain strings or OO.ui.HtmlSnippet instances.
9438 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
9439 this.notices
= notices
.slice();
9440 this.updateMessages();
9445 * Update the rendering of error and notice messages.
9449 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
9451 this.$messages
.empty();
9453 if ( this.errors
.length
|| this.notices
.length
) {
9454 this.$body
.after( this.$messages
);
9456 this.$messages
.remove();
9460 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
9461 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
9463 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
9464 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
9469 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
9470 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
9471 * is required and is specified before any optional configuration settings.
9473 * Labels can be aligned in one of four ways:
9475 * - **left**: The label is placed before the field-widget and aligned with the left margin.
9476 * A left-alignment is used for forms with many fields.
9477 * - **right**: The label is placed before the field-widget and aligned to the right margin.
9478 * A right-alignment is used for long but familiar forms which users tab through,
9479 * verifying the current field with a quick glance at the label.
9480 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9481 * that users fill out from top to bottom.
9482 * - **inline**: The label is placed after the field-widget and aligned to the left.
9483 * An inline-alignment is best used with checkboxes or radio buttons.
9485 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
9486 * text is specified.
9489 * // Example of an ActionFieldLayout
9490 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
9491 * new OO.ui.TextInputWidget( {
9492 * placeholder: 'Field widget'
9494 * new OO.ui.ButtonWidget( {
9498 * label: 'An ActionFieldLayout. This label is aligned top',
9500 * help: 'This is help text'
9504 * $( 'body' ).append( actionFieldLayout.$element );
9507 * @extends OO.ui.FieldLayout
9510 * @param {OO.ui.Widget} fieldWidget Field widget
9511 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
9513 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
9514 // Allow passing positional parameters inside the config object
9515 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
9516 config
= fieldWidget
;
9517 fieldWidget
= config
.fieldWidget
;
9518 buttonWidget
= config
.buttonWidget
;
9521 // Parent constructor
9522 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
9525 this.buttonWidget
= buttonWidget
;
9526 this.$button
= $( '<div>' );
9527 this.$input
= $( '<div>' );
9531 .addClass( 'oo-ui-actionFieldLayout' );
9533 .addClass( 'oo-ui-actionFieldLayout-button' )
9534 .append( this.buttonWidget
.$element
);
9536 .addClass( 'oo-ui-actionFieldLayout-input' )
9537 .append( this.fieldWidget
.$element
);
9539 .append( this.$input
, this.$button
);
9544 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
9547 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
9548 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
9549 * configured with a label as well. For more information and examples,
9550 * please see the [OOjs UI documentation on MediaWiki][1].
9553 * // Example of a fieldset layout
9554 * var input1 = new OO.ui.TextInputWidget( {
9555 * placeholder: 'A text input field'
9558 * var input2 = new OO.ui.TextInputWidget( {
9559 * placeholder: 'A text input field'
9562 * var fieldset = new OO.ui.FieldsetLayout( {
9563 * label: 'Example of a fieldset layout'
9566 * fieldset.addItems( [
9567 * new OO.ui.FieldLayout( input1, {
9568 * label: 'Field One'
9570 * new OO.ui.FieldLayout( input2, {
9571 * label: 'Field Two'
9574 * $( 'body' ).append( fieldset.$element );
9576 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9579 * @extends OO.ui.Layout
9580 * @mixins OO.ui.mixin.IconElement
9581 * @mixins OO.ui.mixin.LabelElement
9582 * @mixins OO.ui.mixin.GroupElement
9585 * @param {Object} [config] Configuration options
9586 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
9588 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
9589 // Configuration initialization
9590 config
= config
|| {};
9592 // Parent constructor
9593 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
9595 // Mixin constructors
9596 OO
.ui
.mixin
.IconElement
.call( this, config
);
9597 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9598 OO
.ui
.mixin
.GroupElement
.call( this, config
);
9600 if ( config
.help
) {
9601 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
9602 classes
: [ 'oo-ui-fieldsetLayout-help' ],
9607 this.popupButtonWidget
.getPopup().$body
.append(
9609 .text( config
.help
)
9610 .addClass( 'oo-ui-fieldsetLayout-help-content' )
9612 this.$help
= this.popupButtonWidget
.$element
;
9614 this.$help
= $( [] );
9619 .addClass( 'oo-ui-fieldsetLayout' )
9620 .prepend( this.$help
, this.$icon
, this.$label
, this.$group
);
9621 if ( Array
.isArray( config
.items
) ) {
9622 this.addItems( config
.items
);
9628 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
9629 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
9630 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
9631 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
9634 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
9635 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
9636 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
9637 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9639 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
9640 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
9641 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
9642 * some fancier controls. Some controls have both regular and InputWidget variants, for example
9643 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
9644 * often have simplified APIs to match the capabilities of HTML forms.
9645 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
9647 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
9648 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9651 * // Example of a form layout that wraps a fieldset layout
9652 * var input1 = new OO.ui.TextInputWidget( {
9653 * placeholder: 'Username'
9655 * var input2 = new OO.ui.TextInputWidget( {
9656 * placeholder: 'Password',
9659 * var submit = new OO.ui.ButtonInputWidget( {
9663 * var fieldset = new OO.ui.FieldsetLayout( {
9664 * label: 'A form layout'
9666 * fieldset.addItems( [
9667 * new OO.ui.FieldLayout( input1, {
9668 * label: 'Username',
9671 * new OO.ui.FieldLayout( input2, {
9672 * label: 'Password',
9675 * new OO.ui.FieldLayout( submit )
9677 * var form = new OO.ui.FormLayout( {
9678 * items: [ fieldset ],
9679 * action: '/api/formhandler',
9682 * $( 'body' ).append( form.$element );
9685 * @extends OO.ui.Layout
9686 * @mixins OO.ui.mixin.GroupElement
9689 * @param {Object} [config] Configuration options
9690 * @cfg {string} [method] HTML form `method` attribute
9691 * @cfg {string} [action] HTML form `action` attribute
9692 * @cfg {string} [enctype] HTML form `enctype` attribute
9693 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
9695 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
9698 // Configuration initialization
9699 config
= config
|| {};
9701 // Parent constructor
9702 OO
.ui
.FormLayout
.parent
.call( this, config
);
9704 // Mixin constructors
9705 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
9708 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
9710 // Make sure the action is safe
9711 action
= config
.action
;
9712 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
9713 action
= './' + action
;
9718 .addClass( 'oo-ui-formLayout' )
9720 method
: config
.method
,
9722 enctype
: config
.enctype
9724 if ( Array
.isArray( config
.items
) ) {
9725 this.addItems( config
.items
);
9731 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
9732 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
9737 * A 'submit' event is emitted when the form is submitted.
9742 /* Static Properties */
9744 OO
.ui
.FormLayout
.static.tagName
= 'form';
9749 * Handle form submit events.
9752 * @param {jQuery.Event} e Submit event
9755 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
9756 if ( this.emit( 'submit' ) ) {
9762 * 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)
9763 * and its size is customized with the #menuSize config. The content area will fill all remaining space.
9766 * var menuLayout = new OO.ui.MenuLayout( {
9769 * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
9770 * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
9771 * select = new OO.ui.SelectWidget( {
9773 * new OO.ui.OptionWidget( {
9777 * new OO.ui.OptionWidget( {
9781 * new OO.ui.OptionWidget( {
9785 * new OO.ui.OptionWidget( {
9790 * } ).on( 'select', function ( item ) {
9791 * menuLayout.setMenuPosition( item.getData() );
9794 * menuLayout.$menu.append(
9795 * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
9797 * menuLayout.$content.append(
9798 * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
9800 * $( 'body' ).append( menuLayout.$element );
9802 * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
9803 * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
9804 * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
9807 * .oo-ui-menuLayout-menu {
9811 * .oo-ui-menuLayout-content {
9819 * @extends OO.ui.Layout
9822 * @param {Object} [config] Configuration options
9823 * @cfg {boolean} [showMenu=true] Show menu
9824 * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
9826 OO
.ui
.MenuLayout
= function OoUiMenuLayout( config
) {
9827 // Configuration initialization
9828 config
= $.extend( {
9830 menuPosition
: 'before'
9833 // Parent constructor
9834 OO
.ui
.MenuLayout
.parent
.call( this, config
);
9839 * @property {jQuery}
9841 this.$menu
= $( '<div>' );
9845 * @property {jQuery}
9847 this.$content
= $( '<div>' );
9851 .addClass( 'oo-ui-menuLayout-menu' );
9852 this.$content
.addClass( 'oo-ui-menuLayout-content' );
9854 .addClass( 'oo-ui-menuLayout' )
9855 .append( this.$content
, this.$menu
);
9856 this.setMenuPosition( config
.menuPosition
);
9857 this.toggleMenu( config
.showMenu
);
9862 OO
.inheritClass( OO
.ui
.MenuLayout
, OO
.ui
.Layout
);
9869 * @param {boolean} showMenu Show menu, omit to toggle
9872 OO
.ui
.MenuLayout
.prototype.toggleMenu = function ( showMenu
) {
9873 showMenu
= showMenu
=== undefined ? !this.showMenu
: !!showMenu
;
9875 if ( this.showMenu
!== showMenu
) {
9876 this.showMenu
= showMenu
;
9878 .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu
)
9879 .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu
);
9886 * Check if menu is visible
9888 * @return {boolean} Menu is visible
9890 OO
.ui
.MenuLayout
.prototype.isMenuVisible = function () {
9891 return this.showMenu
;
9895 * Set menu position.
9897 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
9898 * @throws {Error} If position value is not supported
9901 OO
.ui
.MenuLayout
.prototype.setMenuPosition = function ( position
) {
9902 this.$element
.removeClass( 'oo-ui-menuLayout-' + this.menuPosition
);
9903 this.menuPosition
= position
;
9904 this.$element
.addClass( 'oo-ui-menuLayout-' + position
);
9910 * Get menu position.
9912 * @return {string} Menu position
9914 OO
.ui
.MenuLayout
.prototype.getMenuPosition = function () {
9915 return this.menuPosition
;
9919 * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
9920 * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
9921 * through the pages and select which one to display. By default, only one page is
9922 * displayed at a time and the outline is hidden. When a user navigates to a new page,
9923 * the booklet layout automatically focuses on the first focusable element, unless the
9924 * default setting is changed. Optionally, booklets can be configured to show
9925 * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
9928 * // Example of a BookletLayout that contains two PageLayouts.
9930 * function PageOneLayout( name, config ) {
9931 * PageOneLayout.parent.call( this, name, config );
9932 * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
9934 * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
9935 * PageOneLayout.prototype.setupOutlineItem = function () {
9936 * this.outlineItem.setLabel( 'Page One' );
9939 * function PageTwoLayout( name, config ) {
9940 * PageTwoLayout.parent.call( this, name, config );
9941 * this.$element.append( '<p>Second page</p>' );
9943 * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
9944 * PageTwoLayout.prototype.setupOutlineItem = function () {
9945 * this.outlineItem.setLabel( 'Page Two' );
9948 * var page1 = new PageOneLayout( 'one' ),
9949 * page2 = new PageTwoLayout( 'two' );
9951 * var booklet = new OO.ui.BookletLayout( {
9955 * booklet.addPages ( [ page1, page2 ] );
9956 * $( 'body' ).append( booklet.$element );
9959 * @extends OO.ui.MenuLayout
9962 * @param {Object} [config] Configuration options
9963 * @cfg {boolean} [continuous=false] Show all pages, one after another
9964 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
9965 * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
9966 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
9968 OO
.ui
.BookletLayout
= function OoUiBookletLayout( config
) {
9969 // Configuration initialization
9970 config
= config
|| {};
9972 // Parent constructor
9973 OO
.ui
.BookletLayout
.parent
.call( this, config
);
9976 this.currentPageName
= null;
9978 this.ignoreFocus
= false;
9979 this.stackLayout
= new OO
.ui
.StackLayout( { continuous
: !!config
.continuous
} );
9980 this.$content
.append( this.stackLayout
.$element
);
9981 this.autoFocus
= config
.autoFocus
=== undefined || !!config
.autoFocus
;
9982 this.outlineVisible
= false;
9983 this.outlined
= !!config
.outlined
;
9984 if ( this.outlined
) {
9985 this.editable
= !!config
.editable
;
9986 this.outlineControlsWidget
= null;
9987 this.outlineSelectWidget
= new OO
.ui
.OutlineSelectWidget();
9988 this.outlinePanel
= new OO
.ui
.PanelLayout( { scrollable
: true } );
9989 this.$menu
.append( this.outlinePanel
.$element
);
9990 this.outlineVisible
= true;
9991 if ( this.editable
) {
9992 this.outlineControlsWidget
= new OO
.ui
.OutlineControlsWidget(
9993 this.outlineSelectWidget
9997 this.toggleMenu( this.outlined
);
10000 this.stackLayout
.connect( this, { set: 'onStackLayoutSet' } );
10001 if ( this.outlined
) {
10002 this.outlineSelectWidget
.connect( this, { select
: 'onOutlineSelectWidgetSelect' } );
10003 this.scrolling
= false;
10004 this.stackLayout
.connect( this, { visibleItemChange
: 'onStackLayoutVisibleItemChange' } );
10006 if ( this.autoFocus
) {
10007 // Event 'focus' does not bubble, but 'focusin' does
10008 this.stackLayout
.$element
.on( 'focusin', this.onStackLayoutFocus
.bind( this ) );
10012 this.$element
.addClass( 'oo-ui-bookletLayout' );
10013 this.stackLayout
.$element
.addClass( 'oo-ui-bookletLayout-stackLayout' );
10014 if ( this.outlined
) {
10015 this.outlinePanel
.$element
10016 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
10017 .append( this.outlineSelectWidget
.$element
);
10018 if ( this.editable
) {
10019 this.outlinePanel
.$element
10020 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
10021 .append( this.outlineControlsWidget
.$element
);
10028 OO
.inheritClass( OO
.ui
.BookletLayout
, OO
.ui
.MenuLayout
);
10033 * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
10035 * @param {OO.ui.PageLayout} page Current page
10039 * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
10042 * @param {OO.ui.PageLayout[]} page Added pages
10043 * @param {number} index Index pages were added at
10047 * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
10048 * {@link #removePages removed} from the booklet.
10051 * @param {OO.ui.PageLayout[]} pages Removed pages
10057 * Handle stack layout focus.
10060 * @param {jQuery.Event} e Focusin event
10062 OO
.ui
.BookletLayout
.prototype.onStackLayoutFocus = function ( e
) {
10065 // Find the page that an element was focused within
10066 $target
= $( e
.target
).closest( '.oo-ui-pageLayout' );
10067 for ( name
in this.pages
) {
10068 // Check for page match, exclude current page to find only page changes
10069 if ( this.pages
[ name
].$element
[ 0 ] === $target
[ 0 ] && name
!== this.currentPageName
) {
10070 this.setPage( name
);
10077 * Handle visibleItemChange events from the stackLayout
10079 * The next visible page is set as the current page by selecting it
10082 * @param {OO.ui.PageLayout} page The next visible page in the layout
10084 OO
.ui
.BookletLayout
.prototype.onStackLayoutVisibleItemChange = function ( page
) {
10085 // Set a flag to so that the resulting call to #onStackLayoutSet doesn't
10086 // try and scroll the item into view again.
10087 this.scrolling
= true;
10088 this.outlineSelectWidget
.selectItemByData( page
.getName() );
10089 this.scrolling
= false;
10093 * Handle stack layout set events.
10096 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
10098 OO
.ui
.BookletLayout
.prototype.onStackLayoutSet = function ( page
) {
10100 if ( !this.scrolling
&& page
) {
10101 page
.scrollElementIntoView( { complete: function () {
10102 if ( layout
.autoFocus
) {
10110 * Focus the first input in the current page.
10112 * If no page is selected, the first selectable page will be selected.
10113 * If the focus is already in an element on the current page, nothing will happen.
10114 * @param {number} [itemIndex] A specific item to focus on
10116 OO
.ui
.BookletLayout
.prototype.focus = function ( itemIndex
) {
10118 items
= this.stackLayout
.getItems();
10120 if ( itemIndex
!== undefined && items
[ itemIndex
] ) {
10121 page
= items
[ itemIndex
];
10123 page
= this.stackLayout
.getCurrentItem();
10126 if ( !page
&& this.outlined
) {
10127 this.selectFirstSelectablePage();
10128 page
= this.stackLayout
.getCurrentItem();
10133 // Only change the focus if is not already in the current page
10134 if ( !OO
.ui
.contains( page
.$element
[ 0 ], this.getElementDocument().activeElement
, true ) ) {
10140 * Find the first focusable input in the booklet layout and focus
10143 OO
.ui
.BookletLayout
.prototype.focusFirstFocusable = function () {
10144 OO
.ui
.findFocusable( this.stackLayout
.$element
).focus();
10148 * Handle outline widget select events.
10151 * @param {OO.ui.OptionWidget|null} item Selected item
10153 OO
.ui
.BookletLayout
.prototype.onOutlineSelectWidgetSelect = function ( item
) {
10155 this.setPage( item
.getData() );
10160 * Check if booklet has an outline.
10162 * @return {boolean} Booklet has an outline
10164 OO
.ui
.BookletLayout
.prototype.isOutlined = function () {
10165 return this.outlined
;
10169 * Check if booklet has editing controls.
10171 * @return {boolean} Booklet is editable
10173 OO
.ui
.BookletLayout
.prototype.isEditable = function () {
10174 return this.editable
;
10178 * Check if booklet has a visible outline.
10180 * @return {boolean} Outline is visible
10182 OO
.ui
.BookletLayout
.prototype.isOutlineVisible = function () {
10183 return this.outlined
&& this.outlineVisible
;
10187 * Hide or show the outline.
10189 * @param {boolean} [show] Show outline, omit to invert current state
10192 OO
.ui
.BookletLayout
.prototype.toggleOutline = function ( show
) {
10193 if ( this.outlined
) {
10194 show
= show
=== undefined ? !this.outlineVisible
: !!show
;
10195 this.outlineVisible
= show
;
10196 this.toggleMenu( show
);
10203 * Get the page closest to the specified page.
10205 * @param {OO.ui.PageLayout} page Page to use as a reference point
10206 * @return {OO.ui.PageLayout|null} Page closest to the specified page
10208 OO
.ui
.BookletLayout
.prototype.getClosestPage = function ( page
) {
10209 var next
, prev
, level
,
10210 pages
= this.stackLayout
.getItems(),
10211 index
= pages
.indexOf( page
);
10213 if ( index
!== -1 ) {
10214 next
= pages
[ index
+ 1 ];
10215 prev
= pages
[ index
- 1 ];
10216 // Prefer adjacent pages at the same level
10217 if ( this.outlined
) {
10218 level
= this.outlineSelectWidget
.getItemFromData( page
.getName() ).getLevel();
10221 level
=== this.outlineSelectWidget
.getItemFromData( prev
.getName() ).getLevel()
10227 level
=== this.outlineSelectWidget
.getItemFromData( next
.getName() ).getLevel()
10233 return prev
|| next
|| null;
10237 * Get the outline widget.
10239 * If the booklet is not outlined, the method will return `null`.
10241 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
10243 OO
.ui
.BookletLayout
.prototype.getOutline = function () {
10244 return this.outlineSelectWidget
;
10248 * Get the outline controls widget.
10250 * If the outline is not editable, the method will return `null`.
10252 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
10254 OO
.ui
.BookletLayout
.prototype.getOutlineControls = function () {
10255 return this.outlineControlsWidget
;
10259 * Get a page by its symbolic name.
10261 * @param {string} name Symbolic name of page
10262 * @return {OO.ui.PageLayout|undefined} Page, if found
10264 OO
.ui
.BookletLayout
.prototype.getPage = function ( name
) {
10265 return this.pages
[ name
];
10269 * Get the current page.
10271 * @return {OO.ui.PageLayout|undefined} Current page, if found
10273 OO
.ui
.BookletLayout
.prototype.getCurrentPage = function () {
10274 var name
= this.getCurrentPageName();
10275 return name
? this.getPage( name
) : undefined;
10279 * Get the symbolic name of the current page.
10281 * @return {string|null} Symbolic name of the current page
10283 OO
.ui
.BookletLayout
.prototype.getCurrentPageName = function () {
10284 return this.currentPageName
;
10288 * Add pages to the booklet layout
10290 * When pages are added with the same names as existing pages, the existing pages will be
10291 * automatically removed before the new pages are added.
10293 * @param {OO.ui.PageLayout[]} pages Pages to add
10294 * @param {number} index Index of the insertion point
10298 OO
.ui
.BookletLayout
.prototype.addPages = function ( pages
, index
) {
10299 var i
, len
, name
, page
, item
, currentIndex
,
10300 stackLayoutPages
= this.stackLayout
.getItems(),
10304 // Remove pages with same names
10305 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
10307 name
= page
.getName();
10309 if ( Object
.prototype.hasOwnProperty
.call( this.pages
, name
) ) {
10310 // Correct the insertion index
10311 currentIndex
= stackLayoutPages
.indexOf( this.pages
[ name
] );
10312 if ( currentIndex
!== -1 && currentIndex
+ 1 < index
) {
10315 remove
.push( this.pages
[ name
] );
10318 if ( remove
.length
) {
10319 this.removePages( remove
);
10323 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
10325 name
= page
.getName();
10326 this.pages
[ page
.getName() ] = page
;
10327 if ( this.outlined
) {
10328 item
= new OO
.ui
.OutlineOptionWidget( { data
: name
} );
10329 page
.setOutlineItem( item
);
10330 items
.push( item
);
10334 if ( this.outlined
&& items
.length
) {
10335 this.outlineSelectWidget
.addItems( items
, index
);
10336 this.selectFirstSelectablePage();
10338 this.stackLayout
.addItems( pages
, index
);
10339 this.emit( 'add', pages
, index
);
10345 * Remove the specified pages from the booklet layout.
10347 * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
10349 * @param {OO.ui.PageLayout[]} pages An array of pages to remove
10353 OO
.ui
.BookletLayout
.prototype.removePages = function ( pages
) {
10354 var i
, len
, name
, page
,
10357 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
10359 name
= page
.getName();
10360 delete this.pages
[ name
];
10361 if ( this.outlined
) {
10362 items
.push( this.outlineSelectWidget
.getItemFromData( name
) );
10363 page
.setOutlineItem( null );
10366 if ( this.outlined
&& items
.length
) {
10367 this.outlineSelectWidget
.removeItems( items
);
10368 this.selectFirstSelectablePage();
10370 this.stackLayout
.removeItems( pages
);
10371 this.emit( 'remove', pages
);
10377 * Clear all pages from the booklet layout.
10379 * To remove only a subset of pages from the booklet, use the #removePages method.
10384 OO
.ui
.BookletLayout
.prototype.clearPages = function () {
10386 pages
= this.stackLayout
.getItems();
10389 this.currentPageName
= null;
10390 if ( this.outlined
) {
10391 this.outlineSelectWidget
.clearItems();
10392 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
10393 pages
[ i
].setOutlineItem( null );
10396 this.stackLayout
.clearItems();
10398 this.emit( 'remove', pages
);
10404 * Set the current page by symbolic name.
10407 * @param {string} name Symbolic name of page
10409 OO
.ui
.BookletLayout
.prototype.setPage = function ( name
) {
10412 page
= this.pages
[ name
],
10413 previousPage
= this.currentPageName
&& this.pages
[ this.currentPageName
];
10415 if ( name
!== this.currentPageName
) {
10416 if ( this.outlined
) {
10417 selectedItem
= this.outlineSelectWidget
.getSelectedItem();
10418 if ( selectedItem
&& selectedItem
.getData() !== name
) {
10419 this.outlineSelectWidget
.selectItemByData( name
);
10423 if ( previousPage
) {
10424 previousPage
.setActive( false );
10425 // Blur anything focused if the next page doesn't have anything focusable.
10426 // This is not needed if the next page has something focusable (because once it is focused
10427 // this blur happens automatically). If the layout is non-continuous, this check is
10428 // meaningless because the next page is not visible yet and thus can't hold focus.
10431 this.stackLayout
.continuous
&&
10432 OO
.ui
.findFocusable( page
.$element
).length
!== 0
10434 $focused
= previousPage
.$element
.find( ':focus' );
10435 if ( $focused
.length
) {
10436 $focused
[ 0 ].blur();
10440 this.currentPageName
= name
;
10441 page
.setActive( true );
10442 this.stackLayout
.setItem( page
);
10443 if ( !this.stackLayout
.continuous
&& previousPage
) {
10444 // This should not be necessary, since any inputs on the previous page should have been
10445 // blurred when it was hidden, but browsers are not very consistent about this.
10446 $focused
= previousPage
.$element
.find( ':focus' );
10447 if ( $focused
.length
) {
10448 $focused
[ 0 ].blur();
10451 this.emit( 'set', page
);
10457 * Select the first selectable page.
10461 OO
.ui
.BookletLayout
.prototype.selectFirstSelectablePage = function () {
10462 if ( !this.outlineSelectWidget
.getSelectedItem() ) {
10463 this.outlineSelectWidget
.selectItem( this.outlineSelectWidget
.getFirstSelectableItem() );
10470 * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
10471 * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
10472 * select which one to display. By default, only one card is displayed at a time. When a user
10473 * navigates to a new card, the index layout automatically focuses on the first focusable element,
10474 * unless the default setting is changed.
10476 * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
10479 * // Example of a IndexLayout that contains two CardLayouts.
10481 * function CardOneLayout( name, config ) {
10482 * CardOneLayout.parent.call( this, name, config );
10483 * this.$element.append( '<p>First card</p>' );
10485 * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
10486 * CardOneLayout.prototype.setupTabItem = function () {
10487 * this.tabItem.setLabel( 'Card one' );
10490 * var card1 = new CardOneLayout( 'one' ),
10491 * card2 = new CardLayout( 'two', { label: 'Card two' } );
10493 * card2.$element.append( '<p>Second card</p>' );
10495 * var index = new OO.ui.IndexLayout();
10497 * index.addCards ( [ card1, card2 ] );
10498 * $( 'body' ).append( index.$element );
10501 * @extends OO.ui.MenuLayout
10504 * @param {Object} [config] Configuration options
10505 * @cfg {boolean} [continuous=false] Show all cards, one after another
10506 * @cfg {boolean} [expanded=true] Expand the content panel to fill the entire parent element.
10507 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
10509 OO
.ui
.IndexLayout
= function OoUiIndexLayout( config
) {
10510 // Configuration initialization
10511 config
= $.extend( {}, config
, { menuPosition
: 'top' } );
10513 // Parent constructor
10514 OO
.ui
.IndexLayout
.parent
.call( this, config
);
10517 this.currentCardName
= null;
10519 this.ignoreFocus
= false;
10520 this.stackLayout
= new OO
.ui
.StackLayout( {
10521 continuous
: !!config
.continuous
,
10522 expanded
: config
.expanded
10524 this.$content
.append( this.stackLayout
.$element
);
10525 this.autoFocus
= config
.autoFocus
=== undefined || !!config
.autoFocus
;
10527 this.tabSelectWidget
= new OO
.ui
.TabSelectWidget();
10528 this.tabPanel
= new OO
.ui
.PanelLayout();
10529 this.$menu
.append( this.tabPanel
.$element
);
10531 this.toggleMenu( true );
10534 this.stackLayout
.connect( this, { set: 'onStackLayoutSet' } );
10535 this.tabSelectWidget
.connect( this, { select
: 'onTabSelectWidgetSelect' } );
10536 if ( this.autoFocus
) {
10537 // Event 'focus' does not bubble, but 'focusin' does
10538 this.stackLayout
.$element
.on( 'focusin', this.onStackLayoutFocus
.bind( this ) );
10542 this.$element
.addClass( 'oo-ui-indexLayout' );
10543 this.stackLayout
.$element
.addClass( 'oo-ui-indexLayout-stackLayout' );
10544 this.tabPanel
.$element
10545 .addClass( 'oo-ui-indexLayout-tabPanel' )
10546 .append( this.tabSelectWidget
.$element
);
10551 OO
.inheritClass( OO
.ui
.IndexLayout
, OO
.ui
.MenuLayout
);
10556 * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
10558 * @param {OO.ui.CardLayout} card Current card
10562 * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
10565 * @param {OO.ui.CardLayout[]} card Added cards
10566 * @param {number} index Index cards were added at
10570 * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
10571 * {@link #removeCards removed} from the index.
10574 * @param {OO.ui.CardLayout[]} cards Removed cards
10580 * Handle stack layout focus.
10583 * @param {jQuery.Event} e Focusin event
10585 OO
.ui
.IndexLayout
.prototype.onStackLayoutFocus = function ( e
) {
10588 // Find the card that an element was focused within
10589 $target
= $( e
.target
).closest( '.oo-ui-cardLayout' );
10590 for ( name
in this.cards
) {
10591 // Check for card match, exclude current card to find only card changes
10592 if ( this.cards
[ name
].$element
[ 0 ] === $target
[ 0 ] && name
!== this.currentCardName
) {
10593 this.setCard( name
);
10600 * Handle stack layout set events.
10603 * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
10605 OO
.ui
.IndexLayout
.prototype.onStackLayoutSet = function ( card
) {
10608 card
.scrollElementIntoView( { complete: function () {
10609 if ( layout
.autoFocus
) {
10617 * Focus the first input in the current card.
10619 * If no card is selected, the first selectable card will be selected.
10620 * If the focus is already in an element on the current card, nothing will happen.
10621 * @param {number} [itemIndex] A specific item to focus on
10623 OO
.ui
.IndexLayout
.prototype.focus = function ( itemIndex
) {
10625 items
= this.stackLayout
.getItems();
10627 if ( itemIndex
!== undefined && items
[ itemIndex
] ) {
10628 card
= items
[ itemIndex
];
10630 card
= this.stackLayout
.getCurrentItem();
10634 this.selectFirstSelectableCard();
10635 card
= this.stackLayout
.getCurrentItem();
10640 // Only change the focus if is not already in the current page
10641 if ( !OO
.ui
.contains( card
.$element
[ 0 ], this.getElementDocument().activeElement
, true ) ) {
10647 * Find the first focusable input in the index layout and focus
10650 OO
.ui
.IndexLayout
.prototype.focusFirstFocusable = function () {
10651 OO
.ui
.findFocusable( this.stackLayout
.$element
).focus();
10655 * Handle tab widget select events.
10658 * @param {OO.ui.OptionWidget|null} item Selected item
10660 OO
.ui
.IndexLayout
.prototype.onTabSelectWidgetSelect = function ( item
) {
10662 this.setCard( item
.getData() );
10667 * Get the card closest to the specified card.
10669 * @param {OO.ui.CardLayout} card Card to use as a reference point
10670 * @return {OO.ui.CardLayout|null} Card closest to the specified card
10672 OO
.ui
.IndexLayout
.prototype.getClosestCard = function ( card
) {
10673 var next
, prev
, level
,
10674 cards
= this.stackLayout
.getItems(),
10675 index
= cards
.indexOf( card
);
10677 if ( index
!== -1 ) {
10678 next
= cards
[ index
+ 1 ];
10679 prev
= cards
[ index
- 1 ];
10680 // Prefer adjacent cards at the same level
10681 level
= this.tabSelectWidget
.getItemFromData( card
.getName() ).getLevel();
10684 level
=== this.tabSelectWidget
.getItemFromData( prev
.getName() ).getLevel()
10690 level
=== this.tabSelectWidget
.getItemFromData( next
.getName() ).getLevel()
10695 return prev
|| next
|| null;
10699 * Get the tabs widget.
10701 * @return {OO.ui.TabSelectWidget} Tabs widget
10703 OO
.ui
.IndexLayout
.prototype.getTabs = function () {
10704 return this.tabSelectWidget
;
10708 * Get a card by its symbolic name.
10710 * @param {string} name Symbolic name of card
10711 * @return {OO.ui.CardLayout|undefined} Card, if found
10713 OO
.ui
.IndexLayout
.prototype.getCard = function ( name
) {
10714 return this.cards
[ name
];
10718 * Get the current card.
10720 * @return {OO.ui.CardLayout|undefined} Current card, if found
10722 OO
.ui
.IndexLayout
.prototype.getCurrentCard = function () {
10723 var name
= this.getCurrentCardName();
10724 return name
? this.getCard( name
) : undefined;
10728 * Get the symbolic name of the current card.
10730 * @return {string|null} Symbolic name of the current card
10732 OO
.ui
.IndexLayout
.prototype.getCurrentCardName = function () {
10733 return this.currentCardName
;
10737 * Add cards to the index layout
10739 * When cards are added with the same names as existing cards, the existing cards will be
10740 * automatically removed before the new cards are added.
10742 * @param {OO.ui.CardLayout[]} cards Cards to add
10743 * @param {number} index Index of the insertion point
10747 OO
.ui
.IndexLayout
.prototype.addCards = function ( cards
, index
) {
10748 var i
, len
, name
, card
, item
, currentIndex
,
10749 stackLayoutCards
= this.stackLayout
.getItems(),
10753 // Remove cards with same names
10754 for ( i
= 0, len
= cards
.length
; i
< len
; i
++ ) {
10756 name
= card
.getName();
10758 if ( Object
.prototype.hasOwnProperty
.call( this.cards
, name
) ) {
10759 // Correct the insertion index
10760 currentIndex
= stackLayoutCards
.indexOf( this.cards
[ name
] );
10761 if ( currentIndex
!== -1 && currentIndex
+ 1 < index
) {
10764 remove
.push( this.cards
[ name
] );
10767 if ( remove
.length
) {
10768 this.removeCards( remove
);
10772 for ( i
= 0, len
= cards
.length
; i
< len
; i
++ ) {
10774 name
= card
.getName();
10775 this.cards
[ card
.getName() ] = card
;
10776 item
= new OO
.ui
.TabOptionWidget( { data
: name
} );
10777 card
.setTabItem( item
);
10778 items
.push( item
);
10781 if ( items
.length
) {
10782 this.tabSelectWidget
.addItems( items
, index
);
10783 this.selectFirstSelectableCard();
10785 this.stackLayout
.addItems( cards
, index
);
10786 this.emit( 'add', cards
, index
);
10792 * Remove the specified cards from the index layout.
10794 * To remove all cards from the index, you may wish to use the #clearCards method instead.
10796 * @param {OO.ui.CardLayout[]} cards An array of cards to remove
10800 OO
.ui
.IndexLayout
.prototype.removeCards = function ( cards
) {
10801 var i
, len
, name
, card
,
10804 for ( i
= 0, len
= cards
.length
; i
< len
; i
++ ) {
10806 name
= card
.getName();
10807 delete this.cards
[ name
];
10808 items
.push( this.tabSelectWidget
.getItemFromData( name
) );
10809 card
.setTabItem( null );
10811 if ( items
.length
) {
10812 this.tabSelectWidget
.removeItems( items
);
10813 this.selectFirstSelectableCard();
10815 this.stackLayout
.removeItems( cards
);
10816 this.emit( 'remove', cards
);
10822 * Clear all cards from the index layout.
10824 * To remove only a subset of cards from the index, use the #removeCards method.
10829 OO
.ui
.IndexLayout
.prototype.clearCards = function () {
10831 cards
= this.stackLayout
.getItems();
10834 this.currentCardName
= null;
10835 this.tabSelectWidget
.clearItems();
10836 for ( i
= 0, len
= cards
.length
; i
< len
; i
++ ) {
10837 cards
[ i
].setTabItem( null );
10839 this.stackLayout
.clearItems();
10841 this.emit( 'remove', cards
);
10847 * Set the current card by symbolic name.
10850 * @param {string} name Symbolic name of card
10852 OO
.ui
.IndexLayout
.prototype.setCard = function ( name
) {
10855 card
= this.cards
[ name
],
10856 previousCard
= this.currentCardName
&& this.cards
[ this.currentCardName
];
10858 if ( name
!== this.currentCardName
) {
10859 selectedItem
= this.tabSelectWidget
.getSelectedItem();
10860 if ( selectedItem
&& selectedItem
.getData() !== name
) {
10861 this.tabSelectWidget
.selectItemByData( name
);
10864 if ( previousCard
) {
10865 previousCard
.setActive( false );
10866 // Blur anything focused if the next card doesn't have anything focusable.
10867 // This is not needed if the next card has something focusable (because once it is focused
10868 // this blur happens automatically). If the layout is non-continuous, this check is
10869 // meaningless because the next card is not visible yet and thus can't hold focus.
10872 this.stackLayout
.continuous
&&
10873 OO
.ui
.findFocusable( card
.$element
).length
!== 0
10875 $focused
= previousCard
.$element
.find( ':focus' );
10876 if ( $focused
.length
) {
10877 $focused
[ 0 ].blur();
10881 this.currentCardName
= name
;
10882 card
.setActive( true );
10883 this.stackLayout
.setItem( card
);
10884 if ( !this.stackLayout
.continuous
&& previousCard
) {
10885 // This should not be necessary, since any inputs on the previous card should have been
10886 // blurred when it was hidden, but browsers are not very consistent about this.
10887 $focused
= previousCard
.$element
.find( ':focus' );
10888 if ( $focused
.length
) {
10889 $focused
[ 0 ].blur();
10892 this.emit( 'set', card
);
10898 * Select the first selectable card.
10902 OO
.ui
.IndexLayout
.prototype.selectFirstSelectableCard = function () {
10903 if ( !this.tabSelectWidget
.getSelectedItem() ) {
10904 this.tabSelectWidget
.selectItem( this.tabSelectWidget
.getFirstSelectableItem() );
10911 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
10912 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
10915 * // Example of a panel layout
10916 * var panel = new OO.ui.PanelLayout( {
10920 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
10922 * $( 'body' ).append( panel.$element );
10925 * @extends OO.ui.Layout
10928 * @param {Object} [config] Configuration options
10929 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10930 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10931 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10932 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10934 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
10935 // Configuration initialization
10936 config
= $.extend( {
10943 // Parent constructor
10944 OO
.ui
.PanelLayout
.parent
.call( this, config
);
10947 this.$element
.addClass( 'oo-ui-panelLayout' );
10948 if ( config
.scrollable
) {
10949 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
10951 if ( config
.padded
) {
10952 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
10954 if ( config
.expanded
) {
10955 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
10957 if ( config
.framed
) {
10958 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
10964 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
10969 * Focus the panel layout
10971 * The default implementation just focuses the first focusable element in the panel
10973 OO
.ui
.PanelLayout
.prototype.focus = function () {
10974 OO
.ui
.findFocusable( this.$element
).focus();
10978 * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
10979 * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
10980 * rather extended to include the required content and functionality.
10982 * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
10983 * item is customized (with a label) using the #setupTabItem method. See
10984 * {@link OO.ui.IndexLayout IndexLayout} for an example.
10987 * @extends OO.ui.PanelLayout
10990 * @param {string} name Unique symbolic name of card
10991 * @param {Object} [config] Configuration options
10992 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for card's tab
10994 OO
.ui
.CardLayout
= function OoUiCardLayout( name
, config
) {
10995 // Allow passing positional parameters inside the config object
10996 if ( OO
.isPlainObject( name
) && config
=== undefined ) {
10998 name
= config
.name
;
11001 // Configuration initialization
11002 config
= $.extend( { scrollable
: true }, config
);
11004 // Parent constructor
11005 OO
.ui
.CardLayout
.parent
.call( this, config
);
11009 this.label
= config
.label
;
11010 this.tabItem
= null;
11011 this.active
= false;
11014 this.$element
.addClass( 'oo-ui-cardLayout' );
11019 OO
.inheritClass( OO
.ui
.CardLayout
, OO
.ui
.PanelLayout
);
11024 * An 'active' event is emitted when the card becomes active. Cards become active when they are
11025 * shown in a index layout that is configured to display only one card at a time.
11028 * @param {boolean} active Card is active
11034 * Get the symbolic name of the card.
11036 * @return {string} Symbolic name of card
11038 OO
.ui
.CardLayout
.prototype.getName = function () {
11043 * Check if card is active.
11045 * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
11046 * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
11048 * @return {boolean} Card is active
11050 OO
.ui
.CardLayout
.prototype.isActive = function () {
11051 return this.active
;
11057 * The tab item allows users to access the card from the index's tab
11058 * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
11060 * @return {OO.ui.TabOptionWidget|null} Tab option widget
11062 OO
.ui
.CardLayout
.prototype.getTabItem = function () {
11063 return this.tabItem
;
11067 * Set or unset the tab item.
11069 * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
11070 * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
11071 * level), use #setupTabItem instead of this method.
11073 * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
11076 OO
.ui
.CardLayout
.prototype.setTabItem = function ( tabItem
) {
11077 this.tabItem
= tabItem
|| null;
11079 this.setupTabItem();
11085 * Set up the tab item.
11087 * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
11088 * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
11089 * the #setTabItem method instead.
11091 * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
11094 OO
.ui
.CardLayout
.prototype.setupTabItem = function () {
11095 if ( this.label
) {
11096 this.tabItem
.setLabel( this.label
);
11102 * Set the card to its 'active' state.
11104 * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
11105 * CSS is applied to the tab item to reflect the card's active state. Outside of the index
11106 * context, setting the active state on a card does nothing.
11108 * @param {boolean} value Card is active
11111 OO
.ui
.CardLayout
.prototype.setActive = function ( active
) {
11114 if ( active
!== this.active
) {
11115 this.active
= active
;
11116 this.$element
.toggleClass( 'oo-ui-cardLayout-active', this.active
);
11117 this.emit( 'active', this.active
);
11122 * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
11123 * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
11124 * rather extended to include the required content and functionality.
11126 * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
11127 * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
11128 * {@link OO.ui.BookletLayout BookletLayout} for an example.
11131 * @extends OO.ui.PanelLayout
11134 * @param {string} name Unique symbolic name of page
11135 * @param {Object} [config] Configuration options
11137 OO
.ui
.PageLayout
= function OoUiPageLayout( name
, config
) {
11138 // Allow passing positional parameters inside the config object
11139 if ( OO
.isPlainObject( name
) && config
=== undefined ) {
11141 name
= config
.name
;
11144 // Configuration initialization
11145 config
= $.extend( { scrollable
: true }, config
);
11147 // Parent constructor
11148 OO
.ui
.PageLayout
.parent
.call( this, config
);
11152 this.outlineItem
= null;
11153 this.active
= false;
11156 this.$element
.addClass( 'oo-ui-pageLayout' );
11161 OO
.inheritClass( OO
.ui
.PageLayout
, OO
.ui
.PanelLayout
);
11166 * An 'active' event is emitted when the page becomes active. Pages become active when they are
11167 * shown in a booklet layout that is configured to display only one page at a time.
11170 * @param {boolean} active Page is active
11176 * Get the symbolic name of the page.
11178 * @return {string} Symbolic name of page
11180 OO
.ui
.PageLayout
.prototype.getName = function () {
11185 * Check if page is active.
11187 * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
11188 * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
11190 * @return {boolean} Page is active
11192 OO
.ui
.PageLayout
.prototype.isActive = function () {
11193 return this.active
;
11197 * Get outline item.
11199 * The outline item allows users to access the page from the booklet's outline
11200 * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
11202 * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
11204 OO
.ui
.PageLayout
.prototype.getOutlineItem = function () {
11205 return this.outlineItem
;
11209 * Set or unset the outline item.
11211 * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
11212 * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
11213 * level), use #setupOutlineItem instead of this method.
11215 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
11218 OO
.ui
.PageLayout
.prototype.setOutlineItem = function ( outlineItem
) {
11219 this.outlineItem
= outlineItem
|| null;
11220 if ( outlineItem
) {
11221 this.setupOutlineItem();
11227 * Set up the outline item.
11229 * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
11230 * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
11231 * the #setOutlineItem method instead.
11233 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
11236 OO
.ui
.PageLayout
.prototype.setupOutlineItem = function () {
11241 * Set the page to its 'active' state.
11243 * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
11244 * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
11245 * context, setting the active state on a page does nothing.
11247 * @param {boolean} value Page is active
11250 OO
.ui
.PageLayout
.prototype.setActive = function ( active
) {
11253 if ( active
!== this.active
) {
11254 this.active
= active
;
11255 this.$element
.toggleClass( 'oo-ui-pageLayout-active', active
);
11256 this.emit( 'active', this.active
);
11261 * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
11262 * at a time, though the stack layout can also be configured to show all contained panels, one after another,
11263 * by setting the #continuous option to 'true'.
11266 * // A stack layout with two panels, configured to be displayed continously
11267 * var myStack = new OO.ui.StackLayout( {
11269 * new OO.ui.PanelLayout( {
11270 * $content: $( '<p>Panel One</p>' ),
11274 * new OO.ui.PanelLayout( {
11275 * $content: $( '<p>Panel Two</p>' ),
11282 * $( 'body' ).append( myStack.$element );
11285 * @extends OO.ui.PanelLayout
11286 * @mixins OO.ui.mixin.GroupElement
11289 * @param {Object} [config] Configuration options
11290 * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
11291 * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
11293 OO
.ui
.StackLayout
= function OoUiStackLayout( config
) {
11294 // Configuration initialization
11295 config
= $.extend( { scrollable
: true }, config
);
11297 // Parent constructor
11298 OO
.ui
.StackLayout
.parent
.call( this, config
);
11300 // Mixin constructors
11301 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11304 this.currentItem
= null;
11305 this.continuous
= !!config
.continuous
;
11308 this.$element
.addClass( 'oo-ui-stackLayout' );
11309 if ( this.continuous
) {
11310 this.$element
.addClass( 'oo-ui-stackLayout-continuous' );
11311 this.$element
.on( 'scroll', OO
.ui
.debounce( this.onScroll
.bind( this ), 250 ) );
11313 if ( Array
.isArray( config
.items
) ) {
11314 this.addItems( config
.items
);
11320 OO
.inheritClass( OO
.ui
.StackLayout
, OO
.ui
.PanelLayout
);
11321 OO
.mixinClass( OO
.ui
.StackLayout
, OO
.ui
.mixin
.GroupElement
);
11326 * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
11327 * {@link #clearItems cleared} or {@link #setItem displayed}.
11330 * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
11334 * When used in continuous mode, this event is emitted when the user scrolls down
11335 * far enough such that currentItem is no longer visible.
11337 * @event visibleItemChange
11338 * @param {OO.ui.PanelLayout} panel The next visible item in the layout
11344 * Handle scroll events from the layout element
11346 * @param {jQuery.Event} e
11347 * @fires visibleItemChange
11349 OO
.ui
.StackLayout
.prototype.onScroll = function () {
11351 len
= this.items
.length
,
11352 currentIndex
= this.items
.indexOf( this.currentItem
),
11353 newIndex
= currentIndex
,
11354 containerRect
= this.$element
[ 0 ].getBoundingClientRect();
11356 if ( !containerRect
|| ( !containerRect
.top
&& !containerRect
.bottom
) ) {
11357 // Can't get bounding rect, possibly not attached.
11361 function getRect( item
) {
11362 return item
.$element
[ 0 ].getBoundingClientRect();
11365 function isVisible( item
) {
11366 var rect
= getRect( item
);
11367 return rect
.bottom
> containerRect
.top
&& rect
.top
< containerRect
.bottom
;
11370 currentRect
= getRect( this.currentItem
);
11372 if ( currentRect
.bottom
< containerRect
.top
) {
11373 // Scrolled down past current item
11374 while ( ++newIndex
< len
) {
11375 if ( isVisible( this.items
[ newIndex
] ) ) {
11379 } else if ( currentRect
.top
> containerRect
.bottom
) {
11380 // Scrolled up past current item
11381 while ( --newIndex
>= 0 ) {
11382 if ( isVisible( this.items
[ newIndex
] ) ) {
11388 if ( newIndex
!== currentIndex
) {
11389 this.emit( 'visibleItemChange', this.items
[ newIndex
] );
11394 * Get the current panel.
11396 * @return {OO.ui.Layout|null}
11398 OO
.ui
.StackLayout
.prototype.getCurrentItem = function () {
11399 return this.currentItem
;
11403 * Unset the current item.
11406 * @param {OO.ui.StackLayout} layout
11409 OO
.ui
.StackLayout
.prototype.unsetCurrentItem = function () {
11410 var prevItem
= this.currentItem
;
11411 if ( prevItem
=== null ) {
11415 this.currentItem
= null;
11416 this.emit( 'set', null );
11420 * Add panel layouts to the stack layout.
11422 * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
11423 * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
11426 * @param {OO.ui.Layout[]} items Panels to add
11427 * @param {number} [index] Index of the insertion point
11430 OO
.ui
.StackLayout
.prototype.addItems = function ( items
, index
) {
11431 // Update the visibility
11432 this.updateHiddenState( items
, this.currentItem
);
11435 OO
.ui
.mixin
.GroupElement
.prototype.addItems
.call( this, items
, index
);
11437 if ( !this.currentItem
&& items
.length
) {
11438 this.setItem( items
[ 0 ] );
11445 * Remove the specified panels from the stack layout.
11447 * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
11448 * you may wish to use the #clearItems method instead.
11450 * @param {OO.ui.Layout[]} items Panels to remove
11454 OO
.ui
.StackLayout
.prototype.removeItems = function ( items
) {
11456 OO
.ui
.mixin
.GroupElement
.prototype.removeItems
.call( this, items
);
11458 if ( items
.indexOf( this.currentItem
) !== -1 ) {
11459 if ( this.items
.length
) {
11460 this.setItem( this.items
[ 0 ] );
11462 this.unsetCurrentItem();
11470 * Clear all panels from the stack layout.
11472 * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
11473 * a subset of panels, use the #removeItems method.
11478 OO
.ui
.StackLayout
.prototype.clearItems = function () {
11479 this.unsetCurrentItem();
11480 OO
.ui
.mixin
.GroupElement
.prototype.clearItems
.call( this );
11486 * Show the specified panel.
11488 * If another panel is currently displayed, it will be hidden.
11490 * @param {OO.ui.Layout} item Panel to show
11494 OO
.ui
.StackLayout
.prototype.setItem = function ( item
) {
11495 if ( item
!== this.currentItem
) {
11496 this.updateHiddenState( this.items
, item
);
11498 if ( this.items
.indexOf( item
) !== -1 ) {
11499 this.currentItem
= item
;
11500 this.emit( 'set', item
);
11502 this.unsetCurrentItem();
11510 * Update the visibility of all items in case of non-continuous view.
11512 * Ensure all items are hidden except for the selected one.
11513 * This method does nothing when the stack is continuous.
11516 * @param {OO.ui.Layout[]} items Item list iterate over
11517 * @param {OO.ui.Layout} [selectedItem] Selected item to show
11519 OO
.ui
.StackLayout
.prototype.updateHiddenState = function ( items
, selectedItem
) {
11522 if ( !this.continuous
) {
11523 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
11524 if ( !selectedItem
|| selectedItem
!== items
[ i
] ) {
11525 items
[ i
].$element
.addClass( 'oo-ui-element-hidden' );
11528 if ( selectedItem
) {
11529 selectedItem
.$element
.removeClass( 'oo-ui-element-hidden' );
11535 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
11536 * items), with small margins between them. Convenient when you need to put a number of block-level
11537 * widgets on a single line next to each other.
11539 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
11542 * // HorizontalLayout with a text input and a label
11543 * var layout = new OO.ui.HorizontalLayout( {
11545 * new OO.ui.LabelWidget( { label: 'Label' } ),
11546 * new OO.ui.TextInputWidget( { value: 'Text' } )
11549 * $( 'body' ).append( layout.$element );
11552 * @extends OO.ui.Layout
11553 * @mixins OO.ui.mixin.GroupElement
11556 * @param {Object} [config] Configuration options
11557 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
11559 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
11560 // Configuration initialization
11561 config
= config
|| {};
11563 // Parent constructor
11564 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
11566 // Mixin constructors
11567 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11570 this.$element
.addClass( 'oo-ui-horizontalLayout' );
11571 if ( Array
.isArray( config
.items
) ) {
11572 this.addItems( config
.items
);
11578 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
11579 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
11582 * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11583 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
11584 * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
11585 * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
11588 * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
11592 * // Example of a BarToolGroup with two tools
11593 * var toolFactory = new OO.ui.ToolFactory();
11594 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
11595 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11597 * // We will be placing status text in this element when tools are used
11598 * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
11600 * // Define the tools that we're going to place in our toolbar
11602 * // Create a class inheriting from OO.ui.Tool
11603 * function SearchTool() {
11604 * SearchTool.parent.apply( this, arguments );
11606 * OO.inheritClass( SearchTool, OO.ui.Tool );
11607 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
11608 * // of 'icon' and 'title' (displayed icon and text).
11609 * SearchTool.static.name = 'search';
11610 * SearchTool.static.icon = 'search';
11611 * SearchTool.static.title = 'Search...';
11612 * // Defines the action that will happen when this tool is selected (clicked).
11613 * SearchTool.prototype.onSelect = function () {
11614 * $area.text( 'Search tool clicked!' );
11615 * // Never display this tool as "active" (selected).
11616 * this.setActive( false );
11618 * SearchTool.prototype.onUpdateState = function () {};
11619 * // Make this tool available in our toolFactory and thus our toolbar
11620 * toolFactory.register( SearchTool );
11622 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
11623 * // little popup window (a PopupWidget).
11624 * function HelpTool( toolGroup, config ) {
11625 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
11630 * this.popup.$body.append( '<p>I am helpful!</p>' );
11632 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
11633 * HelpTool.static.name = 'help';
11634 * HelpTool.static.icon = 'help';
11635 * HelpTool.static.title = 'Help';
11636 * toolFactory.register( HelpTool );
11638 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
11639 * // used once (but not all defined tools must be used).
11642 * // 'bar' tool groups display tools by icon only
11644 * include: [ 'search', 'help' ]
11648 * // Create some UI around the toolbar and place it in the document
11649 * var frame = new OO.ui.PanelLayout( {
11653 * var contentFrame = new OO.ui.PanelLayout( {
11657 * frame.$element.append(
11658 * toolbar.$element,
11659 * contentFrame.$element.append( $area )
11661 * $( 'body' ).append( frame.$element );
11663 * // Here is where the toolbar is actually built. This must be done after inserting it into the
11665 * toolbar.initialize();
11667 * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
11668 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
11670 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11673 * @extends OO.ui.ToolGroup
11676 * @param {OO.ui.Toolbar} toolbar
11677 * @param {Object} [config] Configuration options
11679 OO
.ui
.BarToolGroup
= function OoUiBarToolGroup( toolbar
, config
) {
11680 // Allow passing positional parameters inside the config object
11681 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
11683 toolbar
= config
.toolbar
;
11686 // Parent constructor
11687 OO
.ui
.BarToolGroup
.parent
.call( this, toolbar
, config
);
11690 this.$element
.addClass( 'oo-ui-barToolGroup' );
11695 OO
.inheritClass( OO
.ui
.BarToolGroup
, OO
.ui
.ToolGroup
);
11697 /* Static Properties */
11699 OO
.ui
.BarToolGroup
.static.titleTooltips
= true;
11701 OO
.ui
.BarToolGroup
.static.accelTooltips
= true;
11703 OO
.ui
.BarToolGroup
.static.name
= 'bar';
11706 * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
11707 * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
11708 * optional icon and label. This class can be used for other base classes that also use this functionality.
11712 * @extends OO.ui.ToolGroup
11713 * @mixins OO.ui.mixin.IconElement
11714 * @mixins OO.ui.mixin.IndicatorElement
11715 * @mixins OO.ui.mixin.LabelElement
11716 * @mixins OO.ui.mixin.TitledElement
11717 * @mixins OO.ui.mixin.ClippableElement
11718 * @mixins OO.ui.mixin.TabIndexedElement
11721 * @param {OO.ui.Toolbar} toolbar
11722 * @param {Object} [config] Configuration options
11723 * @cfg {string} [header] Text to display at the top of the popup
11725 OO
.ui
.PopupToolGroup
= function OoUiPopupToolGroup( toolbar
, config
) {
11726 // Allow passing positional parameters inside the config object
11727 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
11729 toolbar
= config
.toolbar
;
11732 // Configuration initialization
11733 config
= config
|| {};
11735 // Parent constructor
11736 OO
.ui
.PopupToolGroup
.parent
.call( this, toolbar
, config
);
11739 this.active
= false;
11740 this.dragging
= false;
11741 this.onBlurHandler
= this.onBlur
.bind( this );
11742 this.$handle
= $( '<span>' );
11744 // Mixin constructors
11745 OO
.ui
.mixin
.IconElement
.call( this, config
);
11746 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
11747 OO
.ui
.mixin
.LabelElement
.call( this, config
);
11748 OO
.ui
.mixin
.TitledElement
.call( this, config
);
11749 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
11750 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
11754 keydown
: this.onHandleMouseKeyDown
.bind( this ),
11755 keyup
: this.onHandleMouseKeyUp
.bind( this ),
11756 mousedown
: this.onHandleMouseKeyDown
.bind( this ),
11757 mouseup
: this.onHandleMouseKeyUp
.bind( this )
11762 .addClass( 'oo-ui-popupToolGroup-handle' )
11763 .append( this.$icon
, this.$label
, this.$indicator
);
11764 // If the pop-up should have a header, add it to the top of the toolGroup.
11765 // Note: If this feature is useful for other widgets, we could abstract it into an
11766 // OO.ui.HeaderedElement mixin constructor.
11767 if ( config
.header
!== undefined ) {
11769 .prepend( $( '<span>' )
11770 .addClass( 'oo-ui-popupToolGroup-header' )
11771 .text( config
.header
)
11775 .addClass( 'oo-ui-popupToolGroup' )
11776 .prepend( this.$handle
);
11781 OO
.inheritClass( OO
.ui
.PopupToolGroup
, OO
.ui
.ToolGroup
);
11782 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.IconElement
);
11783 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.IndicatorElement
);
11784 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.LabelElement
);
11785 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.TitledElement
);
11786 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.ClippableElement
);
11787 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.mixin
.TabIndexedElement
);
11794 OO
.ui
.PopupToolGroup
.prototype.setDisabled = function () {
11796 OO
.ui
.PopupToolGroup
.parent
.prototype.setDisabled
.apply( this, arguments
);
11798 if ( this.isDisabled() && this.isElementAttached() ) {
11799 this.setActive( false );
11804 * Handle focus being lost.
11806 * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
11809 * @param {jQuery.Event} e Mouse up or key up event
11811 OO
.ui
.PopupToolGroup
.prototype.onBlur = function ( e
) {
11812 // Only deactivate when clicking outside the dropdown element
11813 if ( $( e
.target
).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element
[ 0 ] ) {
11814 this.setActive( false );
11821 OO
.ui
.PopupToolGroup
.prototype.onMouseKeyUp = function ( e
) {
11822 // Only close toolgroup when a tool was actually selected
11824 !this.isDisabled() && this.pressed
&& this.pressed
=== this.getTargetTool( e
) &&
11825 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
11827 this.setActive( false );
11829 return OO
.ui
.PopupToolGroup
.parent
.prototype.onMouseKeyUp
.call( this, e
);
11833 * Handle mouse up and key up events.
11836 * @param {jQuery.Event} e Mouse up or key up event
11838 OO
.ui
.PopupToolGroup
.prototype.onHandleMouseKeyUp = function ( e
) {
11840 !this.isDisabled() &&
11841 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
11848 * Handle mouse down and key down events.
11851 * @param {jQuery.Event} e Mouse down or key down event
11853 OO
.ui
.PopupToolGroup
.prototype.onHandleMouseKeyDown = function ( e
) {
11855 !this.isDisabled() &&
11856 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
11858 this.setActive( !this.active
);
11864 * Switch into 'active' mode.
11866 * When active, the popup is visible. A mouseup event anywhere in the document will trigger
11869 OO
.ui
.PopupToolGroup
.prototype.setActive = function ( value
) {
11870 var containerWidth
, containerLeft
;
11872 if ( this.active
!== value
) {
11873 this.active
= value
;
11875 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler
, true );
11876 this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler
, true );
11878 this.$clippable
.css( 'left', '' );
11879 // Try anchoring the popup to the left first
11880 this.$element
.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
11881 this.toggleClipping( true );
11882 if ( this.isClippedHorizontally() ) {
11883 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
11884 this.toggleClipping( false );
11886 .removeClass( 'oo-ui-popupToolGroup-left' )
11887 .addClass( 'oo-ui-popupToolGroup-right' );
11888 this.toggleClipping( true );
11890 if ( this.isClippedHorizontally() ) {
11891 // Anchoring to the right also caused the popup to clip, so just make it fill the container
11892 containerWidth
= this.$clippableScrollableContainer
.width();
11893 containerLeft
= this.$clippableScrollableContainer
.offset().left
;
11895 this.toggleClipping( false );
11896 this.$element
.removeClass( 'oo-ui-popupToolGroup-right' );
11898 this.$clippable
.css( {
11899 left
: -( this.$element
.offset().left
- containerLeft
),
11900 width
: containerWidth
11904 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler
, true );
11905 this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler
, true );
11906 this.$element
.removeClass(
11907 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
11909 this.toggleClipping( false );
11915 * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11916 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
11917 * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
11918 * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
11919 * with a label, icon, indicator, header, and title.
11921 * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
11922 * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
11923 * users to collapse the list again.
11925 * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
11926 * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
11927 * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
11930 * // Example of a ListToolGroup
11931 * var toolFactory = new OO.ui.ToolFactory();
11932 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
11933 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11935 * // Configure and register two tools
11936 * function SettingsTool() {
11937 * SettingsTool.parent.apply( this, arguments );
11939 * OO.inheritClass( SettingsTool, OO.ui.Tool );
11940 * SettingsTool.static.name = 'settings';
11941 * SettingsTool.static.icon = 'settings';
11942 * SettingsTool.static.title = 'Change settings';
11943 * SettingsTool.prototype.onSelect = function () {
11944 * this.setActive( false );
11946 * SettingsTool.prototype.onUpdateState = function () {};
11947 * toolFactory.register( SettingsTool );
11948 * // Register two more tools, nothing interesting here
11949 * function StuffTool() {
11950 * StuffTool.parent.apply( this, arguments );
11952 * OO.inheritClass( StuffTool, OO.ui.Tool );
11953 * StuffTool.static.name = 'stuff';
11954 * StuffTool.static.icon = 'search';
11955 * StuffTool.static.title = 'Change the world';
11956 * StuffTool.prototype.onSelect = function () {
11957 * this.setActive( false );
11959 * StuffTool.prototype.onUpdateState = function () {};
11960 * toolFactory.register( StuffTool );
11963 * // Configurations for list toolgroup.
11965 * label: 'ListToolGroup',
11966 * indicator: 'down',
11967 * icon: 'ellipsis',
11968 * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
11969 * header: 'This is the header',
11970 * include: [ 'settings', 'stuff' ],
11971 * allowCollapse: ['stuff']
11975 * // Create some UI around the toolbar and place it in the document
11976 * var frame = new OO.ui.PanelLayout( {
11980 * frame.$element.append(
11983 * $( 'body' ).append( frame.$element );
11984 * // Build the toolbar. This must be done after the toolbar has been appended to the document.
11985 * toolbar.initialize();
11987 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
11989 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11992 * @extends OO.ui.PopupToolGroup
11995 * @param {OO.ui.Toolbar} toolbar
11996 * @param {Object} [config] Configuration options
11997 * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
11998 * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
11999 * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
12000 * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
12001 * To open a collapsible list in its expanded state, set #expanded to 'true'.
12002 * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
12003 * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
12004 * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
12005 * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
12006 * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
12008 OO
.ui
.ListToolGroup
= function OoUiListToolGroup( toolbar
, config
) {
12009 // Allow passing positional parameters inside the config object
12010 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
12012 toolbar
= config
.toolbar
;
12015 // Configuration initialization
12016 config
= config
|| {};
12018 // Properties (must be set before parent constructor, which calls #populate)
12019 this.allowCollapse
= config
.allowCollapse
;
12020 this.forceExpand
= config
.forceExpand
;
12021 this.expanded
= config
.expanded
!== undefined ? config
.expanded
: false;
12022 this.collapsibleTools
= [];
12024 // Parent constructor
12025 OO
.ui
.ListToolGroup
.parent
.call( this, toolbar
, config
);
12028 this.$element
.addClass( 'oo-ui-listToolGroup' );
12033 OO
.inheritClass( OO
.ui
.ListToolGroup
, OO
.ui
.PopupToolGroup
);
12035 /* Static Properties */
12037 OO
.ui
.ListToolGroup
.static.name
= 'list';
12044 OO
.ui
.ListToolGroup
.prototype.populate = function () {
12045 var i
, len
, allowCollapse
= [];
12047 OO
.ui
.ListToolGroup
.parent
.prototype.populate
.call( this );
12049 // Update the list of collapsible tools
12050 if ( this.allowCollapse
!== undefined ) {
12051 allowCollapse
= this.allowCollapse
;
12052 } else if ( this.forceExpand
!== undefined ) {
12053 allowCollapse
= OO
.simpleArrayDifference( Object
.keys( this.tools
), this.forceExpand
);
12056 this.collapsibleTools
= [];
12057 for ( i
= 0, len
= allowCollapse
.length
; i
< len
; i
++ ) {
12058 if ( this.tools
[ allowCollapse
[ i
] ] !== undefined ) {
12059 this.collapsibleTools
.push( this.tools
[ allowCollapse
[ i
] ] );
12063 // Keep at the end, even when tools are added
12064 this.$group
.append( this.getExpandCollapseTool().$element
);
12066 this.getExpandCollapseTool().toggle( this.collapsibleTools
.length
!== 0 );
12067 this.updateCollapsibleState();
12070 OO
.ui
.ListToolGroup
.prototype.getExpandCollapseTool = function () {
12071 var ExpandCollapseTool
;
12072 if ( this.expandCollapseTool
=== undefined ) {
12073 ExpandCollapseTool = function () {
12074 ExpandCollapseTool
.parent
.apply( this, arguments
);
12077 OO
.inheritClass( ExpandCollapseTool
, OO
.ui
.Tool
);
12079 ExpandCollapseTool
.prototype.onSelect = function () {
12080 this.toolGroup
.expanded
= !this.toolGroup
.expanded
;
12081 this.toolGroup
.updateCollapsibleState();
12082 this.setActive( false );
12084 ExpandCollapseTool
.prototype.onUpdateState = function () {
12085 // Do nothing. Tool interface requires an implementation of this function.
12088 ExpandCollapseTool
.static.name
= 'more-fewer';
12090 this.expandCollapseTool
= new ExpandCollapseTool( this );
12092 return this.expandCollapseTool
;
12098 OO
.ui
.ListToolGroup
.prototype.onMouseKeyUp = function ( e
) {
12099 // Do not close the popup when the user wants to show more/fewer tools
12101 $( e
.target
).closest( '.oo-ui-tool-name-more-fewer' ).length
&&
12102 ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
|| e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
12104 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
12105 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
12106 return OO
.ui
.ListToolGroup
.parent
.parent
.prototype.onMouseKeyUp
.call( this, e
);
12108 return OO
.ui
.ListToolGroup
.parent
.prototype.onMouseKeyUp
.call( this, e
);
12112 OO
.ui
.ListToolGroup
.prototype.updateCollapsibleState = function () {
12115 this.getExpandCollapseTool()
12116 .setIcon( this.expanded
? 'collapse' : 'expand' )
12117 .setTitle( OO
.ui
.msg( this.expanded
? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
12119 for ( i
= 0, len
= this.collapsibleTools
.length
; i
< len
; i
++ ) {
12120 this.collapsibleTools
[ i
].toggle( this.expanded
);
12125 * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
12126 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
12127 * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
12128 * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
12129 * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
12130 * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
12132 * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
12136 * // Example of a MenuToolGroup
12137 * var toolFactory = new OO.ui.ToolFactory();
12138 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
12139 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
12141 * // We will be placing status text in this element when tools are used
12142 * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
12144 * // Define the tools that we're going to place in our toolbar
12146 * function SettingsTool() {
12147 * SettingsTool.parent.apply( this, arguments );
12148 * this.reallyActive = false;
12150 * OO.inheritClass( SettingsTool, OO.ui.Tool );
12151 * SettingsTool.static.name = 'settings';
12152 * SettingsTool.static.icon = 'settings';
12153 * SettingsTool.static.title = 'Change settings';
12154 * SettingsTool.prototype.onSelect = function () {
12155 * $area.text( 'Settings tool clicked!' );
12156 * // Toggle the active state on each click
12157 * this.reallyActive = !this.reallyActive;
12158 * this.setActive( this.reallyActive );
12159 * // To update the menu label
12160 * this.toolbar.emit( 'updateState' );
12162 * SettingsTool.prototype.onUpdateState = function () {};
12163 * toolFactory.register( SettingsTool );
12165 * function StuffTool() {
12166 * StuffTool.parent.apply( this, arguments );
12167 * this.reallyActive = false;
12169 * OO.inheritClass( StuffTool, OO.ui.Tool );
12170 * StuffTool.static.name = 'stuff';
12171 * StuffTool.static.icon = 'ellipsis';
12172 * StuffTool.static.title = 'More stuff';
12173 * StuffTool.prototype.onSelect = function () {
12174 * $area.text( 'More stuff tool clicked!' );
12175 * // Toggle the active state on each click
12176 * this.reallyActive = !this.reallyActive;
12177 * this.setActive( this.reallyActive );
12178 * // To update the menu label
12179 * this.toolbar.emit( 'updateState' );
12181 * StuffTool.prototype.onUpdateState = function () {};
12182 * toolFactory.register( StuffTool );
12184 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
12185 * // used once (but not all defined tools must be used).
12189 * header: 'This is the (optional) header',
12190 * title: 'This is the (optional) title',
12191 * indicator: 'down',
12192 * include: [ 'settings', 'stuff' ]
12196 * // Create some UI around the toolbar and place it in the document
12197 * var frame = new OO.ui.PanelLayout( {
12201 * var contentFrame = new OO.ui.PanelLayout( {
12205 * frame.$element.append(
12206 * toolbar.$element,
12207 * contentFrame.$element.append( $area )
12209 * $( 'body' ).append( frame.$element );
12211 * // Here is where the toolbar is actually built. This must be done after inserting it into the
12213 * toolbar.initialize();
12214 * toolbar.emit( 'updateState' );
12216 * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
12217 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
12219 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
12222 * @extends OO.ui.PopupToolGroup
12225 * @param {OO.ui.Toolbar} toolbar
12226 * @param {Object} [config] Configuration options
12228 OO
.ui
.MenuToolGroup
= function OoUiMenuToolGroup( toolbar
, config
) {
12229 // Allow passing positional parameters inside the config object
12230 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
12232 toolbar
= config
.toolbar
;
12235 // Configuration initialization
12236 config
= config
|| {};
12238 // Parent constructor
12239 OO
.ui
.MenuToolGroup
.parent
.call( this, toolbar
, config
);
12242 this.toolbar
.connect( this, { updateState
: 'onUpdateState' } );
12245 this.$element
.addClass( 'oo-ui-menuToolGroup' );
12250 OO
.inheritClass( OO
.ui
.MenuToolGroup
, OO
.ui
.PopupToolGroup
);
12252 /* Static Properties */
12254 OO
.ui
.MenuToolGroup
.static.name
= 'menu';
12259 * Handle the toolbar state being updated.
12261 * When the state changes, the title of each active item in the menu will be joined together and
12262 * used as a label for the group. The label will be empty if none of the items are active.
12266 OO
.ui
.MenuToolGroup
.prototype.onUpdateState = function () {
12270 for ( name
in this.tools
) {
12271 if ( this.tools
[ name
].isActive() ) {
12272 labelTexts
.push( this.tools
[ name
].getTitle() );
12276 this.setLabel( labelTexts
.join( ', ' ) || ' ' );
12280 * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
12281 * 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
12282 * an #onSelect or #onUpdateState method, as these methods have been implemented already.
12284 * // Example of a popup tool. When selected, a popup tool displays
12285 * // a popup window.
12286 * function HelpTool( toolGroup, config ) {
12287 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
12292 * this.popup.$body.append( '<p>I am helpful!</p>' );
12294 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
12295 * HelpTool.static.name = 'help';
12296 * HelpTool.static.icon = 'help';
12297 * HelpTool.static.title = 'Help';
12298 * toolFactory.register( HelpTool );
12300 * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
12301 * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
12303 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
12307 * @extends OO.ui.Tool
12308 * @mixins OO.ui.mixin.PopupElement
12311 * @param {OO.ui.ToolGroup} toolGroup
12312 * @param {Object} [config] Configuration options
12314 OO
.ui
.PopupTool
= function OoUiPopupTool( toolGroup
, config
) {
12315 // Allow passing positional parameters inside the config object
12316 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
12317 config
= toolGroup
;
12318 toolGroup
= config
.toolGroup
;
12321 // Parent constructor
12322 OO
.ui
.PopupTool
.parent
.call( this, toolGroup
, config
);
12324 // Mixin constructors
12325 OO
.ui
.mixin
.PopupElement
.call( this, config
);
12329 .addClass( 'oo-ui-popupTool' )
12330 .append( this.popup
.$element
);
12335 OO
.inheritClass( OO
.ui
.PopupTool
, OO
.ui
.Tool
);
12336 OO
.mixinClass( OO
.ui
.PopupTool
, OO
.ui
.mixin
.PopupElement
);
12341 * Handle the tool being selected.
12345 OO
.ui
.PopupTool
.prototype.onSelect = function () {
12346 if ( !this.isDisabled() ) {
12347 this.popup
.toggle();
12349 this.setActive( false );
12354 * Handle the toolbar state being updated.
12358 OO
.ui
.PopupTool
.prototype.onUpdateState = function () {
12359 this.setActive( false );
12363 * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
12364 * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
12365 * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
12366 * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
12367 * when the ToolGroupTool is selected.
12369 * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
12371 * function SettingsTool() {
12372 * SettingsTool.parent.apply( this, arguments );
12374 * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
12375 * SettingsTool.static.name = 'settings';
12376 * SettingsTool.static.title = 'Change settings';
12377 * SettingsTool.static.groupConfig = {
12378 * icon: 'settings',
12379 * label: 'ToolGroupTool',
12380 * include: [ 'setting1', 'setting2' ]
12382 * toolFactory.register( SettingsTool );
12384 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
12386 * Please note that this implementation is subject to change per [T74159] [2].
12388 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
12389 * [2]: https://phabricator.wikimedia.org/T74159
12393 * @extends OO.ui.Tool
12396 * @param {OO.ui.ToolGroup} toolGroup
12397 * @param {Object} [config] Configuration options
12399 OO
.ui
.ToolGroupTool
= function OoUiToolGroupTool( toolGroup
, config
) {
12400 // Allow passing positional parameters inside the config object
12401 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
12402 config
= toolGroup
;
12403 toolGroup
= config
.toolGroup
;
12406 // Parent constructor
12407 OO
.ui
.ToolGroupTool
.parent
.call( this, toolGroup
, config
);
12410 this.innerToolGroup
= this.createGroup( this.constructor.static.groupConfig
);
12413 this.innerToolGroup
.connect( this, { disable
: 'onToolGroupDisable' } );
12416 this.$link
.remove();
12418 .addClass( 'oo-ui-toolGroupTool' )
12419 .append( this.innerToolGroup
.$element
);
12424 OO
.inheritClass( OO
.ui
.ToolGroupTool
, OO
.ui
.Tool
);
12426 /* Static Properties */
12429 * Toolgroup configuration.
12431 * The toolgroup configuration consists of the tools to include, as well as an icon and label
12432 * to use for the bar item. Tools can be included by symbolic name, group, or with the
12433 * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
12435 * @property {Object.<string,Array>}
12437 OO
.ui
.ToolGroupTool
.static.groupConfig
= {};
12442 * Handle the tool being selected.
12446 OO
.ui
.ToolGroupTool
.prototype.onSelect = function () {
12447 this.innerToolGroup
.setActive( !this.innerToolGroup
.active
);
12452 * Synchronize disabledness state of the tool with the inner toolgroup.
12455 * @param {boolean} disabled Element is disabled
12457 OO
.ui
.ToolGroupTool
.prototype.onToolGroupDisable = function ( disabled
) {
12458 this.setDisabled( disabled
);
12462 * Handle the toolbar state being updated.
12466 OO
.ui
.ToolGroupTool
.prototype.onUpdateState = function () {
12467 this.setActive( false );
12471 * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
12473 * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
12474 * more information.
12475 * @return {OO.ui.ListToolGroup}
12477 OO
.ui
.ToolGroupTool
.prototype.createGroup = function ( group
) {
12478 if ( group
.include
=== '*' ) {
12479 // Apply defaults to catch-all groups
12480 if ( group
.label
=== undefined ) {
12481 group
.label
= OO
.ui
.msg( 'ooui-toolbar-more' );
12485 return this.toolbar
.getToolGroupFactory().create( 'list', this.toolbar
, group
);
12489 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
12491 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
12496 * @extends OO.ui.mixin.GroupElement
12499 * @param {Object} [config] Configuration options
12501 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
12502 // Parent constructor
12503 OO
.ui
.mixin
.GroupWidget
.parent
.call( this, config
);
12508 OO
.inheritClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
12513 * Set the disabled state of the widget.
12515 * This will also update the disabled state of child widgets.
12517 * @param {boolean} disabled Disable widget
12520 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
12524 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
12525 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
12527 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
12528 if ( this.items
) {
12529 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
12530 this.items
[ i
].updateDisabled();
12538 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
12540 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
12541 * allows bidirectional communication.
12543 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
12551 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
12558 * Check if widget is disabled.
12560 * Checks parent if present, making disabled state inheritable.
12562 * @return {boolean} Widget is disabled
12564 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
12565 return this.disabled
||
12566 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
12570 * Set group element is in.
12572 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
12575 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
12577 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
12578 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
12580 // Initialize item disabled states
12581 this.updateDisabled();
12587 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
12588 * Controls include moving items up and down, removing items, and adding different kinds of items.
12590 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
12593 * @extends OO.ui.Widget
12594 * @mixins OO.ui.mixin.GroupElement
12595 * @mixins OO.ui.mixin.IconElement
12598 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
12599 * @param {Object} [config] Configuration options
12600 * @cfg {Object} [abilities] List of abilties
12601 * @cfg {boolean} [abilities.move=true] Allow moving movable items
12602 * @cfg {boolean} [abilities.remove=true] Allow removing removable items
12604 OO
.ui
.OutlineControlsWidget
= function OoUiOutlineControlsWidget( outline
, config
) {
12605 // Allow passing positional parameters inside the config object
12606 if ( OO
.isPlainObject( outline
) && config
=== undefined ) {
12608 outline
= config
.outline
;
12611 // Configuration initialization
12612 config
= $.extend( { icon
: 'add' }, config
);
12614 // Parent constructor
12615 OO
.ui
.OutlineControlsWidget
.parent
.call( this, config
);
12617 // Mixin constructors
12618 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12619 OO
.ui
.mixin
.IconElement
.call( this, config
);
12622 this.outline
= outline
;
12623 this.$movers
= $( '<div>' );
12624 this.upButton
= new OO
.ui
.ButtonWidget( {
12627 title
: OO
.ui
.msg( 'ooui-outline-control-move-up' )
12629 this.downButton
= new OO
.ui
.ButtonWidget( {
12632 title
: OO
.ui
.msg( 'ooui-outline-control-move-down' )
12634 this.removeButton
= new OO
.ui
.ButtonWidget( {
12637 title
: OO
.ui
.msg( 'ooui-outline-control-remove' )
12639 this.abilities
= { move: true, remove
: true };
12642 outline
.connect( this, {
12643 select
: 'onOutlineChange',
12644 add
: 'onOutlineChange',
12645 remove
: 'onOutlineChange'
12647 this.upButton
.connect( this, { click
: [ 'emit', 'move', -1 ] } );
12648 this.downButton
.connect( this, { click
: [ 'emit', 'move', 1 ] } );
12649 this.removeButton
.connect( this, { click
: [ 'emit', 'remove' ] } );
12652 this.$element
.addClass( 'oo-ui-outlineControlsWidget' );
12653 this.$group
.addClass( 'oo-ui-outlineControlsWidget-items' );
12655 .addClass( 'oo-ui-outlineControlsWidget-movers' )
12656 .append( this.removeButton
.$element
, this.upButton
.$element
, this.downButton
.$element
);
12657 this.$element
.append( this.$icon
, this.$group
, this.$movers
);
12658 this.setAbilities( config
.abilities
|| {} );
12663 OO
.inheritClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.Widget
);
12664 OO
.mixinClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.mixin
.GroupElement
);
12665 OO
.mixinClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.mixin
.IconElement
);
12671 * @param {number} places Number of places to move
12683 * @param {Object} abilities List of abilties
12684 * @param {boolean} [abilities.move] Allow moving movable items
12685 * @param {boolean} [abilities.remove] Allow removing removable items
12687 OO
.ui
.OutlineControlsWidget
.prototype.setAbilities = function ( abilities
) {
12690 for ( ability
in this.abilities
) {
12691 if ( abilities
[ ability
] !== undefined ) {
12692 this.abilities
[ ability
] = !!abilities
[ ability
];
12696 this.onOutlineChange();
12701 * Handle outline change events.
12703 OO
.ui
.OutlineControlsWidget
.prototype.onOutlineChange = function () {
12704 var i
, len
, firstMovable
, lastMovable
,
12705 items
= this.outline
.getItems(),
12706 selectedItem
= this.outline
.getSelectedItem(),
12707 movable
= this.abilities
.move && selectedItem
&& selectedItem
.isMovable(),
12708 removable
= this.abilities
.remove
&& selectedItem
&& selectedItem
.isRemovable();
12712 len
= items
.length
;
12713 while ( ++i
< len
) {
12714 if ( items
[ i
].isMovable() ) {
12715 firstMovable
= items
[ i
];
12721 if ( items
[ i
].isMovable() ) {
12722 lastMovable
= items
[ i
];
12727 this.upButton
.setDisabled( !movable
|| selectedItem
=== firstMovable
);
12728 this.downButton
.setDisabled( !movable
|| selectedItem
=== lastMovable
);
12729 this.removeButton
.setDisabled( !removable
);
12733 * ToggleWidget implements basic behavior of widgets with an on/off state.
12734 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
12738 * @extends OO.ui.Widget
12741 * @param {Object} [config] Configuration options
12742 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
12743 * By default, the toggle is in the 'off' state.
12745 OO
.ui
.ToggleWidget
= function OoUiToggleWidget( config
) {
12746 // Configuration initialization
12747 config
= config
|| {};
12749 // Parent constructor
12750 OO
.ui
.ToggleWidget
.parent
.call( this, config
);
12756 this.$element
.addClass( 'oo-ui-toggleWidget' );
12757 this.setValue( !!config
.value
);
12762 OO
.inheritClass( OO
.ui
.ToggleWidget
, OO
.ui
.Widget
);
12769 * A change event is emitted when the on/off state of the toggle changes.
12771 * @param {boolean} value Value representing the new state of the toggle
12777 * Get the value representing the toggle’s state.
12779 * @return {boolean} The on/off state of the toggle
12781 OO
.ui
.ToggleWidget
.prototype.getValue = function () {
12786 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
12788 * @param {boolean} value The state of the toggle
12792 OO
.ui
.ToggleWidget
.prototype.setValue = function ( value
) {
12794 if ( this.value
!== value
) {
12795 this.value
= value
;
12796 this.emit( 'change', value
);
12797 this.$element
.toggleClass( 'oo-ui-toggleWidget-on', value
);
12798 this.$element
.toggleClass( 'oo-ui-toggleWidget-off', !value
);
12799 this.$element
.attr( 'aria-checked', value
.toString() );
12805 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
12806 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
12807 * removed, and cleared from the group.
12810 * // Example: A ButtonGroupWidget with two buttons
12811 * var button1 = new OO.ui.PopupButtonWidget( {
12812 * label: 'Select a category',
12815 * $content: $( '<p>List of categories...</p>' ),
12820 * var button2 = new OO.ui.ButtonWidget( {
12821 * label: 'Add item'
12823 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
12824 * items: [button1, button2]
12826 * $( 'body' ).append( buttonGroup.$element );
12829 * @extends OO.ui.Widget
12830 * @mixins OO.ui.mixin.GroupElement
12833 * @param {Object} [config] Configuration options
12834 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
12836 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
12837 // Configuration initialization
12838 config
= config
|| {};
12840 // Parent constructor
12841 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
12843 // Mixin constructors
12844 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12847 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
12848 if ( Array
.isArray( config
.items
) ) {
12849 this.addItems( config
.items
);
12855 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
12856 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
12859 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
12860 * feels, and functionality can be customized via the class’s configuration options
12861 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
12864 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
12867 * // A button widget
12868 * var button = new OO.ui.ButtonWidget( {
12869 * label: 'Button with Icon',
12871 * iconTitle: 'Remove'
12873 * $( 'body' ).append( button.$element );
12875 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
12878 * @extends OO.ui.Widget
12879 * @mixins OO.ui.mixin.ButtonElement
12880 * @mixins OO.ui.mixin.IconElement
12881 * @mixins OO.ui.mixin.IndicatorElement
12882 * @mixins OO.ui.mixin.LabelElement
12883 * @mixins OO.ui.mixin.TitledElement
12884 * @mixins OO.ui.mixin.FlaggedElement
12885 * @mixins OO.ui.mixin.TabIndexedElement
12886 * @mixins OO.ui.mixin.AccessKeyedElement
12889 * @param {Object} [config] Configuration options
12890 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
12891 * @cfg {string} [target] The frame or window in which to open the hyperlink.
12892 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
12894 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
12895 // Configuration initialization
12896 config
= config
|| {};
12898 // Parent constructor
12899 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
12901 // Mixin constructors
12902 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
12903 OO
.ui
.mixin
.IconElement
.call( this, config
);
12904 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
12905 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12906 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
12907 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
12908 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
12909 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
12913 this.target
= null;
12914 this.noFollow
= false;
12917 this.connect( this, { disable
: 'onDisable' } );
12920 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
12922 .addClass( 'oo-ui-buttonWidget' )
12923 .append( this.$button
);
12924 this.setHref( config
.href
);
12925 this.setTarget( config
.target
);
12926 this.setNoFollow( config
.noFollow
);
12931 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
12932 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
12933 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
12934 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
12935 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
12936 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
12937 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
12938 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
12939 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
12946 OO
.ui
.ButtonWidget
.prototype.onMouseDown = function ( e
) {
12947 if ( !this.isDisabled() ) {
12948 // Remove the tab-index while the button is down to prevent the button from stealing focus
12949 this.$button
.removeAttr( 'tabindex' );
12952 return OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown
.call( this, e
);
12958 OO
.ui
.ButtonWidget
.prototype.onMouseUp = function ( e
) {
12959 if ( !this.isDisabled() ) {
12960 // Restore the tab-index after the button is up to restore the button's accessibility
12961 this.$button
.attr( 'tabindex', this.tabIndex
);
12964 return OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp
.call( this, e
);
12968 * Get hyperlink location.
12970 * @return {string} Hyperlink location
12972 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
12977 * Get hyperlink target.
12979 * @return {string} Hyperlink target
12981 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
12982 return this.target
;
12986 * Get search engine traversal hint.
12988 * @return {boolean} Whether search engines should avoid traversing this hyperlink
12990 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
12991 return this.noFollow
;
12995 * Set hyperlink location.
12997 * @param {string|null} href Hyperlink location, null to remove
12999 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
13000 href
= typeof href
=== 'string' ? href
: null;
13001 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
13002 href
= './' + href
;
13005 if ( href
!== this.href
) {
13014 * Update the `href` attribute, in case of changes to href or
13020 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
13021 if ( this.href
!== null && !this.isDisabled() ) {
13022 this.$button
.attr( 'href', this.href
);
13024 this.$button
.removeAttr( 'href' );
13031 * Handle disable events.
13034 * @param {boolean} disabled Element is disabled
13036 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
13041 * Set hyperlink target.
13043 * @param {string|null} target Hyperlink target, null to remove
13045 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
13046 target
= typeof target
=== 'string' ? target
: null;
13048 if ( target
!== this.target
) {
13049 this.target
= target
;
13050 if ( target
!== null ) {
13051 this.$button
.attr( 'target', target
);
13053 this.$button
.removeAttr( 'target' );
13061 * Set search engine traversal hint.
13063 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
13065 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
13066 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
13068 if ( noFollow
!== this.noFollow
) {
13069 this.noFollow
= noFollow
;
13071 this.$button
.attr( 'rel', 'nofollow' );
13073 this.$button
.removeAttr( 'rel' );
13081 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
13082 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
13085 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
13086 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
13089 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
13092 * @extends OO.ui.ButtonWidget
13093 * @mixins OO.ui.mixin.PendingElement
13096 * @param {Object} [config] Configuration options
13097 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
13098 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
13099 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
13100 * for more information about setting modes.
13101 * @cfg {boolean} [framed=false] Render the action button with a frame
13103 OO
.ui
.ActionWidget
= function OoUiActionWidget( config
) {
13104 // Configuration initialization
13105 config
= $.extend( { framed
: false }, config
);
13107 // Parent constructor
13108 OO
.ui
.ActionWidget
.parent
.call( this, config
);
13110 // Mixin constructors
13111 OO
.ui
.mixin
.PendingElement
.call( this, config
);
13114 this.action
= config
.action
|| '';
13115 this.modes
= config
.modes
|| [];
13120 this.$element
.addClass( 'oo-ui-actionWidget' );
13125 OO
.inheritClass( OO
.ui
.ActionWidget
, OO
.ui
.ButtonWidget
);
13126 OO
.mixinClass( OO
.ui
.ActionWidget
, OO
.ui
.mixin
.PendingElement
);
13131 * A resize event is emitted when the size of the widget changes.
13139 * Check if the action is configured to be available in the specified `mode`.
13141 * @param {string} mode Name of mode
13142 * @return {boolean} The action is configured with the mode
13144 OO
.ui
.ActionWidget
.prototype.hasMode = function ( mode
) {
13145 return this.modes
.indexOf( mode
) !== -1;
13149 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
13153 OO
.ui
.ActionWidget
.prototype.getAction = function () {
13154 return this.action
;
13158 * Get the symbolic name of the mode or modes for which the action is configured to be available.
13160 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
13161 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
13164 * @return {string[]}
13166 OO
.ui
.ActionWidget
.prototype.getModes = function () {
13167 return this.modes
.slice();
13171 * Emit a resize event if the size has changed.
13176 OO
.ui
.ActionWidget
.prototype.propagateResize = function () {
13179 if ( this.isElementAttached() ) {
13180 width
= this.$element
.width();
13181 height
= this.$element
.height();
13183 if ( width
!== this.width
|| height
!== this.height
) {
13184 this.width
= width
;
13185 this.height
= height
;
13186 this.emit( 'resize' );
13196 OO
.ui
.ActionWidget
.prototype.setIcon = function () {
13198 OO
.ui
.mixin
.IconElement
.prototype.setIcon
.apply( this, arguments
);
13199 this.propagateResize();
13207 OO
.ui
.ActionWidget
.prototype.setLabel = function () {
13209 OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.apply( this, arguments
);
13210 this.propagateResize();
13218 OO
.ui
.ActionWidget
.prototype.setFlags = function () {
13220 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags
.apply( this, arguments
);
13221 this.propagateResize();
13229 OO
.ui
.ActionWidget
.prototype.clearFlags = function () {
13231 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags
.apply( this, arguments
);
13232 this.propagateResize();
13238 * Toggle the visibility of the action button.
13240 * @param {boolean} [show] Show button, omit to toggle visibility
13243 OO
.ui
.ActionWidget
.prototype.toggle = function () {
13245 OO
.ui
.ActionWidget
.parent
.prototype.toggle
.apply( this, arguments
);
13246 this.propagateResize();
13252 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
13253 * which is used to display additional information or options.
13256 * // Example of a popup button.
13257 * var popupButton = new OO.ui.PopupButtonWidget( {
13258 * label: 'Popup button with options',
13261 * $content: $( '<p>Additional options here.</p>' ),
13263 * align: 'force-left'
13266 * // Append the button to the DOM.
13267 * $( 'body' ).append( popupButton.$element );
13270 * @extends OO.ui.ButtonWidget
13271 * @mixins OO.ui.mixin.PopupElement
13274 * @param {Object} [config] Configuration options
13276 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
13277 // Parent constructor
13278 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
13280 // Mixin constructors
13281 OO
.ui
.mixin
.PopupElement
.call( this, config
);
13284 this.connect( this, { click
: 'onAction' } );
13288 .addClass( 'oo-ui-popupButtonWidget' )
13289 .attr( 'aria-haspopup', 'true' )
13290 .append( this.popup
.$element
);
13295 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
13296 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
13301 * Handle the button action being triggered.
13305 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
13306 this.popup
.toggle();
13310 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
13311 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
13312 * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
13313 * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
13314 * and {@link OO.ui.mixin.LabelElement labels}. Please see
13315 * the [OOjs UI documentation][1] on MediaWiki for more information.
13318 * // Toggle buttons in the 'off' and 'on' state.
13319 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
13320 * label: 'Toggle Button off'
13322 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
13323 * label: 'Toggle Button on',
13326 * // Append the buttons to the DOM.
13327 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
13329 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
13332 * @extends OO.ui.ToggleWidget
13333 * @mixins OO.ui.mixin.ButtonElement
13334 * @mixins OO.ui.mixin.IconElement
13335 * @mixins OO.ui.mixin.IndicatorElement
13336 * @mixins OO.ui.mixin.LabelElement
13337 * @mixins OO.ui.mixin.TitledElement
13338 * @mixins OO.ui.mixin.FlaggedElement
13339 * @mixins OO.ui.mixin.TabIndexedElement
13342 * @param {Object} [config] Configuration options
13343 * @cfg {boolean} [value=false] The toggle button’s initial on/off
13344 * state. By default, the button is in the 'off' state.
13346 OO
.ui
.ToggleButtonWidget
= function OoUiToggleButtonWidget( config
) {
13347 // Configuration initialization
13348 config
= config
|| {};
13350 // Parent constructor
13351 OO
.ui
.ToggleButtonWidget
.parent
.call( this, config
);
13353 // Mixin constructors
13354 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
13355 OO
.ui
.mixin
.IconElement
.call( this, config
);
13356 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
13357 OO
.ui
.mixin
.LabelElement
.call( this, config
);
13358 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
13359 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
13360 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
13363 this.connect( this, { click
: 'onAction' } );
13366 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
13368 .addClass( 'oo-ui-toggleButtonWidget' )
13369 .append( this.$button
);
13374 OO
.inheritClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.ToggleWidget
);
13375 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
13376 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.IconElement
);
13377 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
13378 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.LabelElement
);
13379 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.TitledElement
);
13380 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
13381 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
13386 * Handle the button action being triggered.
13390 OO
.ui
.ToggleButtonWidget
.prototype.onAction = function () {
13391 this.setValue( !this.value
);
13397 OO
.ui
.ToggleButtonWidget
.prototype.setValue = function ( value
) {
13399 if ( value
!== this.value
) {
13400 // Might be called from parent constructor before ButtonElement constructor
13401 if ( this.$button
) {
13402 this.$button
.attr( 'aria-pressed', value
.toString() );
13404 this.setActive( value
);
13408 OO
.ui
.ToggleButtonWidget
.parent
.prototype.setValue
.call( this, value
);
13416 OO
.ui
.ToggleButtonWidget
.prototype.setButtonElement = function ( $button
) {
13417 if ( this.$button
) {
13418 this.$button
.removeAttr( 'aria-pressed' );
13420 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement
.call( this, $button
);
13421 this.$button
.attr( 'aria-pressed', this.value
.toString() );
13425 * CapsuleMultiSelectWidgets are something like a {@link OO.ui.ComboBoxInputWidget combo box widget}
13426 * that allows for selecting multiple values.
13428 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
13431 * // Example: A CapsuleMultiSelectWidget.
13432 * var capsule = new OO.ui.CapsuleMultiSelectWidget( {
13433 * label: 'CapsuleMultiSelectWidget',
13434 * selected: [ 'Option 1', 'Option 3' ],
13437 * new OO.ui.MenuOptionWidget( {
13438 * data: 'Option 1',
13439 * label: 'Option One'
13441 * new OO.ui.MenuOptionWidget( {
13442 * data: 'Option 2',
13443 * label: 'Option Two'
13445 * new OO.ui.MenuOptionWidget( {
13446 * data: 'Option 3',
13447 * label: 'Option Three'
13449 * new OO.ui.MenuOptionWidget( {
13450 * data: 'Option 4',
13451 * label: 'Option Four'
13453 * new OO.ui.MenuOptionWidget( {
13454 * data: 'Option 5',
13455 * label: 'Option Five'
13460 * $( 'body' ).append( capsule.$element );
13462 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
13465 * @extends OO.ui.Widget
13466 * @mixins OO.ui.mixin.TabIndexedElement
13467 * @mixins OO.ui.mixin.GroupElement
13470 * @param {Object} [config] Configuration options
13471 * @cfg {boolean} [allowArbitrary=false] Allow data items to be added even if not present in the menu.
13472 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
13473 * @cfg {Object} [popup] Configuration options to pass to the {@link OO.ui.PopupWidget popup widget}.
13474 * If specified, this popup will be shown instead of the menu (but the menu
13475 * will still be used for item labels and allowArbitrary=false). The widgets
13476 * in the popup should use this.addItemsFromData() or this.addItems() as necessary.
13477 * @cfg {jQuery} [$overlay] Render the menu or popup into a separate layer.
13478 * This configuration is useful in cases where the expanded menu is larger than
13479 * its containing `<div>`. The specified overlay layer is usually on top of
13480 * the containing `<div>` and has a larger area. By default, the menu uses
13481 * relative positioning.
13483 OO
.ui
.CapsuleMultiSelectWidget
= function OoUiCapsuleMultiSelectWidget( config
) {
13486 // Configuration initialization
13487 config
= config
|| {};
13489 // Parent constructor
13490 OO
.ui
.CapsuleMultiSelectWidget
.parent
.call( this, config
);
13492 // Properties (must be set before mixin constructor calls)
13493 this.$input
= config
.popup
? null : $( '<input>' );
13494 this.$handle
= $( '<div>' );
13496 // Mixin constructors
13497 OO
.ui
.mixin
.GroupElement
.call( this, config
);
13498 if ( config
.popup
) {
13499 config
.popup
= $.extend( {}, config
.popup
, {
13503 OO
.ui
.mixin
.PopupElement
.call( this, config
);
13504 $tabFocus
= $( '<span>' );
13505 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: $tabFocus
} ) );
13509 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
13511 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
13512 OO
.ui
.mixin
.IconElement
.call( this, config
);
13515 this.$content
= $( '<div>' );
13516 this.allowArbitrary
= !!config
.allowArbitrary
;
13517 this.$overlay
= config
.$overlay
|| this.$element
;
13518 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend(
13521 $input
: this.$input
,
13522 $container
: this.$element
,
13523 filterFromInput
: true,
13524 disabled
: this.isDisabled()
13530 if ( this.popup
) {
13532 focus
: this.onFocusForPopup
.bind( this )
13534 this.popup
.$element
.on( 'focusout', this.onPopupFocusOut
.bind( this ) );
13535 if ( this.popup
.$autoCloseIgnore
) {
13536 this.popup
.$autoCloseIgnore
.on( 'focusout', this.onPopupFocusOut
.bind( this ) );
13538 this.popup
.connect( this, {
13539 toggle: function ( visible
) {
13540 $tabFocus
.toggle( !visible
);
13545 focus
: this.onInputFocus
.bind( this ),
13546 blur
: this.onInputBlur
.bind( this ),
13547 'propertychange change click mouseup keydown keyup input cut paste select focus':
13548 OO
.ui
.debounce( this.updateInputSize
.bind( this ) ),
13549 keydown
: this.onKeyDown
.bind( this ),
13550 keypress
: this.onKeyPress
.bind( this )
13553 this.menu
.connect( this, {
13554 choose
: 'onMenuChoose',
13555 add
: 'onMenuItemsChange',
13556 remove
: 'onMenuItemsChange'
13559 mousedown
: this.onMouseDown
.bind( this )
13563 if ( this.$input
) {
13564 this.$input
.prop( 'disabled', this.isDisabled() );
13565 this.$input
.attr( {
13567 'aria-autocomplete': 'list'
13569 this.updateInputSize();
13571 if ( config
.data
) {
13572 this.setItemsFromData( config
.data
);
13574 this.$content
.addClass( 'oo-ui-capsuleMultiSelectWidget-content' )
13575 .append( this.$group
);
13576 this.$group
.addClass( 'oo-ui-capsuleMultiSelectWidget-group' );
13577 this.$handle
.addClass( 'oo-ui-capsuleMultiSelectWidget-handle' )
13578 .append( this.$indicator
, this.$icon
, this.$content
);
13579 this.$element
.addClass( 'oo-ui-capsuleMultiSelectWidget' )
13580 .append( this.$handle
);
13581 if ( this.popup
) {
13582 this.$content
.append( $tabFocus
);
13583 this.$overlay
.append( this.popup
.$element
);
13585 this.$content
.append( this.$input
);
13586 this.$overlay
.append( this.menu
.$element
);
13588 this.onMenuItemsChange();
13593 OO
.inheritClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.Widget
);
13594 OO
.mixinClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.mixin
.GroupElement
);
13595 OO
.mixinClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.mixin
.PopupElement
);
13596 OO
.mixinClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
13597 OO
.mixinClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.mixin
.IndicatorElement
);
13598 OO
.mixinClass( OO
.ui
.CapsuleMultiSelectWidget
, OO
.ui
.mixin
.IconElement
);
13605 * A change event is emitted when the set of selected items changes.
13607 * @param {Mixed[]} datas Data of the now-selected items
13613 * Construct a OO.ui.CapsuleItemWidget (or a subclass thereof) from given label and data.
13616 * @param {Mixed} data Custom data of any type.
13617 * @param {string} label The label text.
13618 * @return {OO.ui.CapsuleItemWidget}
13620 OO
.ui
.CapsuleMultiSelectWidget
.prototype.createItemWidget = function ( data
, label
) {
13621 return new OO
.ui
.CapsuleItemWidget( { data
: data
, label
: label
} );
13625 * Get the data of the items in the capsule
13626 * @return {Mixed[]}
13628 OO
.ui
.CapsuleMultiSelectWidget
.prototype.getItemsData = function () {
13629 return $.map( this.getItems(), function ( e
) { return e
.data
; } );
13633 * Set the items in the capsule by providing data
13635 * @param {Mixed[]} datas
13636 * @return {OO.ui.CapsuleMultiSelectWidget}
13638 OO
.ui
.CapsuleMultiSelectWidget
.prototype.setItemsFromData = function ( datas
) {
13641 items
= this.getItems();
13643 $.each( datas
, function ( i
, data
) {
13645 item
= menu
.getItemFromData( data
);
13648 label
= item
.label
;
13649 } else if ( widget
.allowArbitrary
) {
13650 label
= String( data
);
13656 for ( j
= 0; j
< items
.length
; j
++ ) {
13657 if ( items
[ j
].data
=== data
&& items
[ j
].label
=== label
) {
13659 items
.splice( j
, 1 );
13664 item
= widget
.createItemWidget( data
, label
);
13666 widget
.addItems( [ item
], i
);
13669 if ( items
.length
) {
13670 widget
.removeItems( items
);
13677 * Add items to the capsule by providing their data
13679 * @param {Mixed[]} datas
13680 * @return {OO.ui.CapsuleMultiSelectWidget}
13682 OO
.ui
.CapsuleMultiSelectWidget
.prototype.addItemsFromData = function ( datas
) {
13687 $.each( datas
, function ( i
, data
) {
13690 if ( !widget
.getItemFromData( data
) ) {
13691 item
= menu
.getItemFromData( data
);
13693 items
.push( widget
.createItemWidget( data
, item
.label
) );
13694 } else if ( widget
.allowArbitrary
) {
13695 items
.push( widget
.createItemWidget( data
, String( data
) ) );
13700 if ( items
.length
) {
13701 this.addItems( items
);
13708 * Remove items by data
13710 * @param {Mixed[]} datas
13711 * @return {OO.ui.CapsuleMultiSelectWidget}
13713 OO
.ui
.CapsuleMultiSelectWidget
.prototype.removeItemsFromData = function ( datas
) {
13717 $.each( datas
, function ( i
, data
) {
13718 var item
= widget
.getItemFromData( data
);
13720 items
.push( item
);
13724 if ( items
.length
) {
13725 this.removeItems( items
);
13734 OO
.ui
.CapsuleMultiSelectWidget
.prototype.addItems = function ( items
) {
13736 oldItems
= this.items
.slice();
13738 OO
.ui
.mixin
.GroupElement
.prototype.addItems
.call( this, items
);
13740 if ( this.items
.length
!== oldItems
.length
) {
13744 for ( i
= 0, l
= oldItems
.length
; same
&& i
< l
; i
++ ) {
13745 same
= same
&& this.items
[ i
] === oldItems
[ i
];
13749 this.emit( 'change', this.getItemsData() );
13750 this.menu
.position();
13759 OO
.ui
.CapsuleMultiSelectWidget
.prototype.removeItems = function ( items
) {
13761 oldItems
= this.items
.slice();
13763 OO
.ui
.mixin
.GroupElement
.prototype.removeItems
.call( this, items
);
13765 if ( this.items
.length
!== oldItems
.length
) {
13769 for ( i
= 0, l
= oldItems
.length
; same
&& i
< l
; i
++ ) {
13770 same
= same
&& this.items
[ i
] === oldItems
[ i
];
13774 this.emit( 'change', this.getItemsData() );
13775 this.menu
.position();
13784 OO
.ui
.CapsuleMultiSelectWidget
.prototype.clearItems = function () {
13785 if ( this.items
.length
) {
13786 OO
.ui
.mixin
.GroupElement
.prototype.clearItems
.call( this );
13787 this.emit( 'change', this.getItemsData() );
13788 this.menu
.position();
13794 * Get the capsule widget's menu.
13795 * @return {OO.ui.MenuSelectWidget} Menu widget
13797 OO
.ui
.CapsuleMultiSelectWidget
.prototype.getMenu = function () {
13802 * Handle focus events
13805 * @param {jQuery.Event} event
13807 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onInputFocus = function () {
13808 if ( !this.isDisabled() ) {
13809 this.menu
.toggle( true );
13814 * Handle blur events
13817 * @param {jQuery.Event} event
13819 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onInputBlur = function () {
13820 if ( this.allowArbitrary
&& this.$input
.val().trim() !== '' ) {
13821 this.addItemsFromData( [ this.$input
.val() ] );
13827 * Handle focus events
13830 * @param {jQuery.Event} event
13832 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onFocusForPopup = function () {
13833 if ( !this.isDisabled() ) {
13834 this.popup
.setSize( this.$handle
.width() );
13835 this.popup
.toggle( true );
13836 this.popup
.$element
.find( '*' )
13837 .filter( function () { return OO
.ui
.isFocusableElement( $( this ), true ); } )
13844 * Handles popup focus out events.
13847 * @param {Event} e Focus out event
13849 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onPopupFocusOut = function () {
13850 var widget
= this.popup
;
13852 setTimeout( function () {
13854 widget
.isVisible() &&
13855 !OO
.ui
.contains( widget
.$element
[ 0 ], document
.activeElement
, true ) &&
13856 ( !widget
.$autoCloseIgnore
|| !widget
.$autoCloseIgnore
.has( document
.activeElement
).length
)
13858 widget
.toggle( false );
13864 * Handle mouse down events.
13867 * @param {jQuery.Event} e Mouse down event
13869 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onMouseDown = function ( e
) {
13870 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
13874 this.updateInputSize();
13879 * Handle key press events.
13882 * @param {jQuery.Event} e Key press event
13884 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onKeyPress = function ( e
) {
13887 if ( !this.isDisabled() ) {
13888 if ( e
.which
=== OO
.ui
.Keys
.ESCAPE
) {
13893 if ( !this.popup
) {
13894 this.menu
.toggle( true );
13895 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
13896 item
= this.menu
.getItemFromLabel( this.$input
.val(), true );
13898 this.addItemsFromData( [ item
.data
] );
13900 } else if ( this.allowArbitrary
&& this.$input
.val().trim() !== '' ) {
13901 this.addItemsFromData( [ this.$input
.val() ] );
13907 // Make sure the input gets resized.
13908 setTimeout( this.updateInputSize
.bind( this ), 0 );
13914 * Handle key down events.
13917 * @param {jQuery.Event} e Key down event
13919 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onKeyDown = function ( e
) {
13920 if ( !this.isDisabled() ) {
13921 // 'keypress' event is not triggered for Backspace
13922 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.$input
.val() === '' ) {
13923 if ( this.items
.length
) {
13924 this.removeItems( this.items
.slice( -1 ) );
13932 * Update the dimensions of the text input field to encompass all available area.
13935 * @param {jQuery.Event} e Event of some sort
13937 OO
.ui
.CapsuleMultiSelectWidget
.prototype.updateInputSize = function () {
13938 var $lastItem
, direction
, contentWidth
, currentWidth
, bestWidth
;
13939 if ( !this.isDisabled() ) {
13940 this.$input
.css( 'width', '1em' );
13941 $lastItem
= this.$group
.children().last();
13942 direction
= OO
.ui
.Element
.static.getDir( this.$handle
);
13943 contentWidth
= this.$input
[ 0 ].scrollWidth
;
13944 currentWidth
= this.$input
.width();
13946 if ( contentWidth
< currentWidth
) {
13947 // All is fine, don't perform expensive calculations
13951 if ( !$lastItem
.length
) {
13952 bestWidth
= this.$content
.innerWidth();
13954 bestWidth
= direction
=== 'ltr' ?
13955 this.$content
.innerWidth() - $lastItem
.position().left
- $lastItem
.outerWidth() :
13956 $lastItem
.position().left
;
13958 // Some safety margin for sanity, because I *really* don't feel like finding out where the few
13959 // pixels this is off by are coming from.
13961 if ( contentWidth
> bestWidth
) {
13962 // This will result in the input getting shifted to the next line
13963 bestWidth
= this.$content
.innerWidth() - 10;
13965 this.$input
.width( Math
.floor( bestWidth
) );
13967 this.menu
.position();
13972 * Handle menu choose events.
13975 * @param {OO.ui.OptionWidget} item Chosen item
13977 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onMenuChoose = function ( item
) {
13978 if ( item
&& item
.isVisible() ) {
13979 this.addItemsFromData( [ item
.getData() ] );
13985 * Handle menu item change events.
13989 OO
.ui
.CapsuleMultiSelectWidget
.prototype.onMenuItemsChange = function () {
13990 this.setItemsFromData( this.getItemsData() );
13991 this.$element
.toggleClass( 'oo-ui-capsuleMultiSelectWidget-empty', this.menu
.isEmpty() );
13995 * Clear the input field
13998 OO
.ui
.CapsuleMultiSelectWidget
.prototype.clearInput = function () {
13999 if ( this.$input
) {
14000 this.$input
.val( '' );
14001 this.updateInputSize();
14003 if ( this.popup
) {
14004 this.popup
.toggle( false );
14006 this.menu
.toggle( false );
14007 this.menu
.selectItem();
14008 this.menu
.highlightItem();
14014 OO
.ui
.CapsuleMultiSelectWidget
.prototype.setDisabled = function ( disabled
) {
14018 OO
.ui
.CapsuleMultiSelectWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
14020 if ( this.$input
) {
14021 this.$input
.prop( 'disabled', this.isDisabled() );
14024 this.menu
.setDisabled( this.isDisabled() );
14026 if ( this.popup
) {
14027 this.popup
.setDisabled( this.isDisabled() );
14030 if ( this.items
) {
14031 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
14032 this.items
[ i
].updateDisabled();
14042 * @return {OO.ui.CapsuleMultiSelectWidget}
14044 OO
.ui
.CapsuleMultiSelectWidget
.prototype.focus = function () {
14045 if ( !this.isDisabled() ) {
14046 if ( this.popup
) {
14047 this.popup
.setSize( this.$handle
.width() );
14048 this.popup
.toggle( true );
14049 this.popup
.$element
.find( '*' )
14050 .filter( function () { return OO
.ui
.isFocusableElement( $( this ), true ); } )
14054 this.updateInputSize();
14055 this.menu
.toggle( true );
14056 this.$input
.focus();
14063 * CapsuleItemWidgets are used within a {@link OO.ui.CapsuleMultiSelectWidget
14064 * CapsuleMultiSelectWidget} to display the selected items.
14067 * @extends OO.ui.Widget
14068 * @mixins OO.ui.mixin.ItemWidget
14069 * @mixins OO.ui.mixin.IndicatorElement
14070 * @mixins OO.ui.mixin.LabelElement
14071 * @mixins OO.ui.mixin.FlaggedElement
14072 * @mixins OO.ui.mixin.TabIndexedElement
14075 * @param {Object} [config] Configuration options
14077 OO
.ui
.CapsuleItemWidget
= function OoUiCapsuleItemWidget( config
) {
14078 // Configuration initialization
14079 config
= config
|| {};
14081 // Parent constructor
14082 OO
.ui
.CapsuleItemWidget
.parent
.call( this, config
);
14084 // Properties (must be set before mixin constructor calls)
14085 this.$indicator
= $( '<span>' );
14087 // Mixin constructors
14088 OO
.ui
.mixin
.ItemWidget
.call( this );
14089 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$indicator
, indicator
: 'clear' } ) );
14090 OO
.ui
.mixin
.LabelElement
.call( this, config
);
14091 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
14092 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$indicator
} ) );
14095 this.$indicator
.on( {
14096 keydown
: this.onCloseKeyDown
.bind( this ),
14097 click
: this.onCloseClick
.bind( this )
14102 .addClass( 'oo-ui-capsuleItemWidget' )
14103 .append( this.$indicator
, this.$label
);
14108 OO
.inheritClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.Widget
);
14109 OO
.mixinClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.mixin
.ItemWidget
);
14110 OO
.mixinClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.mixin
.IndicatorElement
);
14111 OO
.mixinClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.mixin
.LabelElement
);
14112 OO
.mixinClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.mixin
.FlaggedElement
);
14113 OO
.mixinClass( OO
.ui
.CapsuleItemWidget
, OO
.ui
.mixin
.TabIndexedElement
);
14118 * Handle close icon clicks
14119 * @param {jQuery.Event} event
14121 OO
.ui
.CapsuleItemWidget
.prototype.onCloseClick = function () {
14122 var element
= this.getElementGroup();
14124 if ( !this.isDisabled() && element
&& $.isFunction( element
.removeItems
) ) {
14125 element
.removeItems( [ this ] );
14131 * Handle close keyboard events
14132 * @param {jQuery.Event} event Key down event
14134 OO
.ui
.CapsuleItemWidget
.prototype.onCloseKeyDown = function ( e
) {
14135 if ( !this.isDisabled() && $.isFunction( this.getElementGroup().removeItems
) ) {
14136 switch ( e
.which
) {
14137 case OO
.ui
.Keys
.ENTER
:
14138 case OO
.ui
.Keys
.BACKSPACE
:
14139 case OO
.ui
.Keys
.SPACE
:
14140 this.getElementGroup().removeItems( [ this ] );
14147 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
14148 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
14149 * users can interact with it.
14151 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
14152 * OO.ui.DropdownInputWidget instead.
14155 * // Example: A DropdownWidget with a menu that contains three options
14156 * var dropDown = new OO.ui.DropdownWidget( {
14157 * label: 'Dropdown menu: Select a menu option',
14160 * new OO.ui.MenuOptionWidget( {
14164 * new OO.ui.MenuOptionWidget( {
14168 * new OO.ui.MenuOptionWidget( {
14176 * $( 'body' ).append( dropDown.$element );
14178 * dropDown.getMenu().selectItemByData( 'b' );
14180 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
14182 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
14184 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
14187 * @extends OO.ui.Widget
14188 * @mixins OO.ui.mixin.IconElement
14189 * @mixins OO.ui.mixin.IndicatorElement
14190 * @mixins OO.ui.mixin.LabelElement
14191 * @mixins OO.ui.mixin.TitledElement
14192 * @mixins OO.ui.mixin.TabIndexedElement
14195 * @param {Object} [config] Configuration options
14196 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
14197 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
14198 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
14199 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
14201 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
14202 // Configuration initialization
14203 config
= $.extend( { indicator
: 'down' }, config
);
14205 // Parent constructor
14206 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
14208 // Properties (must be set before TabIndexedElement constructor call)
14209 this.$handle
= this.$( '<span>' );
14210 this.$overlay
= config
.$overlay
|| this.$element
;
14212 // Mixin constructors
14213 OO
.ui
.mixin
.IconElement
.call( this, config
);
14214 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
14215 OO
.ui
.mixin
.LabelElement
.call( this, config
);
14216 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
14217 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
14220 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend( {
14222 $container
: this.$element
14223 }, config
.menu
) );
14227 click
: this.onClick
.bind( this ),
14228 keypress
: this.onKeyPress
.bind( this )
14230 this.menu
.connect( this, { select
: 'onMenuSelect' } );
14234 .addClass( 'oo-ui-dropdownWidget-handle' )
14235 .append( this.$icon
, this.$label
, this.$indicator
);
14237 .addClass( 'oo-ui-dropdownWidget' )
14238 .append( this.$handle
);
14239 this.$overlay
.append( this.menu
.$element
);
14244 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
14245 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
14246 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
14247 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
14248 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
14249 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
14256 * @return {OO.ui.MenuSelectWidget} Menu of widget
14258 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
14263 * Handles menu select events.
14266 * @param {OO.ui.MenuOptionWidget} item Selected menu item
14268 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
14272 this.setLabel( null );
14276 selectedLabel
= item
.getLabel();
14278 // If the label is a DOM element, clone it, because setLabel will append() it
14279 if ( selectedLabel
instanceof jQuery
) {
14280 selectedLabel
= selectedLabel
.clone();
14283 this.setLabel( selectedLabel
);
14287 * Handle mouse click events.
14290 * @param {jQuery.Event} e Mouse click event
14292 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
14293 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
14294 this.menu
.toggle();
14300 * Handle key press events.
14303 * @param {jQuery.Event} e Key press event
14305 OO
.ui
.DropdownWidget
.prototype.onKeyPress = function ( e
) {
14306 if ( !this.isDisabled() &&
14307 ( ( e
.which
=== OO
.ui
.Keys
.SPACE
&& !this.menu
.isVisible() ) || e
.which
=== OO
.ui
.Keys
.ENTER
)
14309 this.menu
.toggle();
14315 * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
14316 * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
14317 * OO.ui.mixin.IndicatorElement indicators}.
14318 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
14321 * // Example of a file select widget
14322 * var selectFile = new OO.ui.SelectFileWidget();
14323 * $( 'body' ).append( selectFile.$element );
14325 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
14328 * @extends OO.ui.Widget
14329 * @mixins OO.ui.mixin.IconElement
14330 * @mixins OO.ui.mixin.IndicatorElement
14331 * @mixins OO.ui.mixin.PendingElement
14332 * @mixins OO.ui.mixin.LabelElement
14335 * @param {Object} [config] Configuration options
14336 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
14337 * @cfg {string} [placeholder] Text to display when no file is selected.
14338 * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
14339 * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
14340 * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be true.
14341 * @cfg {boolean} [dragDropUI=false] Deprecated alias for showDropTarget
14343 OO
.ui
.SelectFileWidget
= function OoUiSelectFileWidget( config
) {
14346 // TODO: Remove in next release
14347 if ( config
&& config
.dragDropUI
) {
14348 config
.showDropTarget
= true;
14351 // Configuration initialization
14352 config
= $.extend( {
14354 placeholder
: OO
.ui
.msg( 'ooui-selectfile-placeholder' ),
14355 notsupported
: OO
.ui
.msg( 'ooui-selectfile-not-supported' ),
14357 showDropTarget
: false
14360 // Parent constructor
14361 OO
.ui
.SelectFileWidget
.parent
.call( this, config
);
14363 // Mixin constructors
14364 OO
.ui
.mixin
.IconElement
.call( this, config
);
14365 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
14366 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$info
} ) );
14367 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { autoFitLabel
: true } ) );
14370 this.$info
= $( '<span>' );
14373 this.showDropTarget
= config
.showDropTarget
;
14374 this.isSupported
= this.constructor.static.isSupported();
14375 this.currentFile
= null;
14376 if ( Array
.isArray( config
.accept
) ) {
14377 this.accept
= config
.accept
;
14379 this.accept
= null;
14381 this.placeholder
= config
.placeholder
;
14382 this.notsupported
= config
.notsupported
;
14383 this.onFileSelectedHandler
= this.onFileSelected
.bind( this );
14385 this.selectButton
= new OO
.ui
.ButtonWidget( {
14386 classes
: [ 'oo-ui-selectFileWidget-selectButton' ],
14387 label
: OO
.ui
.msg( 'ooui-selectfile-button-select' ),
14388 disabled
: this.disabled
|| !this.isSupported
14391 this.clearButton
= new OO
.ui
.ButtonWidget( {
14392 classes
: [ 'oo-ui-selectFileWidget-clearButton' ],
14395 disabled
: this.disabled
14399 this.selectButton
.$button
.on( {
14400 keypress
: this.onKeyPress
.bind( this )
14402 this.clearButton
.connect( this, {
14403 click
: 'onClearClick'
14405 if ( config
.droppable
) {
14406 dragHandler
= this.onDragEnterOrOver
.bind( this );
14407 this.$element
.on( {
14408 dragenter
: dragHandler
,
14409 dragover
: dragHandler
,
14410 dragleave
: this.onDragLeave
.bind( this ),
14411 drop
: this.onDrop
.bind( this )
14418 this.$label
.addClass( 'oo-ui-selectFileWidget-label' );
14420 .addClass( 'oo-ui-selectFileWidget-info' )
14421 .append( this.$icon
, this.$label
, this.clearButton
.$element
, this.$indicator
);
14423 .addClass( 'oo-ui-selectFileWidget' )
14424 .append( this.$info
, this.selectButton
.$element
);
14425 if ( config
.droppable
&& config
.showDropTarget
) {
14426 this.$dropTarget
= $( '<div>' )
14427 .addClass( 'oo-ui-selectFileWidget-dropTarget' )
14428 .text( OO
.ui
.msg( 'ooui-selectfile-dragdrop-placeholder' ) )
14430 click
: this.onDropTargetClick
.bind( this )
14432 this.$element
.prepend( this.$dropTarget
);
14438 OO
.inheritClass( OO
.ui
.SelectFileWidget
, OO
.ui
.Widget
);
14439 OO
.mixinClass( OO
.ui
.SelectFileWidget
, OO
.ui
.mixin
.IconElement
);
14440 OO
.mixinClass( OO
.ui
.SelectFileWidget
, OO
.ui
.mixin
.IndicatorElement
);
14441 OO
.mixinClass( OO
.ui
.SelectFileWidget
, OO
.ui
.mixin
.PendingElement
);
14442 OO
.mixinClass( OO
.ui
.SelectFileWidget
, OO
.ui
.mixin
.LabelElement
);
14444 /* Static Properties */
14447 * Check if this widget is supported
14450 * @return {boolean}
14452 OO
.ui
.SelectFileWidget
.static.isSupported = function () {
14454 if ( OO
.ui
.SelectFileWidget
.static.isSupportedCache
=== null ) {
14455 $input
= $( '<input type="file">' );
14456 OO
.ui
.SelectFileWidget
.static.isSupportedCache
= $input
[ 0 ].files
!== undefined;
14458 return OO
.ui
.SelectFileWidget
.static.isSupportedCache
;
14461 OO
.ui
.SelectFileWidget
.static.isSupportedCache
= null;
14468 * A change event is emitted when the on/off state of the toggle changes.
14470 * @param {File|null} value New value
14476 * Get the current value of the field
14478 * @return {File|null}
14480 OO
.ui
.SelectFileWidget
.prototype.getValue = function () {
14481 return this.currentFile
;
14485 * Set the current value of the field
14487 * @param {File|null} file File to select
14489 OO
.ui
.SelectFileWidget
.prototype.setValue = function ( file
) {
14490 if ( this.currentFile
!== file
) {
14491 this.currentFile
= file
;
14493 this.emit( 'change', this.currentFile
);
14498 * Focus the widget.
14500 * Focusses the select file button.
14504 OO
.ui
.SelectFileWidget
.prototype.focus = function () {
14505 this.selectButton
.$button
[ 0 ].focus();
14510 * Update the user interface when a file is selected or unselected
14514 OO
.ui
.SelectFileWidget
.prototype.updateUI = function () {
14516 if ( !this.isSupported
) {
14517 this.$element
.addClass( 'oo-ui-selectFileWidget-notsupported' );
14518 this.$element
.removeClass( 'oo-ui-selectFileWidget-empty' );
14519 this.setLabel( this.notsupported
);
14521 this.$element
.addClass( 'oo-ui-selectFileWidget-supported' );
14522 if ( this.currentFile
) {
14523 this.$element
.removeClass( 'oo-ui-selectFileWidget-empty' );
14525 $label
= $label
.add(
14527 .addClass( 'oo-ui-selectFileWidget-fileName' )
14528 .text( this.currentFile
.name
)
14530 if ( this.currentFile
.type
!== '' ) {
14531 $label
= $label
.add(
14533 .addClass( 'oo-ui-selectFileWidget-fileType' )
14534 .text( this.currentFile
.type
)
14537 this.setLabel( $label
);
14539 this.$element
.addClass( 'oo-ui-selectFileWidget-empty' );
14540 this.setLabel( this.placeholder
);
14546 * Add the input to the widget
14550 OO
.ui
.SelectFileWidget
.prototype.addInput = function () {
14551 if ( this.$input
) {
14552 this.$input
.remove();
14555 if ( !this.isSupported
) {
14556 this.$input
= null;
14560 this.$input
= $( '<input type="file">' );
14561 this.$input
.on( 'change', this.onFileSelectedHandler
);
14562 this.$input
.attr( {
14565 if ( this.accept
) {
14566 this.$input
.attr( 'accept', this.accept
.join( ', ' ) );
14568 this.selectButton
.$button
.append( this.$input
);
14572 * Determine if we should accept this file
14575 * @param {string} File MIME type
14576 * @return {boolean}
14578 OO
.ui
.SelectFileWidget
.prototype.isAllowedType = function ( mimeType
) {
14581 if ( !this.accept
|| !mimeType
) {
14585 for ( i
= 0; i
< this.accept
.length
; i
++ ) {
14586 mimeTest
= this.accept
[ i
];
14587 if ( mimeTest
=== mimeType
) {
14589 } else if ( mimeTest
.substr( -2 ) === '/*' ) {
14590 mimeTest
= mimeTest
.substr( 0, mimeTest
.length
- 1 );
14591 if ( mimeType
.substr( 0, mimeTest
.length
) === mimeTest
) {
14601 * Handle file selection from the input
14604 * @param {jQuery.Event} e
14606 OO
.ui
.SelectFileWidget
.prototype.onFileSelected = function ( e
) {
14607 var file
= OO
.getProp( e
.target
, 'files', 0 ) || null;
14609 if ( file
&& !this.isAllowedType( file
.type
) ) {
14613 this.setValue( file
);
14618 * Handle clear button click events.
14622 OO
.ui
.SelectFileWidget
.prototype.onClearClick = function () {
14623 this.setValue( null );
14628 * Handle key press events.
14631 * @param {jQuery.Event} e Key press event
14633 OO
.ui
.SelectFileWidget
.prototype.onKeyPress = function ( e
) {
14634 if ( this.isSupported
&& !this.isDisabled() && this.$input
&&
14635 ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
14637 this.$input
.click();
14643 * Handle drop target click events.
14646 * @param {jQuery.Event} e Key press event
14648 OO
.ui
.SelectFileWidget
.prototype.onDropTargetClick = function () {
14649 if ( this.isSupported
&& !this.isDisabled() && this.$input
) {
14650 this.$input
.click();
14656 * Handle drag enter and over events
14659 * @param {jQuery.Event} e Drag event
14661 OO
.ui
.SelectFileWidget
.prototype.onDragEnterOrOver = function ( e
) {
14663 droppableFile
= false,
14664 dt
= e
.originalEvent
.dataTransfer
;
14666 e
.preventDefault();
14667 e
.stopPropagation();
14669 if ( this.isDisabled() || !this.isSupported
) {
14670 this.$element
.removeClass( 'oo-ui-selectFileWidget-canDrop' );
14671 dt
.dropEffect
= 'none';
14675 // DataTransferItem and File both have a type property, but in Chrome files
14676 // have no information at this point.
14677 itemOrFile
= OO
.getProp( dt
, 'items', 0 ) || OO
.getProp( dt
, 'files', 0 );
14678 if ( itemOrFile
) {
14679 if ( this.isAllowedType( itemOrFile
.type
) ) {
14680 droppableFile
= true;
14682 // dt.types is Array-like, but not an Array
14683 } else if ( Array
.prototype.indexOf
.call( OO
.getProp( dt
, 'types' ) || [], 'Files' ) !== -1 ) {
14684 // File information is not available at this point for security so just assume
14685 // it is acceptable for now.
14686 // https://bugzilla.mozilla.org/show_bug.cgi?id=640534
14687 droppableFile
= true;
14690 this.$element
.toggleClass( 'oo-ui-selectFileWidget-canDrop', droppableFile
);
14691 if ( !droppableFile
) {
14692 dt
.dropEffect
= 'none';
14699 * Handle drag leave events
14702 * @param {jQuery.Event} e Drag event
14704 OO
.ui
.SelectFileWidget
.prototype.onDragLeave = function () {
14705 this.$element
.removeClass( 'oo-ui-selectFileWidget-canDrop' );
14709 * Handle drop events
14712 * @param {jQuery.Event} e Drop event
14714 OO
.ui
.SelectFileWidget
.prototype.onDrop = function ( e
) {
14716 dt
= e
.originalEvent
.dataTransfer
;
14718 e
.preventDefault();
14719 e
.stopPropagation();
14720 this.$element
.removeClass( 'oo-ui-selectFileWidget-canDrop' );
14722 if ( this.isDisabled() || !this.isSupported
) {
14726 file
= OO
.getProp( dt
, 'files', 0 );
14727 if ( file
&& !this.isAllowedType( file
.type
) ) {
14731 this.setValue( file
);
14740 OO
.ui
.SelectFileWidget
.prototype.setDisabled = function ( disabled
) {
14741 OO
.ui
.SelectFileWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
14742 if ( this.selectButton
) {
14743 this.selectButton
.setDisabled( disabled
);
14745 if ( this.clearButton
) {
14746 this.clearButton
.setDisabled( disabled
);
14752 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
14753 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
14754 * for a list of icons included in the library.
14757 * // An icon widget with a label
14758 * var myIcon = new OO.ui.IconWidget( {
14760 * iconTitle: 'Help'
14762 * // Create a label.
14763 * var iconLabel = new OO.ui.LabelWidget( {
14766 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
14768 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
14771 * @extends OO.ui.Widget
14772 * @mixins OO.ui.mixin.IconElement
14773 * @mixins OO.ui.mixin.TitledElement
14774 * @mixins OO.ui.mixin.FlaggedElement
14777 * @param {Object} [config] Configuration options
14779 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
14780 // Configuration initialization
14781 config
= config
|| {};
14783 // Parent constructor
14784 OO
.ui
.IconWidget
.parent
.call( this, config
);
14786 // Mixin constructors
14787 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
14788 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
14789 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
14792 this.$element
.addClass( 'oo-ui-iconWidget' );
14797 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
14798 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
14799 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
14800 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
14802 /* Static Properties */
14804 OO
.ui
.IconWidget
.static.tagName
= 'span';
14807 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
14808 * attention to the status of an item or to clarify the function of a control. For a list of
14809 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
14812 * // Example of an indicator widget
14813 * var indicator1 = new OO.ui.IndicatorWidget( {
14814 * indicator: 'alert'
14817 * // Create a fieldset layout to add a label
14818 * var fieldset = new OO.ui.FieldsetLayout();
14819 * fieldset.addItems( [
14820 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
14822 * $( 'body' ).append( fieldset.$element );
14824 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
14827 * @extends OO.ui.Widget
14828 * @mixins OO.ui.mixin.IndicatorElement
14829 * @mixins OO.ui.mixin.TitledElement
14832 * @param {Object} [config] Configuration options
14834 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
14835 // Configuration initialization
14836 config
= config
|| {};
14838 // Parent constructor
14839 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
14841 // Mixin constructors
14842 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
14843 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
14846 this.$element
.addClass( 'oo-ui-indicatorWidget' );
14851 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
14852 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
14853 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
14855 /* Static Properties */
14857 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
14860 * InputWidget is the base class for all input widgets, which
14861 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
14862 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
14863 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
14865 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
14869 * @extends OO.ui.Widget
14870 * @mixins OO.ui.mixin.FlaggedElement
14871 * @mixins OO.ui.mixin.TabIndexedElement
14872 * @mixins OO.ui.mixin.TitledElement
14873 * @mixins OO.ui.mixin.AccessKeyedElement
14876 * @param {Object} [config] Configuration options
14877 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
14878 * @cfg {string} [value=''] The value of the input.
14879 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
14880 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
14881 * before it is accepted.
14883 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
14884 // Configuration initialization
14885 config
= config
|| {};
14887 // Parent constructor
14888 OO
.ui
.InputWidget
.parent
.call( this, config
);
14891 this.$input
= this.getInputElement( config
);
14893 this.inputFilter
= config
.inputFilter
;
14895 // Mixin constructors
14896 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
14897 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
14898 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
14899 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
14902 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
14906 .addClass( 'oo-ui-inputWidget-input' )
14907 .attr( 'name', config
.name
)
14908 .prop( 'disabled', this.isDisabled() );
14910 .addClass( 'oo-ui-inputWidget' )
14911 .append( this.$input
);
14912 this.setValue( config
.value
);
14913 this.setAccessKey( config
.accessKey
);
14914 if ( config
.dir
) {
14915 this.setDir( config
.dir
);
14921 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
14922 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
14923 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
14924 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
14925 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
14927 /* Static Properties */
14929 OO
.ui
.InputWidget
.static.supportsSimpleLabel
= true;
14931 /* Static Methods */
14936 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
14937 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
14938 // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
14939 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
14946 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
14947 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
14948 state
.value
= config
.$input
.val();
14949 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
14950 state
.focus
= config
.$input
.is( ':focus' );
14959 * A change event is emitted when the value of the input changes.
14961 * @param {string} value
14967 * Get input element.
14969 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
14970 * different circumstances. The element must have a `value` property (like form elements).
14973 * @param {Object} config Configuration options
14974 * @return {jQuery} Input element
14976 OO
.ui
.InputWidget
.prototype.getInputElement = function ( config
) {
14977 // See #reusePreInfuseDOM about config.$input
14978 return config
.$input
|| $( '<input>' );
14982 * Handle potentially value-changing events.
14985 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
14987 OO
.ui
.InputWidget
.prototype.onEdit = function () {
14989 if ( !this.isDisabled() ) {
14990 // Allow the stack to clear so the value will be updated
14991 setTimeout( function () {
14992 widget
.setValue( widget
.$input
.val() );
14998 * Get the value of the input.
15000 * @return {string} Input value
15002 OO
.ui
.InputWidget
.prototype.getValue = function () {
15003 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
15004 // it, and we won't know unless they're kind enough to trigger a 'change' event.
15005 var value
= this.$input
.val();
15006 if ( this.value
!== value
) {
15007 this.setValue( value
);
15013 * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
15015 * @deprecated since v0.13.1, use #setDir directly
15016 * @param {boolean} isRTL Directionality is right-to-left
15019 OO
.ui
.InputWidget
.prototype.setRTL = function ( isRTL
) {
15020 this.setDir( isRTL
? 'rtl' : 'ltr' );
15025 * Set the directionality of the input.
15027 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
15030 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
15031 this.$input
.prop( 'dir', dir
);
15036 * Set the value of the input.
15038 * @param {string} value New value
15042 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
15043 value
= this.cleanUpValue( value
);
15044 // Update the DOM if it has changed. Note that with cleanUpValue, it
15045 // is possible for the DOM value to change without this.value changing.
15046 if ( this.$input
.val() !== value
) {
15047 this.$input
.val( value
);
15049 if ( this.value
!== value
) {
15050 this.value
= value
;
15051 this.emit( 'change', this.value
);
15057 * Set the input's access key.
15058 * FIXME: This is the same code as in OO.ui.mixin.ButtonElement, maybe find a better place for it?
15060 * @param {string} accessKey Input's access key, use empty string to remove
15063 OO
.ui
.InputWidget
.prototype.setAccessKey = function ( accessKey
) {
15064 accessKey
= typeof accessKey
=== 'string' && accessKey
.length
? accessKey
: null;
15066 if ( this.accessKey
!== accessKey
) {
15067 if ( this.$input
) {
15068 if ( accessKey
!== null ) {
15069 this.$input
.attr( 'accesskey', accessKey
);
15071 this.$input
.removeAttr( 'accesskey' );
15074 this.accessKey
= accessKey
;
15081 * Clean up incoming value.
15083 * Ensures value is a string, and converts undefined and null to empty string.
15086 * @param {string} value Original value
15087 * @return {string} Cleaned up value
15089 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
15090 if ( value
=== undefined || value
=== null ) {
15092 } else if ( this.inputFilter
) {
15093 return this.inputFilter( String( value
) );
15095 return String( value
);
15100 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
15101 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
15104 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
15105 if ( !this.isDisabled() ) {
15106 if ( this.$input
.is( ':checkbox, :radio' ) ) {
15107 this.$input
.click();
15109 if ( this.$input
.is( ':input' ) ) {
15110 this.$input
[ 0 ].focus();
15118 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
15119 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
15120 if ( this.$input
) {
15121 this.$input
.prop( 'disabled', this.isDisabled() );
15131 OO
.ui
.InputWidget
.prototype.focus = function () {
15132 this.$input
[ 0 ].focus();
15141 OO
.ui
.InputWidget
.prototype.blur = function () {
15142 this.$input
[ 0 ].blur();
15149 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
15150 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
15151 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
15152 this.setValue( state
.value
);
15154 if ( state
.focus
) {
15160 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
15161 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
15162 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
15163 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
15164 * [OOjs UI documentation on MediaWiki] [1] for more information.
15167 * // A ButtonInputWidget rendered as an HTML button, the default.
15168 * var button = new OO.ui.ButtonInputWidget( {
15169 * label: 'Input button',
15173 * $( 'body' ).append( button.$element );
15175 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
15178 * @extends OO.ui.InputWidget
15179 * @mixins OO.ui.mixin.ButtonElement
15180 * @mixins OO.ui.mixin.IconElement
15181 * @mixins OO.ui.mixin.IndicatorElement
15182 * @mixins OO.ui.mixin.LabelElement
15183 * @mixins OO.ui.mixin.TitledElement
15186 * @param {Object} [config] Configuration options
15187 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
15188 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
15189 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
15190 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
15191 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
15193 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
15194 // Configuration initialization
15195 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
15197 // Properties (must be set before parent constructor, which calls #setValue)
15198 this.useInputTag
= config
.useInputTag
;
15200 // Parent constructor
15201 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
15203 // Mixin constructors
15204 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
15205 OO
.ui
.mixin
.IconElement
.call( this, config
);
15206 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
15207 OO
.ui
.mixin
.LabelElement
.call( this, config
);
15208 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
15211 if ( !config
.useInputTag
) {
15212 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
15214 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
15219 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
15220 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
15221 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
15222 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
15223 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
15224 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
15226 /* Static Properties */
15229 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
15230 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
15232 OO
.ui
.ButtonInputWidget
.static.supportsSimpleLabel
= false;
15240 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
15242 // See InputWidget#reusePreInfuseDOM about config.$input
15243 if ( config
.$input
) {
15244 return config
.$input
.empty();
15246 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
15247 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
15253 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
15255 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
15256 * text, or `null` for no label
15259 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
15260 OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
15262 if ( this.useInputTag
) {
15263 if ( typeof label
=== 'function' ) {
15264 label
= OO
.ui
.resolveMsg( label
);
15266 if ( label
instanceof jQuery
) {
15267 label
= label
.text();
15272 this.$input
.val( label
);
15279 * Set the value of the input.
15281 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
15282 * they do not support {@link #value values}.
15284 * @param {string} value New value
15287 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
15288 if ( !this.useInputTag
) {
15289 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
15295 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
15296 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
15297 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
15298 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
15300 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
15303 * // An example of selected, unselected, and disabled checkbox inputs
15304 * var checkbox1=new OO.ui.CheckboxInputWidget( {
15308 * var checkbox2=new OO.ui.CheckboxInputWidget( {
15311 * var checkbox3=new OO.ui.CheckboxInputWidget( {
15315 * // Create a fieldset layout with fields for each checkbox.
15316 * var fieldset = new OO.ui.FieldsetLayout( {
15317 * label: 'Checkboxes'
15319 * fieldset.addItems( [
15320 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
15321 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
15322 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
15324 * $( 'body' ).append( fieldset.$element );
15326 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15329 * @extends OO.ui.InputWidget
15332 * @param {Object} [config] Configuration options
15333 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
15335 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
15336 // Configuration initialization
15337 config
= config
|| {};
15339 // Parent constructor
15340 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
15344 .addClass( 'oo-ui-checkboxInputWidget' )
15345 // Required for pretty styling in MediaWiki theme
15346 .append( $( '<span>' ) );
15347 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
15352 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
15354 /* Static Methods */
15359 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
15360 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
15361 state
.checked
= config
.$input
.prop( 'checked' );
15371 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
15372 return $( '<input type="checkbox" />' );
15378 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
15380 if ( !this.isDisabled() ) {
15381 // Allow the stack to clear so the value will be updated
15382 setTimeout( function () {
15383 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
15389 * Set selection state of this checkbox.
15391 * @param {boolean} state `true` for selected
15394 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
15396 if ( this.selected
!== state
) {
15397 this.selected
= state
;
15398 this.$input
.prop( 'checked', this.selected
);
15399 this.emit( 'change', this.selected
);
15405 * Check if this checkbox is selected.
15407 * @return {boolean} Checkbox is selected
15409 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
15410 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
15411 // it, and we won't know unless they're kind enough to trigger a 'change' event.
15412 var selected
= this.$input
.prop( 'checked' );
15413 if ( this.selected
!== selected
) {
15414 this.setSelected( selected
);
15416 return this.selected
;
15422 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
15423 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
15424 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
15425 this.setSelected( state
.checked
);
15430 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
15431 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
15432 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
15433 * more information about input widgets.
15435 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
15436 * are no options. If no `value` configuration option is provided, the first option is selected.
15437 * If you need a state representing no value (no option being selected), use a DropdownWidget.
15439 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
15442 * // Example: A DropdownInputWidget with three options
15443 * var dropdownInput = new OO.ui.DropdownInputWidget( {
15445 * { data: 'a', label: 'First' },
15446 * { data: 'b', label: 'Second'},
15447 * { data: 'c', label: 'Third' }
15450 * $( 'body' ).append( dropdownInput.$element );
15452 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15455 * @extends OO.ui.InputWidget
15456 * @mixins OO.ui.mixin.TitledElement
15459 * @param {Object} [config] Configuration options
15460 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
15461 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
15463 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
15464 // Configuration initialization
15465 config
= config
|| {};
15467 // Properties (must be done before parent constructor which calls #setDisabled)
15468 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
15470 // Parent constructor
15471 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
15473 // Mixin constructors
15474 OO
.ui
.mixin
.TitledElement
.call( this, config
);
15477 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
15480 this.setOptions( config
.options
|| [] );
15482 .addClass( 'oo-ui-dropdownInputWidget' )
15483 .append( this.dropdownWidget
.$element
);
15488 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
15489 OO
.mixinClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.mixin
.TitledElement
);
15497 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function ( config
) {
15498 // See InputWidget#reusePreInfuseDOM about config.$input
15499 if ( config
.$input
) {
15500 return config
.$input
.addClass( 'oo-ui-element-hidden' );
15502 return $( '<input type="hidden">' );
15506 * Handles menu select events.
15509 * @param {OO.ui.MenuOptionWidget} item Selected menu item
15511 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
15512 this.setValue( item
.getData() );
15518 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
15519 value
= this.cleanUpValue( value
);
15520 this.dropdownWidget
.getMenu().selectItemByData( value
);
15521 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
15528 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
15529 this.dropdownWidget
.setDisabled( state
);
15530 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
15535 * Set the options available for this input.
15537 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
15540 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
15542 value
= this.getValue(),
15545 // Rebuild the dropdown menu
15546 this.dropdownWidget
.getMenu()
15548 .addItems( options
.map( function ( opt
) {
15549 var optValue
= widget
.cleanUpValue( opt
.data
);
15550 return new OO
.ui
.MenuOptionWidget( {
15552 label
: opt
.label
!== undefined ? opt
.label
: optValue
15556 // Restore the previous value, or reset to something sensible
15557 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
15558 // Previous value is still available, ensure consistency with the dropdown
15559 this.setValue( value
);
15561 // No longer valid, reset
15562 if ( options
.length
) {
15563 this.setValue( options
[ 0 ].data
);
15573 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
15574 this.dropdownWidget
.getMenu().toggle( true );
15581 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
15582 this.dropdownWidget
.getMenu().toggle( false );
15587 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
15588 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
15589 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
15590 * please see the [OOjs UI documentation on MediaWiki][1].
15592 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
15595 * // An example of selected, unselected, and disabled radio inputs
15596 * var radio1 = new OO.ui.RadioInputWidget( {
15600 * var radio2 = new OO.ui.RadioInputWidget( {
15603 * var radio3 = new OO.ui.RadioInputWidget( {
15607 * // Create a fieldset layout with fields for each radio button.
15608 * var fieldset = new OO.ui.FieldsetLayout( {
15609 * label: 'Radio inputs'
15611 * fieldset.addItems( [
15612 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
15613 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
15614 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
15616 * $( 'body' ).append( fieldset.$element );
15618 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15621 * @extends OO.ui.InputWidget
15624 * @param {Object} [config] Configuration options
15625 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
15627 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
15628 // Configuration initialization
15629 config
= config
|| {};
15631 // Parent constructor
15632 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
15636 .addClass( 'oo-ui-radioInputWidget' )
15637 // Required for pretty styling in MediaWiki theme
15638 .append( $( '<span>' ) );
15639 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
15644 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
15646 /* Static Methods */
15651 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
15652 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
15653 state
.checked
= config
.$input
.prop( 'checked' );
15663 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
15664 return $( '<input type="radio" />' );
15670 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
15671 // RadioInputWidget doesn't track its state.
15675 * Set selection state of this radio button.
15677 * @param {boolean} state `true` for selected
15680 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
15681 // RadioInputWidget doesn't track its state.
15682 this.$input
.prop( 'checked', state
);
15687 * Check if this radio button is selected.
15689 * @return {boolean} Radio is selected
15691 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
15692 return this.$input
.prop( 'checked' );
15698 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
15699 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
15700 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
15701 this.setSelected( state
.checked
);
15706 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
15707 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
15708 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
15709 * more information about input widgets.
15711 * This and OO.ui.DropdownInputWidget support the same configuration options.
15714 * // Example: A RadioSelectInputWidget with three options
15715 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
15717 * { data: 'a', label: 'First' },
15718 * { data: 'b', label: 'Second'},
15719 * { data: 'c', label: 'Third' }
15722 * $( 'body' ).append( radioSelectInput.$element );
15724 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15727 * @extends OO.ui.InputWidget
15730 * @param {Object} [config] Configuration options
15731 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
15733 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
15734 // Configuration initialization
15735 config
= config
|| {};
15737 // Properties (must be done before parent constructor which calls #setDisabled)
15738 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
15740 // Parent constructor
15741 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
15744 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
15747 this.setOptions( config
.options
|| [] );
15749 .addClass( 'oo-ui-radioSelectInputWidget' )
15750 .append( this.radioSelectWidget
.$element
);
15755 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
15757 /* Static Properties */
15759 OO
.ui
.RadioSelectInputWidget
.static.supportsSimpleLabel
= false;
15761 /* Static Methods */
15766 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
15767 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
15768 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
15778 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
15779 return $( '<input type="hidden">' );
15783 * Handles menu select events.
15786 * @param {OO.ui.RadioOptionWidget} item Selected menu item
15788 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
15789 this.setValue( item
.getData() );
15795 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
15796 value
= this.cleanUpValue( value
);
15797 this.radioSelectWidget
.selectItemByData( value
);
15798 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
15805 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
15806 this.radioSelectWidget
.setDisabled( state
);
15807 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
15812 * Set the options available for this input.
15814 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
15817 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
15819 value
= this.getValue(),
15822 // Rebuild the radioSelect menu
15823 this.radioSelectWidget
15825 .addItems( options
.map( function ( opt
) {
15826 var optValue
= widget
.cleanUpValue( opt
.data
);
15827 return new OO
.ui
.RadioOptionWidget( {
15829 label
: opt
.label
!== undefined ? opt
.label
: optValue
15833 // Restore the previous value, or reset to something sensible
15834 if ( this.radioSelectWidget
.getItemFromData( value
) ) {
15835 // Previous value is still available, ensure consistency with the radioSelect
15836 this.setValue( value
);
15838 // No longer valid, reset
15839 if ( options
.length
) {
15840 this.setValue( options
[ 0 ].data
);
15848 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
15849 * size of the field as well as its presentation. In addition, these widgets can be configured
15850 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
15851 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
15852 * which modifies incoming values rather than validating them.
15853 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
15855 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
15858 * // Example of a text input widget
15859 * var textInput = new OO.ui.TextInputWidget( {
15860 * value: 'Text input'
15862 * $( 'body' ).append( textInput.$element );
15864 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15867 * @extends OO.ui.InputWidget
15868 * @mixins OO.ui.mixin.IconElement
15869 * @mixins OO.ui.mixin.IndicatorElement
15870 * @mixins OO.ui.mixin.PendingElement
15871 * @mixins OO.ui.mixin.LabelElement
15874 * @param {Object} [config] Configuration options
15875 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
15876 * 'email' or 'url'. Ignored if `multiline` is true.
15878 * Some values of `type` result in additional behaviors:
15880 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
15881 * empties the text field
15882 * @cfg {string} [placeholder] Placeholder text
15883 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
15884 * instruct the browser to focus this widget.
15885 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
15886 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
15887 * @cfg {boolean} [multiline=false] Allow multiple lines of text
15888 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
15889 * specifies minimum number of rows to display.
15890 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
15891 * Use the #maxRows config to specify a maximum number of displayed rows.
15892 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
15893 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
15894 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
15895 * the value or placeholder text: `'before'` or `'after'`
15896 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
15897 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
15898 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
15899 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
15900 * (the value must contain only numbers); when RegExp, a regular expression that must match the
15901 * value for it to be considered valid; when Function, a function receiving the value as parameter
15902 * that must return true, or promise resolving to true, for it to be considered valid.
15904 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
15905 // Configuration initialization
15906 config
= $.extend( {
15908 labelPosition
: 'after'
15910 if ( config
.type
=== 'search' ) {
15911 if ( config
.icon
=== undefined ) {
15912 config
.icon
= 'search';
15914 // indicator: 'clear' is set dynamically later, depending on value
15916 if ( config
.required
) {
15917 if ( config
.indicator
=== undefined ) {
15918 config
.indicator
= 'required';
15922 // Parent constructor
15923 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
15925 // Mixin constructors
15926 OO
.ui
.mixin
.IconElement
.call( this, config
);
15927 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
15928 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
15929 OO
.ui
.mixin
.LabelElement
.call( this, config
);
15932 this.type
= this.getSaneType( config
);
15933 this.readOnly
= false;
15934 this.multiline
= !!config
.multiline
;
15935 this.autosize
= !!config
.autosize
;
15936 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
15937 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
15938 this.validate
= null;
15939 this.styleHeight
= null;
15940 this.scrollWidth
= null;
15942 // Clone for resizing
15943 if ( this.autosize
) {
15944 this.$clone
= this.$input
15946 .insertAfter( this.$input
)
15947 .attr( 'aria-hidden', 'true' )
15948 .addClass( 'oo-ui-element-hidden' );
15951 this.setValidation( config
.validate
);
15952 this.setLabelPosition( config
.labelPosition
);
15956 keypress
: this.onKeyPress
.bind( this ),
15957 blur
: this.onBlur
.bind( this )
15960 focus
: this.onElementAttach
.bind( this )
15962 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
15963 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
15964 this.on( 'labelChange', this.updatePosition
.bind( this ) );
15965 this.connect( this, {
15966 change
: 'onChange',
15967 disable
: 'onDisable'
15972 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
15973 .append( this.$icon
, this.$indicator
);
15974 this.setReadOnly( !!config
.readOnly
);
15975 this.updateSearchIndicator();
15976 if ( config
.placeholder
) {
15977 this.$input
.attr( 'placeholder', config
.placeholder
);
15979 if ( config
.maxLength
!== undefined ) {
15980 this.$input
.attr( 'maxlength', config
.maxLength
);
15982 if ( config
.autofocus
) {
15983 this.$input
.attr( 'autofocus', 'autofocus' );
15985 if ( config
.required
) {
15986 this.$input
.attr( 'required', 'required' );
15987 this.$input
.attr( 'aria-required', 'true' );
15989 if ( config
.autocomplete
=== false ) {
15990 this.$input
.attr( 'autocomplete', 'off' );
15991 // Turning off autocompletion also disables "form caching" when the user navigates to a
15992 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
15994 beforeunload: function () {
15995 this.$input
.removeAttr( 'autocomplete' );
15997 pageshow: function () {
15998 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
15999 // whole page... it shouldn't hurt, though.
16000 this.$input
.attr( 'autocomplete', 'off' );
16004 if ( this.multiline
&& config
.rows
) {
16005 this.$input
.attr( 'rows', config
.rows
);
16007 if ( this.label
|| config
.autosize
) {
16008 this.installParentChangeDetector();
16014 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
16015 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
16016 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
16017 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
16018 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
16020 /* Static Properties */
16022 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
16027 /* Static Methods */
16032 OO
.ui
.TextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
16033 var state
= OO
.ui
.TextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
16034 if ( config
.multiline
) {
16035 state
.scrollTop
= config
.$input
.scrollTop();
16043 * An `enter` event is emitted when the user presses 'enter' inside the text box.
16045 * Not emitted if the input is multiline.
16051 * A `resize` event is emitted when autosize is set and the widget resizes
16059 * Handle icon mouse down events.
16062 * @param {jQuery.Event} e Mouse down event
16065 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
16066 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
16067 this.$input
[ 0 ].focus();
16073 * Handle indicator mouse down events.
16076 * @param {jQuery.Event} e Mouse down event
16079 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
16080 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
16081 if ( this.type
=== 'search' ) {
16082 // Clear the text field
16083 this.setValue( '' );
16085 this.$input
[ 0 ].focus();
16091 * Handle key press events.
16094 * @param {jQuery.Event} e Key press event
16095 * @fires enter If enter key is pressed and input is not multiline
16097 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
16098 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
16099 this.emit( 'enter', e
);
16104 * Handle blur events.
16107 * @param {jQuery.Event} e Blur event
16109 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
16110 this.setValidityFlag();
16114 * Handle element attach events.
16117 * @param {jQuery.Event} e Element attach event
16119 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
16120 // Any previously calculated size is now probably invalid if we reattached elsewhere
16121 this.valCache
= null;
16123 this.positionLabel();
16127 * Handle change events.
16129 * @param {string} value
16132 OO
.ui
.TextInputWidget
.prototype.onChange = function () {
16133 this.updateSearchIndicator();
16134 this.setValidityFlag();
16139 * Handle disable events.
16141 * @param {boolean} disabled Element is disabled
16144 OO
.ui
.TextInputWidget
.prototype.onDisable = function () {
16145 this.updateSearchIndicator();
16149 * Check if the input is {@link #readOnly read-only}.
16151 * @return {boolean}
16153 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
16154 return this.readOnly
;
16158 * Set the {@link #readOnly read-only} state of the input.
16160 * @param {boolean} state Make input read-only
16163 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
16164 this.readOnly
= !!state
;
16165 this.$input
.prop( 'readOnly', this.readOnly
);
16166 this.updateSearchIndicator();
16171 * Support function for making #onElementAttach work across browsers.
16173 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
16174 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
16176 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
16177 * first time that the element gets attached to the documented.
16179 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
16180 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
16181 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
16184 if ( MutationObserver
) {
16185 // The new way. If only it wasn't so ugly.
16187 if ( this.$element
.closest( 'html' ).length
) {
16188 // Widget is attached already, do nothing. This breaks the functionality of this function when
16189 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
16190 // would require observation of the whole document, which would hurt performance of other,
16191 // more important code.
16195 // Find topmost node in the tree
16196 topmostNode
= this.$element
[ 0 ];
16197 while ( topmostNode
.parentNode
) {
16198 topmostNode
= topmostNode
.parentNode
;
16201 // We have no way to detect the $element being attached somewhere without observing the entire
16202 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
16203 // parent node of $element, and instead detect when $element is removed from it (and thus
16204 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
16205 // doesn't get attached, we end up back here and create the parent.
16207 mutationObserver
= new MutationObserver( function ( mutations
) {
16208 var i
, j
, removedNodes
;
16209 for ( i
= 0; i
< mutations
.length
; i
++ ) {
16210 removedNodes
= mutations
[ i
].removedNodes
;
16211 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
16212 if ( removedNodes
[ j
] === topmostNode
) {
16213 setTimeout( onRemove
, 0 );
16220 onRemove = function () {
16221 // If the node was attached somewhere else, report it
16222 if ( widget
.$element
.closest( 'html' ).length
) {
16223 widget
.onElementAttach();
16225 mutationObserver
.disconnect();
16226 widget
.installParentChangeDetector();
16229 // Create a fake parent and observe it
16230 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
16231 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
16233 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
16234 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
16235 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
16240 * Automatically adjust the size of the text input.
16242 * This only affects #multiline inputs that are {@link #autosize autosized}.
16247 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
16248 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
16249 idealHeight
, newHeight
, scrollWidth
, property
;
16251 if ( this.multiline
&& this.$input
.val() !== this.valCache
) {
16252 if ( this.autosize
) {
16254 .val( this.$input
.val() )
16255 .attr( 'rows', this.minRows
)
16256 // Set inline height property to 0 to measure scroll height
16257 .css( 'height', 0 );
16259 this.$clone
.removeClass( 'oo-ui-element-hidden' );
16261 this.valCache
= this.$input
.val();
16263 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
16265 // Remove inline height property to measure natural heights
16266 this.$clone
.css( 'height', '' );
16267 innerHeight
= this.$clone
.innerHeight();
16268 outerHeight
= this.$clone
.outerHeight();
16270 // Measure max rows height
16272 .attr( 'rows', this.maxRows
)
16273 .css( 'height', 'auto' )
16275 maxInnerHeight
= this.$clone
.innerHeight();
16277 // Difference between reported innerHeight and scrollHeight with no scrollbars present
16278 // Equals 1 on Blink-based browsers and 0 everywhere else
16279 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
16280 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
16282 this.$clone
.addClass( 'oo-ui-element-hidden' );
16284 // Only apply inline height when expansion beyond natural height is needed
16285 // Use the difference between the inner and outer height as a buffer
16286 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
16287 if ( newHeight
!== this.styleHeight
) {
16288 this.$input
.css( 'height', newHeight
);
16289 this.styleHeight
= newHeight
;
16290 this.emit( 'resize' );
16293 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
16294 if ( scrollWidth
!== this.scrollWidth
) {
16295 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
16297 this.$label
.css( { right
: '', left
: '' } );
16298 this.$indicator
.css( { right
: '', left
: '' } );
16300 if ( scrollWidth
) {
16301 this.$indicator
.css( property
, scrollWidth
);
16302 if ( this.labelPosition
=== 'after' ) {
16303 this.$label
.css( property
, scrollWidth
);
16307 this.scrollWidth
= scrollWidth
;
16308 this.positionLabel();
16318 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
16319 return config
.multiline
?
16320 $( '<textarea>' ) :
16321 $( '<input type="' + this.getSaneType( config
) + '" />' );
16325 * Get sanitized value for 'type' for given config.
16327 * @param {Object} config Configuration options
16328 * @return {string|null}
16331 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
16332 var type
= [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config
.type
) !== -1 ?
16335 return config
.multiline
? 'multiline' : type
;
16339 * Check if the input supports multiple lines.
16341 * @return {boolean}
16343 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
16344 return !!this.multiline
;
16348 * Check if the input automatically adjusts its size.
16350 * @return {boolean}
16352 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
16353 return !!this.autosize
;
16357 * Focus the input and select a specified range within the text.
16359 * @param {number} from Select from offset
16360 * @param {number} [to] Select to offset, defaults to from
16363 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
16364 var isBackwards
, start
, end
,
16365 input
= this.$input
[ 0 ];
16369 isBackwards
= to
< from;
16370 start
= isBackwards
? to
: from;
16371 end
= isBackwards
? from : to
;
16375 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
16380 * Get an object describing the current selection range in a directional manner
16382 * @return {Object} Object containing 'from' and 'to' offsets
16384 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
16385 var input
= this.$input
[ 0 ],
16386 start
= input
.selectionStart
,
16387 end
= input
.selectionEnd
,
16388 isBackwards
= input
.selectionDirection
=== 'backward';
16391 from: isBackwards
? end
: start
,
16392 to
: isBackwards
? start
: end
16397 * Get the length of the text input value.
16399 * This could differ from the length of #getValue if the
16400 * value gets filtered
16402 * @return {number} Input length
16404 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
16405 return this.$input
[ 0 ].value
.length
;
16409 * Focus the input and select the entire text.
16413 OO
.ui
.TextInputWidget
.prototype.select = function () {
16414 return this.selectRange( 0, this.getInputLength() );
16418 * Focus the input and move the cursor to the start.
16422 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
16423 return this.selectRange( 0 );
16427 * Focus the input and move the cursor to the end.
16431 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
16432 return this.selectRange( this.getInputLength() );
16436 * Insert new content into the input.
16438 * @param {string} content Content to be inserted
16441 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
16443 range
= this.getRange(),
16444 value
= this.getValue();
16446 start
= Math
.min( range
.from, range
.to
);
16447 end
= Math
.max( range
.from, range
.to
);
16449 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
16450 this.selectRange( start
+ content
.length
);
16455 * Insert new content either side of a selection.
16457 * @param {string} pre Content to be inserted before the selection
16458 * @param {string} post Content to be inserted after the selection
16461 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
16463 range
= this.getRange(),
16464 offset
= pre
.length
;
16466 start
= Math
.min( range
.from, range
.to
);
16467 end
= Math
.max( range
.from, range
.to
);
16469 this.selectRange( start
).insertContent( pre
);
16470 this.selectRange( offset
+ end
).insertContent( post
);
16472 this.selectRange( offset
+ start
, offset
+ end
);
16477 * Set the validation pattern.
16479 * The validation pattern is either a regular expression, a function, or the symbolic name of a
16480 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
16481 * value must contain only numbers).
16483 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
16484 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
16486 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
16487 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
16488 this.validate
= validate
;
16490 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
16495 * Sets the 'invalid' flag appropriately.
16497 * @param {boolean} [isValid] Optionally override validation result
16499 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
16501 setFlag = function ( valid
) {
16503 widget
.$input
.attr( 'aria-invalid', 'true' );
16505 widget
.$input
.removeAttr( 'aria-invalid' );
16507 widget
.setFlags( { invalid
: !valid
} );
16510 if ( isValid
!== undefined ) {
16511 setFlag( isValid
);
16513 this.getValidity().then( function () {
16522 * Check if a value is valid.
16524 * This method returns a promise that resolves with a boolean `true` if the current value is
16525 * considered valid according to the supplied {@link #validate validation pattern}.
16528 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
16530 OO
.ui
.TextInputWidget
.prototype.isValid = function () {
16533 if ( this.validate
instanceof Function
) {
16534 result
= this.validate( this.getValue() );
16535 if ( result
&& $.isFunction( result
.promise
) ) {
16536 return result
.promise();
16538 return $.Deferred().resolve( !!result
).promise();
16541 return $.Deferred().resolve( !!this.getValue().match( this.validate
) ).promise();
16546 * Get the validity of current value.
16548 * This method returns a promise that resolves if the value is valid and rejects if
16549 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
16551 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
16553 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
16556 function rejectOrResolve( valid
) {
16558 return $.Deferred().resolve().promise();
16560 return $.Deferred().reject().promise();
16564 if ( this.validate
instanceof Function
) {
16565 result
= this.validate( this.getValue() );
16566 if ( result
&& $.isFunction( result
.promise
) ) {
16567 return result
.promise().then( function ( valid
) {
16568 return rejectOrResolve( valid
);
16571 return rejectOrResolve( result
);
16574 return rejectOrResolve( this.getValue().match( this.validate
) );
16579 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
16581 * @param {string} labelPosition Label position, 'before' or 'after'
16584 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
16585 this.labelPosition
= labelPosition
;
16586 this.updatePosition();
16591 * Update the position of the inline label.
16593 * This method is called by #setLabelPosition, and can also be called on its own if
16594 * something causes the label to be mispositioned.
16598 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
16599 var after
= this.labelPosition
=== 'after';
16602 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
16603 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
16605 this.valCache
= null;
16606 this.scrollWidth
= null;
16608 this.positionLabel();
16614 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
16615 * already empty or when it's not editable.
16617 OO
.ui
.TextInputWidget
.prototype.updateSearchIndicator = function () {
16618 if ( this.type
=== 'search' ) {
16619 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
16620 this.setIndicator( null );
16622 this.setIndicator( 'clear' );
16628 * Position the label by setting the correct padding on the input.
16633 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
16634 var after
, rtl
, property
;
16635 // Clear old values
16637 // Clear old values if present
16639 'padding-right': '',
16643 if ( this.label
) {
16644 this.$element
.append( this.$label
);
16646 this.$label
.detach();
16650 after
= this.labelPosition
=== 'after';
16651 rtl
= this.$element
.css( 'direction' ) === 'rtl';
16652 property
= after
=== rtl
? 'padding-left' : 'padding-right';
16654 this.$input
.css( property
, this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 ) );
16662 OO
.ui
.TextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
16663 OO
.ui
.TextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
16664 if ( state
.scrollTop
!== undefined ) {
16665 this.$input
.scrollTop( state
.scrollTop
);
16670 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
16671 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
16672 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
16674 * - by typing a value in the text input field. If the value exactly matches the value of a menu
16675 * option, that option will appear to be selected.
16676 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
16679 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
16681 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
16684 * // Example: A ComboBoxInputWidget.
16685 * var comboBox = new OO.ui.ComboBoxInputWidget( {
16686 * label: 'ComboBoxInputWidget',
16687 * value: 'Option 1',
16690 * new OO.ui.MenuOptionWidget( {
16691 * data: 'Option 1',
16692 * label: 'Option One'
16694 * new OO.ui.MenuOptionWidget( {
16695 * data: 'Option 2',
16696 * label: 'Option Two'
16698 * new OO.ui.MenuOptionWidget( {
16699 * data: 'Option 3',
16700 * label: 'Option Three'
16702 * new OO.ui.MenuOptionWidget( {
16703 * data: 'Option 4',
16704 * label: 'Option Four'
16706 * new OO.ui.MenuOptionWidget( {
16707 * data: 'Option 5',
16708 * label: 'Option Five'
16713 * $( 'body' ).append( comboBox.$element );
16715 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
16718 * @extends OO.ui.TextInputWidget
16721 * @param {Object} [config] Configuration options
16722 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
16723 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
16724 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
16725 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
16726 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
16728 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
16729 // Configuration initialization
16730 config
= $.extend( {
16733 // For backwards-compatibility with ComboBoxWidget config
16734 $.extend( config
, config
.input
);
16736 // Parent constructor
16737 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
16740 this.$overlay
= config
.$overlay
|| this.$element
;
16741 this.menu
= new OO
.ui
.FloatingMenuSelectWidget( $.extend(
16745 $container
: this.$element
,
16746 disabled
: this.isDisabled()
16750 // For backwards-compatibility with ComboBoxWidget
16754 this.$indicator
.on( {
16755 click
: this.onIndicatorClick
.bind( this ),
16756 keypress
: this.onIndicatorKeyPress
.bind( this )
16758 this.connect( this, {
16759 change
: 'onInputChange',
16760 enter
: 'onInputEnter'
16762 this.menu
.connect( this, {
16763 choose
: 'onMenuChoose',
16764 add
: 'onMenuItemsChange',
16765 remove
: 'onMenuItemsChange'
16769 this.$input
.attr( {
16771 'aria-autocomplete': 'list'
16773 // Do not override options set via config.menu.items
16774 if ( config
.options
!== undefined ) {
16775 this.setOptions( config
.options
);
16777 // Extra class for backwards-compatibility with ComboBoxWidget
16778 this.$element
.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
16779 this.$overlay
.append( this.menu
.$element
);
16780 this.onMenuItemsChange();
16785 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
16790 * Get the combobox's menu.
16791 * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
16793 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
16798 * Get the combobox's text input widget.
16799 * @return {OO.ui.TextInputWidget} Text input widget
16801 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
16806 * Handle input change events.
16809 * @param {string} value New value
16811 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
16812 var match
= this.menu
.getItemFromData( value
);
16814 this.menu
.selectItem( match
);
16815 if ( this.menu
.getHighlightedItem() ) {
16816 this.menu
.highlightItem( match
);
16819 if ( !this.isDisabled() ) {
16820 this.menu
.toggle( true );
16825 * Handle mouse click events.
16828 * @param {jQuery.Event} e Mouse click event
16830 OO
.ui
.ComboBoxInputWidget
.prototype.onIndicatorClick = function ( e
) {
16831 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
16832 this.menu
.toggle();
16833 this.$input
[ 0 ].focus();
16839 * Handle key press events.
16842 * @param {jQuery.Event} e Key press event
16844 OO
.ui
.ComboBoxInputWidget
.prototype.onIndicatorKeyPress = function ( e
) {
16845 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
16846 this.menu
.toggle();
16847 this.$input
[ 0 ].focus();
16853 * Handle input enter events.
16857 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
16858 if ( !this.isDisabled() ) {
16859 this.menu
.toggle( false );
16864 * Handle menu choose events.
16867 * @param {OO.ui.OptionWidget} item Chosen item
16869 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
16870 this.setValue( item
.getData() );
16874 * Handle menu item change events.
16878 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
16879 var match
= this.menu
.getItemFromData( this.getValue() );
16880 this.menu
.selectItem( match
);
16881 if ( this.menu
.getHighlightedItem() ) {
16882 this.menu
.highlightItem( match
);
16884 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
16890 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
16892 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
16895 this.menu
.setDisabled( this.isDisabled() );
16902 * Set the options available for this input.
16904 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
16907 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
16910 .addItems( options
.map( function ( opt
) {
16911 return new OO
.ui
.MenuOptionWidget( {
16913 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
16922 * @deprecated Use OO.ui.ComboBoxInputWidget instead.
16924 OO
.ui
.ComboBoxWidget
= OO
.ui
.ComboBoxInputWidget
;
16927 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
16928 * be configured with a `label` option that is set to a string, a label node, or a function:
16930 * - String: a plaintext string
16931 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
16932 * label that includes a link or special styling, such as a gray color or additional graphical elements.
16933 * - Function: a function that will produce a string in the future. Functions are used
16934 * in cases where the value of the label is not currently defined.
16936 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
16937 * will come into focus when the label is clicked.
16940 * // Examples of LabelWidgets
16941 * var label1 = new OO.ui.LabelWidget( {
16942 * label: 'plaintext label'
16944 * var label2 = new OO.ui.LabelWidget( {
16945 * label: $( '<a href="default.html">jQuery label</a>' )
16947 * // Create a fieldset layout with fields for each example
16948 * var fieldset = new OO.ui.FieldsetLayout();
16949 * fieldset.addItems( [
16950 * new OO.ui.FieldLayout( label1 ),
16951 * new OO.ui.FieldLayout( label2 )
16953 * $( 'body' ).append( fieldset.$element );
16956 * @extends OO.ui.Widget
16957 * @mixins OO.ui.mixin.LabelElement
16960 * @param {Object} [config] Configuration options
16961 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
16962 * Clicking the label will focus the specified input field.
16964 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
16965 // Configuration initialization
16966 config
= config
|| {};
16968 // Parent constructor
16969 OO
.ui
.LabelWidget
.parent
.call( this, config
);
16971 // Mixin constructors
16972 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
16973 OO
.ui
.mixin
.TitledElement
.call( this, config
);
16976 this.input
= config
.input
;
16979 if ( this.input
instanceof OO
.ui
.InputWidget
) {
16980 this.$element
.on( 'click', this.onClick
.bind( this ) );
16984 this.$element
.addClass( 'oo-ui-labelWidget' );
16989 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
16990 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
16991 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
16993 /* Static Properties */
16995 OO
.ui
.LabelWidget
.static.tagName
= 'span';
17000 * Handles label mouse click events.
17003 * @param {jQuery.Event} e Mouse click event
17005 OO
.ui
.LabelWidget
.prototype.onClick = function () {
17006 this.input
.simulateLabelClick();
17011 * OptionWidgets are special elements that can be selected and configured with data. The
17012 * data is often unique for each option, but it does not have to be. OptionWidgets are used
17013 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
17014 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
17016 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
17019 * @extends OO.ui.Widget
17020 * @mixins OO.ui.mixin.LabelElement
17021 * @mixins OO.ui.mixin.FlaggedElement
17024 * @param {Object} [config] Configuration options
17026 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
17027 // Configuration initialization
17028 config
= config
|| {};
17030 // Parent constructor
17031 OO
.ui
.OptionWidget
.parent
.call( this, config
);
17033 // Mixin constructors
17034 OO
.ui
.mixin
.ItemWidget
.call( this );
17035 OO
.ui
.mixin
.LabelElement
.call( this, config
);
17036 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
17039 this.selected
= false;
17040 this.highlighted
= false;
17041 this.pressed
= false;
17045 .data( 'oo-ui-optionWidget', this )
17046 .attr( 'role', 'option' )
17047 .attr( 'aria-selected', 'false' )
17048 .addClass( 'oo-ui-optionWidget' )
17049 .append( this.$label
);
17054 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
17055 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
17056 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
17057 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
17059 /* Static Properties */
17061 OO
.ui
.OptionWidget
.static.selectable
= true;
17063 OO
.ui
.OptionWidget
.static.highlightable
= true;
17065 OO
.ui
.OptionWidget
.static.pressable
= true;
17067 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
17072 * Check if the option can be selected.
17074 * @return {boolean} Item is selectable
17076 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
17077 return this.constructor.static.selectable
&& !this.isDisabled() && this.isVisible();
17081 * Check if the option can be highlighted. A highlight indicates that the option
17082 * may be selected when a user presses enter or clicks. Disabled items cannot
17085 * @return {boolean} Item is highlightable
17087 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
17088 return this.constructor.static.highlightable
&& !this.isDisabled() && this.isVisible();
17092 * Check if the option can be pressed. The pressed state occurs when a user mouses
17093 * down on an item, but has not yet let go of the mouse.
17095 * @return {boolean} Item is pressable
17097 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
17098 return this.constructor.static.pressable
&& !this.isDisabled() && this.isVisible();
17102 * Check if the option is selected.
17104 * @return {boolean} Item is selected
17106 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
17107 return this.selected
;
17111 * Check if the option is highlighted. A highlight indicates that the
17112 * item may be selected when a user presses enter or clicks.
17114 * @return {boolean} Item is highlighted
17116 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
17117 return this.highlighted
;
17121 * Check if the option is pressed. The pressed state occurs when a user mouses
17122 * down on an item, but has not yet let go of the mouse. The item may appear
17123 * selected, but it will not be selected until the user releases the mouse.
17125 * @return {boolean} Item is pressed
17127 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
17128 return this.pressed
;
17132 * Set the option’s selected state. In general, all modifications to the selection
17133 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
17134 * method instead of this method.
17136 * @param {boolean} [state=false] Select option
17139 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
17140 if ( this.constructor.static.selectable
) {
17141 this.selected
= !!state
;
17143 .toggleClass( 'oo-ui-optionWidget-selected', state
)
17144 .attr( 'aria-selected', state
.toString() );
17145 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
17146 this.scrollElementIntoView();
17148 this.updateThemeClasses();
17154 * Set the option’s highlighted state. In general, all programmatic
17155 * modifications to the highlight should be handled by the
17156 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
17157 * method instead of this method.
17159 * @param {boolean} [state=false] Highlight option
17162 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
17163 if ( this.constructor.static.highlightable
) {
17164 this.highlighted
= !!state
;
17165 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
17166 this.updateThemeClasses();
17172 * Set the option’s pressed state. In general, all
17173 * programmatic modifications to the pressed state should be handled by the
17174 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
17175 * method instead of this method.
17177 * @param {boolean} [state=false] Press option
17180 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
17181 if ( this.constructor.static.pressable
) {
17182 this.pressed
= !!state
;
17183 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
17184 this.updateThemeClasses();
17190 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
17191 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
17192 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
17193 * options. For more information about options and selects, please see the
17194 * [OOjs UI documentation on MediaWiki][1].
17197 * // Decorated options in a select widget
17198 * var select = new OO.ui.SelectWidget( {
17200 * new OO.ui.DecoratedOptionWidget( {
17202 * label: 'Option with icon',
17205 * new OO.ui.DecoratedOptionWidget( {
17207 * label: 'Option with indicator',
17208 * indicator: 'next'
17212 * $( 'body' ).append( select.$element );
17214 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
17217 * @extends OO.ui.OptionWidget
17218 * @mixins OO.ui.mixin.IconElement
17219 * @mixins OO.ui.mixin.IndicatorElement
17222 * @param {Object} [config] Configuration options
17224 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
17225 // Parent constructor
17226 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
17228 // Mixin constructors
17229 OO
.ui
.mixin
.IconElement
.call( this, config
);
17230 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
17234 .addClass( 'oo-ui-decoratedOptionWidget' )
17235 .prepend( this.$icon
)
17236 .append( this.$indicator
);
17241 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
17242 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
17243 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
17246 * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
17247 * can be selected and configured with data. The class is
17248 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
17249 * [OOjs UI documentation on MediaWiki] [1] for more information.
17251 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
17254 * @extends OO.ui.DecoratedOptionWidget
17255 * @mixins OO.ui.mixin.ButtonElement
17256 * @mixins OO.ui.mixin.TabIndexedElement
17257 * @mixins OO.ui.mixin.TitledElement
17260 * @param {Object} [config] Configuration options
17262 OO
.ui
.ButtonOptionWidget
= function OoUiButtonOptionWidget( config
) {
17263 // Configuration initialization
17264 config
= config
|| {};
17266 // Parent constructor
17267 OO
.ui
.ButtonOptionWidget
.parent
.call( this, config
);
17269 // Mixin constructors
17270 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
17271 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
17272 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, {
17273 $tabIndexed
: this.$button
,
17278 this.$element
.addClass( 'oo-ui-buttonOptionWidget' );
17279 this.$button
.append( this.$element
.contents() );
17280 this.$element
.append( this.$button
);
17285 OO
.inheritClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
17286 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.mixin
.ButtonElement
);
17287 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.mixin
.TitledElement
);
17288 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.mixin
.TabIndexedElement
);
17290 /* Static Properties */
17292 // Allow button mouse down events to pass through so they can be handled by the parent select widget
17293 OO
.ui
.ButtonOptionWidget
.static.cancelButtonMouseDownEvents
= false;
17295 OO
.ui
.ButtonOptionWidget
.static.highlightable
= false;
17302 OO
.ui
.ButtonOptionWidget
.prototype.setSelected = function ( state
) {
17303 OO
.ui
.ButtonOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
17305 if ( this.constructor.static.selectable
) {
17306 this.setActive( state
);
17313 * RadioOptionWidget is an option widget that looks like a radio button.
17314 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
17315 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
17317 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
17320 * @extends OO.ui.OptionWidget
17323 * @param {Object} [config] Configuration options
17325 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
17326 // Configuration initialization
17327 config
= config
|| {};
17329 // Properties (must be done before parent constructor which calls #setDisabled)
17330 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
17332 // Parent constructor
17333 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
17336 this.radio
.$input
.on( 'focus', this.onInputFocus
.bind( this ) );
17339 // Remove implicit role, we're handling it ourselves
17340 this.radio
.$input
.attr( 'role', 'presentation' );
17342 .addClass( 'oo-ui-radioOptionWidget' )
17343 .attr( 'role', 'radio' )
17344 .attr( 'aria-checked', 'false' )
17345 .removeAttr( 'aria-selected' )
17346 .prepend( this.radio
.$element
);
17351 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
17353 /* Static Properties */
17355 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
17357 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
17359 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
17361 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
17366 * @param {jQuery.Event} e Focus event
17369 OO
.ui
.RadioOptionWidget
.prototype.onInputFocus = function () {
17370 this.radio
.$input
.blur();
17371 this.$element
.parent().focus();
17377 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
17378 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
17380 this.radio
.setSelected( state
);
17382 .attr( 'aria-checked', state
.toString() )
17383 .removeAttr( 'aria-selected' );
17391 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
17392 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
17394 this.radio
.setDisabled( this.isDisabled() );
17400 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
17401 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
17402 * the [OOjs UI documentation on MediaWiki] [1] for more information.
17404 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
17407 * @extends OO.ui.DecoratedOptionWidget
17410 * @param {Object} [config] Configuration options
17412 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
17413 // Configuration initialization
17414 config
= $.extend( { icon
: 'check' }, config
);
17416 // Parent constructor
17417 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
17421 .attr( 'role', 'menuitem' )
17422 .addClass( 'oo-ui-menuOptionWidget' );
17427 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
17429 /* Static Properties */
17431 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
17434 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
17435 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
17438 * var myDropdown = new OO.ui.DropdownWidget( {
17441 * new OO.ui.MenuSectionOptionWidget( {
17444 * new OO.ui.MenuOptionWidget( {
17446 * label: 'Welsh Corgi'
17448 * new OO.ui.MenuOptionWidget( {
17450 * label: 'Standard Poodle'
17452 * new OO.ui.MenuSectionOptionWidget( {
17455 * new OO.ui.MenuOptionWidget( {
17462 * $( 'body' ).append( myDropdown.$element );
17465 * @extends OO.ui.DecoratedOptionWidget
17468 * @param {Object} [config] Configuration options
17470 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
17471 // Parent constructor
17472 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
17475 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' );
17480 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
17482 /* Static Properties */
17484 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
17486 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
17489 * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
17491 * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
17492 * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
17496 * @extends OO.ui.DecoratedOptionWidget
17499 * @param {Object} [config] Configuration options
17500 * @cfg {number} [level] Indentation level
17501 * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
17503 OO
.ui
.OutlineOptionWidget
= function OoUiOutlineOptionWidget( config
) {
17504 // Configuration initialization
17505 config
= config
|| {};
17507 // Parent constructor
17508 OO
.ui
.OutlineOptionWidget
.parent
.call( this, config
);
17512 this.movable
= !!config
.movable
;
17513 this.removable
= !!config
.removable
;
17516 this.$element
.addClass( 'oo-ui-outlineOptionWidget' );
17517 this.setLevel( config
.level
);
17522 OO
.inheritClass( OO
.ui
.OutlineOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
17524 /* Static Properties */
17526 OO
.ui
.OutlineOptionWidget
.static.highlightable
= false;
17528 OO
.ui
.OutlineOptionWidget
.static.scrollIntoViewOnSelect
= true;
17530 OO
.ui
.OutlineOptionWidget
.static.levelClass
= 'oo-ui-outlineOptionWidget-level-';
17532 OO
.ui
.OutlineOptionWidget
.static.levels
= 3;
17537 * Check if item is movable.
17539 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
17541 * @return {boolean} Item is movable
17543 OO
.ui
.OutlineOptionWidget
.prototype.isMovable = function () {
17544 return this.movable
;
17548 * Check if item is removable.
17550 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
17552 * @return {boolean} Item is removable
17554 OO
.ui
.OutlineOptionWidget
.prototype.isRemovable = function () {
17555 return this.removable
;
17559 * Get indentation level.
17561 * @return {number} Indentation level
17563 OO
.ui
.OutlineOptionWidget
.prototype.getLevel = function () {
17570 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
17572 * @param {boolean} movable Item is movable
17575 OO
.ui
.OutlineOptionWidget
.prototype.setMovable = function ( movable
) {
17576 this.movable
= !!movable
;
17577 this.updateThemeClasses();
17582 * Set removability.
17584 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
17586 * @param {boolean} removable Item is removable
17589 OO
.ui
.OutlineOptionWidget
.prototype.setRemovable = function ( removable
) {
17590 this.removable
= !!removable
;
17591 this.updateThemeClasses();
17596 * Set indentation level.
17598 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
17601 OO
.ui
.OutlineOptionWidget
.prototype.setLevel = function ( level
) {
17602 var levels
= this.constructor.static.levels
,
17603 levelClass
= this.constructor.static.levelClass
,
17606 this.level
= level
? Math
.max( 0, Math
.min( levels
- 1, level
) ) : 0;
17608 if ( this.level
=== i
) {
17609 this.$element
.addClass( levelClass
+ i
);
17611 this.$element
.removeClass( levelClass
+ i
);
17614 this.updateThemeClasses();
17620 * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
17622 * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
17623 * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
17627 * @extends OO.ui.OptionWidget
17630 * @param {Object} [config] Configuration options
17632 OO
.ui
.TabOptionWidget
= function OoUiTabOptionWidget( config
) {
17633 // Configuration initialization
17634 config
= config
|| {};
17636 // Parent constructor
17637 OO
.ui
.TabOptionWidget
.parent
.call( this, config
);
17640 this.$element
.addClass( 'oo-ui-tabOptionWidget' );
17645 OO
.inheritClass( OO
.ui
.TabOptionWidget
, OO
.ui
.OptionWidget
);
17647 /* Static Properties */
17649 OO
.ui
.TabOptionWidget
.static.highlightable
= false;
17652 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
17653 * By default, each popup has an anchor that points toward its origin.
17654 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
17657 * // A popup widget.
17658 * var popup = new OO.ui.PopupWidget( {
17659 * $content: $( '<p>Hi there!</p>' ),
17664 * $( 'body' ).append( popup.$element );
17665 * // To display the popup, toggle the visibility to 'true'.
17666 * popup.toggle( true );
17668 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
17671 * @extends OO.ui.Widget
17672 * @mixins OO.ui.mixin.LabelElement
17673 * @mixins OO.ui.mixin.ClippableElement
17676 * @param {Object} [config] Configuration options
17677 * @cfg {number} [width=320] Width of popup in pixels
17678 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
17679 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
17680 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
17681 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
17682 * popup is leaning towards the right of the screen.
17683 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
17684 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
17685 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
17686 * sentence in the given language.
17687 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
17688 * See the [OOjs UI docs on MediaWiki][3] for an example.
17689 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
17690 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
17691 * @cfg {jQuery} [$content] Content to append to the popup's body
17692 * @cfg {jQuery} [$footer] Content to append to the popup's footer
17693 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
17694 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
17695 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
17697 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
17698 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
17700 * @cfg {boolean} [padded] Add padding to the popup's body
17702 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
17703 // Configuration initialization
17704 config
= config
|| {};
17706 // Parent constructor
17707 OO
.ui
.PopupWidget
.parent
.call( this, config
);
17709 // Properties (must be set before ClippableElement constructor call)
17710 this.$body
= $( '<div>' );
17711 this.$popup
= $( '<div>' );
17713 // Mixin constructors
17714 OO
.ui
.mixin
.LabelElement
.call( this, config
);
17715 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
17716 $clippable
: this.$body
,
17717 $clippableContainer
: this.$popup
17721 this.$head
= $( '<div>' );
17722 this.$footer
= $( '<div>' );
17723 this.$anchor
= $( '<div>' );
17724 // If undefined, will be computed lazily in updateDimensions()
17725 this.$container
= config
.$container
;
17726 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
17727 this.autoClose
= !!config
.autoClose
;
17728 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
17729 this.transitionTimeout
= null;
17730 this.anchor
= null;
17731 this.width
= config
.width
!== undefined ? config
.width
: 320;
17732 this.height
= config
.height
!== undefined ? config
.height
: null;
17733 this.setAlignment( config
.align
);
17734 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
17735 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
17736 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
17739 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
17742 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
17743 this.$body
.addClass( 'oo-ui-popupWidget-body' );
17744 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
17746 .addClass( 'oo-ui-popupWidget-head' )
17747 .append( this.$label
, this.closeButton
.$element
);
17748 this.$footer
.addClass( 'oo-ui-popupWidget-footer' );
17749 if ( !config
.head
) {
17750 this.$head
.addClass( 'oo-ui-element-hidden' );
17752 if ( !config
.$footer
) {
17753 this.$footer
.addClass( 'oo-ui-element-hidden' );
17756 .addClass( 'oo-ui-popupWidget-popup' )
17757 .append( this.$head
, this.$body
, this.$footer
);
17759 .addClass( 'oo-ui-popupWidget' )
17760 .append( this.$popup
, this.$anchor
);
17761 // Move content, which was added to #$element by OO.ui.Widget, to the body
17762 if ( config
.$content
instanceof jQuery
) {
17763 this.$body
.append( config
.$content
);
17765 if ( config
.$footer
instanceof jQuery
) {
17766 this.$footer
.append( config
.$footer
);
17768 if ( config
.padded
) {
17769 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
17772 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
17773 // that reference properties not initialized at that time of parent class construction
17774 // TODO: Find a better way to handle post-constructor setup
17775 this.visible
= false;
17776 this.$element
.addClass( 'oo-ui-element-hidden' );
17781 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
17782 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
17783 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
17788 * Handles mouse down events.
17791 * @param {MouseEvent} e Mouse down event
17793 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
17795 this.isVisible() &&
17796 !$.contains( this.$element
[ 0 ], e
.target
) &&
17797 ( !this.$autoCloseIgnore
|| !this.$autoCloseIgnore
.has( e
.target
).length
)
17799 this.toggle( false );
17804 * Bind mouse down listener.
17808 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
17809 // Capture clicks outside popup
17810 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
17814 * Handles close button click events.
17818 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
17819 if ( this.isVisible() ) {
17820 this.toggle( false );
17825 * Unbind mouse down listener.
17829 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
17830 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
17834 * Handles key down events.
17837 * @param {KeyboardEvent} e Key down event
17839 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
17841 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
17844 this.toggle( false );
17845 e
.preventDefault();
17846 e
.stopPropagation();
17851 * Bind key down listener.
17855 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
17856 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
17860 * Unbind key down listener.
17864 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
17865 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
17869 * Show, hide, or toggle the visibility of the anchor.
17871 * @param {boolean} [show] Show anchor, omit to toggle
17873 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
17874 show
= show
=== undefined ? !this.anchored
: !!show
;
17876 if ( this.anchored
!== show
) {
17878 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
17880 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
17882 this.anchored
= show
;
17887 * Check if the anchor is visible.
17889 * @return {boolean} Anchor is visible
17891 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
17892 return this.anchor
;
17898 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
17900 show
= show
=== undefined ? !this.isVisible() : !!show
;
17902 change
= show
!== this.isVisible();
17905 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
17909 if ( this.autoClose
) {
17910 this.bindMouseDownListener();
17911 this.bindKeyDownListener();
17913 this.updateDimensions();
17914 this.toggleClipping( true );
17916 this.toggleClipping( false );
17917 if ( this.autoClose
) {
17918 this.unbindMouseDownListener();
17919 this.unbindKeyDownListener();
17928 * Set the size of the popup.
17930 * Changing the size may also change the popup's position depending on the alignment.
17932 * @param {number} width Width in pixels
17933 * @param {number} height Height in pixels
17934 * @param {boolean} [transition=false] Use a smooth transition
17937 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
17938 this.width
= width
;
17939 this.height
= height
!== undefined ? height
: null;
17940 if ( this.isVisible() ) {
17941 this.updateDimensions( transition
);
17946 * Update the size and position.
17948 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
17949 * be called automatically.
17951 * @param {boolean} [transition=false] Use a smooth transition
17954 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
17955 var popupOffset
, originOffset
, containerLeft
, containerWidth
, containerRight
,
17956 popupLeft
, popupRight
, overlapLeft
, overlapRight
, anchorWidth
,
17957 align
= this.align
,
17960 if ( !this.$container
) {
17961 // Lazy-initialize $container if not specified in constructor
17962 this.$container
= $( this.getClosestScrollableElementContainer() );
17965 // Set height and width before measuring things, since it might cause our measurements
17966 // to change (e.g. due to scrollbars appearing or disappearing)
17969 height
: this.height
!== null ? this.height
: 'auto'
17972 // If we are in RTL, we need to flip the alignment, unless it is center
17973 if ( align
=== 'forwards' || align
=== 'backwards' ) {
17974 if ( this.$container
.css( 'direction' ) === 'rtl' ) {
17975 align
= ( { forwards
: 'force-left', backwards
: 'force-right' } )[ this.align
];
17977 align
= ( { forwards
: 'force-right', backwards
: 'force-left' } )[ this.align
];
17982 // Compute initial popupOffset based on alignment
17983 popupOffset
= this.width
* ( { 'force-left': -1, center
: -0.5, 'force-right': 0 } )[ align
];
17985 // Figure out if this will cause the popup to go beyond the edge of the container
17986 originOffset
= this.$element
.offset().left
;
17987 containerLeft
= this.$container
.offset().left
;
17988 containerWidth
= this.$container
.innerWidth();
17989 containerRight
= containerLeft
+ containerWidth
;
17990 popupLeft
= popupOffset
- this.containerPadding
;
17991 popupRight
= popupOffset
+ this.containerPadding
+ this.width
+ this.containerPadding
;
17992 overlapLeft
= ( originOffset
+ popupLeft
) - containerLeft
;
17993 overlapRight
= containerRight
- ( originOffset
+ popupRight
);
17995 // Adjust offset to make the popup not go beyond the edge, if needed
17996 if ( overlapRight
< 0 ) {
17997 popupOffset
+= overlapRight
;
17998 } else if ( overlapLeft
< 0 ) {
17999 popupOffset
-= overlapLeft
;
18002 // Adjust offset to avoid anchor being rendered too close to the edge
18003 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
18004 // TODO: Find a measurement that works for CSS anchors and image anchors
18005 anchorWidth
= this.$anchor
[ 0 ].scrollWidth
* 2;
18006 if ( popupOffset
+ this.width
< anchorWidth
) {
18007 popupOffset
= anchorWidth
- this.width
;
18008 } else if ( -popupOffset
< anchorWidth
) {
18009 popupOffset
= -anchorWidth
;
18012 // Prevent transition from being interrupted
18013 clearTimeout( this.transitionTimeout
);
18014 if ( transition
) {
18015 // Enable transition
18016 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
18019 // Position body relative to anchor
18020 this.$popup
.css( 'margin-left', popupOffset
);
18022 if ( transition
) {
18023 // Prevent transitioning after transition is complete
18024 this.transitionTimeout
= setTimeout( function () {
18025 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
18028 // Prevent transitioning immediately
18029 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
18032 // Reevaluate clipping state since we've relocated and resized the popup
18039 * Set popup alignment
18040 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
18041 * `backwards` or `forwards`.
18043 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
18044 // Validate alignment and transform deprecated values
18045 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
18046 this.align
= { left
: 'force-right', right
: 'force-left' }[ align
] || align
;
18048 this.align
= 'center';
18053 * Get popup alignment
18054 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
18055 * `backwards` or `forwards`.
18057 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
18062 * Progress bars visually display the status of an operation, such as a download,
18063 * and can be either determinate or indeterminate:
18065 * - **determinate** process bars show the percent of an operation that is complete.
18067 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
18068 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
18069 * not use percentages.
18071 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
18074 * // Examples of determinate and indeterminate progress bars.
18075 * var progressBar1 = new OO.ui.ProgressBarWidget( {
18078 * var progressBar2 = new OO.ui.ProgressBarWidget();
18080 * // Create a FieldsetLayout to layout progress bars
18081 * var fieldset = new OO.ui.FieldsetLayout;
18082 * fieldset.addItems( [
18083 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
18084 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
18086 * $( 'body' ).append( fieldset.$element );
18089 * @extends OO.ui.Widget
18092 * @param {Object} [config] Configuration options
18093 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
18094 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
18095 * By default, the progress bar is indeterminate.
18097 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
18098 // Configuration initialization
18099 config
= config
|| {};
18101 // Parent constructor
18102 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
18105 this.$bar
= $( '<div>' );
18106 this.progress
= null;
18109 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
18110 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
18113 role
: 'progressbar',
18114 'aria-valuemin': 0,
18115 'aria-valuemax': 100
18117 .addClass( 'oo-ui-progressBarWidget' )
18118 .append( this.$bar
);
18123 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
18125 /* Static Properties */
18127 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
18132 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
18134 * @return {number|boolean} Progress percent
18136 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
18137 return this.progress
;
18141 * Set the percent of the process completed or `false` for an indeterminate process.
18143 * @param {number|boolean} progress Progress percent or `false` for indeterminate
18145 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
18146 this.progress
= progress
;
18148 if ( progress
!== false ) {
18149 this.$bar
.css( 'width', this.progress
+ '%' );
18150 this.$element
.attr( 'aria-valuenow', this.progress
);
18152 this.$bar
.css( 'width', '' );
18153 this.$element
.removeAttr( 'aria-valuenow' );
18155 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress
);
18159 * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
18160 * and a menu of search results, which is displayed beneath the query
18161 * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
18162 * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
18163 * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
18165 * Each time the query is changed, the search result menu is cleared and repopulated. Please see
18166 * the [OOjs UI demos][1] for an example.
18168 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
18171 * @extends OO.ui.Widget
18174 * @param {Object} [config] Configuration options
18175 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
18176 * @cfg {string} [value] Initial query value
18178 OO
.ui
.SearchWidget
= function OoUiSearchWidget( config
) {
18179 // Configuration initialization
18180 config
= config
|| {};
18182 // Parent constructor
18183 OO
.ui
.SearchWidget
.parent
.call( this, config
);
18186 this.query
= new OO
.ui
.TextInputWidget( {
18188 placeholder
: config
.placeholder
,
18189 value
: config
.value
18191 this.results
= new OO
.ui
.SelectWidget();
18192 this.$query
= $( '<div>' );
18193 this.$results
= $( '<div>' );
18196 this.query
.connect( this, {
18197 change
: 'onQueryChange',
18198 enter
: 'onQueryEnter'
18200 this.query
.$input
.on( 'keydown', this.onQueryKeydown
.bind( this ) );
18204 .addClass( 'oo-ui-searchWidget-query' )
18205 .append( this.query
.$element
);
18207 .addClass( 'oo-ui-searchWidget-results' )
18208 .append( this.results
.$element
);
18210 .addClass( 'oo-ui-searchWidget' )
18211 .append( this.$results
, this.$query
);
18216 OO
.inheritClass( OO
.ui
.SearchWidget
, OO
.ui
.Widget
);
18221 * Handle query key down events.
18224 * @param {jQuery.Event} e Key down event
18226 OO
.ui
.SearchWidget
.prototype.onQueryKeydown = function ( e
) {
18227 var highlightedItem
, nextItem
,
18228 dir
= e
.which
=== OO
.ui
.Keys
.DOWN
? 1 : ( e
.which
=== OO
.ui
.Keys
.UP
? -1 : 0 );
18231 highlightedItem
= this.results
.getHighlightedItem();
18232 if ( !highlightedItem
) {
18233 highlightedItem
= this.results
.getSelectedItem();
18235 nextItem
= this.results
.getRelativeSelectableItem( highlightedItem
, dir
);
18236 this.results
.highlightItem( nextItem
);
18237 nextItem
.scrollElementIntoView();
18242 * Handle select widget select events.
18244 * Clears existing results. Subclasses should repopulate items according to new query.
18247 * @param {string} value New value
18249 OO
.ui
.SearchWidget
.prototype.onQueryChange = function () {
18251 this.results
.clearItems();
18255 * Handle select widget enter key events.
18257 * Chooses highlighted item.
18260 * @param {string} value New value
18262 OO
.ui
.SearchWidget
.prototype.onQueryEnter = function () {
18263 var highlightedItem
= this.results
.getHighlightedItem();
18264 if ( highlightedItem
) {
18265 this.results
.chooseItem( highlightedItem
);
18270 * Get the query input.
18272 * @return {OO.ui.TextInputWidget} Query input
18274 OO
.ui
.SearchWidget
.prototype.getQuery = function () {
18279 * Get the search results menu.
18281 * @return {OO.ui.SelectWidget} Menu of search results
18283 OO
.ui
.SearchWidget
.prototype.getResults = function () {
18284 return this.results
;
18288 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
18289 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
18290 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
18293 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
18294 * information, please see the [OOjs UI documentation on MediaWiki][1].
18297 * // Example of a select widget with three options
18298 * var select = new OO.ui.SelectWidget( {
18300 * new OO.ui.OptionWidget( {
18302 * label: 'Option One',
18304 * new OO.ui.OptionWidget( {
18306 * label: 'Option Two',
18308 * new OO.ui.OptionWidget( {
18310 * label: 'Option Three',
18314 * $( 'body' ).append( select.$element );
18316 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
18320 * @extends OO.ui.Widget
18321 * @mixins OO.ui.mixin.GroupWidget
18324 * @param {Object} [config] Configuration options
18325 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
18326 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
18327 * the [OOjs UI documentation on MediaWiki] [2] for examples.
18328 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
18330 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
18331 // Configuration initialization
18332 config
= config
|| {};
18334 // Parent constructor
18335 OO
.ui
.SelectWidget
.parent
.call( this, config
);
18337 // Mixin constructors
18338 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
18341 this.pressed
= false;
18342 this.selecting
= null;
18343 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
18344 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
18345 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
18346 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
18347 this.keyPressBuffer
= '';
18348 this.keyPressBufferTimer
= null;
18351 this.connect( this, {
18354 this.$element
.on( {
18355 mousedown
: this.onMouseDown
.bind( this ),
18356 mouseover
: this.onMouseOver
.bind( this ),
18357 mouseleave
: this.onMouseLeave
.bind( this )
18362 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
18363 .attr( 'role', 'listbox' );
18364 if ( Array
.isArray( config
.items
) ) {
18365 this.addItems( config
.items
);
18371 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
18373 // Need to mixin base class as well
18374 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupElement
);
18375 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
18378 OO
.ui
.SelectWidget
.static.passAllFilter = function () {
18387 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
18389 * @param {OO.ui.OptionWidget|null} item Highlighted item
18395 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
18396 * pressed state of an option.
18398 * @param {OO.ui.OptionWidget|null} item Pressed item
18404 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
18406 * @param {OO.ui.OptionWidget|null} item Selected item
18411 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
18412 * @param {OO.ui.OptionWidget} item Chosen item
18418 * An `add` event is emitted when options are added to the select with the #addItems method.
18420 * @param {OO.ui.OptionWidget[]} items Added items
18421 * @param {number} index Index of insertion point
18427 * A `remove` event is emitted when options are removed from the select with the #clearItems
18428 * or #removeItems methods.
18430 * @param {OO.ui.OptionWidget[]} items Removed items
18436 * Handle mouse down events.
18439 * @param {jQuery.Event} e Mouse down event
18441 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
18444 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
18445 this.togglePressed( true );
18446 item
= this.getTargetItem( e
);
18447 if ( item
&& item
.isSelectable() ) {
18448 this.pressItem( item
);
18449 this.selecting
= item
;
18450 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
18451 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
18458 * Handle mouse up events.
18461 * @param {jQuery.Event} e Mouse up event
18463 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
18466 this.togglePressed( false );
18467 if ( !this.selecting
) {
18468 item
= this.getTargetItem( e
);
18469 if ( item
&& item
.isSelectable() ) {
18470 this.selecting
= item
;
18473 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
18474 this.pressItem( null );
18475 this.chooseItem( this.selecting
);
18476 this.selecting
= null;
18479 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
18480 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
18486 * Handle mouse move events.
18489 * @param {jQuery.Event} e Mouse move event
18491 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
18494 if ( !this.isDisabled() && this.pressed
) {
18495 item
= this.getTargetItem( e
);
18496 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
18497 this.pressItem( item
);
18498 this.selecting
= item
;
18505 * Handle mouse over events.
18508 * @param {jQuery.Event} e Mouse over event
18510 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
18513 if ( !this.isDisabled() ) {
18514 item
= this.getTargetItem( e
);
18515 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
18521 * Handle mouse leave events.
18524 * @param {jQuery.Event} e Mouse over event
18526 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
18527 if ( !this.isDisabled() ) {
18528 this.highlightItem( null );
18534 * Handle key down events.
18537 * @param {jQuery.Event} e Key down event
18539 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
18542 currentItem
= this.getHighlightedItem() || this.getSelectedItem();
18544 if ( !this.isDisabled() && this.isVisible() ) {
18545 switch ( e
.keyCode
) {
18546 case OO
.ui
.Keys
.ENTER
:
18547 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
18548 // Was only highlighted, now let's select it. No-op if already selected.
18549 this.chooseItem( currentItem
);
18553 case OO
.ui
.Keys
.UP
:
18554 case OO
.ui
.Keys
.LEFT
:
18555 this.clearKeyPressBuffer();
18556 nextItem
= this.getRelativeSelectableItem( currentItem
, -1 );
18559 case OO
.ui
.Keys
.DOWN
:
18560 case OO
.ui
.Keys
.RIGHT
:
18561 this.clearKeyPressBuffer();
18562 nextItem
= this.getRelativeSelectableItem( currentItem
, 1 );
18565 case OO
.ui
.Keys
.ESCAPE
:
18566 case OO
.ui
.Keys
.TAB
:
18567 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
18568 currentItem
.setHighlighted( false );
18570 this.unbindKeyDownListener();
18571 this.unbindKeyPressListener();
18572 // Don't prevent tabbing away / defocusing
18578 if ( nextItem
.constructor.static.highlightable
) {
18579 this.highlightItem( nextItem
);
18581 this.chooseItem( nextItem
);
18583 nextItem
.scrollElementIntoView();
18587 // Can't just return false, because e is not always a jQuery event
18588 e
.preventDefault();
18589 e
.stopPropagation();
18595 * Bind key down listener.
18599 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
18600 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
18604 * Unbind key down listener.
18608 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
18609 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
18613 * Clear the key-press buffer
18617 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
18618 if ( this.keyPressBufferTimer
) {
18619 clearTimeout( this.keyPressBufferTimer
);
18620 this.keyPressBufferTimer
= null;
18622 this.keyPressBuffer
= '';
18626 * Handle key press events.
18629 * @param {jQuery.Event} e Key press event
18631 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
18632 var c
, filter
, item
;
18634 if ( !e
.charCode
) {
18635 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
18636 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
18641 if ( String
.fromCodePoint
) {
18642 c
= String
.fromCodePoint( e
.charCode
);
18644 c
= String
.fromCharCode( e
.charCode
);
18647 if ( this.keyPressBufferTimer
) {
18648 clearTimeout( this.keyPressBufferTimer
);
18650 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
18652 item
= this.getHighlightedItem() || this.getSelectedItem();
18654 if ( this.keyPressBuffer
=== c
) {
18655 // Common (if weird) special case: typing "xxxx" will cycle through all
18656 // the items beginning with "x".
18658 item
= this.getRelativeSelectableItem( item
, 1 );
18661 this.keyPressBuffer
+= c
;
18664 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
18665 if ( !item
|| !filter( item
) ) {
18666 item
= this.getRelativeSelectableItem( item
, 1, filter
);
18669 if ( item
.constructor.static.highlightable
) {
18670 this.highlightItem( item
);
18672 this.chooseItem( item
);
18674 item
.scrollElementIntoView();
18681 * Get a matcher for the specific string
18684 * @param {string} s String to match against items
18685 * @param {boolean} [exact=false] Only accept exact matches
18686 * @return {Function} function ( OO.ui.OptionItem ) => boolean
18688 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
18691 if ( s
.normalize
) {
18694 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
18695 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
18699 re
= new RegExp( re
, 'i' );
18700 return function ( item
) {
18701 var l
= item
.getLabel();
18702 if ( typeof l
!== 'string' ) {
18703 l
= item
.$label
.text();
18705 if ( l
.normalize
) {
18708 return re
.test( l
);
18713 * Bind key press listener.
18717 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
18718 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
18722 * Unbind key down listener.
18724 * If you override this, be sure to call this.clearKeyPressBuffer() from your
18729 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
18730 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
18731 this.clearKeyPressBuffer();
18735 * Visibility change handler
18738 * @param {boolean} visible
18740 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
18742 this.clearKeyPressBuffer();
18747 * Get the closest item to a jQuery.Event.
18750 * @param {jQuery.Event} e
18751 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
18753 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
18754 return $( e
.target
).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
18758 * Get selected item.
18760 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
18762 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
18765 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
18766 if ( this.items
[ i
].isSelected() ) {
18767 return this.items
[ i
];
18774 * Get highlighted item.
18776 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
18778 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
18781 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
18782 if ( this.items
[ i
].isHighlighted() ) {
18783 return this.items
[ i
];
18790 * Toggle pressed state.
18792 * Press is a state that occurs when a user mouses down on an item, but
18793 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
18794 * until the user releases the mouse.
18796 * @param {boolean} pressed An option is being pressed
18798 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
18799 if ( pressed
=== undefined ) {
18800 pressed
= !this.pressed
;
18802 if ( pressed
!== this.pressed
) {
18804 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
18805 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
18806 this.pressed
= pressed
;
18811 * Highlight an option. If the `item` param is omitted, no options will be highlighted
18812 * and any existing highlight will be removed. The highlight is mutually exclusive.
18814 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
18818 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
18819 var i
, len
, highlighted
,
18822 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
18823 highlighted
= this.items
[ i
] === item
;
18824 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
18825 this.items
[ i
].setHighlighted( highlighted
);
18830 this.emit( 'highlight', item
);
18837 * Fetch an item by its label.
18839 * @param {string} label Label of the item to select.
18840 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
18841 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
18843 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
18844 var i
, item
, found
,
18845 len
= this.items
.length
,
18846 filter
= this.getItemMatcher( label
, true );
18848 for ( i
= 0; i
< len
; i
++ ) {
18849 item
= this.items
[ i
];
18850 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
18857 filter
= this.getItemMatcher( label
, false );
18858 for ( i
= 0; i
< len
; i
++ ) {
18859 item
= this.items
[ i
];
18860 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
18876 * Programmatically select an option by its label. If the item does not exist,
18877 * all options will be deselected.
18879 * @param {string} [label] Label of the item to select.
18880 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
18884 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
18885 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
18886 if ( label
=== undefined || !itemFromLabel
) {
18887 return this.selectItem();
18889 return this.selectItem( itemFromLabel
);
18893 * Programmatically select an option by its data. If the `data` parameter is omitted,
18894 * or if the item does not exist, all options will be deselected.
18896 * @param {Object|string} [data] Value of the item to select, omit to deselect all
18900 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
18901 var itemFromData
= this.getItemFromData( data
);
18902 if ( data
=== undefined || !itemFromData
) {
18903 return this.selectItem();
18905 return this.selectItem( itemFromData
);
18909 * Programmatically select an option by its reference. If the `item` parameter is omitted,
18910 * all options will be deselected.
18912 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
18916 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
18917 var i
, len
, selected
,
18920 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
18921 selected
= this.items
[ i
] === item
;
18922 if ( this.items
[ i
].isSelected() !== selected
) {
18923 this.items
[ i
].setSelected( selected
);
18928 this.emit( 'select', item
);
18937 * Press is a state that occurs when a user mouses down on an item, but has not
18938 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
18939 * releases the mouse.
18941 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
18945 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
18946 var i
, len
, pressed
,
18949 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
18950 pressed
= this.items
[ i
] === item
;
18951 if ( this.items
[ i
].isPressed() !== pressed
) {
18952 this.items
[ i
].setPressed( pressed
);
18957 this.emit( 'press', item
);
18966 * Note that ‘choose’ should never be modified programmatically. A user can choose
18967 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
18968 * use the #selectItem method.
18970 * This method is identical to #selectItem, but may vary in subclasses that take additional action
18971 * when users choose an item with the keyboard or mouse.
18973 * @param {OO.ui.OptionWidget} item Item to choose
18977 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
18979 this.selectItem( item
);
18980 this.emit( 'choose', item
);
18987 * Get an option by its position relative to the specified item (or to the start of the option array,
18988 * if item is `null`). The direction in which to search through the option array is specified with a
18989 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
18990 * `null` if there are no options in the array.
18992 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
18993 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
18994 * @param {Function} filter Only consider items for which this function returns
18995 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
18996 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
18998 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
, filter
) {
18999 var currentIndex
, nextIndex
, i
,
19000 increase
= direction
> 0 ? 1 : -1,
19001 len
= this.items
.length
;
19003 if ( !$.isFunction( filter
) ) {
19004 filter
= OO
.ui
.SelectWidget
.static.passAllFilter
;
19007 if ( item
instanceof OO
.ui
.OptionWidget
) {
19008 currentIndex
= this.items
.indexOf( item
);
19009 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
19011 // If no item is selected and moving forward, start at the beginning.
19012 // If moving backward, start at the end.
19013 nextIndex
= direction
> 0 ? 0 : len
- 1;
19016 for ( i
= 0; i
< len
; i
++ ) {
19017 item
= this.items
[ nextIndex
];
19018 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
19021 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
19027 * Get the next selectable item or `null` if there are no selectable items.
19028 * Disabled options and menu-section markers and breaks are not selectable.
19030 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
19032 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
19035 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
19036 item
= this.items
[ i
];
19037 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() ) {
19046 * Add an array of options to the select. Optionally, an index number can be used to
19047 * specify an insertion point.
19049 * @param {OO.ui.OptionWidget[]} items Items to add
19050 * @param {number} [index] Index to insert items after
19054 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
19056 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
19058 // Always provide an index, even if it was omitted
19059 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
19065 * Remove the specified array of options from the select. Options will be detached
19066 * from the DOM, not removed, so they can be reused later. To remove all options from
19067 * the select, you may wish to use the #clearItems method instead.
19069 * @param {OO.ui.OptionWidget[]} items Items to remove
19073 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
19076 // Deselect items being removed
19077 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
19079 if ( item
.isSelected() ) {
19080 this.selectItem( null );
19085 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
19087 this.emit( 'remove', items
);
19093 * Clear all options from the select. Options will be detached from the DOM, not removed,
19094 * so that they can be reused later. To remove a subset of options from the select, use
19095 * the #removeItems method.
19100 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
19101 var items
= this.items
.slice();
19104 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
19107 this.selectItem( null );
19109 this.emit( 'remove', items
);
19115 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
19116 * button options and is used together with
19117 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
19118 * highlighting, choosing, and selecting mutually exclusive options. Please see
19119 * the [OOjs UI documentation on MediaWiki] [1] for more information.
19122 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
19123 * var option1 = new OO.ui.ButtonOptionWidget( {
19125 * label: 'Option 1',
19126 * title: 'Button option 1'
19129 * var option2 = new OO.ui.ButtonOptionWidget( {
19131 * label: 'Option 2',
19132 * title: 'Button option 2'
19135 * var option3 = new OO.ui.ButtonOptionWidget( {
19137 * label: 'Option 3',
19138 * title: 'Button option 3'
19141 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
19142 * items: [ option1, option2, option3 ]
19144 * $( 'body' ).append( buttonSelect.$element );
19146 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
19149 * @extends OO.ui.SelectWidget
19150 * @mixins OO.ui.mixin.TabIndexedElement
19153 * @param {Object} [config] Configuration options
19155 OO
.ui
.ButtonSelectWidget
= function OoUiButtonSelectWidget( config
) {
19156 // Parent constructor
19157 OO
.ui
.ButtonSelectWidget
.parent
.call( this, config
);
19159 // Mixin constructors
19160 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
19163 this.$element
.on( {
19164 focus
: this.bindKeyDownListener
.bind( this ),
19165 blur
: this.unbindKeyDownListener
.bind( this )
19169 this.$element
.addClass( 'oo-ui-buttonSelectWidget' );
19174 OO
.inheritClass( OO
.ui
.ButtonSelectWidget
, OO
.ui
.SelectWidget
);
19175 OO
.mixinClass( OO
.ui
.ButtonSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
19178 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
19179 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
19180 * an interface for adding, removing and selecting options.
19181 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
19183 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
19184 * OO.ui.RadioSelectInputWidget instead.
19187 * // A RadioSelectWidget with RadioOptions.
19188 * var option1 = new OO.ui.RadioOptionWidget( {
19190 * label: 'Selected radio option'
19193 * var option2 = new OO.ui.RadioOptionWidget( {
19195 * label: 'Unselected radio option'
19198 * var radioSelect=new OO.ui.RadioSelectWidget( {
19199 * items: [ option1, option2 ]
19202 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
19203 * radioSelect.selectItem( option1 );
19205 * $( 'body' ).append( radioSelect.$element );
19207 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
19211 * @extends OO.ui.SelectWidget
19212 * @mixins OO.ui.mixin.TabIndexedElement
19215 * @param {Object} [config] Configuration options
19217 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
19218 // Parent constructor
19219 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
19221 // Mixin constructors
19222 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
19225 this.$element
.on( {
19226 focus
: this.bindKeyDownListener
.bind( this ),
19227 blur
: this.unbindKeyDownListener
.bind( this )
19232 .addClass( 'oo-ui-radioSelectWidget' )
19233 .attr( 'role', 'radiogroup' );
19238 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
19239 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
19242 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
19243 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
19244 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
19245 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
19246 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
19247 * and customized to be opened, closed, and displayed as needed.
19249 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
19250 * mouse outside the menu.
19252 * Menus also have support for keyboard interaction:
19254 * - Enter/Return key: choose and select a menu option
19255 * - Up-arrow key: highlight the previous menu option
19256 * - Down-arrow key: highlight the next menu option
19257 * - Esc key: hide the menu
19259 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
19260 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
19263 * @extends OO.ui.SelectWidget
19264 * @mixins OO.ui.mixin.ClippableElement
19267 * @param {Object} [config] Configuration options
19268 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
19269 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
19270 * and {@link OO.ui.mixin.LookupElement LookupElement}
19271 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
19272 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiSelectWidget CapsuleMultiSelectWidget}
19273 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
19274 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
19275 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
19276 * that button, unless the button (or its parent widget) is passed in here.
19277 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
19278 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
19280 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
19281 // Configuration initialization
19282 config
= config
|| {};
19284 // Parent constructor
19285 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
19287 // Mixin constructors
19288 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
19291 this.newItems
= null;
19292 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
19293 this.filterFromInput
= !!config
.filterFromInput
;
19294 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
19295 this.$widget
= config
.widget
? config
.widget
.$element
: null;
19296 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
19297 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
19301 .addClass( 'oo-ui-menuSelectWidget' )
19302 .attr( 'role', 'menu' );
19304 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
19305 // that reference properties not initialized at that time of parent class construction
19306 // TODO: Find a better way to handle post-constructor setup
19307 this.visible
= false;
19308 this.$element
.addClass( 'oo-ui-element-hidden' );
19313 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
19314 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
19319 * Handles document mouse down events.
19322 * @param {jQuery.Event} e Key down event
19324 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
19326 !OO
.ui
.contains( this.$element
[ 0 ], e
.target
, true ) &&
19327 ( !this.$widget
|| !OO
.ui
.contains( this.$widget
[ 0 ], e
.target
, true ) )
19329 this.toggle( false );
19336 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
19337 var currentItem
= this.getHighlightedItem() || this.getSelectedItem();
19339 if ( !this.isDisabled() && this.isVisible() ) {
19340 switch ( e
.keyCode
) {
19341 case OO
.ui
.Keys
.LEFT
:
19342 case OO
.ui
.Keys
.RIGHT
:
19343 // Do nothing if a text field is associated, arrow keys will be handled natively
19344 if ( !this.$input
) {
19345 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
19348 case OO
.ui
.Keys
.ESCAPE
:
19349 case OO
.ui
.Keys
.TAB
:
19350 if ( currentItem
) {
19351 currentItem
.setHighlighted( false );
19353 this.toggle( false );
19354 // Don't prevent tabbing away, prevent defocusing
19355 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
19356 e
.preventDefault();
19357 e
.stopPropagation();
19361 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
19368 * Update menu item visibility after input changes.
19371 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
19373 len
= this.items
.length
,
19374 showAll
= !this.isVisible(),
19375 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
19377 for ( i
= 0; i
< len
; i
++ ) {
19378 item
= this.items
[ i
];
19379 if ( item
instanceof OO
.ui
.OptionWidget
) {
19380 item
.toggle( showAll
|| filter( item
) );
19384 // Reevaluate clipping
19391 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
19392 if ( this.$input
) {
19393 this.$input
.on( 'keydown', this.onKeyDownHandler
);
19395 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
19402 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
19403 if ( this.$input
) {
19404 this.$input
.off( 'keydown', this.onKeyDownHandler
);
19406 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
19413 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
19414 if ( this.$input
) {
19415 if ( this.filterFromInput
) {
19416 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
19419 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
19426 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
19427 if ( this.$input
) {
19428 if ( this.filterFromInput
) {
19429 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
19430 this.updateItemVisibility();
19433 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
19440 * When a user chooses an item, the menu is closed.
19442 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
19443 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
19444 * @param {OO.ui.OptionWidget} item Item to choose
19447 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
19448 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
19449 this.toggle( false );
19456 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
19460 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
19463 if ( !this.newItems
) {
19464 this.newItems
= [];
19467 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
19469 if ( this.isVisible() ) {
19470 // Defer fitting label until item has been attached
19473 this.newItems
.push( item
);
19477 // Reevaluate clipping
19486 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
19488 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
19490 // Reevaluate clipping
19499 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
19501 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
19503 // Reevaluate clipping
19512 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
19513 var i
, len
, change
;
19515 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
19516 change
= visible
!== this.isVisible();
19519 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
19523 this.bindKeyDownListener();
19524 this.bindKeyPressListener();
19526 if ( this.newItems
&& this.newItems
.length
) {
19527 for ( i
= 0, len
= this.newItems
.length
; i
< len
; i
++ ) {
19528 this.newItems
[ i
].fitLabel();
19530 this.newItems
= null;
19532 this.toggleClipping( true );
19535 if ( this.autoHide
) {
19536 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
19539 this.unbindKeyDownListener();
19540 this.unbindKeyPressListener();
19541 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
19542 this.toggleClipping( false );
19550 * FloatingMenuSelectWidget is a menu that will stick under a specified
19551 * container, even when it is inserted elsewhere in the document (for example,
19552 * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
19553 * menu from being clipped too aggresively.
19555 * The menu's position is automatically calculated and maintained when the menu
19556 * is toggled or the window is resized.
19558 * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
19561 * @extends OO.ui.MenuSelectWidget
19562 * @mixins OO.ui.mixin.FloatableElement
19565 * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
19566 * Deprecated, omit this parameter and specify `$container` instead.
19567 * @param {Object} [config] Configuration options
19568 * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
19570 OO
.ui
.FloatingMenuSelectWidget
= function OoUiFloatingMenuSelectWidget( inputWidget
, config
) {
19571 // Allow 'inputWidget' parameter and config for backwards compatibility
19572 if ( OO
.isPlainObject( inputWidget
) && config
=== undefined ) {
19573 config
= inputWidget
;
19574 inputWidget
= config
.inputWidget
;
19577 // Configuration initialization
19578 config
= config
|| {};
19580 // Parent constructor
19581 OO
.ui
.FloatingMenuSelectWidget
.parent
.call( this, config
);
19583 // Properties (must be set before mixin constructors)
19584 this.inputWidget
= inputWidget
; // For backwards compatibility
19585 this.$container
= config
.$container
|| this.inputWidget
.$element
;
19587 // Mixins constructors
19588 OO
.ui
.mixin
.FloatableElement
.call( this, $.extend( {}, config
, { $floatableContainer
: this.$container
} ) );
19591 this.$element
.addClass( 'oo-ui-floatingMenuSelectWidget' );
19592 // For backwards compatibility
19593 this.$element
.addClass( 'oo-ui-textInputMenuSelectWidget' );
19598 OO
.inheritClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.MenuSelectWidget
);
19599 OO
.mixinClass( OO
.ui
.FloatingMenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
19601 // For backwards compatibility
19602 OO
.ui
.TextInputMenuSelectWidget
= OO
.ui
.FloatingMenuSelectWidget
;
19609 OO
.ui
.FloatingMenuSelectWidget
.prototype.toggle = function ( visible
) {
19611 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
19612 change
= visible
!== this.isVisible();
19614 if ( change
&& visible
) {
19615 // Make sure the width is set before the parent method runs.
19616 this.setIdealSize( this.$container
.width() );
19620 // This will call this.clip(), which is nonsensical since we're not positioned yet...
19621 OO
.ui
.FloatingMenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
19624 this.togglePositioning( this.isVisible() );
19631 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
19632 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
19634 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
19637 * @extends OO.ui.SelectWidget
19638 * @mixins OO.ui.mixin.TabIndexedElement
19641 * @param {Object} [config] Configuration options
19643 OO
.ui
.OutlineSelectWidget
= function OoUiOutlineSelectWidget( config
) {
19644 // Parent constructor
19645 OO
.ui
.OutlineSelectWidget
.parent
.call( this, config
);
19647 // Mixin constructors
19648 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
19651 this.$element
.on( {
19652 focus
: this.bindKeyDownListener
.bind( this ),
19653 blur
: this.unbindKeyDownListener
.bind( this )
19657 this.$element
.addClass( 'oo-ui-outlineSelectWidget' );
19662 OO
.inheritClass( OO
.ui
.OutlineSelectWidget
, OO
.ui
.SelectWidget
);
19663 OO
.mixinClass( OO
.ui
.OutlineSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
19666 * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
19668 * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
19671 * @extends OO.ui.SelectWidget
19672 * @mixins OO.ui.mixin.TabIndexedElement
19675 * @param {Object} [config] Configuration options
19677 OO
.ui
.TabSelectWidget
= function OoUiTabSelectWidget( config
) {
19678 // Parent constructor
19679 OO
.ui
.TabSelectWidget
.parent
.call( this, config
);
19681 // Mixin constructors
19682 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
19685 this.$element
.on( {
19686 focus
: this.bindKeyDownListener
.bind( this ),
19687 blur
: this.unbindKeyDownListener
.bind( this )
19691 this.$element
.addClass( 'oo-ui-tabSelectWidget' );
19696 OO
.inheritClass( OO
.ui
.TabSelectWidget
, OO
.ui
.SelectWidget
);
19697 OO
.mixinClass( OO
.ui
.TabSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
19700 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
19701 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
19702 * (to adjust the value in increments) to allow the user to enter a number.
19705 * // Example: A NumberInputWidget.
19706 * var numberInput = new OO.ui.NumberInputWidget( {
19707 * label: 'NumberInputWidget',
19708 * input: { value: 5, min: 1, max: 10 }
19710 * $( 'body' ).append( numberInput.$element );
19713 * @extends OO.ui.Widget
19716 * @param {Object} [config] Configuration options
19717 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
19718 * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
19719 * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
19720 * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
19721 * @cfg {number} [min=-Infinity] Minimum allowed value
19722 * @cfg {number} [max=Infinity] Maximum allowed value
19723 * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
19724 * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
19726 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
19727 // Configuration initialization
19728 config
= $.extend( {
19736 // Parent constructor
19737 OO
.ui
.NumberInputWidget
.parent
.call( this, config
);
19740 this.input
= new OO
.ui
.TextInputWidget( $.extend(
19742 disabled
: this.isDisabled()
19746 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
19748 disabled
: this.isDisabled(),
19751 config
.minusButton
,
19753 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
19757 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
19759 disabled
: this.isDisabled(),
19764 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
19770 this.input
.connect( this, {
19771 change
: this.emit
.bind( this, 'change' ),
19772 enter
: this.emit
.bind( this, 'enter' )
19774 this.input
.$input
.on( {
19775 keydown
: this.onKeyDown
.bind( this ),
19776 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
19778 this.plusButton
.connect( this, {
19779 click
: [ 'onButtonClick', +1 ]
19781 this.minusButton
.connect( this, {
19782 click
: [ 'onButtonClick', -1 ]
19786 this.setIsInteger( !!config
.isInteger
);
19787 this.setRange( config
.min
, config
.max
);
19788 this.setStep( config
.step
, config
.pageStep
);
19790 this.$field
= $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
19792 this.minusButton
.$element
,
19793 this.input
.$element
,
19794 this.plusButton
.$element
19796 this.$element
.addClass( 'oo-ui-numberInputWidget' ).append( this.$field
);
19797 this.input
.setValidation( this.validateNumber
.bind( this ) );
19802 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.Widget
);
19807 * A `change` event is emitted when the value of the input changes.
19813 * An `enter` event is emitted when the user presses 'enter' inside the text box.
19821 * Set whether only integers are allowed
19822 * @param {boolean} flag
19824 OO
.ui
.NumberInputWidget
.prototype.setIsInteger = function ( flag
) {
19825 this.isInteger
= !!flag
;
19826 this.input
.setValidityFlag();
19830 * Get whether only integers are allowed
19831 * @return {boolean} Flag value
19833 OO
.ui
.NumberInputWidget
.prototype.getIsInteger = function () {
19834 return this.isInteger
;
19838 * Set the range of allowed values
19839 * @param {number} min Minimum allowed value
19840 * @param {number} max Maximum allowed value
19842 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
19844 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
19848 this.input
.setValidityFlag();
19852 * Get the current range
19853 * @return {number[]} Minimum and maximum values
19855 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
19856 return [ this.min
, this.max
];
19860 * Set the stepping deltas
19861 * @param {number} step Normal step
19862 * @param {number|null} pageStep Page step. If null, 10 * step will be used.
19864 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( step
, pageStep
) {
19866 throw new Error( 'Step value must be positive' );
19868 if ( pageStep
=== null ) {
19869 pageStep
= step
* 10;
19870 } else if ( pageStep
<= 0 ) {
19871 throw new Error( 'Page step value must be positive' );
19874 this.pageStep
= pageStep
;
19878 * Get the current stepping values
19879 * @return {number[]} Step and page step
19881 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
19882 return [ this.step
, this.pageStep
];
19886 * Get the current value of the widget
19889 OO
.ui
.NumberInputWidget
.prototype.getValue = function () {
19890 return this.input
.getValue();
19894 * Get the current value of the widget as a number
19895 * @return {number} May be NaN, or an invalid number
19897 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
19898 return +this.input
.getValue();
19902 * Set the value of the widget
19903 * @param {string} value Invalid values are allowed
19905 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
19906 this.input
.setValue( value
);
19910 * Adjust the value of the widget
19911 * @param {number} delta Adjustment amount
19913 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
19914 var n
, v
= this.getNumericValue();
19917 if ( isNaN( delta
) || !isFinite( delta
) ) {
19918 throw new Error( 'Delta must be a finite number' );
19921 if ( isNaN( v
) ) {
19925 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
19926 if ( this.isInteger
) {
19927 n
= Math
.round( n
);
19932 this.setValue( n
);
19939 * @param {string} value Field value
19940 * @return {boolean}
19942 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
19944 if ( isNaN( n
) || !isFinite( n
) ) {
19948 /*jshint bitwise: false */
19949 if ( this.isInteger
&& ( n
| 0 ) !== n
) {
19952 /*jshint bitwise: true */
19954 if ( n
< this.min
|| n
> this.max
) {
19962 * Handle mouse click events.
19965 * @param {number} dir +1 or -1
19967 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
19968 this.adjustValue( dir
* this.step
);
19972 * Handle mouse wheel events.
19975 * @param {jQuery.Event} event
19977 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
19980 // Standard 'wheel' event
19981 if ( event
.originalEvent
.deltaMode
!== undefined ) {
19982 this.sawWheelEvent
= true;
19984 if ( event
.originalEvent
.deltaY
) {
19985 delta
= -event
.originalEvent
.deltaY
;
19986 } else if ( event
.originalEvent
.deltaX
) {
19987 delta
= event
.originalEvent
.deltaX
;
19990 // Non-standard events
19991 if ( !this.sawWheelEvent
) {
19992 if ( event
.originalEvent
.wheelDeltaX
) {
19993 delta
= -event
.originalEvent
.wheelDeltaX
;
19994 } else if ( event
.originalEvent
.wheelDeltaY
) {
19995 delta
= event
.originalEvent
.wheelDeltaY
;
19996 } else if ( event
.originalEvent
.wheelDelta
) {
19997 delta
= event
.originalEvent
.wheelDelta
;
19998 } else if ( event
.originalEvent
.detail
) {
19999 delta
= -event
.originalEvent
.detail
;
20004 delta
= delta
< 0 ? -1 : 1;
20005 this.adjustValue( delta
* this.step
);
20012 * Handle key down events.
20015 * @param {jQuery.Event} e Key down event
20017 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
20018 if ( !this.isDisabled() ) {
20019 switch ( e
.which
) {
20020 case OO
.ui
.Keys
.UP
:
20021 this.adjustValue( this.step
);
20023 case OO
.ui
.Keys
.DOWN
:
20024 this.adjustValue( -this.step
);
20026 case OO
.ui
.Keys
.PAGEUP
:
20027 this.adjustValue( this.pageStep
);
20029 case OO
.ui
.Keys
.PAGEDOWN
:
20030 this.adjustValue( -this.pageStep
);
20039 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
20041 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
20043 if ( this.input
) {
20044 this.input
.setDisabled( this.isDisabled() );
20046 if ( this.minusButton
) {
20047 this.minusButton
.setDisabled( this.isDisabled() );
20049 if ( this.plusButton
) {
20050 this.plusButton
.setDisabled( this.isDisabled() );
20057 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
20058 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
20059 * visually by a slider in the leftmost position.
20062 * // Toggle switches in the 'off' and 'on' position.
20063 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
20064 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
20068 * // Create a FieldsetLayout to layout and label switches
20069 * var fieldset = new OO.ui.FieldsetLayout( {
20070 * label: 'Toggle switches'
20072 * fieldset.addItems( [
20073 * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
20074 * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
20076 * $( 'body' ).append( fieldset.$element );
20079 * @extends OO.ui.ToggleWidget
20080 * @mixins OO.ui.mixin.TabIndexedElement
20083 * @param {Object} [config] Configuration options
20084 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
20085 * By default, the toggle switch is in the 'off' position.
20087 OO
.ui
.ToggleSwitchWidget
= function OoUiToggleSwitchWidget( config
) {
20088 // Parent constructor
20089 OO
.ui
.ToggleSwitchWidget
.parent
.call( this, config
);
20091 // Mixin constructors
20092 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
20095 this.dragging
= false;
20096 this.dragStart
= null;
20097 this.sliding
= false;
20098 this.$glow
= $( '<span>' );
20099 this.$grip
= $( '<span>' );
20102 this.$element
.on( {
20103 click
: this.onClick
.bind( this ),
20104 keypress
: this.onKeyPress
.bind( this )
20108 this.$glow
.addClass( 'oo-ui-toggleSwitchWidget-glow' );
20109 this.$grip
.addClass( 'oo-ui-toggleSwitchWidget-grip' );
20111 .addClass( 'oo-ui-toggleSwitchWidget' )
20112 .attr( 'role', 'checkbox' )
20113 .append( this.$glow
, this.$grip
);
20118 OO
.inheritClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.ToggleWidget
);
20119 OO
.mixinClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.mixin
.TabIndexedElement
);
20124 * Handle mouse click events.
20127 * @param {jQuery.Event} e Mouse click event
20129 OO
.ui
.ToggleSwitchWidget
.prototype.onClick = function ( e
) {
20130 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
20131 this.setValue( !this.value
);
20137 * Handle key press events.
20140 * @param {jQuery.Event} e Key press event
20142 OO
.ui
.ToggleSwitchWidget
.prototype.onKeyPress = function ( e
) {
20143 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
20144 this.setValue( !this.value
);