3 * https://www.mediawiki.org/wiki/OOjs_UI
5 * Copyright 2011–2015 OOjs Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2015-03-12T19:08:47Z
16 * Namespace for all classes, static methods and static properties.
48 * Get the user's language and any fallback languages.
50 * These language codes are used to localize user interface elements in the user's language.
52 * In environments that provide a localization system, this function should be overridden to
53 * return the user's language(s). The default implementation returns English (en) only.
55 * @return {string[]} Language codes, in descending order of priority
57 OO
.ui
.getUserLanguages = function () {
62 * Get a value in an object keyed by language code.
64 * @param {Object.<string,Mixed>} obj Object keyed by language code
65 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
66 * @param {string} [fallback] Fallback code, used if no matching language can be found
67 * @return {Mixed} Local value
69 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
76 // Known user language
77 langs
= OO
.ui
.getUserLanguages();
78 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
85 if ( obj
[ fallback
] ) {
86 return obj
[ fallback
];
88 // First existing language
97 * Check if a node is contained within another node
99 * Similar to jQuery#contains except a list of containers can be supplied
100 * and a boolean argument allows you to include the container in the match list
102 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
103 * @param {HTMLElement} contained Node to find
104 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
105 * @return {boolean} The node is in the list of target nodes
107 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
109 if ( !Array
.isArray( containers
) ) {
110 containers
= [ containers
];
112 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
113 if ( ( matchContainers
&& contained
=== containers
[ i
] ) || $.contains( containers
[ i
], contained
) ) {
121 * Reconstitute a JavaScript object corresponding to a widget created by
122 * the PHP implementation.
124 * This is an alias for `OO.ui.Element.static.infuse()`.
126 * @param {string|HTMLElement|jQuery} idOrNode
127 * A DOM id (if a string) or node for the widget to infuse.
128 * @return {OO.ui.Element}
129 * The `OO.ui.Element` corresponding to this (infusable) document node.
131 OO
.ui
.infuse = function ( idOrNode
) {
132 return OO
.ui
.Element
.static.infuse( idOrNode
);
137 * Message store for the default implementation of OO.ui.msg
139 * Environments that provide a localization system should not use this, but should override
140 * OO.ui.msg altogether.
145 // Tool tip for a button that moves items in a list down one place
146 'ooui-outline-control-move-down': 'Move item down',
147 // Tool tip for a button that moves items in a list up one place
148 'ooui-outline-control-move-up': 'Move item up',
149 // Tool tip for a button that removes items from a list
150 'ooui-outline-control-remove': 'Remove item',
151 // Label for the toolbar group that contains a list of all other available tools
152 'ooui-toolbar-more': 'More',
153 // Label for the fake tool that expands the full list of tools in a toolbar group
154 'ooui-toolgroup-expand': 'More',
155 // Label for the fake tool that collapses the full list of tools in a toolbar group
156 'ooui-toolgroup-collapse': 'Fewer',
157 // Default label for the accept button of a confirmation dialog
158 'ooui-dialog-message-accept': 'OK',
159 // Default label for the reject button of a confirmation dialog
160 'ooui-dialog-message-reject': 'Cancel',
161 // Title for process dialog error description
162 'ooui-dialog-process-error': 'Something went wrong',
163 // Label for process dialog dismiss error button, visible when describing errors
164 'ooui-dialog-process-dismiss': 'Dismiss',
165 // Label for process dialog retry action button, visible when describing only recoverable errors
166 'ooui-dialog-process-retry': 'Try again',
167 // Label for process dialog retry action button, visible when describing only warnings
168 'ooui-dialog-process-continue': 'Continue'
172 * Get a localized message.
174 * In environments that provide a localization system, this function should be overridden to
175 * return the message translated in the user's language. The default implementation always returns
178 * After the message key, message parameters may optionally be passed. In the default implementation,
179 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
180 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
181 * they support unnamed, ordered message parameters.
184 * @param {string} key Message key
185 * @param {Mixed...} [params] Message parameters
186 * @return {string} Translated message with parameters substituted
188 OO
.ui
.msg = function ( key
) {
189 var message
= messages
[ key
],
190 params
= Array
.prototype.slice
.call( arguments
, 1 );
191 if ( typeof message
=== 'string' ) {
192 // Perform $1 substitution
193 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
194 var i
= parseInt( n
, 10 );
195 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
198 // Return placeholder if message not found
199 message
= '[' + key
+ ']';
205 * Package a message and arguments for deferred resolution.
207 * Use this when you are statically specifying a message and the message may not yet be present.
209 * @param {string} key Message key
210 * @param {Mixed...} [params] Message parameters
211 * @return {Function} Function that returns the resolved message when executed
213 OO
.ui
.deferMsg = function () {
214 var args
= arguments
;
216 return OO
.ui
.msg
.apply( OO
.ui
, args
);
223 * If the message is a function it will be executed, otherwise it will pass through directly.
225 * @param {Function|string} msg Deferred message, or message text
226 * @return {string} Resolved message
228 OO
.ui
.resolveMsg = function ( msg
) {
229 if ( $.isFunction( msg
) ) {
238 * Element that can be marked as pending.
244 * @param {Object} [config] Configuration options
245 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
247 OO
.ui
.PendingElement
= function OoUiPendingElement( config
) {
248 // Configuration initialization
249 config
= config
|| {};
253 this.$pending
= null;
256 this.setPendingElement( config
.$pending
|| this.$element
);
261 OO
.initClass( OO
.ui
.PendingElement
);
266 * Set the pending element (and clean up any existing one).
268 * @param {jQuery} $pending The element to set to pending.
270 OO
.ui
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
271 if ( this.$pending
) {
272 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
275 this.$pending
= $pending
;
276 if ( this.pending
> 0 ) {
277 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
282 * Check if input is pending.
286 OO
.ui
.PendingElement
.prototype.isPending = function () {
287 return !!this.pending
;
291 * Increase the pending stack.
295 OO
.ui
.PendingElement
.prototype.pushPending = function () {
296 if ( this.pending
=== 0 ) {
297 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
298 this.updateThemeClasses();
306 * Reduce the pending stack.
312 OO
.ui
.PendingElement
.prototype.popPending = function () {
313 if ( this.pending
=== 1 ) {
314 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
315 this.updateThemeClasses();
317 this.pending
= Math
.max( 0, this.pending
- 1 );
323 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
324 * Actions can be made available for specific contexts (modes) and circumstances
325 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
327 * ActionSets contain two types of actions:
329 * - 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.
330 * - Other: Other actions include all non-special visible actions.
332 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
335 * // Example: An action set used in a process dialog
336 * function ProcessDialog( config ) {
337 * ProcessDialog.super.call( this, config );
339 * OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
340 * ProcessDialog.static.title = 'An action set in a process dialog';
341 * // An action set that uses modes ('edit' and 'help' mode, in this example).
342 * ProcessDialog.static.actions = [
343 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
344 * { action: 'help', modes: 'edit', label: 'Help' },
345 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
346 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
349 * ProcessDialog.prototype.initialize = function () {
350 * ProcessDialog.super.prototype.initialize.apply( this, arguments );
351 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
352 * 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>' );
353 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
354 * 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>' );
355 * this.stackLayout= new OO.ui.StackLayout( {
356 * items: [ this.panel1, this.panel2 ]
358 * this.$body.append( this.stackLayout.$element );
360 * ProcessDialog.prototype.getSetupProcess = function ( data ) {
361 * return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
362 * .next( function () {
363 * this.actions.setMode('edit');
366 * ProcessDialog.prototype.getActionProcess = function ( action ) {
367 * if ( action === 'help' ) {
368 * this.actions.setMode( 'help' );
369 * this.stackLayout.setItem( this.panel2 );
370 * } else if ( action === 'back' ) {
371 * this.actions.setMode( 'edit' );
372 * this.stackLayout.setItem( this.panel1 );
373 * } else if ( action === 'continue' ) {
375 * return new OO.ui.Process( function () {
379 * return ProcessDialog.super.prototype.getActionProcess.call( this, action );
381 * ProcessDialog.prototype.getBodyHeight = function () {
382 * return this.panel1.$element.outerHeight( true );
384 * var windowManager = new OO.ui.WindowManager();
385 * $( 'body' ).append( windowManager.$element );
386 * var processDialog = new ProcessDialog({
388 * windowManager.addWindows( [ processDialog ] );
389 * windowManager.openWindow( processDialog );
391 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
395 * @mixins OO.EventEmitter
398 * @param {Object} [config] Configuration options
400 OO
.ui
.ActionSet
= function OoUiActionSet( config
) {
401 // Configuration initialization
402 config
= config
|| {};
404 // Mixin constructors
405 OO
.EventEmitter
.call( this );
410 actions
: 'getAction',
414 this.categorized
= {};
417 this.organized
= false;
418 this.changing
= false;
419 this.changed
= false;
424 OO
.mixinClass( OO
.ui
.ActionSet
, OO
.EventEmitter
);
426 /* Static Properties */
429 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
430 * header of a {@link OO.ui.ProcessDialog process dialog}.
431 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
433 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
440 OO
.ui
.ActionSet
.static.specialFlags
= [ 'safe', 'primary' ];
447 * A 'click' event is emitted when an action is clicked.
449 * @param {OO.ui.ActionWidget} action Action that was clicked
455 * A 'resize' event is emitted when an action widget is resized.
457 * @param {OO.ui.ActionWidget} action Action that was resized
463 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
465 * @param {OO.ui.ActionWidget[]} added Actions added
471 * A 'remove' event is emitted when actions are {@link #method-remove removed}
472 * or {@link #clear cleared}.
474 * @param {OO.ui.ActionWidget[]} added Actions removed
480 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
481 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
488 * Handle action change events.
493 OO
.ui
.ActionSet
.prototype.onActionChange = function () {
494 this.organized
= false;
495 if ( this.changing
) {
498 this.emit( 'change' );
503 * Check if an action is one of the special actions.
505 * @param {OO.ui.ActionWidget} action Action to check
506 * @return {boolean} Action is special
508 OO
.ui
.ActionSet
.prototype.isSpecial = function ( action
) {
511 for ( flag
in this.special
) {
512 if ( action
=== this.special
[ flag
] ) {
521 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
524 * @param {Object} [filters] Filters to use, omit to get all actions
525 * @param {string|string[]} [filters.actions] Actions that action widgets must have
526 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
527 * @param {string|string[]} [filters.modes] Modes that action widgets must have
528 * @param {boolean} [filters.visible] Action widgets must be visible
529 * @param {boolean} [filters.disabled] Action widgets must be disabled
530 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
532 OO
.ui
.ActionSet
.prototype.get = function ( filters
) {
533 var i
, len
, list
, category
, actions
, index
, match
, matches
;
538 // Collect category candidates
540 for ( category
in this.categorized
) {
541 list
= filters
[ category
];
543 if ( !Array
.isArray( list
) ) {
546 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
547 actions
= this.categorized
[ category
][ list
[ i
] ];
548 if ( Array
.isArray( actions
) ) {
549 matches
.push
.apply( matches
, actions
);
554 // Remove by boolean filters
555 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
556 match
= matches
[ i
];
558 ( filters
.visible
!== undefined && match
.isVisible() !== filters
.visible
) ||
559 ( filters
.disabled
!== undefined && match
.isDisabled() !== filters
.disabled
)
561 matches
.splice( i
, 1 );
567 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
568 match
= matches
[ i
];
569 index
= matches
.lastIndexOf( match
);
570 while ( index
!== i
) {
571 matches
.splice( index
, 1 );
573 index
= matches
.lastIndexOf( match
);
578 return this.list
.slice();
582 * Get 'special' actions.
584 * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
585 * Special flags can be configured in subclasses by changing the static #specialFlags property.
587 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
589 OO
.ui
.ActionSet
.prototype.getSpecial = function () {
591 return $.extend( {}, this.special
);
595 * Get 'other' actions.
597 * Other actions include all non-special visible action widgets.
599 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
601 OO
.ui
.ActionSet
.prototype.getOthers = function () {
603 return this.others
.slice();
607 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
608 * to be available in the specified mode will be made visible. All other actions will be hidden.
610 * @param {string} mode The mode. Only actions configured to be available in the specified
611 * mode will be made visible.
616 OO
.ui
.ActionSet
.prototype.setMode = function ( mode
) {
619 this.changing
= true;
620 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
621 action
= this.list
[ i
];
622 action
.toggle( action
.hasMode( mode
) );
625 this.organized
= false;
626 this.changing
= false;
627 this.emit( 'change' );
633 * Set the abilities of the specified actions.
635 * Action widgets that are configured with the specified actions will be enabled
636 * or disabled based on the boolean values specified in the `actions`
639 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
640 * values that indicate whether or not the action should be enabled.
643 OO
.ui
.ActionSet
.prototype.setAbilities = function ( actions
) {
644 var i
, len
, action
, item
;
646 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
647 item
= this.list
[ i
];
648 action
= item
.getAction();
649 if ( actions
[ action
] !== undefined ) {
650 item
.setDisabled( !actions
[ action
] );
658 * Executes a function once per action.
660 * When making changes to multiple actions, use this method instead of iterating over the actions
661 * manually to defer emitting a #change event until after all actions have been changed.
663 * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get
664 * @param {Function} callback Callback to run for each action; callback is invoked with three
665 * arguments: the action, the action's index, the list of actions being iterated over
668 OO
.ui
.ActionSet
.prototype.forEach = function ( filter
, callback
) {
669 this.changed
= false;
670 this.changing
= true;
671 this.get( filter
).forEach( callback
);
672 this.changing
= false;
673 if ( this.changed
) {
674 this.emit( 'change' );
681 * Add action widgets to the action set.
683 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
688 OO
.ui
.ActionSet
.prototype.add = function ( actions
) {
691 this.changing
= true;
692 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
693 action
= actions
[ i
];
694 action
.connect( this, {
695 click
: [ 'emit', 'click', action
],
696 resize
: [ 'emit', 'resize', action
],
697 toggle
: [ 'onActionChange' ]
699 this.list
.push( action
);
701 this.organized
= false;
702 this.emit( 'add', actions
);
703 this.changing
= false;
704 this.emit( 'change' );
710 * Remove action widgets from the set.
712 * To remove all actions, you may wish to use the #clear method instead.
714 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
719 OO
.ui
.ActionSet
.prototype.remove = function ( actions
) {
720 var i
, len
, index
, action
;
722 this.changing
= true;
723 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
724 action
= actions
[ i
];
725 index
= this.list
.indexOf( action
);
726 if ( index
!== -1 ) {
727 action
.disconnect( this );
728 this.list
.splice( index
, 1 );
731 this.organized
= false;
732 this.emit( 'remove', actions
);
733 this.changing
= false;
734 this.emit( 'change' );
740 * Remove all action widets from the set.
742 * To remove only specified actions, use the {@link #method-remove remove} method instead.
748 OO
.ui
.ActionSet
.prototype.clear = function () {
750 removed
= this.list
.slice();
752 this.changing
= true;
753 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
754 action
= this.list
[ i
];
755 action
.disconnect( this );
760 this.organized
= false;
761 this.emit( 'remove', removed
);
762 this.changing
= false;
763 this.emit( 'change' );
771 * This is called whenever organized information is requested. It will only reorganize the actions
772 * if something has changed since the last time it ran.
777 OO
.ui
.ActionSet
.prototype.organize = function () {
778 var i
, iLen
, j
, jLen
, flag
, action
, category
, list
, item
, special
,
779 specialFlags
= this.constructor.static.specialFlags
;
781 if ( !this.organized
) {
782 this.categorized
= {};
785 for ( i
= 0, iLen
= this.list
.length
; i
< iLen
; i
++ ) {
786 action
= this.list
[ i
];
787 if ( action
.isVisible() ) {
788 // Populate categories
789 for ( category
in this.categories
) {
790 if ( !this.categorized
[ category
] ) {
791 this.categorized
[ category
] = {};
793 list
= action
[ this.categories
[ category
] ]();
794 if ( !Array
.isArray( list
) ) {
797 for ( j
= 0, jLen
= list
.length
; j
< jLen
; j
++ ) {
799 if ( !this.categorized
[ category
][ item
] ) {
800 this.categorized
[ category
][ item
] = [];
802 this.categorized
[ category
][ item
].push( action
);
805 // Populate special/others
807 for ( j
= 0, jLen
= specialFlags
.length
; j
< jLen
; j
++ ) {
808 flag
= specialFlags
[ j
];
809 if ( !this.special
[ flag
] && action
.hasFlag( flag
) ) {
810 this.special
[ flag
] = action
;
816 this.others
.push( action
);
820 this.organized
= true;
827 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
828 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
829 * connected to them and can't be interacted with.
835 * @param {Object} [config] Configuration options
836 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
837 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
839 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
840 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
841 * @cfg {string} [text] Text to insert
842 * @cfg {Array} [content] An array of content elements to append (after #text).
843 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
844 * Instances of OO.ui.Element will have their $element appended.
845 * @cfg {jQuery} [$content] Content elements to append (after #text)
846 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
847 * Data can also be specified with the #setData method.
849 OO
.ui
.Element
= function OoUiElement( config
) {
850 // Configuration initialization
851 config
= config
|| {};
856 this.data
= config
.data
;
857 this.$element
= config
.$element
||
858 $( document
.createElement( this.getTagName() ) );
859 this.elementGroup
= null;
860 this.debouncedUpdateThemeClassesHandler
= this.debouncedUpdateThemeClasses
.bind( this );
861 this.updateThemeClassesPending
= false;
864 if ( Array
.isArray( config
.classes
) ) {
865 this.$element
.addClass( config
.classes
.join( ' ' ) );
868 this.$element
.attr( 'id', config
.id
);
871 this.$element
.text( config
.text
);
873 if ( config
.content
) {
874 // The `content` property treats plain strings as text; use an
875 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
876 // appropriate $element appended.
877 this.$element
.append( config
.content
.map( function ( v
) {
878 if ( typeof v
=== 'string' ) {
879 // Escape string so it is properly represented in HTML.
880 return document
.createTextNode( v
);
881 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
884 } else if ( v
instanceof OO
.ui
.Element
) {
890 if ( config
.$content
) {
891 // The `$content` property treats plain strings as HTML.
892 this.$element
.append( config
.$content
);
898 OO
.initClass( OO
.ui
.Element
);
900 /* Static Properties */
903 * The name of the HTML tag used by the element.
905 * The static value may be ignored if the #getTagName method is overridden.
911 OO
.ui
.Element
.static.tagName
= 'div';
916 * Reconstitute a JavaScript object corresponding to a widget created
917 * by the PHP implementation.
919 * @param {string|HTMLElement|jQuery} idOrNode
920 * A DOM id (if a string) or node for the widget to infuse.
921 * @return {OO.ui.Element}
922 * The `OO.ui.Element` corresponding to this (infusable) document node.
923 * For `Tag` objects emitted on the HTML side (used occasionally for content)
924 * the value returned is a newly-created Element wrapping around the existing
927 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
928 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, true );
929 // Verify that the type matches up.
930 // FIXME: uncomment after T89721 is fixed (see T90929)
932 if ( !( obj instanceof this['class'] ) ) {
933 throw new Error( 'Infusion type mismatch!' );
940 * Implementation helper for `infuse`; skips the type check and has an
941 * extra property so that only the top-level invocation touches the DOM.
943 * @param {string|HTMLElement|jQuery} idOrNode
944 * @param {boolean} top True only for top-level invocation.
945 * @return {OO.ui.Element}
947 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, top
) {
948 // look for a cached result of a previous infusion.
949 var id
, $elem
, data
, cls
, obj
;
950 if ( typeof idOrNode
=== 'string' ) {
952 $elem
= $( document
.getElementById( id
) );
954 $elem
= $( idOrNode
);
955 id
= $elem
.attr( 'id' );
957 data
= $elem
.data( 'ooui-infused' );
960 if ( data
=== true ) {
961 throw new Error( 'Circular dependency! ' + id
);
965 if ( !$elem
.length
) {
966 throw new Error( 'Widget not found: ' + id
);
968 data
= $elem
.attr( 'data-ooui' );
970 throw new Error( 'No infusion data found: ' + id
);
973 data
= $.parseJSON( data
);
977 if ( !( data
&& data
._
) ) {
978 throw new Error( 'No valid infusion data found: ' + id
);
980 if ( data
._
=== 'Tag' ) {
981 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
982 return new OO
.ui
.Element( { $element
: $elem
} );
986 throw new Error( 'Unknown widget type: ' + id
);
988 $elem
.data( 'ooui-infused', true ); // prevent loops
989 data
.id
= id
; // implicit
990 data
= OO
.copy( data
, null, function deserialize( value
) {
991 if ( OO
.isPlainObject( value
) ) {
993 return OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, false );
996 return new OO
.ui
.HtmlSnippet( value
.html
);
1000 // jscs:disable requireCapitalizedConstructors
1001 obj
= new cls( data
); // rebuild widget
1002 // now replace old DOM with this new DOM.
1004 $elem
.replaceWith( obj
.$element
);
1006 obj
.$element
.data( 'ooui-infused', obj
);
1007 // set the 'data-ooui' attribute so we can identify infused widgets
1008 obj
.$element
.attr( 'data-ooui', '' );
1013 * Get a jQuery function within a specific document.
1016 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
1017 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
1019 * @return {Function} Bound jQuery function
1021 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
1022 function wrapper( selector
) {
1023 return $( selector
, wrapper
.context
);
1026 wrapper
.context
= this.getDocument( context
);
1029 wrapper
.$iframe
= $iframe
;
1036 * Get the document of an element.
1039 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1040 * @return {HTMLDocument|null} Document object
1042 OO
.ui
.Element
.static.getDocument = function ( obj
) {
1043 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1044 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
1045 // Empty jQuery selections might have a context
1048 obj
.ownerDocument
||
1052 ( obj
.nodeType
=== 9 && obj
) ||
1057 * Get the window of an element or document.
1060 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1061 * @return {Window} Window object
1063 OO
.ui
.Element
.static.getWindow = function ( obj
) {
1064 var doc
= this.getDocument( obj
);
1065 return doc
.parentWindow
|| doc
.defaultView
;
1069 * Get the direction of an element or document.
1072 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1073 * @return {string} Text direction, either 'ltr' or 'rtl'
1075 OO
.ui
.Element
.static.getDir = function ( obj
) {
1078 if ( obj
instanceof jQuery
) {
1081 isDoc
= obj
.nodeType
=== 9;
1082 isWin
= obj
.document
!== undefined;
1083 if ( isDoc
|| isWin
) {
1089 return $( obj
).css( 'direction' );
1093 * Get the offset between two frames.
1095 * TODO: Make this function not use recursion.
1098 * @param {Window} from Window of the child frame
1099 * @param {Window} [to=window] Window of the parent frame
1100 * @param {Object} [offset] Offset to start with, used internally
1101 * @return {Object} Offset object, containing left and top properties
1103 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
1104 var i
, len
, frames
, frame
, rect
;
1110 offset
= { top
: 0, left
: 0 };
1112 if ( from.parent
=== from ) {
1116 // Get iframe element
1117 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
1118 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
1119 if ( frames
[ i
].contentWindow
=== from ) {
1120 frame
= frames
[ i
];
1125 // Recursively accumulate offset values
1127 rect
= frame
.getBoundingClientRect();
1128 offset
.left
+= rect
.left
;
1129 offset
.top
+= rect
.top
;
1130 if ( from !== to
) {
1131 this.getFrameOffset( from.parent
, offset
);
1138 * Get the offset between two elements.
1140 * The two elements may be in a different frame, but in that case the frame $element is in must
1141 * be contained in the frame $anchor is in.
1144 * @param {jQuery} $element Element whose position to get
1145 * @param {jQuery} $anchor Element to get $element's position relative to
1146 * @return {Object} Translated position coordinates, containing top and left properties
1148 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1149 var iframe
, iframePos
,
1150 pos
= $element
.offset(),
1151 anchorPos
= $anchor
.offset(),
1152 elementDocument
= this.getDocument( $element
),
1153 anchorDocument
= this.getDocument( $anchor
);
1155 // If $element isn't in the same document as $anchor, traverse up
1156 while ( elementDocument
!== anchorDocument
) {
1157 iframe
= elementDocument
.defaultView
.frameElement
;
1159 throw new Error( '$element frame is not contained in $anchor frame' );
1161 iframePos
= $( iframe
).offset();
1162 pos
.left
+= iframePos
.left
;
1163 pos
.top
+= iframePos
.top
;
1164 elementDocument
= iframe
.ownerDocument
;
1166 pos
.left
-= anchorPos
.left
;
1167 pos
.top
-= anchorPos
.top
;
1172 * Get element border sizes.
1175 * @param {HTMLElement} el Element to measure
1176 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1178 OO
.ui
.Element
.static.getBorders = function ( el
) {
1179 var doc
= el
.ownerDocument
,
1180 win
= doc
.parentWindow
|| doc
.defaultView
,
1181 style
= win
&& win
.getComputedStyle
?
1182 win
.getComputedStyle( el
, null ) :
1185 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1186 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1187 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1188 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1199 * Get dimensions of an element or window.
1202 * @param {HTMLElement|Window} el Element to measure
1203 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1205 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1207 doc
= el
.ownerDocument
|| el
.document
,
1208 win
= doc
.parentWindow
|| doc
.defaultView
;
1210 if ( win
=== el
|| el
=== doc
.documentElement
) {
1213 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1215 top
: $win
.scrollTop(),
1216 left
: $win
.scrollLeft()
1218 scrollbar
: { right
: 0, bottom
: 0 },
1222 bottom
: $win
.innerHeight(),
1223 right
: $win
.innerWidth()
1229 borders
: this.getBorders( el
),
1231 top
: $el
.scrollTop(),
1232 left
: $el
.scrollLeft()
1235 right
: $el
.innerWidth() - el
.clientWidth
,
1236 bottom
: $el
.innerHeight() - el
.clientHeight
1238 rect
: el
.getBoundingClientRect()
1244 * Get scrollable object parent
1246 * documentElement can't be used to get or set the scrollTop
1247 * property on Blink. Changing and testing its value lets us
1248 * use 'body' or 'documentElement' based on what is working.
1250 * https://code.google.com/p/chromium/issues/detail?id=303131
1253 * @param {HTMLElement} el Element to find scrollable parent for
1254 * @return {HTMLElement} Scrollable parent
1256 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1257 var scrollTop
, body
;
1259 if ( OO
.ui
.scrollableElement
=== undefined ) {
1260 body
= el
.ownerDocument
.body
;
1261 scrollTop
= body
.scrollTop
;
1264 if ( body
.scrollTop
=== 1 ) {
1265 body
.scrollTop
= scrollTop
;
1266 OO
.ui
.scrollableElement
= 'body';
1268 OO
.ui
.scrollableElement
= 'documentElement';
1272 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1276 * Get closest scrollable container.
1278 * Traverses up until either a scrollable element or the root is reached, in which case the window
1282 * @param {HTMLElement} el Element to find scrollable container for
1283 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1284 * @return {HTMLElement} Closest scrollable container
1286 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1288 props
= [ 'overflow' ],
1289 $parent
= $( el
).parent();
1291 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1292 props
.push( 'overflow-' + dimension
);
1295 while ( $parent
.length
) {
1296 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1297 return $parent
[ 0 ];
1301 val
= $parent
.css( props
[ i
] );
1302 if ( val
=== 'auto' || val
=== 'scroll' ) {
1303 return $parent
[ 0 ];
1306 $parent
= $parent
.parent();
1308 return this.getDocument( el
).body
;
1312 * Scroll element into view.
1315 * @param {HTMLElement} el Element to scroll into view
1316 * @param {Object} [config] Configuration options
1317 * @param {string} [config.duration] jQuery animation duration value
1318 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1319 * to scroll in both directions
1320 * @param {Function} [config.complete] Function to call when scrolling completes
1322 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1323 // Configuration initialization
1324 config
= config
|| {};
1327 callback
= typeof config
.complete
=== 'function' && config
.complete
,
1328 sc
= this.getClosestScrollableContainer( el
, config
.direction
),
1330 eld
= this.getDimensions( el
),
1331 scd
= this.getDimensions( sc
),
1332 $win
= $( this.getWindow( el
) );
1334 // Compute the distances between the edges of el and the edges of the scroll viewport
1335 if ( $sc
.is( 'html, body' ) ) {
1336 // If the scrollable container is the root, this is easy
1339 bottom
: $win
.innerHeight() - eld
.rect
.bottom
,
1340 left
: eld
.rect
.left
,
1341 right
: $win
.innerWidth() - eld
.rect
.right
1344 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1346 top
: eld
.rect
.top
- ( scd
.rect
.top
+ scd
.borders
.top
),
1347 bottom
: scd
.rect
.bottom
- scd
.borders
.bottom
- scd
.scrollbar
.bottom
- eld
.rect
.bottom
,
1348 left
: eld
.rect
.left
- ( scd
.rect
.left
+ scd
.borders
.left
),
1349 right
: scd
.rect
.right
- scd
.borders
.right
- scd
.scrollbar
.right
- eld
.rect
.right
1353 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1354 if ( rel
.top
< 0 ) {
1355 anim
.scrollTop
= scd
.scroll
.top
+ rel
.top
;
1356 } else if ( rel
.top
> 0 && rel
.bottom
< 0 ) {
1357 anim
.scrollTop
= scd
.scroll
.top
+ Math
.min( rel
.top
, -rel
.bottom
);
1360 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1361 if ( rel
.left
< 0 ) {
1362 anim
.scrollLeft
= scd
.scroll
.left
+ rel
.left
;
1363 } else if ( rel
.left
> 0 && rel
.right
< 0 ) {
1364 anim
.scrollLeft
= scd
.scroll
.left
+ Math
.min( rel
.left
, -rel
.right
);
1367 if ( !$.isEmptyObject( anim
) ) {
1368 $sc
.stop( true ).animate( anim
, config
.duration
|| 'fast' );
1370 $sc
.queue( function ( next
) {
1383 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1384 * and reserve space for them, because it probably doesn't.
1386 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1387 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1388 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1389 * and then reattach (or show) them back.
1392 * @param {HTMLElement} el Element to reconsider the scrollbars on
1394 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1395 var i
, len
, nodes
= [];
1396 // Detach all children
1397 while ( el
.firstChild
) {
1398 nodes
.push( el
.firstChild
);
1399 el
.removeChild( el
.firstChild
);
1402 void el
.offsetHeight
;
1403 // Reattach all children
1404 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1405 el
.appendChild( nodes
[ i
] );
1412 * Toggle visibility of an element.
1414 * @param {boolean} [show] Make element visible, omit to toggle visibility
1418 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1419 show
= show
=== undefined ? !this.visible
: !!show
;
1421 if ( show
!== this.isVisible() ) {
1422 this.visible
= show
;
1423 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1424 this.emit( 'toggle', show
);
1431 * Check if element is visible.
1433 * @return {boolean} element is visible
1435 OO
.ui
.Element
.prototype.isVisible = function () {
1436 return this.visible
;
1442 * @return {Mixed} Element data
1444 OO
.ui
.Element
.prototype.getData = function () {
1451 * @param {Mixed} Element data
1454 OO
.ui
.Element
.prototype.setData = function ( data
) {
1460 * Check if element supports one or more methods.
1462 * @param {string|string[]} methods Method or list of methods to check
1463 * @return {boolean} All methods are supported
1465 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1469 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1470 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1471 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1476 return methods
.length
=== support
;
1480 * Update the theme-provided classes.
1482 * @localdoc This is called in element mixins and widget classes any time state changes.
1483 * Updating is debounced, minimizing overhead of changing multiple attributes and
1484 * guaranteeing that theme updates do not occur within an element's constructor
1486 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1487 if ( !this.updateThemeClassesPending
) {
1488 this.updateThemeClassesPending
= true;
1489 setTimeout( this.debouncedUpdateThemeClassesHandler
);
1496 OO
.ui
.Element
.prototype.debouncedUpdateThemeClasses = function () {
1497 OO
.ui
.theme
.updateElementClasses( this );
1498 this.updateThemeClassesPending
= false;
1502 * Get the HTML tag name.
1504 * Override this method to base the result on instance information.
1506 * @return {string} HTML tag name
1508 OO
.ui
.Element
.prototype.getTagName = function () {
1509 return this.constructor.static.tagName
;
1513 * Check if the element is attached to the DOM
1514 * @return {boolean} The element is attached to the DOM
1516 OO
.ui
.Element
.prototype.isElementAttached = function () {
1517 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1521 * Get the DOM document.
1523 * @return {HTMLDocument} Document object
1525 OO
.ui
.Element
.prototype.getElementDocument = function () {
1526 // Don't cache this in other ways either because subclasses could can change this.$element
1527 return OO
.ui
.Element
.static.getDocument( this.$element
);
1531 * Get the DOM window.
1533 * @return {Window} Window object
1535 OO
.ui
.Element
.prototype.getElementWindow = function () {
1536 return OO
.ui
.Element
.static.getWindow( this.$element
);
1540 * Get closest scrollable container.
1542 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1543 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1547 * Get group element is in.
1549 * @return {OO.ui.GroupElement|null} Group element, null if none
1551 OO
.ui
.Element
.prototype.getElementGroup = function () {
1552 return this.elementGroup
;
1556 * Set group element is in.
1558 * @param {OO.ui.GroupElement|null} group Group element, null if none
1561 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1562 this.elementGroup
= group
;
1567 * Scroll element into view.
1569 * @param {Object} [config] Configuration options
1571 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1572 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1576 * Container for elements.
1580 * @extends OO.ui.Element
1581 * @mixins OO.EventEmitter
1584 * @param {Object} [config] Configuration options
1586 OO
.ui
.Layout
= function OoUiLayout( config
) {
1587 // Configuration initialization
1588 config
= config
|| {};
1590 // Parent constructor
1591 OO
.ui
.Layout
.super.call( this, config
);
1593 // Mixin constructors
1594 OO
.EventEmitter
.call( this );
1597 this.$element
.addClass( 'oo-ui-layout' );
1602 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1603 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1606 * Widgets are compositions of one or more OOjs UI elements that users can both view
1607 * and interact with. All widgets can be configured and modified via a standard API,
1608 * and their state can change dynamically according to a model.
1612 * @extends OO.ui.Element
1613 * @mixins OO.EventEmitter
1616 * @param {Object} [config] Configuration options
1617 * @cfg {boolean} [disabled=false] Disable
1619 OO
.ui
.Widget
= function OoUiWidget( config
) {
1620 // Initialize config
1621 config
= $.extend( { disabled
: false }, config
);
1623 // Parent constructor
1624 OO
.ui
.Widget
.super.call( this, config
);
1626 // Mixin constructors
1627 OO
.EventEmitter
.call( this );
1630 this.disabled
= null;
1631 this.wasDisabled
= null;
1634 this.$element
.addClass( 'oo-ui-widget' );
1635 this.setDisabled( !!config
.disabled
);
1640 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1641 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1647 * @param {boolean} disabled Widget is disabled
1652 * @param {boolean} visible Widget is visible
1658 * Check if the widget is disabled.
1660 * @return {boolean} Button is disabled
1662 OO
.ui
.Widget
.prototype.isDisabled = function () {
1663 return this.disabled
;
1667 * Set the disabled state of the widget.
1669 * This should probably change the widgets' appearance and prevent it from being used.
1671 * @param {boolean} disabled Disable widget
1674 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1677 this.disabled
= !!disabled
;
1678 isDisabled
= this.isDisabled();
1679 if ( isDisabled
!== this.wasDisabled
) {
1680 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1681 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1682 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1683 this.emit( 'disable', isDisabled
);
1684 this.updateThemeClasses();
1686 this.wasDisabled
= isDisabled
;
1692 * Update the disabled state, in case of changes in parent widget.
1696 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1697 this.setDisabled( this.disabled
);
1702 * A window is a container for elements that are in a child frame. They are used with
1703 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1704 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1705 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1706 * the window manager will choose a sensible fallback.
1708 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1709 * different processes are executed:
1711 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1712 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1715 * - {@link #getSetupProcess} method is called and its result executed
1716 * - {@link #getReadyProcess} method is called and its result executed
1718 * **opened**: The window is now open
1720 * **closing**: The closing stage begins when the window manager's
1721 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1722 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1724 * - {@link #getHoldProcess} method is called and its result executed
1725 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1727 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1728 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1729 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1730 * processing can complete. Always assume window processes are executed asynchronously.
1732 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1734 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1738 * @extends OO.ui.Element
1739 * @mixins OO.EventEmitter
1742 * @param {Object} [config] Configuration options
1743 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1744 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
1746 OO
.ui
.Window
= function OoUiWindow( config
) {
1747 // Configuration initialization
1748 config
= config
|| {};
1750 // Parent constructor
1751 OO
.ui
.Window
.super.call( this, config
);
1753 // Mixin constructors
1754 OO
.EventEmitter
.call( this );
1757 this.manager
= null;
1758 this.size
= config
.size
|| this.constructor.static.size
;
1759 this.frame
= new OO
.ui
.PanelLayout( {
1763 this.$frame
= this.frame
.$element
;
1764 this.$overlay
= $( '<div>' );
1765 this.$content
= $( '<div>' );
1768 this.$overlay
.addClass( 'oo-ui-window-overlay' );
1770 .addClass( 'oo-ui-window-content' )
1771 .attr( 'tabIndex', 0 );
1773 .addClass( 'oo-ui-window-frame' )
1774 .append( this.$content
);
1777 .addClass( 'oo-ui-window' )
1778 .append( this.$frame
, this.$overlay
);
1780 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1781 // that reference properties not initialized at that time of parent class construction
1782 // TODO: Find a better way to handle post-constructor setup
1783 this.visible
= false;
1784 this.$element
.addClass( 'oo-ui-element-hidden' );
1789 OO
.inheritClass( OO
.ui
.Window
, OO
.ui
.Element
);
1790 OO
.mixinClass( OO
.ui
.Window
, OO
.EventEmitter
);
1792 /* Static Properties */
1795 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
1797 * The static size is used if no #size is configured during construction.
1801 * @property {string}
1803 OO
.ui
.Window
.static.size
= 'medium';
1808 * Handle mouse down events.
1811 * @param {jQuery.Event} e Mouse down event
1813 OO
.ui
.Window
.prototype.onMouseDown = function ( e
) {
1814 // Prevent clicking on the click-block from stealing focus
1815 if ( e
.target
=== this.$element
[ 0 ] ) {
1821 * Check if the window has been initialized.
1823 * Initialization occurs when a window is added to a manager.
1825 * @return {boolean} Window has been initialized
1827 OO
.ui
.Window
.prototype.isInitialized = function () {
1828 return !!this.manager
;
1832 * Check if the window is visible.
1834 * @return {boolean} Window is visible
1836 OO
.ui
.Window
.prototype.isVisible = function () {
1837 return this.visible
;
1841 * Check if the window is opening.
1843 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
1846 * @return {boolean} Window is opening
1848 OO
.ui
.Window
.prototype.isOpening = function () {
1849 return this.manager
.isOpening( this );
1853 * Check if the window is closing.
1855 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
1857 * @return {boolean} Window is closing
1859 OO
.ui
.Window
.prototype.isClosing = function () {
1860 return this.manager
.isClosing( this );
1864 * Check if the window is opened.
1866 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
1868 * @return {boolean} Window is opened
1870 OO
.ui
.Window
.prototype.isOpened = function () {
1871 return this.manager
.isOpened( this );
1875 * Get the window manager.
1877 * All windows must be attached to a window manager, which is used to open
1878 * and close the window and control its presentation.
1880 * @return {OO.ui.WindowManager} Manager of window
1882 OO
.ui
.Window
.prototype.getManager = function () {
1883 return this.manager
;
1887 * Get the symbolic name of the window size (e.g., `small` or `medium`).
1889 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
1891 OO
.ui
.Window
.prototype.getSize = function () {
1896 * Disable transitions on window's frame for the duration of the callback function, then enable them
1900 * @param {Function} callback Function to call while transitions are disabled
1902 OO
.ui
.Window
.prototype.withoutSizeTransitions = function ( callback
) {
1903 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1904 // Disable transitions first, otherwise we'll get values from when the window was animating.
1906 styleObj
= this.$frame
[ 0 ].style
;
1907 oldTransition
= styleObj
.transition
|| styleObj
.OTransition
|| styleObj
.MsTransition
||
1908 styleObj
.MozTransition
|| styleObj
.WebkitTransition
;
1909 styleObj
.transition
= styleObj
.OTransition
= styleObj
.MsTransition
=
1910 styleObj
.MozTransition
= styleObj
.WebkitTransition
= 'none';
1912 // Force reflow to make sure the style changes done inside callback really are not transitioned
1913 this.$frame
.height();
1914 styleObj
.transition
= styleObj
.OTransition
= styleObj
.MsTransition
=
1915 styleObj
.MozTransition
= styleObj
.WebkitTransition
= oldTransition
;
1919 * Get the height of the full window contents (i.e., the window head, body and foot together).
1921 * What consistitutes the head, body, and foot varies depending on the window type.
1922 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
1923 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
1924 * and special actions in the head, and dialog content in the body.
1926 * To get just the height of the dialog body, use the #getBodyHeight method.
1928 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
1930 OO
.ui
.Window
.prototype.getContentHeight = function () {
1933 bodyStyleObj
= this.$body
[ 0 ].style
,
1934 frameStyleObj
= this.$frame
[ 0 ].style
;
1936 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1937 // Disable transitions first, otherwise we'll get values from when the window was animating.
1938 this.withoutSizeTransitions( function () {
1939 var oldHeight
= frameStyleObj
.height
,
1940 oldPosition
= bodyStyleObj
.position
;
1941 frameStyleObj
.height
= '1px';
1942 // Force body to resize to new width
1943 bodyStyleObj
.position
= 'relative';
1944 bodyHeight
= win
.getBodyHeight();
1945 frameStyleObj
.height
= oldHeight
;
1946 bodyStyleObj
.position
= oldPosition
;
1950 // Add buffer for border
1951 ( this.$frame
.outerHeight() - this.$frame
.innerHeight() ) +
1952 // Use combined heights of children
1953 ( this.$head
.outerHeight( true ) + bodyHeight
+ this.$foot
.outerHeight( true ) )
1958 * Get the height of the window body.
1960 * To get the height of the full window contents (the window body, head, and foot together),
1961 * use #getContentHeight.
1963 * When this function is called, the window will temporarily have been resized
1964 * to height=1px, so .scrollHeight measurements can be taken accurately.
1966 * @return {number} Height of the window body in pixels
1968 OO
.ui
.Window
.prototype.getBodyHeight = function () {
1969 return this.$body
[ 0 ].scrollHeight
;
1973 * Get the directionality of the frame (right-to-left or left-to-right).
1975 * @return {string} Directionality: `'ltr'` or `'rtl'`
1977 OO
.ui
.Window
.prototype.getDir = function () {
1982 * Get the 'setup' process.
1984 * The setup process is used to set up a window for use in a particular context,
1985 * based on the `data` argument. This method is called during the opening phase of the window’s
1988 * Override this method to add additional steps to the ‘setup’ process the parent method provides
1989 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
1992 * To add window content that persists between openings, you may wish to use the #initialize method
1996 * @param {Object} [data] Window opening data
1997 * @return {OO.ui.Process} Setup process
1999 OO
.ui
.Window
.prototype.getSetupProcess = function () {
2000 return new OO
.ui
.Process();
2004 * Get the ‘ready’ process.
2006 * The ready process is used to ready a window for use in a particular
2007 * context, based on the `data` argument. This method is called during the opening phase of
2008 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
2010 * Override this method to add additional steps to the ‘ready’ process the parent method
2011 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2012 * methods of OO.ui.Process.
2015 * @param {Object} [data] Window opening data
2016 * @return {OO.ui.Process} Ready process
2018 OO
.ui
.Window
.prototype.getReadyProcess = function () {
2019 return new OO
.ui
.Process();
2023 * Get the 'hold' process.
2025 * The hold proccess is used to keep a window from being used in a particular context,
2026 * based on the `data` argument. This method is called during the closing phase of the window’s
2029 * Override this method to add additional steps to the 'hold' process the parent method provides
2030 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2034 * @param {Object} [data] Window closing data
2035 * @return {OO.ui.Process} Hold process
2037 OO
.ui
.Window
.prototype.getHoldProcess = function () {
2038 return new OO
.ui
.Process();
2042 * Get the ‘teardown’ process.
2044 * The teardown process is used to teardown a window after use. During teardown,
2045 * user interactions within the window are conveyed and the window is closed, based on the `data`
2046 * argument. This method is called during the closing phase of the window’s lifecycle.
2048 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2049 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2053 * @param {Object} [data] Window closing data
2054 * @return {OO.ui.Process} Teardown process
2056 OO
.ui
.Window
.prototype.getTeardownProcess = function () {
2057 return new OO
.ui
.Process();
2061 * Set the window manager.
2063 * This will cause the window to initialize. Calling it more than once will cause an error.
2065 * @param {OO.ui.WindowManager} manager Manager for this window
2066 * @throws {Error} An error is thrown if the method is called more than once
2069 OO
.ui
.Window
.prototype.setManager = function ( manager
) {
2070 if ( this.manager
) {
2071 throw new Error( 'Cannot set window manager, window already has a manager' );
2074 this.manager
= manager
;
2081 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2083 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2087 OO
.ui
.Window
.prototype.setSize = function ( size
) {
2094 * Update the window size.
2096 * @throws {Error} An error is thrown if the window is not attached to a window manager
2099 OO
.ui
.Window
.prototype.updateSize = function () {
2100 if ( !this.manager
) {
2101 throw new Error( 'Cannot update window size, must be attached to a manager' );
2104 this.manager
.updateWindowSize( this );
2110 * Set window dimensions.
2112 * Properties are applied to the frame container.
2114 * @param {Object} dim CSS dimension properties
2115 * @param {string|number} [dim.width] Width
2116 * @param {string|number} [dim.minWidth] Minimum width
2117 * @param {string|number} [dim.maxWidth] Maximum width
2118 * @param {string|number} [dim.width] Height, omit to set based on height of contents
2119 * @param {string|number} [dim.minWidth] Minimum height
2120 * @param {string|number} [dim.maxWidth] Maximum height
2123 OO
.ui
.Window
.prototype.setDimensions = function ( dim
) {
2126 styleObj
= this.$frame
[ 0 ].style
;
2128 // Calculate the height we need to set using the correct width
2129 if ( dim
.height
=== undefined ) {
2130 this.withoutSizeTransitions( function () {
2131 var oldWidth
= styleObj
.width
;
2132 win
.$frame
.css( 'width', dim
.width
|| '' );
2133 height
= win
.getContentHeight();
2134 styleObj
.width
= oldWidth
;
2137 height
= dim
.height
;
2141 width
: dim
.width
|| '',
2142 minWidth
: dim
.minWidth
|| '',
2143 maxWidth
: dim
.maxWidth
|| '',
2144 height
: height
|| '',
2145 minHeight
: dim
.minHeight
|| '',
2146 maxHeight
: dim
.maxHeight
|| ''
2153 * Initialize window contents.
2155 * Before the window is opened for the first time, #initialize is called so that content that
2156 * persists between openings can be added to the window.
2158 * To set up a window with new content each time the window opens, use #getSetupProcess.
2160 * @throws {Error} An error is thrown if the window is not attached to a window manager
2163 OO
.ui
.Window
.prototype.initialize = function () {
2164 if ( !this.manager
) {
2165 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2169 this.$head
= $( '<div>' );
2170 this.$body
= $( '<div>' );
2171 this.$foot
= $( '<div>' );
2172 this.dir
= OO
.ui
.Element
.static.getDir( this.$content
) || 'ltr';
2173 this.$document
= $( this.getElementDocument() );
2176 this.$element
.on( 'mousedown', this.onMouseDown
.bind( this ) );
2179 this.$head
.addClass( 'oo-ui-window-head' );
2180 this.$body
.addClass( 'oo-ui-window-body' );
2181 this.$foot
.addClass( 'oo-ui-window-foot' );
2182 this.$content
.append( this.$head
, this.$body
, this.$foot
);
2190 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2191 * method, which returns a promise resolved when the window is done opening.
2193 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2195 * @param {Object} [data] Window opening data
2196 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2197 * if the window fails to open. When the promise is resolved successfully, the first argument of the
2198 * value is a new promise, which is resolved when the window begins closing.
2199 * @throws {Error} An error is thrown if the window is not attached to a window manager
2201 OO
.ui
.Window
.prototype.open = function ( data
) {
2202 if ( !this.manager
) {
2203 throw new Error( 'Cannot open window, must be attached to a manager' );
2206 return this.manager
.openWindow( this, data
);
2212 * This method is a wrapper around a call to the window
2213 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2214 * which returns a closing promise resolved when the window is done closing.
2216 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2217 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2218 * the window closes.
2220 * @param {Object} [data] Window closing data
2221 * @return {jQuery.Promise} Promise resolved when window is closed
2222 * @throws {Error} An error is thrown if the window is not attached to a window manager
2224 OO
.ui
.Window
.prototype.close = function ( data
) {
2225 if ( !this.manager
) {
2226 throw new Error( 'Cannot close window, must be attached to a manager' );
2229 return this.manager
.closeWindow( this, data
);
2235 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2238 * @param {Object} [data] Window opening data
2239 * @return {jQuery.Promise} Promise resolved when window is setup
2241 OO
.ui
.Window
.prototype.setup = function ( data
) {
2243 deferred
= $.Deferred();
2245 this.toggle( true );
2247 this.getSetupProcess( data
).execute().done( function () {
2248 // Force redraw by asking the browser to measure the elements' widths
2249 win
.$element
.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2250 win
.$content
.addClass( 'oo-ui-window-content-setup' ).width();
2254 return deferred
.promise();
2260 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2263 * @param {Object} [data] Window opening data
2264 * @return {jQuery.Promise} Promise resolved when window is ready
2266 OO
.ui
.Window
.prototype.ready = function ( data
) {
2268 deferred
= $.Deferred();
2270 this.$content
.focus();
2271 this.getReadyProcess( data
).execute().done( function () {
2272 // Force redraw by asking the browser to measure the elements' widths
2273 win
.$element
.addClass( 'oo-ui-window-ready' ).width();
2274 win
.$content
.addClass( 'oo-ui-window-content-ready' ).width();
2278 return deferred
.promise();
2284 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2287 * @param {Object} [data] Window closing data
2288 * @return {jQuery.Promise} Promise resolved when window is held
2290 OO
.ui
.Window
.prototype.hold = function ( data
) {
2292 deferred
= $.Deferred();
2294 this.getHoldProcess( data
).execute().done( function () {
2295 // Get the focused element within the window's content
2296 var $focus
= win
.$content
.find( OO
.ui
.Element
.static.getDocument( win
.$content
).activeElement
);
2298 // Blur the focused element
2299 if ( $focus
.length
) {
2303 // Force redraw by asking the browser to measure the elements' widths
2304 win
.$element
.removeClass( 'oo-ui-window-ready' ).width();
2305 win
.$content
.removeClass( 'oo-ui-window-content-ready' ).width();
2309 return deferred
.promise();
2315 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2318 * @param {Object} [data] Window closing data
2319 * @return {jQuery.Promise} Promise resolved when window is torn down
2321 OO
.ui
.Window
.prototype.teardown = function ( data
) {
2324 return this.getTeardownProcess( data
).execute()
2325 .done( function () {
2326 // Force redraw by asking the browser to measure the elements' widths
2327 win
.$element
.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2328 win
.$content
.removeClass( 'oo-ui-window-content-setup' ).width();
2329 win
.toggle( false );
2334 * The Dialog class serves as the base class for the other types of dialogs.
2335 * Unless extended to include controls, the rendered dialog box is a simple window
2336 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2337 * which opens, closes, and controls the presentation of the window. See the
2338 * [OOjs UI documentation on MediaWiki] [1] for more information.
2341 * // A simple dialog window.
2342 * function MyDialog( config ) {
2343 * MyDialog.super.call( this, config );
2345 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2346 * MyDialog.prototype.initialize = function () {
2347 * MyDialog.super.prototype.initialize.call( this );
2348 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2349 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2350 * this.$body.append( this.content.$element );
2352 * MyDialog.prototype.getBodyHeight = function () {
2353 * return this.content.$element.outerHeight( true );
2355 * var myDialog = new MyDialog( {
2358 * // Create and append a window manager, which opens and closes the window.
2359 * var windowManager = new OO.ui.WindowManager();
2360 * $( 'body' ).append( windowManager.$element );
2361 * windowManager.addWindows( [ myDialog ] );
2362 * // Open the window!
2363 * windowManager.openWindow( myDialog );
2365 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2369 * @extends OO.ui.Window
2370 * @mixins OO.ui.PendingElement
2373 * @param {Object} [config] Configuration options
2375 OO
.ui
.Dialog
= function OoUiDialog( config
) {
2376 // Parent constructor
2377 OO
.ui
.Dialog
.super.call( this, config
);
2379 // Mixin constructors
2380 OO
.ui
.PendingElement
.call( this );
2383 this.actions
= new OO
.ui
.ActionSet();
2384 this.attachedActions
= [];
2385 this.currentAction
= null;
2386 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
2389 this.actions
.connect( this, {
2390 click
: 'onActionClick',
2391 resize
: 'onActionResize',
2392 change
: 'onActionsChange'
2397 .addClass( 'oo-ui-dialog' )
2398 .attr( 'role', 'dialog' );
2403 OO
.inheritClass( OO
.ui
.Dialog
, OO
.ui
.Window
);
2404 OO
.mixinClass( OO
.ui
.Dialog
, OO
.ui
.PendingElement
);
2406 /* Static Properties */
2409 * Symbolic name of dialog.
2411 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2412 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2414 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2419 * @property {string}
2421 OO
.ui
.Dialog
.static.name
= '';
2426 * The title can be specified as a plaintext string, a {@link OO.ui.LabelElement Label} node, or a function
2427 * that will produce a Label node or string. The title can also be specified with data passed to the
2428 * constructor (see #getSetupProcess). In this case, the static value will be overriden.
2433 * @property {jQuery|string|Function}
2435 OO
.ui
.Dialog
.static.title
= '';
2438 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2440 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2441 * value will be overriden.
2443 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2447 * @property {Object[]}
2449 OO
.ui
.Dialog
.static.actions
= [];
2452 * Close the dialog when the 'Esc' key is pressed.
2457 * @property {boolean}
2459 OO
.ui
.Dialog
.static.escapable
= true;
2464 * Handle frame document key down events.
2467 * @param {jQuery.Event} e Key down event
2469 OO
.ui
.Dialog
.prototype.onDocumentKeyDown = function ( e
) {
2470 if ( e
.which
=== OO
.ui
.Keys
.ESCAPE
) {
2473 e
.stopPropagation();
2478 * Handle action resized events.
2481 * @param {OO.ui.ActionWidget} action Action that was resized
2483 OO
.ui
.Dialog
.prototype.onActionResize = function () {
2484 // Override in subclass
2488 * Handle action click events.
2491 * @param {OO.ui.ActionWidget} action Action that was clicked
2493 OO
.ui
.Dialog
.prototype.onActionClick = function ( action
) {
2494 if ( !this.isPending() ) {
2495 this.executeAction( action
.getAction() );
2500 * Handle actions change event.
2504 OO
.ui
.Dialog
.prototype.onActionsChange = function () {
2505 this.detachActions();
2506 if ( !this.isClosing() ) {
2507 this.attachActions();
2512 * Get the set of actions used by the dialog.
2514 * @return {OO.ui.ActionSet}
2516 OO
.ui
.Dialog
.prototype.getActions = function () {
2517 return this.actions
;
2521 * Get a process for taking action.
2523 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
2524 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
2525 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
2528 * @param {string} [action] Symbolic name of action
2529 * @return {OO.ui.Process} Action process
2531 OO
.ui
.Dialog
.prototype.getActionProcess = function ( action
) {
2532 return new OO
.ui
.Process()
2533 .next( function () {
2535 // An empty action always closes the dialog without data, which should always be
2536 // safe and make no changes
2545 * @param {Object} [data] Dialog opening data
2546 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
2547 * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
2548 * action item, omit to use #static-actions
2550 OO
.ui
.Dialog
.prototype.getSetupProcess = function ( data
) {
2554 return OO
.ui
.Dialog
.super.prototype.getSetupProcess
.call( this, data
)
2555 .next( function () {
2558 config
= this.constructor.static,
2559 actions
= data
.actions
!== undefined ? data
.actions
: config
.actions
;
2561 this.title
.setLabel(
2562 data
.title
!== undefined ? data
.title
: this.constructor.static.title
2564 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
2566 new OO
.ui
.ActionWidget( actions
[ i
] )
2569 this.actions
.add( items
);
2571 if ( this.constructor.static.escapable
) {
2572 this.$document
.on( 'keydown', this.onDocumentKeyDownHandler
);
2580 OO
.ui
.Dialog
.prototype.getTeardownProcess = function ( data
) {
2582 return OO
.ui
.Dialog
.super.prototype.getTeardownProcess
.call( this, data
)
2583 .first( function () {
2584 if ( this.constructor.static.escapable
) {
2585 this.$document
.off( 'keydown', this.onDocumentKeyDownHandler
);
2588 this.actions
.clear();
2589 this.currentAction
= null;
2596 OO
.ui
.Dialog
.prototype.initialize = function () {
2598 OO
.ui
.Dialog
.super.prototype.initialize
.call( this );
2601 this.title
= new OO
.ui
.LabelWidget();
2604 this.$content
.addClass( 'oo-ui-dialog-content' );
2605 this.setPendingElement( this.$head
);
2609 * Attach action actions.
2613 OO
.ui
.Dialog
.prototype.attachActions = function () {
2614 // Remember the list of potentially attached actions
2615 this.attachedActions
= this.actions
.get();
2619 * Detach action actions.
2624 OO
.ui
.Dialog
.prototype.detachActions = function () {
2627 // Detach all actions that may have been previously attached
2628 for ( i
= 0, len
= this.attachedActions
.length
; i
< len
; i
++ ) {
2629 this.attachedActions
[ i
].$element
.detach();
2631 this.attachedActions
= [];
2635 * Execute an action.
2637 * @param {string} action Symbolic name of action to execute
2638 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2640 OO
.ui
.Dialog
.prototype.executeAction = function ( action
) {
2642 this.currentAction
= action
;
2643 return this.getActionProcess( action
).execute()
2644 .always( this.popPending
.bind( this ) );
2648 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2649 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2650 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2651 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2652 * pertinent data and reused.
2654 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2655 * `opened`, and `closing`, which represent the primary stages of the cycle:
2657 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2658 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2660 * - an `opening` event is emitted with an `opening` promise
2661 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2662 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2663 * window and its result executed
2664 * - a `setup` progress notification is emitted from the `opening` promise
2665 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2666 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2667 * window and its result executed
2668 * - a `ready` progress notification is emitted from the `opening` promise
2669 * - the `opening` promise is resolved with an `opened` promise
2671 * **Opened**: the window is now open.
2673 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2674 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2675 * to close the window.
2677 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2678 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2679 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2680 * window and its result executed
2681 * - a `hold` progress notification is emitted from the `closing` promise
2682 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2683 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2684 * window and its result executed
2685 * - a `teardown` progress notification is emitted from the `closing` promise
2686 * - the `closing` promise is resolved. The window is now closed
2688 * See the [OOjs UI documentation on MediaWiki][1] for more information.
2690 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2693 * @extends OO.ui.Element
2694 * @mixins OO.EventEmitter
2697 * @param {Object} [config] Configuration options
2698 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2699 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2701 OO
.ui
.WindowManager
= function OoUiWindowManager( config
) {
2702 // Configuration initialization
2703 config
= config
|| {};
2705 // Parent constructor
2706 OO
.ui
.WindowManager
.super.call( this, config
);
2708 // Mixin constructors
2709 OO
.EventEmitter
.call( this );
2712 this.factory
= config
.factory
;
2713 this.modal
= config
.modal
=== undefined || !!config
.modal
;
2715 this.opening
= null;
2717 this.closing
= null;
2718 this.preparingToOpen
= null;
2719 this.preparingToClose
= null;
2720 this.currentWindow
= null;
2721 this.globalEvents
= false;
2722 this.$ariaHidden
= null;
2723 this.onWindowResizeTimeout
= null;
2724 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
2725 this.afterWindowResizeHandler
= this.afterWindowResize
.bind( this );
2729 .addClass( 'oo-ui-windowManager' )
2730 .toggleClass( 'oo-ui-windowManager-modal', this.modal
);
2735 OO
.inheritClass( OO
.ui
.WindowManager
, OO
.ui
.Element
);
2736 OO
.mixinClass( OO
.ui
.WindowManager
, OO
.EventEmitter
);
2741 * Window is opening.
2743 * Fired when the window begins to be opened.
2746 * @param {OO.ui.Window} win Window that's being opened
2747 * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
2748 * resolved the first argument will be a promise which will be resolved when the window begins
2749 * closing, the second argument will be the opening data; progress notifications will be fired on
2750 * the promise for `setup` and `ready` when those processes are completed respectively.
2751 * @param {Object} data Window opening data
2755 * Window is closing.
2757 * Fired when the window begins to be closed.
2760 * @param {OO.ui.Window} win Window that's being closed
2761 * @param {jQuery.Promise} closing Promise resolved when window is closed; when the promise
2762 * is resolved the first argument will be a the closing data; progress notifications will be fired
2763 * on the promise for `hold` and `teardown` when those processes are completed respectively.
2764 * @param {Object} data Window closing data
2768 * Window was resized.
2771 * @param {OO.ui.Window} win Window that was resized
2774 /* Static Properties */
2777 * Map of symbolic size names and CSS properties.
2781 * @property {Object}
2783 OO
.ui
.WindowManager
.static.sizes
= {
2797 // These can be non-numeric because they are never used in calculations
2804 * Symbolic name of default size.
2806 * Default size is used if the window's requested size is not recognized.
2810 * @property {string}
2812 OO
.ui
.WindowManager
.static.defaultSize
= 'medium';
2817 * Handle window resize events.
2819 * @param {jQuery.Event} e Window resize event
2821 OO
.ui
.WindowManager
.prototype.onWindowResize = function () {
2822 clearTimeout( this.onWindowResizeTimeout
);
2823 this.onWindowResizeTimeout
= setTimeout( this.afterWindowResizeHandler
, 200 );
2827 * Handle window resize events.
2829 * @param {jQuery.Event} e Window resize event
2831 OO
.ui
.WindowManager
.prototype.afterWindowResize = function () {
2832 if ( this.currentWindow
) {
2833 this.updateWindowSize( this.currentWindow
);
2838 * Check if window is opening.
2840 * @return {boolean} Window is opening
2842 OO
.ui
.WindowManager
.prototype.isOpening = function ( win
) {
2843 return win
=== this.currentWindow
&& !!this.opening
&& this.opening
.state() === 'pending';
2847 * Check if window is closing.
2849 * @return {boolean} Window is closing
2851 OO
.ui
.WindowManager
.prototype.isClosing = function ( win
) {
2852 return win
=== this.currentWindow
&& !!this.closing
&& this.closing
.state() === 'pending';
2856 * Check if window is opened.
2858 * @return {boolean} Window is opened
2860 OO
.ui
.WindowManager
.prototype.isOpened = function ( win
) {
2861 return win
=== this.currentWindow
&& !!this.opened
&& this.opened
.state() === 'pending';
2865 * Check if a window is being managed.
2867 * @param {OO.ui.Window} win Window to check
2868 * @return {boolean} Window is being managed
2870 OO
.ui
.WindowManager
.prototype.hasWindow = function ( win
) {
2873 for ( name
in this.windows
) {
2874 if ( this.windows
[ name
] === win
) {
2883 * Get the number of milliseconds to wait between beginning opening and executing setup process.
2885 * @param {OO.ui.Window} win Window being opened
2886 * @param {Object} [data] Window opening data
2887 * @return {number} Milliseconds to wait
2889 OO
.ui
.WindowManager
.prototype.getSetupDelay = function () {
2894 * Get the number of milliseconds to wait between finishing setup and executing ready process.
2896 * @param {OO.ui.Window} win Window being opened
2897 * @param {Object} [data] Window opening data
2898 * @return {number} Milliseconds to wait
2900 OO
.ui
.WindowManager
.prototype.getReadyDelay = function () {
2905 * Get the number of milliseconds to wait between beginning closing and executing hold process.
2907 * @param {OO.ui.Window} win Window being closed
2908 * @param {Object} [data] Window closing data
2909 * @return {number} Milliseconds to wait
2911 OO
.ui
.WindowManager
.prototype.getHoldDelay = function () {
2916 * Get the number of milliseconds to wait between finishing hold and executing teardown process.
2918 * @param {OO.ui.Window} win Window being closed
2919 * @param {Object} [data] Window closing data
2920 * @return {number} Milliseconds to wait
2922 OO
.ui
.WindowManager
.prototype.getTeardownDelay = function () {
2923 return this.modal
? 250 : 0;
2927 * Get managed window by symbolic name.
2929 * If window is not yet instantiated, it will be instantiated and added automatically.
2931 * @param {string} name Symbolic window name
2932 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2933 * @throws {Error} If the symbolic name is unrecognized by the factory
2934 * @throws {Error} If the symbolic name unrecognized as a managed window
2936 OO
.ui
.WindowManager
.prototype.getWindow = function ( name
) {
2937 var deferred
= $.Deferred(),
2938 win
= this.windows
[ name
];
2940 if ( !( win
instanceof OO
.ui
.Window
) ) {
2941 if ( this.factory
) {
2942 if ( !this.factory
.lookup( name
) ) {
2943 deferred
.reject( new OO
.ui
.Error(
2944 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
2947 win
= this.factory
.create( name
);
2948 this.addWindows( [ win
] );
2949 deferred
.resolve( win
);
2952 deferred
.reject( new OO
.ui
.Error(
2953 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
2957 deferred
.resolve( win
);
2960 return deferred
.promise();
2964 * Get current window.
2966 * @return {OO.ui.Window|null} Currently opening/opened/closing window
2968 OO
.ui
.WindowManager
.prototype.getCurrentWindow = function () {
2969 return this.currentWindow
;
2975 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
2976 * @param {Object} [data] Window opening data
2977 * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
2978 * for more details about the `opening` promise
2981 OO
.ui
.WindowManager
.prototype.openWindow = function ( win
, data
) {
2983 opening
= $.Deferred();
2985 // Argument handling
2986 if ( typeof win
=== 'string' ) {
2987 return this.getWindow( win
).then( function ( win
) {
2988 return manager
.openWindow( win
, data
);
2993 if ( !this.hasWindow( win
) ) {
2994 opening
.reject( new OO
.ui
.Error(
2995 'Cannot open window: window is not attached to manager'
2997 } else if ( this.preparingToOpen
|| this.opening
|| this.opened
) {
2998 opening
.reject( new OO
.ui
.Error(
2999 'Cannot open window: another window is opening or open'
3004 if ( opening
.state() !== 'rejected' ) {
3005 // If a window is currently closing, wait for it to complete
3006 this.preparingToOpen
= $.when( this.closing
);
3007 // Ensure handlers get called after preparingToOpen is set
3008 this.preparingToOpen
.done( function () {
3009 if ( manager
.modal
) {
3010 manager
.toggleGlobalEvents( true );
3011 manager
.toggleAriaIsolation( true );
3013 manager
.currentWindow
= win
;
3014 manager
.opening
= opening
;
3015 manager
.preparingToOpen
= null;
3016 manager
.emit( 'opening', win
, opening
, data
);
3017 setTimeout( function () {
3018 win
.setup( data
).then( function () {
3019 manager
.updateWindowSize( win
);
3020 manager
.opening
.notify( { state
: 'setup' } );
3021 setTimeout( function () {
3022 win
.ready( data
).then( function () {
3023 manager
.opening
.notify( { state
: 'ready' } );
3024 manager
.opening
= null;
3025 manager
.opened
= $.Deferred();
3026 opening
.resolve( manager
.opened
.promise(), data
);
3028 }, manager
.getReadyDelay() );
3030 }, manager
.getSetupDelay() );
3034 return opening
.promise();
3040 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
3041 * @param {Object} [data] Window closing data
3042 * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
3043 * for more details about the `closing` promise
3044 * @throws {Error} If no window by that name is being managed
3047 OO
.ui
.WindowManager
.prototype.closeWindow = function ( win
, data
) {
3049 closing
= $.Deferred(),
3052 // Argument handling
3053 if ( typeof win
=== 'string' ) {
3054 win
= this.windows
[ win
];
3055 } else if ( !this.hasWindow( win
) ) {
3061 closing
.reject( new OO
.ui
.Error(
3062 'Cannot close window: window is not attached to manager'
3064 } else if ( win
!== this.currentWindow
) {
3065 closing
.reject( new OO
.ui
.Error(
3066 'Cannot close window: window already closed with different data'
3068 } else if ( this.preparingToClose
|| this.closing
) {
3069 closing
.reject( new OO
.ui
.Error(
3070 'Cannot close window: window already closing with different data'
3075 if ( closing
.state() !== 'rejected' ) {
3076 // If the window is currently opening, close it when it's done
3077 this.preparingToClose
= $.when( this.opening
);
3078 // Ensure handlers get called after preparingToClose is set
3079 this.preparingToClose
.done( function () {
3080 manager
.closing
= closing
;
3081 manager
.preparingToClose
= null;
3082 manager
.emit( 'closing', win
, closing
, data
);
3083 opened
= manager
.opened
;
3084 manager
.opened
= null;
3085 opened
.resolve( closing
.promise(), data
);
3086 setTimeout( function () {
3087 win
.hold( data
).then( function () {
3088 closing
.notify( { state
: 'hold' } );
3089 setTimeout( function () {
3090 win
.teardown( data
).then( function () {
3091 closing
.notify( { state
: 'teardown' } );
3092 if ( manager
.modal
) {
3093 manager
.toggleGlobalEvents( false );
3094 manager
.toggleAriaIsolation( false );
3096 manager
.closing
= null;
3097 manager
.currentWindow
= null;
3098 closing
.resolve( data
);
3100 }, manager
.getTeardownDelay() );
3102 }, manager
.getHoldDelay() );
3106 return closing
.promise();
3112 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
3113 * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
3114 * a statically configured symbolic name
3116 OO
.ui
.WindowManager
.prototype.addWindows = function ( windows
) {
3117 var i
, len
, win
, name
, list
;
3119 if ( Array
.isArray( windows
) ) {
3120 // Convert to map of windows by looking up symbolic names from static configuration
3122 for ( i
= 0, len
= windows
.length
; i
< len
; i
++ ) {
3123 name
= windows
[ i
].constructor.static.name
;
3124 if ( typeof name
!== 'string' ) {
3125 throw new Error( 'Cannot add window' );
3127 list
[ name
] = windows
[ i
];
3129 } else if ( OO
.isPlainObject( windows
) ) {
3134 for ( name
in list
) {
3136 this.windows
[ name
] = win
.toggle( false );
3137 this.$element
.append( win
.$element
);
3138 win
.setManager( this );
3145 * Windows will be closed before they are removed.
3147 * @param {string[]} names Symbolic names of windows to remove
3148 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3149 * @throws {Error} If windows being removed are not being managed
3151 OO
.ui
.WindowManager
.prototype.removeWindows = function ( names
) {
3152 var i
, len
, win
, name
, cleanupWindow
,
3155 cleanup = function ( name
, win
) {
3156 delete manager
.windows
[ name
];
3157 win
.$element
.detach();
3160 for ( i
= 0, len
= names
.length
; i
< len
; i
++ ) {
3162 win
= this.windows
[ name
];
3164 throw new Error( 'Cannot remove window' );
3166 cleanupWindow
= cleanup
.bind( null, name
, win
);
3167 promises
.push( this.closeWindow( name
).then( cleanupWindow
, cleanupWindow
) );
3170 return $.when
.apply( $, promises
);
3174 * Remove all windows.
3176 * Windows will be closed before they are removed.
3178 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3180 OO
.ui
.WindowManager
.prototype.clearWindows = function () {
3181 return this.removeWindows( Object
.keys( this.windows
) );
3187 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3191 OO
.ui
.WindowManager
.prototype.updateWindowSize = function ( win
) {
3192 // Bypass for non-current, and thus invisible, windows
3193 if ( win
!== this.currentWindow
) {
3197 var viewport
= OO
.ui
.Element
.static.getDimensions( win
.getElementWindow() ),
3198 sizes
= this.constructor.static.sizes
,
3199 size
= win
.getSize();
3201 if ( !sizes
[ size
] ) {
3202 size
= this.constructor.static.defaultSize
;
3204 if ( size
!== 'full' && viewport
.rect
.right
- viewport
.rect
.left
< sizes
[ size
].width
) {
3208 this.$element
.toggleClass( 'oo-ui-windowManager-fullscreen', size
=== 'full' );
3209 this.$element
.toggleClass( 'oo-ui-windowManager-floating', size
!== 'full' );
3210 win
.setDimensions( sizes
[ size
] );
3212 this.emit( 'resize', win
);
3218 * Bind or unbind global events for scrolling.
3220 * @param {boolean} [on] Bind global events
3223 OO
.ui
.WindowManager
.prototype.toggleGlobalEvents = function ( on
) {
3224 on
= on
=== undefined ? !!this.globalEvents
: !!on
;
3226 var $body
= $( this.getElementDocument().body
),
3227 // We could have multiple window managers open to only modify
3228 // the body class at the bottom of the stack
3229 stackDepth
= $body
.data( 'windowManagerGlobalEvents' ) || 0 ;
3232 if ( !this.globalEvents
) {
3233 $( this.getElementWindow() ).on( {
3234 // Start listening for top-level window dimension changes
3235 'orientationchange resize': this.onWindowResizeHandler
3237 if ( stackDepth
=== 0 ) {
3238 $body
.css( 'overflow', 'hidden' );
3241 this.globalEvents
= true;
3243 } else if ( this.globalEvents
) {
3244 $( this.getElementWindow() ).off( {
3245 // Stop listening for top-level window dimension changes
3246 'orientationchange resize': this.onWindowResizeHandler
3249 if ( stackDepth
=== 0 ) {
3250 $( this.getElementDocument().body
).css( 'overflow', '' );
3252 this.globalEvents
= false;
3254 $body
.data( 'windowManagerGlobalEvents', stackDepth
);
3260 * Toggle screen reader visibility of content other than the window manager.
3262 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3265 OO
.ui
.WindowManager
.prototype.toggleAriaIsolation = function ( isolate
) {
3266 isolate
= isolate
=== undefined ? !this.$ariaHidden
: !!isolate
;
3269 if ( !this.$ariaHidden
) {
3270 // Hide everything other than the window manager from screen readers
3271 this.$ariaHidden
= $( 'body' )
3273 .not( this.$element
.parentsUntil( 'body' ).last() )
3274 .attr( 'aria-hidden', '' );
3276 } else if ( this.$ariaHidden
) {
3277 // Restore screen reader visibility
3278 this.$ariaHidden
.removeAttr( 'aria-hidden' );
3279 this.$ariaHidden
= null;
3286 * Destroy window manager.
3288 OO
.ui
.WindowManager
.prototype.destroy = function () {
3289 this.toggleGlobalEvents( false );
3290 this.toggleAriaIsolation( false );
3291 this.clearWindows();
3292 this.$element
.remove();
3299 * @param {string|jQuery} message Description of error
3300 * @param {Object} [config] Configuration options
3301 * @cfg {boolean} [recoverable=true] Error is recoverable
3302 * @cfg {boolean} [warning=false] Whether this error is a warning or not.
3304 OO
.ui
.Error
= function OoUiError( message
, config
) {
3305 // Allow passing positional parameters inside the config object
3306 if ( OO
.isPlainObject( message
) && config
=== undefined ) {
3308 message
= config
.message
;
3311 // Configuration initialization
3312 config
= config
|| {};
3315 this.message
= message
instanceof jQuery
? message
: String( message
);
3316 this.recoverable
= config
.recoverable
=== undefined || !!config
.recoverable
;
3317 this.warning
= !!config
.warning
;
3322 OO
.initClass( OO
.ui
.Error
);
3327 * Check if error can be recovered from.
3329 * @return {boolean} Error is recoverable
3331 OO
.ui
.Error
.prototype.isRecoverable = function () {
3332 return this.recoverable
;
3336 * Check if the error is a warning
3338 * @return {boolean} Error is warning
3340 OO
.ui
.Error
.prototype.isWarning = function () {
3341 return this.warning
;
3345 * Get error message as DOM nodes.
3347 * @return {jQuery} Error message in DOM nodes
3349 OO
.ui
.Error
.prototype.getMessage = function () {
3350 return this.message
instanceof jQuery
?
3351 this.message
.clone() :
3352 $( '<div>' ).text( this.message
).contents();
3356 * Get error message as text.
3358 * @return {string} Error message
3360 OO
.ui
.Error
.prototype.getMessageText = function () {
3361 return this.message
instanceof jQuery
? this.message
.text() : this.message
;
3365 * Wraps an HTML snippet for use with configuration values which default
3366 * to strings. This bypasses the default html-escaping done to string
3372 * @param {string} [content] HTML content
3374 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
3376 this.content
= content
;
3381 OO
.initClass( OO
.ui
.HtmlSnippet
);
3388 * @return {string} Unchanged HTML snippet.
3390 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
3391 return this.content
;
3395 * A list of functions, called in sequence.
3397 * If a function added to a process returns boolean false the process will stop; if it returns an
3398 * object with a `promise` method the process will use the promise to either continue to the next
3399 * step when the promise is resolved or stop when the promise is rejected.
3404 * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
3405 * call, see #createStep for more information
3406 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3408 * @return {Object} Step object, with `callback` and `context` properties
3410 OO
.ui
.Process = function ( step
, context
) {
3415 if ( step
!== undefined ) {
3416 this.next( step
, context
);
3422 OO
.initClass( OO
.ui
.Process
);
3427 * Start the process.
3429 * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
3430 * any of the steps return boolean false or a promise which gets rejected; upon stopping the
3431 * process, the remaining steps will not be taken
3433 OO
.ui
.Process
.prototype.execute = function () {
3434 var i
, len
, promise
;
3437 * Continue execution.
3440 * @param {Array} step A function and the context it should be called in
3441 * @return {Function} Function that continues the process
3443 function proceed( step
) {
3444 return function () {
3445 // Execute step in the correct context
3447 result
= step
.callback
.call( step
.context
);
3449 if ( result
=== false ) {
3450 // Use rejected promise for boolean false results
3451 return $.Deferred().reject( [] ).promise();
3453 if ( typeof result
=== 'number' ) {
3455 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3457 // Use a delayed promise for numbers, expecting them to be in milliseconds
3458 deferred
= $.Deferred();
3459 setTimeout( deferred
.resolve
, result
);
3460 return deferred
.promise();
3462 if ( result
instanceof OO
.ui
.Error
) {
3463 // Use rejected promise for error
3464 return $.Deferred().reject( [ result
] ).promise();
3466 if ( Array
.isArray( result
) && result
.length
&& result
[ 0 ] instanceof OO
.ui
.Error
) {
3467 // Use rejected promise for list of errors
3468 return $.Deferred().reject( result
).promise();
3470 // Duck-type the object to see if it can produce a promise
3471 if ( result
&& $.isFunction( result
.promise
) ) {
3472 // Use a promise generated from the result
3473 return result
.promise();
3475 // Use resolved promise for other results
3476 return $.Deferred().resolve().promise();
3480 if ( this.steps
.length
) {
3481 // Generate a chain reaction of promises
3482 promise
= proceed( this.steps
[ 0 ] )();
3483 for ( i
= 1, len
= this.steps
.length
; i
< len
; i
++ ) {
3484 promise
= promise
.then( proceed( this.steps
[ i
] ) );
3487 promise
= $.Deferred().resolve().promise();
3494 * Create a process step.
3497 * @param {number|jQuery.Promise|Function} step
3499 * - Number of milliseconds to wait; or
3500 * - Promise to wait to be resolved; or
3501 * - Function to execute
3502 * - If it returns boolean false the process will stop
3503 * - If it returns an object with a `promise` method the process will use the promise to either
3504 * continue to the next step when the promise is resolved or stop when the promise is rejected
3505 * - If it returns a number, the process will wait for that number of milliseconds before
3507 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3509 * @return {Object} Step object, with `callback` and `context` properties
3511 OO
.ui
.Process
.prototype.createStep = function ( step
, context
) {
3512 if ( typeof step
=== 'number' || $.isFunction( step
.promise
) ) {
3514 callback: function () {
3520 if ( $.isFunction( step
) ) {
3526 throw new Error( 'Cannot create process step: number, promise or function expected' );
3530 * Add step to the beginning of the process.
3532 * @inheritdoc #createStep
3533 * @return {OO.ui.Process} this
3536 OO
.ui
.Process
.prototype.first = function ( step
, context
) {
3537 this.steps
.unshift( this.createStep( step
, context
) );
3542 * Add step to the end of the process.
3544 * @inheritdoc #createStep
3545 * @return {OO.ui.Process} this
3548 OO
.ui
.Process
.prototype.next = function ( step
, context
) {
3549 this.steps
.push( this.createStep( step
, context
) );
3554 * Factory for tools.
3557 * @extends OO.Factory
3560 OO
.ui
.ToolFactory
= function OoUiToolFactory() {
3561 // Parent constructor
3562 OO
.ui
.ToolFactory
.super.call( this );
3567 OO
.inheritClass( OO
.ui
.ToolFactory
, OO
.Factory
);
3572 * Get tools from the factory
3574 * @param {Array} include Included tools
3575 * @param {Array} exclude Excluded tools
3576 * @param {Array} promote Promoted tools
3577 * @param {Array} demote Demoted tools
3578 * @return {string[]} List of tools
3580 OO
.ui
.ToolFactory
.prototype.getTools = function ( include
, exclude
, promote
, demote
) {
3581 var i
, len
, included
, promoted
, demoted
,
3585 // Collect included and not excluded tools
3586 included
= OO
.simpleArrayDifference( this.extract( include
), this.extract( exclude
) );
3589 promoted
= this.extract( promote
, used
);
3590 demoted
= this.extract( demote
, used
);
3593 for ( i
= 0, len
= included
.length
; i
< len
; i
++ ) {
3594 if ( !used
[ included
[ i
] ] ) {
3595 auto
.push( included
[ i
] );
3599 return promoted
.concat( auto
).concat( demoted
);
3603 * Get a flat list of names from a list of names or groups.
3605 * Tools can be specified in the following ways:
3607 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3608 * - All tools in a group: `{ group: 'group-name' }`
3609 * - All tools: `'*'`
3612 * @param {Array|string} collection List of tools
3613 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3614 * names will be added as properties
3615 * @return {string[]} List of extracted names
3617 OO
.ui
.ToolFactory
.prototype.extract = function ( collection
, used
) {
3618 var i
, len
, item
, name
, tool
,
3621 if ( collection
=== '*' ) {
3622 for ( name
in this.registry
) {
3623 tool
= this.registry
[ name
];
3625 // Only add tools by group name when auto-add is enabled
3626 tool
.static.autoAddToCatchall
&&
3627 // Exclude already used tools
3628 ( !used
|| !used
[ name
] )
3632 used
[ name
] = true;
3636 } else if ( Array
.isArray( collection
) ) {
3637 for ( i
= 0, len
= collection
.length
; i
< len
; i
++ ) {
3638 item
= collection
[ i
];
3639 // Allow plain strings as shorthand for named tools
3640 if ( typeof item
=== 'string' ) {
3641 item
= { name
: item
};
3643 if ( OO
.isPlainObject( item
) ) {
3645 for ( name
in this.registry
) {
3646 tool
= this.registry
[ name
];
3648 // Include tools with matching group
3649 tool
.static.group
=== item
.group
&&
3650 // Only add tools by group name when auto-add is enabled
3651 tool
.static.autoAddToGroup
&&
3652 // Exclude already used tools
3653 ( !used
|| !used
[ name
] )
3657 used
[ name
] = true;
3661 // Include tools with matching name and exclude already used tools
3662 } else if ( item
.name
&& ( !used
|| !used
[ item
.name
] ) ) {
3663 names
.push( item
.name
);
3665 used
[ item
.name
] = true;
3675 * Factory for tool groups.
3678 * @extends OO.Factory
3681 OO
.ui
.ToolGroupFactory
= function OoUiToolGroupFactory() {
3682 // Parent constructor
3683 OO
.Factory
.call( this );
3686 defaultClasses
= this.constructor.static.getDefaultClasses();
3688 // Register default toolgroups
3689 for ( i
= 0, l
= defaultClasses
.length
; i
< l
; i
++ ) {
3690 this.register( defaultClasses
[ i
] );
3696 OO
.inheritClass( OO
.ui
.ToolGroupFactory
, OO
.Factory
);
3698 /* Static Methods */
3701 * Get a default set of classes to be registered on construction
3703 * @return {Function[]} Default classes
3705 OO
.ui
.ToolGroupFactory
.static.getDefaultClasses = function () {
3708 OO
.ui
.ListToolGroup
,
3720 * @param {Object} [config] Configuration options
3722 OO
.ui
.Theme
= function OoUiTheme( config
) {
3723 // Configuration initialization
3724 config
= config
|| {};
3729 OO
.initClass( OO
.ui
.Theme
);
3734 * Get a list of classes to be applied to a widget.
3736 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
3737 * otherwise state transitions will not work properly.
3739 * @param {OO.ui.Element} element Element for which to get classes
3740 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3742 OO
.ui
.Theme
.prototype.getElementClasses = function ( /* element */ ) {
3743 return { on
: [], off
: [] };
3747 * Update CSS classes provided by the theme.
3749 * For elements with theme logic hooks, this should be called any time there's a state change.
3751 * @param {OO.ui.Element} element Element for which to update classes
3752 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3754 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
3755 var classes
= this.getElementClasses( element
);
3758 .removeClass( classes
.off
.join( ' ' ) )
3759 .addClass( classes
.on
.join( ' ' ) );
3763 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
3764 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
3765 * order in which users will navigate through the focusable elements via the "tab" key.
3768 * // TabIndexedElement is mixed into the ButtonWidget class
3769 * // to provide a tabIndex property.
3770 * var button1 = new OO.ui.ButtonWidget( {
3774 * var button2 = new OO.ui.ButtonWidget( {
3778 * var button3 = new OO.ui.ButtonWidget( {
3782 * var button4 = new OO.ui.ButtonWidget( {
3786 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
3792 * @param {Object} [config] Configuration options
3793 * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element
3794 * @cfg {number|null} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
3795 * prevent tab focusing, use null to suppress the `tabindex` attribute.
3797 OO
.ui
.TabIndexedElement
= function OoUiTabIndexedElement( config
) {
3798 // Configuration initialization
3799 config
= $.extend( { tabIndex
: 0 }, config
);
3802 this.$tabIndexed
= null;
3803 this.tabIndex
= null;
3806 this.connect( this, { disable
: 'onDisable' } );
3809 this.setTabIndex( config
.tabIndex
);
3810 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
3815 OO
.initClass( OO
.ui
.TabIndexedElement
);
3820 * Set the element with `tabindex` attribute.
3822 * If an element is already set, it will be cleaned up before setting up the new element.
3824 * @param {jQuery} $tabIndexed Element to set tab index on
3827 OO
.ui
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
3828 var tabIndex
= this.tabIndex
;
3829 // Remove attributes from old $tabIndexed
3830 this.setTabIndex( null );
3831 // Force update of new $tabIndexed
3832 this.$tabIndexed
= $tabIndexed
;
3833 this.tabIndex
= tabIndex
;
3834 return this.updateTabIndex();
3838 * Set tab index value.
3840 * @param {number|null} tabIndex Tab index value or null for no tab index
3843 OO
.ui
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
3844 tabIndex
= typeof tabIndex
=== 'number' ? tabIndex
: null;
3846 if ( this.tabIndex
!== tabIndex
) {
3847 this.tabIndex
= tabIndex
;
3848 this.updateTabIndex();
3855 * Update the `tabindex` attribute, in case of changes to tab index or
3860 OO
.ui
.TabIndexedElement
.prototype.updateTabIndex = function () {
3861 if ( this.$tabIndexed
) {
3862 if ( this.tabIndex
!== null ) {
3863 // Do not index over disabled elements
3864 this.$tabIndexed
.attr( {
3865 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
3866 // ChromeVox and NVDA do not seem to inherit this from parent elements
3867 'aria-disabled': this.isDisabled().toString()
3870 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
3877 * Handle disable events.
3880 * @param {boolean} disabled Element is disabled
3882 OO
.ui
.TabIndexedElement
.prototype.onDisable = function () {
3883 this.updateTabIndex();
3887 * Get tab index value.
3889 * @return {number|null} Tab index value
3891 OO
.ui
.TabIndexedElement
.prototype.getTabIndex = function () {
3892 return this.tabIndex
;
3896 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
3897 * interface element that can be configured with access keys for accessibility.
3898 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
3900 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
3905 * @param {Object} [config] Configuration options
3906 * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
3907 * @cfg {boolean} [framed=true] Render button with a frame
3908 * @cfg {string} [accessKey] Button's access key
3910 OO
.ui
.ButtonElement
= function OoUiButtonElement( config
) {
3911 // Configuration initialization
3912 config
= config
|| {};
3915 this.$button
= null;
3917 this.accessKey
= null;
3918 this.active
= false;
3919 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
3920 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
3921 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
3922 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
3923 this.onClickHandler
= this.onClick
.bind( this );
3924 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
3927 this.$element
.addClass( 'oo-ui-buttonElement' );
3928 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
3929 this.setAccessKey( config
.accessKey
);
3930 this.setButtonElement( config
.$button
|| $( '<a>' ) );
3935 OO
.initClass( OO
.ui
.ButtonElement
);
3937 /* Static Properties */
3940 * Cancel mouse down events.
3944 * @property {boolean}
3946 OO
.ui
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
3957 * Set the button element.
3959 * If an element is already set, it will be cleaned up before setting up the new element.
3961 * @param {jQuery} $button Element to use as button
3963 OO
.ui
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
3964 if ( this.$button
) {
3966 .removeClass( 'oo-ui-buttonElement-button' )
3967 .removeAttr( 'role accesskey' )
3969 mousedown
: this.onMouseDownHandler
,
3970 keydown
: this.onKeyDownHandler
,
3971 click
: this.onClickHandler
,
3972 keypress
: this.onKeyPressHandler
3976 this.$button
= $button
3977 .addClass( 'oo-ui-buttonElement-button' )
3978 .attr( { role
: 'button', accesskey
: this.accessKey
} )
3980 mousedown
: this.onMouseDownHandler
,
3981 keydown
: this.onKeyDownHandler
,
3982 click
: this.onClickHandler
,
3983 keypress
: this.onKeyPressHandler
3988 * Handles mouse down events.
3991 * @param {jQuery.Event} e Mouse down event
3993 OO
.ui
.ButtonElement
.prototype.onMouseDown = function ( e
) {
3994 if ( this.isDisabled() || e
.which
!== 1 ) {
3997 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
3998 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
3999 // reliably remove the pressed class
4000 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
4001 // Prevent change of focus unless specifically configured otherwise
4002 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
4008 * Handles mouse up events.
4011 * @param {jQuery.Event} e Mouse up event
4013 OO
.ui
.ButtonElement
.prototype.onMouseUp = function ( e
) {
4014 if ( this.isDisabled() || e
.which
!== 1 ) {
4017 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
4018 // Stop listening for mouseup, since we only needed this once
4019 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
4023 * Handles mouse click events.
4026 * @param {jQuery.Event} e Mouse click event
4029 OO
.ui
.ButtonElement
.prototype.onClick = function ( e
) {
4030 if ( !this.isDisabled() && e
.which
=== 1 ) {
4031 this.emit( 'click' );
4037 * Handles key down events.
4040 * @param {jQuery.Event} e Key down event
4042 OO
.ui
.ButtonElement
.prototype.onKeyDown = function ( e
) {
4043 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
4046 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
4047 // Run the keyup handler no matter where the key is when the button is let go, so we can
4048 // reliably remove the pressed class
4049 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
4053 * Handles key up events.
4056 * @param {jQuery.Event} e Key up event
4058 OO
.ui
.ButtonElement
.prototype.onKeyUp = function ( e
) {
4059 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
4062 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
4063 // Stop listening for keyup, since we only needed this once
4064 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
4068 * Handles key press events.
4071 * @param {jQuery.Event} e Key press event
4074 OO
.ui
.ButtonElement
.prototype.onKeyPress = function ( e
) {
4075 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
4076 this.emit( 'click' );
4082 * Check if button has a frame.
4084 * @return {boolean} Button is framed
4086 OO
.ui
.ButtonElement
.prototype.isFramed = function () {
4093 * @param {boolean} [framed] Make button framed, omit to toggle
4096 OO
.ui
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
4097 framed
= framed
=== undefined ? !this.framed
: !!framed
;
4098 if ( framed
!== this.framed
) {
4099 this.framed
= framed
;
4101 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
4102 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
4103 this.updateThemeClasses();
4112 * @param {string} accessKey Button's access key, use empty string to remove
4115 OO
.ui
.ButtonElement
.prototype.setAccessKey = function ( accessKey
) {
4116 accessKey
= typeof accessKey
=== 'string' && accessKey
.length
? accessKey
: null;
4118 if ( this.accessKey
!== accessKey
) {
4119 if ( this.$button
) {
4120 if ( accessKey
!== null ) {
4121 this.$button
.attr( 'accesskey', accessKey
);
4123 this.$button
.removeAttr( 'accesskey' );
4126 this.accessKey
= accessKey
;
4135 * @param {boolean} [value] Make button active
4138 OO
.ui
.ButtonElement
.prototype.setActive = function ( value
) {
4139 this.$element
.toggleClass( 'oo-ui-buttonElement-active', !!value
);
4144 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4145 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4146 * items from the group is done through the interface the class provides.
4147 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4149 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4155 * @param {Object} [config] Configuration options
4156 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4158 OO
.ui
.GroupElement
= function OoUiGroupElement( config
) {
4159 // Configuration initialization
4160 config
= config
|| {};
4165 this.aggregateItemEvents
= {};
4168 this.setGroupElement( config
.$group
|| $( '<div>' ) );
4174 * Set the group element.
4176 * If an element is already set, items will be moved to the new element.
4178 * @param {jQuery} $group Element to use as group
4180 OO
.ui
.GroupElement
.prototype.setGroupElement = function ( $group
) {
4183 this.$group
= $group
;
4184 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4185 this.$group
.append( this.items
[ i
].$element
);
4190 * Check if there are no items.
4192 * @return {boolean} Group is empty
4194 OO
.ui
.GroupElement
.prototype.isEmpty = function () {
4195 return !this.items
.length
;
4201 * @return {OO.ui.Element[]} Items
4203 OO
.ui
.GroupElement
.prototype.getItems = function () {
4204 return this.items
.slice( 0 );
4208 * Get an item by its data.
4210 * Data is compared by a hash of its value. Only the first item with matching data will be returned.
4212 * @param {Object} data Item data to search for
4213 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4215 OO
.ui
.GroupElement
.prototype.getItemFromData = function ( data
) {
4217 hash
= OO
.getHash( data
);
4219 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4220 item
= this.items
[ i
];
4221 if ( hash
=== OO
.getHash( item
.getData() ) ) {
4230 * Get items by their data.
4232 * Data is compared by a hash of its value. All items with matching data will be returned.
4234 * @param {Object} data Item data to search for
4235 * @return {OO.ui.Element[]} Items with equivalent data
4237 OO
.ui
.GroupElement
.prototype.getItemsFromData = function ( data
) {
4239 hash
= OO
.getHash( data
),
4242 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4243 item
= this.items
[ i
];
4244 if ( hash
=== OO
.getHash( item
.getData() ) ) {
4253 * Add an aggregate item event.
4255 * Aggregated events are listened to on each item and then emitted by the group under a new name,
4256 * and with an additional leading parameter containing the item that emitted the original event.
4257 * Other arguments that were emitted from the original event are passed through.
4259 * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
4260 * event, use null value to remove aggregation
4261 * @throws {Error} If aggregation already exists
4263 OO
.ui
.GroupElement
.prototype.aggregate = function ( events
) {
4264 var i
, len
, item
, add
, remove
, itemEvent
, groupEvent
;
4266 for ( itemEvent
in events
) {
4267 groupEvent
= events
[ itemEvent
];
4269 // Remove existing aggregated event
4270 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
4271 // Don't allow duplicate aggregations
4273 throw new Error( 'Duplicate item event aggregation for ' + itemEvent
);
4275 // Remove event aggregation from existing items
4276 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4277 item
= this.items
[ i
];
4278 if ( item
.connect
&& item
.disconnect
) {
4280 remove
[ itemEvent
] = [ 'emit', groupEvent
, item
];
4281 item
.disconnect( this, remove
);
4284 // Prevent future items from aggregating event
4285 delete this.aggregateItemEvents
[ itemEvent
];
4288 // Add new aggregate event
4290 // Make future items aggregate event
4291 this.aggregateItemEvents
[ itemEvent
] = groupEvent
;
4292 // Add event aggregation to existing items
4293 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4294 item
= this.items
[ i
];
4295 if ( item
.connect
&& item
.disconnect
) {
4297 add
[ itemEvent
] = [ 'emit', groupEvent
, item
];
4298 item
.connect( this, add
);
4308 * Adding an existing item will move it.
4310 * @param {OO.ui.Element[]} items Items
4311 * @param {number} [index] Index to insert items at
4314 OO
.ui
.GroupElement
.prototype.addItems = function ( items
, index
) {
4315 var i
, len
, item
, event
, events
, currentIndex
,
4318 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
4321 // Check if item exists then remove it first, effectively "moving" it
4322 currentIndex
= $.inArray( item
, this.items
);
4323 if ( currentIndex
>= 0 ) {
4324 this.removeItems( [ item
] );
4325 // Adjust index to compensate for removal
4326 if ( currentIndex
< index
) {
4331 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
4333 for ( event
in this.aggregateItemEvents
) {
4334 events
[ event
] = [ 'emit', this.aggregateItemEvents
[ event
], item
];
4336 item
.connect( this, events
);
4338 item
.setElementGroup( this );
4339 itemElements
.push( item
.$element
.get( 0 ) );
4342 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
4343 this.$group
.append( itemElements
);
4344 this.items
.push
.apply( this.items
, items
);
4345 } else if ( index
=== 0 ) {
4346 this.$group
.prepend( itemElements
);
4347 this.items
.unshift
.apply( this.items
, items
);
4349 this.items
[ index
].$element
.before( itemElements
);
4350 this.items
.splice
.apply( this.items
, [ index
, 0 ].concat( items
) );
4359 * Items will be detached, not removed, so they can be used later.
4361 * @param {OO.ui.Element[]} items Items to remove
4364 OO
.ui
.GroupElement
.prototype.removeItems = function ( items
) {
4365 var i
, len
, item
, index
, remove
, itemEvent
;
4367 // Remove specific items
4368 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
4370 index
= $.inArray( item
, this.items
);
4371 if ( index
!== -1 ) {
4373 item
.connect
&& item
.disconnect
&&
4374 !$.isEmptyObject( this.aggregateItemEvents
)
4377 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
4378 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
4380 item
.disconnect( this, remove
);
4382 item
.setElementGroup( null );
4383 this.items
.splice( index
, 1 );
4384 item
.$element
.detach();
4394 * Items will be detached, not removed, so they can be used later.
4398 OO
.ui
.GroupElement
.prototype.clearItems = function () {
4399 var i
, len
, item
, remove
, itemEvent
;
4402 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4403 item
= this.items
[ i
];
4405 item
.connect
&& item
.disconnect
&&
4406 !$.isEmptyObject( this.aggregateItemEvents
)
4409 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
4410 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
4412 item
.disconnect( this, remove
);
4414 item
.setElementGroup( null );
4415 item
.$element
.detach();
4423 * DraggableElement is a mixin class used to create elements that can be clicked
4424 * and dragged by a mouse to a new position within a group. This class must be used
4425 * in conjunction with OO.ui.DraggableGroupElement, which provides a container for
4426 * the draggable elements.
4433 OO
.ui
.DraggableElement
= function OoUiDraggableElement() {
4437 // Initialize and events
4439 .attr( 'draggable', true )
4440 .addClass( 'oo-ui-draggableElement' )
4442 dragstart
: this.onDragStart
.bind( this ),
4443 dragover
: this.onDragOver
.bind( this ),
4444 dragend
: this.onDragEnd
.bind( this ),
4445 drop
: this.onDrop
.bind( this )
4449 OO
.initClass( OO
.ui
.DraggableElement
);
4456 * A dragstart event is emitted when the user clicks and begins dragging an item.
4457 * @param {OO.ui.DraggableElement} item The item the user has clicked and is dragging with the mouse.
4462 * A dragend event is emitted when the user drags an item and releases the mouse,
4463 * thus terminating the drag operation.
4468 * A drop event is emitted when the user drags an item and then releases the mouse button
4469 * over a valid target.
4472 /* Static Properties */
4475 * @inheritdoc OO.ui.ButtonElement
4477 OO
.ui
.DraggableElement
.static.cancelButtonMouseDownEvents
= false;
4482 * Respond to dragstart event.
4485 * @param {jQuery.Event} event jQuery event
4488 OO
.ui
.DraggableElement
.prototype.onDragStart = function ( e
) {
4489 var dataTransfer
= e
.originalEvent
.dataTransfer
;
4490 // Define drop effect
4491 dataTransfer
.dropEffect
= 'none';
4492 dataTransfer
.effectAllowed
= 'move';
4493 // We must set up a dataTransfer data property or Firefox seems to
4494 // ignore the fact the element is draggable.
4496 dataTransfer
.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4498 // The above is only for firefox. No need to set a catch clause
4499 // if it fails, move on.
4501 // Add dragging class
4502 this.$element
.addClass( 'oo-ui-draggableElement-dragging' );
4504 this.emit( 'dragstart', this );
4509 * Respond to dragend event.
4514 OO
.ui
.DraggableElement
.prototype.onDragEnd = function () {
4515 this.$element
.removeClass( 'oo-ui-draggableElement-dragging' );
4516 this.emit( 'dragend' );
4520 * Handle drop event.
4523 * @param {jQuery.Event} event jQuery event
4526 OO
.ui
.DraggableElement
.prototype.onDrop = function ( e
) {
4528 this.emit( 'drop', e
);
4532 * In order for drag/drop to work, the dragover event must
4533 * return false and stop propogation.
4537 OO
.ui
.DraggableElement
.prototype.onDragOver = function ( e
) {
4543 * Store it in the DOM so we can access from the widget drag event
4546 * @param {number} Item index
4548 OO
.ui
.DraggableElement
.prototype.setIndex = function ( index
) {
4549 if ( this.index
!== index
) {
4551 this.$element
.data( 'index', index
);
4559 * @return {number} Item index
4561 OO
.ui
.DraggableElement
.prototype.getIndex = function () {
4566 * DraggableGroupElement is a mixin class used to create a group element to
4567 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4568 * The class is used with OO.ui.DraggableElement.
4574 * @param {Object} [config] Configuration options
4575 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4576 * @cfg {string} [orientation] Item orientation, 'horizontal' or 'vertical'. Defaults to 'vertical'
4578 OO
.ui
.DraggableGroupElement
= function OoUiDraggableGroupElement( config
) {
4579 // Configuration initialization
4580 config
= config
|| {};
4582 // Parent constructor
4583 OO
.ui
.GroupElement
.call( this, config
);
4586 this.orientation
= config
.orientation
|| 'vertical';
4587 this.dragItem
= null;
4588 this.itemDragOver
= null;
4590 this.sideInsertion
= '';
4594 dragstart
: 'itemDragStart',
4595 dragend
: 'itemDragEnd',
4598 this.connect( this, {
4599 itemDragStart
: 'onItemDragStart',
4600 itemDrop
: 'onItemDrop',
4601 itemDragEnd
: 'onItemDragEnd'
4604 dragover
: $.proxy( this.onDragOver
, this ),
4605 dragleave
: $.proxy( this.onDragLeave
, this )
4609 if ( Array
.isArray( config
.items
) ) {
4610 this.addItems( config
.items
);
4612 this.$placeholder
= $( '<div>' )
4613 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4615 .addClass( 'oo-ui-draggableGroupElement' )
4616 .append( this.$status
)
4617 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation
=== 'horizontal' )
4618 .prepend( this.$placeholder
);
4622 OO
.mixinClass( OO
.ui
.DraggableGroupElement
, OO
.ui
.GroupElement
);
4628 * @param {OO.ui.DraggableElement} item Reordered item
4629 * @param {number} [newIndex] New index for the item
4635 * Respond to item drag start event
4636 * @param {OO.ui.DraggableElement} item Dragged item
4638 OO
.ui
.DraggableGroupElement
.prototype.onItemDragStart = function ( item
) {
4641 // Map the index of each object
4642 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4643 this.items
[ i
].setIndex( i
);
4646 if ( this.orientation
=== 'horizontal' ) {
4647 // Set the height of the indicator
4648 this.$placeholder
.css( {
4649 height
: item
.$element
.outerHeight(),
4653 // Set the width of the indicator
4654 this.$placeholder
.css( {
4656 width
: item
.$element
.outerWidth()
4659 this.setDragItem( item
);
4663 * Respond to item drag end event
4665 OO
.ui
.DraggableGroupElement
.prototype.onItemDragEnd = function () {
4666 this.unsetDragItem();
4671 * Handle drop event and switch the order of the items accordingly
4672 * @param {OO.ui.DraggableElement} item Dropped item
4675 OO
.ui
.DraggableGroupElement
.prototype.onItemDrop = function ( item
) {
4676 var toIndex
= item
.getIndex();
4677 // Check if the dropped item is from the current group
4678 // TODO: Figure out a way to configure a list of legally droppable
4679 // elements even if they are not yet in the list
4680 if ( this.getDragItem() ) {
4681 // If the insertion point is 'after', the insertion index
4682 // is shifted to the right (or to the left in RTL, hence 'after')
4683 if ( this.sideInsertion
=== 'after' ) {
4686 // Emit change event
4687 this.emit( 'reorder', this.getDragItem(), toIndex
);
4689 this.unsetDragItem();
4690 // Return false to prevent propogation
4695 * Handle dragleave event.
4697 OO
.ui
.DraggableGroupElement
.prototype.onDragLeave = function () {
4698 // This means the item was dragged outside the widget
4701 .addClass( 'oo-ui-element-hidden' );
4705 * Respond to dragover event
4706 * @param {jQuery.Event} event Event details
4708 OO
.ui
.DraggableGroupElement
.prototype.onDragOver = function ( e
) {
4709 var dragOverObj
, $optionWidget
, itemOffset
, itemMidpoint
, itemBoundingRect
,
4710 itemSize
, cssOutput
, dragPosition
, itemIndex
, itemPosition
,
4711 clientX
= e
.originalEvent
.clientX
,
4712 clientY
= e
.originalEvent
.clientY
;
4714 // Get the OptionWidget item we are dragging over
4715 dragOverObj
= this.getElementDocument().elementFromPoint( clientX
, clientY
);
4716 $optionWidget
= $( dragOverObj
).closest( '.oo-ui-draggableElement' );
4717 if ( $optionWidget
[ 0 ] ) {
4718 itemOffset
= $optionWidget
.offset();
4719 itemBoundingRect
= $optionWidget
[ 0 ].getBoundingClientRect();
4720 itemPosition
= $optionWidget
.position();
4721 itemIndex
= $optionWidget
.data( 'index' );
4726 this.isDragging() &&
4727 itemIndex
!== this.getDragItem().getIndex()
4729 if ( this.orientation
=== 'horizontal' ) {
4730 // Calculate where the mouse is relative to the item width
4731 itemSize
= itemBoundingRect
.width
;
4732 itemMidpoint
= itemBoundingRect
.left
+ itemSize
/ 2;
4733 dragPosition
= clientX
;
4734 // Which side of the item we hover over will dictate
4735 // where the placeholder will appear, on the left or
4738 left
: dragPosition
< itemMidpoint
? itemPosition
.left
: itemPosition
.left
+ itemSize
,
4739 top
: itemPosition
.top
4742 // Calculate where the mouse is relative to the item height
4743 itemSize
= itemBoundingRect
.height
;
4744 itemMidpoint
= itemBoundingRect
.top
+ itemSize
/ 2;
4745 dragPosition
= clientY
;
4746 // Which side of the item we hover over will dictate
4747 // where the placeholder will appear, on the top or
4750 top
: dragPosition
< itemMidpoint
? itemPosition
.top
: itemPosition
.top
+ itemSize
,
4751 left
: itemPosition
.left
4754 // Store whether we are before or after an item to rearrange
4755 // For horizontal layout, we need to account for RTL, as this is flipped
4756 if ( this.orientation
=== 'horizontal' && this.$element
.css( 'direction' ) === 'rtl' ) {
4757 this.sideInsertion
= dragPosition
< itemMidpoint
? 'after' : 'before';
4759 this.sideInsertion
= dragPosition
< itemMidpoint
? 'before' : 'after';
4761 // Add drop indicator between objects
4764 .removeClass( 'oo-ui-element-hidden' );
4766 // This means the item was dragged outside the widget
4769 .addClass( 'oo-ui-element-hidden' );
4776 * Set a dragged item
4777 * @param {OO.ui.DraggableElement} item Dragged item
4779 OO
.ui
.DraggableGroupElement
.prototype.setDragItem = function ( item
) {
4780 this.dragItem
= item
;
4784 * Unset the current dragged item
4786 OO
.ui
.DraggableGroupElement
.prototype.unsetDragItem = function () {
4787 this.dragItem
= null;
4788 this.itemDragOver
= null;
4789 this.$placeholder
.addClass( 'oo-ui-element-hidden' );
4790 this.sideInsertion
= '';
4794 * Get the current dragged item
4795 * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
4797 OO
.ui
.DraggableGroupElement
.prototype.getDragItem = function () {
4798 return this.dragItem
;
4802 * Check if there's an item being dragged.
4803 * @return {Boolean} Item is being dragged
4805 OO
.ui
.DraggableGroupElement
.prototype.isDragging = function () {
4806 return this.getDragItem() !== null;
4810 * IconElement is often mixed into other classes to generate an icon.
4811 * Icons are graphics, about the size of normal text. They are used to aid the user
4812 * in locating a control or to convey information in a space-efficient way. See the
4813 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
4814 * included in the library.
4816 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4822 * @param {Object} [config] Configuration options
4823 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
4824 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
4825 * the icon element be set to an existing icon instead of the one generated by this class, set a
4826 * value using a jQuery selection. For example:
4828 * // Use a <div> tag instead of a <span>
4830 * // Use an existing icon element instead of the one generated by the class
4831 * $icon: this.$element
4832 * // Use an icon element from a child widget
4833 * $icon: this.childwidget.$element
4834 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
4835 * symbolic names. A map is used for i18n purposes and contains a `default` icon
4836 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
4837 * by the user's language.
4839 * Example of an i18n map:
4841 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4842 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
4843 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4844 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
4845 * text. The icon title is displayed when users move the mouse over the icon.
4847 OO
.ui
.IconElement
= function OoUiIconElement( config
) {
4848 // Configuration initialization
4849 config
= config
|| {};
4854 this.iconTitle
= null;
4857 this.setIcon( config
.icon
|| this.constructor.static.icon
);
4858 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
4859 this.setIconElement( config
.$icon
|| $( '<span>' ) );
4864 OO
.initClass( OO
.ui
.IconElement
);
4866 /* Static Properties */
4869 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
4870 * for i18n purposes and contains a `default` icon name and additional names keyed by
4871 * language code. The `default` name is used when no icon is keyed by the user's language.
4873 * Example of an i18n map:
4875 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4877 * Note: the static property will be overridden if the #icon configuration is used.
4881 * @property {Object|string}
4883 OO
.ui
.IconElement
.static.icon
= null;
4886 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
4887 * function that returns title text, or `null` for no title.
4889 * The static property will be overridden if the #iconTitle configuration is used.
4893 * @property {string|Function|null}
4895 OO
.ui
.IconElement
.static.iconTitle
= null;
4900 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
4901 * applies to the specified icon element instead of the one created by the class. If an icon
4902 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
4903 * and mixin methods will no longer affect the element.
4905 * @param {jQuery} $icon Element to use as icon
4907 OO
.ui
.IconElement
.prototype.setIconElement = function ( $icon
) {
4910 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
4911 .removeAttr( 'title' );
4915 .addClass( 'oo-ui-iconElement-icon' )
4916 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
4917 if ( this.iconTitle
!== null ) {
4918 this.$icon
.attr( 'title', this.iconTitle
);
4923 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
4924 * The icon parameter can also be set to a map of icon names. See the #icon config setting
4927 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
4928 * by language code, or `null` to remove the icon.
4931 OO
.ui
.IconElement
.prototype.setIcon = function ( icon
) {
4932 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
4933 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
4935 if ( this.icon
!== icon
) {
4937 if ( this.icon
!== null ) {
4938 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
4940 if ( icon
!== null ) {
4941 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
4947 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
4948 this.updateThemeClasses();
4954 * Set the icon title. Use `null` to remove the title.
4956 * @param {string|Function|null} iconTitle A text string used as the icon title,
4957 * a function that returns title text, or `null` for no title.
4960 OO
.ui
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
4961 iconTitle
= typeof iconTitle
=== 'function' ||
4962 ( typeof iconTitle
=== 'string' && iconTitle
.length
) ?
4963 OO
.ui
.resolveMsg( iconTitle
) : null;
4965 if ( this.iconTitle
!== iconTitle
) {
4966 this.iconTitle
= iconTitle
;
4968 if ( this.iconTitle
!== null ) {
4969 this.$icon
.attr( 'title', iconTitle
);
4971 this.$icon
.removeAttr( 'title' );
4980 * Get the symbolic name of the icon.
4982 * @return {string} Icon name
4984 OO
.ui
.IconElement
.prototype.getIcon = function () {
4989 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
4991 * @return {string} Icon title text
4993 OO
.ui
.IconElement
.prototype.getIconTitle = function () {
4994 return this.iconTitle
;
4998 * IndicatorElement is often mixed into other classes to generate an indicator.
4999 * Indicators are small graphics that are generally used in two ways:
5001 * - To draw attention to the status of an item. For example, an indicator might be
5002 * used to show that an item in a list has errors that need to be resolved.
5003 * - To clarify the function of a control that acts in an exceptional way (a button
5004 * that opens a menu instead of performing an action directly, for example).
5006 * For a list of indicators included in the library, please see the
5007 * [OOjs UI documentation on MediaWiki] [1].
5009 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5015 * @param {Object} [config] Configuration options
5016 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
5017 * configuration is omitted, the indicator element will use a generated `<span>`.
5018 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5019 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
5021 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5022 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
5023 * or a function that returns title text. The indicator title is displayed when users move
5024 * the mouse over the indicator.
5026 OO
.ui
.IndicatorElement
= function OoUiIndicatorElement( config
) {
5027 // Configuration initialization
5028 config
= config
|| {};
5031 this.$indicator
= null;
5032 this.indicator
= null;
5033 this.indicatorTitle
= null;
5036 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
5037 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
5038 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
5043 OO
.initClass( OO
.ui
.IndicatorElement
);
5045 /* Static Properties */
5048 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5049 * The static property will be overridden if the #indicator configuration is used.
5053 * @property {string|null}
5055 OO
.ui
.IndicatorElement
.static.indicator
= null;
5058 * A text string used as the indicator title, a function that returns title text, or `null`
5059 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
5063 * @property {string|Function|null}
5065 OO
.ui
.IndicatorElement
.static.indicatorTitle
= null;
5070 * Set the indicator element.
5072 * If an element is already set, it will be cleaned up before setting up the new element.
5074 * @param {jQuery} $indicator Element to use as indicator
5076 OO
.ui
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
5077 if ( this.$indicator
) {
5079 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
5080 .removeAttr( 'title' );
5083 this.$indicator
= $indicator
5084 .addClass( 'oo-ui-indicatorElement-indicator' )
5085 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
5086 if ( this.indicatorTitle
!== null ) {
5087 this.$indicator
.attr( 'title', this.indicatorTitle
);
5092 * Set indicator name.
5094 * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
5097 OO
.ui
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
5098 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
5100 if ( this.indicator
!== indicator
) {
5101 if ( this.$indicator
) {
5102 if ( this.indicator
!== null ) {
5103 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
5105 if ( indicator
!== null ) {
5106 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
5109 this.indicator
= indicator
;
5112 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
5113 this.updateThemeClasses();
5119 * Set indicator title.
5121 * @param {string|Function|null} indicator Indicator title text, a function that returns text or
5122 * null for no indicator title
5125 OO
.ui
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
5126 indicatorTitle
= typeof indicatorTitle
=== 'function' ||
5127 ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ?
5128 OO
.ui
.resolveMsg( indicatorTitle
) : null;
5130 if ( this.indicatorTitle
!== indicatorTitle
) {
5131 this.indicatorTitle
= indicatorTitle
;
5132 if ( this.$indicator
) {
5133 if ( this.indicatorTitle
!== null ) {
5134 this.$indicator
.attr( 'title', indicatorTitle
);
5136 this.$indicator
.removeAttr( 'title' );
5145 * Get indicator name.
5147 * @return {string} Symbolic name of indicator
5149 OO
.ui
.IndicatorElement
.prototype.getIndicator = function () {
5150 return this.indicator
;
5154 * Get indicator title.
5156 * @return {string} Indicator title text
5158 OO
.ui
.IndicatorElement
.prototype.getIndicatorTitle = function () {
5159 return this.indicatorTitle
;
5163 * LabelElement is often mixed into other classes to generate a label, which
5164 * helps identify the function of an interface element.
5165 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5167 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5173 * @param {Object} [config] Configuration options
5174 * @cfg {jQuery} [$label] The label element created by the class. If this
5175 * configuration is omitted, the label element will use a generated `<span>`.
5176 * @cfg {jQuery|string|Function} [label] The label text. The label can be specified as a plaintext string,
5177 * a jQuery selection of elements, or a function that will produce a string in the future. See the
5178 * [OOjs UI documentation on MediaWiki] [2] for examples.
5179 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5180 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5181 * The label will be truncated to fit if necessary.
5183 OO
.ui
.LabelElement
= function OoUiLabelElement( config
) {
5184 // Configuration initialization
5185 config
= config
|| {};
5190 this.autoFitLabel
= config
.autoFitLabel
=== undefined || !!config
.autoFitLabel
;
5193 this.setLabel( config
.label
|| this.constructor.static.label
);
5194 this.setLabelElement( config
.$label
|| $( '<span>' ) );
5199 OO
.initClass( OO
.ui
.LabelElement
);
5204 * @event labelChange
5205 * @param {string} value
5208 /* Static Properties */
5211 * The label text. The label can be specified as a plaintext string, a function that will
5212 * produce a string in the future, or `null` for no label. The static value will
5213 * be overridden if a label is specified with the #label config option.
5217 * @property {string|Function|null}
5219 OO
.ui
.LabelElement
.static.label
= null;
5224 * Set the label element.
5226 * If an element is already set, it will be cleaned up before setting up the new element.
5228 * @param {jQuery} $label Element to use as label
5230 OO
.ui
.LabelElement
.prototype.setLabelElement = function ( $label
) {
5231 if ( this.$label
) {
5232 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
5235 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
5236 this.setLabelContent( this.label
);
5242 * An empty string will result in the label being hidden. A string containing only whitespace will
5243 * be converted to a single ` `.
5245 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
5246 * text; or null for no label
5249 OO
.ui
.LabelElement
.prototype.setLabel = function ( label
) {
5250 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
5251 label
= ( ( typeof label
=== 'string' && label
.length
) || label
instanceof jQuery
|| label
instanceof OO
.ui
.HtmlSnippet
) ? label
: null;
5253 this.$element
.toggleClass( 'oo-ui-labelElement', !!label
);
5255 if ( this.label
!== label
) {
5256 if ( this.$label
) {
5257 this.setLabelContent( label
);
5260 this.emit( 'labelChange' );
5269 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
5270 * text; or null for no label
5272 OO
.ui
.LabelElement
.prototype.getLabel = function () {
5281 OO
.ui
.LabelElement
.prototype.fitLabel = function () {
5282 if ( this.$label
&& this.$label
.autoEllipsis
&& this.autoFitLabel
) {
5283 this.$label
.autoEllipsis( { hasSpan
: false, tooltip
: true } );
5290 * Set the content of the label.
5292 * Do not call this method until after the label element has been set by #setLabelElement.
5295 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
5296 * text; or null for no label
5298 OO
.ui
.LabelElement
.prototype.setLabelContent = function ( label
) {
5299 if ( typeof label
=== 'string' ) {
5300 if ( label
.match( /^\s*$/ ) ) {
5301 // Convert whitespace only string to a single non-breaking space
5302 this.$label
.html( ' ' );
5304 this.$label
.text( label
);
5306 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
5307 this.$label
.html( label
.toString() );
5308 } else if ( label
instanceof jQuery
) {
5309 this.$label
.empty().append( label
);
5311 this.$label
.empty();
5316 * Mixin that adds a menu showing suggested values for a OO.ui.TextInputWidget.
5318 * Subclasses that set the value of #lookupInput from #onLookupMenuItemChoose should
5319 * be aware that this will cause new suggestions to be looked up for the new value. If this is
5320 * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
5326 * @param {Object} [config] Configuration options
5327 * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
5328 * @cfg {jQuery} [$container=this.$element] Element to render menu under
5330 OO
.ui
.LookupElement
= function OoUiLookupElement( config
) {
5331 // Configuration initialization
5332 config
= config
|| {};
5335 this.$overlay
= config
.$overlay
|| this.$element
;
5336 this.lookupMenu
= new OO
.ui
.TextInputMenuSelectWidget( this, {
5339 $container
: config
.$container
5341 this.lookupCache
= {};
5342 this.lookupQuery
= null;
5343 this.lookupRequest
= null;
5344 this.lookupsDisabled
= false;
5345 this.lookupInputFocused
= false;
5349 focus
: this.onLookupInputFocus
.bind( this ),
5350 blur
: this.onLookupInputBlur
.bind( this ),
5351 mousedown
: this.onLookupInputMouseDown
.bind( this )
5353 this.connect( this, { change
: 'onLookupInputChange' } );
5354 this.lookupMenu
.connect( this, {
5355 toggle
: 'onLookupMenuToggle',
5356 choose
: 'onLookupMenuItemChoose'
5360 this.$element
.addClass( 'oo-ui-lookupElement' );
5361 this.lookupMenu
.$element
.addClass( 'oo-ui-lookupElement-menu' );
5362 this.$overlay
.append( this.lookupMenu
.$element
);
5368 * Handle input focus event.
5370 * @param {jQuery.Event} e Input focus event
5372 OO
.ui
.LookupElement
.prototype.onLookupInputFocus = function () {
5373 this.lookupInputFocused
= true;
5374 this.populateLookupMenu();
5378 * Handle input blur event.
5380 * @param {jQuery.Event} e Input blur event
5382 OO
.ui
.LookupElement
.prototype.onLookupInputBlur = function () {
5383 this.closeLookupMenu();
5384 this.lookupInputFocused
= false;
5388 * Handle input mouse down event.
5390 * @param {jQuery.Event} e Input mouse down event
5392 OO
.ui
.LookupElement
.prototype.onLookupInputMouseDown = function () {
5393 // Only open the menu if the input was already focused.
5394 // This way we allow the user to open the menu again after closing it with Esc
5395 // by clicking in the input. Opening (and populating) the menu when initially
5396 // clicking into the input is handled by the focus handler.
5397 if ( this.lookupInputFocused
&& !this.lookupMenu
.isVisible() ) {
5398 this.populateLookupMenu();
5403 * Handle input change event.
5405 * @param {string} value New input value
5407 OO
.ui
.LookupElement
.prototype.onLookupInputChange = function () {
5408 if ( this.lookupInputFocused
) {
5409 this.populateLookupMenu();
5414 * Handle the lookup menu being shown/hidden.
5416 * @param {boolean} visible Whether the lookup menu is now visible.
5418 OO
.ui
.LookupElement
.prototype.onLookupMenuToggle = function ( visible
) {
5420 // When the menu is hidden, abort any active request and clear the menu.
5421 // This has to be done here in addition to closeLookupMenu(), because
5422 // MenuSelectWidget will close itself when the user presses Esc.
5423 this.abortLookupRequest();
5424 this.lookupMenu
.clearItems();
5429 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5431 * @param {OO.ui.MenuOptionWidget|null} item Selected item
5433 OO
.ui
.LookupElement
.prototype.onLookupMenuItemChoose = function ( item
) {
5435 this.setValue( item
.getData() );
5442 * @return {OO.ui.TextInputMenuSelectWidget}
5444 OO
.ui
.LookupElement
.prototype.getLookupMenu = function () {
5445 return this.lookupMenu
;
5449 * Disable or re-enable lookups.
5451 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5453 * @param {boolean} disabled Disable lookups
5455 OO
.ui
.LookupElement
.prototype.setLookupsDisabled = function ( disabled
) {
5456 this.lookupsDisabled
= !!disabled
;
5460 * Open the menu. If there are no entries in the menu, this does nothing.
5464 OO
.ui
.LookupElement
.prototype.openLookupMenu = function () {
5465 if ( !this.lookupMenu
.isEmpty() ) {
5466 this.lookupMenu
.toggle( true );
5472 * Close the menu, empty it, and abort any pending request.
5476 OO
.ui
.LookupElement
.prototype.closeLookupMenu = function () {
5477 this.lookupMenu
.toggle( false );
5478 this.abortLookupRequest();
5479 this.lookupMenu
.clearItems();
5484 * Request menu items based on the input's current value, and when they arrive,
5485 * populate the menu with these items and show the menu.
5487 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5491 OO
.ui
.LookupElement
.prototype.populateLookupMenu = function () {
5493 value
= this.getValue();
5495 if ( this.lookupsDisabled
) {
5499 // If the input is empty, clear the menu
5500 if ( value
=== '' ) {
5501 this.closeLookupMenu();
5502 // Skip population if there is already a request pending for the current value
5503 } else if ( value
!== this.lookupQuery
) {
5504 this.getLookupMenuItems()
5505 .done( function ( items
) {
5506 widget
.lookupMenu
.clearItems();
5507 if ( items
.length
) {
5511 widget
.initializeLookupMenuSelection();
5513 widget
.lookupMenu
.toggle( false );
5516 .fail( function () {
5517 widget
.lookupMenu
.clearItems();
5525 * Select and highlight the first selectable item in the menu.
5529 OO
.ui
.LookupElement
.prototype.initializeLookupMenuSelection = function () {
5530 if ( !this.lookupMenu
.getSelectedItem() ) {
5531 this.lookupMenu
.selectItem( this.lookupMenu
.getFirstSelectableItem() );
5533 this.lookupMenu
.highlightItem( this.lookupMenu
.getSelectedItem() );
5537 * Get lookup menu items for the current query.
5539 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5540 * the done event. If the request was aborted to make way for a subsequent request, this promise
5541 * will not be rejected: it will remain pending forever.
5543 OO
.ui
.LookupElement
.prototype.getLookupMenuItems = function () {
5545 value
= this.getValue(),
5546 deferred
= $.Deferred(),
5549 this.abortLookupRequest();
5550 if ( Object
.prototype.hasOwnProperty
.call( this.lookupCache
, value
) ) {
5551 deferred
.resolve( this.getLookupMenuOptionsFromData( this.lookupCache
[ value
] ) );
5554 this.lookupQuery
= value
;
5555 ourRequest
= this.lookupRequest
= this.getLookupRequest();
5557 .always( function () {
5558 // We need to pop pending even if this is an old request, otherwise
5559 // the widget will remain pending forever.
5560 // TODO: this assumes that an aborted request will fail or succeed soon after
5561 // being aborted, or at least eventually. It would be nice if we could popPending()
5562 // at abort time, but only if we knew that we hadn't already called popPending()
5563 // for that request.
5564 widget
.popPending();
5566 .done( function ( data
) {
5567 // If this is an old request (and aborting it somehow caused it to still succeed),
5568 // ignore its success completely
5569 if ( ourRequest
=== widget
.lookupRequest
) {
5570 widget
.lookupQuery
= null;
5571 widget
.lookupRequest
= null;
5572 widget
.lookupCache
[ value
] = widget
.getLookupCacheDataFromResponse( data
);
5573 deferred
.resolve( widget
.getLookupMenuOptionsFromData( widget
.lookupCache
[ value
] ) );
5576 .fail( function () {
5577 // If this is an old request (or a request failing because it's being aborted),
5578 // ignore its failure completely
5579 if ( ourRequest
=== widget
.lookupRequest
) {
5580 widget
.lookupQuery
= null;
5581 widget
.lookupRequest
= null;
5586 return deferred
.promise();
5590 * Abort the currently pending lookup request, if any.
5592 OO
.ui
.LookupElement
.prototype.abortLookupRequest = function () {
5593 var oldRequest
= this.lookupRequest
;
5595 // First unset this.lookupRequest to the fail handler will notice
5596 // that the request is no longer current
5597 this.lookupRequest
= null;
5598 this.lookupQuery
= null;
5604 * Get a new request object of the current lookup query value.
5607 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5609 OO
.ui
.LookupElement
.prototype.getLookupRequest = function () {
5610 // Stub, implemented in subclass
5615 * Pre-process data returned by the request from #getLookupRequest.
5617 * The return value of this function will be cached, and any further queries for the given value
5618 * will use the cache rather than doing API requests.
5621 * @param {Mixed} data Response from server
5622 * @return {Mixed} Cached result data
5624 OO
.ui
.LookupElement
.prototype.getLookupCacheDataFromResponse = function () {
5625 // Stub, implemented in subclass
5630 * Get a list of menu option widgets from the (possibly cached) data returned by
5631 * #getLookupCacheDataFromResponse.
5634 * @param {Mixed} data Cached result data, usually an array
5635 * @return {OO.ui.MenuOptionWidget[]} Menu items
5637 OO
.ui
.LookupElement
.prototype.getLookupMenuOptionsFromData = function () {
5638 // Stub, implemented in subclass
5643 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5644 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5645 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5646 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5652 * @param {Object} [config] Configuration options
5653 * @cfg {Object} [popup] Configuration to pass to popup
5654 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5656 OO
.ui
.PopupElement
= function OoUiPopupElement( config
) {
5657 // Configuration initialization
5658 config
= config
|| {};
5661 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5662 { autoClose
: true },
5664 { $autoCloseIgnore
: this.$element
}
5673 * @return {OO.ui.PopupWidget} Popup widget
5675 OO
.ui
.PopupElement
.prototype.getPopup = function () {
5680 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
5681 * additional functionality to an element created by another class. The class provides
5682 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
5683 * which are used to customize the look and feel of a widget to better describe its
5684 * importance and functionality.
5686 * The library currently contains the following styling flags for general use:
5688 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
5689 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
5690 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
5692 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
5693 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
5695 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5701 * @param {Object} [config] Configuration options
5702 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
5703 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
5704 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5705 * @cfg {jQuery} [$flagged] Flagged node, assigned to $flagged, omit to use $element
5707 OO
.ui
.FlaggedElement
= function OoUiFlaggedElement( config
) {
5708 // Configuration initialization
5709 config
= config
|| {};
5713 this.$flagged
= null;
5716 this.setFlags( config
.flags
);
5717 this.setFlaggedElement( config
.$flagged
|| this.$element
);
5724 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
5725 * parameter contains the name of each modified flag and indicates whether it was
5728 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
5729 * that the flag was added, `false` that the flag was removed.
5735 * Set the flagged element.
5737 * If an element is already set, it will be cleaned up before setting up the new element.
5739 * @param {jQuery} $flagged Element to add flags to
5741 OO
.ui
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
5742 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
5743 return 'oo-ui-flaggedElement-' + flag
;
5746 if ( this.$flagged
) {
5747 this.$flagged
.removeClass( classNames
);
5750 this.$flagged
= $flagged
.addClass( classNames
);
5754 * Check if a flag is set.
5756 * @param {string} flag Name of flag
5757 * @return {boolean} Has flag
5759 OO
.ui
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
5760 return flag
in this.flags
;
5764 * Get the names of all flags set.
5766 * @return {string[]} Flag names
5768 OO
.ui
.FlaggedElement
.prototype.getFlags = function () {
5769 return Object
.keys( this.flags
);
5778 OO
.ui
.FlaggedElement
.prototype.clearFlags = function () {
5779 var flag
, className
,
5782 classPrefix
= 'oo-ui-flaggedElement-';
5784 for ( flag
in this.flags
) {
5785 className
= classPrefix
+ flag
;
5786 changes
[ flag
] = false;
5787 delete this.flags
[ flag
];
5788 remove
.push( className
);
5791 if ( this.$flagged
) {
5792 this.$flagged
.removeClass( remove
.join( ' ' ) );
5795 this.updateThemeClasses();
5796 this.emit( 'flag', changes
);
5802 * Add one or more flags.
5804 * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
5805 * keyed by flag name containing boolean set/remove instructions.
5809 OO
.ui
.FlaggedElement
.prototype.setFlags = function ( flags
) {
5810 var i
, len
, flag
, className
,
5814 classPrefix
= 'oo-ui-flaggedElement-';
5816 if ( typeof flags
=== 'string' ) {
5817 className
= classPrefix
+ flags
;
5819 if ( !this.flags
[ flags
] ) {
5820 this.flags
[ flags
] = true;
5821 add
.push( className
);
5823 } else if ( Array
.isArray( flags
) ) {
5824 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
5826 className
= classPrefix
+ flag
;
5828 if ( !this.flags
[ flag
] ) {
5829 changes
[ flag
] = true;
5830 this.flags
[ flag
] = true;
5831 add
.push( className
);
5834 } else if ( OO
.isPlainObject( flags
) ) {
5835 for ( flag
in flags
) {
5836 className
= classPrefix
+ flag
;
5837 if ( flags
[ flag
] ) {
5839 if ( !this.flags
[ flag
] ) {
5840 changes
[ flag
] = true;
5841 this.flags
[ flag
] = true;
5842 add
.push( className
);
5846 if ( this.flags
[ flag
] ) {
5847 changes
[ flag
] = false;
5848 delete this.flags
[ flag
];
5849 remove
.push( className
);
5855 if ( this.$flagged
) {
5857 .addClass( add
.join( ' ' ) )
5858 .removeClass( remove
.join( ' ' ) );
5861 this.updateThemeClasses();
5862 this.emit( 'flag', changes
);
5868 * TitledElement is mixed into other classes to provide a `title` attribute.
5869 * Titles are rendered by the browser and are made visible when the user moves
5870 * the mouse over the element. Titles are not visible on touch devices.
5873 * // TitledElement provides a 'title' attribute to the
5874 * // ButtonWidget class
5875 * var button = new OO.ui.ButtonWidget( {
5876 * label : 'Button with Title',
5877 * title : 'I am a button'
5879 * $( 'body' ).append( button.$element );
5885 * @param {Object} [config] Configuration options
5886 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
5887 * If this config is omitted, the title functionality is applied to $element, the
5888 * element created by the class.
5889 * @cfg {string|Function} [title] The title text or a function that returns text. If
5890 * this config is omitted, the value of the static `title` property is used.
5892 OO
.ui
.TitledElement
= function OoUiTitledElement( config
) {
5893 // Configuration initialization
5894 config
= config
|| {};
5897 this.$titled
= null;
5901 this.setTitle( config
.title
|| this.constructor.static.title
);
5902 this.setTitledElement( config
.$titled
|| this.$element
);
5907 OO
.initClass( OO
.ui
.TitledElement
);
5909 /* Static Properties */
5912 * The title text, a function that returns text, or `null` for no title. The value of the static property
5913 * is overridden if the #title config option is used.
5917 * @property {string|Function|null}
5919 OO
.ui
.TitledElement
.static.title
= null;
5924 * Set the titled element.
5926 * If an element is already set, it will be cleaned up before setting up the new element.
5928 * @param {jQuery} $titled Element to set title on
5930 OO
.ui
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
5931 if ( this.$titled
) {
5932 this.$titled
.removeAttr( 'title' );
5935 this.$titled
= $titled
;
5937 this.$titled
.attr( 'title', this.title
);
5944 * @param {string|Function|null} title Title text, a function that returns text or null for no title
5947 OO
.ui
.TitledElement
.prototype.setTitle = function ( title
) {
5948 title
= typeof title
=== 'string' ? OO
.ui
.resolveMsg( title
) : null;
5950 if ( this.title
!== title
) {
5951 if ( this.$titled
) {
5952 if ( title
!== null ) {
5953 this.$titled
.attr( 'title', title
);
5955 this.$titled
.removeAttr( 'title' );
5967 * @return {string} Title string
5969 OO
.ui
.TitledElement
.prototype.getTitle = function () {
5974 * Element that can be automatically clipped to visible boundaries.
5976 * Whenever the element's natural height changes, you have to call
5977 * #clip to make sure it's still clipping correctly.
5983 * @param {Object} [config] Configuration options
5984 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
5986 OO
.ui
.ClippableElement
= function OoUiClippableElement( config
) {
5987 // Configuration initialization
5988 config
= config
|| {};
5991 this.$clippable
= null;
5992 this.clipping
= false;
5993 this.clippedHorizontally
= false;
5994 this.clippedVertically
= false;
5995 this.$clippableContainer
= null;
5996 this.$clippableScroller
= null;
5997 this.$clippableWindow
= null;
5998 this.idealWidth
= null;
5999 this.idealHeight
= null;
6000 this.onClippableContainerScrollHandler
= this.clip
.bind( this );
6001 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
6004 this.setClippableElement( config
.$clippable
|| this.$element
);
6010 * Set clippable element.
6012 * If an element is already set, it will be cleaned up before setting up the new element.
6014 * @param {jQuery} $clippable Element to make clippable
6016 OO
.ui
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
6017 if ( this.$clippable
) {
6018 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
6019 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
6020 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
6023 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
6030 * Do not turn clipping on until after the element is attached to the DOM and visible.
6032 * @param {boolean} [clipping] Enable clipping, omit to toggle
6035 OO
.ui
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
6036 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
6038 if ( this.clipping
!== clipping
) {
6039 this.clipping
= clipping
;
6041 this.$clippableContainer
= $( this.getClosestScrollableElementContainer() );
6042 // If the clippable container is the root, we have to listen to scroll events and check
6043 // jQuery.scrollTop on the window because of browser inconsistencies
6044 this.$clippableScroller
= this.$clippableContainer
.is( 'html, body' ) ?
6045 $( OO
.ui
.Element
.static.getWindow( this.$clippableContainer
) ) :
6046 this.$clippableContainer
;
6047 this.$clippableScroller
.on( 'scroll', this.onClippableContainerScrollHandler
);
6048 this.$clippableWindow
= $( this.getElementWindow() )
6049 .on( 'resize', this.onClippableWindowResizeHandler
);
6050 // Initial clip after visible
6053 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
6054 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
6056 this.$clippableContainer
= null;
6057 this.$clippableScroller
.off( 'scroll', this.onClippableContainerScrollHandler
);
6058 this.$clippableScroller
= null;
6059 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
6060 this.$clippableWindow
= null;
6068 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
6070 * @return {boolean} Element will be clipped to the visible area
6072 OO
.ui
.ClippableElement
.prototype.isClipping = function () {
6073 return this.clipping
;
6077 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
6079 * @return {boolean} Part of the element is being clipped
6081 OO
.ui
.ClippableElement
.prototype.isClipped = function () {
6082 return this.clippedHorizontally
|| this.clippedVertically
;
6086 * Check if the right of the element is being clipped by the nearest scrollable container.
6088 * @return {boolean} Part of the element is being clipped
6090 OO
.ui
.ClippableElement
.prototype.isClippedHorizontally = function () {
6091 return this.clippedHorizontally
;
6095 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6097 * @return {boolean} Part of the element is being clipped
6099 OO
.ui
.ClippableElement
.prototype.isClippedVertically = function () {
6100 return this.clippedVertically
;
6104 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6106 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6107 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6109 OO
.ui
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
6110 this.idealWidth
= width
;
6111 this.idealHeight
= height
;
6113 if ( !this.clipping
) {
6114 // Update dimensions
6115 this.$clippable
.css( { width
: width
, height
: height
} );
6117 // While clipping, idealWidth and idealHeight are not considered
6121 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6122 * the element's natural height changes.
6124 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6125 * overlapped by, the visible area of the nearest scrollable container.
6129 OO
.ui
.ClippableElement
.prototype.clip = function () {
6130 if ( !this.clipping
) {
6131 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
6135 var buffer
= 7, // Chosen by fair dice roll
6136 cOffset
= this.$clippable
.offset(),
6137 $container
= this.$clippableContainer
.is( 'html, body' ) ?
6138 this.$clippableWindow
: this.$clippableContainer
,
6139 ccOffset
= $container
.offset() || { top
: 0, left
: 0 },
6140 ccHeight
= $container
.innerHeight() - buffer
,
6141 ccWidth
= $container
.innerWidth() - buffer
,
6142 cHeight
= this.$clippable
.outerHeight() + buffer
,
6143 cWidth
= this.$clippable
.outerWidth() + buffer
,
6144 scrollTop
= this.$clippableScroller
.scrollTop(),
6145 scrollLeft
= this.$clippableScroller
.scrollLeft(),
6146 desiredWidth
= cOffset
.left
< 0 ?
6147 cWidth
+ cOffset
.left
:
6148 ( ccOffset
.left
+ scrollLeft
+ ccWidth
) - cOffset
.left
,
6149 desiredHeight
= cOffset
.top
< 0 ?
6150 cHeight
+ cOffset
.top
:
6151 ( ccOffset
.top
+ scrollTop
+ ccHeight
) - cOffset
.top
,
6152 naturalWidth
= this.$clippable
.prop( 'scrollWidth' ),
6153 naturalHeight
= this.$clippable
.prop( 'scrollHeight' ),
6154 clipWidth
= desiredWidth
< naturalWidth
,
6155 clipHeight
= desiredHeight
< naturalHeight
;
6158 this.$clippable
.css( { overflowX
: 'scroll', width
: desiredWidth
} );
6160 this.$clippable
.css( { width
: this.idealWidth
|| '', overflowX
: '' } );
6163 this.$clippable
.css( { overflowY
: 'scroll', height
: desiredHeight
} );
6165 this.$clippable
.css( { height
: this.idealHeight
|| '', overflowY
: '' } );
6168 // If we stopped clipping in at least one of the dimensions
6169 if ( !clipWidth
|| !clipHeight
) {
6170 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
6173 this.clippedHorizontally
= clipWidth
;
6174 this.clippedVertically
= clipHeight
;
6180 * Generic toolbar tool.
6184 * @extends OO.ui.Widget
6185 * @mixins OO.ui.IconElement
6186 * @mixins OO.ui.FlaggedElement
6189 * @param {OO.ui.ToolGroup} toolGroup
6190 * @param {Object} [config] Configuration options
6191 * @cfg {string|Function} [title] Title text or a function that returns text
6193 OO
.ui
.Tool
= function OoUiTool( toolGroup
, config
) {
6194 // Allow passing positional parameters inside the config object
6195 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
6197 toolGroup
= config
.toolGroup
;
6200 // Configuration initialization
6201 config
= config
|| {};
6203 // Parent constructor
6204 OO
.ui
.Tool
.super.call( this, config
);
6206 // Mixin constructors
6207 OO
.ui
.IconElement
.call( this, config
);
6208 OO
.ui
.FlaggedElement
.call( this, config
);
6211 this.toolGroup
= toolGroup
;
6212 this.toolbar
= this.toolGroup
.getToolbar();
6213 this.active
= false;
6214 this.$title
= $( '<span>' );
6215 this.$accel
= $( '<span>' );
6216 this.$link
= $( '<a>' );
6220 this.toolbar
.connect( this, { updateState
: 'onUpdateState' } );
6223 this.$title
.addClass( 'oo-ui-tool-title' );
6225 .addClass( 'oo-ui-tool-accel' )
6227 // This may need to be changed if the key names are ever localized,
6228 // but for now they are essentially written in English
6233 .addClass( 'oo-ui-tool-link' )
6234 .append( this.$icon
, this.$title
, this.$accel
)
6235 .prop( 'tabIndex', 0 )
6236 .attr( 'role', 'button' );
6238 .data( 'oo-ui-tool', this )
6240 'oo-ui-tool ' + 'oo-ui-tool-name-' +
6241 this.constructor.static.name
.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
6243 .append( this.$link
);
6244 this.setTitle( config
.title
|| this.constructor.static.title
);
6249 OO
.inheritClass( OO
.ui
.Tool
, OO
.ui
.Widget
);
6250 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.IconElement
);
6251 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.FlaggedElement
);
6259 /* Static Properties */
6265 OO
.ui
.Tool
.static.tagName
= 'span';
6268 * Symbolic name of tool.
6273 * @property {string}
6275 OO
.ui
.Tool
.static.name
= '';
6283 * @property {string}
6285 OO
.ui
.Tool
.static.group
= '';
6290 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
6291 * is part of a list or menu tool group. If a trigger is associated with an action by the same name
6292 * as the tool, a description of its keyboard shortcut for the appropriate platform will be
6293 * appended to the title if the tool is part of a bar tool group.
6298 * @property {string|Function} Title text or a function that returns text
6300 OO
.ui
.Tool
.static.title
= '';
6303 * Tool can be automatically added to catch-all groups.
6307 * @property {boolean}
6309 OO
.ui
.Tool
.static.autoAddToCatchall
= true;
6312 * Tool can be automatically added to named groups.
6315 * @property {boolean}
6318 OO
.ui
.Tool
.static.autoAddToGroup
= true;
6321 * Check if this tool is compatible with given data.
6325 * @param {Mixed} data Data to check
6326 * @return {boolean} Tool can be used with data
6328 OO
.ui
.Tool
.static.isCompatibleWith = function () {
6335 * Handle the toolbar state being updated.
6337 * This is an abstract method that must be overridden in a concrete subclass.
6341 OO
.ui
.Tool
.prototype.onUpdateState = function () {
6343 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
6348 * Handle the tool being selected.
6350 * This is an abstract method that must be overridden in a concrete subclass.
6354 OO
.ui
.Tool
.prototype.onSelect = function () {
6356 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
6361 * Check if the button is active.
6363 * @return {boolean} Button is active
6365 OO
.ui
.Tool
.prototype.isActive = function () {
6370 * Make the button appear active or inactive.
6372 * @param {boolean} state Make button appear active
6374 OO
.ui
.Tool
.prototype.setActive = function ( state
) {
6375 this.active
= !!state
;
6376 if ( this.active
) {
6377 this.$element
.addClass( 'oo-ui-tool-active' );
6379 this.$element
.removeClass( 'oo-ui-tool-active' );
6384 * Get the tool title.
6386 * @param {string|Function} title Title text or a function that returns text
6389 OO
.ui
.Tool
.prototype.setTitle = function ( title
) {
6390 this.title
= OO
.ui
.resolveMsg( title
);
6396 * Get the tool title.
6398 * @return {string} Title text
6400 OO
.ui
.Tool
.prototype.getTitle = function () {
6405 * Get the tool's symbolic name.
6407 * @return {string} Symbolic name of tool
6409 OO
.ui
.Tool
.prototype.getName = function () {
6410 return this.constructor.static.name
;
6416 OO
.ui
.Tool
.prototype.updateTitle = function () {
6417 var titleTooltips
= this.toolGroup
.constructor.static.titleTooltips
,
6418 accelTooltips
= this.toolGroup
.constructor.static.accelTooltips
,
6419 accel
= this.toolbar
.getToolAccelerator( this.constructor.static.name
),
6422 this.$title
.text( this.title
);
6423 this.$accel
.text( accel
);
6425 if ( titleTooltips
&& typeof this.title
=== 'string' && this.title
.length
) {
6426 tooltipParts
.push( this.title
);
6428 if ( accelTooltips
&& typeof accel
=== 'string' && accel
.length
) {
6429 tooltipParts
.push( accel
);
6431 if ( tooltipParts
.length
) {
6432 this.$link
.attr( 'title', tooltipParts
.join( ' ' ) );
6434 this.$link
.removeAttr( 'title' );
6441 OO
.ui
.Tool
.prototype.destroy = function () {
6442 this.toolbar
.disconnect( this );
6443 this.$element
.remove();
6447 * Collection of tool groups.
6450 * @extends OO.ui.Element
6451 * @mixins OO.EventEmitter
6452 * @mixins OO.ui.GroupElement
6455 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
6456 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
6457 * @param {Object} [config] Configuration options
6458 * @cfg {boolean} [actions] Add an actions section opposite to the tools
6459 * @cfg {boolean} [shadow] Add a shadow below the toolbar
6461 OO
.ui
.Toolbar
= function OoUiToolbar( toolFactory
, toolGroupFactory
, config
) {
6462 // Allow passing positional parameters inside the config object
6463 if ( OO
.isPlainObject( toolFactory
) && config
=== undefined ) {
6464 config
= toolFactory
;
6465 toolFactory
= config
.toolFactory
;
6466 toolGroupFactory
= config
.toolGroupFactory
;
6469 // Configuration initialization
6470 config
= config
|| {};
6472 // Parent constructor
6473 OO
.ui
.Toolbar
.super.call( this, config
);
6475 // Mixin constructors
6476 OO
.EventEmitter
.call( this );
6477 OO
.ui
.GroupElement
.call( this, config
);
6480 this.toolFactory
= toolFactory
;
6481 this.toolGroupFactory
= toolGroupFactory
;
6484 this.$bar
= $( '<div>' );
6485 this.$actions
= $( '<div>' );
6486 this.initialized
= false;
6487 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
6491 .add( this.$bar
).add( this.$group
).add( this.$actions
)
6492 .on( 'mousedown', this.onPointerDown
.bind( this ) );
6495 this.$group
.addClass( 'oo-ui-toolbar-tools' );
6496 if ( config
.actions
) {
6497 this.$bar
.append( this.$actions
.addClass( 'oo-ui-toolbar-actions' ) );
6500 .addClass( 'oo-ui-toolbar-bar' )
6501 .append( this.$group
, '<div style="clear:both"></div>' );
6502 if ( config
.shadow
) {
6503 this.$bar
.append( '<div class="oo-ui-toolbar-shadow"></div>' );
6505 this.$element
.addClass( 'oo-ui-toolbar' ).append( this.$bar
);
6510 OO
.inheritClass( OO
.ui
.Toolbar
, OO
.ui
.Element
);
6511 OO
.mixinClass( OO
.ui
.Toolbar
, OO
.EventEmitter
);
6512 OO
.mixinClass( OO
.ui
.Toolbar
, OO
.ui
.GroupElement
);
6517 * Get the tool factory.
6519 * @return {OO.ui.ToolFactory} Tool factory
6521 OO
.ui
.Toolbar
.prototype.getToolFactory = function () {
6522 return this.toolFactory
;
6526 * Get the tool group factory.
6528 * @return {OO.Factory} Tool group factory
6530 OO
.ui
.Toolbar
.prototype.getToolGroupFactory = function () {
6531 return this.toolGroupFactory
;
6535 * Handles mouse down events.
6537 * @param {jQuery.Event} e Mouse down event
6539 OO
.ui
.Toolbar
.prototype.onPointerDown = function ( e
) {
6540 var $closestWidgetToEvent
= $( e
.target
).closest( '.oo-ui-widget' ),
6541 $closestWidgetToToolbar
= this.$element
.closest( '.oo-ui-widget' );
6542 if ( !$closestWidgetToEvent
.length
|| $closestWidgetToEvent
[ 0 ] === $closestWidgetToToolbar
[ 0 ] ) {
6548 * Handle window resize event.
6551 * @param {jQuery.Event} e Window resize event
6553 OO
.ui
.Toolbar
.prototype.onWindowResize = function () {
6554 this.$element
.toggleClass(
6555 'oo-ui-toolbar-narrow',
6556 this.$bar
.width() <= this.narrowThreshold
6561 * Sets up handles and preloads required information for the toolbar to work.
6562 * This must be called after it is attached to a visible document and before doing anything else.
6564 OO
.ui
.Toolbar
.prototype.initialize = function () {
6565 this.initialized
= true;
6566 this.narrowThreshold
= this.$group
.width() + this.$actions
.width();
6567 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler
);
6568 this.onWindowResize();
6574 * Tools can be specified in the following ways:
6576 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6577 * - All tools in a group: `{ group: 'group-name' }`
6578 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
6580 * @param {Object.<string,Array>} groups List of tool group configurations
6581 * @param {Array|string} [groups.include] Tools to include
6582 * @param {Array|string} [groups.exclude] Tools to exclude
6583 * @param {Array|string} [groups.promote] Tools to promote to the beginning
6584 * @param {Array|string} [groups.demote] Tools to demote to the end
6586 OO
.ui
.Toolbar
.prototype.setup = function ( groups
) {
6587 var i
, len
, type
, group
,
6589 defaultType
= 'bar';
6591 // Cleanup previous groups
6594 // Build out new groups
6595 for ( i
= 0, len
= groups
.length
; i
< len
; i
++ ) {
6596 group
= groups
[ i
];
6597 if ( group
.include
=== '*' ) {
6598 // Apply defaults to catch-all groups
6599 if ( group
.type
=== undefined ) {
6600 group
.type
= 'list';
6602 if ( group
.label
=== undefined ) {
6603 group
.label
= OO
.ui
.msg( 'ooui-toolbar-more' );
6606 // Check type has been registered
6607 type
= this.getToolGroupFactory().lookup( group
.type
) ? group
.type
: defaultType
;
6609 this.getToolGroupFactory().create( type
, this, group
)
6612 this.addItems( items
);
6616 * Remove all tools and groups from the toolbar.
6618 OO
.ui
.Toolbar
.prototype.reset = function () {
6623 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6624 this.items
[ i
].destroy();
6630 * Destroys toolbar, removing event handlers and DOM elements.
6632 * Call this whenever you are done using a toolbar.
6634 OO
.ui
.Toolbar
.prototype.destroy = function () {
6635 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler
);
6637 this.$element
.remove();
6641 * Check if tool has not been used yet.
6643 * @param {string} name Symbolic name of tool
6644 * @return {boolean} Tool is available
6646 OO
.ui
.Toolbar
.prototype.isToolAvailable = function ( name
) {
6647 return !this.tools
[ name
];
6651 * Prevent tool from being used again.
6653 * @param {OO.ui.Tool} tool Tool to reserve
6655 OO
.ui
.Toolbar
.prototype.reserveTool = function ( tool
) {
6656 this.tools
[ tool
.getName() ] = tool
;
6660 * Allow tool to be used again.
6662 * @param {OO.ui.Tool} tool Tool to release
6664 OO
.ui
.Toolbar
.prototype.releaseTool = function ( tool
) {
6665 delete this.tools
[ tool
.getName() ];
6669 * Get accelerator label for tool.
6671 * This is a stub that should be overridden to provide access to accelerator information.
6673 * @param {string} name Symbolic name of tool
6674 * @return {string|undefined} Tool accelerator label if available
6676 OO
.ui
.Toolbar
.prototype.getToolAccelerator = function () {
6681 * Collection of tools.
6683 * Tools can be specified in the following ways:
6685 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6686 * - All tools in a group: `{ group: 'group-name' }`
6687 * - All tools: `'*'`
6691 * @extends OO.ui.Widget
6692 * @mixins OO.ui.GroupElement
6695 * @param {OO.ui.Toolbar} toolbar
6696 * @param {Object} [config] Configuration options
6697 * @cfg {Array|string} [include=[]] List of tools to include
6698 * @cfg {Array|string} [exclude=[]] List of tools to exclude
6699 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
6700 * @cfg {Array|string} [demote=[]] List of tools to demote to the end
6702 OO
.ui
.ToolGroup
= function OoUiToolGroup( toolbar
, config
) {
6703 // Allow passing positional parameters inside the config object
6704 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
6706 toolbar
= config
.toolbar
;
6709 // Configuration initialization
6710 config
= config
|| {};
6712 // Parent constructor
6713 OO
.ui
.ToolGroup
.super.call( this, config
);
6715 // Mixin constructors
6716 OO
.ui
.GroupElement
.call( this, config
);
6719 this.toolbar
= toolbar
;
6721 this.pressed
= null;
6722 this.autoDisabled
= false;
6723 this.include
= config
.include
|| [];
6724 this.exclude
= config
.exclude
|| [];
6725 this.promote
= config
.promote
|| [];
6726 this.demote
= config
.demote
|| [];
6727 this.onCapturedMouseUpHandler
= this.onCapturedMouseUp
.bind( this );
6731 mousedown
: this.onPointerDown
.bind( this ),
6732 mouseup
: this.onPointerUp
.bind( this ),
6733 mouseover
: this.onMouseOver
.bind( this ),
6734 mouseout
: this.onMouseOut
.bind( this )
6736 this.toolbar
.getToolFactory().connect( this, { register
: 'onToolFactoryRegister' } );
6737 this.aggregate( { disable
: 'itemDisable' } );
6738 this.connect( this, { itemDisable
: 'updateDisabled' } );
6741 this.$group
.addClass( 'oo-ui-toolGroup-tools' );
6743 .addClass( 'oo-ui-toolGroup' )
6744 .append( this.$group
);
6750 OO
.inheritClass( OO
.ui
.ToolGroup
, OO
.ui
.Widget
);
6751 OO
.mixinClass( OO
.ui
.ToolGroup
, OO
.ui
.GroupElement
);
6759 /* Static Properties */
6762 * Show labels in tooltips.
6766 * @property {boolean}
6768 OO
.ui
.ToolGroup
.static.titleTooltips
= false;
6771 * Show acceleration labels in tooltips.
6775 * @property {boolean}
6777 OO
.ui
.ToolGroup
.static.accelTooltips
= false;
6780 * Automatically disable the toolgroup when all tools are disabled
6784 * @property {boolean}
6786 OO
.ui
.ToolGroup
.static.autoDisable
= true;
6793 OO
.ui
.ToolGroup
.prototype.isDisabled = function () {
6794 return this.autoDisabled
|| OO
.ui
.ToolGroup
.super.prototype.isDisabled
.apply( this, arguments
);
6800 OO
.ui
.ToolGroup
.prototype.updateDisabled = function () {
6801 var i
, item
, allDisabled
= true;
6803 if ( this.constructor.static.autoDisable
) {
6804 for ( i
= this.items
.length
- 1; i
>= 0; i
-- ) {
6805 item
= this.items
[ i
];
6806 if ( !item
.isDisabled() ) {
6807 allDisabled
= false;
6811 this.autoDisabled
= allDisabled
;
6813 OO
.ui
.ToolGroup
.super.prototype.updateDisabled
.apply( this, arguments
);
6817 * Handle mouse down events.
6819 * @param {jQuery.Event} e Mouse down event
6821 OO
.ui
.ToolGroup
.prototype.onPointerDown = function ( e
) {
6822 if ( !this.isDisabled() && e
.which
=== 1 ) {
6823 this.pressed
= this.getTargetTool( e
);
6824 if ( this.pressed
) {
6825 this.pressed
.setActive( true );
6826 this.getElementDocument().addEventListener(
6827 'mouseup', this.onCapturedMouseUpHandler
, true
6835 * Handle captured mouse up events.
6837 * @param {Event} e Mouse up event
6839 OO
.ui
.ToolGroup
.prototype.onCapturedMouseUp = function ( e
) {
6840 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler
, true );
6841 // onPointerUp may be called a second time, depending on where the mouse is when the button is
6842 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
6843 this.onPointerUp( e
);
6847 * Handle mouse up events.
6849 * @param {jQuery.Event} e Mouse up event
6851 OO
.ui
.ToolGroup
.prototype.onPointerUp = function ( e
) {
6852 var tool
= this.getTargetTool( e
);
6854 if ( !this.isDisabled() && e
.which
=== 1 && this.pressed
&& this.pressed
=== tool
) {
6855 this.pressed
.onSelect();
6858 this.pressed
= null;
6863 * Handle mouse over events.
6865 * @param {jQuery.Event} e Mouse over event
6867 OO
.ui
.ToolGroup
.prototype.onMouseOver = function ( e
) {
6868 var tool
= this.getTargetTool( e
);
6870 if ( this.pressed
&& this.pressed
=== tool
) {
6871 this.pressed
.setActive( true );
6876 * Handle mouse out events.
6878 * @param {jQuery.Event} e Mouse out event
6880 OO
.ui
.ToolGroup
.prototype.onMouseOut = function ( e
) {
6881 var tool
= this.getTargetTool( e
);
6883 if ( this.pressed
&& this.pressed
=== tool
) {
6884 this.pressed
.setActive( false );
6889 * Get the closest tool to a jQuery.Event.
6891 * Only tool links are considered, which prevents other elements in the tool such as popups from
6892 * triggering tool group interactions.
6895 * @param {jQuery.Event} e
6896 * @return {OO.ui.Tool|null} Tool, `null` if none was found
6898 OO
.ui
.ToolGroup
.prototype.getTargetTool = function ( e
) {
6900 $item
= $( e
.target
).closest( '.oo-ui-tool-link' );
6902 if ( $item
.length
) {
6903 tool
= $item
.parent().data( 'oo-ui-tool' );
6906 return tool
&& !tool
.isDisabled() ? tool
: null;
6910 * Handle tool registry register events.
6912 * If a tool is registered after the group is created, we must repopulate the list to account for:
6914 * - a tool being added that may be included
6915 * - a tool already included being overridden
6917 * @param {string} name Symbolic name of tool
6919 OO
.ui
.ToolGroup
.prototype.onToolFactoryRegister = function () {
6924 * Get the toolbar this group is in.
6926 * @return {OO.ui.Toolbar} Toolbar of group
6928 OO
.ui
.ToolGroup
.prototype.getToolbar = function () {
6929 return this.toolbar
;
6933 * Add and remove tools based on configuration.
6935 OO
.ui
.ToolGroup
.prototype.populate = function () {
6936 var i
, len
, name
, tool
,
6937 toolFactory
= this.toolbar
.getToolFactory(),
6941 list
= this.toolbar
.getToolFactory().getTools(
6942 this.include
, this.exclude
, this.promote
, this.demote
6945 // Build a list of needed tools
6946 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
6950 toolFactory
.lookup( name
) &&
6951 // Tool is available or is already in this group
6952 ( this.toolbar
.isToolAvailable( name
) || this.tools
[ name
] )
6954 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
6955 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
6956 this.toolbar
.tools
[ name
] = true;
6957 tool
= this.tools
[ name
];
6959 // Auto-initialize tools on first use
6960 this.tools
[ name
] = tool
= toolFactory
.create( name
, this );
6963 this.toolbar
.reserveTool( tool
);
6965 names
[ name
] = true;
6968 // Remove tools that are no longer needed
6969 for ( name
in this.tools
) {
6970 if ( !names
[ name
] ) {
6971 this.tools
[ name
].destroy();
6972 this.toolbar
.releaseTool( this.tools
[ name
] );
6973 remove
.push( this.tools
[ name
] );
6974 delete this.tools
[ name
];
6977 if ( remove
.length
) {
6978 this.removeItems( remove
);
6980 // Update emptiness state
6982 this.$element
.removeClass( 'oo-ui-toolGroup-empty' );
6984 this.$element
.addClass( 'oo-ui-toolGroup-empty' );
6986 // Re-add tools (moving existing ones to new locations)
6987 this.addItems( add
);
6988 // Disabled state may depend on items
6989 this.updateDisabled();
6993 * Destroy tool group.
6995 OO
.ui
.ToolGroup
.prototype.destroy = function () {
6999 this.toolbar
.getToolFactory().disconnect( this );
7000 for ( name
in this.tools
) {
7001 this.toolbar
.releaseTool( this.tools
[ name
] );
7002 this.tools
[ name
].disconnect( this ).destroy();
7003 delete this.tools
[ name
];
7005 this.$element
.remove();
7009 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
7010 * consists of a header that contains the dialog title, a body with the message, and a footer that
7011 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
7012 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
7014 * There are two basic types of message dialogs, confirmation and alert:
7016 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
7017 * more details about the consequences.
7018 * - **alert**: the dialog title describes which event occurred and the message provides more information
7019 * about why the event occurred.
7021 * The MessageDialog class specifies two actions: ‘accept’, the primary
7022 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
7023 * passing along the selected action.
7025 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
7028 * // Example: Creating and opening a message dialog window.
7029 * var messageDialog = new OO.ui.MessageDialog();
7031 * // Create and append a window manager.
7032 * var windowManager = new OO.ui.WindowManager();
7033 * $( 'body' ).append( windowManager.$element );
7034 * windowManager.addWindows( [ messageDialog ] );
7035 * // Open the window.
7036 * windowManager.openWindow( messageDialog, {
7037 * title: 'Basic message dialog',
7038 * message: 'This is the message'
7041 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
7044 * @extends OO.ui.Dialog
7047 * @param {Object} [config] Configuration options
7049 OO
.ui
.MessageDialog
= function OoUiMessageDialog( config
) {
7050 // Parent constructor
7051 OO
.ui
.MessageDialog
.super.call( this, config
);
7054 this.verticalActionLayout
= null;
7057 this.$element
.addClass( 'oo-ui-messageDialog' );
7062 OO
.inheritClass( OO
.ui
.MessageDialog
, OO
.ui
.Dialog
);
7064 /* Static Properties */
7066 OO
.ui
.MessageDialog
.static.name
= 'message';
7068 OO
.ui
.MessageDialog
.static.size
= 'small';
7070 OO
.ui
.MessageDialog
.static.verbose
= false;
7075 * The title of a confirmation dialog describes what a progressive action will do. The
7076 * title of an alert dialog describes which event occurred.
7080 * @property {jQuery|string|Function|null}
7082 OO
.ui
.MessageDialog
.static.title
= null;
7085 * The message displayed in the dialog body.
7087 * A confirmation message describes the consequences of a progressive action. An alert
7088 * message describes why an event occurred.
7092 * @property {jQuery|string|Function|null}
7094 OO
.ui
.MessageDialog
.static.message
= null;
7096 OO
.ui
.MessageDialog
.static.actions
= [
7097 { action
: 'accept', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-accept' ), flags
: 'primary' },
7098 { action
: 'reject', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-reject' ), flags
: 'safe' }
7106 OO
.ui
.MessageDialog
.prototype.setManager = function ( manager
) {
7107 OO
.ui
.MessageDialog
.super.prototype.setManager
.call( this, manager
);
7110 this.manager
.connect( this, {
7120 OO
.ui
.MessageDialog
.prototype.onActionResize = function ( action
) {
7122 return OO
.ui
.MessageDialog
.super.prototype.onActionResize
.call( this, action
);
7126 * Handle window resized events.
7130 OO
.ui
.MessageDialog
.prototype.onResize = function () {
7132 dialog
.fitActions();
7133 // Wait for CSS transition to finish and do it again :(
7134 setTimeout( function () {
7135 dialog
.fitActions();
7140 * Toggle action layout between vertical and horizontal.
7144 * @param {boolean} [value] Layout actions vertically, omit to toggle
7147 OO
.ui
.MessageDialog
.prototype.toggleVerticalActionLayout = function ( value
) {
7148 value
= value
=== undefined ? !this.verticalActionLayout
: !!value
;
7150 if ( value
!== this.verticalActionLayout
) {
7151 this.verticalActionLayout
= value
;
7153 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value
)
7154 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value
);
7163 OO
.ui
.MessageDialog
.prototype.getActionProcess = function ( action
) {
7165 return new OO
.ui
.Process( function () {
7166 this.close( { action
: action
} );
7169 return OO
.ui
.MessageDialog
.super.prototype.getActionProcess
.call( this, action
);
7175 * @param {Object} [data] Dialog opening data
7176 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
7177 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
7178 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
7179 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
7182 OO
.ui
.MessageDialog
.prototype.getSetupProcess = function ( data
) {
7186 return OO
.ui
.MessageDialog
.super.prototype.getSetupProcess
.call( this, data
)
7187 .next( function () {
7188 this.title
.setLabel(
7189 data
.title
!== undefined ? data
.title
: this.constructor.static.title
7191 this.message
.setLabel(
7192 data
.message
!== undefined ? data
.message
: this.constructor.static.message
7194 this.message
.$element
.toggleClass(
7195 'oo-ui-messageDialog-message-verbose',
7196 data
.verbose
!== undefined ? data
.verbose
: this.constructor.static.verbose
7204 OO
.ui
.MessageDialog
.prototype.getBodyHeight = function () {
7205 var bodyHeight
, oldOverflow
,
7206 $scrollable
= this.container
.$element
;
7208 oldOverflow
= $scrollable
[ 0 ].style
.overflow
;
7209 $scrollable
[ 0 ].style
.overflow
= 'hidden';
7211 OO
.ui
.Element
.static.reconsiderScrollbars( $scrollable
[ 0 ] );
7213 bodyHeight
= this.text
.$element
.outerHeight( true );
7214 $scrollable
[ 0 ].style
.overflow
= oldOverflow
;
7222 OO
.ui
.MessageDialog
.prototype.setDimensions = function ( dim
) {
7223 var $scrollable
= this.container
.$element
;
7224 OO
.ui
.MessageDialog
.super.prototype.setDimensions
.call( this, dim
);
7226 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
7227 // Need to do it after transition completes (250ms), add 50ms just in case.
7228 setTimeout( function () {
7229 var oldOverflow
= $scrollable
[ 0 ].style
.overflow
;
7230 $scrollable
[ 0 ].style
.overflow
= 'hidden';
7232 OO
.ui
.Element
.static.reconsiderScrollbars( $scrollable
[ 0 ] );
7234 $scrollable
[ 0 ].style
.overflow
= oldOverflow
;
7243 OO
.ui
.MessageDialog
.prototype.initialize = function () {
7245 OO
.ui
.MessageDialog
.super.prototype.initialize
.call( this );
7248 this.$actions
= $( '<div>' );
7249 this.container
= new OO
.ui
.PanelLayout( {
7250 scrollable
: true, classes
: [ 'oo-ui-messageDialog-container' ]
7252 this.text
= new OO
.ui
.PanelLayout( {
7253 padded
: true, expanded
: false, classes
: [ 'oo-ui-messageDialog-text' ]
7255 this.message
= new OO
.ui
.LabelWidget( {
7256 classes
: [ 'oo-ui-messageDialog-message' ]
7260 this.title
.$element
.addClass( 'oo-ui-messageDialog-title' );
7261 this.$content
.addClass( 'oo-ui-messageDialog-content' );
7262 this.container
.$element
.append( this.text
.$element
);
7263 this.text
.$element
.append( this.title
.$element
, this.message
.$element
);
7264 this.$body
.append( this.container
.$element
);
7265 this.$actions
.addClass( 'oo-ui-messageDialog-actions' );
7266 this.$foot
.append( this.$actions
);
7272 OO
.ui
.MessageDialog
.prototype.attachActions = function () {
7273 var i
, len
, other
, special
, others
;
7276 OO
.ui
.MessageDialog
.super.prototype.attachActions
.call( this );
7278 special
= this.actions
.getSpecial();
7279 others
= this.actions
.getOthers();
7280 if ( special
.safe
) {
7281 this.$actions
.append( special
.safe
.$element
);
7282 special
.safe
.toggleFramed( false );
7284 if ( others
.length
) {
7285 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
7286 other
= others
[ i
];
7287 this.$actions
.append( other
.$element
);
7288 other
.toggleFramed( false );
7291 if ( special
.primary
) {
7292 this.$actions
.append( special
.primary
.$element
);
7293 special
.primary
.toggleFramed( false );
7296 if ( !this.isOpening() ) {
7297 // If the dialog is currently opening, this will be called automatically soon.
7298 // This also calls #fitActions.
7304 * Fit action actions into columns or rows.
7306 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
7310 OO
.ui
.MessageDialog
.prototype.fitActions = function () {
7312 previous
= this.verticalActionLayout
,
7313 actions
= this.actions
.get();
7316 this.toggleVerticalActionLayout( false );
7317 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
7318 action
= actions
[ i
];
7319 if ( action
.$element
.innerWidth() < action
.$label
.outerWidth( true ) ) {
7320 this.toggleVerticalActionLayout( true );
7325 // Move the body out of the way of the foot
7326 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
7328 if ( this.verticalActionLayout
!== previous
) {
7329 // We changed the layout, window height might need to be updated.
7335 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
7336 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
7337 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
7338 * relevant. The ProcessDialog class is always extended and customized with the actions and content
7339 * required for each process.
7341 * The process dialog box consists of a header that visually represents the ‘working’ state of long
7342 * processes with an animation. The header contains the dialog title as well as
7343 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
7344 * a ‘primary’ action on the right (e.g., ‘Done’).
7346 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
7347 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
7350 * // Example: Creating and opening a process dialog window.
7351 * function ProcessDialog( config ) {
7352 * ProcessDialog.super.call( this, config );
7354 * OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
7356 * ProcessDialog.static.title = 'Process dialog';
7357 * ProcessDialog.static.actions = [
7358 * { action: 'save', label: 'Done', flags: 'primary' },
7359 * { label: 'Cancel', flags: 'safe' }
7362 * ProcessDialog.prototype.initialize = function () {
7363 * ProcessDialog.super.prototype.initialize.apply( this, arguments );
7364 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } );
7365 * 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>' );
7366 * this.$body.append( this.content.$element );
7368 * ProcessDialog.prototype.getActionProcess = function ( action ) {
7369 * var dialog = this;
7371 * return new OO.ui.Process( function () {
7372 * dialog.close( { action: action } );
7375 * return ProcessDialog.super.prototype.getActionProcess.call( this, action );
7378 * var windowManager = new OO.ui.WindowManager();
7379 * $( 'body' ).append( windowManager.$element );
7381 * var processDialog = new ProcessDialog();
7382 * windowManager.addWindows( [ processDialog ] );
7383 * windowManager.openWindow( processDialog );
7385 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
7389 * @extends OO.ui.Dialog
7392 * @param {Object} [config] Configuration options
7394 OO
.ui
.ProcessDialog
= function OoUiProcessDialog( config
) {
7395 // Parent constructor
7396 OO
.ui
.ProcessDialog
.super.call( this, config
);
7399 this.$element
.addClass( 'oo-ui-processDialog' );
7404 OO
.inheritClass( OO
.ui
.ProcessDialog
, OO
.ui
.Dialog
);
7409 * Handle dismiss button click events.
7415 OO
.ui
.ProcessDialog
.prototype.onDismissErrorButtonClick = function () {
7420 * Handle retry button click events.
7422 * Hides errors and then tries again.
7426 OO
.ui
.ProcessDialog
.prototype.onRetryButtonClick = function () {
7428 this.executeAction( this.currentAction
);
7434 OO
.ui
.ProcessDialog
.prototype.onActionResize = function ( action
) {
7435 if ( this.actions
.isSpecial( action
) ) {
7438 return OO
.ui
.ProcessDialog
.super.prototype.onActionResize
.call( this, action
);
7444 OO
.ui
.ProcessDialog
.prototype.initialize = function () {
7446 OO
.ui
.ProcessDialog
.super.prototype.initialize
.call( this );
7449 this.$navigation
= $( '<div>' );
7450 this.$location
= $( '<div>' );
7451 this.$safeActions
= $( '<div>' );
7452 this.$primaryActions
= $( '<div>' );
7453 this.$otherActions
= $( '<div>' );
7454 this.dismissButton
= new OO
.ui
.ButtonWidget( {
7455 label
: OO
.ui
.msg( 'ooui-dialog-process-dismiss' )
7457 this.retryButton
= new OO
.ui
.ButtonWidget();
7458 this.$errors
= $( '<div>' );
7459 this.$errorsTitle
= $( '<div>' );
7462 this.dismissButton
.connect( this, { click
: 'onDismissErrorButtonClick' } );
7463 this.retryButton
.connect( this, { click
: 'onRetryButtonClick' } );
7466 this.title
.$element
.addClass( 'oo-ui-processDialog-title' );
7468 .append( this.title
.$element
)
7469 .addClass( 'oo-ui-processDialog-location' );
7470 this.$safeActions
.addClass( 'oo-ui-processDialog-actions-safe' );
7471 this.$primaryActions
.addClass( 'oo-ui-processDialog-actions-primary' );
7472 this.$otherActions
.addClass( 'oo-ui-processDialog-actions-other' );
7474 .addClass( 'oo-ui-processDialog-errors-title' )
7475 .text( OO
.ui
.msg( 'ooui-dialog-process-error' ) );
7477 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
7478 .append( this.$errorsTitle
, this.dismissButton
.$element
, this.retryButton
.$element
);
7480 .addClass( 'oo-ui-processDialog-content' )
7481 .append( this.$errors
);
7483 .addClass( 'oo-ui-processDialog-navigation' )
7484 .append( this.$safeActions
, this.$location
, this.$primaryActions
);
7485 this.$head
.append( this.$navigation
);
7486 this.$foot
.append( this.$otherActions
);
7492 OO
.ui
.ProcessDialog
.prototype.attachActions = function () {
7493 var i
, len
, other
, special
, others
;
7496 OO
.ui
.ProcessDialog
.super.prototype.attachActions
.call( this );
7498 special
= this.actions
.getSpecial();
7499 others
= this.actions
.getOthers();
7500 if ( special
.primary
) {
7501 this.$primaryActions
.append( special
.primary
.$element
);
7502 special
.primary
.toggleFramed( true );
7504 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
7505 other
= others
[ i
];
7506 this.$otherActions
.append( other
.$element
);
7507 other
.toggleFramed( true );
7509 if ( special
.safe
) {
7510 this.$safeActions
.append( special
.safe
.$element
);
7511 special
.safe
.toggleFramed( true );
7515 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
7521 OO
.ui
.ProcessDialog
.prototype.executeAction = function ( action
) {
7522 OO
.ui
.ProcessDialog
.super.prototype.executeAction
.call( this, action
)
7523 .fail( this.showErrors
.bind( this ) );
7527 * Fit label between actions.
7532 OO
.ui
.ProcessDialog
.prototype.fitLabel = function () {
7533 var width
= Math
.max(
7534 this.$safeActions
.is( ':visible' ) ? this.$safeActions
.width() : 0,
7535 this.$primaryActions
.is( ':visible' ) ? this.$primaryActions
.width() : 0
7537 this.$location
.css( { paddingLeft
: width
, paddingRight
: width
} );
7543 * Handle errors that occurred during accept or reject processes.
7546 * @param {OO.ui.Error[]} errors Errors to be handled
7548 OO
.ui
.ProcessDialog
.prototype.showErrors = function ( errors
) {
7549 var i
, len
, $item
, actions
,
7555 for ( i
= 0, len
= errors
.length
; i
< len
; i
++ ) {
7556 if ( !errors
[ i
].isRecoverable() ) {
7557 recoverable
= false;
7559 if ( errors
[ i
].isWarning() ) {
7562 $item
= $( '<div>' )
7563 .addClass( 'oo-ui-processDialog-error' )
7564 .append( errors
[ i
].getMessage() );
7565 items
.push( $item
[ 0 ] );
7567 this.$errorItems
= $( items
);
7568 if ( recoverable
) {
7569 abilities
[this.currentAction
] = true;
7570 // Copy the flags from the first matching action
7571 actions
= this.actions
.get( { actions
: this.currentAction
} );
7572 if ( actions
.length
) {
7573 this.retryButton
.clearFlags().setFlags( actions
[0].getFlags() );
7576 abilities
[this.currentAction
] = false;
7577 this.actions
.setAbilities( abilities
);
7580 this.retryButton
.setLabel( OO
.ui
.msg( 'ooui-dialog-process-continue' ) );
7582 this.retryButton
.setLabel( OO
.ui
.msg( 'ooui-dialog-process-retry' ) );
7584 this.retryButton
.toggle( recoverable
);
7585 this.$errorsTitle
.after( this.$errorItems
);
7586 this.$errors
.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
7594 OO
.ui
.ProcessDialog
.prototype.hideErrors = function () {
7595 this.$errors
.addClass( 'oo-ui-element-hidden' );
7596 if ( this.$errorItems
) {
7597 this.$errorItems
.remove();
7598 this.$errorItems
= null;
7605 OO
.ui
.ProcessDialog
.prototype.getTeardownProcess = function ( data
) {
7607 return OO
.ui
.ProcessDialog
.super.prototype.getTeardownProcess
.call( this, data
)
7608 .first( function () {
7609 // Make sure to hide errors
7615 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
7616 * which is a widget that is specified by reference before any optional configuration settings.
7618 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
7620 * - **left**: The label is placed before the field-widget and aligned with the left margin.
7621 * A left-alignment is used for forms with many fields.
7622 * - **right**: The label is placed before the field-widget and aligned to the right margin.
7623 * A right-alignment is used for long but familiar forms which users tab through,
7624 * verifying the current field with a quick glance at the label.
7625 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
7626 * that users fill out from top to bottom.
7627 * - **inline**: The label is placed after the field-widget and aligned to the left.
7628 * An inline-alignment is best used with checkboxes or radio buttons.
7630 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
7631 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
7633 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
7635 * @extends OO.ui.Layout
7636 * @mixins OO.ui.LabelElement
7639 * @param {OO.ui.Widget} fieldWidget Field widget
7640 * @param {Object} [config] Configuration options
7641 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7642 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7644 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
7645 // Allow passing positional parameters inside the config object
7646 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
7647 config
= fieldWidget
;
7648 fieldWidget
= config
.fieldWidget
;
7651 var hasInputWidget
= fieldWidget
instanceof OO
.ui
.InputWidget
;
7653 // Configuration initialization
7654 config
= $.extend( { align
: 'left' }, config
);
7656 // Parent constructor
7657 OO
.ui
.FieldLayout
.super.call( this, config
);
7659 // Mixin constructors
7660 OO
.ui
.LabelElement
.call( this, config
);
7663 this.fieldWidget
= fieldWidget
;
7664 this.$field
= $( '<div>' );
7665 this.$body
= $( '<' + ( hasInputWidget
? 'label' : 'div' ) + '>' );
7667 if ( config
.help
) {
7668 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
7669 classes
: [ 'oo-ui-fieldLayout-help' ],
7674 this.popupButtonWidget
.getPopup().$body
.append(
7676 .text( config
.help
)
7677 .addClass( 'oo-ui-fieldLayout-help-content' )
7679 this.$help
= this.popupButtonWidget
.$element
;
7681 this.$help
= $( [] );
7685 if ( hasInputWidget
) {
7686 this.$label
.on( 'click', this.onLabelClick
.bind( this ) );
7688 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
7692 .addClass( 'oo-ui-fieldLayout' )
7693 .append( this.$help
, this.$body
);
7694 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
7696 .addClass( 'oo-ui-fieldLayout-field' )
7697 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget
.isDisabled() )
7698 .append( this.fieldWidget
.$element
);
7700 this.setAlignment( config
.align
);
7705 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
7706 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.LabelElement
);
7711 * Handle field disable events.
7713 * @param {boolean} value Field is disabled
7715 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
7716 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
7720 * Handle label mouse click events.
7722 * @param {jQuery.Event} e Mouse click event
7724 OO
.ui
.FieldLayout
.prototype.onLabelClick = function () {
7725 this.fieldWidget
.simulateLabelClick();
7732 * @return {OO.ui.Widget} Field widget
7734 OO
.ui
.FieldLayout
.prototype.getField = function () {
7735 return this.fieldWidget
;
7739 * Set the field alignment mode.
7742 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
7745 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
7746 if ( value
!== this.align
) {
7747 // Default to 'left'
7748 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
7752 if ( value
=== 'inline' ) {
7753 this.$body
.append( this.$field
, this.$label
);
7755 this.$body
.append( this.$label
, this.$field
);
7757 // Set classes. The following classes can be used here:
7758 // * oo-ui-fieldLayout-align-left
7759 // * oo-ui-fieldLayout-align-right
7760 // * oo-ui-fieldLayout-align-top
7761 // * oo-ui-fieldLayout-align-inline
7763 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
7765 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
7773 * Layout made of a field, a button, and an optional label.
7776 * @extends OO.ui.FieldLayout
7779 * @param {OO.ui.Widget} fieldWidget Field widget
7780 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
7781 * @param {Object} [config] Configuration options
7782 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7783 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7785 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
7786 // Allow passing positional parameters inside the config object
7787 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
7788 config
= fieldWidget
;
7789 fieldWidget
= config
.fieldWidget
;
7790 buttonWidget
= config
.buttonWidget
;
7793 // Configuration initialization
7794 config
= $.extend( { align
: 'left' }, config
);
7796 // Parent constructor
7797 OO
.ui
.ActionFieldLayout
.super.call( this, fieldWidget
, config
);
7800 this.fieldWidget
= fieldWidget
;
7801 this.buttonWidget
= buttonWidget
;
7802 this.$button
= $( '<div>' )
7803 .addClass( 'oo-ui-actionFieldLayout-button' )
7804 .append( this.buttonWidget
.$element
);
7805 this.$input
= $( '<div>' )
7806 .addClass( 'oo-ui-actionFieldLayout-input' )
7807 .append( this.fieldWidget
.$element
);
7809 .addClass( 'oo-ui-actionFieldLayout' )
7810 .append( this.$input
, this.$button
);
7815 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
7818 * Layout made of a fieldset and optional legend.
7820 * Just add OO.ui.FieldLayout items.
7823 * @extends OO.ui.Layout
7824 * @mixins OO.ui.IconElement
7825 * @mixins OO.ui.LabelElement
7826 * @mixins OO.ui.GroupElement
7829 * @param {Object} [config] Configuration options
7830 * @cfg {OO.ui.FieldLayout[]} [items] Items to add
7832 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
7833 // Configuration initialization
7834 config
= config
|| {};
7836 // Parent constructor
7837 OO
.ui
.FieldsetLayout
.super.call( this, config
);
7839 // Mixin constructors
7840 OO
.ui
.IconElement
.call( this, config
);
7841 OO
.ui
.LabelElement
.call( this, config
);
7842 OO
.ui
.GroupElement
.call( this, config
);
7844 if ( config
.help
) {
7845 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
7846 classes
: [ 'oo-ui-fieldsetLayout-help' ],
7851 this.popupButtonWidget
.getPopup().$body
.append(
7853 .text( config
.help
)
7854 .addClass( 'oo-ui-fieldsetLayout-help-content' )
7856 this.$help
= this.popupButtonWidget
.$element
;
7858 this.$help
= $( [] );
7863 .addClass( 'oo-ui-fieldsetLayout' )
7864 .prepend( this.$help
, this.$icon
, this.$label
, this.$group
);
7865 if ( Array
.isArray( config
.items
) ) {
7866 this.addItems( config
.items
);
7872 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
7873 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.IconElement
);
7874 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.LabelElement
);
7875 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.GroupElement
);
7878 * Layout with an HTML form.
7881 * @extends OO.ui.Layout
7882 * @mixins OO.ui.GroupElement
7885 * @param {Object} [config] Configuration options
7886 * @cfg {string} [method] HTML form `method` attribute
7887 * @cfg {string} [action] HTML form `action` attribute
7888 * @cfg {string} [enctype] HTML form `enctype` attribute
7889 * @cfg {OO.ui.FieldsetLayout[]} [items] Items to add
7891 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
7892 // Configuration initialization
7893 config
= config
|| {};
7895 // Parent constructor
7896 OO
.ui
.FormLayout
.super.call( this, config
);
7898 // Mixin constructors
7899 OO
.ui
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
7902 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
7906 .addClass( 'oo-ui-formLayout' )
7908 method
: config
.method
,
7909 action
: config
.action
,
7910 enctype
: config
.enctype
7912 if ( Array
.isArray( config
.items
) ) {
7913 this.addItems( config
.items
);
7919 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
7920 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.GroupElement
);
7928 /* Static Properties */
7930 OO
.ui
.FormLayout
.static.tagName
= 'form';
7935 * Handle form submit events.
7937 * @param {jQuery.Event} e Submit event
7940 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
7941 this.emit( 'submit' );
7946 * Layout with a content and menu area.
7948 * The menu area can be positioned at the top, after, bottom or before. The content area will fill
7949 * all remaining space.
7952 * @extends OO.ui.Layout
7955 * @param {Object} [config] Configuration options
7956 * @cfg {number|string} [menuSize='18em'] Size of menu in pixels or any CSS unit
7957 * @cfg {boolean} [showMenu=true] Show menu
7958 * @cfg {string} [position='before'] Position of menu, either `top`, `after`, `bottom` or `before`
7959 * @cfg {boolean} [collapse] Collapse the menu out of view
7961 OO
.ui
.MenuLayout
= function OoUiMenuLayout( config
) {
7962 var positions
= this.constructor.static.menuPositions
;
7964 // Configuration initialization
7965 config
= config
|| {};
7967 // Parent constructor
7968 OO
.ui
.MenuLayout
.super.call( this, config
);
7971 this.showMenu
= config
.showMenu
!== false;
7972 this.menuSize
= config
.menuSize
|| '18em';
7973 this.menuPosition
= positions
[ config
.menuPosition
] || positions
.before
;
7978 * @property {jQuery}
7980 this.$menu
= $( '<div>' );
7984 * @property {jQuery}
7986 this.$content
= $( '<div>' );
7989 this.toggleMenu( this.showMenu
);
7992 .addClass( 'oo-ui-menuLayout-menu' )
7993 .css( this.menuPosition
.sizeProperty
, this.menuSize
);
7994 this.$content
.addClass( 'oo-ui-menuLayout-content' );
7996 .addClass( 'oo-ui-menuLayout ' + this.menuPosition
.className
)
7997 .append( this.$content
, this.$menu
);
8002 OO
.inheritClass( OO
.ui
.MenuLayout
, OO
.ui
.Layout
);
8004 /* Static Properties */
8006 OO
.ui
.MenuLayout
.static.menuPositions
= {
8008 sizeProperty
: 'height',
8009 className
: 'oo-ui-menuLayout-top'
8012 sizeProperty
: 'width',
8013 className
: 'oo-ui-menuLayout-after'
8016 sizeProperty
: 'height',
8017 className
: 'oo-ui-menuLayout-bottom'
8020 sizeProperty
: 'width',
8021 className
: 'oo-ui-menuLayout-before'
8030 * @param {boolean} showMenu Show menu, omit to toggle
8033 OO
.ui
.MenuLayout
.prototype.toggleMenu = function ( showMenu
) {
8034 showMenu
= showMenu
=== undefined ? !this.showMenu
: !!showMenu
;
8036 if ( this.showMenu
!== showMenu
) {
8037 this.showMenu
= showMenu
;
8045 * Check if menu is visible
8047 * @return {boolean} Menu is visible
8049 OO
.ui
.MenuLayout
.prototype.isMenuVisible = function () {
8050 return this.showMenu
;
8056 * @param {number|string} size Size of menu in pixels or any CSS unit
8059 OO
.ui
.MenuLayout
.prototype.setMenuSize = function ( size
) {
8060 this.menuSize
= size
;
8067 * Update menu and content CSS based on current menu size and visibility
8069 * This method is called internally when size or position is changed.
8071 OO
.ui
.MenuLayout
.prototype.updateSizes = function () {
8072 if ( this.showMenu
) {
8074 .css( this.menuPosition
.sizeProperty
, this.menuSize
)
8075 .css( 'overflow', '' );
8076 // Set offsets on all sides. CSS resets all but one with
8077 // 'important' rules so directionality flips are supported
8078 this.$content
.css( {
8080 right
: this.menuSize
,
8081 bottom
: this.menuSize
,
8086 .css( this.menuPosition
.sizeProperty
, 0 )
8087 .css( 'overflow', 'hidden' );
8088 this.$content
.css( {
8100 * @return {number|string} Menu size
8102 OO
.ui
.MenuLayout
.prototype.getMenuSize = function () {
8103 return this.menuSize
;
8107 * Set menu position.
8109 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
8110 * @throws {Error} If position value is not supported
8113 OO
.ui
.MenuLayout
.prototype.setMenuPosition = function ( position
) {
8114 var positions
= this.constructor.static.menuPositions
;
8116 if ( !positions
[ position
] ) {
8117 throw new Error( 'Cannot set position; unsupported position value: ' + position
);
8120 this.$menu
.css( this.menuPosition
.sizeProperty
, '' );
8121 this.$element
.removeClass( this.menuPosition
.className
);
8123 this.menuPosition
= positions
[ position
];
8126 this.$element
.addClass( this.menuPosition
.className
);
8132 * Get menu position.
8134 * @return {string} Menu position
8136 OO
.ui
.MenuLayout
.prototype.getMenuPosition = function () {
8137 return this.menuPosition
;
8141 * Layout containing a series of pages.
8144 * @extends OO.ui.MenuLayout
8147 * @param {Object} [config] Configuration options
8148 * @cfg {boolean} [continuous=false] Show all pages, one after another
8149 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
8150 * @cfg {boolean} [outlined=false] Show an outline
8151 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
8153 OO
.ui
.BookletLayout
= function OoUiBookletLayout( config
) {
8154 // Configuration initialization
8155 config
= config
|| {};
8157 // Parent constructor
8158 OO
.ui
.BookletLayout
.super.call( this, config
);
8161 this.currentPageName
= null;
8163 this.ignoreFocus
= false;
8164 this.stackLayout
= new OO
.ui
.StackLayout( { continuous
: !!config
.continuous
} );
8165 this.$content
.append( this.stackLayout
.$element
);
8166 this.autoFocus
= config
.autoFocus
=== undefined || !!config
.autoFocus
;
8167 this.outlineVisible
= false;
8168 this.outlined
= !!config
.outlined
;
8169 if ( this.outlined
) {
8170 this.editable
= !!config
.editable
;
8171 this.outlineControlsWidget
= null;
8172 this.outlineSelectWidget
= new OO
.ui
.OutlineSelectWidget();
8173 this.outlinePanel
= new OO
.ui
.PanelLayout( { scrollable
: true } );
8174 this.$menu
.append( this.outlinePanel
.$element
);
8175 this.outlineVisible
= true;
8176 if ( this.editable
) {
8177 this.outlineControlsWidget
= new OO
.ui
.OutlineControlsWidget(
8178 this.outlineSelectWidget
8182 this.toggleMenu( this.outlined
);
8185 this.stackLayout
.connect( this, { set: 'onStackLayoutSet' } );
8186 if ( this.outlined
) {
8187 this.outlineSelectWidget
.connect( this, { select
: 'onOutlineSelectWidgetSelect' } );
8189 if ( this.autoFocus
) {
8190 // Event 'focus' does not bubble, but 'focusin' does
8191 this.stackLayout
.$element
.on( 'focusin', this.onStackLayoutFocus
.bind( this ) );
8195 this.$element
.addClass( 'oo-ui-bookletLayout' );
8196 this.stackLayout
.$element
.addClass( 'oo-ui-bookletLayout-stackLayout' );
8197 if ( this.outlined
) {
8198 this.outlinePanel
.$element
8199 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
8200 .append( this.outlineSelectWidget
.$element
);
8201 if ( this.editable
) {
8202 this.outlinePanel
.$element
8203 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
8204 .append( this.outlineControlsWidget
.$element
);
8211 OO
.inheritClass( OO
.ui
.BookletLayout
, OO
.ui
.MenuLayout
);
8217 * @param {OO.ui.PageLayout} page Current page
8222 * @param {OO.ui.PageLayout[]} page Added pages
8223 * @param {number} index Index pages were added at
8228 * @param {OO.ui.PageLayout[]} pages Removed pages
8234 * Handle stack layout focus.
8236 * @param {jQuery.Event} e Focusin event
8238 OO
.ui
.BookletLayout
.prototype.onStackLayoutFocus = function ( e
) {
8241 // Find the page that an element was focused within
8242 $target
= $( e
.target
).closest( '.oo-ui-pageLayout' );
8243 for ( name
in this.pages
) {
8244 // Check for page match, exclude current page to find only page changes
8245 if ( this.pages
[ name
].$element
[ 0 ] === $target
[ 0 ] && name
!== this.currentPageName
) {
8246 this.setPage( name
);
8253 * Handle stack layout set events.
8255 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
8257 OO
.ui
.BookletLayout
.prototype.onStackLayoutSet = function ( page
) {
8260 page
.scrollElementIntoView( { complete: function () {
8261 if ( layout
.autoFocus
) {
8269 * Focus the first input in the current page.
8271 * If no page is selected, the first selectable page will be selected.
8272 * If the focus is already in an element on the current page, nothing will happen.
8274 OO
.ui
.BookletLayout
.prototype.focus = function () {
8275 var $input
, page
= this.stackLayout
.getCurrentItem();
8276 if ( !page
&& this.outlined
) {
8277 this.selectFirstSelectablePage();
8278 page
= this.stackLayout
.getCurrentItem();
8283 // Only change the focus if is not already in the current page
8284 if ( !page
.$element
.find( ':focus' ).length
) {
8285 $input
= page
.$element
.find( ':input:first' );
8286 if ( $input
.length
) {
8287 $input
[ 0 ].focus();
8293 * Handle outline widget select events.
8295 * @param {OO.ui.OptionWidget|null} item Selected item
8297 OO
.ui
.BookletLayout
.prototype.onOutlineSelectWidgetSelect = function ( item
) {
8299 this.setPage( item
.getData() );
8304 * Check if booklet has an outline.
8308 OO
.ui
.BookletLayout
.prototype.isOutlined = function () {
8309 return this.outlined
;
8313 * Check if booklet has editing controls.
8317 OO
.ui
.BookletLayout
.prototype.isEditable = function () {
8318 return this.editable
;
8322 * Check if booklet has a visible outline.
8326 OO
.ui
.BookletLayout
.prototype.isOutlineVisible = function () {
8327 return this.outlined
&& this.outlineVisible
;
8331 * Hide or show the outline.
8333 * @param {boolean} [show] Show outline, omit to invert current state
8336 OO
.ui
.BookletLayout
.prototype.toggleOutline = function ( show
) {
8337 if ( this.outlined
) {
8338 show
= show
=== undefined ? !this.outlineVisible
: !!show
;
8339 this.outlineVisible
= show
;
8340 this.toggleMenu( show
);
8347 * Get the outline widget.
8349 * @param {OO.ui.PageLayout} page Page to be selected
8350 * @return {OO.ui.PageLayout|null} Closest page to another
8352 OO
.ui
.BookletLayout
.prototype.getClosestPage = function ( page
) {
8353 var next
, prev
, level
,
8354 pages
= this.stackLayout
.getItems(),
8355 index
= $.inArray( page
, pages
);
8357 if ( index
!== -1 ) {
8358 next
= pages
[ index
+ 1 ];
8359 prev
= pages
[ index
- 1 ];
8360 // Prefer adjacent pages at the same level
8361 if ( this.outlined
) {
8362 level
= this.outlineSelectWidget
.getItemFromData( page
.getName() ).getLevel();
8365 level
=== this.outlineSelectWidget
.getItemFromData( prev
.getName() ).getLevel()
8371 level
=== this.outlineSelectWidget
.getItemFromData( next
.getName() ).getLevel()
8377 return prev
|| next
|| null;
8381 * Get the outline widget.
8383 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
8385 OO
.ui
.BookletLayout
.prototype.getOutline = function () {
8386 return this.outlineSelectWidget
;
8390 * Get the outline controls widget. If the outline is not editable, null is returned.
8392 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
8394 OO
.ui
.BookletLayout
.prototype.getOutlineControls = function () {
8395 return this.outlineControlsWidget
;
8399 * Get a page by name.
8401 * @param {string} name Symbolic name of page
8402 * @return {OO.ui.PageLayout|undefined} Page, if found
8404 OO
.ui
.BookletLayout
.prototype.getPage = function ( name
) {
8405 return this.pages
[ name
];
8409 * Get the current page
8411 * @return {OO.ui.PageLayout|undefined} Current page, if found
8413 OO
.ui
.BookletLayout
.prototype.getCurrentPage = function () {
8414 var name
= this.getCurrentPageName();
8415 return name
? this.getPage( name
) : undefined;
8419 * Get the current page name.
8421 * @return {string|null} Current page name
8423 OO
.ui
.BookletLayout
.prototype.getCurrentPageName = function () {
8424 return this.currentPageName
;
8428 * Add a page to the layout.
8430 * When pages are added with the same names as existing pages, the existing pages will be
8431 * automatically removed before the new pages are added.
8433 * @param {OO.ui.PageLayout[]} pages Pages to add
8434 * @param {number} index Index to insert pages after
8438 OO
.ui
.BookletLayout
.prototype.addPages = function ( pages
, index
) {
8439 var i
, len
, name
, page
, item
, currentIndex
,
8440 stackLayoutPages
= this.stackLayout
.getItems(),
8444 // Remove pages with same names
8445 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
8447 name
= page
.getName();
8449 if ( Object
.prototype.hasOwnProperty
.call( this.pages
, name
) ) {
8450 // Correct the insertion index
8451 currentIndex
= $.inArray( this.pages
[ name
], stackLayoutPages
);
8452 if ( currentIndex
!== -1 && currentIndex
+ 1 < index
) {
8455 remove
.push( this.pages
[ name
] );
8458 if ( remove
.length
) {
8459 this.removePages( remove
);
8463 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
8465 name
= page
.getName();
8466 this.pages
[ page
.getName() ] = page
;
8467 if ( this.outlined
) {
8468 item
= new OO
.ui
.OutlineOptionWidget( { data
: name
} );
8469 page
.setOutlineItem( item
);
8474 if ( this.outlined
&& items
.length
) {
8475 this.outlineSelectWidget
.addItems( items
, index
);
8476 this.selectFirstSelectablePage();
8478 this.stackLayout
.addItems( pages
, index
);
8479 this.emit( 'add', pages
, index
);
8485 * Remove a page from the layout.
8490 OO
.ui
.BookletLayout
.prototype.removePages = function ( pages
) {
8491 var i
, len
, name
, page
,
8494 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
8496 name
= page
.getName();
8497 delete this.pages
[ name
];
8498 if ( this.outlined
) {
8499 items
.push( this.outlineSelectWidget
.getItemFromData( name
) );
8500 page
.setOutlineItem( null );
8503 if ( this.outlined
&& items
.length
) {
8504 this.outlineSelectWidget
.removeItems( items
);
8505 this.selectFirstSelectablePage();
8507 this.stackLayout
.removeItems( pages
);
8508 this.emit( 'remove', pages
);
8514 * Clear all pages from the layout.
8519 OO
.ui
.BookletLayout
.prototype.clearPages = function () {
8521 pages
= this.stackLayout
.getItems();
8524 this.currentPageName
= null;
8525 if ( this.outlined
) {
8526 this.outlineSelectWidget
.clearItems();
8527 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
8528 pages
[ i
].setOutlineItem( null );
8531 this.stackLayout
.clearItems();
8533 this.emit( 'remove', pages
);
8539 * Set the current page by name.
8542 * @param {string} name Symbolic name of page
8544 OO
.ui
.BookletLayout
.prototype.setPage = function ( name
) {
8547 page
= this.pages
[ name
];
8549 if ( name
!== this.currentPageName
) {
8550 if ( this.outlined
) {
8551 selectedItem
= this.outlineSelectWidget
.getSelectedItem();
8552 if ( selectedItem
&& selectedItem
.getData() !== name
) {
8553 this.outlineSelectWidget
.selectItem( this.outlineSelectWidget
.getItemFromData( name
) );
8557 if ( this.currentPageName
&& this.pages
[ this.currentPageName
] ) {
8558 this.pages
[ this.currentPageName
].setActive( false );
8559 // Blur anything focused if the next page doesn't have anything focusable - this
8560 // is not needed if the next page has something focusable because once it is focused
8561 // this blur happens automatically
8562 if ( this.autoFocus
&& !page
.$element
.find( ':input' ).length
) {
8563 $focused
= this.pages
[ this.currentPageName
].$element
.find( ':focus' );
8564 if ( $focused
.length
) {
8565 $focused
[ 0 ].blur();
8569 this.currentPageName
= name
;
8570 this.stackLayout
.setItem( page
);
8571 page
.setActive( true );
8572 this.emit( 'set', page
);
8578 * Select the first selectable page.
8582 OO
.ui
.BookletLayout
.prototype.selectFirstSelectablePage = function () {
8583 if ( !this.outlineSelectWidget
.getSelectedItem() ) {
8584 this.outlineSelectWidget
.selectItem( this.outlineSelectWidget
.getFirstSelectableItem() );
8591 * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
8594 * @extends OO.ui.Layout
8597 * @param {Object} [config] Configuration options
8598 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
8599 * @cfg {boolean} [padded=false] Pad the content from the edges
8600 * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
8601 * @cfg {boolean} [framed=false] Wrap in a frame to visually separate from outside content
8603 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
8604 // Configuration initialization
8605 config
= $.extend( {
8612 // Parent constructor
8613 OO
.ui
.PanelLayout
.super.call( this, config
);
8616 this.$element
.addClass( 'oo-ui-panelLayout' );
8617 if ( config
.scrollable
) {
8618 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
8620 if ( config
.padded
) {
8621 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
8623 if ( config
.expanded
) {
8624 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
8626 if ( config
.framed
) {
8627 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
8633 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
8636 * Page within an booklet layout.
8639 * @extends OO.ui.PanelLayout
8642 * @param {string} name Unique symbolic name of page
8643 * @param {Object} [config] Configuration options
8645 OO
.ui
.PageLayout
= function OoUiPageLayout( name
, config
) {
8646 // Allow passing positional parameters inside the config object
8647 if ( OO
.isPlainObject( name
) && config
=== undefined ) {
8652 // Configuration initialization
8653 config
= $.extend( { scrollable
: true }, config
);
8655 // Parent constructor
8656 OO
.ui
.PageLayout
.super.call( this, config
);
8660 this.outlineItem
= null;
8661 this.active
= false;
8664 this.$element
.addClass( 'oo-ui-pageLayout' );
8669 OO
.inheritClass( OO
.ui
.PageLayout
, OO
.ui
.PanelLayout
);
8675 * @param {boolean} active Page is active
8683 * @return {string} Symbolic name of page
8685 OO
.ui
.PageLayout
.prototype.getName = function () {
8690 * Check if page is active.
8692 * @return {boolean} Page is active
8694 OO
.ui
.PageLayout
.prototype.isActive = function () {
8701 * @return {OO.ui.OutlineOptionWidget|null} Outline item widget
8703 OO
.ui
.PageLayout
.prototype.getOutlineItem = function () {
8704 return this.outlineItem
;
8710 * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
8711 * outline item as desired; this method is called for setting (with an object) and unsetting
8712 * (with null) and overriding methods would have to check the value of `outlineItem` to avoid
8713 * operating on null instead of an OO.ui.OutlineOptionWidget object.
8715 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear
8718 OO
.ui
.PageLayout
.prototype.setOutlineItem = function ( outlineItem
) {
8719 this.outlineItem
= outlineItem
|| null;
8720 if ( outlineItem
) {
8721 this.setupOutlineItem();
8727 * Setup outline item.
8729 * @localdoc Subclasses should override this method to adjust the outline item as desired.
8731 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup
8734 OO
.ui
.PageLayout
.prototype.setupOutlineItem = function () {
8739 * Set page active state.
8741 * @param {boolean} Page is active
8744 OO
.ui
.PageLayout
.prototype.setActive = function ( active
) {
8747 if ( active
!== this.active
) {
8748 this.active
= active
;
8749 this.$element
.toggleClass( 'oo-ui-pageLayout-active', active
);
8750 this.emit( 'active', this.active
);
8755 * Layout containing a series of mutually exclusive pages.
8758 * @extends OO.ui.PanelLayout
8759 * @mixins OO.ui.GroupElement
8762 * @param {Object} [config] Configuration options
8763 * @cfg {boolean} [continuous=false] Show all pages, one after another
8764 * @cfg {OO.ui.Layout[]} [items] Layouts to add
8766 OO
.ui
.StackLayout
= function OoUiStackLayout( config
) {
8767 // Configuration initialization
8768 config
= $.extend( { scrollable
: true }, config
);
8770 // Parent constructor
8771 OO
.ui
.StackLayout
.super.call( this, config
);
8773 // Mixin constructors
8774 OO
.ui
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
8777 this.currentItem
= null;
8778 this.continuous
= !!config
.continuous
;
8781 this.$element
.addClass( 'oo-ui-stackLayout' );
8782 if ( this.continuous
) {
8783 this.$element
.addClass( 'oo-ui-stackLayout-continuous' );
8785 if ( Array
.isArray( config
.items
) ) {
8786 this.addItems( config
.items
);
8792 OO
.inheritClass( OO
.ui
.StackLayout
, OO
.ui
.PanelLayout
);
8793 OO
.mixinClass( OO
.ui
.StackLayout
, OO
.ui
.GroupElement
);
8799 * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
8805 * Get the current item.
8807 * @return {OO.ui.Layout|null}
8809 OO
.ui
.StackLayout
.prototype.getCurrentItem = function () {
8810 return this.currentItem
;
8814 * Unset the current item.
8817 * @param {OO.ui.StackLayout} layout
8820 OO
.ui
.StackLayout
.prototype.unsetCurrentItem = function () {
8821 var prevItem
= this.currentItem
;
8822 if ( prevItem
=== null ) {
8826 this.currentItem
= null;
8827 this.emit( 'set', null );
8833 * Adding an existing item (by value) will move it.
8835 * @param {OO.ui.Layout[]} items Items to add
8836 * @param {number} [index] Index to insert items after
8839 OO
.ui
.StackLayout
.prototype.addItems = function ( items
, index
) {
8840 // Update the visibility
8841 this.updateHiddenState( items
, this.currentItem
);
8844 OO
.ui
.GroupElement
.prototype.addItems
.call( this, items
, index
);
8846 if ( !this.currentItem
&& items
.length
) {
8847 this.setItem( items
[ 0 ] );
8856 * Items will be detached, not removed, so they can be used later.
8858 * @param {OO.ui.Layout[]} items Items to remove
8862 OO
.ui
.StackLayout
.prototype.removeItems = function ( items
) {
8864 OO
.ui
.GroupElement
.prototype.removeItems
.call( this, items
);
8866 if ( $.inArray( this.currentItem
, items
) !== -1 ) {
8867 if ( this.items
.length
) {
8868 this.setItem( this.items
[ 0 ] );
8870 this.unsetCurrentItem();
8880 * Items will be detached, not removed, so they can be used later.
8885 OO
.ui
.StackLayout
.prototype.clearItems = function () {
8886 this.unsetCurrentItem();
8887 OO
.ui
.GroupElement
.prototype.clearItems
.call( this );
8895 * Any currently shown item will be hidden.
8897 * FIXME: If the passed item to show has not been added in the items list, then
8898 * this method drops it and unsets the current item.
8900 * @param {OO.ui.Layout} item Item to show
8904 OO
.ui
.StackLayout
.prototype.setItem = function ( item
) {
8905 if ( item
!== this.currentItem
) {
8906 this.updateHiddenState( this.items
, item
);
8908 if ( $.inArray( item
, this.items
) !== -1 ) {
8909 this.currentItem
= item
;
8910 this.emit( 'set', item
);
8912 this.unsetCurrentItem();
8920 * Update the visibility of all items in case of non-continuous view.
8922 * Ensure all items are hidden except for the selected one.
8923 * This method does nothing when the stack is continuous.
8925 * @param {OO.ui.Layout[]} items Item list iterate over
8926 * @param {OO.ui.Layout} [selectedItem] Selected item to show
8928 OO
.ui
.StackLayout
.prototype.updateHiddenState = function ( items
, selectedItem
) {
8931 if ( !this.continuous
) {
8932 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
8933 if ( !selectedItem
|| selectedItem
!== items
[ i
] ) {
8934 items
[ i
].$element
.addClass( 'oo-ui-element-hidden' );
8937 if ( selectedItem
) {
8938 selectedItem
.$element
.removeClass( 'oo-ui-element-hidden' );
8944 * Horizontal bar layout of tools as icon buttons.
8947 * @extends OO.ui.ToolGroup
8950 * @param {OO.ui.Toolbar} toolbar
8951 * @param {Object} [config] Configuration options
8953 OO
.ui
.BarToolGroup
= function OoUiBarToolGroup( toolbar
, config
) {
8954 // Allow passing positional parameters inside the config object
8955 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
8957 toolbar
= config
.toolbar
;
8960 // Parent constructor
8961 OO
.ui
.BarToolGroup
.super.call( this, toolbar
, config
);
8964 this.$element
.addClass( 'oo-ui-barToolGroup' );
8969 OO
.inheritClass( OO
.ui
.BarToolGroup
, OO
.ui
.ToolGroup
);
8971 /* Static Properties */
8973 OO
.ui
.BarToolGroup
.static.titleTooltips
= true;
8975 OO
.ui
.BarToolGroup
.static.accelTooltips
= true;
8977 OO
.ui
.BarToolGroup
.static.name
= 'bar';
8980 * Popup list of tools with an icon and optional label.
8984 * @extends OO.ui.ToolGroup
8985 * @mixins OO.ui.IconElement
8986 * @mixins OO.ui.IndicatorElement
8987 * @mixins OO.ui.LabelElement
8988 * @mixins OO.ui.TitledElement
8989 * @mixins OO.ui.ClippableElement
8992 * @param {OO.ui.Toolbar} toolbar
8993 * @param {Object} [config] Configuration options
8994 * @cfg {string} [header] Text to display at the top of the pop-up
8996 OO
.ui
.PopupToolGroup
= function OoUiPopupToolGroup( toolbar
, config
) {
8997 // Allow passing positional parameters inside the config object
8998 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
9000 toolbar
= config
.toolbar
;
9003 // Configuration initialization
9004 config
= config
|| {};
9006 // Parent constructor
9007 OO
.ui
.PopupToolGroup
.super.call( this, toolbar
, config
);
9009 // Mixin constructors
9010 OO
.ui
.IconElement
.call( this, config
);
9011 OO
.ui
.IndicatorElement
.call( this, config
);
9012 OO
.ui
.LabelElement
.call( this, config
);
9013 OO
.ui
.TitledElement
.call( this, config
);
9014 OO
.ui
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
9017 this.active
= false;
9018 this.dragging
= false;
9019 this.onBlurHandler
= this.onBlur
.bind( this );
9020 this.$handle
= $( '<span>' );
9024 mousedown
: this.onHandlePointerDown
.bind( this ),
9025 mouseup
: this.onHandlePointerUp
.bind( this )
9030 .addClass( 'oo-ui-popupToolGroup-handle' )
9031 .append( this.$icon
, this.$label
, this.$indicator
);
9032 // If the pop-up should have a header, add it to the top of the toolGroup.
9033 // Note: If this feature is useful for other widgets, we could abstract it into an
9034 // OO.ui.HeaderedElement mixin constructor.
9035 if ( config
.header
!== undefined ) {
9037 .prepend( $( '<span>' )
9038 .addClass( 'oo-ui-popupToolGroup-header' )
9039 .text( config
.header
)
9043 .addClass( 'oo-ui-popupToolGroup' )
9044 .prepend( this.$handle
);
9049 OO
.inheritClass( OO
.ui
.PopupToolGroup
, OO
.ui
.ToolGroup
);
9050 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.IconElement
);
9051 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.IndicatorElement
);
9052 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.LabelElement
);
9053 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.TitledElement
);
9054 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.ClippableElement
);
9056 /* Static Properties */
9063 OO
.ui
.PopupToolGroup
.prototype.setDisabled = function () {
9065 OO
.ui
.PopupToolGroup
.super.prototype.setDisabled
.apply( this, arguments
);
9067 if ( this.isDisabled() && this.isElementAttached() ) {
9068 this.setActive( false );
9073 * Handle focus being lost.
9075 * The event is actually generated from a mouseup, so it is not a normal blur event object.
9077 * @param {jQuery.Event} e Mouse up event
9079 OO
.ui
.PopupToolGroup
.prototype.onBlur = function ( e
) {
9080 // Only deactivate when clicking outside the dropdown element
9081 if ( $( e
.target
).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element
[ 0 ] ) {
9082 this.setActive( false );
9089 OO
.ui
.PopupToolGroup
.prototype.onPointerUp = function ( e
) {
9090 // Only close toolgroup when a tool was actually selected
9091 if ( !this.isDisabled() && e
.which
=== 1 && this.pressed
&& this.pressed
=== this.getTargetTool( e
) ) {
9092 this.setActive( false );
9094 return OO
.ui
.PopupToolGroup
.super.prototype.onPointerUp
.call( this, e
);
9098 * Handle mouse up events.
9100 * @param {jQuery.Event} e Mouse up event
9102 OO
.ui
.PopupToolGroup
.prototype.onHandlePointerUp = function () {
9107 * Handle mouse down events.
9109 * @param {jQuery.Event} e Mouse down event
9111 OO
.ui
.PopupToolGroup
.prototype.onHandlePointerDown = function ( e
) {
9112 if ( !this.isDisabled() && e
.which
=== 1 ) {
9113 this.setActive( !this.active
);
9119 * Switch into active mode.
9121 * When active, mouseup events anywhere in the document will trigger deactivation.
9123 OO
.ui
.PopupToolGroup
.prototype.setActive = function ( value
) {
9125 if ( this.active
!== value
) {
9126 this.active
= value
;
9128 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler
, true );
9130 // Try anchoring the popup to the left first
9131 this.$element
.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
9132 this.toggleClipping( true );
9133 if ( this.isClippedHorizontally() ) {
9134 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
9135 this.toggleClipping( false );
9137 .removeClass( 'oo-ui-popupToolGroup-left' )
9138 .addClass( 'oo-ui-popupToolGroup-right' );
9139 this.toggleClipping( true );
9142 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler
, true );
9143 this.$element
.removeClass(
9144 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
9146 this.toggleClipping( false );
9152 * Drop down list layout of tools as labeled icon buttons.
9154 * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the
9155 * bottom of the main list. These are not automatically positioned at the bottom of the list; you
9156 * may want to use the 'promote' and 'demote' configuration options to achieve this.
9159 * @extends OO.ui.PopupToolGroup
9162 * @param {OO.ui.Toolbar} toolbar
9163 * @param {Object} [config] Configuration options
9164 * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always
9166 * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be
9167 * allowed to be collapsed.
9168 * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
9170 OO
.ui
.ListToolGroup
= function OoUiListToolGroup( toolbar
, config
) {
9171 // Allow passing positional parameters inside the config object
9172 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
9174 toolbar
= config
.toolbar
;
9177 // Configuration initialization
9178 config
= config
|| {};
9180 // Properties (must be set before parent constructor, which calls #populate)
9181 this.allowCollapse
= config
.allowCollapse
;
9182 this.forceExpand
= config
.forceExpand
;
9183 this.expanded
= config
.expanded
!== undefined ? config
.expanded
: false;
9184 this.collapsibleTools
= [];
9186 // Parent constructor
9187 OO
.ui
.ListToolGroup
.super.call( this, toolbar
, config
);
9190 this.$element
.addClass( 'oo-ui-listToolGroup' );
9195 OO
.inheritClass( OO
.ui
.ListToolGroup
, OO
.ui
.PopupToolGroup
);
9197 /* Static Properties */
9199 OO
.ui
.ListToolGroup
.static.accelTooltips
= true;
9201 OO
.ui
.ListToolGroup
.static.name
= 'list';
9208 OO
.ui
.ListToolGroup
.prototype.populate = function () {
9209 var i
, len
, allowCollapse
= [];
9211 OO
.ui
.ListToolGroup
.super.prototype.populate
.call( this );
9213 // Update the list of collapsible tools
9214 if ( this.allowCollapse
!== undefined ) {
9215 allowCollapse
= this.allowCollapse
;
9216 } else if ( this.forceExpand
!== undefined ) {
9217 allowCollapse
= OO
.simpleArrayDifference( Object
.keys( this.tools
), this.forceExpand
);
9220 this.collapsibleTools
= [];
9221 for ( i
= 0, len
= allowCollapse
.length
; i
< len
; i
++ ) {
9222 if ( this.tools
[ allowCollapse
[ i
] ] !== undefined ) {
9223 this.collapsibleTools
.push( this.tools
[ allowCollapse
[ i
] ] );
9227 // Keep at the end, even when tools are added
9228 this.$group
.append( this.getExpandCollapseTool().$element
);
9230 this.getExpandCollapseTool().toggle( this.collapsibleTools
.length
!== 0 );
9231 this.updateCollapsibleState();
9234 OO
.ui
.ListToolGroup
.prototype.getExpandCollapseTool = function () {
9235 if ( this.expandCollapseTool
=== undefined ) {
9236 var ExpandCollapseTool = function () {
9237 ExpandCollapseTool
.super.apply( this, arguments
);
9240 OO
.inheritClass( ExpandCollapseTool
, OO
.ui
.Tool
);
9242 ExpandCollapseTool
.prototype.onSelect = function () {
9243 this.toolGroup
.expanded
= !this.toolGroup
.expanded
;
9244 this.toolGroup
.updateCollapsibleState();
9245 this.setActive( false );
9247 ExpandCollapseTool
.prototype.onUpdateState = function () {
9248 // Do nothing. Tool interface requires an implementation of this function.
9251 ExpandCollapseTool
.static.name
= 'more-fewer';
9253 this.expandCollapseTool
= new ExpandCollapseTool( this );
9255 return this.expandCollapseTool
;
9261 OO
.ui
.ListToolGroup
.prototype.onPointerUp = function ( e
) {
9262 var ret
= OO
.ui
.ListToolGroup
.super.prototype.onPointerUp
.call( this, e
);
9264 // Do not close the popup when the user wants to show more/fewer tools
9265 if ( $( e
.target
).closest( '.oo-ui-tool-name-more-fewer' ).length
) {
9266 // Prevent the popup list from being hidden
9267 this.setActive( true );
9273 OO
.ui
.ListToolGroup
.prototype.updateCollapsibleState = function () {
9276 this.getExpandCollapseTool()
9277 .setIcon( this.expanded
? 'collapse' : 'expand' )
9278 .setTitle( OO
.ui
.msg( this.expanded
? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
9280 for ( i
= 0, len
= this.collapsibleTools
.length
; i
< len
; i
++ ) {
9281 this.collapsibleTools
[ i
].toggle( this.expanded
);
9286 * Drop down menu layout of tools as selectable menu items.
9289 * @extends OO.ui.PopupToolGroup
9292 * @param {OO.ui.Toolbar} toolbar
9293 * @param {Object} [config] Configuration options
9295 OO
.ui
.MenuToolGroup
= function OoUiMenuToolGroup( toolbar
, config
) {
9296 // Allow passing positional parameters inside the config object
9297 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
9299 toolbar
= config
.toolbar
;
9302 // Configuration initialization
9303 config
= config
|| {};
9305 // Parent constructor
9306 OO
.ui
.MenuToolGroup
.super.call( this, toolbar
, config
);
9309 this.toolbar
.connect( this, { updateState
: 'onUpdateState' } );
9312 this.$element
.addClass( 'oo-ui-menuToolGroup' );
9317 OO
.inheritClass( OO
.ui
.MenuToolGroup
, OO
.ui
.PopupToolGroup
);
9319 /* Static Properties */
9321 OO
.ui
.MenuToolGroup
.static.accelTooltips
= true;
9323 OO
.ui
.MenuToolGroup
.static.name
= 'menu';
9328 * Handle the toolbar state being updated.
9330 * When the state changes, the title of each active item in the menu will be joined together and
9331 * used as a label for the group. The label will be empty if none of the items are active.
9333 OO
.ui
.MenuToolGroup
.prototype.onUpdateState = function () {
9337 for ( name
in this.tools
) {
9338 if ( this.tools
[ name
].isActive() ) {
9339 labelTexts
.push( this.tools
[ name
].getTitle() );
9343 this.setLabel( labelTexts
.join( ', ' ) || ' ' );
9347 * Tool that shows a popup when selected.
9351 * @extends OO.ui.Tool
9352 * @mixins OO.ui.PopupElement
9355 * @param {OO.ui.ToolGroup} toolGroup
9356 * @param {Object} [config] Configuration options
9358 OO
.ui
.PopupTool
= function OoUiPopupTool( toolGroup
, config
) {
9359 // Allow passing positional parameters inside the config object
9360 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
9362 toolGroup
= config
.toolGroup
;
9365 // Parent constructor
9366 OO
.ui
.PopupTool
.super.call( this, toolGroup
, config
);
9368 // Mixin constructors
9369 OO
.ui
.PopupElement
.call( this, config
);
9373 .addClass( 'oo-ui-popupTool' )
9374 .append( this.popup
.$element
);
9379 OO
.inheritClass( OO
.ui
.PopupTool
, OO
.ui
.Tool
);
9380 OO
.mixinClass( OO
.ui
.PopupTool
, OO
.ui
.PopupElement
);
9385 * Handle the tool being selected.
9389 OO
.ui
.PopupTool
.prototype.onSelect = function () {
9390 if ( !this.isDisabled() ) {
9391 this.popup
.toggle();
9393 this.setActive( false );
9398 * Handle the toolbar state being updated.
9402 OO
.ui
.PopupTool
.prototype.onUpdateState = function () {
9403 this.setActive( false );
9407 * Tool that has a tool group inside. This is a bad workaround for the lack of proper hierarchical
9408 * menus in toolbars (T74159).
9412 * @extends OO.ui.Tool
9415 * @param {OO.ui.ToolGroup} toolGroup
9416 * @param {Object} [config] Configuration options
9418 OO
.ui
.ToolGroupTool
= function OoUiToolGroupTool( toolGroup
, config
) {
9419 // Allow passing positional parameters inside the config object
9420 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
9422 toolGroup
= config
.toolGroup
;
9425 // Parent constructor
9426 OO
.ui
.ToolGroupTool
.super.call( this, toolGroup
, config
);
9429 this.innerToolGroup
= this.createGroup( this.constructor.static.groupConfig
);
9432 this.$link
.remove();
9434 .addClass( 'oo-ui-toolGroupTool' )
9435 .append( this.innerToolGroup
.$element
);
9440 OO
.inheritClass( OO
.ui
.ToolGroupTool
, OO
.ui
.Tool
);
9442 /* Static Properties */
9445 * Tool group configuration. See OO.ui.Toolbar#setup for the accepted values.
9447 * @property {Object.<string,Array>}
9449 OO
.ui
.ToolGroupTool
.static.groupConfig
= {};
9454 * Handle the tool being selected.
9458 OO
.ui
.ToolGroupTool
.prototype.onSelect = function () {
9459 this.innerToolGroup
.setActive( !this.innerToolGroup
.active
);
9464 * Handle the toolbar state being updated.
9468 OO
.ui
.ToolGroupTool
.prototype.onUpdateState = function () {
9469 this.setActive( false );
9473 * Build a OO.ui.ToolGroup from the configuration.
9475 * @param {Object.<string,Array>} group Tool group configuration. See OO.ui.Toolbar#setup for the
9477 * @return {OO.ui.ListToolGroup}
9479 OO
.ui
.ToolGroupTool
.prototype.createGroup = function ( group
) {
9480 if ( group
.include
=== '*' ) {
9481 // Apply defaults to catch-all groups
9482 if ( group
.label
=== undefined ) {
9483 group
.label
= OO
.ui
.msg( 'ooui-toolbar-more' );
9487 return this.toolbar
.getToolGroupFactory().create( 'list', this.toolbar
, group
);
9491 * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
9493 * Use together with OO.ui.ItemWidget to make disabled state inheritable.
9498 * @extends OO.ui.GroupElement
9501 * @param {Object} [config] Configuration options
9503 OO
.ui
.GroupWidget
= function OoUiGroupWidget( config
) {
9504 // Parent constructor
9505 OO
.ui
.GroupWidget
.super.call( this, config
);
9510 OO
.inheritClass( OO
.ui
.GroupWidget
, OO
.ui
.GroupElement
);
9515 * Set the disabled state of the widget.
9517 * This will also update the disabled state of child widgets.
9519 * @param {boolean} disabled Disable widget
9522 OO
.ui
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
9526 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
9527 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
9529 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
9531 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
9532 this.items
[ i
].updateDisabled();
9540 * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
9542 * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
9543 * allows bidirectional communication.
9545 * Use together with OO.ui.GroupWidget to make disabled state inheritable.
9553 OO
.ui
.ItemWidget
= function OoUiItemWidget() {
9560 * Check if widget is disabled.
9562 * Checks parent if present, making disabled state inheritable.
9564 * @return {boolean} Widget is disabled
9566 OO
.ui
.ItemWidget
.prototype.isDisabled = function () {
9567 return this.disabled
||
9568 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
9572 * Set group element is in.
9574 * @param {OO.ui.GroupElement|null} group Group element, null if none
9577 OO
.ui
.ItemWidget
.prototype.setElementGroup = function ( group
) {
9579 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
9580 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
9582 // Initialize item disabled states
9583 this.updateDisabled();
9589 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
9590 * Controls include moving items up and down, removing items, and adding different kinds of items.
9591 * ####Currently, this class is only used by {@link OO.ui.BookletLayout BookletLayouts}.####
9594 * @extends OO.ui.Widget
9595 * @mixins OO.ui.GroupElement
9596 * @mixins OO.ui.IconElement
9599 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
9600 * @param {Object} [config] Configuration options
9602 OO
.ui
.OutlineControlsWidget
= function OoUiOutlineControlsWidget( outline
, config
) {
9603 // Allow passing positional parameters inside the config object
9604 if ( OO
.isPlainObject( outline
) && config
=== undefined ) {
9606 outline
= config
.outline
;
9609 // Configuration initialization
9610 config
= $.extend( { icon
: 'add' }, config
);
9612 // Parent constructor
9613 OO
.ui
.OutlineControlsWidget
.super.call( this, config
);
9615 // Mixin constructors
9616 OO
.ui
.GroupElement
.call( this, config
);
9617 OO
.ui
.IconElement
.call( this, config
);
9620 this.outline
= outline
;
9621 this.$movers
= $( '<div>' );
9622 this.upButton
= new OO
.ui
.ButtonWidget( {
9625 title
: OO
.ui
.msg( 'ooui-outline-control-move-up' )
9627 this.downButton
= new OO
.ui
.ButtonWidget( {
9630 title
: OO
.ui
.msg( 'ooui-outline-control-move-down' )
9632 this.removeButton
= new OO
.ui
.ButtonWidget( {
9635 title
: OO
.ui
.msg( 'ooui-outline-control-remove' )
9639 outline
.connect( this, {
9640 select
: 'onOutlineChange',
9641 add
: 'onOutlineChange',
9642 remove
: 'onOutlineChange'
9644 this.upButton
.connect( this, { click
: [ 'emit', 'move', -1 ] } );
9645 this.downButton
.connect( this, { click
: [ 'emit', 'move', 1 ] } );
9646 this.removeButton
.connect( this, { click
: [ 'emit', 'remove' ] } );
9649 this.$element
.addClass( 'oo-ui-outlineControlsWidget' );
9650 this.$group
.addClass( 'oo-ui-outlineControlsWidget-items' );
9652 .addClass( 'oo-ui-outlineControlsWidget-movers' )
9653 .append( this.removeButton
.$element
, this.upButton
.$element
, this.downButton
.$element
);
9654 this.$element
.append( this.$icon
, this.$group
, this.$movers
);
9659 OO
.inheritClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.Widget
);
9660 OO
.mixinClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.GroupElement
);
9661 OO
.mixinClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.IconElement
);
9667 * @param {number} places Number of places to move
9679 * Handle outline change events.
9681 OO
.ui
.OutlineControlsWidget
.prototype.onOutlineChange = function () {
9682 var i
, len
, firstMovable
, lastMovable
,
9683 items
= this.outline
.getItems(),
9684 selectedItem
= this.outline
.getSelectedItem(),
9685 movable
= selectedItem
&& selectedItem
.isMovable(),
9686 removable
= selectedItem
&& selectedItem
.isRemovable();
9691 while ( ++i
< len
) {
9692 if ( items
[ i
].isMovable() ) {
9693 firstMovable
= items
[ i
];
9699 if ( items
[ i
].isMovable() ) {
9700 lastMovable
= items
[ i
];
9705 this.upButton
.setDisabled( !movable
|| selectedItem
=== firstMovable
);
9706 this.downButton
.setDisabled( !movable
|| selectedItem
=== lastMovable
);
9707 this.removeButton
.setDisabled( !removable
);
9711 * ToggleWidget is mixed into other classes to create widgets with an on/off state.
9712 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
9718 * @param {Object} [config] Configuration options
9719 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
9720 * By default, the toggle is in the 'off' state.
9722 OO
.ui
.ToggleWidget
= function OoUiToggleWidget( config
) {
9723 // Configuration initialization
9724 config
= config
|| {};
9730 this.$element
.addClass( 'oo-ui-toggleWidget' );
9731 this.setValue( !!config
.value
);
9739 * A change event is emitted when the on/off state of the toggle changes.
9741 * @param {boolean} value Value representing the new state of the toggle
9747 * Get the value representing the toggle’s state.
9749 * @return {boolean} The on/off state of the toggle
9751 OO
.ui
.ToggleWidget
.prototype.getValue = function () {
9756 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
9758 * @param {boolean} value The state of the toggle
9762 OO
.ui
.ToggleWidget
.prototype.setValue = function ( value
) {
9764 if ( this.value
!== value
) {
9766 this.emit( 'change', value
);
9767 this.$element
.toggleClass( 'oo-ui-toggleWidget-on', value
);
9768 this.$element
.toggleClass( 'oo-ui-toggleWidget-off', !value
);
9769 this.$element
.attr( 'aria-checked', value
.toString() );
9775 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
9776 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
9777 * removed, and cleared from the group.
9780 * // Example: A ButtonGroupWidget with two buttons
9781 * var button1 = new OO.ui.PopupButtonWidget( {
9782 * label : 'Select a category',
9785 * $content: $( '<p>List of categories...</p>' ),
9790 * var button2 = new OO.ui.ButtonWidget( {
9791 * label : 'Add item'
9793 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
9794 * items: [button1, button2]
9796 * $('body').append(buttonGroup.$element);
9799 * @extends OO.ui.Widget
9800 * @mixins OO.ui.GroupElement
9803 * @param {Object} [config] Configuration options
9804 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
9806 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
9807 // Configuration initialization
9808 config
= config
|| {};
9810 // Parent constructor
9811 OO
.ui
.ButtonGroupWidget
.super.call( this, config
);
9813 // Mixin constructors
9814 OO
.ui
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
9817 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
9818 if ( Array
.isArray( config
.items
) ) {
9819 this.addItems( config
.items
);
9825 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
9826 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.GroupElement
);
9829 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
9830 * feels, and functionality can be customized via the class’s configuration options
9831 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
9834 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
9837 * // A button widget
9838 * var button = new OO.ui.ButtonWidget( {
9839 * label : 'Button with Icon',
9841 * iconTitle : 'Remove'
9843 * $( 'body' ).append( button.$element );
9845 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
9848 * @extends OO.ui.Widget
9849 * @mixins OO.ui.ButtonElement
9850 * @mixins OO.ui.IconElement
9851 * @mixins OO.ui.IndicatorElement
9852 * @mixins OO.ui.LabelElement
9853 * @mixins OO.ui.TitledElement
9854 * @mixins OO.ui.FlaggedElement
9855 * @mixins OO.ui.TabIndexedElement
9858 * @param {Object} [config] Configuration options
9859 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
9860 * @cfg {string} [target] The frame or window in which to open the hyperlink.
9861 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
9863 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
9864 // Configuration initialization
9865 // FIXME: The `nofollow` alias is deprecated and will be removed (T89767)
9866 config
= $.extend( { noFollow
: config
&& config
.nofollow
}, config
);
9868 // Parent constructor
9869 OO
.ui
.ButtonWidget
.super.call( this, config
);
9871 // Mixin constructors
9872 OO
.ui
.ButtonElement
.call( this, config
);
9873 OO
.ui
.IconElement
.call( this, config
);
9874 OO
.ui
.IndicatorElement
.call( this, config
);
9875 OO
.ui
.LabelElement
.call( this, config
);
9876 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
9877 OO
.ui
.FlaggedElement
.call( this, config
);
9878 OO
.ui
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
9883 this.noFollow
= false;
9884 this.isHyperlink
= false;
9887 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
9889 .addClass( 'oo-ui-buttonWidget' )
9890 .append( this.$button
);
9891 this.setHref( config
.href
);
9892 this.setTarget( config
.target
);
9893 this.setNoFollow( config
.noFollow
);
9898 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
9899 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.ButtonElement
);
9900 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.IconElement
);
9901 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.IndicatorElement
);
9902 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.LabelElement
);
9903 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.TitledElement
);
9904 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.FlaggedElement
);
9905 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.TabIndexedElement
);
9912 OO
.ui
.ButtonWidget
.prototype.onMouseDown = function ( e
) {
9913 if ( !this.isDisabled() ) {
9914 // Remove the tab-index while the button is down to prevent the button from stealing focus
9915 this.$button
.removeAttr( 'tabindex' );
9918 return OO
.ui
.ButtonElement
.prototype.onMouseDown
.call( this, e
);
9924 OO
.ui
.ButtonWidget
.prototype.onMouseUp = function ( e
) {
9925 if ( !this.isDisabled() ) {
9926 // Restore the tab-index after the button is up to restore the button's accessibility
9927 this.$button
.attr( 'tabindex', this.tabIndex
);
9930 return OO
.ui
.ButtonElement
.prototype.onMouseUp
.call( this, e
);
9936 OO
.ui
.ButtonWidget
.prototype.onClick = function ( e
) {
9937 var ret
= OO
.ui
.ButtonElement
.prototype.onClick
.call( this, e
);
9938 if ( this.isHyperlink
) {
9947 OO
.ui
.ButtonWidget
.prototype.onKeyPress = function ( e
) {
9948 var ret
= OO
.ui
.ButtonElement
.prototype.onKeyPress
.call( this, e
);
9949 if ( this.isHyperlink
) {
9956 * Get hyperlink location.
9958 * @return {string} Hyperlink location
9960 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
9965 * Get hyperlink target.
9967 * @return {string} Hyperlink target
9969 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
9974 * Get search engine traversal hint.
9976 * @return {boolean} Whether search engines should avoid traversing this hyperlink
9978 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
9979 return this.noFollow
;
9983 * Set hyperlink location.
9985 * @param {string|null} href Hyperlink location, null to remove
9987 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
9988 href
= typeof href
=== 'string' ? href
: null;
9990 if ( href
!== this.href
) {
9992 if ( href
!== null ) {
9993 this.$button
.attr( 'href', href
);
9994 this.isHyperlink
= true;
9996 this.$button
.removeAttr( 'href' );
9997 this.isHyperlink
= false;
10005 * Set hyperlink target.
10007 * @param {string|null} target Hyperlink target, null to remove
10009 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
10010 target
= typeof target
=== 'string' ? target
: null;
10012 if ( target
!== this.target
) {
10013 this.target
= target
;
10014 if ( target
!== null ) {
10015 this.$button
.attr( 'target', target
);
10017 this.$button
.removeAttr( 'target' );
10025 * Set search engine traversal hint.
10027 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
10029 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
10030 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
10032 if ( noFollow
!== this.noFollow
) {
10033 this.noFollow
= noFollow
;
10035 this.$button
.attr( 'rel', 'nofollow' );
10037 this.$button
.removeAttr( 'rel' );
10045 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
10046 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
10049 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
10050 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
10053 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
10056 * @extends OO.ui.ButtonWidget
10057 * @mixins OO.ui.PendingElement
10060 * @param {Object} [config] Configuration options
10061 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
10062 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
10063 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
10064 * for more information about setting modes.
10065 * @cfg {boolean} [framed=false] Render the action button with a frame
10067 OO
.ui
.ActionWidget
= function OoUiActionWidget( config
) {
10068 // Configuration initialization
10069 config
= $.extend( { framed
: false }, config
);
10071 // Parent constructor
10072 OO
.ui
.ActionWidget
.super.call( this, config
);
10074 // Mixin constructors
10075 OO
.ui
.PendingElement
.call( this, config
);
10078 this.action
= config
.action
|| '';
10079 this.modes
= config
.modes
|| [];
10084 this.$element
.addClass( 'oo-ui-actionWidget' );
10089 OO
.inheritClass( OO
.ui
.ActionWidget
, OO
.ui
.ButtonWidget
);
10090 OO
.mixinClass( OO
.ui
.ActionWidget
, OO
.ui
.PendingElement
);
10095 * A resize event is emitted when the size of the widget changes.
10103 * Check if the action is configured to be available in the specified `mode`.
10105 * @param {string} mode Name of mode
10106 * @return {boolean} The action is configured with the mode
10108 OO
.ui
.ActionWidget
.prototype.hasMode = function ( mode
) {
10109 return this.modes
.indexOf( mode
) !== -1;
10113 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
10117 OO
.ui
.ActionWidget
.prototype.getAction = function () {
10118 return this.action
;
10122 * Get the symbolic name of the mode or modes for which the action is configured to be available.
10124 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
10125 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
10128 * @return {string[]}
10130 OO
.ui
.ActionWidget
.prototype.getModes = function () {
10131 return this.modes
.slice();
10135 * Emit a resize event if the size has changed.
10140 OO
.ui
.ActionWidget
.prototype.propagateResize = function () {
10143 if ( this.isElementAttached() ) {
10144 width
= this.$element
.width();
10145 height
= this.$element
.height();
10147 if ( width
!== this.width
|| height
!== this.height
) {
10148 this.width
= width
;
10149 this.height
= height
;
10150 this.emit( 'resize' );
10160 OO
.ui
.ActionWidget
.prototype.setIcon = function () {
10162 OO
.ui
.IconElement
.prototype.setIcon
.apply( this, arguments
);
10163 this.propagateResize();
10171 OO
.ui
.ActionWidget
.prototype.setLabel = function () {
10173 OO
.ui
.LabelElement
.prototype.setLabel
.apply( this, arguments
);
10174 this.propagateResize();
10182 OO
.ui
.ActionWidget
.prototype.setFlags = function () {
10184 OO
.ui
.FlaggedElement
.prototype.setFlags
.apply( this, arguments
);
10185 this.propagateResize();
10193 OO
.ui
.ActionWidget
.prototype.clearFlags = function () {
10195 OO
.ui
.FlaggedElement
.prototype.clearFlags
.apply( this, arguments
);
10196 this.propagateResize();
10202 * Toggle the visibility of the action button.
10204 * @param {boolean} [show] Show button, omit to toggle visibility
10207 OO
.ui
.ActionWidget
.prototype.toggle = function () {
10209 OO
.ui
.ActionWidget
.super.prototype.toggle
.apply( this, arguments
);
10210 this.propagateResize();
10216 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
10217 * which is used to display additional information or options.
10220 * // Example of a popup button.
10221 * var popupButton = new OO.ui.PopupButtonWidget( {
10222 * label: 'Popup button with options',
10225 * $content: $( '<p>Additional options here.</p>' ),
10230 * // Append the button to the DOM.
10231 * $( 'body' ).append( popupButton.$element );
10234 * @extends OO.ui.ButtonWidget
10235 * @mixins OO.ui.PopupElement
10238 * @param {Object} [config] Configuration options
10240 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
10241 // Parent constructor
10242 OO
.ui
.PopupButtonWidget
.super.call( this, config
);
10244 // Mixin constructors
10245 OO
.ui
.PopupElement
.call( this, config
);
10248 this.connect( this, { click
: 'onAction' } );
10252 .addClass( 'oo-ui-popupButtonWidget' )
10253 .attr( 'aria-haspopup', 'true' )
10254 .append( this.popup
.$element
);
10259 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
10260 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.PopupElement
);
10265 * Handle the button action being triggered.
10269 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
10270 this.popup
.toggle();
10274 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
10275 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
10276 * configured with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators},
10277 * {@link OO.ui.TitledElement titles}, {@link OO.ui.FlaggedElement styling flags},
10278 * and {@link OO.ui.LabelElement labels}. Please see
10279 * the [OOjs UI documentation][1] on MediaWiki for more information.
10282 * // Toggle buttons in the 'off' and 'on' state.
10283 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
10284 * label: 'Toggle Button off'
10286 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
10287 * label: 'Toggle Button on',
10290 * // Append the buttons to the DOM.
10291 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
10293 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
10296 * @extends OO.ui.ButtonWidget
10297 * @mixins OO.ui.ToggleWidget
10300 * @param {Object} [config] Configuration options
10301 * @cfg {boolean} [value=false] The toggle button’s initial on/off
10302 * state. By default, the button is in the 'off' state.
10304 OO
.ui
.ToggleButtonWidget
= function OoUiToggleButtonWidget( config
) {
10305 // Configuration initialization
10306 config
= config
|| {};
10308 // Parent constructor
10309 OO
.ui
.ToggleButtonWidget
.super.call( this, config
);
10311 // Mixin constructors
10312 OO
.ui
.ToggleWidget
.call( this, config
);
10315 this.connect( this, { click
: 'onAction' } );
10318 this.$element
.addClass( 'oo-ui-toggleButtonWidget' );
10323 OO
.inheritClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.ButtonWidget
);
10324 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.ToggleWidget
);
10331 * Handle the button action being triggered.
10333 OO
.ui
.ToggleButtonWidget
.prototype.onAction = function () {
10334 this.setValue( !this.value
);
10340 OO
.ui
.ToggleButtonWidget
.prototype.setValue = function ( value
) {
10342 if ( value
!== this.value
) {
10343 this.$button
.attr( 'aria-pressed', value
.toString() );
10344 this.setActive( value
);
10347 // Parent method (from mixin)
10348 OO
.ui
.ToggleWidget
.prototype.setValue
.call( this, value
);
10354 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
10355 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
10356 * users can interact with it.
10359 * // Example: A DropdownWidget with a menu that contains three options
10360 * var dropDown=new OO.ui.DropdownWidget( {
10361 * label: 'Dropdown menu: Select a menu option',
10364 * new OO.ui.MenuOptionWidget( {
10368 * new OO.ui.MenuOptionWidget( {
10372 * new OO.ui.MenuOptionWidget( {
10380 * $('body').append(dropDown.$element);
10382 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
10384 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10387 * @extends OO.ui.Widget
10388 * @mixins OO.ui.IconElement
10389 * @mixins OO.ui.IndicatorElement
10390 * @mixins OO.ui.LabelElement
10391 * @mixins OO.ui.TitledElement
10392 * @mixins OO.ui.TabIndexedElement
10395 * @param {Object} [config] Configuration options
10396 * @cfg {Object} [menu] Configuration options to pass to menu widget
10398 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
10399 // Configuration initialization
10400 config
= $.extend( { indicator
: 'down' }, config
);
10402 // Parent constructor
10403 OO
.ui
.DropdownWidget
.super.call( this, config
);
10405 // Properties (must be set before TabIndexedElement constructor call)
10406 this.$handle
= this.$( '<span>' );
10408 // Mixin constructors
10409 OO
.ui
.IconElement
.call( this, config
);
10410 OO
.ui
.IndicatorElement
.call( this, config
);
10411 OO
.ui
.LabelElement
.call( this, config
);
10412 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
10413 OO
.ui
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
10416 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( { widget
: this }, config
.menu
) );
10420 click
: this.onClick
.bind( this ),
10421 keypress
: this.onKeyPress
.bind( this )
10423 this.menu
.connect( this, { select
: 'onMenuSelect' } );
10427 .addClass( 'oo-ui-dropdownWidget-handle' )
10428 .append( this.$icon
, this.$label
, this.$indicator
);
10430 .addClass( 'oo-ui-dropdownWidget' )
10431 .append( this.$handle
, this.menu
.$element
);
10436 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
10437 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.IconElement
);
10438 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.IndicatorElement
);
10439 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.LabelElement
);
10440 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.TitledElement
);
10441 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.TabIndexedElement
);
10448 * @return {OO.ui.MenuSelectWidget} Menu of widget
10450 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
10455 * Handles menu select events.
10458 * @param {OO.ui.MenuOptionWidget} item Selected menu item
10460 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
10467 selectedLabel
= item
.getLabel();
10469 // If the label is a DOM element, clone it, because setLabel will append() it
10470 if ( selectedLabel
instanceof jQuery
) {
10471 selectedLabel
= selectedLabel
.clone();
10474 this.setLabel( selectedLabel
);
10478 * Handle mouse click events.
10481 * @param {jQuery.Event} e Mouse click event
10483 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
10484 if ( !this.isDisabled() && e
.which
=== 1 ) {
10485 this.menu
.toggle();
10491 * Handle key press events.
10494 * @param {jQuery.Event} e Key press event
10496 OO
.ui
.DropdownWidget
.prototype.onKeyPress = function ( e
) {
10497 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
10498 this.menu
.toggle();
10504 * IconWidget is a generic widget for {@link OO.ui.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
10505 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
10506 * for a list of icons included in the library.
10509 * // An icon widget with a label
10510 * var myIcon = new OO.ui.IconWidget({
10512 * iconTitle: 'Help'
10514 * // Create a label.
10515 * var iconLabel = new OO.ui.LabelWidget({
10518 * $('body').append(myIcon.$element, iconLabel.$element);
10520 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
10523 * @extends OO.ui.Widget
10524 * @mixins OO.ui.IconElement
10525 * @mixins OO.ui.TitledElement
10528 * @param {Object} [config] Configuration options
10530 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
10531 // Configuration initialization
10532 config
= config
|| {};
10534 // Parent constructor
10535 OO
.ui
.IconWidget
.super.call( this, config
);
10537 // Mixin constructors
10538 OO
.ui
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
10539 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
10542 this.$element
.addClass( 'oo-ui-iconWidget' );
10547 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
10548 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.IconElement
);
10549 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.TitledElement
);
10551 /* Static Properties */
10553 OO
.ui
.IconWidget
.static.tagName
= 'span';
10556 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
10557 * attention to the status of an item or to clarify the function of a control. For a list of
10558 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
10561 * // Example of an indicator widget
10562 * var indicator1 = new OO.ui.IndicatorWidget( {
10563 * indicator: 'alert'
10566 * // Create a fieldset layout to add a label
10567 * var fieldset = new OO.ui.FieldsetLayout( );
10568 * fieldset.addItems( [
10569 * new OO.ui.FieldLayout( indicator1, {label: 'An alert indicator:'} )
10571 * $( 'body' ).append( fieldset.$element );
10573 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
10576 * @extends OO.ui.Widget
10577 * @mixins OO.ui.IndicatorElement
10578 * @mixins OO.ui.TitledElement
10581 * @param {Object} [config] Configuration options
10583 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
10584 // Configuration initialization
10585 config
= config
|| {};
10587 // Parent constructor
10588 OO
.ui
.IndicatorWidget
.super.call( this, config
);
10590 // Mixin constructors
10591 OO
.ui
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
10592 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
10595 this.$element
.addClass( 'oo-ui-indicatorWidget' );
10600 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
10601 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.IndicatorElement
);
10602 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.TitledElement
);
10604 /* Static Properties */
10606 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
10609 * InputWidget is the base class for all input widgets, which
10610 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
10611 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
10612 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10614 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10618 * @extends OO.ui.Widget
10619 * @mixins OO.ui.FlaggedElement
10620 * @mixins OO.ui.TabIndexedElement
10623 * @param {Object} [config] Configuration options
10624 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
10625 * @cfg {string} [value=''] The value of the input.
10626 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
10627 * before it is accepted.
10629 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
10630 // Configuration initialization
10631 config
= config
|| {};
10633 // Parent constructor
10634 OO
.ui
.InputWidget
.super.call( this, config
);
10637 this.$input
= this.getInputElement( config
);
10639 this.inputFilter
= config
.inputFilter
;
10641 // Mixin constructors
10642 OO
.ui
.FlaggedElement
.call( this, config
);
10643 OO
.ui
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
10646 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
10650 .attr( 'name', config
.name
)
10651 .prop( 'disabled', this.isDisabled() );
10652 this.$element
.addClass( 'oo-ui-inputWidget' ).append( this.$input
, $( '<span>' ) );
10653 this.setValue( config
.value
);
10658 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
10659 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.FlaggedElement
);
10660 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.TabIndexedElement
);
10667 * A change event is emitted when the value of the input changes.
10669 * @param {string} value
10675 * Get input element.
10677 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
10678 * different circumstances. The element must have a `value` property (like form elements).
10681 * @param {Object} config Configuration options
10682 * @return {jQuery} Input element
10684 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
10685 return $( '<input>' );
10689 * Handle potentially value-changing events.
10692 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
10694 OO
.ui
.InputWidget
.prototype.onEdit = function () {
10696 if ( !this.isDisabled() ) {
10697 // Allow the stack to clear so the value will be updated
10698 setTimeout( function () {
10699 widget
.setValue( widget
.$input
.val() );
10705 * Get the value of the input.
10707 * @return {string} Input value
10709 OO
.ui
.InputWidget
.prototype.getValue = function () {
10710 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10711 // it, and we won't know unless they're kind enough to trigger a 'change' event.
10712 var value
= this.$input
.val();
10713 if ( this.value
!== value
) {
10714 this.setValue( value
);
10720 * Set the direction of the input, either RTL (right-to-left) or LTR (left-to-right).
10722 * @param {boolean} isRTL
10723 * Direction is right-to-left
10725 OO
.ui
.InputWidget
.prototype.setRTL = function ( isRTL
) {
10726 this.$input
.prop( 'dir', isRTL
? 'rtl' : 'ltr' );
10730 * Set the value of the input.
10732 * @param {string} value New value
10736 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
10737 value
= this.cleanUpValue( value
);
10738 // Update the DOM if it has changed. Note that with cleanUpValue, it
10739 // is possible for the DOM value to change without this.value changing.
10740 if ( this.$input
.val() !== value
) {
10741 this.$input
.val( value
);
10743 if ( this.value
!== value
) {
10744 this.value
= value
;
10745 this.emit( 'change', this.value
);
10751 * Clean up incoming value.
10753 * Ensures value is a string, and converts undefined and null to empty string.
10756 * @param {string} value Original value
10757 * @return {string} Cleaned up value
10759 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
10760 if ( value
=== undefined || value
=== null ) {
10762 } else if ( this.inputFilter
) {
10763 return this.inputFilter( String( value
) );
10765 return String( value
);
10770 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
10771 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
10774 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
10775 if ( !this.isDisabled() ) {
10776 if ( this.$input
.is( ':checkbox, :radio' ) ) {
10777 this.$input
.click();
10779 if ( this.$input
.is( ':input' ) ) {
10780 this.$input
[ 0 ].focus();
10788 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
10789 OO
.ui
.InputWidget
.super.prototype.setDisabled
.call( this, state
);
10790 if ( this.$input
) {
10791 this.$input
.prop( 'disabled', this.isDisabled() );
10801 OO
.ui
.InputWidget
.prototype.focus = function () {
10802 this.$input
[ 0 ].focus();
10811 OO
.ui
.InputWidget
.prototype.blur = function () {
10812 this.$input
[ 0 ].blur();
10817 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
10818 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
10819 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
10820 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
10821 * [OOjs UI documentation on MediaWiki] [1] for more information.
10824 * // A ButtonInputWidget rendered as an HTML button, the default.
10825 * var button = new OO.ui.ButtonInputWidget( {
10826 * label: 'Input button',
10830 * $( 'body' ).append( button.$element );
10832 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
10835 * @extends OO.ui.InputWidget
10836 * @mixins OO.ui.ButtonElement
10837 * @mixins OO.ui.IconElement
10838 * @mixins OO.ui.IndicatorElement
10839 * @mixins OO.ui.LabelElement
10840 * @mixins OO.ui.TitledElement
10841 * @mixins OO.ui.FlaggedElement
10844 * @param {Object} [config] Configuration options
10845 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
10846 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
10847 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
10848 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
10849 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
10851 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
10852 // Configuration initialization
10853 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
10855 // Properties (must be set before parent constructor, which calls #setValue)
10856 this.useInputTag
= config
.useInputTag
;
10858 // Parent constructor
10859 OO
.ui
.ButtonInputWidget
.super.call( this, config
);
10861 // Mixin constructors
10862 OO
.ui
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
10863 OO
.ui
.IconElement
.call( this, config
);
10864 OO
.ui
.IndicatorElement
.call( this, config
);
10865 OO
.ui
.LabelElement
.call( this, config
);
10866 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
10867 OO
.ui
.FlaggedElement
.call( this, config
);
10870 if ( !config
.useInputTag
) {
10871 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
10873 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
10878 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
10879 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.ButtonElement
);
10880 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.IconElement
);
10881 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.IndicatorElement
);
10882 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.LabelElement
);
10883 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.TitledElement
);
10884 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.FlaggedElement
);
10892 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
10893 var html
= '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + config
.type
+ '">';
10900 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
10902 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
10903 * text, or `null` for no label
10906 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
10907 OO
.ui
.LabelElement
.prototype.setLabel
.call( this, label
);
10909 if ( this.useInputTag
) {
10910 if ( typeof label
=== 'function' ) {
10911 label
= OO
.ui
.resolveMsg( label
);
10913 if ( label
instanceof jQuery
) {
10914 label
= label
.text();
10919 this.$input
.val( label
);
10926 * Set the value of the input.
10928 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
10929 * they do not support {@link #value values}.
10931 * @param {string} value New value
10934 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
10935 if ( !this.useInputTag
) {
10936 OO
.ui
.ButtonInputWidget
.super.prototype.setValue
.call( this, value
);
10942 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
10943 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
10944 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
10945 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
10948 * // An example of selected, unselected, and disabled checkbox inputs
10949 * var checkbox1=new OO.ui.CheckboxInputWidget({
10953 * var checkbox2=new OO.ui.CheckboxInputWidget({
10956 * var checkbox3=new OO.ui.CheckboxInputWidget( {
10960 * // Create a fieldset layout with fields for each checkbox.
10961 * var fieldset = new OO.ui.FieldsetLayout( {
10962 * label: 'Checkboxes'
10964 * fieldset.addItems( [
10965 * new OO.ui.FieldLayout( checkbox1, {label : 'Selected checkbox', align : 'inline'}),
10966 * new OO.ui.FieldLayout( checkbox2, {label : 'Unselected checkbox', align : 'inline'}),
10967 * new OO.ui.FieldLayout( checkbox3, {label : 'Disabled checkbox', align : 'inline'}),
10969 * $( 'body' ).append( fieldset.$element );
10971 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10974 * @extends OO.ui.InputWidget
10977 * @param {Object} [config] Configuration options
10978 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
10980 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
10981 // Configuration initialization
10982 config
= config
|| {};
10984 // Parent constructor
10985 OO
.ui
.CheckboxInputWidget
.super.call( this, config
);
10988 this.$element
.addClass( 'oo-ui-checkboxInputWidget' );
10989 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
10994 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
11002 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
11003 return $( '<input type="checkbox" />' );
11009 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
11011 if ( !this.isDisabled() ) {
11012 // Allow the stack to clear so the value will be updated
11013 setTimeout( function () {
11014 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
11020 * Set selection state of this checkbox.
11022 * @param {boolean} state `true` for selected
11025 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
11027 if ( this.selected
!== state
) {
11028 this.selected
= state
;
11029 this.$input
.prop( 'checked', this.selected
);
11030 this.emit( 'change', this.selected
);
11036 * Check if this checkbox is selected.
11038 * @return {boolean} Checkbox is selected
11040 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
11041 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
11042 // it, and we won't know unless they're kind enough to trigger a 'change' event.
11043 var selected
= this.$input
.prop( 'checked' );
11044 if ( this.selected
!== selected
) {
11045 this.setSelected( selected
);
11047 return this.selected
;
11051 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
11052 * within a {@link OO.ui.FormLayout form}. The selected value is synchronized with the value
11053 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
11054 * more information about input widgets.
11057 * // Example: A DropdownInputWidget with three options
11058 * var dropDown=new OO.ui.DropdownInputWidget( {
11059 * label: 'Dropdown menu: Select a menu option',
11061 * { data: 'a', label: 'First' } ,
11062 * { data: 'b', label: 'Second'} ,
11063 * { data: 'c', label: 'Third' }
11066 * $('body').append(dropDown.$element);
11068 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11071 * @extends OO.ui.InputWidget
11074 * @param {Object} [config] Configuration options
11075 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11077 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
11078 // Configuration initialization
11079 config
= config
|| {};
11081 // Properties (must be done before parent constructor which calls #setDisabled)
11082 this.dropdownWidget
= new OO
.ui
.DropdownWidget();
11084 // Parent constructor
11085 OO
.ui
.DropdownInputWidget
.super.call( this, config
);
11088 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
11091 this.setOptions( config
.options
|| [] );
11093 .addClass( 'oo-ui-dropdownInputWidget' )
11094 .append( this.dropdownWidget
.$element
);
11099 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
11107 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
11108 return $( '<input type="hidden">' );
11112 * Handles menu select events.
11115 * @param {OO.ui.MenuOptionWidget} item Selected menu item
11117 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
11118 this.setValue( item
.getData() );
11124 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
11125 var item
= this.dropdownWidget
.getMenu().getItemFromData( value
);
11127 this.dropdownWidget
.getMenu().selectItem( item
);
11129 OO
.ui
.DropdownInputWidget
.super.prototype.setValue
.call( this, value
);
11136 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
11137 this.dropdownWidget
.setDisabled( state
);
11138 OO
.ui
.DropdownInputWidget
.super.prototype.setDisabled
.call( this, state
);
11143 * Set the options available for this input.
11145 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11148 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
11149 var value
= this.getValue();
11151 // Rebuild the dropdown menu
11152 this.dropdownWidget
.getMenu()
11154 .addItems( options
.map( function ( opt
) {
11155 return new OO
.ui
.MenuOptionWidget( {
11157 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11161 // Restore the previous value, or reset to something sensible
11162 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
11163 // Previous value is still available, ensure consistency with the dropdown
11164 this.setValue( value
);
11166 // No longer valid, reset
11167 if ( options
.length
) {
11168 this.setValue( options
[ 0 ].data
);
11178 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
11179 this.dropdownWidget
.getMenu().toggle( true );
11186 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
11187 this.dropdownWidget
.getMenu().toggle( false );
11192 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
11193 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
11194 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
11195 * please see the [OOjs UI documentation on MediaWiki][1].
11198 * // An example of selected, unselected, and disabled radio inputs
11199 * var radio1=new OO.ui.RadioInputWidget({
11203 * var radio2=new OO.ui.RadioInputWidget({
11206 * var radio3=new OO.ui.RadioInputWidget( {
11210 * // Create a fieldset layout with fields for each radio button.
11211 * var fieldset = new OO.ui.FieldsetLayout( {
11212 * label: 'Radio inputs'
11214 * fieldset.addItems( [
11215 * new OO.ui.FieldLayout( radio1, {label : 'Selected', align : 'inline'}),
11216 * new OO.ui.FieldLayout( radio2, {label : 'Unselected', align : 'inline'}),
11217 * new OO.ui.FieldLayout( radio3, {label : 'Disabled', align : 'inline'}),
11219 * $( 'body' ).append( fieldset.$element );
11221 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11224 * @extends OO.ui.InputWidget
11227 * @param {Object} [config] Configuration options
11228 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
11230 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
11231 // Configuration initialization
11232 config
= config
|| {};
11234 // Parent constructor
11235 OO
.ui
.RadioInputWidget
.super.call( this, config
);
11238 this.$element
.addClass( 'oo-ui-radioInputWidget' );
11239 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
11244 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
11252 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
11253 return $( '<input type="radio" />' );
11259 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
11260 // RadioInputWidget doesn't track its state.
11264 * Set selection state of this radio button.
11266 * @param {boolean} state `true` for selected
11269 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
11270 // RadioInputWidget doesn't track its state.
11271 this.$input
.prop( 'checked', state
);
11276 * Check if this radio button is selected.
11278 * @return {boolean} Radio is selected
11280 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
11281 return this.$input
.prop( 'checked' );
11285 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
11286 * size of the field as well as its presentation. In addition, these widgets can be configured
11287 * with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators}, an optional
11288 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
11289 * which modifies incoming values rather than validating them.
11290 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
11293 * // Example of a text input widget
11294 * var textInput=new OO.ui.TextInputWidget( {
11295 * value: 'Text input'
11297 * $('body').append(textInput.$element);
11299 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11302 * @extends OO.ui.InputWidget
11303 * @mixins OO.ui.IconElement
11304 * @mixins OO.ui.IndicatorElement
11305 * @mixins OO.ui.PendingElement
11306 * @mixins OO.ui.LabelElement
11309 * @param {Object} [config] Configuration options
11310 * @cfg {string} [type='text'] The value of the HTML `type` attribute
11311 * @cfg {string} [placeholder] Placeholder text
11312 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
11313 * instruct the browser to focus this widget.
11314 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
11315 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
11316 * @cfg {boolean} [multiline=false] Allow multiple lines of text
11317 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11318 * Use the #maxRows config to specify a maximum number of displayed rows.
11319 * @cfg {boolean} [maxRows=10] Maximum number of rows to display when #autosize is set to true.
11320 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
11321 * the value or placeholder text: `'before'` or `'after'`
11322 * @cfg {boolean} [required=false] Mark the field as required
11323 * @cfg {RegExp|string} [validate] Validation pattern, either a regular expression or the
11324 * symbolic name of a pattern defined by the class: 'non-empty' (the value cannot be an empty string)
11325 * or 'integer' (the value must contain only numbers).
11327 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
11328 // Configuration initialization
11329 config
= $.extend( {
11331 labelPosition
: 'after',
11335 // Parent constructor
11336 OO
.ui
.TextInputWidget
.super.call( this, config
);
11338 // Mixin constructors
11339 OO
.ui
.IconElement
.call( this, config
);
11340 OO
.ui
.IndicatorElement
.call( this, config
);
11341 OO
.ui
.PendingElement
.call( this, config
);
11342 OO
.ui
.LabelElement
.call( this, config
);
11345 this.readOnly
= false;
11346 this.multiline
= !!config
.multiline
;
11347 this.autosize
= !!config
.autosize
;
11348 this.maxRows
= config
.maxRows
;
11349 this.validate
= null;
11351 // Clone for resizing
11352 if ( this.autosize
) {
11353 this.$clone
= this.$input
11355 .insertAfter( this.$input
)
11356 .attr( 'aria-hidden', 'true' )
11357 .addClass( 'oo-ui-element-hidden' );
11360 this.setValidation( config
.validate
);
11361 this.setLabelPosition( config
.labelPosition
);
11365 keypress
: this.onKeyPress
.bind( this ),
11366 blur
: this.setValidityFlag
.bind( this )
11369 focus
: this.onElementAttach
.bind( this )
11371 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
11372 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
11373 this.on( 'labelChange', this.updatePosition
.bind( this ) );
11377 .addClass( 'oo-ui-textInputWidget' )
11378 .append( this.$icon
, this.$indicator
);
11379 this.setReadOnly( !!config
.readOnly
);
11380 if ( config
.placeholder
) {
11381 this.$input
.attr( 'placeholder', config
.placeholder
);
11383 if ( config
.maxLength
!== undefined ) {
11384 this.$input
.attr( 'maxlength', config
.maxLength
);
11386 if ( config
.autofocus
) {
11387 this.$input
.attr( 'autofocus', 'autofocus' );
11389 if ( config
.required
) {
11390 this.$input
.attr( 'required', 'true' );
11392 if ( this.label
|| config
.autosize
) {
11393 this.installParentChangeDetector();
11399 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
11400 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.IconElement
);
11401 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.IndicatorElement
);
11402 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.PendingElement
);
11403 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.LabelElement
);
11405 /* Static properties */
11407 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
11415 * An `enter` event is emitted when the user presses 'enter' inside the text box.
11417 * Not emitted if the input is multiline.
11425 * Handle icon mouse down events.
11428 * @param {jQuery.Event} e Mouse down event
11431 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
11432 if ( e
.which
=== 1 ) {
11433 this.$input
[ 0 ].focus();
11439 * Handle indicator mouse down events.
11442 * @param {jQuery.Event} e Mouse down event
11445 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
11446 if ( e
.which
=== 1 ) {
11447 this.$input
[ 0 ].focus();
11453 * Handle key press events.
11456 * @param {jQuery.Event} e Key press event
11457 * @fires enter If enter key is pressed and input is not multiline
11459 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
11460 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
11461 this.emit( 'enter', e
);
11466 * Handle element attach events.
11469 * @param {jQuery.Event} e Element attach event
11471 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
11472 // Any previously calculated size is now probably invalid if we reattached elsewhere
11473 this.valCache
= null;
11475 this.positionLabel();
11481 OO
.ui
.TextInputWidget
.prototype.onEdit = function () {
11485 return OO
.ui
.TextInputWidget
.super.prototype.onEdit
.call( this );
11491 OO
.ui
.TextInputWidget
.prototype.setValue = function ( value
) {
11493 OO
.ui
.TextInputWidget
.super.prototype.setValue
.call( this, value
);
11495 this.setValidityFlag();
11501 * Check if the input is {@link #readOnly read-only}.
11503 * @return {boolean}
11505 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
11506 return this.readOnly
;
11510 * Set the {@link #readOnly read-only} state of the input.
11512 * @param {boolean} state Make input read-only
11515 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
11516 this.readOnly
= !!state
;
11517 this.$input
.prop( 'readOnly', this.readOnly
);
11522 * Support function for making #onElementAttach work across browsers.
11524 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
11525 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
11527 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
11528 * first time that the element gets attached to the documented.
11530 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
11531 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
11532 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
11535 if ( MutationObserver
) {
11536 // The new way. If only it wasn't so ugly.
11538 if ( this.$element
.closest( 'html' ).length
) {
11539 // Widget is attached already, do nothing. This breaks the functionality of this function when
11540 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
11541 // would require observation of the whole document, which would hurt performance of other,
11542 // more important code.
11546 // Find topmost node in the tree
11547 topmostNode
= this.$element
[0];
11548 while ( topmostNode
.parentNode
) {
11549 topmostNode
= topmostNode
.parentNode
;
11552 // We have no way to detect the $element being attached somewhere without observing the entire
11553 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
11554 // parent node of $element, and instead detect when $element is removed from it (and thus
11555 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
11556 // doesn't get attached, we end up back here and create the parent.
11558 mutationObserver
= new MutationObserver( function ( mutations
) {
11559 var i
, j
, removedNodes
;
11560 for ( i
= 0; i
< mutations
.length
; i
++ ) {
11561 removedNodes
= mutations
[ i
].removedNodes
;
11562 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
11563 if ( removedNodes
[ j
] === topmostNode
) {
11564 setTimeout( onRemove
, 0 );
11571 onRemove = function () {
11572 // If the node was attached somewhere else, report it
11573 if ( widget
.$element
.closest( 'html' ).length
) {
11574 widget
.onElementAttach();
11576 mutationObserver
.disconnect();
11577 widget
.installParentChangeDetector();
11580 // Create a fake parent and observe it
11581 fakeParentNode
= $( '<div>' ).append( this.$element
)[0];
11582 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
11584 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
11585 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
11586 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
11591 * Automatically adjust the size of the text input.
11593 * This only affects #multiline inputs that are {@link #autosize autosized}.
11597 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
11598 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
, idealHeight
;
11600 if ( this.multiline
&& this.autosize
&& this.$input
.val() !== this.valCache
) {
11602 .val( this.$input
.val() )
11603 .attr( 'rows', '' )
11604 // Set inline height property to 0 to measure scroll height
11605 .css( 'height', 0 );
11607 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11609 this.valCache
= this.$input
.val();
11611 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11613 // Remove inline height property to measure natural heights
11614 this.$clone
.css( 'height', '' );
11615 innerHeight
= this.$clone
.innerHeight();
11616 outerHeight
= this.$clone
.outerHeight();
11618 // Measure max rows height
11620 .attr( 'rows', this.maxRows
)
11621 .css( 'height', 'auto' )
11623 maxInnerHeight
= this.$clone
.innerHeight();
11625 // Difference between reported innerHeight and scrollHeight with no scrollbars present
11626 // Equals 1 on Blink-based browsers and 0 everywhere else
11627 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11628 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11630 this.$clone
.addClass( 'oo-ui-element-hidden' );
11632 // Only apply inline height when expansion beyond natural height is needed
11633 if ( idealHeight
> innerHeight
) {
11634 // Use the difference between the inner and outer height as a buffer
11635 this.$input
.css( 'height', idealHeight
+ ( outerHeight
- innerHeight
) );
11637 this.$input
.css( 'height', '' );
11647 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
11648 return config
.multiline
? $( '<textarea>' ) : $( '<input type="' + config
.type
+ '" />' );
11652 * Check if the input supports multiple lines.
11654 * @return {boolean}
11656 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
11657 return !!this.multiline
;
11661 * Check if the input automatically adjusts its size.
11663 * @return {boolean}
11665 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
11666 return !!this.autosize
;
11670 * Select the entire text of the input.
11674 OO
.ui
.TextInputWidget
.prototype.select = function () {
11675 this.$input
.select();
11680 * Set the validation pattern.
11682 * The validation pattern is either a regular expression or the symbolic name of a pattern
11683 * defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11684 * value must contain only numbers).
11686 * @param {RegExp|string|null} validate Regular expression or the symbolic name of a
11687 * pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11689 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
11690 if ( validate
instanceof RegExp
) {
11691 this.validate
= validate
;
11693 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
11698 * Sets the 'invalid' flag appropriately.
11700 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function () {
11702 this.isValid().done( function ( valid
) {
11703 widget
.setFlags( { invalid
: !valid
} );
11708 * Check if a value is valid.
11710 * This method returns a promise that resolves with a boolean `true` if the current value is
11711 * considered valid according to the supplied {@link #validate validation pattern}.
11713 * @return {jQuery.Deferred} A promise that resolves to a boolean `true` if the value is valid.
11715 OO
.ui
.TextInputWidget
.prototype.isValid = function () {
11716 return $.Deferred().resolve( !!this.getValue().match( this.validate
) ).promise();
11720 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11722 * @param {string} labelPosition Label position, 'before' or 'after'
11725 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
11726 this.labelPosition
= labelPosition
;
11727 this.updatePosition();
11732 * Deprecated alias of #setLabelPosition
11734 * @deprecated Use setLabelPosition instead.
11736 OO
.ui
.TextInputWidget
.prototype.setPosition
=
11737 OO
.ui
.TextInputWidget
.prototype.setLabelPosition
;
11740 * Update the position of the inline label.
11742 * This method is called by #setLabelPosition, and can also be called on its own if
11743 * something causes the label to be mispositioned.
11748 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
11749 var after
= this.labelPosition
=== 'after';
11752 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
11753 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
11755 if ( this.label
) {
11756 this.positionLabel();
11763 * Position the label by setting the correct padding on the input.
11768 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
11769 // Clear old values
11771 // Clear old values if present
11773 'padding-right': '',
11777 if ( this.label
) {
11778 this.$element
.append( this.$label
);
11780 this.$label
.detach();
11784 var after
= this.labelPosition
=== 'after',
11785 rtl
= this.$element
.css( 'direction' ) === 'rtl',
11786 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11788 this.$input
.css( property
, this.$label
.outerWidth( true ) );
11794 * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11795 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11796 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11798 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11799 * option, that option will appear to be selected.
11800 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11803 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
11806 * // Example: A ComboBoxWidget.
11807 * var comboBox=new OO.ui.ComboBoxWidget( {
11808 * label: 'ComboBoxWidget',
11809 * input: { value: 'Option One' },
11812 * new OO.ui.MenuOptionWidget( {
11813 * data: 'Option 1',
11814 * label: 'Option One' } ),
11815 * new OO.ui.MenuOptionWidget( {
11816 * data: 'Option 2',
11817 * label: 'Option Two' } ),
11818 * new OO.ui.MenuOptionWidget( {
11819 * data: 'Option 3',
11820 * label: 'Option Three'} ),
11821 * new OO.ui.MenuOptionWidget( {
11822 * data: 'Option 4',
11823 * label: 'Option Four' } ),
11824 * new OO.ui.MenuOptionWidget( {
11825 * data: 'Option 5',
11826 * label: 'Option Five' } )
11830 * $('body').append(comboBox.$element);
11832 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
11835 * @extends OO.ui.Widget
11836 * @mixins OO.ui.TabIndexedElement
11839 * @param {Object} [config] Configuration options
11840 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11841 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
11842 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11843 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11844 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11846 OO
.ui
.ComboBoxWidget
= function OoUiComboBoxWidget( config
) {
11847 // Configuration initialization
11848 config
= config
|| {};
11850 // Parent constructor
11851 OO
.ui
.ComboBoxWidget
.super.call( this, config
);
11853 // Properties (must be set before TabIndexedElement constructor call)
11854 this.$indicator
= this.$( '<span>' );
11856 // Mixin constructors
11857 OO
.ui
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$indicator
} ) );
11860 this.$overlay
= config
.$overlay
|| this.$element
;
11861 this.input
= new OO
.ui
.TextInputWidget( $.extend(
11864 $indicator
: this.$indicator
,
11865 disabled
: this.isDisabled()
11869 this.input
.$input
.eq( 0 ).attr( {
11871 'aria-autocomplete': 'list'
11873 this.menu
= new OO
.ui
.TextInputMenuSelectWidget( this.input
, $.extend(
11877 disabled
: this.isDisabled()
11883 this.$indicator
.on( {
11884 click
: this.onClick
.bind( this ),
11885 keypress
: this.onKeyPress
.bind( this )
11887 this.input
.connect( this, {
11888 change
: 'onInputChange',
11889 enter
: 'onInputEnter'
11891 this.menu
.connect( this, {
11892 choose
: 'onMenuChoose',
11893 add
: 'onMenuItemsChange',
11894 remove
: 'onMenuItemsChange'
11898 this.$element
.addClass( 'oo-ui-comboBoxWidget' ).append( this.input
.$element
);
11899 this.$overlay
.append( this.menu
.$element
);
11900 this.onMenuItemsChange();
11905 OO
.inheritClass( OO
.ui
.ComboBoxWidget
, OO
.ui
.Widget
);
11906 OO
.mixinClass( OO
.ui
.ComboBoxWidget
, OO
.ui
.TabIndexedElement
);
11911 * Get the combobox's menu.
11912 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
11914 OO
.ui
.ComboBoxWidget
.prototype.getMenu = function () {
11919 * Handle input change events.
11922 * @param {string} value New value
11924 OO
.ui
.ComboBoxWidget
.prototype.onInputChange = function ( value
) {
11925 var match
= this.menu
.getItemFromData( value
);
11927 this.menu
.selectItem( match
);
11928 if ( this.menu
.getHighlightedItem() ) {
11929 this.menu
.highlightItem( match
);
11932 if ( !this.isDisabled() ) {
11933 this.menu
.toggle( true );
11938 * Handle mouse click events.
11942 * @param {jQuery.Event} e Mouse click event
11944 OO
.ui
.ComboBoxWidget
.prototype.onClick = function ( e
) {
11945 if ( !this.isDisabled() && e
.which
=== 1 ) {
11946 this.menu
.toggle();
11947 this.input
.$input
[ 0 ].focus();
11953 * Handle key press events.
11957 * @param {jQuery.Event} e Key press event
11959 OO
.ui
.ComboBoxWidget
.prototype.onKeyPress = function ( e
) {
11960 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
11961 this.menu
.toggle();
11962 this.input
.$input
[ 0 ].focus();
11968 * Handle input enter events.
11972 OO
.ui
.ComboBoxWidget
.prototype.onInputEnter = function () {
11973 if ( !this.isDisabled() ) {
11974 this.menu
.toggle( false );
11979 * Handle menu choose events.
11982 * @param {OO.ui.OptionWidget} item Chosen item
11984 OO
.ui
.ComboBoxWidget
.prototype.onMenuChoose = function ( item
) {
11986 this.input
.setValue( item
.getData() );
11991 * Handle menu item change events.
11995 OO
.ui
.ComboBoxWidget
.prototype.onMenuItemsChange = function () {
11996 var match
= this.menu
.getItemFromData( this.input
.getValue() );
11997 this.menu
.selectItem( match
);
11998 if ( this.menu
.getHighlightedItem() ) {
11999 this.menu
.highlightItem( match
);
12001 this.$element
.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu
.isEmpty() );
12007 OO
.ui
.ComboBoxWidget
.prototype.setDisabled = function ( disabled
) {
12009 OO
.ui
.ComboBoxWidget
.super.prototype.setDisabled
.call( this, disabled
);
12011 if ( this.input
) {
12012 this.input
.setDisabled( this.isDisabled() );
12015 this.menu
.setDisabled( this.isDisabled() );
12022 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
12023 * be configured with a `label` option that is set to a string, a label node, or a function:
12025 * - String: a plaintext string
12026 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
12027 * label that includes a link or special styling, such as a gray color or additional graphical elements.
12028 * - Function: a function that will produce a string in the future. Functions are used
12029 * in cases where the value of the label is not currently defined.
12031 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
12032 * will come into focus when the label is clicked.
12035 * // Examples of LabelWidgets
12036 * var label1 = new OO.ui.LabelWidget({
12037 * label: 'plaintext label'
12039 * var label2 = new OO.ui.LabelWidget({
12040 * label: $( '<a href="default.html">jQuery label</a>' )
12042 * // Create a fieldset layout with fields for each example
12043 * var fieldset = new OO.ui.FieldsetLayout( );
12044 * fieldset.addItems( [
12045 * new OO.ui.FieldLayout( label1 ),
12046 * new OO.ui.FieldLayout( label2 )
12048 * $( 'body' ).append( fieldset.$element );
12052 * @extends OO.ui.Widget
12053 * @mixins OO.ui.LabelElement
12056 * @param {Object} [config] Configuration options
12057 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
12058 * Clicking the label will focus the specified input field.
12060 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
12061 // Configuration initialization
12062 config
= config
|| {};
12064 // Parent constructor
12065 OO
.ui
.LabelWidget
.super.call( this, config
);
12067 // Mixin constructors
12068 OO
.ui
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
12069 OO
.ui
.TitledElement
.call( this, config
);
12072 this.input
= config
.input
;
12075 if ( this.input
instanceof OO
.ui
.InputWidget
) {
12076 this.$element
.on( 'click', this.onClick
.bind( this ) );
12080 this.$element
.addClass( 'oo-ui-labelWidget' );
12085 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
12086 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.LabelElement
);
12087 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.TitledElement
);
12089 /* Static Properties */
12091 OO
.ui
.LabelWidget
.static.tagName
= 'span';
12096 * Handles label mouse click events.
12099 * @param {jQuery.Event} e Mouse click event
12101 OO
.ui
.LabelWidget
.prototype.onClick = function () {
12102 this.input
.simulateLabelClick();
12107 * OptionWidgets are special elements that can be selected and configured with data. The
12108 * data is often unique for each option, but it does not have to be. OptionWidgets are used
12109 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
12110 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
12112 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12115 * @extends OO.ui.Widget
12116 * @mixins OO.ui.LabelElement
12117 * @mixins OO.ui.FlaggedElement
12120 * @param {Object} [config] Configuration options
12122 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
12123 // Configuration initialization
12124 config
= config
|| {};
12126 // Parent constructor
12127 OO
.ui
.OptionWidget
.super.call( this, config
);
12129 // Mixin constructors
12130 OO
.ui
.ItemWidget
.call( this );
12131 OO
.ui
.LabelElement
.call( this, config
);
12132 OO
.ui
.FlaggedElement
.call( this, config
);
12135 this.selected
= false;
12136 this.highlighted
= false;
12137 this.pressed
= false;
12141 .data( 'oo-ui-optionWidget', this )
12142 .attr( 'role', 'option' )
12143 .addClass( 'oo-ui-optionWidget' )
12144 .append( this.$label
);
12149 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
12150 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.ItemWidget
);
12151 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.LabelElement
);
12152 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.FlaggedElement
);
12154 /* Static Properties */
12156 OO
.ui
.OptionWidget
.static.selectable
= true;
12158 OO
.ui
.OptionWidget
.static.highlightable
= true;
12160 OO
.ui
.OptionWidget
.static.pressable
= true;
12162 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
12167 * Check if the option can be selected.
12169 * @return {boolean} Item is selectable
12171 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
12172 return this.constructor.static.selectable
&& !this.isDisabled();
12176 * Check if the option can be highlighted. A highlight indicates that the option
12177 * may be selected when a user presses enter or clicks. Disabled items cannot
12180 * @return {boolean} Item is highlightable
12182 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
12183 return this.constructor.static.highlightable
&& !this.isDisabled();
12187 * Check if the option can be pressed. The pressed state occurs when a user mouses
12188 * down on an item, but has not yet let go of the mouse.
12190 * @return {boolean} Item is pressable
12192 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
12193 return this.constructor.static.pressable
&& !this.isDisabled();
12197 * Check if the option is selected.
12199 * @return {boolean} Item is selected
12201 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
12202 return this.selected
;
12206 * Check if the option is highlighted. A highlight indicates that the
12207 * item may be selected when a user presses enter or clicks.
12209 * @return {boolean} Item is highlighted
12211 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
12212 return this.highlighted
;
12216 * Check if the option is pressed. The pressed state occurs when a user mouses
12217 * down on an item, but has not yet let go of the mouse. The item may appear
12218 * selected, but it will not be selected until the user releases the mouse.
12220 * @return {boolean} Item is pressed
12222 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
12223 return this.pressed
;
12227 * Set the option’s selected state. In general, all modifications to the selection
12228 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
12229 * method instead of this method.
12231 * @param {boolean} [state=false] Select option
12234 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
12235 if ( this.constructor.static.selectable
) {
12236 this.selected
= !!state
;
12238 .toggleClass( 'oo-ui-optionWidget-selected', state
)
12239 .attr( 'aria-selected', state
.toString() );
12240 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
12241 this.scrollElementIntoView();
12243 this.updateThemeClasses();
12249 * Set the option’s highlighted state. In general, all programmatic
12250 * modifications to the highlight should be handled by the
12251 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
12252 * method instead of this method.
12254 * @param {boolean} [state=false] Highlight option
12257 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
12258 if ( this.constructor.static.highlightable
) {
12259 this.highlighted
= !!state
;
12260 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
12261 this.updateThemeClasses();
12267 * Set the option’s pressed state. In general, all
12268 * programmatic modifications to the pressed state should be handled by the
12269 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
12270 * method instead of this method.
12272 * @param {boolean} [state=false] Press option
12275 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
12276 if ( this.constructor.static.pressable
) {
12277 this.pressed
= !!state
;
12278 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
12279 this.updateThemeClasses();
12285 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
12286 * with an {@link OO.ui.IconElement icon} and/or {@link OO.ui.IndicatorElement indicator}.
12287 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
12288 * options. For more information about options and selects, please see the
12289 * [OOjs UI documentation on MediaWiki][1].
12292 * // Decorated options in a select widget
12293 * var select=new OO.ui.SelectWidget( {
12295 * new OO.ui.DecoratedOptionWidget( {
12297 * label: 'Option with icon',
12300 * new OO.ui.DecoratedOptionWidget( {
12302 * label: 'Option with indicator',
12303 * indicator: 'next'
12307 * $('body').append(select.$element);
12309 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12312 * @extends OO.ui.OptionWidget
12313 * @mixins OO.ui.IconElement
12314 * @mixins OO.ui.IndicatorElement
12317 * @param {Object} [config] Configuration options
12319 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
12320 // Parent constructor
12321 OO
.ui
.DecoratedOptionWidget
.super.call( this, config
);
12323 // Mixin constructors
12324 OO
.ui
.IconElement
.call( this, config
);
12325 OO
.ui
.IndicatorElement
.call( this, config
);
12329 .addClass( 'oo-ui-decoratedOptionWidget' )
12330 .prepend( this.$icon
)
12331 .append( this.$indicator
);
12336 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
12337 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.IconElement
);
12338 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.IndicatorElement
);
12341 * ButtonOptionWidget is a special type of {@link OO.ui.ButtonElement button element} that
12342 * can be selected and configured with data. The class is
12343 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
12344 * [OOjs UI documentation on MediaWiki] [1] for more information.
12346 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
12349 * @extends OO.ui.DecoratedOptionWidget
12350 * @mixins OO.ui.ButtonElement
12351 * @mixins OO.ui.TabIndexedElement
12354 * @param {Object} [config] Configuration options
12356 OO
.ui
.ButtonOptionWidget
= function OoUiButtonOptionWidget( config
) {
12357 // Configuration initialization
12358 config
= $.extend( { tabIndex
: -1 }, config
);
12360 // Parent constructor
12361 OO
.ui
.ButtonOptionWidget
.super.call( this, config
);
12363 // Mixin constructors
12364 OO
.ui
.ButtonElement
.call( this, config
);
12365 OO
.ui
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
12368 this.$element
.addClass( 'oo-ui-buttonOptionWidget' );
12369 this.$button
.append( this.$element
.contents() );
12370 this.$element
.append( this.$button
);
12375 OO
.inheritClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
12376 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.ButtonElement
);
12377 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.TabIndexedElement
);
12379 /* Static Properties */
12381 // Allow button mouse down events to pass through so they can be handled by the parent select widget
12382 OO
.ui
.ButtonOptionWidget
.static.cancelButtonMouseDownEvents
= false;
12384 OO
.ui
.ButtonOptionWidget
.static.highlightable
= false;
12391 OO
.ui
.ButtonOptionWidget
.prototype.setSelected = function ( state
) {
12392 OO
.ui
.ButtonOptionWidget
.super.prototype.setSelected
.call( this, state
);
12394 if ( this.constructor.static.selectable
) {
12395 this.setActive( state
);
12402 * RadioOptionWidget is an option widget that looks like a radio button.
12403 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
12404 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
12406 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
12409 * @extends OO.ui.OptionWidget
12412 * @param {Object} [config] Configuration options
12414 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
12415 // Configuration initialization
12416 config
= config
|| {};
12418 // Properties (must be done before parent constructor which calls #setDisabled)
12419 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
12421 // Parent constructor
12422 OO
.ui
.RadioOptionWidget
.super.call( this, config
);
12426 .addClass( 'oo-ui-radioOptionWidget' )
12427 .prepend( this.radio
.$element
);
12432 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
12434 /* Static Properties */
12436 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
12438 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
12440 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
12442 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
12449 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
12450 OO
.ui
.RadioOptionWidget
.super.prototype.setSelected
.call( this, state
);
12452 this.radio
.setSelected( state
);
12460 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
12461 OO
.ui
.RadioOptionWidget
.super.prototype.setDisabled
.call( this, disabled
);
12463 this.radio
.setDisabled( this.isDisabled() );
12469 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
12470 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
12471 * the [OOjs UI documentation on MediaWiki] [1] for more information.
12473 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12476 * @extends OO.ui.DecoratedOptionWidget
12479 * @param {Object} [config] Configuration options
12481 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
12482 // Configuration initialization
12483 config
= $.extend( { icon
: 'check' }, config
);
12485 // Parent constructor
12486 OO
.ui
.MenuOptionWidget
.super.call( this, config
);
12490 .attr( 'role', 'menuitem' )
12491 .addClass( 'oo-ui-menuOptionWidget' );
12496 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
12498 /* Static Properties */
12500 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
12503 * Section to group one or more items in a OO.ui.MenuSelectWidget.
12506 * @extends OO.ui.DecoratedOptionWidget
12509 * @param {Object} [config] Configuration options
12511 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
12512 // Parent constructor
12513 OO
.ui
.MenuSectionOptionWidget
.super.call( this, config
);
12516 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' );
12521 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
12523 /* Static Properties */
12525 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
12527 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
12530 * Items for an OO.ui.OutlineSelectWidget.
12533 * @extends OO.ui.DecoratedOptionWidget
12536 * @param {Object} [config] Configuration options
12537 * @cfg {number} [level] Indentation level
12538 * @cfg {boolean} [movable] Allow modification from outline controls
12540 OO
.ui
.OutlineOptionWidget
= function OoUiOutlineOptionWidget( config
) {
12541 // Configuration initialization
12542 config
= config
|| {};
12544 // Parent constructor
12545 OO
.ui
.OutlineOptionWidget
.super.call( this, config
);
12549 this.movable
= !!config
.movable
;
12550 this.removable
= !!config
.removable
;
12553 this.$element
.addClass( 'oo-ui-outlineOptionWidget' );
12554 this.setLevel( config
.level
);
12559 OO
.inheritClass( OO
.ui
.OutlineOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
12561 /* Static Properties */
12563 OO
.ui
.OutlineOptionWidget
.static.highlightable
= false;
12565 OO
.ui
.OutlineOptionWidget
.static.scrollIntoViewOnSelect
= true;
12567 OO
.ui
.OutlineOptionWidget
.static.levelClass
= 'oo-ui-outlineOptionWidget-level-';
12569 OO
.ui
.OutlineOptionWidget
.static.levels
= 3;
12574 * Check if item is movable.
12576 * Movability is used by outline controls.
12578 * @return {boolean} Item is movable
12580 OO
.ui
.OutlineOptionWidget
.prototype.isMovable = function () {
12581 return this.movable
;
12585 * Check if item is removable.
12587 * Removability is used by outline controls.
12589 * @return {boolean} Item is removable
12591 OO
.ui
.OutlineOptionWidget
.prototype.isRemovable = function () {
12592 return this.removable
;
12596 * Get indentation level.
12598 * @return {number} Indentation level
12600 OO
.ui
.OutlineOptionWidget
.prototype.getLevel = function () {
12607 * Movability is used by outline controls.
12609 * @param {boolean} movable Item is movable
12612 OO
.ui
.OutlineOptionWidget
.prototype.setMovable = function ( movable
) {
12613 this.movable
= !!movable
;
12614 this.updateThemeClasses();
12619 * Set removability.
12621 * Removability is used by outline controls.
12623 * @param {boolean} movable Item is removable
12626 OO
.ui
.OutlineOptionWidget
.prototype.setRemovable = function ( removable
) {
12627 this.removable
= !!removable
;
12628 this.updateThemeClasses();
12633 * Set indentation level.
12635 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
12638 OO
.ui
.OutlineOptionWidget
.prototype.setLevel = function ( level
) {
12639 var levels
= this.constructor.static.levels
,
12640 levelClass
= this.constructor.static.levelClass
,
12643 this.level
= level
? Math
.max( 0, Math
.min( levels
- 1, level
) ) : 0;
12645 if ( this.level
=== i
) {
12646 this.$element
.addClass( levelClass
+ i
);
12648 this.$element
.removeClass( levelClass
+ i
);
12651 this.updateThemeClasses();
12657 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
12658 * By default, each popup has an anchor that points toward its origin.
12659 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
12662 * // A popup widget.
12663 * var popup=new OO.ui.PopupWidget({
12664 * $content: $( '<p>Hi there!</p>' ),
12669 * $('body').append(popup.$element);
12670 * // To display the popup, toggle the visibility to 'true'.
12671 * popup.toggle(true);
12673 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
12676 * @extends OO.ui.Widget
12677 * @mixins OO.ui.LabelElement
12680 * @param {Object} [config] Configuration options
12681 * @cfg {number} [width=320] Width of popup in pixels
12682 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
12683 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
12684 * @cfg {string} [align='center'] Alignment of the popup: `center`, `left`, or `right`.
12685 * If the popup is right-aligned, the right edge of the popup is aligned to the anchor.
12686 * For left-aligned popups, the left edge is aligned to the anchor.
12687 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
12688 * See the [OOjs UI docs on MediaWiki][3] for an example.
12689 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
12690 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
12691 * @cfg {jQuery} [$content] Content to append to the popup's body
12692 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
12693 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
12694 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
12696 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
12697 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
12699 * @cfg {boolean} [padded] Add padding to the popup's body
12701 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
12702 // Configuration initialization
12703 config
= config
|| {};
12705 // Parent constructor
12706 OO
.ui
.PopupWidget
.super.call( this, config
);
12708 // Properties (must be set before ClippableElement constructor call)
12709 this.$body
= $( '<div>' );
12711 // Mixin constructors
12712 OO
.ui
.LabelElement
.call( this, config
);
12713 OO
.ui
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$body
} ) );
12716 this.$popup
= $( '<div>' );
12717 this.$head
= $( '<div>' );
12718 this.$anchor
= $( '<div>' );
12719 // If undefined, will be computed lazily in updateDimensions()
12720 this.$container
= config
.$container
;
12721 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
12722 this.autoClose
= !!config
.autoClose
;
12723 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
12724 this.transitionTimeout
= null;
12725 this.anchor
= null;
12726 this.width
= config
.width
!== undefined ? config
.width
: 320;
12727 this.height
= config
.height
!== undefined ? config
.height
: null;
12728 this.align
= config
.align
|| 'center';
12729 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
12730 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
12731 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
12734 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
12737 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
12738 this.$body
.addClass( 'oo-ui-popupWidget-body' );
12739 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
12741 .addClass( 'oo-ui-popupWidget-head' )
12742 .append( this.$label
, this.closeButton
.$element
);
12743 if ( !config
.head
) {
12744 this.$head
.addClass( 'oo-ui-element-hidden' );
12747 .addClass( 'oo-ui-popupWidget-popup' )
12748 .append( this.$head
, this.$body
);
12750 .addClass( 'oo-ui-popupWidget' )
12751 .append( this.$popup
, this.$anchor
);
12752 // Move content, which was added to #$element by OO.ui.Widget, to the body
12753 if ( config
.$content
instanceof jQuery
) {
12754 this.$body
.append( config
.$content
);
12756 if ( config
.padded
) {
12757 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
12760 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
12761 // that reference properties not initialized at that time of parent class construction
12762 // TODO: Find a better way to handle post-constructor setup
12763 this.visible
= false;
12764 this.$element
.addClass( 'oo-ui-element-hidden' );
12769 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
12770 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.LabelElement
);
12771 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.ClippableElement
);
12776 * Handles mouse down events.
12779 * @param {MouseEvent} e Mouse down event
12781 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
12783 this.isVisible() &&
12784 !$.contains( this.$element
[ 0 ], e
.target
) &&
12785 ( !this.$autoCloseIgnore
|| !this.$autoCloseIgnore
.has( e
.target
).length
)
12787 this.toggle( false );
12792 * Bind mouse down listener.
12796 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
12797 // Capture clicks outside popup
12798 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
12802 * Handles close button click events.
12806 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
12807 if ( this.isVisible() ) {
12808 this.toggle( false );
12813 * Unbind mouse down listener.
12817 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
12818 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
12822 * Handles key down events.
12825 * @param {KeyboardEvent} e Key down event
12827 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
12829 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
12832 this.toggle( false );
12833 e
.preventDefault();
12834 e
.stopPropagation();
12839 * Bind key down listener.
12843 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
12844 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
12848 * Unbind key down listener.
12852 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
12853 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
12857 * Show, hide, or toggle the visibility of the anchor.
12859 * @param {boolean} [show] Show anchor, omit to toggle
12861 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
12862 show
= show
=== undefined ? !this.anchored
: !!show
;
12864 if ( this.anchored
!== show
) {
12866 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
12868 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
12870 this.anchored
= show
;
12875 * Check if the anchor is visible.
12877 * @return {boolean} Anchor is visible
12879 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
12880 return this.anchor
;
12886 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
12887 show
= show
=== undefined ? !this.isVisible() : !!show
;
12889 var change
= show
!== this.isVisible();
12892 OO
.ui
.PopupWidget
.super.prototype.toggle
.call( this, show
);
12896 if ( this.autoClose
) {
12897 this.bindMouseDownListener();
12898 this.bindKeyDownListener();
12900 this.updateDimensions();
12901 this.toggleClipping( true );
12903 this.toggleClipping( false );
12904 if ( this.autoClose
) {
12905 this.unbindMouseDownListener();
12906 this.unbindKeyDownListener();
12915 * Set the size of the popup.
12917 * Changing the size may also change the popup's position depending on the alignment.
12919 * @param {number} width Width in pixels
12920 * @param {number} height Height in pixels
12921 * @param {boolean} [transition=false] Use a smooth transition
12924 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
12925 this.width
= width
;
12926 this.height
= height
!== undefined ? height
: null;
12927 if ( this.isVisible() ) {
12928 this.updateDimensions( transition
);
12933 * Update the size and position.
12935 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
12936 * be called automatically.
12938 * @param {boolean} [transition=false] Use a smooth transition
12941 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
12942 var popupOffset
, originOffset
, containerLeft
, containerWidth
, containerRight
,
12943 popupLeft
, popupRight
, overlapLeft
, overlapRight
, anchorWidth
,
12946 if ( !this.$container
) {
12947 // Lazy-initialize $container if not specified in constructor
12948 this.$container
= $( this.getClosestScrollableElementContainer() );
12951 // Set height and width before measuring things, since it might cause our measurements
12952 // to change (e.g. due to scrollbars appearing or disappearing)
12955 height
: this.height
!== null ? this.height
: 'auto'
12958 // Compute initial popupOffset based on alignment
12959 popupOffset
= this.width
* ( { left
: 0, center
: -0.5, right
: -1 } )[ this.align
];
12961 // Figure out if this will cause the popup to go beyond the edge of the container
12962 originOffset
= this.$element
.offset().left
;
12963 containerLeft
= this.$container
.offset().left
;
12964 containerWidth
= this.$container
.innerWidth();
12965 containerRight
= containerLeft
+ containerWidth
;
12966 popupLeft
= popupOffset
- this.containerPadding
;
12967 popupRight
= popupOffset
+ this.containerPadding
+ this.width
+ this.containerPadding
;
12968 overlapLeft
= ( originOffset
+ popupLeft
) - containerLeft
;
12969 overlapRight
= containerRight
- ( originOffset
+ popupRight
);
12971 // Adjust offset to make the popup not go beyond the edge, if needed
12972 if ( overlapRight
< 0 ) {
12973 popupOffset
+= overlapRight
;
12974 } else if ( overlapLeft
< 0 ) {
12975 popupOffset
-= overlapLeft
;
12978 // Adjust offset to avoid anchor being rendered too close to the edge
12979 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
12980 // TODO: Find a measurement that works for CSS anchors and image anchors
12981 anchorWidth
= this.$anchor
[ 0 ].scrollWidth
* 2;
12982 if ( popupOffset
+ this.width
< anchorWidth
) {
12983 popupOffset
= anchorWidth
- this.width
;
12984 } else if ( -popupOffset
< anchorWidth
) {
12985 popupOffset
= -anchorWidth
;
12988 // Prevent transition from being interrupted
12989 clearTimeout( this.transitionTimeout
);
12990 if ( transition
) {
12991 // Enable transition
12992 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
12995 // Position body relative to anchor
12996 this.$popup
.css( 'margin-left', popupOffset
);
12998 if ( transition
) {
12999 // Prevent transitioning after transition is complete
13000 this.transitionTimeout
= setTimeout( function () {
13001 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
13004 // Prevent transitioning immediately
13005 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
13008 // Reevaluate clipping state since we've relocated and resized the popup
13015 * Progress bars visually display the status of an operation, such as a download,
13016 * and can be either determinate or indeterminate:
13018 * - **determinate** process bars show the percent of an operation that is complete.
13020 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
13021 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
13022 * not use percentages.
13024 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
13027 * // Examples of determinate and indeterminate progress bars.
13028 * var progressBar1=new OO.ui.ProgressBarWidget( {
13032 * var progressBar2=new OO.ui.ProgressBarWidget( {
13035 * // Create a FieldsetLayout to layout progress bars
13036 * var fieldset = new OO.ui.FieldsetLayout;
13037 * fieldset.addItems( [
13038 * new OO.ui.FieldLayout( progressBar1, {label : 'Determinate', align : 'top'}),
13039 * new OO.ui.FieldLayout( progressBar2, {label : 'Indeterminate', align : 'top'})
13041 * $( 'body' ).append( fieldset.$element );
13044 * @extends OO.ui.Widget
13047 * @param {Object} [config] Configuration options
13048 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
13049 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
13050 * By default, the progress bar is indeterminate.
13052 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
13053 // Configuration initialization
13054 config
= config
|| {};
13056 // Parent constructor
13057 OO
.ui
.ProgressBarWidget
.super.call( this, config
);
13060 this.$bar
= $( '<div>' );
13061 this.progress
= null;
13064 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
13065 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
13068 role
: 'progressbar',
13069 'aria-valuemin': 0,
13070 'aria-valuemax': 100
13072 .addClass( 'oo-ui-progressBarWidget' )
13073 .append( this.$bar
);
13078 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
13080 /* Static Properties */
13082 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
13087 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
13089 * @return {number|boolean} Progress percent
13091 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
13092 return this.progress
;
13096 * Set the percent of the process completed or `false` for an indeterminate process.
13098 * @param {number|boolean} progress Progress percent or `false` for indeterminate
13100 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
13101 this.progress
= progress
;
13103 if ( progress
!== false ) {
13104 this.$bar
.css( 'width', this.progress
+ '%' );
13105 this.$element
.attr( 'aria-valuenow', this.progress
);
13107 this.$bar
.css( 'width', '' );
13108 this.$element
.removeAttr( 'aria-valuenow' );
13110 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress
);
13116 * Search widgets combine a query input, placed above, and a results selection widget, placed below.
13117 * Results are cleared and populated each time the query is changed.
13120 * @extends OO.ui.Widget
13123 * @param {Object} [config] Configuration options
13124 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
13125 * @cfg {string} [value] Initial query value
13127 OO
.ui
.SearchWidget
= function OoUiSearchWidget( config
) {
13128 // Configuration initialization
13129 config
= config
|| {};
13131 // Parent constructor
13132 OO
.ui
.SearchWidget
.super.call( this, config
);
13135 this.query
= new OO
.ui
.TextInputWidget( {
13137 placeholder
: config
.placeholder
,
13138 value
: config
.value
13140 this.results
= new OO
.ui
.SelectWidget();
13141 this.$query
= $( '<div>' );
13142 this.$results
= $( '<div>' );
13145 this.query
.connect( this, {
13146 change
: 'onQueryChange',
13147 enter
: 'onQueryEnter'
13149 this.results
.connect( this, {
13150 highlight
: 'onResultsHighlight',
13151 select
: 'onResultsSelect'
13153 this.query
.$input
.on( 'keydown', this.onQueryKeydown
.bind( this ) );
13157 .addClass( 'oo-ui-searchWidget-query' )
13158 .append( this.query
.$element
);
13160 .addClass( 'oo-ui-searchWidget-results' )
13161 .append( this.results
.$element
);
13163 .addClass( 'oo-ui-searchWidget' )
13164 .append( this.$results
, this.$query
);
13169 OO
.inheritClass( OO
.ui
.SearchWidget
, OO
.ui
.Widget
);
13175 * @param {Object|null} item Item data or null if no item is highlighted
13180 * @param {Object|null} item Item data or null if no item is selected
13186 * Handle query key down events.
13188 * @param {jQuery.Event} e Key down event
13190 OO
.ui
.SearchWidget
.prototype.onQueryKeydown = function ( e
) {
13191 var highlightedItem
, nextItem
,
13192 dir
= e
.which
=== OO
.ui
.Keys
.DOWN
? 1 : ( e
.which
=== OO
.ui
.Keys
.UP
? -1 : 0 );
13195 highlightedItem
= this.results
.getHighlightedItem();
13196 if ( !highlightedItem
) {
13197 highlightedItem
= this.results
.getSelectedItem();
13199 nextItem
= this.results
.getRelativeSelectableItem( highlightedItem
, dir
);
13200 this.results
.highlightItem( nextItem
);
13201 nextItem
.scrollElementIntoView();
13206 * Handle select widget select events.
13208 * Clears existing results. Subclasses should repopulate items according to new query.
13210 * @param {string} value New value
13212 OO
.ui
.SearchWidget
.prototype.onQueryChange = function () {
13214 this.results
.clearItems();
13218 * Handle select widget enter key events.
13220 * Selects highlighted item.
13222 * @param {string} value New value
13224 OO
.ui
.SearchWidget
.prototype.onQueryEnter = function () {
13226 this.results
.selectItem( this.results
.getHighlightedItem() );
13230 * Handle select widget highlight events.
13232 * @param {OO.ui.OptionWidget} item Highlighted item
13235 OO
.ui
.SearchWidget
.prototype.onResultsHighlight = function ( item
) {
13236 this.emit( 'highlight', item
? item
.getData() : null );
13240 * Handle select widget select events.
13242 * @param {OO.ui.OptionWidget} item Selected item
13245 OO
.ui
.SearchWidget
.prototype.onResultsSelect = function ( item
) {
13246 this.emit( 'select', item
? item
.getData() : null );
13250 * Get the query input.
13252 * @return {OO.ui.TextInputWidget} Query input
13254 OO
.ui
.SearchWidget
.prototype.getQuery = function () {
13259 * Get the results list.
13261 * @return {OO.ui.SelectWidget} Select list
13263 OO
.ui
.SearchWidget
.prototype.getResults = function () {
13264 return this.results
;
13268 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
13269 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
13270 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
13273 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
13274 * information, please see the [OOjs UI documentation on MediaWiki][1].
13277 * // Example of a select widget with three options
13278 * var select=new OO.ui.SelectWidget( {
13280 * new OO.ui.OptionWidget( {
13282 * label: 'Option One',
13284 * new OO.ui.OptionWidget( {
13286 * label: 'Option Two',
13288 * new OO.ui.OptionWidget( {
13290 * label: 'Option Three',
13294 * $('body').append(select.$element);
13296 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13299 * @extends OO.ui.Widget
13300 * @mixins OO.ui.GroupElement
13303 * @param {Object} [config] Configuration options
13304 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
13305 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
13306 * the [OOjs UI documentation on MediaWiki] [2] for examples.
13307 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13309 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
13310 // Configuration initialization
13311 config
= config
|| {};
13313 // Parent constructor
13314 OO
.ui
.SelectWidget
.super.call( this, config
);
13316 // Mixin constructors
13317 OO
.ui
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
13320 this.pressed
= false;
13321 this.selecting
= null;
13322 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
13323 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
13324 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
13327 this.$element
.on( {
13328 mousedown
: this.onMouseDown
.bind( this ),
13329 mouseover
: this.onMouseOver
.bind( this ),
13330 mouseleave
: this.onMouseLeave
.bind( this )
13335 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
13336 .attr( 'role', 'listbox' );
13337 if ( Array
.isArray( config
.items
) ) {
13338 this.addItems( config
.items
);
13344 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
13346 // Need to mixin base class as well
13347 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.GroupElement
);
13348 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.GroupWidget
);
13355 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
13357 * @param {OO.ui.OptionWidget|null} item Highlighted item
13363 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
13364 * pressed state of an option.
13366 * @param {OO.ui.OptionWidget|null} item Pressed item
13372 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
13374 * @param {OO.ui.OptionWidget|null} item Selected item
13379 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
13380 * @param {OO.ui.OptionWidget|null} item Chosen item
13386 * An `add` event is emitted when options are added to the select with the #addItems method.
13388 * @param {OO.ui.OptionWidget[]} items Added items
13389 * @param {number} index Index of insertion point
13395 * A `remove` event is emitted when options are removed from the select with the #clearItems
13396 * or #removeItems methods.
13398 * @param {OO.ui.OptionWidget[]} items Removed items
13404 * Handle mouse down events.
13407 * @param {jQuery.Event} e Mouse down event
13409 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
13412 if ( !this.isDisabled() && e
.which
=== 1 ) {
13413 this.togglePressed( true );
13414 item
= this.getTargetItem( e
);
13415 if ( item
&& item
.isSelectable() ) {
13416 this.pressItem( item
);
13417 this.selecting
= item
;
13418 this.getElementDocument().addEventListener(
13420 this.onMouseUpHandler
,
13423 this.getElementDocument().addEventListener(
13425 this.onMouseMoveHandler
,
13434 * Handle mouse up events.
13437 * @param {jQuery.Event} e Mouse up event
13439 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
13442 this.togglePressed( false );
13443 if ( !this.selecting
) {
13444 item
= this.getTargetItem( e
);
13445 if ( item
&& item
.isSelectable() ) {
13446 this.selecting
= item
;
13449 if ( !this.isDisabled() && e
.which
=== 1 && this.selecting
) {
13450 this.pressItem( null );
13451 this.chooseItem( this.selecting
);
13452 this.selecting
= null;
13455 this.getElementDocument().removeEventListener(
13457 this.onMouseUpHandler
,
13460 this.getElementDocument().removeEventListener(
13462 this.onMouseMoveHandler
,
13470 * Handle mouse move events.
13473 * @param {jQuery.Event} e Mouse move event
13475 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
13478 if ( !this.isDisabled() && this.pressed
) {
13479 item
= this.getTargetItem( e
);
13480 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
13481 this.pressItem( item
);
13482 this.selecting
= item
;
13489 * Handle mouse over events.
13492 * @param {jQuery.Event} e Mouse over event
13494 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
13497 if ( !this.isDisabled() ) {
13498 item
= this.getTargetItem( e
);
13499 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
13505 * Handle mouse leave events.
13508 * @param {jQuery.Event} e Mouse over event
13510 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
13511 if ( !this.isDisabled() ) {
13512 this.highlightItem( null );
13518 * Handle key down events.
13521 * @param {jQuery.Event} e Key down event
13523 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
13526 currentItem
= this.getHighlightedItem() || this.getSelectedItem();
13528 if ( !this.isDisabled() && this.isVisible() ) {
13529 switch ( e
.keyCode
) {
13530 case OO
.ui
.Keys
.ENTER
:
13531 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
13532 // Was only highlighted, now let's select it. No-op if already selected.
13533 this.chooseItem( currentItem
);
13537 case OO
.ui
.Keys
.UP
:
13538 case OO
.ui
.Keys
.LEFT
:
13539 nextItem
= this.getRelativeSelectableItem( currentItem
, -1 );
13542 case OO
.ui
.Keys
.DOWN
:
13543 case OO
.ui
.Keys
.RIGHT
:
13544 nextItem
= this.getRelativeSelectableItem( currentItem
, 1 );
13547 case OO
.ui
.Keys
.ESCAPE
:
13548 case OO
.ui
.Keys
.TAB
:
13549 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
13550 currentItem
.setHighlighted( false );
13552 this.unbindKeyDownListener();
13553 // Don't prevent tabbing away / defocusing
13559 if ( nextItem
.constructor.static.highlightable
) {
13560 this.highlightItem( nextItem
);
13562 this.chooseItem( nextItem
);
13564 nextItem
.scrollElementIntoView();
13568 // Can't just return false, because e is not always a jQuery event
13569 e
.preventDefault();
13570 e
.stopPropagation();
13576 * Bind key down listener.
13580 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
13581 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
13585 * Unbind key down listener.
13589 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
13590 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
13594 * Get the closest item to a jQuery.Event.
13597 * @param {jQuery.Event} e
13598 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
13600 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
13601 var $item
= $( e
.target
).closest( '.oo-ui-optionWidget' );
13602 if ( $item
.length
) {
13603 return $item
.data( 'oo-ui-optionWidget' );
13609 * Get selected item.
13611 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
13613 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
13616 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13617 if ( this.items
[ i
].isSelected() ) {
13618 return this.items
[ i
];
13625 * Get highlighted item.
13627 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
13629 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
13632 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13633 if ( this.items
[ i
].isHighlighted() ) {
13634 return this.items
[ i
];
13641 * Toggle pressed state.
13643 * Press is a state that occurs when a user mouses down on an item, but
13644 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
13645 * until the user releases the mouse.
13647 * @param {boolean} pressed An option is being pressed
13649 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
13650 if ( pressed
=== undefined ) {
13651 pressed
= !this.pressed
;
13653 if ( pressed
!== this.pressed
) {
13655 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
13656 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
13657 this.pressed
= pressed
;
13662 * Highlight an option. If the `item` param is omitted, no options will be highlighted
13663 * and any existing highlight will be removed. The highlight is mutually exclusive.
13665 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
13669 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
13670 var i
, len
, highlighted
,
13673 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13674 highlighted
= this.items
[ i
] === item
;
13675 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
13676 this.items
[ i
].setHighlighted( highlighted
);
13681 this.emit( 'highlight', item
);
13688 * Programmatically select an option by its reference. If the `item` parameter is omitted,
13689 * all options will be deselected.
13691 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
13695 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
13696 var i
, len
, selected
,
13699 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13700 selected
= this.items
[ i
] === item
;
13701 if ( this.items
[ i
].isSelected() !== selected
) {
13702 this.items
[ i
].setSelected( selected
);
13707 this.emit( 'select', item
);
13716 * Press is a state that occurs when a user mouses down on an item, but has not
13717 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
13718 * releases the mouse.
13720 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
13724 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
13725 var i
, len
, pressed
,
13728 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13729 pressed
= this.items
[ i
] === item
;
13730 if ( this.items
[ i
].isPressed() !== pressed
) {
13731 this.items
[ i
].setPressed( pressed
);
13736 this.emit( 'press', item
);
13745 * Note that ‘choose’ should never be modified programmatically. A user can choose
13746 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
13747 * use the #selectItem method.
13749 * This method is identical to #selectItem, but may vary in subclasses that take additional action
13750 * when users choose an item with the keyboard or mouse.
13752 * @param {OO.ui.OptionWidget} item Item to choose
13756 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
13757 this.selectItem( item
);
13758 this.emit( 'choose', item
);
13764 * Get an option by its position relative to the specified item (or to the start of the option array,
13765 * if item is `null`). The direction in which to search through the option array is specified with a
13766 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
13767 * `null` if there are no options in the array.
13769 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
13770 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
13771 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
13773 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
) {
13774 var currentIndex
, nextIndex
, i
,
13775 increase
= direction
> 0 ? 1 : -1,
13776 len
= this.items
.length
;
13778 if ( item
instanceof OO
.ui
.OptionWidget
) {
13779 currentIndex
= $.inArray( item
, this.items
);
13780 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
13782 // If no item is selected and moving forward, start at the beginning.
13783 // If moving backward, start at the end.
13784 nextIndex
= direction
> 0 ? 0 : len
- 1;
13787 for ( i
= 0; i
< len
; i
++ ) {
13788 item
= this.items
[ nextIndex
];
13789 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() ) {
13792 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
13798 * Get the next selectable item or `null` if there are no selectable items.
13799 * Disabled options and menu-section markers and breaks are not selectable.
13801 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
13803 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
13806 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13807 item
= this.items
[ i
];
13808 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() ) {
13817 * Add an array of options to the select. Optionally, an index number can be used to
13818 * specify an insertion point.
13820 * @param {OO.ui.OptionWidget[]} items Items to add
13821 * @param {number} [index] Index to insert items after
13825 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
13827 OO
.ui
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
13829 // Always provide an index, even if it was omitted
13830 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
13836 * Remove the specified array of options from the select. Options will be detached
13837 * from the DOM, not removed, so they can be reused later. To remove all options from
13838 * the select, you may wish to use the #clearItems method instead.
13840 * @param {OO.ui.OptionWidget[]} items Items to remove
13844 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
13847 // Deselect items being removed
13848 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
13850 if ( item
.isSelected() ) {
13851 this.selectItem( null );
13856 OO
.ui
.GroupWidget
.prototype.removeItems
.call( this, items
);
13858 this.emit( 'remove', items
);
13864 * Clear all options from the select. Options will be detached from the DOM, not removed,
13865 * so that they can be reused later. To remove a subset of options from the select, use
13866 * the #removeItems method.
13871 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
13872 var items
= this.items
.slice();
13875 OO
.ui
.GroupWidget
.prototype.clearItems
.call( this );
13878 this.selectItem( null );
13880 this.emit( 'remove', items
);
13886 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
13887 * button options and is used together with
13888 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
13889 * highlighting, choosing, and selecting mutually exclusive options. Please see
13890 * the [OOjs UI documentation on MediaWiki] [1] for more information.
13893 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
13894 * var option1 = new OO.ui.ButtonOptionWidget( {
13896 * label: 'Option 1',
13897 * title:'Button option 1'
13900 * var option2 = new OO.ui.ButtonOptionWidget( {
13902 * label: 'Option 2',
13903 * title:'Button option 2'
13906 * var option3 = new OO.ui.ButtonOptionWidget( {
13908 * label: 'Option 3',
13909 * title:'Button option 3'
13912 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
13913 * items: [option1, option2, option3]
13915 * $('body').append(buttonSelect.$element);
13917 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13920 * @extends OO.ui.SelectWidget
13921 * @mixins OO.ui.TabIndexedElement
13924 * @param {Object} [config] Configuration options
13926 OO
.ui
.ButtonSelectWidget
= function OoUiButtonSelectWidget( config
) {
13927 // Parent constructor
13928 OO
.ui
.ButtonSelectWidget
.super.call( this, config
);
13930 // Mixin constructors
13931 OO
.ui
.TabIndexedElement
.call( this, config
);
13934 this.$element
.on( {
13935 focus
: this.bindKeyDownListener
.bind( this ),
13936 blur
: this.unbindKeyDownListener
.bind( this )
13940 this.$element
.addClass( 'oo-ui-buttonSelectWidget' );
13945 OO
.inheritClass( OO
.ui
.ButtonSelectWidget
, OO
.ui
.SelectWidget
);
13946 OO
.mixinClass( OO
.ui
.ButtonSelectWidget
, OO
.ui
.TabIndexedElement
);
13949 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
13950 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
13951 * an interface for adding, removing and selecting options.
13952 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
13955 * // A RadioSelectWidget with RadioOptions.
13956 * var option1 = new OO.ui.RadioOptionWidget( {
13958 * label: 'Selected radio option'
13961 * var option2 = new OO.ui.RadioOptionWidget( {
13963 * label: 'Unselected radio option'
13966 * var radioSelect=new OO.ui.RadioSelectWidget( {
13967 * items: [option1, option2]
13970 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
13971 * radioSelect.selectItem( option1 );
13973 * $('body').append(radioSelect.$element);
13975 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13979 * @extends OO.ui.SelectWidget
13980 * @mixins OO.ui.TabIndexedElement
13983 * @param {Object} [config] Configuration options
13985 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
13986 // Parent constructor
13987 OO
.ui
.RadioSelectWidget
.super.call( this, config
);
13989 // Mixin constructors
13990 OO
.ui
.TabIndexedElement
.call( this, config
);
13993 this.$element
.on( {
13994 focus
: this.bindKeyDownListener
.bind( this ),
13995 blur
: this.unbindKeyDownListener
.bind( this )
13999 this.$element
.addClass( 'oo-ui-radioSelectWidget' );
14004 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
14005 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.TabIndexedElement
);
14008 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
14009 * is used together with OO.ui.MenuOptionWidget. See {@link OO.ui.DropdownWidget DropdownWidget} and
14010 * {@link OO.ui.ComboBoxWidget ComboBoxWidget} for examples of interfaces that contain menus.
14011 * MenuSelectWidgets themselves are not designed to be instantiated directly, rather subclassed
14012 * and customized to be opened, closed, and displayed as needed.
14014 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
14015 * mouse outside the menu.
14017 * Menus also have support for keyboard interaction:
14019 * - Enter/Return key: choose and select a menu option
14020 * - Up-arrow key: highlight the previous menu option
14021 * - Down-arrow key: highlight the next menu option
14022 * - Esc key: hide the menu
14024 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
14025 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14028 * @extends OO.ui.SelectWidget
14029 * @mixins OO.ui.ClippableElement
14032 * @param {Object} [config] Configuration options
14033 * @cfg {OO.ui.TextInputWidget} [input] Input to bind keyboard handlers to
14034 * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
14035 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
14037 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
14038 // Configuration initialization
14039 config
= config
|| {};
14041 // Parent constructor
14042 OO
.ui
.MenuSelectWidget
.super.call( this, config
);
14044 // Mixin constructors
14045 OO
.ui
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
14048 this.newItems
= null;
14049 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
14050 this.$input
= config
.input
? config
.input
.$input
: null;
14051 this.$widget
= config
.widget
? config
.widget
.$element
: null;
14052 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
14056 .addClass( 'oo-ui-menuSelectWidget' )
14057 .attr( 'role', 'menu' );
14059 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
14060 // that reference properties not initialized at that time of parent class construction
14061 // TODO: Find a better way to handle post-constructor setup
14062 this.visible
= false;
14063 this.$element
.addClass( 'oo-ui-element-hidden' );
14068 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
14069 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.ClippableElement
);
14074 * Handles document mouse down events.
14077 * @param {jQuery.Event} e Key down event
14079 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
14081 !OO
.ui
.contains( this.$element
[ 0 ], e
.target
, true ) &&
14082 ( !this.$widget
|| !OO
.ui
.contains( this.$widget
[ 0 ], e
.target
, true ) )
14084 this.toggle( false );
14091 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
14092 var currentItem
= this.getHighlightedItem() || this.getSelectedItem();
14094 if ( !this.isDisabled() && this.isVisible() ) {
14095 switch ( e
.keyCode
) {
14096 case OO
.ui
.Keys
.LEFT
:
14097 case OO
.ui
.Keys
.RIGHT
:
14098 // Do nothing if a text field is associated, arrow keys will be handled natively
14099 if ( !this.$input
) {
14100 OO
.ui
.MenuSelectWidget
.super.prototype.onKeyDown
.call( this, e
);
14103 case OO
.ui
.Keys
.ESCAPE
:
14104 case OO
.ui
.Keys
.TAB
:
14105 if ( currentItem
) {
14106 currentItem
.setHighlighted( false );
14108 this.toggle( false );
14109 // Don't prevent tabbing away, prevent defocusing
14110 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
14111 e
.preventDefault();
14112 e
.stopPropagation();
14116 OO
.ui
.MenuSelectWidget
.super.prototype.onKeyDown
.call( this, e
);
14125 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
14126 if ( this.$input
) {
14127 this.$input
.on( 'keydown', this.onKeyDownHandler
);
14129 OO
.ui
.MenuSelectWidget
.super.prototype.bindKeyDownListener
.call( this );
14136 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
14137 if ( this.$input
) {
14138 this.$input
.off( 'keydown', this.onKeyDownHandler
);
14140 OO
.ui
.MenuSelectWidget
.super.prototype.unbindKeyDownListener
.call( this );
14147 * This will close the menu, unlike #selectItem which only changes selection.
14149 * @param {OO.ui.OptionWidget} item Item to choose
14152 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
14153 OO
.ui
.MenuSelectWidget
.super.prototype.chooseItem
.call( this, item
);
14154 this.toggle( false );
14161 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
14165 OO
.ui
.MenuSelectWidget
.super.prototype.addItems
.call( this, items
, index
);
14168 if ( !this.newItems
) {
14169 this.newItems
= [];
14172 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
14174 if ( this.isVisible() ) {
14175 // Defer fitting label until item has been attached
14178 this.newItems
.push( item
);
14182 // Reevaluate clipping
14191 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
14193 OO
.ui
.MenuSelectWidget
.super.prototype.removeItems
.call( this, items
);
14195 // Reevaluate clipping
14204 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
14206 OO
.ui
.MenuSelectWidget
.super.prototype.clearItems
.call( this );
14208 // Reevaluate clipping
14217 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
14218 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
14221 change
= visible
!== this.isVisible();
14224 OO
.ui
.MenuSelectWidget
.super.prototype.toggle
.call( this, visible
);
14228 this.bindKeyDownListener();
14230 if ( this.newItems
&& this.newItems
.length
) {
14231 for ( i
= 0, len
= this.newItems
.length
; i
< len
; i
++ ) {
14232 this.newItems
[ i
].fitLabel();
14234 this.newItems
= null;
14236 this.toggleClipping( true );
14239 if ( this.autoHide
) {
14240 this.getElementDocument().addEventListener(
14241 'mousedown', this.onDocumentMouseDownHandler
, true
14245 this.unbindKeyDownListener();
14246 this.getElementDocument().removeEventListener(
14247 'mousedown', this.onDocumentMouseDownHandler
, true
14249 this.toggleClipping( false );
14257 * TextInputMenuSelectWidget is a menu that is specially designed to be positioned beneath
14258 * a {@link OO.ui.TextInputWidget text input} field. The menu's position is automatically
14259 * calculated and maintained when the menu is toggled or the window is resized.
14260 * See OO.ui.ComboBoxWidget for an example of a widget that uses this class.
14263 * @extends OO.ui.MenuSelectWidget
14266 * @param {OO.ui.TextInputWidget} inputWidget Text input widget to provide menu for
14267 * @param {Object} [config] Configuration options
14268 * @cfg {jQuery} [$container=input.$element] Element to render menu under
14270 OO
.ui
.TextInputMenuSelectWidget
= function OoUiTextInputMenuSelectWidget( inputWidget
, config
) {
14271 // Allow passing positional parameters inside the config object
14272 if ( OO
.isPlainObject( inputWidget
) && config
=== undefined ) {
14273 config
= inputWidget
;
14274 inputWidget
= config
.inputWidget
;
14277 // Configuration initialization
14278 config
= config
|| {};
14280 // Parent constructor
14281 OO
.ui
.TextInputMenuSelectWidget
.super.call( this, config
);
14284 this.inputWidget
= inputWidget
;
14285 this.$container
= config
.$container
|| this.inputWidget
.$element
;
14286 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
14289 this.$element
.addClass( 'oo-ui-textInputMenuSelectWidget' );
14294 OO
.inheritClass( OO
.ui
.TextInputMenuSelectWidget
, OO
.ui
.MenuSelectWidget
);
14299 * Handle window resize event.
14302 * @param {jQuery.Event} e Window resize event
14304 OO
.ui
.TextInputMenuSelectWidget
.prototype.onWindowResize = function () {
14311 OO
.ui
.TextInputMenuSelectWidget
.prototype.toggle = function ( visible
) {
14312 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
14314 var change
= visible
!== this.isVisible();
14316 if ( change
&& visible
) {
14317 // Make sure the width is set before the parent method runs.
14318 // After this we have to call this.position(); again to actually
14319 // position ourselves correctly.
14324 OO
.ui
.TextInputMenuSelectWidget
.super.prototype.toggle
.call( this, visible
);
14327 if ( this.isVisible() ) {
14329 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler
);
14331 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler
);
14339 * Position the menu.
14344 OO
.ui
.TextInputMenuSelectWidget
.prototype.position = function () {
14345 var $container
= this.$container
,
14346 pos
= OO
.ui
.Element
.static.getRelativePosition( $container
, this.$element
.offsetParent() );
14348 // Position under input
14349 pos
.top
+= $container
.height();
14350 this.$element
.css( pos
);
14353 this.setIdealSize( $container
.width() );
14354 // We updated the position, so re-evaluate the clipping state
14361 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
14362 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
14364 * ####Currently, this class is only used by {@link OO.ui.BookletLayout BookletLayouts}.####
14367 * @extends OO.ui.SelectWidget
14368 * @mixins OO.ui.TabIndexedElement
14371 * @param {Object} [config] Configuration options
14373 OO
.ui
.OutlineSelectWidget
= function OoUiOutlineSelectWidget( config
) {
14374 // Parent constructor
14375 OO
.ui
.OutlineSelectWidget
.super.call( this, config
);
14377 // Mixin constructors
14378 OO
.ui
.TabIndexedElement
.call( this, config
);
14381 this.$element
.on( {
14382 focus
: this.bindKeyDownListener
.bind( this ),
14383 blur
: this.unbindKeyDownListener
.bind( this )
14387 this.$element
.addClass( 'oo-ui-outlineSelectWidget' );
14392 OO
.inheritClass( OO
.ui
.OutlineSelectWidget
, OO
.ui
.SelectWidget
);
14393 OO
.mixinClass( OO
.ui
.OutlineSelectWidget
, OO
.ui
.TabIndexedElement
);
14396 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
14397 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
14398 * visually by a slider in the leftmost position.
14401 * // Toggle switches in the 'off' and 'on' position.
14402 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget({
14405 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget({
14409 * // Create a FieldsetLayout to layout and label switches
14410 * var fieldset = new OO.ui.FieldsetLayout( {
14411 * label: 'Toggle switches'
14413 * fieldset.addItems( [
14414 * new OO.ui.FieldLayout( toggleSwitch1, {label : 'Off', align : 'top'}),
14415 * new OO.ui.FieldLayout( toggleSwitch2, {label : 'On', align : 'top'})
14417 * $( 'body' ).append( fieldset.$element );
14420 * @extends OO.ui.Widget
14421 * @mixins OO.ui.ToggleWidget
14422 * @mixins OO.ui.TabIndexedElement
14425 * @param {Object} [config] Configuration options
14426 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
14427 * By default, the toggle switch is in the 'off' position.
14429 OO
.ui
.ToggleSwitchWidget
= function OoUiToggleSwitchWidget( config
) {
14430 // Parent constructor
14431 OO
.ui
.ToggleSwitchWidget
.super.call( this, config
);
14433 // Mixin constructors
14434 OO
.ui
.ToggleWidget
.call( this, config
);
14435 OO
.ui
.TabIndexedElement
.call( this, config
);
14438 this.dragging
= false;
14439 this.dragStart
= null;
14440 this.sliding
= false;
14441 this.$glow
= $( '<span>' );
14442 this.$grip
= $( '<span>' );
14445 this.$element
.on( {
14446 click
: this.onClick
.bind( this ),
14447 keypress
: this.onKeyPress
.bind( this )
14451 this.$glow
.addClass( 'oo-ui-toggleSwitchWidget-glow' );
14452 this.$grip
.addClass( 'oo-ui-toggleSwitchWidget-grip' );
14454 .addClass( 'oo-ui-toggleSwitchWidget' )
14455 .attr( 'role', 'checkbox' )
14456 .append( this.$glow
, this.$grip
);
14461 OO
.inheritClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.Widget
);
14462 OO
.mixinClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.ToggleWidget
);
14463 OO
.mixinClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.TabIndexedElement
);
14468 * Handle mouse click events.
14471 * @param {jQuery.Event} e Mouse click event
14473 OO
.ui
.ToggleSwitchWidget
.prototype.onClick = function ( e
) {
14474 if ( !this.isDisabled() && e
.which
=== 1 ) {
14475 this.setValue( !this.value
);
14481 * Handle key press events.
14484 * @param {jQuery.Event} e Key press event
14486 OO
.ui
.ToggleSwitchWidget
.prototype.onKeyPress = function ( e
) {
14487 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
14488 this.setValue( !this.value
);