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-04T23:55:34Z
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). Please see the [OOjs UI documentation on MediaWiki][1] for more information.
328 * // Example: An action set used in a process dialog
329 * function ProcessDialog( config ) {
330 * ProcessDialog.super.call( this, config );
332 * OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
333 * ProcessDialog.static.title = 'An action set in a process dialog';
334 * // An action set that uses modes ('edit' and 'help' mode, in this example).
335 * ProcessDialog.static.actions = [
336 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
337 * { action: 'help', modes: 'edit', label: 'Help' },
338 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
339 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
342 * ProcessDialog.prototype.initialize = function () {
343 * ProcessDialog.super.prototype.initialize.apply( this, arguments );
344 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
345 * 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>' );
346 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
347 * 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>' );
348 * this.stackLayout= new OO.ui.StackLayout( {
349 * items: [ this.panel1, this.panel2 ]
351 * this.$body.append( this.stackLayout.$element );
353 * ProcessDialog.prototype.getSetupProcess = function ( data ) {
354 * return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
355 * .next( function () {
356 * this.actions.setMode('edit');
359 * ProcessDialog.prototype.getActionProcess = function ( action ) {
360 * if ( action === 'help' ) {
361 * this.actions.setMode( 'help' );
362 * this.stackLayout.setItem( this.panel2 );
363 * } else if ( action === 'back' ) {
364 * this.actions.setMode( 'edit' );
365 * this.stackLayout.setItem( this.panel1 );
366 * } else if ( action === 'continue' ) {
368 * return new OO.ui.Process( function () {
372 * return ProcessDialog.super.prototype.getActionProcess.call( this, action );
374 * ProcessDialog.prototype.getBodyHeight = function () {
375 * return this.panel1.$element.outerHeight( true );
377 * var windowManager = new OO.ui.WindowManager();
378 * $( 'body' ).append( windowManager.$element );
379 * var processDialog = new ProcessDialog({
381 * windowManager.addWindows( [ processDialog ] );
382 * windowManager.openWindow( processDialog );
384 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
388 * @mixins OO.EventEmitter
391 * @param {Object} [config] Configuration options
393 OO
.ui
.ActionSet
= function OoUiActionSet( config
) {
394 // Configuration initialization
395 config
= config
|| {};
397 // Mixin constructors
398 OO
.EventEmitter
.call( this );
403 actions
: 'getAction',
407 this.categorized
= {};
410 this.organized
= false;
411 this.changing
= false;
412 this.changed
= false;
417 OO
.mixinClass( OO
.ui
.ActionSet
, OO
.EventEmitter
);
419 /* Static Properties */
422 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
423 * header of a {@link OO.ui.ProcessDialog process dialog}.
424 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
426 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
433 OO
.ui
.ActionSet
.static.specialFlags
= [ 'safe', 'primary' ];
439 * @param {OO.ui.ActionWidget} action Action that was clicked
444 * @param {OO.ui.ActionWidget} action Action that was resized
449 * @param {OO.ui.ActionWidget[]} added Actions added
454 * @param {OO.ui.ActionWidget[]} added Actions removed
464 * Handle action change events.
469 OO
.ui
.ActionSet
.prototype.onActionChange = function () {
470 this.organized
= false;
471 if ( this.changing
) {
474 this.emit( 'change' );
479 * Check if a action is one of the special actions.
481 * @param {OO.ui.ActionWidget} action Action to check
482 * @return {boolean} Action is special
484 OO
.ui
.ActionSet
.prototype.isSpecial = function ( action
) {
487 for ( flag
in this.special
) {
488 if ( action
=== this.special
[ flag
] ) {
499 * @param {Object} [filters] Filters to use, omit to get all actions
500 * @param {string|string[]} [filters.actions] Actions that actions must have
501 * @param {string|string[]} [filters.flags] Flags that actions must have
502 * @param {string|string[]} [filters.modes] Modes that actions must have
503 * @param {boolean} [filters.visible] Actions must be visible
504 * @param {boolean} [filters.disabled] Actions must be disabled
505 * @return {OO.ui.ActionWidget[]} Actions matching all criteria
507 OO
.ui
.ActionSet
.prototype.get = function ( filters
) {
508 var i
, len
, list
, category
, actions
, index
, match
, matches
;
513 // Collect category candidates
515 for ( category
in this.categorized
) {
516 list
= filters
[ category
];
518 if ( !Array
.isArray( list
) ) {
521 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
522 actions
= this.categorized
[ category
][ list
[ i
] ];
523 if ( Array
.isArray( actions
) ) {
524 matches
.push
.apply( matches
, actions
);
529 // Remove by boolean filters
530 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
531 match
= matches
[ i
];
533 ( filters
.visible
!== undefined && match
.isVisible() !== filters
.visible
) ||
534 ( filters
.disabled
!== undefined && match
.isDisabled() !== filters
.disabled
)
536 matches
.splice( i
, 1 );
542 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
543 match
= matches
[ i
];
544 index
= matches
.lastIndexOf( match
);
545 while ( index
!== i
) {
546 matches
.splice( index
, 1 );
548 index
= matches
.lastIndexOf( match
);
553 return this.list
.slice();
557 * Get special actions.
559 * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'.
560 * Special flags can be configured by changing #static-specialFlags in a subclass.
562 * @return {OO.ui.ActionWidget|null} Safe action
564 OO
.ui
.ActionSet
.prototype.getSpecial = function () {
566 return $.extend( {}, this.special
);
572 * Other actions include all non-special visible actions.
574 * @return {OO.ui.ActionWidget[]} Other actions
576 OO
.ui
.ActionSet
.prototype.getOthers = function () {
578 return this.others
.slice();
582 * Toggle actions based on their modes.
584 * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive
585 * visibility; matching actions will be shown, non-matching actions will be hidden.
587 * @param {string} mode Mode actions must have
592 OO
.ui
.ActionSet
.prototype.setMode = function ( mode
) {
595 this.changing
= true;
596 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
597 action
= this.list
[ i
];
598 action
.toggle( action
.hasMode( mode
) );
601 this.organized
= false;
602 this.changing
= false;
603 this.emit( 'change' );
609 * Change which actions are able to be performed.
611 * Actions with matching actions will be disabled/enabled. Other actions will not be changed.
613 * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values
614 * indicate actions are able to be performed
617 OO
.ui
.ActionSet
.prototype.setAbilities = function ( actions
) {
618 var i
, len
, action
, item
;
620 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
621 item
= this.list
[ i
];
622 action
= item
.getAction();
623 if ( actions
[ action
] !== undefined ) {
624 item
.setDisabled( !actions
[ action
] );
632 * Executes a function once per action.
634 * When making changes to multiple actions, use this method instead of iterating over the actions
635 * manually to defer emitting a change event until after all actions have been changed.
637 * @param {Object|null} actions Filters to use for which actions to iterate over; see #get
638 * @param {Function} callback Callback to run for each action; callback is invoked with three
639 * arguments: the action, the action's index, the list of actions being iterated over
642 OO
.ui
.ActionSet
.prototype.forEach = function ( filter
, callback
) {
643 this.changed
= false;
644 this.changing
= true;
645 this.get( filter
).forEach( callback
);
646 this.changing
= false;
647 if ( this.changed
) {
648 this.emit( 'change' );
657 * @param {OO.ui.ActionWidget[]} actions Actions to add
662 OO
.ui
.ActionSet
.prototype.add = function ( actions
) {
665 this.changing
= true;
666 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
667 action
= actions
[ i
];
668 action
.connect( this, {
669 click
: [ 'emit', 'click', action
],
670 resize
: [ 'emit', 'resize', action
],
671 toggle
: [ 'onActionChange' ]
673 this.list
.push( action
);
675 this.organized
= false;
676 this.emit( 'add', actions
);
677 this.changing
= false;
678 this.emit( 'change' );
686 * @param {OO.ui.ActionWidget[]} actions Actions to remove
691 OO
.ui
.ActionSet
.prototype.remove = function ( actions
) {
692 var i
, len
, index
, action
;
694 this.changing
= true;
695 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
696 action
= actions
[ i
];
697 index
= this.list
.indexOf( action
);
698 if ( index
!== -1 ) {
699 action
.disconnect( this );
700 this.list
.splice( index
, 1 );
703 this.organized
= false;
704 this.emit( 'remove', actions
);
705 this.changing
= false;
706 this.emit( 'change' );
712 * Remove all actions.
718 OO
.ui
.ActionSet
.prototype.clear = function () {
720 removed
= this.list
.slice();
722 this.changing
= true;
723 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
724 action
= this.list
[ i
];
725 action
.disconnect( this );
730 this.organized
= false;
731 this.emit( 'remove', removed
);
732 this.changing
= false;
733 this.emit( 'change' );
741 * This is called whenever organized information is requested. It will only reorganize the actions
742 * if something has changed since the last time it ran.
747 OO
.ui
.ActionSet
.prototype.organize = function () {
748 var i
, iLen
, j
, jLen
, flag
, action
, category
, list
, item
, special
,
749 specialFlags
= this.constructor.static.specialFlags
;
751 if ( !this.organized
) {
752 this.categorized
= {};
755 for ( i
= 0, iLen
= this.list
.length
; i
< iLen
; i
++ ) {
756 action
= this.list
[ i
];
757 if ( action
.isVisible() ) {
758 // Populate categories
759 for ( category
in this.categories
) {
760 if ( !this.categorized
[ category
] ) {
761 this.categorized
[ category
] = {};
763 list
= action
[ this.categories
[ category
] ]();
764 if ( !Array
.isArray( list
) ) {
767 for ( j
= 0, jLen
= list
.length
; j
< jLen
; j
++ ) {
769 if ( !this.categorized
[ category
][ item
] ) {
770 this.categorized
[ category
][ item
] = [];
772 this.categorized
[ category
][ item
].push( action
);
775 // Populate special/others
777 for ( j
= 0, jLen
= specialFlags
.length
; j
< jLen
; j
++ ) {
778 flag
= specialFlags
[ j
];
779 if ( !this.special
[ flag
] && action
.hasFlag( flag
) ) {
780 this.special
[ flag
] = action
;
786 this.others
.push( action
);
790 this.organized
= true;
797 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
798 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
799 * connected to them and can't be interacted with.
805 * @param {Object} [config] Configuration options
806 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
807 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
809 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
810 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
811 * @cfg {string} [text] Text to insert
812 * @cfg {Array} [content] An array of content elements to append (after #text).
813 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
814 * Instances of OO.ui.Element will have their $element appended.
815 * @cfg {jQuery} [$content] Content elements to append (after #text)
816 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
817 * Data can also be specified with the #setData method.
819 OO
.ui
.Element
= function OoUiElement( config
) {
820 // Configuration initialization
821 config
= config
|| {};
826 this.data
= config
.data
;
827 this.$element
= config
.$element
||
828 $( document
.createElement( this.getTagName() ) );
829 this.elementGroup
= null;
830 this.debouncedUpdateThemeClassesHandler
= this.debouncedUpdateThemeClasses
.bind( this );
831 this.updateThemeClassesPending
= false;
834 if ( Array
.isArray( config
.classes
) ) {
835 this.$element
.addClass( config
.classes
.join( ' ' ) );
838 this.$element
.attr( 'id', config
.id
);
841 this.$element
.text( config
.text
);
843 if ( config
.content
) {
844 // The `content` property treats plain strings as text; use an
845 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
846 // appropriate $element appended.
847 this.$element
.append( config
.content
.map( function ( v
) {
848 if ( typeof v
=== 'string' ) {
849 // Escape string so it is properly represented in HTML.
850 return document
.createTextNode( v
);
851 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
854 } else if ( v
instanceof OO
.ui
.Element
) {
860 if ( config
.$content
) {
861 // The `$content` property treats plain strings as HTML.
862 this.$element
.append( config
.$content
);
868 OO
.initClass( OO
.ui
.Element
);
870 /* Static Properties */
873 * The name of the HTML tag used by the element.
875 * The static value may be ignored if the #getTagName method is overridden.
881 OO
.ui
.Element
.static.tagName
= 'div';
886 * Reconstitute a JavaScript object corresponding to a widget created
887 * by the PHP implementation.
889 * @param {string|HTMLElement|jQuery} idOrNode
890 * A DOM id (if a string) or node for the widget to infuse.
891 * @return {OO.ui.Element}
892 * The `OO.ui.Element` corresponding to this (infusable) document node.
893 * For `Tag` objects emitted on the HTML side (used occasionally for content)
894 * the value returned is a newly-created Element wrapping around the existing
897 OO
.ui
.Element
.static.infuse = function ( idOrNode
) {
898 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, true );
899 // Verify that the type matches up.
900 // FIXME: uncomment after T89721 is fixed (see T90929)
902 if ( !( obj instanceof this['class'] ) ) {
903 throw new Error( 'Infusion type mismatch!' );
910 * Implementation helper for `infuse`; skips the type check and has an
911 * extra property so that only the top-level invocation touches the DOM.
913 * @param {string|HTMLElement|jQuery} idOrNode
914 * @param {boolean} top True only for top-level invocation.
915 * @return {OO.ui.Element}
917 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, top
) {
918 // look for a cached result of a previous infusion.
919 var id
, $elem
, data
, cls
, obj
;
920 if ( typeof idOrNode
=== 'string' ) {
922 $elem
= $( document
.getElementById( id
) );
924 $elem
= $( idOrNode
);
925 id
= $elem
.attr( 'id' );
927 data
= $elem
.data( 'ooui-infused' );
930 if ( data
=== true ) {
931 throw new Error( 'Circular dependency! ' + id
);
935 if ( !$elem
.length
) {
936 throw new Error( 'Widget not found: ' + id
);
938 data
= $elem
.attr( 'data-ooui' );
940 throw new Error( 'No infusion data found: ' + id
);
943 data
= $.parseJSON( data
);
947 if ( !( data
&& data
._
) ) {
948 throw new Error( 'No valid infusion data found: ' + id
);
950 if ( data
._
=== 'Tag' ) {
951 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
952 return new OO
.ui
.Element( { $element
: $elem
} );
956 throw new Error( 'Unknown widget type: ' + id
);
958 $elem
.data( 'ooui-infused', true ); // prevent loops
959 data
.id
= id
; // implicit
960 data
= OO
.copy( data
, null, function deserialize( value
) {
961 if ( OO
.isPlainObject( value
) ) {
963 return OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, false );
966 return new OO
.ui
.HtmlSnippet( value
.html
);
970 // jscs:disable requireCapitalizedConstructors
971 obj
= new cls( data
); // rebuild widget
972 // now replace old DOM with this new DOM.
974 $elem
.replaceWith( obj
.$element
);
976 obj
.$element
.data( 'ooui-infused', obj
);
977 // set the 'data-ooui' attribute so we can identify infused widgets
978 obj
.$element
.attr( 'data-ooui', '' );
983 * Get a jQuery function within a specific document.
986 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
987 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
989 * @return {Function} Bound jQuery function
991 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
992 function wrapper( selector
) {
993 return $( selector
, wrapper
.context
);
996 wrapper
.context
= this.getDocument( context
);
999 wrapper
.$iframe
= $iframe
;
1006 * Get the document of an element.
1009 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1010 * @return {HTMLDocument|null} Document object
1012 OO
.ui
.Element
.static.getDocument = function ( obj
) {
1013 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1014 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
1015 // Empty jQuery selections might have a context
1018 obj
.ownerDocument
||
1022 ( obj
.nodeType
=== 9 && obj
) ||
1027 * Get the window of an element or document.
1030 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1031 * @return {Window} Window object
1033 OO
.ui
.Element
.static.getWindow = function ( obj
) {
1034 var doc
= this.getDocument( obj
);
1035 return doc
.parentWindow
|| doc
.defaultView
;
1039 * Get the direction of an element or document.
1042 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1043 * @return {string} Text direction, either 'ltr' or 'rtl'
1045 OO
.ui
.Element
.static.getDir = function ( obj
) {
1048 if ( obj
instanceof jQuery
) {
1051 isDoc
= obj
.nodeType
=== 9;
1052 isWin
= obj
.document
!== undefined;
1053 if ( isDoc
|| isWin
) {
1059 return $( obj
).css( 'direction' );
1063 * Get the offset between two frames.
1065 * TODO: Make this function not use recursion.
1068 * @param {Window} from Window of the child frame
1069 * @param {Window} [to=window] Window of the parent frame
1070 * @param {Object} [offset] Offset to start with, used internally
1071 * @return {Object} Offset object, containing left and top properties
1073 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
1074 var i
, len
, frames
, frame
, rect
;
1080 offset
= { top
: 0, left
: 0 };
1082 if ( from.parent
=== from ) {
1086 // Get iframe element
1087 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
1088 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
1089 if ( frames
[ i
].contentWindow
=== from ) {
1090 frame
= frames
[ i
];
1095 // Recursively accumulate offset values
1097 rect
= frame
.getBoundingClientRect();
1098 offset
.left
+= rect
.left
;
1099 offset
.top
+= rect
.top
;
1100 if ( from !== to
) {
1101 this.getFrameOffset( from.parent
, offset
);
1108 * Get the offset between two elements.
1110 * The two elements may be in a different frame, but in that case the frame $element is in must
1111 * be contained in the frame $anchor is in.
1114 * @param {jQuery} $element Element whose position to get
1115 * @param {jQuery} $anchor Element to get $element's position relative to
1116 * @return {Object} Translated position coordinates, containing top and left properties
1118 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1119 var iframe
, iframePos
,
1120 pos
= $element
.offset(),
1121 anchorPos
= $anchor
.offset(),
1122 elementDocument
= this.getDocument( $element
),
1123 anchorDocument
= this.getDocument( $anchor
);
1125 // If $element isn't in the same document as $anchor, traverse up
1126 while ( elementDocument
!== anchorDocument
) {
1127 iframe
= elementDocument
.defaultView
.frameElement
;
1129 throw new Error( '$element frame is not contained in $anchor frame' );
1131 iframePos
= $( iframe
).offset();
1132 pos
.left
+= iframePos
.left
;
1133 pos
.top
+= iframePos
.top
;
1134 elementDocument
= iframe
.ownerDocument
;
1136 pos
.left
-= anchorPos
.left
;
1137 pos
.top
-= anchorPos
.top
;
1142 * Get element border sizes.
1145 * @param {HTMLElement} el Element to measure
1146 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1148 OO
.ui
.Element
.static.getBorders = function ( el
) {
1149 var doc
= el
.ownerDocument
,
1150 win
= doc
.parentWindow
|| doc
.defaultView
,
1151 style
= win
&& win
.getComputedStyle
?
1152 win
.getComputedStyle( el
, null ) :
1155 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1156 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1157 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1158 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1169 * Get dimensions of an element or window.
1172 * @param {HTMLElement|Window} el Element to measure
1173 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1175 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1177 doc
= el
.ownerDocument
|| el
.document
,
1178 win
= doc
.parentWindow
|| doc
.defaultView
;
1180 if ( win
=== el
|| el
=== doc
.documentElement
) {
1183 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1185 top
: $win
.scrollTop(),
1186 left
: $win
.scrollLeft()
1188 scrollbar
: { right
: 0, bottom
: 0 },
1192 bottom
: $win
.innerHeight(),
1193 right
: $win
.innerWidth()
1199 borders
: this.getBorders( el
),
1201 top
: $el
.scrollTop(),
1202 left
: $el
.scrollLeft()
1205 right
: $el
.innerWidth() - el
.clientWidth
,
1206 bottom
: $el
.innerHeight() - el
.clientHeight
1208 rect
: el
.getBoundingClientRect()
1214 * Get scrollable object parent
1216 * documentElement can't be used to get or set the scrollTop
1217 * property on Blink. Changing and testing its value lets us
1218 * use 'body' or 'documentElement' based on what is working.
1220 * https://code.google.com/p/chromium/issues/detail?id=303131
1223 * @param {HTMLElement} el Element to find scrollable parent for
1224 * @return {HTMLElement} Scrollable parent
1226 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1227 var scrollTop
, body
;
1229 if ( OO
.ui
.scrollableElement
=== undefined ) {
1230 body
= el
.ownerDocument
.body
;
1231 scrollTop
= body
.scrollTop
;
1234 if ( body
.scrollTop
=== 1 ) {
1235 body
.scrollTop
= scrollTop
;
1236 OO
.ui
.scrollableElement
= 'body';
1238 OO
.ui
.scrollableElement
= 'documentElement';
1242 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1246 * Get closest scrollable container.
1248 * Traverses up until either a scrollable element or the root is reached, in which case the window
1252 * @param {HTMLElement} el Element to find scrollable container for
1253 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1254 * @return {HTMLElement} Closest scrollable container
1256 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1258 props
= [ 'overflow' ],
1259 $parent
= $( el
).parent();
1261 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1262 props
.push( 'overflow-' + dimension
);
1265 while ( $parent
.length
) {
1266 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1267 return $parent
[ 0 ];
1271 val
= $parent
.css( props
[ i
] );
1272 if ( val
=== 'auto' || val
=== 'scroll' ) {
1273 return $parent
[ 0 ];
1276 $parent
= $parent
.parent();
1278 return this.getDocument( el
).body
;
1282 * Scroll element into view.
1285 * @param {HTMLElement} el Element to scroll into view
1286 * @param {Object} [config] Configuration options
1287 * @param {string} [config.duration] jQuery animation duration value
1288 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1289 * to scroll in both directions
1290 * @param {Function} [config.complete] Function to call when scrolling completes
1292 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1293 // Configuration initialization
1294 config
= config
|| {};
1297 callback
= typeof config
.complete
=== 'function' && config
.complete
,
1298 sc
= this.getClosestScrollableContainer( el
, config
.direction
),
1300 eld
= this.getDimensions( el
),
1301 scd
= this.getDimensions( sc
),
1302 $win
= $( this.getWindow( el
) );
1304 // Compute the distances between the edges of el and the edges of the scroll viewport
1305 if ( $sc
.is( 'html, body' ) ) {
1306 // If the scrollable container is the root, this is easy
1309 bottom
: $win
.innerHeight() - eld
.rect
.bottom
,
1310 left
: eld
.rect
.left
,
1311 right
: $win
.innerWidth() - eld
.rect
.right
1314 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1316 top
: eld
.rect
.top
- ( scd
.rect
.top
+ scd
.borders
.top
),
1317 bottom
: scd
.rect
.bottom
- scd
.borders
.bottom
- scd
.scrollbar
.bottom
- eld
.rect
.bottom
,
1318 left
: eld
.rect
.left
- ( scd
.rect
.left
+ scd
.borders
.left
),
1319 right
: scd
.rect
.right
- scd
.borders
.right
- scd
.scrollbar
.right
- eld
.rect
.right
1323 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1324 if ( rel
.top
< 0 ) {
1325 anim
.scrollTop
= scd
.scroll
.top
+ rel
.top
;
1326 } else if ( rel
.top
> 0 && rel
.bottom
< 0 ) {
1327 anim
.scrollTop
= scd
.scroll
.top
+ Math
.min( rel
.top
, -rel
.bottom
);
1330 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1331 if ( rel
.left
< 0 ) {
1332 anim
.scrollLeft
= scd
.scroll
.left
+ rel
.left
;
1333 } else if ( rel
.left
> 0 && rel
.right
< 0 ) {
1334 anim
.scrollLeft
= scd
.scroll
.left
+ Math
.min( rel
.left
, -rel
.right
);
1337 if ( !$.isEmptyObject( anim
) ) {
1338 $sc
.stop( true ).animate( anim
, config
.duration
|| 'fast' );
1340 $sc
.queue( function ( next
) {
1353 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1354 * and reserve space for them, because it probably doesn't.
1356 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1357 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1358 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1359 * and then reattach (or show) them back.
1362 * @param {HTMLElement} el Element to reconsider the scrollbars on
1364 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1365 var i
, len
, nodes
= [];
1366 // Detach all children
1367 while ( el
.firstChild
) {
1368 nodes
.push( el
.firstChild
);
1369 el
.removeChild( el
.firstChild
);
1372 void el
.offsetHeight
;
1373 // Reattach all children
1374 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1375 el
.appendChild( nodes
[ i
] );
1382 * Toggle visibility of an element.
1384 * @param {boolean} [show] Make element visible, omit to toggle visibility
1388 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1389 show
= show
=== undefined ? !this.visible
: !!show
;
1391 if ( show
!== this.isVisible() ) {
1392 this.visible
= show
;
1393 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1394 this.emit( 'toggle', show
);
1401 * Check if element is visible.
1403 * @return {boolean} element is visible
1405 OO
.ui
.Element
.prototype.isVisible = function () {
1406 return this.visible
;
1412 * @return {Mixed} Element data
1414 OO
.ui
.Element
.prototype.getData = function () {
1421 * @param {Mixed} Element data
1424 OO
.ui
.Element
.prototype.setData = function ( data
) {
1430 * Check if element supports one or more methods.
1432 * @param {string|string[]} methods Method or list of methods to check
1433 * @return {boolean} All methods are supported
1435 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1439 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1440 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1441 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1446 return methods
.length
=== support
;
1450 * Update the theme-provided classes.
1452 * @localdoc This is called in element mixins and widget classes any time state changes.
1453 * Updating is debounced, minimizing overhead of changing multiple attributes and
1454 * guaranteeing that theme updates do not occur within an element's constructor
1456 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1457 if ( !this.updateThemeClassesPending
) {
1458 this.updateThemeClassesPending
= true;
1459 setTimeout( this.debouncedUpdateThemeClassesHandler
);
1466 OO
.ui
.Element
.prototype.debouncedUpdateThemeClasses = function () {
1467 OO
.ui
.theme
.updateElementClasses( this );
1468 this.updateThemeClassesPending
= false;
1472 * Get the HTML tag name.
1474 * Override this method to base the result on instance information.
1476 * @return {string} HTML tag name
1478 OO
.ui
.Element
.prototype.getTagName = function () {
1479 return this.constructor.static.tagName
;
1483 * Check if the element is attached to the DOM
1484 * @return {boolean} The element is attached to the DOM
1486 OO
.ui
.Element
.prototype.isElementAttached = function () {
1487 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1491 * Get the DOM document.
1493 * @return {HTMLDocument} Document object
1495 OO
.ui
.Element
.prototype.getElementDocument = function () {
1496 // Don't cache this in other ways either because subclasses could can change this.$element
1497 return OO
.ui
.Element
.static.getDocument( this.$element
);
1501 * Get the DOM window.
1503 * @return {Window} Window object
1505 OO
.ui
.Element
.prototype.getElementWindow = function () {
1506 return OO
.ui
.Element
.static.getWindow( this.$element
);
1510 * Get closest scrollable container.
1512 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1513 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1517 * Get group element is in.
1519 * @return {OO.ui.GroupElement|null} Group element, null if none
1521 OO
.ui
.Element
.prototype.getElementGroup = function () {
1522 return this.elementGroup
;
1526 * Set group element is in.
1528 * @param {OO.ui.GroupElement|null} group Group element, null if none
1531 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1532 this.elementGroup
= group
;
1537 * Scroll element into view.
1539 * @param {Object} [config] Configuration options
1541 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1542 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1546 * Container for elements.
1550 * @extends OO.ui.Element
1551 * @mixins OO.EventEmitter
1554 * @param {Object} [config] Configuration options
1556 OO
.ui
.Layout
= function OoUiLayout( config
) {
1557 // Configuration initialization
1558 config
= config
|| {};
1560 // Parent constructor
1561 OO
.ui
.Layout
.super.call( this, config
);
1563 // Mixin constructors
1564 OO
.EventEmitter
.call( this );
1567 this.$element
.addClass( 'oo-ui-layout' );
1572 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1573 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1576 * Widgets are compositions of one or more OOjs UI elements that users can both view
1577 * and interact with. All widgets can be configured and modified via a standard API,
1578 * and their state can change dynamically according to a model.
1582 * @extends OO.ui.Element
1583 * @mixins OO.EventEmitter
1586 * @param {Object} [config] Configuration options
1587 * @cfg {boolean} [disabled=false] Disable
1589 OO
.ui
.Widget
= function OoUiWidget( config
) {
1590 // Initialize config
1591 config
= $.extend( { disabled
: false }, config
);
1593 // Parent constructor
1594 OO
.ui
.Widget
.super.call( this, config
);
1596 // Mixin constructors
1597 OO
.EventEmitter
.call( this );
1600 this.disabled
= null;
1601 this.wasDisabled
= null;
1604 this.$element
.addClass( 'oo-ui-widget' );
1605 this.setDisabled( !!config
.disabled
);
1610 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1611 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1617 * @param {boolean} disabled Widget is disabled
1622 * @param {boolean} visible Widget is visible
1628 * Check if the widget is disabled.
1630 * @return {boolean} Button is disabled
1632 OO
.ui
.Widget
.prototype.isDisabled = function () {
1633 return this.disabled
;
1637 * Set the disabled state of the widget.
1639 * This should probably change the widgets' appearance and prevent it from being used.
1641 * @param {boolean} disabled Disable widget
1644 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1647 this.disabled
= !!disabled
;
1648 isDisabled
= this.isDisabled();
1649 if ( isDisabled
!== this.wasDisabled
) {
1650 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1651 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1652 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1653 this.emit( 'disable', isDisabled
);
1654 this.updateThemeClasses();
1656 this.wasDisabled
= isDisabled
;
1662 * Update the disabled state, in case of changes in parent widget.
1666 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1667 this.setDisabled( this.disabled
);
1672 * A window is a container for elements that are in a child frame. They are used with
1673 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1674 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1675 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1676 * the window manager will choose a sensible fallback.
1678 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1679 * different processes are executed:
1681 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1682 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1685 * - {@link #getSetupProcess} method is called and its result executed
1686 * - {@link #getReadyProcess} method is called and its result executed
1688 * **opened**: The window is now open
1690 * **closing**: The closing stage begins when the window manager's
1691 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1692 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1694 * - {@link #getHoldProcess} method is called and its result executed
1695 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1697 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1698 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1699 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1700 * processing can complete. Always assume window processes are executed asynchronously.
1702 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1704 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1708 * @extends OO.ui.Element
1709 * @mixins OO.EventEmitter
1712 * @param {Object} [config] Configuration options
1713 * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large`, `larger` or
1714 * `full`; omit to use #static-size
1716 OO
.ui
.Window
= function OoUiWindow( config
) {
1717 // Configuration initialization
1718 config
= config
|| {};
1720 // Parent constructor
1721 OO
.ui
.Window
.super.call( this, config
);
1723 // Mixin constructors
1724 OO
.EventEmitter
.call( this );
1727 this.manager
= null;
1728 this.size
= config
.size
|| this.constructor.static.size
;
1729 this.$frame
= $( '<div>' );
1730 this.$overlay
= $( '<div>' );
1731 this.$content
= $( '<div>' );
1734 this.$overlay
.addClass( 'oo-ui-window-overlay' );
1736 .addClass( 'oo-ui-window-content' )
1737 .attr( 'tabIndex', 0 );
1739 .addClass( 'oo-ui-window-frame' )
1740 .append( this.$content
);
1743 .addClass( 'oo-ui-window' )
1744 .append( this.$frame
, this.$overlay
);
1746 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1747 // that reference properties not initialized at that time of parent class construction
1748 // TODO: Find a better way to handle post-constructor setup
1749 this.visible
= false;
1750 this.$element
.addClass( 'oo-ui-element-hidden' );
1755 OO
.inheritClass( OO
.ui
.Window
, OO
.ui
.Element
);
1756 OO
.mixinClass( OO
.ui
.Window
, OO
.EventEmitter
);
1758 /* Static Properties */
1761 * Symbolic name of size.
1763 * Size is used if no size is configured during construction.
1767 * @property {string}
1769 OO
.ui
.Window
.static.size
= 'medium';
1774 * Handle mouse down events.
1776 * @param {jQuery.Event} e Mouse down event
1778 OO
.ui
.Window
.prototype.onMouseDown = function ( e
) {
1779 // Prevent clicking on the click-block from stealing focus
1780 if ( e
.target
=== this.$element
[ 0 ] ) {
1786 * Check if window has been initialized.
1788 * Initialization occurs when a window is added to a manager.
1790 * @return {boolean} Window has been initialized
1792 OO
.ui
.Window
.prototype.isInitialized = function () {
1793 return !!this.manager
;
1797 * Check if window is visible.
1799 * @return {boolean} Window is visible
1801 OO
.ui
.Window
.prototype.isVisible = function () {
1802 return this.visible
;
1806 * Check if window is opening.
1808 * This is a wrapper around OO.ui.WindowManager#isOpening.
1810 * @return {boolean} Window is opening
1812 OO
.ui
.Window
.prototype.isOpening = function () {
1813 return this.manager
.isOpening( this );
1817 * Check if window is closing.
1819 * This is a wrapper around OO.ui.WindowManager#isClosing.
1821 * @return {boolean} Window is closing
1823 OO
.ui
.Window
.prototype.isClosing = function () {
1824 return this.manager
.isClosing( this );
1828 * Check if window is opened.
1830 * This is a wrapper around OO.ui.WindowManager#isOpened.
1832 * @return {boolean} Window is opened
1834 OO
.ui
.Window
.prototype.isOpened = function () {
1835 return this.manager
.isOpened( this );
1839 * Get the window manager.
1841 * @return {OO.ui.WindowManager} Manager of window
1843 OO
.ui
.Window
.prototype.getManager = function () {
1844 return this.manager
;
1848 * Get the window size.
1850 * @return {string} Symbolic size name, e.g. `small`, `medium`, `large`, `larger`, `full`
1852 OO
.ui
.Window
.prototype.getSize = function () {
1857 * Disable transitions on window's frame for the duration of the callback function, then enable them
1861 * @param {Function} callback Function to call while transitions are disabled
1863 OO
.ui
.Window
.prototype.withoutSizeTransitions = function ( callback
) {
1864 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1865 // Disable transitions first, otherwise we'll get values from when the window was animating.
1867 styleObj
= this.$frame
[ 0 ].style
;
1868 oldTransition
= styleObj
.transition
|| styleObj
.OTransition
|| styleObj
.MsTransition
||
1869 styleObj
.MozTransition
|| styleObj
.WebkitTransition
;
1870 styleObj
.transition
= styleObj
.OTransition
= styleObj
.MsTransition
=
1871 styleObj
.MozTransition
= styleObj
.WebkitTransition
= 'none';
1873 // Force reflow to make sure the style changes done inside callback really are not transitioned
1874 this.$frame
.height();
1875 styleObj
.transition
= styleObj
.OTransition
= styleObj
.MsTransition
=
1876 styleObj
.MozTransition
= styleObj
.WebkitTransition
= oldTransition
;
1880 * Get the height of the dialog contents.
1882 * @return {number} Content height
1884 OO
.ui
.Window
.prototype.getContentHeight = function () {
1887 bodyStyleObj
= this.$body
[ 0 ].style
,
1888 frameStyleObj
= this.$frame
[ 0 ].style
;
1890 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1891 // Disable transitions first, otherwise we'll get values from when the window was animating.
1892 this.withoutSizeTransitions( function () {
1893 var oldHeight
= frameStyleObj
.height
,
1894 oldPosition
= bodyStyleObj
.position
;
1895 frameStyleObj
.height
= '1px';
1896 // Force body to resize to new width
1897 bodyStyleObj
.position
= 'relative';
1898 bodyHeight
= win
.getBodyHeight();
1899 frameStyleObj
.height
= oldHeight
;
1900 bodyStyleObj
.position
= oldPosition
;
1904 // Add buffer for border
1905 ( this.$frame
.outerHeight() - this.$frame
.innerHeight() ) +
1906 // Use combined heights of children
1907 ( this.$head
.outerHeight( true ) + bodyHeight
+ this.$foot
.outerHeight( true ) )
1912 * Get the height of the dialog contents.
1914 * When this function is called, the dialog will temporarily have been resized
1915 * to height=1px, so .scrollHeight measurements can be taken accurately.
1917 * @return {number} Height of content
1919 OO
.ui
.Window
.prototype.getBodyHeight = function () {
1920 return this.$body
[ 0 ].scrollHeight
;
1924 * Get the directionality of the frame
1926 * @return {string} Directionality, 'ltr' or 'rtl'
1928 OO
.ui
.Window
.prototype.getDir = function () {
1933 * Get a process for setting up a window for use.
1935 * Each time the window is opened this process will set it up for use in a particular context, based
1936 * on the `data` argument.
1938 * When you override this method, you can add additional setup steps to the process the parent
1939 * method provides using the 'first' and 'next' methods.
1942 * @param {Object} [data] Window opening data
1943 * @return {OO.ui.Process} Setup process
1945 OO
.ui
.Window
.prototype.getSetupProcess = function () {
1946 return new OO
.ui
.Process();
1950 * Get a process for readying a window for use.
1952 * Each time the window is open and setup, this process will ready it up for use in a particular
1953 * context, based on the `data` argument.
1955 * When you override this method, you can add additional setup steps to the process the parent
1956 * method provides using the 'first' and 'next' methods.
1959 * @param {Object} [data] Window opening data
1960 * @return {OO.ui.Process} Setup process
1962 OO
.ui
.Window
.prototype.getReadyProcess = function () {
1963 return new OO
.ui
.Process();
1967 * Get a process for holding a window from use.
1969 * Each time the window is closed, this process will hold it from use in a particular context, based
1970 * on the `data` argument.
1972 * When you override this method, you can add additional setup steps to the process the parent
1973 * method provides using the 'first' and 'next' methods.
1976 * @param {Object} [data] Window closing data
1977 * @return {OO.ui.Process} Hold process
1979 OO
.ui
.Window
.prototype.getHoldProcess = function () {
1980 return new OO
.ui
.Process();
1984 * Get a process for tearing down a window after use.
1986 * Each time the window is closed this process will tear it down and do something with the user's
1987 * interactions within the window, based on the `data` argument.
1989 * When you override this method, you can add additional teardown steps to the process the parent
1990 * method provides using the 'first' and 'next' methods.
1993 * @param {Object} [data] Window closing data
1994 * @return {OO.ui.Process} Teardown process
1996 OO
.ui
.Window
.prototype.getTeardownProcess = function () {
1997 return new OO
.ui
.Process();
2001 * Set the window manager.
2003 * This will cause the window to initialize. Calling it more than once will cause an error.
2005 * @param {OO.ui.WindowManager} manager Manager for this window
2006 * @throws {Error} If called more than once
2009 OO
.ui
.Window
.prototype.setManager = function ( manager
) {
2010 if ( this.manager
) {
2011 throw new Error( 'Cannot set window manager, window already has a manager' );
2014 this.manager
= manager
;
2021 * Set the window size.
2023 * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
2026 OO
.ui
.Window
.prototype.setSize = function ( size
) {
2033 * Update the window size.
2035 * @throws {Error} If not attached to a manager
2038 OO
.ui
.Window
.prototype.updateSize = function () {
2039 if ( !this.manager
) {
2040 throw new Error( 'Cannot update window size, must be attached to a manager' );
2043 this.manager
.updateWindowSize( this );
2049 * Set window dimensions.
2051 * Properties are applied to the frame container.
2053 * @param {Object} dim CSS dimension properties
2054 * @param {string|number} [dim.width] Width
2055 * @param {string|number} [dim.minWidth] Minimum width
2056 * @param {string|number} [dim.maxWidth] Maximum width
2057 * @param {string|number} [dim.width] Height, omit to set based on height of contents
2058 * @param {string|number} [dim.minWidth] Minimum height
2059 * @param {string|number} [dim.maxWidth] Maximum height
2062 OO
.ui
.Window
.prototype.setDimensions = function ( dim
) {
2065 styleObj
= this.$frame
[ 0 ].style
;
2067 // Calculate the height we need to set using the correct width
2068 if ( dim
.height
=== undefined ) {
2069 this.withoutSizeTransitions( function () {
2070 var oldWidth
= styleObj
.width
;
2071 win
.$frame
.css( 'width', dim
.width
|| '' );
2072 height
= win
.getContentHeight();
2073 styleObj
.width
= oldWidth
;
2076 height
= dim
.height
;
2080 width
: dim
.width
|| '',
2081 minWidth
: dim
.minWidth
|| '',
2082 maxWidth
: dim
.maxWidth
|| '',
2083 height
: height
|| '',
2084 minHeight
: dim
.minHeight
|| '',
2085 maxHeight
: dim
.maxHeight
|| ''
2092 * Initialize window contents.
2094 * The first time the window is opened, #initialize is called so that changes to the window that
2095 * will persist between openings can be made. See #getSetupProcess for a way to make changes each
2096 * time the window opens.
2098 * @throws {Error} If not attached to a manager
2101 OO
.ui
.Window
.prototype.initialize = function () {
2102 if ( !this.manager
) {
2103 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2107 this.$head
= $( '<div>' );
2108 this.$body
= $( '<div>' );
2109 this.$foot
= $( '<div>' );
2110 this.dir
= OO
.ui
.Element
.static.getDir( this.$content
) || 'ltr';
2111 this.$document
= $( this.getElementDocument() );
2114 this.$element
.on( 'mousedown', this.onMouseDown
.bind( this ) );
2117 this.$head
.addClass( 'oo-ui-window-head' );
2118 this.$body
.addClass( 'oo-ui-window-body' );
2119 this.$foot
.addClass( 'oo-ui-window-foot' );
2120 this.$content
.append( this.$head
, this.$body
, this.$foot
);
2128 * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager.
2129 * To do something each time the window opens, use #getSetupProcess or #getReadyProcess.
2131 * @param {Object} [data] Window opening data
2132 * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
2133 * first argument will be a promise which will be resolved when the window begins closing
2134 * @throws {Error} If not attached to a manager
2136 OO
.ui
.Window
.prototype.open = function ( data
) {
2137 if ( !this.manager
) {
2138 throw new Error( 'Cannot open window, must be attached to a manager' );
2141 return this.manager
.openWindow( this, data
);
2147 * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager.
2148 * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess.
2150 * @param {Object} [data] Window closing data
2151 * @return {jQuery.Promise} Promise resolved when window is closed
2152 * @throws {Error} If not attached to a manager
2154 OO
.ui
.Window
.prototype.close = function ( data
) {
2155 if ( !this.manager
) {
2156 throw new Error( 'Cannot close window, must be attached to a manager' );
2159 return this.manager
.closeWindow( this, data
);
2165 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2168 * @param {Object} [data] Window opening data
2169 * @return {jQuery.Promise} Promise resolved when window is setup
2171 OO
.ui
.Window
.prototype.setup = function ( data
) {
2173 deferred
= $.Deferred();
2175 this.toggle( true );
2177 this.getSetupProcess( data
).execute().done( function () {
2178 // Force redraw by asking the browser to measure the elements' widths
2179 win
.$element
.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2180 win
.$content
.addClass( 'oo-ui-window-content-setup' ).width();
2184 return deferred
.promise();
2190 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2193 * @param {Object} [data] Window opening data
2194 * @return {jQuery.Promise} Promise resolved when window is ready
2196 OO
.ui
.Window
.prototype.ready = function ( data
) {
2198 deferred
= $.Deferred();
2200 this.$content
.focus();
2201 this.getReadyProcess( data
).execute().done( function () {
2202 // Force redraw by asking the browser to measure the elements' widths
2203 win
.$element
.addClass( 'oo-ui-window-ready' ).width();
2204 win
.$content
.addClass( 'oo-ui-window-content-ready' ).width();
2208 return deferred
.promise();
2214 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2217 * @param {Object} [data] Window closing data
2218 * @return {jQuery.Promise} Promise resolved when window is held
2220 OO
.ui
.Window
.prototype.hold = function ( data
) {
2222 deferred
= $.Deferred();
2224 this.getHoldProcess( data
).execute().done( function () {
2225 // Get the focused element within the window's content
2226 var $focus
= win
.$content
.find( OO
.ui
.Element
.static.getDocument( win
.$content
).activeElement
);
2228 // Blur the focused element
2229 if ( $focus
.length
) {
2233 // Force redraw by asking the browser to measure the elements' widths
2234 win
.$element
.removeClass( 'oo-ui-window-ready' ).width();
2235 win
.$content
.removeClass( 'oo-ui-window-content-ready' ).width();
2239 return deferred
.promise();
2245 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2248 * @param {Object} [data] Window closing data
2249 * @return {jQuery.Promise} Promise resolved when window is torn down
2251 OO
.ui
.Window
.prototype.teardown = function ( data
) {
2254 return this.getTeardownProcess( data
).execute()
2255 .done( function () {
2256 // Force redraw by asking the browser to measure the elements' widths
2257 win
.$element
.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2258 win
.$content
.removeClass( 'oo-ui-window-content-setup' ).width();
2259 win
.toggle( false );
2264 * The Dialog class serves as the base class for the other types of dialogs.
2265 * Unless extended to include controls, the rendered dialog box is a simple window
2266 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2267 * which opens, closes, and controls the presentation of the window. See the
2268 * [OOjs UI documentation on MediaWiki] [1] for more information.
2271 * // A simple dialog window.
2272 * function MyDialog( config ) {
2273 * MyDialog.super.call( this, config );
2275 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2276 * MyDialog.prototype.initialize = function () {
2277 * MyDialog.super.prototype.initialize.call( this );
2278 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2279 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2280 * this.$body.append( this.content.$element );
2282 * MyDialog.prototype.getBodyHeight = function () {
2283 * return this.content.$element.outerHeight( true );
2285 * var myDialog = new MyDialog( {
2288 * // Create and append a window manager, which opens and closes the window.
2289 * var windowManager = new OO.ui.WindowManager();
2290 * $( 'body' ).append( windowManager.$element );
2291 * windowManager.addWindows( [ myDialog ] );
2292 * // Open the window!
2293 * windowManager.openWindow( myDialog );
2295 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2299 * @extends OO.ui.Window
2300 * @mixins OO.ui.PendingElement
2303 * @param {Object} [config] Configuration options
2305 OO
.ui
.Dialog
= function OoUiDialog( config
) {
2306 // Parent constructor
2307 OO
.ui
.Dialog
.super.call( this, config
);
2309 // Mixin constructors
2310 OO
.ui
.PendingElement
.call( this );
2313 this.actions
= new OO
.ui
.ActionSet();
2314 this.attachedActions
= [];
2315 this.currentAction
= null;
2316 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
2319 this.actions
.connect( this, {
2320 click
: 'onActionClick',
2321 resize
: 'onActionResize',
2322 change
: 'onActionsChange'
2327 .addClass( 'oo-ui-dialog' )
2328 .attr( 'role', 'dialog' );
2333 OO
.inheritClass( OO
.ui
.Dialog
, OO
.ui
.Window
);
2334 OO
.mixinClass( OO
.ui
.Dialog
, OO
.ui
.PendingElement
);
2336 /* Static Properties */
2339 * Symbolic name of dialog.
2344 * @property {string}
2346 OO
.ui
.Dialog
.static.name
= '';
2354 * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text
2356 OO
.ui
.Dialog
.static.title
= '';
2359 * List of OO.ui.ActionWidget configuration options.
2363 * @property {Object[]}
2365 OO
.ui
.Dialog
.static.actions
= [];
2368 * Close dialog when the escape key is pressed.
2373 * @property {boolean}
2375 OO
.ui
.Dialog
.static.escapable
= true;
2380 * Handle frame document key down events.
2382 * @param {jQuery.Event} e Key down event
2384 OO
.ui
.Dialog
.prototype.onDocumentKeyDown = function ( e
) {
2385 if ( e
.which
=== OO
.ui
.Keys
.ESCAPE
) {
2388 e
.stopPropagation();
2393 * Handle action resized events.
2395 * @param {OO.ui.ActionWidget} action Action that was resized
2397 OO
.ui
.Dialog
.prototype.onActionResize = function () {
2398 // Override in subclass
2402 * Handle action click events.
2404 * @param {OO.ui.ActionWidget} action Action that was clicked
2406 OO
.ui
.Dialog
.prototype.onActionClick = function ( action
) {
2407 if ( !this.isPending() ) {
2408 this.executeAction( action
.getAction() );
2413 * Handle actions change event.
2415 OO
.ui
.Dialog
.prototype.onActionsChange = function () {
2416 this.detachActions();
2417 if ( !this.isClosing() ) {
2418 this.attachActions();
2423 * Get set of actions.
2425 * @return {OO.ui.ActionSet}
2427 OO
.ui
.Dialog
.prototype.getActions = function () {
2428 return this.actions
;
2432 * Get a process for taking action.
2434 * When you override this method, you can add additional accept steps to the process the parent
2435 * method provides using the 'first' and 'next' methods.
2438 * @param {string} [action] Symbolic name of action
2439 * @return {OO.ui.Process} Action process
2441 OO
.ui
.Dialog
.prototype.getActionProcess = function ( action
) {
2442 return new OO
.ui
.Process()
2443 .next( function () {
2445 // An empty action always closes the dialog without data, which should always be
2446 // safe and make no changes
2455 * @param {Object} [data] Dialog opening data
2456 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
2457 * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
2458 * action item, omit to use #static-actions
2460 OO
.ui
.Dialog
.prototype.getSetupProcess = function ( data
) {
2464 return OO
.ui
.Dialog
.super.prototype.getSetupProcess
.call( this, data
)
2465 .next( function () {
2468 config
= this.constructor.static,
2469 actions
= data
.actions
!== undefined ? data
.actions
: config
.actions
;
2471 this.title
.setLabel(
2472 data
.title
!== undefined ? data
.title
: this.constructor.static.title
2474 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
2476 new OO
.ui
.ActionWidget( actions
[ i
] )
2479 this.actions
.add( items
);
2481 if ( this.constructor.static.escapable
) {
2482 this.$document
.on( 'keydown', this.onDocumentKeyDownHandler
);
2490 OO
.ui
.Dialog
.prototype.getTeardownProcess = function ( data
) {
2492 return OO
.ui
.Dialog
.super.prototype.getTeardownProcess
.call( this, data
)
2493 .first( function () {
2494 if ( this.constructor.static.escapable
) {
2495 this.$document
.off( 'keydown', this.onDocumentKeyDownHandler
);
2498 this.actions
.clear();
2499 this.currentAction
= null;
2506 OO
.ui
.Dialog
.prototype.initialize = function () {
2508 OO
.ui
.Dialog
.super.prototype.initialize
.call( this );
2511 this.title
= new OO
.ui
.LabelWidget();
2514 this.$content
.addClass( 'oo-ui-dialog-content' );
2515 this.setPendingElement( this.$head
);
2519 * Attach action actions.
2521 OO
.ui
.Dialog
.prototype.attachActions = function () {
2522 // Remember the list of potentially attached actions
2523 this.attachedActions
= this.actions
.get();
2527 * Detach action actions.
2531 OO
.ui
.Dialog
.prototype.detachActions = function () {
2534 // Detach all actions that may have been previously attached
2535 for ( i
= 0, len
= this.attachedActions
.length
; i
< len
; i
++ ) {
2536 this.attachedActions
[ i
].$element
.detach();
2538 this.attachedActions
= [];
2542 * Execute an action.
2544 * @param {string} action Symbolic name of action to execute
2545 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2547 OO
.ui
.Dialog
.prototype.executeAction = function ( action
) {
2549 this.currentAction
= action
;
2550 return this.getActionProcess( action
).execute()
2551 .always( this.popPending
.bind( this ) );
2555 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2556 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2557 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2558 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2559 * pertinent data and reused.
2561 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2562 * `opened`, and `closing`, which represent the primary stages of the cycle:
2564 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2565 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2567 * - an `opening` event is emitted with an `opening` promise
2568 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2569 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2570 * window and its result executed
2571 * - a `setup` progress notification is emitted from the `opening` promise
2572 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2573 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2574 * window and its result executed
2575 * - a `ready` progress notification is emitted from the `opening` promise
2576 * - the `opening` promise is resolved with an `opened` promise
2578 * **Opened**: the window is now open.
2580 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2581 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2582 * to close the window.
2584 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2585 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2586 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2587 * window and its result executed
2588 * - a `hold` progress notification is emitted from the `closing` promise
2589 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2590 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2591 * window and its result executed
2592 * - a `teardown` progress notification is emitted from the `closing` promise
2593 * - the `closing` promise is resolved. The window is now closed
2595 * See the [OOjs UI documentation on MediaWiki][1] for more information.
2597 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2600 * @extends OO.ui.Element
2601 * @mixins OO.EventEmitter
2604 * @param {Object} [config] Configuration options
2605 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2606 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2608 OO
.ui
.WindowManager
= function OoUiWindowManager( config
) {
2609 // Configuration initialization
2610 config
= config
|| {};
2612 // Parent constructor
2613 OO
.ui
.WindowManager
.super.call( this, config
);
2615 // Mixin constructors
2616 OO
.EventEmitter
.call( this );
2619 this.factory
= config
.factory
;
2620 this.modal
= config
.modal
=== undefined || !!config
.modal
;
2622 this.opening
= null;
2624 this.closing
= null;
2625 this.preparingToOpen
= null;
2626 this.preparingToClose
= null;
2627 this.currentWindow
= null;
2628 this.globalEvents
= false;
2629 this.$ariaHidden
= null;
2630 this.onWindowResizeTimeout
= null;
2631 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
2632 this.afterWindowResizeHandler
= this.afterWindowResize
.bind( this );
2636 .addClass( 'oo-ui-windowManager' )
2637 .toggleClass( 'oo-ui-windowManager-modal', this.modal
);
2642 OO
.inheritClass( OO
.ui
.WindowManager
, OO
.ui
.Element
);
2643 OO
.mixinClass( OO
.ui
.WindowManager
, OO
.EventEmitter
);
2648 * Window is opening.
2650 * Fired when the window begins to be opened.
2653 * @param {OO.ui.Window} win Window that's being opened
2654 * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
2655 * resolved the first argument will be a promise which will be resolved when the window begins
2656 * closing, the second argument will be the opening data; progress notifications will be fired on
2657 * the promise for `setup` and `ready` when those processes are completed respectively.
2658 * @param {Object} data Window opening data
2662 * Window is closing.
2664 * Fired when the window begins to be closed.
2667 * @param {OO.ui.Window} win Window that's being closed
2668 * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise
2669 * is resolved the first argument will be a the closing data; progress notifications will be fired
2670 * on the promise for `hold` and `teardown` when those processes are completed respectively.
2671 * @param {Object} data Window closing data
2675 * Window was resized.
2678 * @param {OO.ui.Window} win Window that was resized
2681 /* Static Properties */
2684 * Map of symbolic size names and CSS properties.
2688 * @property {Object}
2690 OO
.ui
.WindowManager
.static.sizes
= {
2704 // These can be non-numeric because they are never used in calculations
2711 * Symbolic name of default size.
2713 * Default size is used if the window's requested size is not recognized.
2717 * @property {string}
2719 OO
.ui
.WindowManager
.static.defaultSize
= 'medium';
2724 * Handle window resize events.
2726 * @param {jQuery.Event} e Window resize event
2728 OO
.ui
.WindowManager
.prototype.onWindowResize = function () {
2729 clearTimeout( this.onWindowResizeTimeout
);
2730 this.onWindowResizeTimeout
= setTimeout( this.afterWindowResizeHandler
, 200 );
2734 * Handle window resize events.
2736 * @param {jQuery.Event} e Window resize event
2738 OO
.ui
.WindowManager
.prototype.afterWindowResize = function () {
2739 if ( this.currentWindow
) {
2740 this.updateWindowSize( this.currentWindow
);
2745 * Check if window is opening.
2747 * @return {boolean} Window is opening
2749 OO
.ui
.WindowManager
.prototype.isOpening = function ( win
) {
2750 return win
=== this.currentWindow
&& !!this.opening
&& this.opening
.state() === 'pending';
2754 * Check if window is closing.
2756 * @return {boolean} Window is closing
2758 OO
.ui
.WindowManager
.prototype.isClosing = function ( win
) {
2759 return win
=== this.currentWindow
&& !!this.closing
&& this.closing
.state() === 'pending';
2763 * Check if window is opened.
2765 * @return {boolean} Window is opened
2767 OO
.ui
.WindowManager
.prototype.isOpened = function ( win
) {
2768 return win
=== this.currentWindow
&& !!this.opened
&& this.opened
.state() === 'pending';
2772 * Check if a window is being managed.
2774 * @param {OO.ui.Window} win Window to check
2775 * @return {boolean} Window is being managed
2777 OO
.ui
.WindowManager
.prototype.hasWindow = function ( win
) {
2780 for ( name
in this.windows
) {
2781 if ( this.windows
[ name
] === win
) {
2790 * Get the number of milliseconds to wait between beginning opening and executing setup process.
2792 * @param {OO.ui.Window} win Window being opened
2793 * @param {Object} [data] Window opening data
2794 * @return {number} Milliseconds to wait
2796 OO
.ui
.WindowManager
.prototype.getSetupDelay = function () {
2801 * Get the number of milliseconds to wait between finishing setup and executing ready process.
2803 * @param {OO.ui.Window} win Window being opened
2804 * @param {Object} [data] Window opening data
2805 * @return {number} Milliseconds to wait
2807 OO
.ui
.WindowManager
.prototype.getReadyDelay = function () {
2812 * Get the number of milliseconds to wait between beginning closing and executing hold process.
2814 * @param {OO.ui.Window} win Window being closed
2815 * @param {Object} [data] Window closing data
2816 * @return {number} Milliseconds to wait
2818 OO
.ui
.WindowManager
.prototype.getHoldDelay = function () {
2823 * Get the number of milliseconds to wait between finishing hold and executing teardown process.
2825 * @param {OO.ui.Window} win Window being closed
2826 * @param {Object} [data] Window closing data
2827 * @return {number} Milliseconds to wait
2829 OO
.ui
.WindowManager
.prototype.getTeardownDelay = function () {
2830 return this.modal
? 250 : 0;
2834 * Get managed window by symbolic name.
2836 * If window is not yet instantiated, it will be instantiated and added automatically.
2838 * @param {string} name Symbolic window name
2839 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2840 * @throws {Error} If the symbolic name is unrecognized by the factory
2841 * @throws {Error} If the symbolic name unrecognized as a managed window
2843 OO
.ui
.WindowManager
.prototype.getWindow = function ( name
) {
2844 var deferred
= $.Deferred(),
2845 win
= this.windows
[ name
];
2847 if ( !( win
instanceof OO
.ui
.Window
) ) {
2848 if ( this.factory
) {
2849 if ( !this.factory
.lookup( name
) ) {
2850 deferred
.reject( new OO
.ui
.Error(
2851 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
2854 win
= this.factory
.create( name
);
2855 this.addWindows( [ win
] );
2856 deferred
.resolve( win
);
2859 deferred
.reject( new OO
.ui
.Error(
2860 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
2864 deferred
.resolve( win
);
2867 return deferred
.promise();
2871 * Get current window.
2873 * @return {OO.ui.Window|null} Currently opening/opened/closing window
2875 OO
.ui
.WindowManager
.prototype.getCurrentWindow = function () {
2876 return this.currentWindow
;
2882 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
2883 * @param {Object} [data] Window opening data
2884 * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
2885 * for more details about the `opening` promise
2888 OO
.ui
.WindowManager
.prototype.openWindow = function ( win
, data
) {
2890 opening
= $.Deferred();
2892 // Argument handling
2893 if ( typeof win
=== 'string' ) {
2894 return this.getWindow( win
).then( function ( win
) {
2895 return manager
.openWindow( win
, data
);
2900 if ( !this.hasWindow( win
) ) {
2901 opening
.reject( new OO
.ui
.Error(
2902 'Cannot open window: window is not attached to manager'
2904 } else if ( this.preparingToOpen
|| this.opening
|| this.opened
) {
2905 opening
.reject( new OO
.ui
.Error(
2906 'Cannot open window: another window is opening or open'
2911 if ( opening
.state() !== 'rejected' ) {
2912 // If a window is currently closing, wait for it to complete
2913 this.preparingToOpen
= $.when( this.closing
);
2914 // Ensure handlers get called after preparingToOpen is set
2915 this.preparingToOpen
.done( function () {
2916 if ( manager
.modal
) {
2917 manager
.toggleGlobalEvents( true );
2918 manager
.toggleAriaIsolation( true );
2920 manager
.currentWindow
= win
;
2921 manager
.opening
= opening
;
2922 manager
.preparingToOpen
= null;
2923 manager
.emit( 'opening', win
, opening
, data
);
2924 setTimeout( function () {
2925 win
.setup( data
).then( function () {
2926 manager
.updateWindowSize( win
);
2927 manager
.opening
.notify( { state
: 'setup' } );
2928 setTimeout( function () {
2929 win
.ready( data
).then( function () {
2930 manager
.opening
.notify( { state
: 'ready' } );
2931 manager
.opening
= null;
2932 manager
.opened
= $.Deferred();
2933 opening
.resolve( manager
.opened
.promise(), data
);
2935 }, manager
.getReadyDelay() );
2937 }, manager
.getSetupDelay() );
2941 return opening
.promise();
2947 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
2948 * @param {Object} [data] Window closing data
2949 * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
2950 * for more details about the `closing` promise
2951 * @throws {Error} If no window by that name is being managed
2954 OO
.ui
.WindowManager
.prototype.closeWindow = function ( win
, data
) {
2956 closing
= $.Deferred(),
2959 // Argument handling
2960 if ( typeof win
=== 'string' ) {
2961 win
= this.windows
[ win
];
2962 } else if ( !this.hasWindow( win
) ) {
2968 closing
.reject( new OO
.ui
.Error(
2969 'Cannot close window: window is not attached to manager'
2971 } else if ( win
!== this.currentWindow
) {
2972 closing
.reject( new OO
.ui
.Error(
2973 'Cannot close window: window already closed with different data'
2975 } else if ( this.preparingToClose
|| this.closing
) {
2976 closing
.reject( new OO
.ui
.Error(
2977 'Cannot close window: window already closing with different data'
2982 if ( closing
.state() !== 'rejected' ) {
2983 // If the window is currently opening, close it when it's done
2984 this.preparingToClose
= $.when( this.opening
);
2985 // Ensure handlers get called after preparingToClose is set
2986 this.preparingToClose
.done( function () {
2987 manager
.closing
= closing
;
2988 manager
.preparingToClose
= null;
2989 manager
.emit( 'closing', win
, closing
, data
);
2990 opened
= manager
.opened
;
2991 manager
.opened
= null;
2992 opened
.resolve( closing
.promise(), data
);
2993 setTimeout( function () {
2994 win
.hold( data
).then( function () {
2995 closing
.notify( { state
: 'hold' } );
2996 setTimeout( function () {
2997 win
.teardown( data
).then( function () {
2998 closing
.notify( { state
: 'teardown' } );
2999 if ( manager
.modal
) {
3000 manager
.toggleGlobalEvents( false );
3001 manager
.toggleAriaIsolation( false );
3003 manager
.closing
= null;
3004 manager
.currentWindow
= null;
3005 closing
.resolve( data
);
3007 }, manager
.getTeardownDelay() );
3009 }, manager
.getHoldDelay() );
3013 return closing
.promise();
3019 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
3020 * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
3021 * a statically configured symbolic name
3023 OO
.ui
.WindowManager
.prototype.addWindows = function ( windows
) {
3024 var i
, len
, win
, name
, list
;
3026 if ( Array
.isArray( windows
) ) {
3027 // Convert to map of windows by looking up symbolic names from static configuration
3029 for ( i
= 0, len
= windows
.length
; i
< len
; i
++ ) {
3030 name
= windows
[ i
].constructor.static.name
;
3031 if ( typeof name
!== 'string' ) {
3032 throw new Error( 'Cannot add window' );
3034 list
[ name
] = windows
[ i
];
3036 } else if ( OO
.isPlainObject( windows
) ) {
3041 for ( name
in list
) {
3043 this.windows
[ name
] = win
.toggle( false );
3044 this.$element
.append( win
.$element
);
3045 win
.setManager( this );
3052 * Windows will be closed before they are removed.
3054 * @param {string[]} names Symbolic names of windows to remove
3055 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3056 * @throws {Error} If windows being removed are not being managed
3058 OO
.ui
.WindowManager
.prototype.removeWindows = function ( names
) {
3059 var i
, len
, win
, name
, cleanupWindow
,
3062 cleanup = function ( name
, win
) {
3063 delete manager
.windows
[ name
];
3064 win
.$element
.detach();
3067 for ( i
= 0, len
= names
.length
; i
< len
; i
++ ) {
3069 win
= this.windows
[ name
];
3071 throw new Error( 'Cannot remove window' );
3073 cleanupWindow
= cleanup
.bind( null, name
, win
);
3074 promises
.push( this.closeWindow( name
).then( cleanupWindow
, cleanupWindow
) );
3077 return $.when
.apply( $, promises
);
3081 * Remove all windows.
3083 * Windows will be closed before they are removed.
3085 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3087 OO
.ui
.WindowManager
.prototype.clearWindows = function () {
3088 return this.removeWindows( Object
.keys( this.windows
) );
3094 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3098 OO
.ui
.WindowManager
.prototype.updateWindowSize = function ( win
) {
3099 // Bypass for non-current, and thus invisible, windows
3100 if ( win
!== this.currentWindow
) {
3104 var viewport
= OO
.ui
.Element
.static.getDimensions( win
.getElementWindow() ),
3105 sizes
= this.constructor.static.sizes
,
3106 size
= win
.getSize();
3108 if ( !sizes
[ size
] ) {
3109 size
= this.constructor.static.defaultSize
;
3111 if ( size
!== 'full' && viewport
.rect
.right
- viewport
.rect
.left
< sizes
[ size
].width
) {
3115 this.$element
.toggleClass( 'oo-ui-windowManager-fullscreen', size
=== 'full' );
3116 this.$element
.toggleClass( 'oo-ui-windowManager-floating', size
!== 'full' );
3117 win
.setDimensions( sizes
[ size
] );
3119 this.emit( 'resize', win
);
3125 * Bind or unbind global events for scrolling.
3127 * @param {boolean} [on] Bind global events
3130 OO
.ui
.WindowManager
.prototype.toggleGlobalEvents = function ( on
) {
3131 on
= on
=== undefined ? !!this.globalEvents
: !!on
;
3133 var $body
= $( this.getElementDocument().body
),
3134 // We could have multiple window managers open to only modify
3135 // the body class at the bottom of the stack
3136 stackDepth
= $body
.data( 'windowManagerGlobalEvents' ) || 0 ;
3139 if ( !this.globalEvents
) {
3140 $( this.getElementWindow() ).on( {
3141 // Start listening for top-level window dimension changes
3142 'orientationchange resize': this.onWindowResizeHandler
3144 if ( stackDepth
=== 0 ) {
3145 $body
.css( 'overflow', 'hidden' );
3148 this.globalEvents
= true;
3150 } else if ( this.globalEvents
) {
3151 $( this.getElementWindow() ).off( {
3152 // Stop listening for top-level window dimension changes
3153 'orientationchange resize': this.onWindowResizeHandler
3156 if ( stackDepth
=== 0 ) {
3157 $( this.getElementDocument().body
).css( 'overflow', '' );
3159 this.globalEvents
= false;
3161 $body
.data( 'windowManagerGlobalEvents', stackDepth
);
3167 * Toggle screen reader visibility of content other than the window manager.
3169 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3172 OO
.ui
.WindowManager
.prototype.toggleAriaIsolation = function ( isolate
) {
3173 isolate
= isolate
=== undefined ? !this.$ariaHidden
: !!isolate
;
3176 if ( !this.$ariaHidden
) {
3177 // Hide everything other than the window manager from screen readers
3178 this.$ariaHidden
= $( 'body' )
3180 .not( this.$element
.parentsUntil( 'body' ).last() )
3181 .attr( 'aria-hidden', '' );
3183 } else if ( this.$ariaHidden
) {
3184 // Restore screen reader visibility
3185 this.$ariaHidden
.removeAttr( 'aria-hidden' );
3186 this.$ariaHidden
= null;
3193 * Destroy window manager.
3195 OO
.ui
.WindowManager
.prototype.destroy = function () {
3196 this.toggleGlobalEvents( false );
3197 this.toggleAriaIsolation( false );
3198 this.clearWindows();
3199 this.$element
.remove();
3206 * @param {string|jQuery} message Description of error
3207 * @param {Object} [config] Configuration options
3208 * @cfg {boolean} [recoverable=true] Error is recoverable
3209 * @cfg {boolean} [warning=false] Whether this error is a warning or not.
3211 OO
.ui
.Error
= function OoUiError( message
, config
) {
3212 // Allow passing positional parameters inside the config object
3213 if ( OO
.isPlainObject( message
) && config
=== undefined ) {
3215 message
= config
.message
;
3218 // Configuration initialization
3219 config
= config
|| {};
3222 this.message
= message
instanceof jQuery
? message
: String( message
);
3223 this.recoverable
= config
.recoverable
=== undefined || !!config
.recoverable
;
3224 this.warning
= !!config
.warning
;
3229 OO
.initClass( OO
.ui
.Error
);
3234 * Check if error can be recovered from.
3236 * @return {boolean} Error is recoverable
3238 OO
.ui
.Error
.prototype.isRecoverable = function () {
3239 return this.recoverable
;
3243 * Check if the error is a warning
3245 * @return {boolean} Error is warning
3247 OO
.ui
.Error
.prototype.isWarning = function () {
3248 return this.warning
;
3252 * Get error message as DOM nodes.
3254 * @return {jQuery} Error message in DOM nodes
3256 OO
.ui
.Error
.prototype.getMessage = function () {
3257 return this.message
instanceof jQuery
?
3258 this.message
.clone() :
3259 $( '<div>' ).text( this.message
).contents();
3263 * Get error message as text.
3265 * @return {string} Error message
3267 OO
.ui
.Error
.prototype.getMessageText = function () {
3268 return this.message
instanceof jQuery
? this.message
.text() : this.message
;
3272 * Wraps an HTML snippet for use with configuration values which default
3273 * to strings. This bypasses the default html-escaping done to string
3279 * @param {string} [content] HTML content
3281 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
3283 this.content
= content
;
3288 OO
.initClass( OO
.ui
.HtmlSnippet
);
3295 * @return {string} Unchanged HTML snippet.
3297 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
3298 return this.content
;
3302 * A list of functions, called in sequence.
3304 * If a function added to a process returns boolean false the process will stop; if it returns an
3305 * object with a `promise` method the process will use the promise to either continue to the next
3306 * step when the promise is resolved or stop when the promise is rejected.
3311 * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
3312 * call, see #createStep for more information
3313 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3315 * @return {Object} Step object, with `callback` and `context` properties
3317 OO
.ui
.Process = function ( step
, context
) {
3322 if ( step
!== undefined ) {
3323 this.next( step
, context
);
3329 OO
.initClass( OO
.ui
.Process
);
3334 * Start the process.
3336 * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
3337 * any of the steps return boolean false or a promise which gets rejected; upon stopping the
3338 * process, the remaining steps will not be taken
3340 OO
.ui
.Process
.prototype.execute = function () {
3341 var i
, len
, promise
;
3344 * Continue execution.
3347 * @param {Array} step A function and the context it should be called in
3348 * @return {Function} Function that continues the process
3350 function proceed( step
) {
3351 return function () {
3352 // Execute step in the correct context
3354 result
= step
.callback
.call( step
.context
);
3356 if ( result
=== false ) {
3357 // Use rejected promise for boolean false results
3358 return $.Deferred().reject( [] ).promise();
3360 if ( typeof result
=== 'number' ) {
3362 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3364 // Use a delayed promise for numbers, expecting them to be in milliseconds
3365 deferred
= $.Deferred();
3366 setTimeout( deferred
.resolve
, result
);
3367 return deferred
.promise();
3369 if ( result
instanceof OO
.ui
.Error
) {
3370 // Use rejected promise for error
3371 return $.Deferred().reject( [ result
] ).promise();
3373 if ( Array
.isArray( result
) && result
.length
&& result
[ 0 ] instanceof OO
.ui
.Error
) {
3374 // Use rejected promise for list of errors
3375 return $.Deferred().reject( result
).promise();
3377 // Duck-type the object to see if it can produce a promise
3378 if ( result
&& $.isFunction( result
.promise
) ) {
3379 // Use a promise generated from the result
3380 return result
.promise();
3382 // Use resolved promise for other results
3383 return $.Deferred().resolve().promise();
3387 if ( this.steps
.length
) {
3388 // Generate a chain reaction of promises
3389 promise
= proceed( this.steps
[ 0 ] )();
3390 for ( i
= 1, len
= this.steps
.length
; i
< len
; i
++ ) {
3391 promise
= promise
.then( proceed( this.steps
[ i
] ) );
3394 promise
= $.Deferred().resolve().promise();
3401 * Create a process step.
3404 * @param {number|jQuery.Promise|Function} step
3406 * - Number of milliseconds to wait; or
3407 * - Promise to wait to be resolved; or
3408 * - Function to execute
3409 * - If it returns boolean false the process will stop
3410 * - If it returns an object with a `promise` method the process will use the promise to either
3411 * continue to the next step when the promise is resolved or stop when the promise is rejected
3412 * - If it returns a number, the process will wait for that number of milliseconds before
3414 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3416 * @return {Object} Step object, with `callback` and `context` properties
3418 OO
.ui
.Process
.prototype.createStep = function ( step
, context
) {
3419 if ( typeof step
=== 'number' || $.isFunction( step
.promise
) ) {
3421 callback: function () {
3427 if ( $.isFunction( step
) ) {
3433 throw new Error( 'Cannot create process step: number, promise or function expected' );
3437 * Add step to the beginning of the process.
3439 * @inheritdoc #createStep
3440 * @return {OO.ui.Process} this
3443 OO
.ui
.Process
.prototype.first = function ( step
, context
) {
3444 this.steps
.unshift( this.createStep( step
, context
) );
3449 * Add step to the end of the process.
3451 * @inheritdoc #createStep
3452 * @return {OO.ui.Process} this
3455 OO
.ui
.Process
.prototype.next = function ( step
, context
) {
3456 this.steps
.push( this.createStep( step
, context
) );
3461 * Factory for tools.
3464 * @extends OO.Factory
3467 OO
.ui
.ToolFactory
= function OoUiToolFactory() {
3468 // Parent constructor
3469 OO
.ui
.ToolFactory
.super.call( this );
3474 OO
.inheritClass( OO
.ui
.ToolFactory
, OO
.Factory
);
3479 * Get tools from the factory
3481 * @param {Array} include Included tools
3482 * @param {Array} exclude Excluded tools
3483 * @param {Array} promote Promoted tools
3484 * @param {Array} demote Demoted tools
3485 * @return {string[]} List of tools
3487 OO
.ui
.ToolFactory
.prototype.getTools = function ( include
, exclude
, promote
, demote
) {
3488 var i
, len
, included
, promoted
, demoted
,
3492 // Collect included and not excluded tools
3493 included
= OO
.simpleArrayDifference( this.extract( include
), this.extract( exclude
) );
3496 promoted
= this.extract( promote
, used
);
3497 demoted
= this.extract( demote
, used
);
3500 for ( i
= 0, len
= included
.length
; i
< len
; i
++ ) {
3501 if ( !used
[ included
[ i
] ] ) {
3502 auto
.push( included
[ i
] );
3506 return promoted
.concat( auto
).concat( demoted
);
3510 * Get a flat list of names from a list of names or groups.
3512 * Tools can be specified in the following ways:
3514 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3515 * - All tools in a group: `{ group: 'group-name' }`
3516 * - All tools: `'*'`
3519 * @param {Array|string} collection List of tools
3520 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3521 * names will be added as properties
3522 * @return {string[]} List of extracted names
3524 OO
.ui
.ToolFactory
.prototype.extract = function ( collection
, used
) {
3525 var i
, len
, item
, name
, tool
,
3528 if ( collection
=== '*' ) {
3529 for ( name
in this.registry
) {
3530 tool
= this.registry
[ name
];
3532 // Only add tools by group name when auto-add is enabled
3533 tool
.static.autoAddToCatchall
&&
3534 // Exclude already used tools
3535 ( !used
|| !used
[ name
] )
3539 used
[ name
] = true;
3543 } else if ( Array
.isArray( collection
) ) {
3544 for ( i
= 0, len
= collection
.length
; i
< len
; i
++ ) {
3545 item
= collection
[ i
];
3546 // Allow plain strings as shorthand for named tools
3547 if ( typeof item
=== 'string' ) {
3548 item
= { name
: item
};
3550 if ( OO
.isPlainObject( item
) ) {
3552 for ( name
in this.registry
) {
3553 tool
= this.registry
[ name
];
3555 // Include tools with matching group
3556 tool
.static.group
=== item
.group
&&
3557 // Only add tools by group name when auto-add is enabled
3558 tool
.static.autoAddToGroup
&&
3559 // Exclude already used tools
3560 ( !used
|| !used
[ name
] )
3564 used
[ name
] = true;
3568 // Include tools with matching name and exclude already used tools
3569 } else if ( item
.name
&& ( !used
|| !used
[ item
.name
] ) ) {
3570 names
.push( item
.name
);
3572 used
[ item
.name
] = true;
3582 * Factory for tool groups.
3585 * @extends OO.Factory
3588 OO
.ui
.ToolGroupFactory
= function OoUiToolGroupFactory() {
3589 // Parent constructor
3590 OO
.Factory
.call( this );
3593 defaultClasses
= this.constructor.static.getDefaultClasses();
3595 // Register default toolgroups
3596 for ( i
= 0, l
= defaultClasses
.length
; i
< l
; i
++ ) {
3597 this.register( defaultClasses
[ i
] );
3603 OO
.inheritClass( OO
.ui
.ToolGroupFactory
, OO
.Factory
);
3605 /* Static Methods */
3608 * Get a default set of classes to be registered on construction
3610 * @return {Function[]} Default classes
3612 OO
.ui
.ToolGroupFactory
.static.getDefaultClasses = function () {
3615 OO
.ui
.ListToolGroup
,
3627 * @param {Object} [config] Configuration options
3629 OO
.ui
.Theme
= function OoUiTheme( config
) {
3630 // Configuration initialization
3631 config
= config
|| {};
3636 OO
.initClass( OO
.ui
.Theme
);
3641 * Get a list of classes to be applied to a widget.
3643 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
3644 * otherwise state transitions will not work properly.
3646 * @param {OO.ui.Element} element Element for which to get classes
3647 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3649 OO
.ui
.Theme
.prototype.getElementClasses = function ( /* element */ ) {
3650 return { on
: [], off
: [] };
3654 * Update CSS classes provided by the theme.
3656 * For elements with theme logic hooks, this should be called any time there's a state change.
3658 * @param {OO.ui.Element} element Element for which to update classes
3659 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3661 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
3662 var classes
= this.getElementClasses( element
);
3665 .removeClass( classes
.off
.join( ' ' ) )
3666 .addClass( classes
.on
.join( ' ' ) );
3670 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
3671 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
3672 * order in which users will navigate through the focusable elements via the "tab" key.
3675 * // TabIndexedElement is mixed into the ButtonWidget class
3676 * // to provide a tabIndex property.
3677 * var button1 = new OO.ui.ButtonWidget( {
3681 * var button2 = new OO.ui.ButtonWidget( {
3685 * var button3 = new OO.ui.ButtonWidget( {
3689 * var button4 = new OO.ui.ButtonWidget( {
3693 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
3699 * @param {Object} [config] Configuration options
3700 * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element
3701 * @cfg {number|null} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
3702 * prevent tab focusing, use null to suppress the `tabindex` attribute.
3704 OO
.ui
.TabIndexedElement
= function OoUiTabIndexedElement( config
) {
3705 // Configuration initialization
3706 config
= $.extend( { tabIndex
: 0 }, config
);
3709 this.$tabIndexed
= null;
3710 this.tabIndex
= null;
3713 this.connect( this, { disable
: 'onDisable' } );
3716 this.setTabIndex( config
.tabIndex
);
3717 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
3722 OO
.initClass( OO
.ui
.TabIndexedElement
);
3727 * Set the element with `tabindex` attribute.
3729 * If an element is already set, it will be cleaned up before setting up the new element.
3731 * @param {jQuery} $tabIndexed Element to set tab index on
3734 OO
.ui
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
3735 var tabIndex
= this.tabIndex
;
3736 // Remove attributes from old $tabIndexed
3737 this.setTabIndex( null );
3738 // Force update of new $tabIndexed
3739 this.$tabIndexed
= $tabIndexed
;
3740 this.tabIndex
= tabIndex
;
3741 return this.updateTabIndex();
3745 * Set tab index value.
3747 * @param {number|null} tabIndex Tab index value or null for no tab index
3750 OO
.ui
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
3751 tabIndex
= typeof tabIndex
=== 'number' ? tabIndex
: null;
3753 if ( this.tabIndex
!== tabIndex
) {
3754 this.tabIndex
= tabIndex
;
3755 this.updateTabIndex();
3762 * Update the `tabindex` attribute, in case of changes to tab index or
3767 OO
.ui
.TabIndexedElement
.prototype.updateTabIndex = function () {
3768 if ( this.$tabIndexed
) {
3769 if ( this.tabIndex
!== null ) {
3770 // Do not index over disabled elements
3771 this.$tabIndexed
.attr( {
3772 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
3773 // ChromeVox and NVDA do not seem to inherit this from parent elements
3774 'aria-disabled': this.isDisabled().toString()
3777 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
3784 * Handle disable events.
3787 * @param {boolean} disabled Element is disabled
3789 OO
.ui
.TabIndexedElement
.prototype.onDisable = function () {
3790 this.updateTabIndex();
3794 * Get tab index value.
3796 * @return {number|null} Tab index value
3798 OO
.ui
.TabIndexedElement
.prototype.getTabIndex = function () {
3799 return this.tabIndex
;
3803 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
3804 * interface element that can be configured with access keys for accessibility.
3805 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
3807 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
3812 * @param {Object} [config] Configuration options
3813 * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
3814 * @cfg {boolean} [framed=true] Render button with a frame
3815 * @cfg {string} [accessKey] Button's access key
3817 OO
.ui
.ButtonElement
= function OoUiButtonElement( config
) {
3818 // Configuration initialization
3819 config
= config
|| {};
3822 this.$button
= config
.$button
|| $( '<a>' );
3824 this.accessKey
= null;
3825 this.active
= false;
3826 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
3827 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
3828 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
3829 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
3830 this.onClickHandler
= this.onClick
.bind( this );
3831 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
3834 this.$element
.addClass( 'oo-ui-buttonElement' );
3835 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
3836 this.setAccessKey( config
.accessKey
);
3837 this.setButtonElement( this.$button
);
3842 OO
.initClass( OO
.ui
.ButtonElement
);
3844 /* Static Properties */
3847 * Cancel mouse down events.
3851 * @property {boolean}
3853 OO
.ui
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
3864 * Set the button element.
3866 * If an element is already set, it will be cleaned up before setting up the new element.
3868 * @param {jQuery} $button Element to use as button
3870 OO
.ui
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
3871 if ( this.$button
) {
3873 .removeClass( 'oo-ui-buttonElement-button' )
3874 .removeAttr( 'role accesskey' )
3876 mousedown
: this.onMouseDownHandler
,
3877 keydown
: this.onKeyDownHandler
,
3878 click
: this.onClickHandler
,
3879 keypress
: this.onKeyPressHandler
3883 this.$button
= $button
3884 .addClass( 'oo-ui-buttonElement-button' )
3885 .attr( { role
: 'button', accesskey
: this.accessKey
} )
3887 mousedown
: this.onMouseDownHandler
,
3888 keydown
: this.onKeyDownHandler
,
3889 click
: this.onClickHandler
,
3890 keypress
: this.onKeyPressHandler
3895 * Handles mouse down events.
3898 * @param {jQuery.Event} e Mouse down event
3900 OO
.ui
.ButtonElement
.prototype.onMouseDown = function ( e
) {
3901 if ( this.isDisabled() || e
.which
!== 1 ) {
3904 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
3905 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
3906 // reliably remove the pressed class
3907 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
3908 // Prevent change of focus unless specifically configured otherwise
3909 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
3915 * Handles mouse up events.
3918 * @param {jQuery.Event} e Mouse up event
3920 OO
.ui
.ButtonElement
.prototype.onMouseUp = function ( e
) {
3921 if ( this.isDisabled() || e
.which
!== 1 ) {
3924 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
3925 // Stop listening for mouseup, since we only needed this once
3926 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
3930 * Handles mouse click events.
3933 * @param {jQuery.Event} e Mouse click event
3936 OO
.ui
.ButtonElement
.prototype.onClick = function ( e
) {
3937 if ( !this.isDisabled() && e
.which
=== 1 ) {
3938 this.emit( 'click' );
3944 * Handles key down events.
3947 * @param {jQuery.Event} e Key down event
3949 OO
.ui
.ButtonElement
.prototype.onKeyDown = function ( e
) {
3950 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
3953 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
3954 // Run the keyup handler no matter where the key is when the button is let go, so we can
3955 // reliably remove the pressed class
3956 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
3960 * Handles key up events.
3963 * @param {jQuery.Event} e Key up event
3965 OO
.ui
.ButtonElement
.prototype.onKeyUp = function ( e
) {
3966 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
3969 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
3970 // Stop listening for keyup, since we only needed this once
3971 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
3975 * Handles key press events.
3978 * @param {jQuery.Event} e Key press event
3981 OO
.ui
.ButtonElement
.prototype.onKeyPress = function ( e
) {
3982 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
3983 this.emit( 'click' );
3989 * Check if button has a frame.
3991 * @return {boolean} Button is framed
3993 OO
.ui
.ButtonElement
.prototype.isFramed = function () {
4000 * @param {boolean} [framed] Make button framed, omit to toggle
4003 OO
.ui
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
4004 framed
= framed
=== undefined ? !this.framed
: !!framed
;
4005 if ( framed
!== this.framed
) {
4006 this.framed
= framed
;
4008 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
4009 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
4010 this.updateThemeClasses();
4019 * @param {string} accessKey Button's access key, use empty string to remove
4022 OO
.ui
.ButtonElement
.prototype.setAccessKey = function ( accessKey
) {
4023 accessKey
= typeof accessKey
=== 'string' && accessKey
.length
? accessKey
: null;
4025 if ( this.accessKey
!== accessKey
) {
4026 if ( this.$button
) {
4027 if ( accessKey
!== null ) {
4028 this.$button
.attr( 'accesskey', accessKey
);
4030 this.$button
.removeAttr( 'accesskey' );
4033 this.accessKey
= accessKey
;
4042 * @param {boolean} [value] Make button active
4045 OO
.ui
.ButtonElement
.prototype.setActive = function ( value
) {
4046 this.$element
.toggleClass( 'oo-ui-buttonElement-active', !!value
);
4051 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4052 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4053 * items from the group is done through the interface the class provides.
4054 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4056 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4062 * @param {Object} [config] Configuration options
4063 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4065 OO
.ui
.GroupElement
= function OoUiGroupElement( config
) {
4066 // Configuration initialization
4067 config
= config
|| {};
4072 this.aggregateItemEvents
= {};
4075 this.setGroupElement( config
.$group
|| $( '<div>' ) );
4081 * Set the group element.
4083 * If an element is already set, items will be moved to the new element.
4085 * @param {jQuery} $group Element to use as group
4087 OO
.ui
.GroupElement
.prototype.setGroupElement = function ( $group
) {
4090 this.$group
= $group
;
4091 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4092 this.$group
.append( this.items
[ i
].$element
);
4097 * Check if there are no items.
4099 * @return {boolean} Group is empty
4101 OO
.ui
.GroupElement
.prototype.isEmpty = function () {
4102 return !this.items
.length
;
4108 * @return {OO.ui.Element[]} Items
4110 OO
.ui
.GroupElement
.prototype.getItems = function () {
4111 return this.items
.slice( 0 );
4115 * Get an item by its data.
4117 * Data is compared by a hash of its value. Only the first item with matching data will be returned.
4119 * @param {Object} data Item data to search for
4120 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4122 OO
.ui
.GroupElement
.prototype.getItemFromData = function ( data
) {
4124 hash
= OO
.getHash( data
);
4126 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4127 item
= this.items
[ i
];
4128 if ( hash
=== OO
.getHash( item
.getData() ) ) {
4137 * Get items by their data.
4139 * Data is compared by a hash of its value. All items with matching data will be returned.
4141 * @param {Object} data Item data to search for
4142 * @return {OO.ui.Element[]} Items with equivalent data
4144 OO
.ui
.GroupElement
.prototype.getItemsFromData = function ( data
) {
4146 hash
= OO
.getHash( data
),
4149 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4150 item
= this.items
[ i
];
4151 if ( hash
=== OO
.getHash( item
.getData() ) ) {
4160 * Add an aggregate item event.
4162 * Aggregated events are listened to on each item and then emitted by the group under a new name,
4163 * and with an additional leading parameter containing the item that emitted the original event.
4164 * Other arguments that were emitted from the original event are passed through.
4166 * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
4167 * event, use null value to remove aggregation
4168 * @throws {Error} If aggregation already exists
4170 OO
.ui
.GroupElement
.prototype.aggregate = function ( events
) {
4171 var i
, len
, item
, add
, remove
, itemEvent
, groupEvent
;
4173 for ( itemEvent
in events
) {
4174 groupEvent
= events
[ itemEvent
];
4176 // Remove existing aggregated event
4177 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
4178 // Don't allow duplicate aggregations
4180 throw new Error( 'Duplicate item event aggregation for ' + itemEvent
);
4182 // Remove event aggregation from existing items
4183 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4184 item
= this.items
[ i
];
4185 if ( item
.connect
&& item
.disconnect
) {
4187 remove
[ itemEvent
] = [ 'emit', groupEvent
, item
];
4188 item
.disconnect( this, remove
);
4191 // Prevent future items from aggregating event
4192 delete this.aggregateItemEvents
[ itemEvent
];
4195 // Add new aggregate event
4197 // Make future items aggregate event
4198 this.aggregateItemEvents
[ itemEvent
] = groupEvent
;
4199 // Add event aggregation to existing items
4200 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4201 item
= this.items
[ i
];
4202 if ( item
.connect
&& item
.disconnect
) {
4204 add
[ itemEvent
] = [ 'emit', groupEvent
, item
];
4205 item
.connect( this, add
);
4215 * Adding an existing item will move it.
4217 * @param {OO.ui.Element[]} items Items
4218 * @param {number} [index] Index to insert items at
4221 OO
.ui
.GroupElement
.prototype.addItems = function ( items
, index
) {
4222 var i
, len
, item
, event
, events
, currentIndex
,
4225 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
4228 // Check if item exists then remove it first, effectively "moving" it
4229 currentIndex
= $.inArray( item
, this.items
);
4230 if ( currentIndex
>= 0 ) {
4231 this.removeItems( [ item
] );
4232 // Adjust index to compensate for removal
4233 if ( currentIndex
< index
) {
4238 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
4240 for ( event
in this.aggregateItemEvents
) {
4241 events
[ event
] = [ 'emit', this.aggregateItemEvents
[ event
], item
];
4243 item
.connect( this, events
);
4245 item
.setElementGroup( this );
4246 itemElements
.push( item
.$element
.get( 0 ) );
4249 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
4250 this.$group
.append( itemElements
);
4251 this.items
.push
.apply( this.items
, items
);
4252 } else if ( index
=== 0 ) {
4253 this.$group
.prepend( itemElements
);
4254 this.items
.unshift
.apply( this.items
, items
);
4256 this.items
[ index
].$element
.before( itemElements
);
4257 this.items
.splice
.apply( this.items
, [ index
, 0 ].concat( items
) );
4266 * Items will be detached, not removed, so they can be used later.
4268 * @param {OO.ui.Element[]} items Items to remove
4271 OO
.ui
.GroupElement
.prototype.removeItems = function ( items
) {
4272 var i
, len
, item
, index
, remove
, itemEvent
;
4274 // Remove specific items
4275 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
4277 index
= $.inArray( item
, this.items
);
4278 if ( index
!== -1 ) {
4280 item
.connect
&& item
.disconnect
&&
4281 !$.isEmptyObject( this.aggregateItemEvents
)
4284 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
4285 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
4287 item
.disconnect( this, remove
);
4289 item
.setElementGroup( null );
4290 this.items
.splice( index
, 1 );
4291 item
.$element
.detach();
4301 * Items will be detached, not removed, so they can be used later.
4305 OO
.ui
.GroupElement
.prototype.clearItems = function () {
4306 var i
, len
, item
, remove
, itemEvent
;
4309 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4310 item
= this.items
[ i
];
4312 item
.connect
&& item
.disconnect
&&
4313 !$.isEmptyObject( this.aggregateItemEvents
)
4316 if ( Object
.prototype.hasOwnProperty
.call( this.aggregateItemEvents
, itemEvent
) ) {
4317 remove
[ itemEvent
] = [ 'emit', this.aggregateItemEvents
[ itemEvent
], item
];
4319 item
.disconnect( this, remove
);
4321 item
.setElementGroup( null );
4322 item
.$element
.detach();
4330 * DraggableElement is a mixin class used to create elements that can be clicked
4331 * and dragged by a mouse to a new position within a group. This class must be used
4332 * in conjunction with OO.ui.DraggableGroupElement, which provides a container for
4333 * the draggable elements.
4340 OO
.ui
.DraggableElement
= function OoUiDraggableElement() {
4344 // Initialize and events
4346 .attr( 'draggable', true )
4347 .addClass( 'oo-ui-draggableElement' )
4349 dragstart
: this.onDragStart
.bind( this ),
4350 dragover
: this.onDragOver
.bind( this ),
4351 dragend
: this.onDragEnd
.bind( this ),
4352 drop
: this.onDrop
.bind( this )
4356 OO
.initClass( OO
.ui
.DraggableElement
);
4363 * A dragstart event is emitted when the user clicks and begins dragging an item.
4364 * @param {OO.ui.DraggableElement} item The item the user has clicked and is dragging with the mouse.
4369 * A dragend event is emitted when the user drags an item and releases the mouse,
4370 * thus terminating the drag operation.
4375 * A drop event is emitted when the user drags an item and then releases the mouse button
4376 * over a valid target.
4379 /* Static Properties */
4382 * @inheritdoc OO.ui.ButtonElement
4384 OO
.ui
.DraggableElement
.static.cancelButtonMouseDownEvents
= false;
4389 * Respond to dragstart event.
4392 * @param {jQuery.Event} event jQuery event
4395 OO
.ui
.DraggableElement
.prototype.onDragStart = function ( e
) {
4396 var dataTransfer
= e
.originalEvent
.dataTransfer
;
4397 // Define drop effect
4398 dataTransfer
.dropEffect
= 'none';
4399 dataTransfer
.effectAllowed
= 'move';
4400 // We must set up a dataTransfer data property or Firefox seems to
4401 // ignore the fact the element is draggable.
4403 dataTransfer
.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4405 // The above is only for firefox. No need to set a catch clause
4406 // if it fails, move on.
4408 // Add dragging class
4409 this.$element
.addClass( 'oo-ui-draggableElement-dragging' );
4411 this.emit( 'dragstart', this );
4416 * Respond to dragend event.
4421 OO
.ui
.DraggableElement
.prototype.onDragEnd = function () {
4422 this.$element
.removeClass( 'oo-ui-draggableElement-dragging' );
4423 this.emit( 'dragend' );
4427 * Handle drop event.
4430 * @param {jQuery.Event} event jQuery event
4433 OO
.ui
.DraggableElement
.prototype.onDrop = function ( e
) {
4435 this.emit( 'drop', e
);
4439 * In order for drag/drop to work, the dragover event must
4440 * return false and stop propogation.
4444 OO
.ui
.DraggableElement
.prototype.onDragOver = function ( e
) {
4450 * Store it in the DOM so we can access from the widget drag event
4453 * @param {number} Item index
4455 OO
.ui
.DraggableElement
.prototype.setIndex = function ( index
) {
4456 if ( this.index
!== index
) {
4458 this.$element
.data( 'index', index
);
4466 * @return {number} Item index
4468 OO
.ui
.DraggableElement
.prototype.getIndex = function () {
4473 * DraggableGroupElement is a mixin class used to create a group element to
4474 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4475 * The class is used with OO.ui.DraggableElement.
4481 * @param {Object} [config] Configuration options
4482 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4483 * @cfg {string} [orientation] Item orientation, 'horizontal' or 'vertical'. Defaults to 'vertical'
4485 OO
.ui
.DraggableGroupElement
= function OoUiDraggableGroupElement( config
) {
4486 // Configuration initialization
4487 config
= config
|| {};
4489 // Parent constructor
4490 OO
.ui
.GroupElement
.call( this, config
);
4493 this.orientation
= config
.orientation
|| 'vertical';
4494 this.dragItem
= null;
4495 this.itemDragOver
= null;
4497 this.sideInsertion
= '';
4501 dragstart
: 'itemDragStart',
4502 dragend
: 'itemDragEnd',
4505 this.connect( this, {
4506 itemDragStart
: 'onItemDragStart',
4507 itemDrop
: 'onItemDrop',
4508 itemDragEnd
: 'onItemDragEnd'
4511 dragover
: $.proxy( this.onDragOver
, this ),
4512 dragleave
: $.proxy( this.onDragLeave
, this )
4516 if ( Array
.isArray( config
.items
) ) {
4517 this.addItems( config
.items
);
4519 this.$placeholder
= $( '<div>' )
4520 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4522 .addClass( 'oo-ui-draggableGroupElement' )
4523 .append( this.$status
)
4524 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation
=== 'horizontal' )
4525 .prepend( this.$placeholder
);
4529 OO
.mixinClass( OO
.ui
.DraggableGroupElement
, OO
.ui
.GroupElement
);
4535 * @param {OO.ui.DraggableElement} item Reordered item
4536 * @param {number} [newIndex] New index for the item
4542 * Respond to item drag start event
4543 * @param {OO.ui.DraggableElement} item Dragged item
4545 OO
.ui
.DraggableGroupElement
.prototype.onItemDragStart = function ( item
) {
4548 // Map the index of each object
4549 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
4550 this.items
[ i
].setIndex( i
);
4553 if ( this.orientation
=== 'horizontal' ) {
4554 // Set the height of the indicator
4555 this.$placeholder
.css( {
4556 height
: item
.$element
.outerHeight(),
4560 // Set the width of the indicator
4561 this.$placeholder
.css( {
4563 width
: item
.$element
.outerWidth()
4566 this.setDragItem( item
);
4570 * Respond to item drag end event
4572 OO
.ui
.DraggableGroupElement
.prototype.onItemDragEnd = function () {
4573 this.unsetDragItem();
4578 * Handle drop event and switch the order of the items accordingly
4579 * @param {OO.ui.DraggableElement} item Dropped item
4582 OO
.ui
.DraggableGroupElement
.prototype.onItemDrop = function ( item
) {
4583 var toIndex
= item
.getIndex();
4584 // Check if the dropped item is from the current group
4585 // TODO: Figure out a way to configure a list of legally droppable
4586 // elements even if they are not yet in the list
4587 if ( this.getDragItem() ) {
4588 // If the insertion point is 'after', the insertion index
4589 // is shifted to the right (or to the left in RTL, hence 'after')
4590 if ( this.sideInsertion
=== 'after' ) {
4593 // Emit change event
4594 this.emit( 'reorder', this.getDragItem(), toIndex
);
4596 this.unsetDragItem();
4597 // Return false to prevent propogation
4602 * Handle dragleave event.
4604 OO
.ui
.DraggableGroupElement
.prototype.onDragLeave = function () {
4605 // This means the item was dragged outside the widget
4608 .addClass( 'oo-ui-element-hidden' );
4612 * Respond to dragover event
4613 * @param {jQuery.Event} event Event details
4615 OO
.ui
.DraggableGroupElement
.prototype.onDragOver = function ( e
) {
4616 var dragOverObj
, $optionWidget
, itemOffset
, itemMidpoint
, itemBoundingRect
,
4617 itemSize
, cssOutput
, dragPosition
, itemIndex
, itemPosition
,
4618 clientX
= e
.originalEvent
.clientX
,
4619 clientY
= e
.originalEvent
.clientY
;
4621 // Get the OptionWidget item we are dragging over
4622 dragOverObj
= this.getElementDocument().elementFromPoint( clientX
, clientY
);
4623 $optionWidget
= $( dragOverObj
).closest( '.oo-ui-draggableElement' );
4624 if ( $optionWidget
[ 0 ] ) {
4625 itemOffset
= $optionWidget
.offset();
4626 itemBoundingRect
= $optionWidget
[ 0 ].getBoundingClientRect();
4627 itemPosition
= $optionWidget
.position();
4628 itemIndex
= $optionWidget
.data( 'index' );
4633 this.isDragging() &&
4634 itemIndex
!== this.getDragItem().getIndex()
4636 if ( this.orientation
=== 'horizontal' ) {
4637 // Calculate where the mouse is relative to the item width
4638 itemSize
= itemBoundingRect
.width
;
4639 itemMidpoint
= itemBoundingRect
.left
+ itemSize
/ 2;
4640 dragPosition
= clientX
;
4641 // Which side of the item we hover over will dictate
4642 // where the placeholder will appear, on the left or
4645 left
: dragPosition
< itemMidpoint
? itemPosition
.left
: itemPosition
.left
+ itemSize
,
4646 top
: itemPosition
.top
4649 // Calculate where the mouse is relative to the item height
4650 itemSize
= itemBoundingRect
.height
;
4651 itemMidpoint
= itemBoundingRect
.top
+ itemSize
/ 2;
4652 dragPosition
= clientY
;
4653 // Which side of the item we hover over will dictate
4654 // where the placeholder will appear, on the top or
4657 top
: dragPosition
< itemMidpoint
? itemPosition
.top
: itemPosition
.top
+ itemSize
,
4658 left
: itemPosition
.left
4661 // Store whether we are before or after an item to rearrange
4662 // For horizontal layout, we need to account for RTL, as this is flipped
4663 if ( this.orientation
=== 'horizontal' && this.$element
.css( 'direction' ) === 'rtl' ) {
4664 this.sideInsertion
= dragPosition
< itemMidpoint
? 'after' : 'before';
4666 this.sideInsertion
= dragPosition
< itemMidpoint
? 'before' : 'after';
4668 // Add drop indicator between objects
4671 .removeClass( 'oo-ui-element-hidden' );
4673 // This means the item was dragged outside the widget
4676 .addClass( 'oo-ui-element-hidden' );
4683 * Set a dragged item
4684 * @param {OO.ui.DraggableElement} item Dragged item
4686 OO
.ui
.DraggableGroupElement
.prototype.setDragItem = function ( item
) {
4687 this.dragItem
= item
;
4691 * Unset the current dragged item
4693 OO
.ui
.DraggableGroupElement
.prototype.unsetDragItem = function () {
4694 this.dragItem
= null;
4695 this.itemDragOver
= null;
4696 this.$placeholder
.addClass( 'oo-ui-element-hidden' );
4697 this.sideInsertion
= '';
4701 * Get the current dragged item
4702 * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
4704 OO
.ui
.DraggableGroupElement
.prototype.getDragItem = function () {
4705 return this.dragItem
;
4709 * Check if there's an item being dragged.
4710 * @return {Boolean} Item is being dragged
4712 OO
.ui
.DraggableGroupElement
.prototype.isDragging = function () {
4713 return this.getDragItem() !== null;
4717 * IconElement is often mixed into other classes to generate an icon.
4718 * Icons are graphics, about the size of normal text. They are used to aid the user
4719 * in locating a control or to convey information in a space-efficient way. See the
4720 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
4721 * included in the library.
4723 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4729 * @param {Object} [config] Configuration options
4730 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
4731 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
4732 * the icon element be set to an existing icon instead of the one generated by this class, set a
4733 * value using a jQuery selection. For example:
4735 * // Use a <div> tag instead of a <span>
4737 * // Use an existing icon element instead of the one generated by the class
4738 * $icon: this.$element
4739 * // Use an icon element from a child widget
4740 * $icon: this.childwidget.$element
4741 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
4742 * symbolic names. A map is used for i18n purposes and contains a `default` icon
4743 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
4744 * by the user's language.
4746 * Example of an i18n map:
4748 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4749 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
4750 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4751 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
4752 * text. The icon title is displayed when users move the mouse over the icon.
4754 OO
.ui
.IconElement
= function OoUiIconElement( config
) {
4755 // Configuration initialization
4756 config
= config
|| {};
4761 this.iconTitle
= null;
4764 this.setIcon( config
.icon
|| this.constructor.static.icon
);
4765 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
4766 this.setIconElement( config
.$icon
|| $( '<span>' ) );
4771 OO
.initClass( OO
.ui
.IconElement
);
4773 /* Static Properties */
4776 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
4777 * for i18n purposes and contains a `default` icon name and additional names keyed by
4778 * language code. The `default` name is used when no icon is keyed by the user's language.
4780 * Example of an i18n map:
4782 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4784 * Note: the static property will be overridden if the #icon configuration is used.
4788 * @property {Object|string}
4790 OO
.ui
.IconElement
.static.icon
= null;
4793 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
4794 * function that returns title text, or `null` for no title.
4796 * The static property will be overridden if the #iconTitle configuration is used.
4800 * @property {string|Function|null}
4802 OO
.ui
.IconElement
.static.iconTitle
= null;
4807 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
4808 * applies to the specified icon element instead of the one created by the class. If an icon
4809 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
4810 * and mixin methods will no longer affect the element.
4812 * @param {jQuery} $icon Element to use as icon
4814 OO
.ui
.IconElement
.prototype.setIconElement = function ( $icon
) {
4817 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
4818 .removeAttr( 'title' );
4822 .addClass( 'oo-ui-iconElement-icon' )
4823 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
4824 if ( this.iconTitle
!== null ) {
4825 this.$icon
.attr( 'title', this.iconTitle
);
4830 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
4831 * The icon parameter can also be set to a map of icon names. See the #icon config setting
4834 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
4835 * by language code, or `null` to remove the icon.
4838 OO
.ui
.IconElement
.prototype.setIcon = function ( icon
) {
4839 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
4840 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
4842 if ( this.icon
!== icon
) {
4844 if ( this.icon
!== null ) {
4845 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
4847 if ( icon
!== null ) {
4848 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
4854 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
4855 this.updateThemeClasses();
4861 * Set the icon title. Use `null` to remove the title.
4863 * @param {string|Function|null} iconTitle A text string used as the icon title,
4864 * a function that returns title text, or `null` for no title.
4867 OO
.ui
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
4868 iconTitle
= typeof iconTitle
=== 'function' ||
4869 ( typeof iconTitle
=== 'string' && iconTitle
.length
) ?
4870 OO
.ui
.resolveMsg( iconTitle
) : null;
4872 if ( this.iconTitle
!== iconTitle
) {
4873 this.iconTitle
= iconTitle
;
4875 if ( this.iconTitle
!== null ) {
4876 this.$icon
.attr( 'title', iconTitle
);
4878 this.$icon
.removeAttr( 'title' );
4887 * Get the symbolic name of the icon.
4889 * @return {string} Icon name
4891 OO
.ui
.IconElement
.prototype.getIcon = function () {
4896 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
4898 * @return {string} Icon title text
4900 OO
.ui
.IconElement
.prototype.getIconTitle = function () {
4901 return this.iconTitle
;
4905 * IndicatorElement is often mixed into other classes to generate an indicator.
4906 * Indicators are small graphics that are generally used in two ways:
4908 * - To draw attention to the status of an item. For example, an indicator might be
4909 * used to show that an item in a list has errors that need to be resolved.
4910 * - To clarify the function of a control that acts in an exceptional way (a button
4911 * that opens a menu instead of performing an action directly, for example).
4913 * For a list of indicators included in the library, please see the
4914 * [OOjs UI documentation on MediaWiki] [1].
4916 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4922 * @param {Object} [config] Configuration options
4923 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
4924 * configuration is omitted, the indicator element will use a generated `<span>`.
4925 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
4926 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
4928 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4929 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
4930 * or a function that returns title text. The indicator title is displayed when users move
4931 * the mouse over the indicator.
4933 OO
.ui
.IndicatorElement
= function OoUiIndicatorElement( config
) {
4934 // Configuration initialization
4935 config
= config
|| {};
4938 this.$indicator
= null;
4939 this.indicator
= null;
4940 this.indicatorTitle
= null;
4943 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
4944 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
4945 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
4950 OO
.initClass( OO
.ui
.IndicatorElement
);
4952 /* Static Properties */
4955 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
4956 * The static property will be overridden if the #indicator configuration is used.
4960 * @property {string|null}
4962 OO
.ui
.IndicatorElement
.static.indicator
= null;
4965 * A text string used as the indicator title, a function that returns title text, or `null`
4966 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
4970 * @property {string|Function|null}
4972 OO
.ui
.IndicatorElement
.static.indicatorTitle
= null;
4977 * Set the indicator element.
4979 * If an element is already set, it will be cleaned up before setting up the new element.
4981 * @param {jQuery} $indicator Element to use as indicator
4983 OO
.ui
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
4984 if ( this.$indicator
) {
4986 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
4987 .removeAttr( 'title' );
4990 this.$indicator
= $indicator
4991 .addClass( 'oo-ui-indicatorElement-indicator' )
4992 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
4993 if ( this.indicatorTitle
!== null ) {
4994 this.$indicator
.attr( 'title', this.indicatorTitle
);
4999 * Set indicator name.
5001 * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
5004 OO
.ui
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
5005 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
5007 if ( this.indicator
!== indicator
) {
5008 if ( this.$indicator
) {
5009 if ( this.indicator
!== null ) {
5010 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
5012 if ( indicator
!== null ) {
5013 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
5016 this.indicator
= indicator
;
5019 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
5020 this.updateThemeClasses();
5026 * Set indicator title.
5028 * @param {string|Function|null} indicator Indicator title text, a function that returns text or
5029 * null for no indicator title
5032 OO
.ui
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
5033 indicatorTitle
= typeof indicatorTitle
=== 'function' ||
5034 ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ?
5035 OO
.ui
.resolveMsg( indicatorTitle
) : null;
5037 if ( this.indicatorTitle
!== indicatorTitle
) {
5038 this.indicatorTitle
= indicatorTitle
;
5039 if ( this.$indicator
) {
5040 if ( this.indicatorTitle
!== null ) {
5041 this.$indicator
.attr( 'title', indicatorTitle
);
5043 this.$indicator
.removeAttr( 'title' );
5052 * Get indicator name.
5054 * @return {string} Symbolic name of indicator
5056 OO
.ui
.IndicatorElement
.prototype.getIndicator = function () {
5057 return this.indicator
;
5061 * Get indicator title.
5063 * @return {string} Indicator title text
5065 OO
.ui
.IndicatorElement
.prototype.getIndicatorTitle = function () {
5066 return this.indicatorTitle
;
5070 * LabelElement is often mixed into other classes to generate a label, which
5071 * helps identify the function of an interface element.
5072 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5074 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5080 * @param {Object} [config] Configuration options
5081 * @cfg {jQuery} [$label] The label element created by the class. If this
5082 * configuration is omitted, the label element will use a generated `<span>`.
5083 * @cfg {jQuery|string|Function} [label] The label text. The label can be specified as a plaintext string,
5084 * a jQuery selection of elements, or a function that will produce a string in the future. See the
5085 * [OOjs UI documentation on MediaWiki] [2] for examples.
5086 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5087 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5088 * The label will be truncated to fit if necessary.
5090 OO
.ui
.LabelElement
= function OoUiLabelElement( config
) {
5091 // Configuration initialization
5092 config
= config
|| {};
5097 this.autoFitLabel
= config
.autoFitLabel
=== undefined || !!config
.autoFitLabel
;
5100 this.setLabel( config
.label
|| this.constructor.static.label
);
5101 this.setLabelElement( config
.$label
|| $( '<span>' ) );
5106 OO
.initClass( OO
.ui
.LabelElement
);
5111 * @event labelChange
5112 * @param {string} value
5115 /* Static Properties */
5118 * The label text. The label can be specified as a plaintext string, a function that will
5119 * produce a string in the future, or `null` for no label. The static value will
5120 * be overridden if a label is specified with the #label config option.
5124 * @property {string|Function|null}
5126 OO
.ui
.LabelElement
.static.label
= null;
5131 * Set the label element.
5133 * If an element is already set, it will be cleaned up before setting up the new element.
5135 * @param {jQuery} $label Element to use as label
5137 OO
.ui
.LabelElement
.prototype.setLabelElement = function ( $label
) {
5138 if ( this.$label
) {
5139 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
5142 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
5143 this.setLabelContent( this.label
);
5149 * An empty string will result in the label being hidden. A string containing only whitespace will
5150 * be converted to a single ` `.
5152 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
5153 * text; or null for no label
5156 OO
.ui
.LabelElement
.prototype.setLabel = function ( label
) {
5157 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
5158 label
= ( ( typeof label
=== 'string' && label
.length
) || label
instanceof jQuery
|| label
instanceof OO
.ui
.HtmlSnippet
) ? label
: null;
5160 this.$element
.toggleClass( 'oo-ui-labelElement', !!label
);
5162 if ( this.label
!== label
) {
5163 if ( this.$label
) {
5164 this.setLabelContent( label
);
5167 this.emit( 'labelChange' );
5176 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
5177 * text; or null for no label
5179 OO
.ui
.LabelElement
.prototype.getLabel = function () {
5188 OO
.ui
.LabelElement
.prototype.fitLabel = function () {
5189 if ( this.$label
&& this.$label
.autoEllipsis
&& this.autoFitLabel
) {
5190 this.$label
.autoEllipsis( { hasSpan
: false, tooltip
: true } );
5197 * Set the content of the label.
5199 * Do not call this method until after the label element has been set by #setLabelElement.
5202 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
5203 * text; or null for no label
5205 OO
.ui
.LabelElement
.prototype.setLabelContent = function ( label
) {
5206 if ( typeof label
=== 'string' ) {
5207 if ( label
.match( /^\s*$/ ) ) {
5208 // Convert whitespace only string to a single non-breaking space
5209 this.$label
.html( ' ' );
5211 this.$label
.text( label
);
5213 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
5214 this.$label
.html( label
.toString() );
5215 } else if ( label
instanceof jQuery
) {
5216 this.$label
.empty().append( label
);
5218 this.$label
.empty();
5223 * Mixin that adds a menu showing suggested values for a OO.ui.TextInputWidget.
5225 * Subclasses that set the value of #lookupInput from #onLookupMenuItemChoose should
5226 * be aware that this will cause new suggestions to be looked up for the new value. If this is
5227 * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
5233 * @param {Object} [config] Configuration options
5234 * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
5235 * @cfg {jQuery} [$container=this.$element] Element to render menu under
5237 OO
.ui
.LookupElement
= function OoUiLookupElement( config
) {
5238 // Configuration initialization
5239 config
= config
|| {};
5242 this.$overlay
= config
.$overlay
|| this.$element
;
5243 this.lookupMenu
= new OO
.ui
.TextInputMenuSelectWidget( this, {
5246 $container
: config
.$container
5248 this.lookupCache
= {};
5249 this.lookupQuery
= null;
5250 this.lookupRequest
= null;
5251 this.lookupsDisabled
= false;
5252 this.lookupInputFocused
= false;
5256 focus
: this.onLookupInputFocus
.bind( this ),
5257 blur
: this.onLookupInputBlur
.bind( this ),
5258 mousedown
: this.onLookupInputMouseDown
.bind( this )
5260 this.connect( this, { change
: 'onLookupInputChange' } );
5261 this.lookupMenu
.connect( this, {
5262 toggle
: 'onLookupMenuToggle',
5263 choose
: 'onLookupMenuItemChoose'
5267 this.$element
.addClass( 'oo-ui-lookupElement' );
5268 this.lookupMenu
.$element
.addClass( 'oo-ui-lookupElement-menu' );
5269 this.$overlay
.append( this.lookupMenu
.$element
);
5275 * Handle input focus event.
5277 * @param {jQuery.Event} e Input focus event
5279 OO
.ui
.LookupElement
.prototype.onLookupInputFocus = function () {
5280 this.lookupInputFocused
= true;
5281 this.populateLookupMenu();
5285 * Handle input blur event.
5287 * @param {jQuery.Event} e Input blur event
5289 OO
.ui
.LookupElement
.prototype.onLookupInputBlur = function () {
5290 this.closeLookupMenu();
5291 this.lookupInputFocused
= false;
5295 * Handle input mouse down event.
5297 * @param {jQuery.Event} e Input mouse down event
5299 OO
.ui
.LookupElement
.prototype.onLookupInputMouseDown = function () {
5300 // Only open the menu if the input was already focused.
5301 // This way we allow the user to open the menu again after closing it with Esc
5302 // by clicking in the input. Opening (and populating) the menu when initially
5303 // clicking into the input is handled by the focus handler.
5304 if ( this.lookupInputFocused
&& !this.lookupMenu
.isVisible() ) {
5305 this.populateLookupMenu();
5310 * Handle input change event.
5312 * @param {string} value New input value
5314 OO
.ui
.LookupElement
.prototype.onLookupInputChange = function () {
5315 if ( this.lookupInputFocused
) {
5316 this.populateLookupMenu();
5321 * Handle the lookup menu being shown/hidden.
5323 * @param {boolean} visible Whether the lookup menu is now visible.
5325 OO
.ui
.LookupElement
.prototype.onLookupMenuToggle = function ( visible
) {
5327 // When the menu is hidden, abort any active request and clear the menu.
5328 // This has to be done here in addition to closeLookupMenu(), because
5329 // MenuSelectWidget will close itself when the user presses Esc.
5330 this.abortLookupRequest();
5331 this.lookupMenu
.clearItems();
5336 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5338 * @param {OO.ui.MenuOptionWidget|null} item Selected item
5340 OO
.ui
.LookupElement
.prototype.onLookupMenuItemChoose = function ( item
) {
5342 this.setValue( item
.getData() );
5349 * @return {OO.ui.TextInputMenuSelectWidget}
5351 OO
.ui
.LookupElement
.prototype.getLookupMenu = function () {
5352 return this.lookupMenu
;
5356 * Disable or re-enable lookups.
5358 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5360 * @param {boolean} disabled Disable lookups
5362 OO
.ui
.LookupElement
.prototype.setLookupsDisabled = function ( disabled
) {
5363 this.lookupsDisabled
= !!disabled
;
5367 * Open the menu. If there are no entries in the menu, this does nothing.
5371 OO
.ui
.LookupElement
.prototype.openLookupMenu = function () {
5372 if ( !this.lookupMenu
.isEmpty() ) {
5373 this.lookupMenu
.toggle( true );
5379 * Close the menu, empty it, and abort any pending request.
5383 OO
.ui
.LookupElement
.prototype.closeLookupMenu = function () {
5384 this.lookupMenu
.toggle( false );
5385 this.abortLookupRequest();
5386 this.lookupMenu
.clearItems();
5391 * Request menu items based on the input's current value, and when they arrive,
5392 * populate the menu with these items and show the menu.
5394 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5398 OO
.ui
.LookupElement
.prototype.populateLookupMenu = function () {
5400 value
= this.getValue();
5402 if ( this.lookupsDisabled
) {
5406 // If the input is empty, clear the menu
5407 if ( value
=== '' ) {
5408 this.closeLookupMenu();
5409 // Skip population if there is already a request pending for the current value
5410 } else if ( value
!== this.lookupQuery
) {
5411 this.getLookupMenuItems()
5412 .done( function ( items
) {
5413 widget
.lookupMenu
.clearItems();
5414 if ( items
.length
) {
5418 widget
.initializeLookupMenuSelection();
5420 widget
.lookupMenu
.toggle( false );
5423 .fail( function () {
5424 widget
.lookupMenu
.clearItems();
5432 * Select and highlight the first selectable item in the menu.
5436 OO
.ui
.LookupElement
.prototype.initializeLookupMenuSelection = function () {
5437 if ( !this.lookupMenu
.getSelectedItem() ) {
5438 this.lookupMenu
.selectItem( this.lookupMenu
.getFirstSelectableItem() );
5440 this.lookupMenu
.highlightItem( this.lookupMenu
.getSelectedItem() );
5444 * Get lookup menu items for the current query.
5446 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5447 * the done event. If the request was aborted to make way for a subsequent request, this promise
5448 * will not be rejected: it will remain pending forever.
5450 OO
.ui
.LookupElement
.prototype.getLookupMenuItems = function () {
5452 value
= this.getValue(),
5453 deferred
= $.Deferred(),
5456 this.abortLookupRequest();
5457 if ( Object
.prototype.hasOwnProperty
.call( this.lookupCache
, value
) ) {
5458 deferred
.resolve( this.getLookupMenuOptionsFromData( this.lookupCache
[ value
] ) );
5461 this.lookupQuery
= value
;
5462 ourRequest
= this.lookupRequest
= this.getLookupRequest();
5464 .always( function () {
5465 // We need to pop pending even if this is an old request, otherwise
5466 // the widget will remain pending forever.
5467 // TODO: this assumes that an aborted request will fail or succeed soon after
5468 // being aborted, or at least eventually. It would be nice if we could popPending()
5469 // at abort time, but only if we knew that we hadn't already called popPending()
5470 // for that request.
5471 widget
.popPending();
5473 .done( function ( data
) {
5474 // If this is an old request (and aborting it somehow caused it to still succeed),
5475 // ignore its success completely
5476 if ( ourRequest
=== widget
.lookupRequest
) {
5477 widget
.lookupQuery
= null;
5478 widget
.lookupRequest
= null;
5479 widget
.lookupCache
[ value
] = widget
.getLookupCacheDataFromResponse( data
);
5480 deferred
.resolve( widget
.getLookupMenuOptionsFromData( widget
.lookupCache
[ value
] ) );
5483 .fail( function () {
5484 // If this is an old request (or a request failing because it's being aborted),
5485 // ignore its failure completely
5486 if ( ourRequest
=== widget
.lookupRequest
) {
5487 widget
.lookupQuery
= null;
5488 widget
.lookupRequest
= null;
5493 return deferred
.promise();
5497 * Abort the currently pending lookup request, if any.
5499 OO
.ui
.LookupElement
.prototype.abortLookupRequest = function () {
5500 var oldRequest
= this.lookupRequest
;
5502 // First unset this.lookupRequest to the fail handler will notice
5503 // that the request is no longer current
5504 this.lookupRequest
= null;
5505 this.lookupQuery
= null;
5511 * Get a new request object of the current lookup query value.
5514 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5516 OO
.ui
.LookupElement
.prototype.getLookupRequest = function () {
5517 // Stub, implemented in subclass
5522 * Pre-process data returned by the request from #getLookupRequest.
5524 * The return value of this function will be cached, and any further queries for the given value
5525 * will use the cache rather than doing API requests.
5528 * @param {Mixed} data Response from server
5529 * @return {Mixed} Cached result data
5531 OO
.ui
.LookupElement
.prototype.getLookupCacheDataFromResponse = function () {
5532 // Stub, implemented in subclass
5537 * Get a list of menu option widgets from the (possibly cached) data returned by
5538 * #getLookupCacheDataFromResponse.
5541 * @param {Mixed} data Cached result data, usually an array
5542 * @return {OO.ui.MenuOptionWidget[]} Menu items
5544 OO
.ui
.LookupElement
.prototype.getLookupMenuOptionsFromData = function () {
5545 // Stub, implemented in subclass
5550 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5551 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5552 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5553 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5559 * @param {Object} [config] Configuration options
5560 * @cfg {Object} [popup] Configuration to pass to popup
5561 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5563 OO
.ui
.PopupElement
= function OoUiPopupElement( config
) {
5564 // Configuration initialization
5565 config
= config
|| {};
5568 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5569 { autoClose
: true },
5571 { $autoCloseIgnore
: this.$element
}
5580 * @return {OO.ui.PopupWidget} Popup widget
5582 OO
.ui
.PopupElement
.prototype.getPopup = function () {
5587 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
5588 * additional functionality to an element created by another class. The class provides
5589 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
5590 * which are used to customize the look and feel of a widget to better describe its
5591 * importance and functionality.
5593 * The library currently contains the following styling flags for general use:
5595 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
5596 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
5597 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
5599 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
5600 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
5602 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5608 * @param {Object} [config] Configuration options
5609 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
5610 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
5611 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5612 * @cfg {jQuery} [$flagged] Flagged node, assigned to $flagged, omit to use $element
5614 OO
.ui
.FlaggedElement
= function OoUiFlaggedElement( config
) {
5615 // Configuration initialization
5616 config
= config
|| {};
5620 this.$flagged
= null;
5623 this.setFlags( config
.flags
);
5624 this.setFlaggedElement( config
.$flagged
|| this.$element
);
5631 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
5632 * parameter contains the name of each modified flag and indicates whether it was
5635 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
5636 * that the flag was added, `false` that the flag was removed.
5642 * Set the flagged element.
5644 * If an element is already set, it will be cleaned up before setting up the new element.
5646 * @param {jQuery} $flagged Element to add flags to
5648 OO
.ui
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
5649 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
5650 return 'oo-ui-flaggedElement-' + flag
;
5653 if ( this.$flagged
) {
5654 this.$flagged
.removeClass( classNames
);
5657 this.$flagged
= $flagged
.addClass( classNames
);
5661 * Check if a flag is set.
5663 * @param {string} flag Name of flag
5664 * @return {boolean} Has flag
5666 OO
.ui
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
5667 return flag
in this.flags
;
5671 * Get the names of all flags set.
5673 * @return {string[]} Flag names
5675 OO
.ui
.FlaggedElement
.prototype.getFlags = function () {
5676 return Object
.keys( this.flags
);
5685 OO
.ui
.FlaggedElement
.prototype.clearFlags = function () {
5686 var flag
, className
,
5689 classPrefix
= 'oo-ui-flaggedElement-';
5691 for ( flag
in this.flags
) {
5692 className
= classPrefix
+ flag
;
5693 changes
[ flag
] = false;
5694 delete this.flags
[ flag
];
5695 remove
.push( className
);
5698 if ( this.$flagged
) {
5699 this.$flagged
.removeClass( remove
.join( ' ' ) );
5702 this.updateThemeClasses();
5703 this.emit( 'flag', changes
);
5709 * Add one or more flags.
5711 * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
5712 * keyed by flag name containing boolean set/remove instructions.
5716 OO
.ui
.FlaggedElement
.prototype.setFlags = function ( flags
) {
5717 var i
, len
, flag
, className
,
5721 classPrefix
= 'oo-ui-flaggedElement-';
5723 if ( typeof flags
=== 'string' ) {
5724 className
= classPrefix
+ flags
;
5726 if ( !this.flags
[ flags
] ) {
5727 this.flags
[ flags
] = true;
5728 add
.push( className
);
5730 } else if ( Array
.isArray( flags
) ) {
5731 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
5733 className
= classPrefix
+ flag
;
5735 if ( !this.flags
[ flag
] ) {
5736 changes
[ flag
] = true;
5737 this.flags
[ flag
] = true;
5738 add
.push( className
);
5741 } else if ( OO
.isPlainObject( flags
) ) {
5742 for ( flag
in flags
) {
5743 className
= classPrefix
+ flag
;
5744 if ( flags
[ flag
] ) {
5746 if ( !this.flags
[ flag
] ) {
5747 changes
[ flag
] = true;
5748 this.flags
[ flag
] = true;
5749 add
.push( className
);
5753 if ( this.flags
[ flag
] ) {
5754 changes
[ flag
] = false;
5755 delete this.flags
[ flag
];
5756 remove
.push( className
);
5762 if ( this.$flagged
) {
5764 .addClass( add
.join( ' ' ) )
5765 .removeClass( remove
.join( ' ' ) );
5768 this.updateThemeClasses();
5769 this.emit( 'flag', changes
);
5775 * TitledElement is mixed into other classes to provide a `title` attribute.
5776 * Titles are rendered by the browser and are made visible when the user moves
5777 * the mouse over the element. Titles are not visible on touch devices.
5780 * // TitledElement provides a 'title' attribute to the
5781 * // ButtonWidget class
5782 * var button = new OO.ui.ButtonWidget( {
5783 * label : 'Button with Title',
5784 * title : 'I am a button'
5786 * $( 'body' ).append( button.$element );
5792 * @param {Object} [config] Configuration options
5793 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
5794 * If this config is omitted, the title functionality is applied to $element, the
5795 * element created by the class.
5796 * @cfg {string|Function} [title] The title text or a function that returns text. If
5797 * this config is omitted, the value of the static `title` property is used.
5799 OO
.ui
.TitledElement
= function OoUiTitledElement( config
) {
5800 // Configuration initialization
5801 config
= config
|| {};
5804 this.$titled
= null;
5808 this.setTitle( config
.title
|| this.constructor.static.title
);
5809 this.setTitledElement( config
.$titled
|| this.$element
);
5814 OO
.initClass( OO
.ui
.TitledElement
);
5816 /* Static Properties */
5819 * The title text, a function that returns text, or `null` for no title. The value of the static property
5820 * is overridden if the #title config option is used.
5824 * @property {string|Function|null}
5826 OO
.ui
.TitledElement
.static.title
= null;
5831 * Set the titled element.
5833 * If an element is already set, it will be cleaned up before setting up the new element.
5835 * @param {jQuery} $titled Element to set title on
5837 OO
.ui
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
5838 if ( this.$titled
) {
5839 this.$titled
.removeAttr( 'title' );
5842 this.$titled
= $titled
;
5844 this.$titled
.attr( 'title', this.title
);
5851 * @param {string|Function|null} title Title text, a function that returns text or null for no title
5854 OO
.ui
.TitledElement
.prototype.setTitle = function ( title
) {
5855 title
= typeof title
=== 'string' ? OO
.ui
.resolveMsg( title
) : null;
5857 if ( this.title
!== title
) {
5858 if ( this.$titled
) {
5859 if ( title
!== null ) {
5860 this.$titled
.attr( 'title', title
);
5862 this.$titled
.removeAttr( 'title' );
5874 * @return {string} Title string
5876 OO
.ui
.TitledElement
.prototype.getTitle = function () {
5881 * Element that can be automatically clipped to visible boundaries.
5883 * Whenever the element's natural height changes, you have to call
5884 * #clip to make sure it's still clipping correctly.
5890 * @param {Object} [config] Configuration options
5891 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
5893 OO
.ui
.ClippableElement
= function OoUiClippableElement( config
) {
5894 // Configuration initialization
5895 config
= config
|| {};
5898 this.$clippable
= null;
5899 this.clipping
= false;
5900 this.clippedHorizontally
= false;
5901 this.clippedVertically
= false;
5902 this.$clippableContainer
= null;
5903 this.$clippableScroller
= null;
5904 this.$clippableWindow
= null;
5905 this.idealWidth
= null;
5906 this.idealHeight
= null;
5907 this.onClippableContainerScrollHandler
= this.clip
.bind( this );
5908 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
5911 this.setClippableElement( config
.$clippable
|| this.$element
);
5917 * Set clippable element.
5919 * If an element is already set, it will be cleaned up before setting up the new element.
5921 * @param {jQuery} $clippable Element to make clippable
5923 OO
.ui
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
5924 if ( this.$clippable
) {
5925 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
5926 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
5927 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5930 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
5937 * Do not turn clipping on until after the element is attached to the DOM and visible.
5939 * @param {boolean} [clipping] Enable clipping, omit to toggle
5942 OO
.ui
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
5943 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
5945 if ( this.clipping
!== clipping
) {
5946 this.clipping
= clipping
;
5948 this.$clippableContainer
= $( this.getClosestScrollableElementContainer() );
5949 // If the clippable container is the root, we have to listen to scroll events and check
5950 // jQuery.scrollTop on the window because of browser inconsistencies
5951 this.$clippableScroller
= this.$clippableContainer
.is( 'html, body' ) ?
5952 $( OO
.ui
.Element
.static.getWindow( this.$clippableContainer
) ) :
5953 this.$clippableContainer
;
5954 this.$clippableScroller
.on( 'scroll', this.onClippableContainerScrollHandler
);
5955 this.$clippableWindow
= $( this.getElementWindow() )
5956 .on( 'resize', this.onClippableWindowResizeHandler
);
5957 // Initial clip after visible
5960 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
5961 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5963 this.$clippableContainer
= null;
5964 this.$clippableScroller
.off( 'scroll', this.onClippableContainerScrollHandler
);
5965 this.$clippableScroller
= null;
5966 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
5967 this.$clippableWindow
= null;
5975 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5977 * @return {boolean} Element will be clipped to the visible area
5979 OO
.ui
.ClippableElement
.prototype.isClipping = function () {
5980 return this.clipping
;
5984 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5986 * @return {boolean} Part of the element is being clipped
5988 OO
.ui
.ClippableElement
.prototype.isClipped = function () {
5989 return this.clippedHorizontally
|| this.clippedVertically
;
5993 * Check if the right of the element is being clipped by the nearest scrollable container.
5995 * @return {boolean} Part of the element is being clipped
5997 OO
.ui
.ClippableElement
.prototype.isClippedHorizontally = function () {
5998 return this.clippedHorizontally
;
6002 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6004 * @return {boolean} Part of the element is being clipped
6006 OO
.ui
.ClippableElement
.prototype.isClippedVertically = function () {
6007 return this.clippedVertically
;
6011 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6013 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6014 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6016 OO
.ui
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
6017 this.idealWidth
= width
;
6018 this.idealHeight
= height
;
6020 if ( !this.clipping
) {
6021 // Update dimensions
6022 this.$clippable
.css( { width
: width
, height
: height
} );
6024 // While clipping, idealWidth and idealHeight are not considered
6028 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6029 * the element's natural height changes.
6031 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6032 * overlapped by, the visible area of the nearest scrollable container.
6036 OO
.ui
.ClippableElement
.prototype.clip = function () {
6037 if ( !this.clipping
) {
6038 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
6042 var buffer
= 7, // Chosen by fair dice roll
6043 cOffset
= this.$clippable
.offset(),
6044 $container
= this.$clippableContainer
.is( 'html, body' ) ?
6045 this.$clippableWindow
: this.$clippableContainer
,
6046 ccOffset
= $container
.offset() || { top
: 0, left
: 0 },
6047 ccHeight
= $container
.innerHeight() - buffer
,
6048 ccWidth
= $container
.innerWidth() - buffer
,
6049 cHeight
= this.$clippable
.outerHeight() + buffer
,
6050 cWidth
= this.$clippable
.outerWidth() + buffer
,
6051 scrollTop
= this.$clippableScroller
.scrollTop(),
6052 scrollLeft
= this.$clippableScroller
.scrollLeft(),
6053 desiredWidth
= cOffset
.left
< 0 ?
6054 cWidth
+ cOffset
.left
:
6055 ( ccOffset
.left
+ scrollLeft
+ ccWidth
) - cOffset
.left
,
6056 desiredHeight
= cOffset
.top
< 0 ?
6057 cHeight
+ cOffset
.top
:
6058 ( ccOffset
.top
+ scrollTop
+ ccHeight
) - cOffset
.top
,
6059 naturalWidth
= this.$clippable
.prop( 'scrollWidth' ),
6060 naturalHeight
= this.$clippable
.prop( 'scrollHeight' ),
6061 clipWidth
= desiredWidth
< naturalWidth
,
6062 clipHeight
= desiredHeight
< naturalHeight
;
6065 this.$clippable
.css( { overflowX
: 'scroll', width
: desiredWidth
} );
6067 this.$clippable
.css( { width
: this.idealWidth
|| '', overflowX
: '' } );
6070 this.$clippable
.css( { overflowY
: 'scroll', height
: desiredHeight
} );
6072 this.$clippable
.css( { height
: this.idealHeight
|| '', overflowY
: '' } );
6075 // If we stopped clipping in at least one of the dimensions
6076 if ( !clipWidth
|| !clipHeight
) {
6077 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
6080 this.clippedHorizontally
= clipWidth
;
6081 this.clippedVertically
= clipHeight
;
6087 * Generic toolbar tool.
6091 * @extends OO.ui.Widget
6092 * @mixins OO.ui.IconElement
6093 * @mixins OO.ui.FlaggedElement
6096 * @param {OO.ui.ToolGroup} toolGroup
6097 * @param {Object} [config] Configuration options
6098 * @cfg {string|Function} [title] Title text or a function that returns text
6100 OO
.ui
.Tool
= function OoUiTool( toolGroup
, config
) {
6101 // Allow passing positional parameters inside the config object
6102 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
6104 toolGroup
= config
.toolGroup
;
6107 // Configuration initialization
6108 config
= config
|| {};
6110 // Parent constructor
6111 OO
.ui
.Tool
.super.call( this, config
);
6113 // Mixin constructors
6114 OO
.ui
.IconElement
.call( this, config
);
6115 OO
.ui
.FlaggedElement
.call( this, config
);
6118 this.toolGroup
= toolGroup
;
6119 this.toolbar
= this.toolGroup
.getToolbar();
6120 this.active
= false;
6121 this.$title
= $( '<span>' );
6122 this.$accel
= $( '<span>' );
6123 this.$link
= $( '<a>' );
6127 this.toolbar
.connect( this, { updateState
: 'onUpdateState' } );
6130 this.$title
.addClass( 'oo-ui-tool-title' );
6132 .addClass( 'oo-ui-tool-accel' )
6134 // This may need to be changed if the key names are ever localized,
6135 // but for now they are essentially written in English
6140 .addClass( 'oo-ui-tool-link' )
6141 .append( this.$icon
, this.$title
, this.$accel
)
6142 .prop( 'tabIndex', 0 )
6143 .attr( 'role', 'button' );
6145 .data( 'oo-ui-tool', this )
6147 'oo-ui-tool ' + 'oo-ui-tool-name-' +
6148 this.constructor.static.name
.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
6150 .append( this.$link
);
6151 this.setTitle( config
.title
|| this.constructor.static.title
);
6156 OO
.inheritClass( OO
.ui
.Tool
, OO
.ui
.Widget
);
6157 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.IconElement
);
6158 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.FlaggedElement
);
6166 /* Static Properties */
6172 OO
.ui
.Tool
.static.tagName
= 'span';
6175 * Symbolic name of tool.
6180 * @property {string}
6182 OO
.ui
.Tool
.static.name
= '';
6190 * @property {string}
6192 OO
.ui
.Tool
.static.group
= '';
6197 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
6198 * is part of a list or menu tool group. If a trigger is associated with an action by the same name
6199 * as the tool, a description of its keyboard shortcut for the appropriate platform will be
6200 * appended to the title if the tool is part of a bar tool group.
6205 * @property {string|Function} Title text or a function that returns text
6207 OO
.ui
.Tool
.static.title
= '';
6210 * Tool can be automatically added to catch-all groups.
6214 * @property {boolean}
6216 OO
.ui
.Tool
.static.autoAddToCatchall
= true;
6219 * Tool can be automatically added to named groups.
6222 * @property {boolean}
6225 OO
.ui
.Tool
.static.autoAddToGroup
= true;
6228 * Check if this tool is compatible with given data.
6232 * @param {Mixed} data Data to check
6233 * @return {boolean} Tool can be used with data
6235 OO
.ui
.Tool
.static.isCompatibleWith = function () {
6242 * Handle the toolbar state being updated.
6244 * This is an abstract method that must be overridden in a concrete subclass.
6248 OO
.ui
.Tool
.prototype.onUpdateState = function () {
6250 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
6255 * Handle the tool being selected.
6257 * This is an abstract method that must be overridden in a concrete subclass.
6261 OO
.ui
.Tool
.prototype.onSelect = function () {
6263 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
6268 * Check if the button is active.
6270 * @return {boolean} Button is active
6272 OO
.ui
.Tool
.prototype.isActive = function () {
6277 * Make the button appear active or inactive.
6279 * @param {boolean} state Make button appear active
6281 OO
.ui
.Tool
.prototype.setActive = function ( state
) {
6282 this.active
= !!state
;
6283 if ( this.active
) {
6284 this.$element
.addClass( 'oo-ui-tool-active' );
6286 this.$element
.removeClass( 'oo-ui-tool-active' );
6291 * Get the tool title.
6293 * @param {string|Function} title Title text or a function that returns text
6296 OO
.ui
.Tool
.prototype.setTitle = function ( title
) {
6297 this.title
= OO
.ui
.resolveMsg( title
);
6303 * Get the tool title.
6305 * @return {string} Title text
6307 OO
.ui
.Tool
.prototype.getTitle = function () {
6312 * Get the tool's symbolic name.
6314 * @return {string} Symbolic name of tool
6316 OO
.ui
.Tool
.prototype.getName = function () {
6317 return this.constructor.static.name
;
6323 OO
.ui
.Tool
.prototype.updateTitle = function () {
6324 var titleTooltips
= this.toolGroup
.constructor.static.titleTooltips
,
6325 accelTooltips
= this.toolGroup
.constructor.static.accelTooltips
,
6326 accel
= this.toolbar
.getToolAccelerator( this.constructor.static.name
),
6329 this.$title
.text( this.title
);
6330 this.$accel
.text( accel
);
6332 if ( titleTooltips
&& typeof this.title
=== 'string' && this.title
.length
) {
6333 tooltipParts
.push( this.title
);
6335 if ( accelTooltips
&& typeof accel
=== 'string' && accel
.length
) {
6336 tooltipParts
.push( accel
);
6338 if ( tooltipParts
.length
) {
6339 this.$link
.attr( 'title', tooltipParts
.join( ' ' ) );
6341 this.$link
.removeAttr( 'title' );
6348 OO
.ui
.Tool
.prototype.destroy = function () {
6349 this.toolbar
.disconnect( this );
6350 this.$element
.remove();
6354 * Collection of tool groups.
6357 * @extends OO.ui.Element
6358 * @mixins OO.EventEmitter
6359 * @mixins OO.ui.GroupElement
6362 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
6363 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
6364 * @param {Object} [config] Configuration options
6365 * @cfg {boolean} [actions] Add an actions section opposite to the tools
6366 * @cfg {boolean} [shadow] Add a shadow below the toolbar
6368 OO
.ui
.Toolbar
= function OoUiToolbar( toolFactory
, toolGroupFactory
, config
) {
6369 // Allow passing positional parameters inside the config object
6370 if ( OO
.isPlainObject( toolFactory
) && config
=== undefined ) {
6371 config
= toolFactory
;
6372 toolFactory
= config
.toolFactory
;
6373 toolGroupFactory
= config
.toolGroupFactory
;
6376 // Configuration initialization
6377 config
= config
|| {};
6379 // Parent constructor
6380 OO
.ui
.Toolbar
.super.call( this, config
);
6382 // Mixin constructors
6383 OO
.EventEmitter
.call( this );
6384 OO
.ui
.GroupElement
.call( this, config
);
6387 this.toolFactory
= toolFactory
;
6388 this.toolGroupFactory
= toolGroupFactory
;
6391 this.$bar
= $( '<div>' );
6392 this.$actions
= $( '<div>' );
6393 this.initialized
= false;
6397 .add( this.$bar
).add( this.$group
).add( this.$actions
)
6398 .on( 'mousedown touchstart', this.onPointerDown
.bind( this ) );
6401 this.$group
.addClass( 'oo-ui-toolbar-tools' );
6402 if ( config
.actions
) {
6403 this.$bar
.append( this.$actions
.addClass( 'oo-ui-toolbar-actions' ) );
6406 .addClass( 'oo-ui-toolbar-bar' )
6407 .append( this.$group
, '<div style="clear:both"></div>' );
6408 if ( config
.shadow
) {
6409 this.$bar
.append( '<div class="oo-ui-toolbar-shadow"></div>' );
6411 this.$element
.addClass( 'oo-ui-toolbar' ).append( this.$bar
);
6416 OO
.inheritClass( OO
.ui
.Toolbar
, OO
.ui
.Element
);
6417 OO
.mixinClass( OO
.ui
.Toolbar
, OO
.EventEmitter
);
6418 OO
.mixinClass( OO
.ui
.Toolbar
, OO
.ui
.GroupElement
);
6423 * Get the tool factory.
6425 * @return {OO.ui.ToolFactory} Tool factory
6427 OO
.ui
.Toolbar
.prototype.getToolFactory = function () {
6428 return this.toolFactory
;
6432 * Get the tool group factory.
6434 * @return {OO.Factory} Tool group factory
6436 OO
.ui
.Toolbar
.prototype.getToolGroupFactory = function () {
6437 return this.toolGroupFactory
;
6441 * Handles mouse down events.
6443 * @param {jQuery.Event} e Mouse down event
6445 OO
.ui
.Toolbar
.prototype.onPointerDown = function ( e
) {
6446 var $closestWidgetToEvent
= $( e
.target
).closest( '.oo-ui-widget' ),
6447 $closestWidgetToToolbar
= this.$element
.closest( '.oo-ui-widget' );
6448 if ( !$closestWidgetToEvent
.length
|| $closestWidgetToEvent
[ 0 ] === $closestWidgetToToolbar
[ 0 ] ) {
6454 * Sets up handles and preloads required information for the toolbar to work.
6455 * This must be called after it is attached to a visible document and before doing anything else.
6457 OO
.ui
.Toolbar
.prototype.initialize = function () {
6458 this.initialized
= true;
6464 * Tools can be specified in the following ways:
6466 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6467 * - All tools in a group: `{ group: 'group-name' }`
6468 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
6470 * @param {Object.<string,Array>} groups List of tool group configurations
6471 * @param {Array|string} [groups.include] Tools to include
6472 * @param {Array|string} [groups.exclude] Tools to exclude
6473 * @param {Array|string} [groups.promote] Tools to promote to the beginning
6474 * @param {Array|string} [groups.demote] Tools to demote to the end
6476 OO
.ui
.Toolbar
.prototype.setup = function ( groups
) {
6477 var i
, len
, type
, group
,
6479 defaultType
= 'bar';
6481 // Cleanup previous groups
6484 // Build out new groups
6485 for ( i
= 0, len
= groups
.length
; i
< len
; i
++ ) {
6486 group
= groups
[ i
];
6487 if ( group
.include
=== '*' ) {
6488 // Apply defaults to catch-all groups
6489 if ( group
.type
=== undefined ) {
6490 group
.type
= 'list';
6492 if ( group
.label
=== undefined ) {
6493 group
.label
= OO
.ui
.msg( 'ooui-toolbar-more' );
6496 // Check type has been registered
6497 type
= this.getToolGroupFactory().lookup( group
.type
) ? group
.type
: defaultType
;
6499 this.getToolGroupFactory().create( type
, this, group
)
6502 this.addItems( items
);
6506 * Remove all tools and groups from the toolbar.
6508 OO
.ui
.Toolbar
.prototype.reset = function () {
6513 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6514 this.items
[ i
].destroy();
6520 * Destroys toolbar, removing event handlers and DOM elements.
6522 * Call this whenever you are done using a toolbar.
6524 OO
.ui
.Toolbar
.prototype.destroy = function () {
6526 this.$element
.remove();
6530 * Check if tool has not been used yet.
6532 * @param {string} name Symbolic name of tool
6533 * @return {boolean} Tool is available
6535 OO
.ui
.Toolbar
.prototype.isToolAvailable = function ( name
) {
6536 return !this.tools
[ name
];
6540 * Prevent tool from being used again.
6542 * @param {OO.ui.Tool} tool Tool to reserve
6544 OO
.ui
.Toolbar
.prototype.reserveTool = function ( tool
) {
6545 this.tools
[ tool
.getName() ] = tool
;
6549 * Allow tool to be used again.
6551 * @param {OO.ui.Tool} tool Tool to release
6553 OO
.ui
.Toolbar
.prototype.releaseTool = function ( tool
) {
6554 delete this.tools
[ tool
.getName() ];
6558 * Get accelerator label for tool.
6560 * This is a stub that should be overridden to provide access to accelerator information.
6562 * @param {string} name Symbolic name of tool
6563 * @return {string|undefined} Tool accelerator label if available
6565 OO
.ui
.Toolbar
.prototype.getToolAccelerator = function () {
6570 * Collection of tools.
6572 * Tools can be specified in the following ways:
6574 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6575 * - All tools in a group: `{ group: 'group-name' }`
6576 * - All tools: `'*'`
6580 * @extends OO.ui.Widget
6581 * @mixins OO.ui.GroupElement
6584 * @param {OO.ui.Toolbar} toolbar
6585 * @param {Object} [config] Configuration options
6586 * @cfg {Array|string} [include=[]] List of tools to include
6587 * @cfg {Array|string} [exclude=[]] List of tools to exclude
6588 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
6589 * @cfg {Array|string} [demote=[]] List of tools to demote to the end
6591 OO
.ui
.ToolGroup
= function OoUiToolGroup( toolbar
, config
) {
6592 // Allow passing positional parameters inside the config object
6593 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
6595 toolbar
= config
.toolbar
;
6598 // Configuration initialization
6599 config
= config
|| {};
6601 // Parent constructor
6602 OO
.ui
.ToolGroup
.super.call( this, config
);
6604 // Mixin constructors
6605 OO
.ui
.GroupElement
.call( this, config
);
6608 this.toolbar
= toolbar
;
6610 this.pressed
= null;
6611 this.autoDisabled
= false;
6612 this.include
= config
.include
|| [];
6613 this.exclude
= config
.exclude
|| [];
6614 this.promote
= config
.promote
|| [];
6615 this.demote
= config
.demote
|| [];
6616 this.onCapturedMouseUpHandler
= this.onCapturedMouseUp
.bind( this );
6620 'mousedown touchstart': this.onPointerDown
.bind( this ),
6621 'mouseup touchend': this.onPointerUp
.bind( this ),
6622 mouseover
: this.onMouseOver
.bind( this ),
6623 mouseout
: this.onMouseOut
.bind( this )
6625 this.toolbar
.getToolFactory().connect( this, { register
: 'onToolFactoryRegister' } );
6626 this.aggregate( { disable
: 'itemDisable' } );
6627 this.connect( this, { itemDisable
: 'updateDisabled' } );
6630 this.$group
.addClass( 'oo-ui-toolGroup-tools' );
6632 .addClass( 'oo-ui-toolGroup' )
6633 .append( this.$group
);
6639 OO
.inheritClass( OO
.ui
.ToolGroup
, OO
.ui
.Widget
);
6640 OO
.mixinClass( OO
.ui
.ToolGroup
, OO
.ui
.GroupElement
);
6648 /* Static Properties */
6651 * Show labels in tooltips.
6655 * @property {boolean}
6657 OO
.ui
.ToolGroup
.static.titleTooltips
= false;
6660 * Show acceleration labels in tooltips.
6664 * @property {boolean}
6666 OO
.ui
.ToolGroup
.static.accelTooltips
= false;
6669 * Automatically disable the toolgroup when all tools are disabled
6673 * @property {boolean}
6675 OO
.ui
.ToolGroup
.static.autoDisable
= true;
6682 OO
.ui
.ToolGroup
.prototype.isDisabled = function () {
6683 return this.autoDisabled
|| OO
.ui
.ToolGroup
.super.prototype.isDisabled
.apply( this, arguments
);
6689 OO
.ui
.ToolGroup
.prototype.updateDisabled = function () {
6690 var i
, item
, allDisabled
= true;
6692 if ( this.constructor.static.autoDisable
) {
6693 for ( i
= this.items
.length
- 1; i
>= 0; i
-- ) {
6694 item
= this.items
[ i
];
6695 if ( !item
.isDisabled() ) {
6696 allDisabled
= false;
6700 this.autoDisabled
= allDisabled
;
6702 OO
.ui
.ToolGroup
.super.prototype.updateDisabled
.apply( this, arguments
);
6706 * Handle mouse down events.
6708 * @param {jQuery.Event} e Mouse down event
6710 OO
.ui
.ToolGroup
.prototype.onPointerDown = function ( e
) {
6711 // e.which is 0 for touch events, 1 for left mouse button
6712 if ( !this.isDisabled() && e
.which
<= 1 ) {
6713 this.pressed
= this.getTargetTool( e
);
6714 if ( this.pressed
) {
6715 this.pressed
.setActive( true );
6716 this.getElementDocument().addEventListener(
6717 'mouseup', this.onCapturedMouseUpHandler
, true
6725 * Handle captured mouse up events.
6727 * @param {Event} e Mouse up event
6729 OO
.ui
.ToolGroup
.prototype.onCapturedMouseUp = function ( e
) {
6730 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler
, true );
6731 // onPointerUp may be called a second time, depending on where the mouse is when the button is
6732 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
6733 this.onPointerUp( e
);
6737 * Handle mouse up events.
6739 * @param {jQuery.Event} e Mouse up event
6741 OO
.ui
.ToolGroup
.prototype.onPointerUp = function ( e
) {
6742 var tool
= this.getTargetTool( e
);
6744 // e.which is 0 for touch events, 1 for left mouse button
6745 if ( !this.isDisabled() && e
.which
<= 1 && this.pressed
&& this.pressed
=== tool
) {
6746 this.pressed
.onSelect();
6749 this.pressed
= null;
6754 * Handle mouse over events.
6756 * @param {jQuery.Event} e Mouse over event
6758 OO
.ui
.ToolGroup
.prototype.onMouseOver = function ( e
) {
6759 var tool
= this.getTargetTool( e
);
6761 if ( this.pressed
&& this.pressed
=== tool
) {
6762 this.pressed
.setActive( true );
6767 * Handle mouse out events.
6769 * @param {jQuery.Event} e Mouse out event
6771 OO
.ui
.ToolGroup
.prototype.onMouseOut = function ( e
) {
6772 var tool
= this.getTargetTool( e
);
6774 if ( this.pressed
&& this.pressed
=== tool
) {
6775 this.pressed
.setActive( false );
6780 * Get the closest tool to a jQuery.Event.
6782 * Only tool links are considered, which prevents other elements in the tool such as popups from
6783 * triggering tool group interactions.
6786 * @param {jQuery.Event} e
6787 * @return {OO.ui.Tool|null} Tool, `null` if none was found
6789 OO
.ui
.ToolGroup
.prototype.getTargetTool = function ( e
) {
6791 $item
= $( e
.target
).closest( '.oo-ui-tool-link' );
6793 if ( $item
.length
) {
6794 tool
= $item
.parent().data( 'oo-ui-tool' );
6797 return tool
&& !tool
.isDisabled() ? tool
: null;
6801 * Handle tool registry register events.
6803 * If a tool is registered after the group is created, we must repopulate the list to account for:
6805 * - a tool being added that may be included
6806 * - a tool already included being overridden
6808 * @param {string} name Symbolic name of tool
6810 OO
.ui
.ToolGroup
.prototype.onToolFactoryRegister = function () {
6815 * Get the toolbar this group is in.
6817 * @return {OO.ui.Toolbar} Toolbar of group
6819 OO
.ui
.ToolGroup
.prototype.getToolbar = function () {
6820 return this.toolbar
;
6824 * Add and remove tools based on configuration.
6826 OO
.ui
.ToolGroup
.prototype.populate = function () {
6827 var i
, len
, name
, tool
,
6828 toolFactory
= this.toolbar
.getToolFactory(),
6832 list
= this.toolbar
.getToolFactory().getTools(
6833 this.include
, this.exclude
, this.promote
, this.demote
6836 // Build a list of needed tools
6837 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
6841 toolFactory
.lookup( name
) &&
6842 // Tool is available or is already in this group
6843 ( this.toolbar
.isToolAvailable( name
) || this.tools
[ name
] )
6845 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
6846 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
6847 this.toolbar
.tools
[ name
] = true;
6848 tool
= this.tools
[ name
];
6850 // Auto-initialize tools on first use
6851 this.tools
[ name
] = tool
= toolFactory
.create( name
, this );
6854 this.toolbar
.reserveTool( tool
);
6856 names
[ name
] = true;
6859 // Remove tools that are no longer needed
6860 for ( name
in this.tools
) {
6861 if ( !names
[ name
] ) {
6862 this.tools
[ name
].destroy();
6863 this.toolbar
.releaseTool( this.tools
[ name
] );
6864 remove
.push( this.tools
[ name
] );
6865 delete this.tools
[ name
];
6868 if ( remove
.length
) {
6869 this.removeItems( remove
);
6871 // Update emptiness state
6873 this.$element
.removeClass( 'oo-ui-toolGroup-empty' );
6875 this.$element
.addClass( 'oo-ui-toolGroup-empty' );
6877 // Re-add tools (moving existing ones to new locations)
6878 this.addItems( add
);
6879 // Disabled state may depend on items
6880 this.updateDisabled();
6884 * Destroy tool group.
6886 OO
.ui
.ToolGroup
.prototype.destroy = function () {
6890 this.toolbar
.getToolFactory().disconnect( this );
6891 for ( name
in this.tools
) {
6892 this.toolbar
.releaseTool( this.tools
[ name
] );
6893 this.tools
[ name
].disconnect( this ).destroy();
6894 delete this.tools
[ name
];
6896 this.$element
.remove();
6900 * Dialog for showing a message.
6903 * - Registers two actions by default (safe and primary).
6904 * - Renders action widgets in the footer.
6907 * @extends OO.ui.Dialog
6910 * @param {Object} [config] Configuration options
6912 OO
.ui
.MessageDialog
= function OoUiMessageDialog( config
) {
6913 // Parent constructor
6914 OO
.ui
.MessageDialog
.super.call( this, config
);
6917 this.verticalActionLayout
= null;
6920 this.$element
.addClass( 'oo-ui-messageDialog' );
6925 OO
.inheritClass( OO
.ui
.MessageDialog
, OO
.ui
.Dialog
);
6927 /* Static Properties */
6929 OO
.ui
.MessageDialog
.static.name
= 'message';
6931 OO
.ui
.MessageDialog
.static.size
= 'small';
6933 OO
.ui
.MessageDialog
.static.verbose
= false;
6938 * A confirmation dialog's title should describe what the progressive action will do. An alert
6939 * dialog's title should describe what event occurred.
6943 * @property {jQuery|string|Function|null}
6945 OO
.ui
.MessageDialog
.static.title
= null;
6948 * A confirmation dialog's message should describe the consequences of the progressive action. An
6949 * alert dialog's message should describe why the event occurred.
6953 * @property {jQuery|string|Function|null}
6955 OO
.ui
.MessageDialog
.static.message
= null;
6957 OO
.ui
.MessageDialog
.static.actions
= [
6958 { action
: 'accept', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-accept' ), flags
: 'primary' },
6959 { action
: 'reject', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-reject' ), flags
: 'safe' }
6967 OO
.ui
.MessageDialog
.prototype.setManager = function ( manager
) {
6968 OO
.ui
.MessageDialog
.super.prototype.setManager
.call( this, manager
);
6971 this.manager
.connect( this, {
6981 OO
.ui
.MessageDialog
.prototype.onActionResize = function ( action
) {
6983 return OO
.ui
.MessageDialog
.super.prototype.onActionResize
.call( this, action
);
6987 * Handle window resized events.
6989 OO
.ui
.MessageDialog
.prototype.onResize = function () {
6991 dialog
.fitActions();
6992 // Wait for CSS transition to finish and do it again :(
6993 setTimeout( function () {
6994 dialog
.fitActions();
6999 * Toggle action layout between vertical and horizontal.
7001 * @param {boolean} [value] Layout actions vertically, omit to toggle
7004 OO
.ui
.MessageDialog
.prototype.toggleVerticalActionLayout = function ( value
) {
7005 value
= value
=== undefined ? !this.verticalActionLayout
: !!value
;
7007 if ( value
!== this.verticalActionLayout
) {
7008 this.verticalActionLayout
= value
;
7010 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value
)
7011 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value
);
7020 OO
.ui
.MessageDialog
.prototype.getActionProcess = function ( action
) {
7022 return new OO
.ui
.Process( function () {
7023 this.close( { action
: action
} );
7026 return OO
.ui
.MessageDialog
.super.prototype.getActionProcess
.call( this, action
);
7032 * @param {Object} [data] Dialog opening data
7033 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
7034 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
7035 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
7036 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
7039 OO
.ui
.MessageDialog
.prototype.getSetupProcess = function ( data
) {
7043 return OO
.ui
.MessageDialog
.super.prototype.getSetupProcess
.call( this, data
)
7044 .next( function () {
7045 this.title
.setLabel(
7046 data
.title
!== undefined ? data
.title
: this.constructor.static.title
7048 this.message
.setLabel(
7049 data
.message
!== undefined ? data
.message
: this.constructor.static.message
7051 this.message
.$element
.toggleClass(
7052 'oo-ui-messageDialog-message-verbose',
7053 data
.verbose
!== undefined ? data
.verbose
: this.constructor.static.verbose
7061 OO
.ui
.MessageDialog
.prototype.getBodyHeight = function () {
7062 var bodyHeight
, oldOverflow
,
7063 $scrollable
= this.container
.$element
;
7065 oldOverflow
= $scrollable
[ 0 ].style
.overflow
;
7066 $scrollable
[ 0 ].style
.overflow
= 'hidden';
7068 OO
.ui
.Element
.static.reconsiderScrollbars( $scrollable
[ 0 ] );
7070 bodyHeight
= this.text
.$element
.outerHeight( true );
7071 $scrollable
[ 0 ].style
.overflow
= oldOverflow
;
7079 OO
.ui
.MessageDialog
.prototype.setDimensions = function ( dim
) {
7080 var $scrollable
= this.container
.$element
;
7081 OO
.ui
.MessageDialog
.super.prototype.setDimensions
.call( this, dim
);
7083 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
7084 // Need to do it after transition completes (250ms), add 50ms just in case.
7085 setTimeout( function () {
7086 var oldOverflow
= $scrollable
[ 0 ].style
.overflow
;
7087 $scrollable
[ 0 ].style
.overflow
= 'hidden';
7089 OO
.ui
.Element
.static.reconsiderScrollbars( $scrollable
[ 0 ] );
7091 $scrollable
[ 0 ].style
.overflow
= oldOverflow
;
7100 OO
.ui
.MessageDialog
.prototype.initialize = function () {
7102 OO
.ui
.MessageDialog
.super.prototype.initialize
.call( this );
7105 this.$actions
= $( '<div>' );
7106 this.container
= new OO
.ui
.PanelLayout( {
7107 scrollable
: true, classes
: [ 'oo-ui-messageDialog-container' ]
7109 this.text
= new OO
.ui
.PanelLayout( {
7110 padded
: true, expanded
: false, classes
: [ 'oo-ui-messageDialog-text' ]
7112 this.message
= new OO
.ui
.LabelWidget( {
7113 classes
: [ 'oo-ui-messageDialog-message' ]
7117 this.title
.$element
.addClass( 'oo-ui-messageDialog-title' );
7118 this.$content
.addClass( 'oo-ui-messageDialog-content' );
7119 this.container
.$element
.append( this.text
.$element
);
7120 this.text
.$element
.append( this.title
.$element
, this.message
.$element
);
7121 this.$body
.append( this.container
.$element
);
7122 this.$actions
.addClass( 'oo-ui-messageDialog-actions' );
7123 this.$foot
.append( this.$actions
);
7129 OO
.ui
.MessageDialog
.prototype.attachActions = function () {
7130 var i
, len
, other
, special
, others
;
7133 OO
.ui
.MessageDialog
.super.prototype.attachActions
.call( this );
7135 special
= this.actions
.getSpecial();
7136 others
= this.actions
.getOthers();
7137 if ( special
.safe
) {
7138 this.$actions
.append( special
.safe
.$element
);
7139 special
.safe
.toggleFramed( false );
7141 if ( others
.length
) {
7142 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
7143 other
= others
[ i
];
7144 this.$actions
.append( other
.$element
);
7145 other
.toggleFramed( false );
7148 if ( special
.primary
) {
7149 this.$actions
.append( special
.primary
.$element
);
7150 special
.primary
.toggleFramed( false );
7153 if ( !this.isOpening() ) {
7154 // If the dialog is currently opening, this will be called automatically soon.
7155 // This also calls #fitActions.
7161 * Fit action actions into columns or rows.
7163 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
7165 OO
.ui
.MessageDialog
.prototype.fitActions = function () {
7167 previous
= this.verticalActionLayout
,
7168 actions
= this.actions
.get();
7171 this.toggleVerticalActionLayout( false );
7172 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
7173 action
= actions
[ i
];
7174 if ( action
.$element
.innerWidth() < action
.$label
.outerWidth( true ) ) {
7175 this.toggleVerticalActionLayout( true );
7180 // Move the body out of the way of the foot
7181 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
7183 if ( this.verticalActionLayout
!== previous
) {
7184 // We changed the layout, window height might need to be updated.
7190 * Navigation dialog window.
7193 * - Show and hide errors.
7194 * - Retry an action.
7197 * - Renders header with dialog title and one action widget on either side
7198 * (a 'safe' button on the left, and a 'primary' button on the right, both of
7199 * which close the dialog).
7200 * - Displays any action widgets in the footer (none by default).
7201 * - Ability to dismiss errors.
7203 * Subclass responsibilities:
7204 * - Register a 'safe' action.
7205 * - Register a 'primary' action.
7206 * - Add content to the dialog.
7210 * @extends OO.ui.Dialog
7213 * @param {Object} [config] Configuration options
7215 OO
.ui
.ProcessDialog
= function OoUiProcessDialog( config
) {
7216 // Parent constructor
7217 OO
.ui
.ProcessDialog
.super.call( this, config
);
7220 this.$element
.addClass( 'oo-ui-processDialog' );
7225 OO
.inheritClass( OO
.ui
.ProcessDialog
, OO
.ui
.Dialog
);
7230 * Handle dismiss button click events.
7234 OO
.ui
.ProcessDialog
.prototype.onDismissErrorButtonClick = function () {
7239 * Handle retry button click events.
7241 * Hides errors and then tries again.
7243 OO
.ui
.ProcessDialog
.prototype.onRetryButtonClick = function () {
7245 this.executeAction( this.currentAction
);
7251 OO
.ui
.ProcessDialog
.prototype.onActionResize = function ( action
) {
7252 if ( this.actions
.isSpecial( action
) ) {
7255 return OO
.ui
.ProcessDialog
.super.prototype.onActionResize
.call( this, action
);
7261 OO
.ui
.ProcessDialog
.prototype.initialize = function () {
7263 OO
.ui
.ProcessDialog
.super.prototype.initialize
.call( this );
7266 this.$navigation
= $( '<div>' );
7267 this.$location
= $( '<div>' );
7268 this.$safeActions
= $( '<div>' );
7269 this.$primaryActions
= $( '<div>' );
7270 this.$otherActions
= $( '<div>' );
7271 this.dismissButton
= new OO
.ui
.ButtonWidget( {
7272 label
: OO
.ui
.msg( 'ooui-dialog-process-dismiss' )
7274 this.retryButton
= new OO
.ui
.ButtonWidget();
7275 this.$errors
= $( '<div>' );
7276 this.$errorsTitle
= $( '<div>' );
7279 this.dismissButton
.connect( this, { click
: 'onDismissErrorButtonClick' } );
7280 this.retryButton
.connect( this, { click
: 'onRetryButtonClick' } );
7283 this.title
.$element
.addClass( 'oo-ui-processDialog-title' );
7285 .append( this.title
.$element
)
7286 .addClass( 'oo-ui-processDialog-location' );
7287 this.$safeActions
.addClass( 'oo-ui-processDialog-actions-safe' );
7288 this.$primaryActions
.addClass( 'oo-ui-processDialog-actions-primary' );
7289 this.$otherActions
.addClass( 'oo-ui-processDialog-actions-other' );
7291 .addClass( 'oo-ui-processDialog-errors-title' )
7292 .text( OO
.ui
.msg( 'ooui-dialog-process-error' ) );
7294 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
7295 .append( this.$errorsTitle
, this.dismissButton
.$element
, this.retryButton
.$element
);
7297 .addClass( 'oo-ui-processDialog-content' )
7298 .append( this.$errors
);
7300 .addClass( 'oo-ui-processDialog-navigation' )
7301 .append( this.$safeActions
, this.$location
, this.$primaryActions
);
7302 this.$head
.append( this.$navigation
);
7303 this.$foot
.append( this.$otherActions
);
7309 OO
.ui
.ProcessDialog
.prototype.attachActions = function () {
7310 var i
, len
, other
, special
, others
;
7313 OO
.ui
.ProcessDialog
.super.prototype.attachActions
.call( this );
7315 special
= this.actions
.getSpecial();
7316 others
= this.actions
.getOthers();
7317 if ( special
.primary
) {
7318 this.$primaryActions
.append( special
.primary
.$element
);
7319 special
.primary
.toggleFramed( true );
7321 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
7322 other
= others
[ i
];
7323 this.$otherActions
.append( other
.$element
);
7324 other
.toggleFramed( true );
7326 if ( special
.safe
) {
7327 this.$safeActions
.append( special
.safe
.$element
);
7328 special
.safe
.toggleFramed( true );
7332 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
7338 OO
.ui
.ProcessDialog
.prototype.executeAction = function ( action
) {
7339 OO
.ui
.ProcessDialog
.super.prototype.executeAction
.call( this, action
)
7340 .fail( this.showErrors
.bind( this ) );
7344 * Fit label between actions.
7348 OO
.ui
.ProcessDialog
.prototype.fitLabel = function () {
7349 var width
= Math
.max(
7350 this.$safeActions
.is( ':visible' ) ? this.$safeActions
.width() : 0,
7351 this.$primaryActions
.is( ':visible' ) ? this.$primaryActions
.width() : 0
7353 this.$location
.css( { paddingLeft
: width
, paddingRight
: width
} );
7359 * Handle errors that occurred during accept or reject processes.
7361 * @param {OO.ui.Error[]} errors Errors to be handled
7363 OO
.ui
.ProcessDialog
.prototype.showErrors = function ( errors
) {
7364 var i
, len
, $item
, actions
,
7370 for ( i
= 0, len
= errors
.length
; i
< len
; i
++ ) {
7371 if ( !errors
[ i
].isRecoverable() ) {
7372 recoverable
= false;
7374 if ( errors
[ i
].isWarning() ) {
7377 $item
= $( '<div>' )
7378 .addClass( 'oo-ui-processDialog-error' )
7379 .append( errors
[ i
].getMessage() );
7380 items
.push( $item
[ 0 ] );
7382 this.$errorItems
= $( items
);
7383 if ( recoverable
) {
7384 abilities
[this.currentAction
] = true;
7385 // Copy the flags from the first matching action
7386 actions
= this.actions
.get( { actions
: this.currentAction
} );
7387 if ( actions
.length
) {
7388 this.retryButton
.clearFlags().setFlags( actions
[0].getFlags() );
7391 abilities
[this.currentAction
] = false;
7392 this.actions
.setAbilities( abilities
);
7395 this.retryButton
.setLabel( OO
.ui
.msg( 'ooui-dialog-process-continue' ) );
7397 this.retryButton
.setLabel( OO
.ui
.msg( 'ooui-dialog-process-retry' ) );
7399 this.retryButton
.toggle( recoverable
);
7400 this.$errorsTitle
.after( this.$errorItems
);
7401 this.$errors
.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
7407 OO
.ui
.ProcessDialog
.prototype.hideErrors = function () {
7408 this.$errors
.addClass( 'oo-ui-element-hidden' );
7409 if ( this.$errorItems
) {
7410 this.$errorItems
.remove();
7411 this.$errorItems
= null;
7418 OO
.ui
.ProcessDialog
.prototype.getTeardownProcess = function ( data
) {
7420 return OO
.ui
.ProcessDialog
.super.prototype.getTeardownProcess
.call( this, data
)
7421 .first( function () {
7422 // Make sure to hide errors
7428 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
7429 * which is a widget that is specified by reference before any optional configuration settings.
7431 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
7433 * - **left**: The label is placed before the field-widget and aligned with the left margin.
7434 * A left-alignment is used for forms with many fields.
7435 * - **right**: The label is placed before the field-widget and aligned to the right margin.
7436 * A right-alignment is used for long but familiar forms which users tab through,
7437 * verifying the current field with a quick glance at the label.
7438 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
7439 * that users fill out from top to bottom.
7440 * - **inline**: The label is placed after the field-widget and aligned to the left.
7441 * An inline-alignment is best used with checkboxes or radio buttons.
7443 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
7444 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
7446 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
7448 * @extends OO.ui.Layout
7449 * @mixins OO.ui.LabelElement
7452 * @param {OO.ui.Widget} fieldWidget Field widget
7453 * @param {Object} [config] Configuration options
7454 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7455 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7457 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
7458 // Allow passing positional parameters inside the config object
7459 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
7460 config
= fieldWidget
;
7461 fieldWidget
= config
.fieldWidget
;
7464 var hasInputWidget
= fieldWidget
instanceof OO
.ui
.InputWidget
;
7466 // Configuration initialization
7467 config
= $.extend( { align
: 'left' }, config
);
7469 // Parent constructor
7470 OO
.ui
.FieldLayout
.super.call( this, config
);
7472 // Mixin constructors
7473 OO
.ui
.LabelElement
.call( this, config
);
7476 this.fieldWidget
= fieldWidget
;
7477 this.$field
= $( '<div>' );
7478 this.$body
= $( '<' + ( hasInputWidget
? 'label' : 'div' ) + '>' );
7480 if ( config
.help
) {
7481 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
7482 classes
: [ 'oo-ui-fieldLayout-help' ],
7487 this.popupButtonWidget
.getPopup().$body
.append(
7489 .text( config
.help
)
7490 .addClass( 'oo-ui-fieldLayout-help-content' )
7492 this.$help
= this.popupButtonWidget
.$element
;
7494 this.$help
= $( [] );
7498 if ( hasInputWidget
) {
7499 this.$label
.on( 'click', this.onLabelClick
.bind( this ) );
7501 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
7505 .addClass( 'oo-ui-fieldLayout' )
7506 .append( this.$help
, this.$body
);
7507 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
7509 .addClass( 'oo-ui-fieldLayout-field' )
7510 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget
.isDisabled() )
7511 .append( this.fieldWidget
.$element
);
7513 this.setAlignment( config
.align
);
7518 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
7519 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.LabelElement
);
7524 * Handle field disable events.
7526 * @param {boolean} value Field is disabled
7528 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
7529 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
7533 * Handle label mouse click events.
7535 * @param {jQuery.Event} e Mouse click event
7537 OO
.ui
.FieldLayout
.prototype.onLabelClick = function () {
7538 this.fieldWidget
.simulateLabelClick();
7545 * @return {OO.ui.Widget} Field widget
7547 OO
.ui
.FieldLayout
.prototype.getField = function () {
7548 return this.fieldWidget
;
7552 * Set the field alignment mode.
7555 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
7558 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
7559 if ( value
!== this.align
) {
7560 // Default to 'left'
7561 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
7565 if ( value
=== 'inline' ) {
7566 this.$body
.append( this.$field
, this.$label
);
7568 this.$body
.append( this.$label
, this.$field
);
7570 // Set classes. The following classes can be used here:
7571 // * oo-ui-fieldLayout-align-left
7572 // * oo-ui-fieldLayout-align-right
7573 // * oo-ui-fieldLayout-align-top
7574 // * oo-ui-fieldLayout-align-inline
7576 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
7578 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
7586 * Layout made of a field, a button, and an optional label.
7589 * @extends OO.ui.FieldLayout
7592 * @param {OO.ui.Widget} fieldWidget Field widget
7593 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
7594 * @param {Object} [config] Configuration options
7595 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7596 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7598 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
7599 // Allow passing positional parameters inside the config object
7600 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
7601 config
= fieldWidget
;
7602 fieldWidget
= config
.fieldWidget
;
7603 buttonWidget
= config
.buttonWidget
;
7606 // Configuration initialization
7607 config
= $.extend( { align
: 'left' }, config
);
7609 // Parent constructor
7610 OO
.ui
.ActionFieldLayout
.super.call( this, fieldWidget
, config
);
7613 this.fieldWidget
= fieldWidget
;
7614 this.buttonWidget
= buttonWidget
;
7615 this.$button
= $( '<div>' )
7616 .addClass( 'oo-ui-actionFieldLayout-button' )
7617 .append( this.buttonWidget
.$element
);
7618 this.$input
= $( '<div>' )
7619 .addClass( 'oo-ui-actionFieldLayout-input' )
7620 .append( this.fieldWidget
.$element
);
7622 .addClass( 'oo-ui-actionFieldLayout' )
7623 .append( this.$input
, this.$button
);
7628 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
7631 * Layout made of a fieldset and optional legend.
7633 * Just add OO.ui.FieldLayout items.
7636 * @extends OO.ui.Layout
7637 * @mixins OO.ui.IconElement
7638 * @mixins OO.ui.LabelElement
7639 * @mixins OO.ui.GroupElement
7642 * @param {Object} [config] Configuration options
7643 * @cfg {OO.ui.FieldLayout[]} [items] Items to add
7645 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
7646 // Configuration initialization
7647 config
= config
|| {};
7649 // Parent constructor
7650 OO
.ui
.FieldsetLayout
.super.call( this, config
);
7652 // Mixin constructors
7653 OO
.ui
.IconElement
.call( this, config
);
7654 OO
.ui
.LabelElement
.call( this, config
);
7655 OO
.ui
.GroupElement
.call( this, config
);
7657 if ( config
.help
) {
7658 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
7659 classes
: [ 'oo-ui-fieldsetLayout-help' ],
7664 this.popupButtonWidget
.getPopup().$body
.append(
7666 .text( config
.help
)
7667 .addClass( 'oo-ui-fieldsetLayout-help-content' )
7669 this.$help
= this.popupButtonWidget
.$element
;
7671 this.$help
= $( [] );
7676 .addClass( 'oo-ui-fieldsetLayout' )
7677 .prepend( this.$help
, this.$icon
, this.$label
, this.$group
);
7678 if ( Array
.isArray( config
.items
) ) {
7679 this.addItems( config
.items
);
7685 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
7686 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.IconElement
);
7687 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.LabelElement
);
7688 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.GroupElement
);
7691 * Layout with an HTML form.
7694 * @extends OO.ui.Layout
7695 * @mixins OO.ui.GroupElement
7698 * @param {Object} [config] Configuration options
7699 * @cfg {string} [method] HTML form `method` attribute
7700 * @cfg {string} [action] HTML form `action` attribute
7701 * @cfg {string} [enctype] HTML form `enctype` attribute
7702 * @cfg {OO.ui.FieldsetLayout[]} [items] Items to add
7704 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
7705 // Configuration initialization
7706 config
= config
|| {};
7708 // Parent constructor
7709 OO
.ui
.FormLayout
.super.call( this, config
);
7711 // Mixin constructors
7712 OO
.ui
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
7715 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
7719 .addClass( 'oo-ui-formLayout' )
7721 method
: config
.method
,
7722 action
: config
.action
,
7723 enctype
: config
.enctype
7725 if ( Array
.isArray( config
.items
) ) {
7726 this.addItems( config
.items
);
7732 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
7733 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.GroupElement
);
7741 /* Static Properties */
7743 OO
.ui
.FormLayout
.static.tagName
= 'form';
7748 * Handle form submit events.
7750 * @param {jQuery.Event} e Submit event
7753 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
7754 this.emit( 'submit' );
7759 * Layout with a content and menu area.
7761 * The menu area can be positioned at the top, after, bottom or before. The content area will fill
7762 * all remaining space.
7765 * @extends OO.ui.Layout
7768 * @param {Object} [config] Configuration options
7769 * @cfg {number|string} [menuSize='18em'] Size of menu in pixels or any CSS unit
7770 * @cfg {boolean} [showMenu=true] Show menu
7771 * @cfg {string} [position='before'] Position of menu, either `top`, `after`, `bottom` or `before`
7772 * @cfg {boolean} [collapse] Collapse the menu out of view
7774 OO
.ui
.MenuLayout
= function OoUiMenuLayout( config
) {
7775 var positions
= this.constructor.static.menuPositions
;
7777 // Configuration initialization
7778 config
= config
|| {};
7780 // Parent constructor
7781 OO
.ui
.MenuLayout
.super.call( this, config
);
7784 this.showMenu
= config
.showMenu
!== false;
7785 this.menuSize
= config
.menuSize
|| '18em';
7786 this.menuPosition
= positions
[ config
.menuPosition
] || positions
.before
;
7791 * @property {jQuery}
7793 this.$menu
= $( '<div>' );
7797 * @property {jQuery}
7799 this.$content
= $( '<div>' );
7802 this.toggleMenu( this.showMenu
);
7805 .addClass( 'oo-ui-menuLayout-menu' )
7806 .css( this.menuPosition
.sizeProperty
, this.menuSize
);
7807 this.$content
.addClass( 'oo-ui-menuLayout-content' );
7809 .addClass( 'oo-ui-menuLayout ' + this.menuPosition
.className
)
7810 .append( this.$content
, this.$menu
);
7815 OO
.inheritClass( OO
.ui
.MenuLayout
, OO
.ui
.Layout
);
7817 /* Static Properties */
7819 OO
.ui
.MenuLayout
.static.menuPositions
= {
7821 sizeProperty
: 'height',
7822 className
: 'oo-ui-menuLayout-top'
7825 sizeProperty
: 'width',
7826 className
: 'oo-ui-menuLayout-after'
7829 sizeProperty
: 'height',
7830 className
: 'oo-ui-menuLayout-bottom'
7833 sizeProperty
: 'width',
7834 className
: 'oo-ui-menuLayout-before'
7843 * @param {boolean} showMenu Show menu, omit to toggle
7846 OO
.ui
.MenuLayout
.prototype.toggleMenu = function ( showMenu
) {
7847 showMenu
= showMenu
=== undefined ? !this.showMenu
: !!showMenu
;
7849 if ( this.showMenu
!== showMenu
) {
7850 this.showMenu
= showMenu
;
7858 * Check if menu is visible
7860 * @return {boolean} Menu is visible
7862 OO
.ui
.MenuLayout
.prototype.isMenuVisible = function () {
7863 return this.showMenu
;
7869 * @param {number|string} size Size of menu in pixels or any CSS unit
7872 OO
.ui
.MenuLayout
.prototype.setMenuSize = function ( size
) {
7873 this.menuSize
= size
;
7880 * Update menu and content CSS based on current menu size and visibility
7882 * This method is called internally when size or position is changed.
7884 OO
.ui
.MenuLayout
.prototype.updateSizes = function () {
7885 if ( this.showMenu
) {
7887 .css( this.menuPosition
.sizeProperty
, this.menuSize
)
7888 .css( 'overflow', '' );
7889 // Set offsets on all sides. CSS resets all but one with
7890 // 'important' rules so directionality flips are supported
7891 this.$content
.css( {
7893 right
: this.menuSize
,
7894 bottom
: this.menuSize
,
7899 .css( this.menuPosition
.sizeProperty
, 0 )
7900 .css( 'overflow', 'hidden' );
7901 this.$content
.css( {
7913 * @return {number|string} Menu size
7915 OO
.ui
.MenuLayout
.prototype.getMenuSize = function () {
7916 return this.menuSize
;
7920 * Set menu position.
7922 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
7923 * @throws {Error} If position value is not supported
7926 OO
.ui
.MenuLayout
.prototype.setMenuPosition = function ( position
) {
7927 var positions
= this.constructor.static.menuPositions
;
7929 if ( !positions
[ position
] ) {
7930 throw new Error( 'Cannot set position; unsupported position value: ' + position
);
7933 this.$menu
.css( this.menuPosition
.sizeProperty
, '' );
7934 this.$element
.removeClass( this.menuPosition
.className
);
7936 this.menuPosition
= positions
[ position
];
7939 this.$element
.addClass( this.menuPosition
.className
);
7945 * Get menu position.
7947 * @return {string} Menu position
7949 OO
.ui
.MenuLayout
.prototype.getMenuPosition = function () {
7950 return this.menuPosition
;
7954 * Layout containing a series of pages.
7957 * @extends OO.ui.MenuLayout
7960 * @param {Object} [config] Configuration options
7961 * @cfg {boolean} [continuous=false] Show all pages, one after another
7962 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
7963 * @cfg {boolean} [outlined=false] Show an outline
7964 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
7966 OO
.ui
.BookletLayout
= function OoUiBookletLayout( config
) {
7967 // Configuration initialization
7968 config
= config
|| {};
7970 // Parent constructor
7971 OO
.ui
.BookletLayout
.super.call( this, config
);
7974 this.currentPageName
= null;
7976 this.ignoreFocus
= false;
7977 this.stackLayout
= new OO
.ui
.StackLayout( { continuous
: !!config
.continuous
} );
7978 this.$content
.append( this.stackLayout
.$element
);
7979 this.autoFocus
= config
.autoFocus
=== undefined || !!config
.autoFocus
;
7980 this.outlineVisible
= false;
7981 this.outlined
= !!config
.outlined
;
7982 if ( this.outlined
) {
7983 this.editable
= !!config
.editable
;
7984 this.outlineControlsWidget
= null;
7985 this.outlineSelectWidget
= new OO
.ui
.OutlineSelectWidget();
7986 this.outlinePanel
= new OO
.ui
.PanelLayout( { scrollable
: true } );
7987 this.$menu
.append( this.outlinePanel
.$element
);
7988 this.outlineVisible
= true;
7989 if ( this.editable
) {
7990 this.outlineControlsWidget
= new OO
.ui
.OutlineControlsWidget(
7991 this.outlineSelectWidget
7995 this.toggleMenu( this.outlined
);
7998 this.stackLayout
.connect( this, { set: 'onStackLayoutSet' } );
7999 if ( this.outlined
) {
8000 this.outlineSelectWidget
.connect( this, { select
: 'onOutlineSelectWidgetSelect' } );
8002 if ( this.autoFocus
) {
8003 // Event 'focus' does not bubble, but 'focusin' does
8004 this.stackLayout
.$element
.on( 'focusin', this.onStackLayoutFocus
.bind( this ) );
8008 this.$element
.addClass( 'oo-ui-bookletLayout' );
8009 this.stackLayout
.$element
.addClass( 'oo-ui-bookletLayout-stackLayout' );
8010 if ( this.outlined
) {
8011 this.outlinePanel
.$element
8012 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
8013 .append( this.outlineSelectWidget
.$element
);
8014 if ( this.editable
) {
8015 this.outlinePanel
.$element
8016 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
8017 .append( this.outlineControlsWidget
.$element
);
8024 OO
.inheritClass( OO
.ui
.BookletLayout
, OO
.ui
.MenuLayout
);
8030 * @param {OO.ui.PageLayout} page Current page
8035 * @param {OO.ui.PageLayout[]} page Added pages
8036 * @param {number} index Index pages were added at
8041 * @param {OO.ui.PageLayout[]} pages Removed pages
8047 * Handle stack layout focus.
8049 * @param {jQuery.Event} e Focusin event
8051 OO
.ui
.BookletLayout
.prototype.onStackLayoutFocus = function ( e
) {
8054 // Find the page that an element was focused within
8055 $target
= $( e
.target
).closest( '.oo-ui-pageLayout' );
8056 for ( name
in this.pages
) {
8057 // Check for page match, exclude current page to find only page changes
8058 if ( this.pages
[ name
].$element
[ 0 ] === $target
[ 0 ] && name
!== this.currentPageName
) {
8059 this.setPage( name
);
8066 * Handle stack layout set events.
8068 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
8070 OO
.ui
.BookletLayout
.prototype.onStackLayoutSet = function ( page
) {
8073 page
.scrollElementIntoView( { complete: function () {
8074 if ( layout
.autoFocus
) {
8082 * Focus the first input in the current page.
8084 * If no page is selected, the first selectable page will be selected.
8085 * If the focus is already in an element on the current page, nothing will happen.
8087 OO
.ui
.BookletLayout
.prototype.focus = function () {
8088 var $input
, page
= this.stackLayout
.getCurrentItem();
8089 if ( !page
&& this.outlined
) {
8090 this.selectFirstSelectablePage();
8091 page
= this.stackLayout
.getCurrentItem();
8096 // Only change the focus if is not already in the current page
8097 if ( !page
.$element
.find( ':focus' ).length
) {
8098 $input
= page
.$element
.find( ':input:first' );
8099 if ( $input
.length
) {
8100 $input
[ 0 ].focus();
8106 * Handle outline widget select events.
8108 * @param {OO.ui.OptionWidget|null} item Selected item
8110 OO
.ui
.BookletLayout
.prototype.onOutlineSelectWidgetSelect = function ( item
) {
8112 this.setPage( item
.getData() );
8117 * Check if booklet has an outline.
8121 OO
.ui
.BookletLayout
.prototype.isOutlined = function () {
8122 return this.outlined
;
8126 * Check if booklet has editing controls.
8130 OO
.ui
.BookletLayout
.prototype.isEditable = function () {
8131 return this.editable
;
8135 * Check if booklet has a visible outline.
8139 OO
.ui
.BookletLayout
.prototype.isOutlineVisible = function () {
8140 return this.outlined
&& this.outlineVisible
;
8144 * Hide or show the outline.
8146 * @param {boolean} [show] Show outline, omit to invert current state
8149 OO
.ui
.BookletLayout
.prototype.toggleOutline = function ( show
) {
8150 if ( this.outlined
) {
8151 show
= show
=== undefined ? !this.outlineVisible
: !!show
;
8152 this.outlineVisible
= show
;
8153 this.toggleMenu( show
);
8160 * Get the outline widget.
8162 * @param {OO.ui.PageLayout} page Page to be selected
8163 * @return {OO.ui.PageLayout|null} Closest page to another
8165 OO
.ui
.BookletLayout
.prototype.getClosestPage = function ( page
) {
8166 var next
, prev
, level
,
8167 pages
= this.stackLayout
.getItems(),
8168 index
= $.inArray( page
, pages
);
8170 if ( index
!== -1 ) {
8171 next
= pages
[ index
+ 1 ];
8172 prev
= pages
[ index
- 1 ];
8173 // Prefer adjacent pages at the same level
8174 if ( this.outlined
) {
8175 level
= this.outlineSelectWidget
.getItemFromData( page
.getName() ).getLevel();
8178 level
=== this.outlineSelectWidget
.getItemFromData( prev
.getName() ).getLevel()
8184 level
=== this.outlineSelectWidget
.getItemFromData( next
.getName() ).getLevel()
8190 return prev
|| next
|| null;
8194 * Get the outline widget.
8196 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
8198 OO
.ui
.BookletLayout
.prototype.getOutline = function () {
8199 return this.outlineSelectWidget
;
8203 * Get the outline controls widget. If the outline is not editable, null is returned.
8205 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
8207 OO
.ui
.BookletLayout
.prototype.getOutlineControls = function () {
8208 return this.outlineControlsWidget
;
8212 * Get a page by name.
8214 * @param {string} name Symbolic name of page
8215 * @return {OO.ui.PageLayout|undefined} Page, if found
8217 OO
.ui
.BookletLayout
.prototype.getPage = function ( name
) {
8218 return this.pages
[ name
];
8222 * Get the current page
8224 * @return {OO.ui.PageLayout|undefined} Current page, if found
8226 OO
.ui
.BookletLayout
.prototype.getCurrentPage = function () {
8227 var name
= this.getCurrentPageName();
8228 return name
? this.getPage( name
) : undefined;
8232 * Get the current page name.
8234 * @return {string|null} Current page name
8236 OO
.ui
.BookletLayout
.prototype.getCurrentPageName = function () {
8237 return this.currentPageName
;
8241 * Add a page to the layout.
8243 * When pages are added with the same names as existing pages, the existing pages will be
8244 * automatically removed before the new pages are added.
8246 * @param {OO.ui.PageLayout[]} pages Pages to add
8247 * @param {number} index Index to insert pages after
8251 OO
.ui
.BookletLayout
.prototype.addPages = function ( pages
, index
) {
8252 var i
, len
, name
, page
, item
, currentIndex
,
8253 stackLayoutPages
= this.stackLayout
.getItems(),
8257 // Remove pages with same names
8258 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
8260 name
= page
.getName();
8262 if ( Object
.prototype.hasOwnProperty
.call( this.pages
, name
) ) {
8263 // Correct the insertion index
8264 currentIndex
= $.inArray( this.pages
[ name
], stackLayoutPages
);
8265 if ( currentIndex
!== -1 && currentIndex
+ 1 < index
) {
8268 remove
.push( this.pages
[ name
] );
8271 if ( remove
.length
) {
8272 this.removePages( remove
);
8276 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
8278 name
= page
.getName();
8279 this.pages
[ page
.getName() ] = page
;
8280 if ( this.outlined
) {
8281 item
= new OO
.ui
.OutlineOptionWidget( { data
: name
} );
8282 page
.setOutlineItem( item
);
8287 if ( this.outlined
&& items
.length
) {
8288 this.outlineSelectWidget
.addItems( items
, index
);
8289 this.selectFirstSelectablePage();
8291 this.stackLayout
.addItems( pages
, index
);
8292 this.emit( 'add', pages
, index
);
8298 * Remove a page from the layout.
8303 OO
.ui
.BookletLayout
.prototype.removePages = function ( pages
) {
8304 var i
, len
, name
, page
,
8307 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
8309 name
= page
.getName();
8310 delete this.pages
[ name
];
8311 if ( this.outlined
) {
8312 items
.push( this.outlineSelectWidget
.getItemFromData( name
) );
8313 page
.setOutlineItem( null );
8316 if ( this.outlined
&& items
.length
) {
8317 this.outlineSelectWidget
.removeItems( items
);
8318 this.selectFirstSelectablePage();
8320 this.stackLayout
.removeItems( pages
);
8321 this.emit( 'remove', pages
);
8327 * Clear all pages from the layout.
8332 OO
.ui
.BookletLayout
.prototype.clearPages = function () {
8334 pages
= this.stackLayout
.getItems();
8337 this.currentPageName
= null;
8338 if ( this.outlined
) {
8339 this.outlineSelectWidget
.clearItems();
8340 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
8341 pages
[ i
].setOutlineItem( null );
8344 this.stackLayout
.clearItems();
8346 this.emit( 'remove', pages
);
8352 * Set the current page by name.
8355 * @param {string} name Symbolic name of page
8357 OO
.ui
.BookletLayout
.prototype.setPage = function ( name
) {
8360 page
= this.pages
[ name
];
8362 if ( name
!== this.currentPageName
) {
8363 if ( this.outlined
) {
8364 selectedItem
= this.outlineSelectWidget
.getSelectedItem();
8365 if ( selectedItem
&& selectedItem
.getData() !== name
) {
8366 this.outlineSelectWidget
.selectItem( this.outlineSelectWidget
.getItemFromData( name
) );
8370 if ( this.currentPageName
&& this.pages
[ this.currentPageName
] ) {
8371 this.pages
[ this.currentPageName
].setActive( false );
8372 // Blur anything focused if the next page doesn't have anything focusable - this
8373 // is not needed if the next page has something focusable because once it is focused
8374 // this blur happens automatically
8375 if ( this.autoFocus
&& !page
.$element
.find( ':input' ).length
) {
8376 $focused
= this.pages
[ this.currentPageName
].$element
.find( ':focus' );
8377 if ( $focused
.length
) {
8378 $focused
[ 0 ].blur();
8382 this.currentPageName
= name
;
8383 this.stackLayout
.setItem( page
);
8384 page
.setActive( true );
8385 this.emit( 'set', page
);
8391 * Select the first selectable page.
8395 OO
.ui
.BookletLayout
.prototype.selectFirstSelectablePage = function () {
8396 if ( !this.outlineSelectWidget
.getSelectedItem() ) {
8397 this.outlineSelectWidget
.selectItem( this.outlineSelectWidget
.getFirstSelectableItem() );
8404 * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
8407 * @extends OO.ui.Layout
8410 * @param {Object} [config] Configuration options
8411 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
8412 * @cfg {boolean} [padded=false] Pad the content from the edges
8413 * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
8415 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
8416 // Configuration initialization
8417 config
= $.extend( {
8423 // Parent constructor
8424 OO
.ui
.PanelLayout
.super.call( this, config
);
8427 this.$element
.addClass( 'oo-ui-panelLayout' );
8428 if ( config
.scrollable
) {
8429 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
8431 if ( config
.padded
) {
8432 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
8434 if ( config
.expanded
) {
8435 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
8441 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
8444 * Page within an booklet layout.
8447 * @extends OO.ui.PanelLayout
8450 * @param {string} name Unique symbolic name of page
8451 * @param {Object} [config] Configuration options
8453 OO
.ui
.PageLayout
= function OoUiPageLayout( name
, config
) {
8454 // Allow passing positional parameters inside the config object
8455 if ( OO
.isPlainObject( name
) && config
=== undefined ) {
8460 // Configuration initialization
8461 config
= $.extend( { scrollable
: true }, config
);
8463 // Parent constructor
8464 OO
.ui
.PageLayout
.super.call( this, config
);
8468 this.outlineItem
= null;
8469 this.active
= false;
8472 this.$element
.addClass( 'oo-ui-pageLayout' );
8477 OO
.inheritClass( OO
.ui
.PageLayout
, OO
.ui
.PanelLayout
);
8483 * @param {boolean} active Page is active
8491 * @return {string} Symbolic name of page
8493 OO
.ui
.PageLayout
.prototype.getName = function () {
8498 * Check if page is active.
8500 * @return {boolean} Page is active
8502 OO
.ui
.PageLayout
.prototype.isActive = function () {
8509 * @return {OO.ui.OutlineOptionWidget|null} Outline item widget
8511 OO
.ui
.PageLayout
.prototype.getOutlineItem = function () {
8512 return this.outlineItem
;
8518 * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
8519 * outline item as desired; this method is called for setting (with an object) and unsetting
8520 * (with null) and overriding methods would have to check the value of `outlineItem` to avoid
8521 * operating on null instead of an OO.ui.OutlineOptionWidget object.
8523 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear
8526 OO
.ui
.PageLayout
.prototype.setOutlineItem = function ( outlineItem
) {
8527 this.outlineItem
= outlineItem
|| null;
8528 if ( outlineItem
) {
8529 this.setupOutlineItem();
8535 * Setup outline item.
8537 * @localdoc Subclasses should override this method to adjust the outline item as desired.
8539 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup
8542 OO
.ui
.PageLayout
.prototype.setupOutlineItem = function () {
8547 * Set page active state.
8549 * @param {boolean} Page is active
8552 OO
.ui
.PageLayout
.prototype.setActive = function ( active
) {
8555 if ( active
!== this.active
) {
8556 this.active
= active
;
8557 this.$element
.toggleClass( 'oo-ui-pageLayout-active', active
);
8558 this.emit( 'active', this.active
);
8563 * Layout containing a series of mutually exclusive pages.
8566 * @extends OO.ui.PanelLayout
8567 * @mixins OO.ui.GroupElement
8570 * @param {Object} [config] Configuration options
8571 * @cfg {boolean} [continuous=false] Show all pages, one after another
8572 * @cfg {OO.ui.Layout[]} [items] Layouts to add
8574 OO
.ui
.StackLayout
= function OoUiStackLayout( config
) {
8575 // Configuration initialization
8576 config
= $.extend( { scrollable
: true }, config
);
8578 // Parent constructor
8579 OO
.ui
.StackLayout
.super.call( this, config
);
8581 // Mixin constructors
8582 OO
.ui
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
8585 this.currentItem
= null;
8586 this.continuous
= !!config
.continuous
;
8589 this.$element
.addClass( 'oo-ui-stackLayout' );
8590 if ( this.continuous
) {
8591 this.$element
.addClass( 'oo-ui-stackLayout-continuous' );
8593 if ( Array
.isArray( config
.items
) ) {
8594 this.addItems( config
.items
);
8600 OO
.inheritClass( OO
.ui
.StackLayout
, OO
.ui
.PanelLayout
);
8601 OO
.mixinClass( OO
.ui
.StackLayout
, OO
.ui
.GroupElement
);
8607 * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
8613 * Get the current item.
8615 * @return {OO.ui.Layout|null}
8617 OO
.ui
.StackLayout
.prototype.getCurrentItem = function () {
8618 return this.currentItem
;
8622 * Unset the current item.
8625 * @param {OO.ui.StackLayout} layout
8628 OO
.ui
.StackLayout
.prototype.unsetCurrentItem = function () {
8629 var prevItem
= this.currentItem
;
8630 if ( prevItem
=== null ) {
8634 this.currentItem
= null;
8635 this.emit( 'set', null );
8641 * Adding an existing item (by value) will move it.
8643 * @param {OO.ui.Layout[]} items Items to add
8644 * @param {number} [index] Index to insert items after
8647 OO
.ui
.StackLayout
.prototype.addItems = function ( items
, index
) {
8648 // Update the visibility
8649 this.updateHiddenState( items
, this.currentItem
);
8652 OO
.ui
.GroupElement
.prototype.addItems
.call( this, items
, index
);
8654 if ( !this.currentItem
&& items
.length
) {
8655 this.setItem( items
[ 0 ] );
8664 * Items will be detached, not removed, so they can be used later.
8666 * @param {OO.ui.Layout[]} items Items to remove
8670 OO
.ui
.StackLayout
.prototype.removeItems = function ( items
) {
8672 OO
.ui
.GroupElement
.prototype.removeItems
.call( this, items
);
8674 if ( $.inArray( this.currentItem
, items
) !== -1 ) {
8675 if ( this.items
.length
) {
8676 this.setItem( this.items
[ 0 ] );
8678 this.unsetCurrentItem();
8688 * Items will be detached, not removed, so they can be used later.
8693 OO
.ui
.StackLayout
.prototype.clearItems = function () {
8694 this.unsetCurrentItem();
8695 OO
.ui
.GroupElement
.prototype.clearItems
.call( this );
8703 * Any currently shown item will be hidden.
8705 * FIXME: If the passed item to show has not been added in the items list, then
8706 * this method drops it and unsets the current item.
8708 * @param {OO.ui.Layout} item Item to show
8712 OO
.ui
.StackLayout
.prototype.setItem = function ( item
) {
8713 if ( item
!== this.currentItem
) {
8714 this.updateHiddenState( this.items
, item
);
8716 if ( $.inArray( item
, this.items
) !== -1 ) {
8717 this.currentItem
= item
;
8718 this.emit( 'set', item
);
8720 this.unsetCurrentItem();
8728 * Update the visibility of all items in case of non-continuous view.
8730 * Ensure all items are hidden except for the selected one.
8731 * This method does nothing when the stack is continuous.
8733 * @param {OO.ui.Layout[]} items Item list iterate over
8734 * @param {OO.ui.Layout} [selectedItem] Selected item to show
8736 OO
.ui
.StackLayout
.prototype.updateHiddenState = function ( items
, selectedItem
) {
8739 if ( !this.continuous
) {
8740 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
8741 if ( !selectedItem
|| selectedItem
!== items
[ i
] ) {
8742 items
[ i
].$element
.addClass( 'oo-ui-element-hidden' );
8745 if ( selectedItem
) {
8746 selectedItem
.$element
.removeClass( 'oo-ui-element-hidden' );
8752 * Horizontal bar layout of tools as icon buttons.
8755 * @extends OO.ui.ToolGroup
8758 * @param {OO.ui.Toolbar} toolbar
8759 * @param {Object} [config] Configuration options
8761 OO
.ui
.BarToolGroup
= function OoUiBarToolGroup( toolbar
, config
) {
8762 // Allow passing positional parameters inside the config object
8763 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
8765 toolbar
= config
.toolbar
;
8768 // Parent constructor
8769 OO
.ui
.BarToolGroup
.super.call( this, toolbar
, config
);
8772 this.$element
.addClass( 'oo-ui-barToolGroup' );
8777 OO
.inheritClass( OO
.ui
.BarToolGroup
, OO
.ui
.ToolGroup
);
8779 /* Static Properties */
8781 OO
.ui
.BarToolGroup
.static.titleTooltips
= true;
8783 OO
.ui
.BarToolGroup
.static.accelTooltips
= true;
8785 OO
.ui
.BarToolGroup
.static.name
= 'bar';
8788 * Popup list of tools with an icon and optional label.
8792 * @extends OO.ui.ToolGroup
8793 * @mixins OO.ui.IconElement
8794 * @mixins OO.ui.IndicatorElement
8795 * @mixins OO.ui.LabelElement
8796 * @mixins OO.ui.TitledElement
8797 * @mixins OO.ui.ClippableElement
8800 * @param {OO.ui.Toolbar} toolbar
8801 * @param {Object} [config] Configuration options
8802 * @cfg {string} [header] Text to display at the top of the pop-up
8804 OO
.ui
.PopupToolGroup
= function OoUiPopupToolGroup( toolbar
, config
) {
8805 // Allow passing positional parameters inside the config object
8806 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
8808 toolbar
= config
.toolbar
;
8811 // Configuration initialization
8812 config
= config
|| {};
8814 // Parent constructor
8815 OO
.ui
.PopupToolGroup
.super.call( this, toolbar
, config
);
8817 // Mixin constructors
8818 OO
.ui
.IconElement
.call( this, config
);
8819 OO
.ui
.IndicatorElement
.call( this, config
);
8820 OO
.ui
.LabelElement
.call( this, config
);
8821 OO
.ui
.TitledElement
.call( this, config
);
8822 OO
.ui
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
8825 this.active
= false;
8826 this.dragging
= false;
8827 this.onBlurHandler
= this.onBlur
.bind( this );
8828 this.$handle
= $( '<span>' );
8832 'mousedown touchstart': this.onHandlePointerDown
.bind( this ),
8833 'mouseup touchend': this.onHandlePointerUp
.bind( this )
8838 .addClass( 'oo-ui-popupToolGroup-handle' )
8839 .append( this.$icon
, this.$label
, this.$indicator
);
8840 // If the pop-up should have a header, add it to the top of the toolGroup.
8841 // Note: If this feature is useful for other widgets, we could abstract it into an
8842 // OO.ui.HeaderedElement mixin constructor.
8843 if ( config
.header
!== undefined ) {
8845 .prepend( $( '<span>' )
8846 .addClass( 'oo-ui-popupToolGroup-header' )
8847 .text( config
.header
)
8851 .addClass( 'oo-ui-popupToolGroup' )
8852 .prepend( this.$handle
);
8857 OO
.inheritClass( OO
.ui
.PopupToolGroup
, OO
.ui
.ToolGroup
);
8858 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.IconElement
);
8859 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.IndicatorElement
);
8860 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.LabelElement
);
8861 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.TitledElement
);
8862 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.ClippableElement
);
8864 /* Static Properties */
8871 OO
.ui
.PopupToolGroup
.prototype.setDisabled = function () {
8873 OO
.ui
.PopupToolGroup
.super.prototype.setDisabled
.apply( this, arguments
);
8875 if ( this.isDisabled() && this.isElementAttached() ) {
8876 this.setActive( false );
8881 * Handle focus being lost.
8883 * The event is actually generated from a mouseup, so it is not a normal blur event object.
8885 * @param {jQuery.Event} e Mouse up event
8887 OO
.ui
.PopupToolGroup
.prototype.onBlur = function ( e
) {
8888 // Only deactivate when clicking outside the dropdown element
8889 if ( $( e
.target
).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element
[ 0 ] ) {
8890 this.setActive( false );
8897 OO
.ui
.PopupToolGroup
.prototype.onPointerUp = function ( e
) {
8898 // e.which is 0 for touch events, 1 for left mouse button
8899 // Only close toolgroup when a tool was actually selected
8900 // FIXME: this duplicates logic from the parent class
8901 if ( !this.isDisabled() && e
.which
<= 1 && this.pressed
&& this.pressed
=== this.getTargetTool( e
) ) {
8902 this.setActive( false );
8904 return OO
.ui
.PopupToolGroup
.super.prototype.onPointerUp
.call( this, e
);
8908 * Handle mouse up events.
8910 * @param {jQuery.Event} e Mouse up event
8912 OO
.ui
.PopupToolGroup
.prototype.onHandlePointerUp = function () {
8917 * Handle mouse down events.
8919 * @param {jQuery.Event} e Mouse down event
8921 OO
.ui
.PopupToolGroup
.prototype.onHandlePointerDown = function ( e
) {
8922 // e.which is 0 for touch events, 1 for left mouse button
8923 if ( !this.isDisabled() && e
.which
<= 1 ) {
8924 this.setActive( !this.active
);
8930 * Switch into active mode.
8932 * When active, mouseup events anywhere in the document will trigger deactivation.
8934 OO
.ui
.PopupToolGroup
.prototype.setActive = function ( value
) {
8936 if ( this.active
!== value
) {
8937 this.active
= value
;
8939 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler
, true );
8941 // Try anchoring the popup to the left first
8942 this.$element
.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
8943 this.toggleClipping( true );
8944 if ( this.isClippedHorizontally() ) {
8945 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
8946 this.toggleClipping( false );
8948 .removeClass( 'oo-ui-popupToolGroup-left' )
8949 .addClass( 'oo-ui-popupToolGroup-right' );
8950 this.toggleClipping( true );
8953 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler
, true );
8954 this.$element
.removeClass(
8955 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
8957 this.toggleClipping( false );
8963 * Drop down list layout of tools as labeled icon buttons.
8965 * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the
8966 * bottom of the main list. These are not automatically positioned at the bottom of the list; you
8967 * may want to use the 'promote' and 'demote' configuration options to achieve this.
8970 * @extends OO.ui.PopupToolGroup
8973 * @param {OO.ui.Toolbar} toolbar
8974 * @param {Object} [config] Configuration options
8975 * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always
8977 * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be
8978 * allowed to be collapsed.
8979 * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
8981 OO
.ui
.ListToolGroup
= function OoUiListToolGroup( toolbar
, config
) {
8982 // Allow passing positional parameters inside the config object
8983 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
8985 toolbar
= config
.toolbar
;
8988 // Configuration initialization
8989 config
= config
|| {};
8991 // Properties (must be set before parent constructor, which calls #populate)
8992 this.allowCollapse
= config
.allowCollapse
;
8993 this.forceExpand
= config
.forceExpand
;
8994 this.expanded
= config
.expanded
!== undefined ? config
.expanded
: false;
8995 this.collapsibleTools
= [];
8997 // Parent constructor
8998 OO
.ui
.ListToolGroup
.super.call( this, toolbar
, config
);
9001 this.$element
.addClass( 'oo-ui-listToolGroup' );
9006 OO
.inheritClass( OO
.ui
.ListToolGroup
, OO
.ui
.PopupToolGroup
);
9008 /* Static Properties */
9010 OO
.ui
.ListToolGroup
.static.accelTooltips
= true;
9012 OO
.ui
.ListToolGroup
.static.name
= 'list';
9019 OO
.ui
.ListToolGroup
.prototype.populate = function () {
9020 var i
, len
, allowCollapse
= [];
9022 OO
.ui
.ListToolGroup
.super.prototype.populate
.call( this );
9024 // Update the list of collapsible tools
9025 if ( this.allowCollapse
!== undefined ) {
9026 allowCollapse
= this.allowCollapse
;
9027 } else if ( this.forceExpand
!== undefined ) {
9028 allowCollapse
= OO
.simpleArrayDifference( Object
.keys( this.tools
), this.forceExpand
);
9031 this.collapsibleTools
= [];
9032 for ( i
= 0, len
= allowCollapse
.length
; i
< len
; i
++ ) {
9033 if ( this.tools
[ allowCollapse
[ i
] ] !== undefined ) {
9034 this.collapsibleTools
.push( this.tools
[ allowCollapse
[ i
] ] );
9038 // Keep at the end, even when tools are added
9039 this.$group
.append( this.getExpandCollapseTool().$element
);
9041 this.getExpandCollapseTool().toggle( this.collapsibleTools
.length
!== 0 );
9042 this.updateCollapsibleState();
9045 OO
.ui
.ListToolGroup
.prototype.getExpandCollapseTool = function () {
9046 if ( this.expandCollapseTool
=== undefined ) {
9047 var ExpandCollapseTool = function () {
9048 ExpandCollapseTool
.super.apply( this, arguments
);
9051 OO
.inheritClass( ExpandCollapseTool
, OO
.ui
.Tool
);
9053 ExpandCollapseTool
.prototype.onSelect = function () {
9054 this.toolGroup
.expanded
= !this.toolGroup
.expanded
;
9055 this.toolGroup
.updateCollapsibleState();
9056 this.setActive( false );
9058 ExpandCollapseTool
.prototype.onUpdateState = function () {
9059 // Do nothing. Tool interface requires an implementation of this function.
9062 ExpandCollapseTool
.static.name
= 'more-fewer';
9064 this.expandCollapseTool
= new ExpandCollapseTool( this );
9066 return this.expandCollapseTool
;
9072 OO
.ui
.ListToolGroup
.prototype.onPointerUp = function ( e
) {
9073 var ret
= OO
.ui
.ListToolGroup
.super.prototype.onPointerUp
.call( this, e
);
9075 // Do not close the popup when the user wants to show more/fewer tools
9076 if ( $( e
.target
).closest( '.oo-ui-tool-name-more-fewer' ).length
) {
9077 // Prevent the popup list from being hidden
9078 this.setActive( true );
9084 OO
.ui
.ListToolGroup
.prototype.updateCollapsibleState = function () {
9087 this.getExpandCollapseTool()
9088 .setIcon( this.expanded
? 'collapse' : 'expand' )
9089 .setTitle( OO
.ui
.msg( this.expanded
? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
9091 for ( i
= 0, len
= this.collapsibleTools
.length
; i
< len
; i
++ ) {
9092 this.collapsibleTools
[ i
].toggle( this.expanded
);
9097 * Drop down menu layout of tools as selectable menu items.
9100 * @extends OO.ui.PopupToolGroup
9103 * @param {OO.ui.Toolbar} toolbar
9104 * @param {Object} [config] Configuration options
9106 OO
.ui
.MenuToolGroup
= function OoUiMenuToolGroup( toolbar
, config
) {
9107 // Allow passing positional parameters inside the config object
9108 if ( OO
.isPlainObject( toolbar
) && config
=== undefined ) {
9110 toolbar
= config
.toolbar
;
9113 // Configuration initialization
9114 config
= config
|| {};
9116 // Parent constructor
9117 OO
.ui
.MenuToolGroup
.super.call( this, toolbar
, config
);
9120 this.toolbar
.connect( this, { updateState
: 'onUpdateState' } );
9123 this.$element
.addClass( 'oo-ui-menuToolGroup' );
9128 OO
.inheritClass( OO
.ui
.MenuToolGroup
, OO
.ui
.PopupToolGroup
);
9130 /* Static Properties */
9132 OO
.ui
.MenuToolGroup
.static.accelTooltips
= true;
9134 OO
.ui
.MenuToolGroup
.static.name
= 'menu';
9139 * Handle the toolbar state being updated.
9141 * When the state changes, the title of each active item in the menu will be joined together and
9142 * used as a label for the group. The label will be empty if none of the items are active.
9144 OO
.ui
.MenuToolGroup
.prototype.onUpdateState = function () {
9148 for ( name
in this.tools
) {
9149 if ( this.tools
[ name
].isActive() ) {
9150 labelTexts
.push( this.tools
[ name
].getTitle() );
9154 this.setLabel( labelTexts
.join( ', ' ) || ' ' );
9158 * Tool that shows a popup when selected.
9162 * @extends OO.ui.Tool
9163 * @mixins OO.ui.PopupElement
9166 * @param {OO.ui.ToolGroup} toolGroup
9167 * @param {Object} [config] Configuration options
9169 OO
.ui
.PopupTool
= function OoUiPopupTool( toolGroup
, config
) {
9170 // Allow passing positional parameters inside the config object
9171 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
9173 toolGroup
= config
.toolGroup
;
9176 // Parent constructor
9177 OO
.ui
.PopupTool
.super.call( this, toolGroup
, config
);
9179 // Mixin constructors
9180 OO
.ui
.PopupElement
.call( this, config
);
9184 .addClass( 'oo-ui-popupTool' )
9185 .append( this.popup
.$element
);
9190 OO
.inheritClass( OO
.ui
.PopupTool
, OO
.ui
.Tool
);
9191 OO
.mixinClass( OO
.ui
.PopupTool
, OO
.ui
.PopupElement
);
9196 * Handle the tool being selected.
9200 OO
.ui
.PopupTool
.prototype.onSelect = function () {
9201 if ( !this.isDisabled() ) {
9202 this.popup
.toggle();
9204 this.setActive( false );
9209 * Handle the toolbar state being updated.
9213 OO
.ui
.PopupTool
.prototype.onUpdateState = function () {
9214 this.setActive( false );
9218 * Tool that has a tool group inside. This is a bad workaround for the lack of proper hierarchical
9219 * menus in toolbars (T74159).
9223 * @extends OO.ui.Tool
9226 * @param {OO.ui.ToolGroup} toolGroup
9227 * @param {Object} [config] Configuration options
9229 OO
.ui
.ToolGroupTool
= function OoUiToolGroupTool( toolGroup
, config
) {
9230 // Allow passing positional parameters inside the config object
9231 if ( OO
.isPlainObject( toolGroup
) && config
=== undefined ) {
9233 toolGroup
= config
.toolGroup
;
9236 // Parent constructor
9237 OO
.ui
.ToolGroupTool
.super.call( this, toolGroup
, config
);
9240 this.innerToolGroup
= this.createGroup( this.constructor.static.groupConfig
);
9243 this.$link
.remove();
9245 .addClass( 'oo-ui-toolGroupTool' )
9246 .append( this.innerToolGroup
.$element
);
9251 OO
.inheritClass( OO
.ui
.ToolGroupTool
, OO
.ui
.Tool
);
9253 /* Static Properties */
9256 * Tool group configuration. See OO.ui.Toolbar#setup for the accepted values.
9258 * @property {Object.<string,Array>}
9260 OO
.ui
.ToolGroupTool
.static.groupConfig
= {};
9265 * Handle the tool being selected.
9269 OO
.ui
.ToolGroupTool
.prototype.onSelect = function () {
9270 this.innerToolGroup
.setActive( !this.innerToolGroup
.active
);
9275 * Handle the toolbar state being updated.
9279 OO
.ui
.ToolGroupTool
.prototype.onUpdateState = function () {
9280 this.setActive( false );
9284 * Build a OO.ui.ToolGroup from the configuration.
9286 * @param {Object.<string,Array>} group Tool group configuration. See OO.ui.Toolbar#setup for the
9288 * @return {OO.ui.ListToolGroup}
9290 OO
.ui
.ToolGroupTool
.prototype.createGroup = function ( group
) {
9291 if ( group
.include
=== '*' ) {
9292 // Apply defaults to catch-all groups
9293 if ( group
.label
=== undefined ) {
9294 group
.label
= OO
.ui
.msg( 'ooui-toolbar-more' );
9298 return this.toolbar
.getToolGroupFactory().create( 'list', this.toolbar
, group
);
9302 * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
9304 * Use together with OO.ui.ItemWidget to make disabled state inheritable.
9309 * @extends OO.ui.GroupElement
9312 * @param {Object} [config] Configuration options
9314 OO
.ui
.GroupWidget
= function OoUiGroupWidget( config
) {
9315 // Parent constructor
9316 OO
.ui
.GroupWidget
.super.call( this, config
);
9321 OO
.inheritClass( OO
.ui
.GroupWidget
, OO
.ui
.GroupElement
);
9326 * Set the disabled state of the widget.
9328 * This will also update the disabled state of child widgets.
9330 * @param {boolean} disabled Disable widget
9333 OO
.ui
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
9337 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
9338 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
9340 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
9342 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
9343 this.items
[ i
].updateDisabled();
9351 * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
9353 * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
9354 * allows bidirectional communication.
9356 * Use together with OO.ui.GroupWidget to make disabled state inheritable.
9364 OO
.ui
.ItemWidget
= function OoUiItemWidget() {
9371 * Check if widget is disabled.
9373 * Checks parent if present, making disabled state inheritable.
9375 * @return {boolean} Widget is disabled
9377 OO
.ui
.ItemWidget
.prototype.isDisabled = function () {
9378 return this.disabled
||
9379 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
9383 * Set group element is in.
9385 * @param {OO.ui.GroupElement|null} group Group element, null if none
9388 OO
.ui
.ItemWidget
.prototype.setElementGroup = function ( group
) {
9390 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
9391 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
9393 // Initialize item disabled states
9394 this.updateDisabled();
9400 * Set of controls for an OO.ui.OutlineSelectWidget.
9402 * Controls include moving items up and down, removing items, and adding different kinds of items.
9405 * @extends OO.ui.Widget
9406 * @mixins OO.ui.GroupElement
9407 * @mixins OO.ui.IconElement
9410 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
9411 * @param {Object} [config] Configuration options
9413 OO
.ui
.OutlineControlsWidget
= function OoUiOutlineControlsWidget( outline
, config
) {
9414 // Allow passing positional parameters inside the config object
9415 if ( OO
.isPlainObject( outline
) && config
=== undefined ) {
9417 outline
= config
.outline
;
9420 // Configuration initialization
9421 config
= $.extend( { icon
: 'add' }, config
);
9423 // Parent constructor
9424 OO
.ui
.OutlineControlsWidget
.super.call( this, config
);
9426 // Mixin constructors
9427 OO
.ui
.GroupElement
.call( this, config
);
9428 OO
.ui
.IconElement
.call( this, config
);
9431 this.outline
= outline
;
9432 this.$movers
= $( '<div>' );
9433 this.upButton
= new OO
.ui
.ButtonWidget( {
9436 title
: OO
.ui
.msg( 'ooui-outline-control-move-up' )
9438 this.downButton
= new OO
.ui
.ButtonWidget( {
9441 title
: OO
.ui
.msg( 'ooui-outline-control-move-down' )
9443 this.removeButton
= new OO
.ui
.ButtonWidget( {
9446 title
: OO
.ui
.msg( 'ooui-outline-control-remove' )
9450 outline
.connect( this, {
9451 select
: 'onOutlineChange',
9452 add
: 'onOutlineChange',
9453 remove
: 'onOutlineChange'
9455 this.upButton
.connect( this, { click
: [ 'emit', 'move', -1 ] } );
9456 this.downButton
.connect( this, { click
: [ 'emit', 'move', 1 ] } );
9457 this.removeButton
.connect( this, { click
: [ 'emit', 'remove' ] } );
9460 this.$element
.addClass( 'oo-ui-outlineControlsWidget' );
9461 this.$group
.addClass( 'oo-ui-outlineControlsWidget-items' );
9463 .addClass( 'oo-ui-outlineControlsWidget-movers' )
9464 .append( this.removeButton
.$element
, this.upButton
.$element
, this.downButton
.$element
);
9465 this.$element
.append( this.$icon
, this.$group
, this.$movers
);
9470 OO
.inheritClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.Widget
);
9471 OO
.mixinClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.GroupElement
);
9472 OO
.mixinClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.IconElement
);
9478 * @param {number} places Number of places to move
9488 * Handle outline change events.
9490 OO
.ui
.OutlineControlsWidget
.prototype.onOutlineChange = function () {
9491 var i
, len
, firstMovable
, lastMovable
,
9492 items
= this.outline
.getItems(),
9493 selectedItem
= this.outline
.getSelectedItem(),
9494 movable
= selectedItem
&& selectedItem
.isMovable(),
9495 removable
= selectedItem
&& selectedItem
.isRemovable();
9500 while ( ++i
< len
) {
9501 if ( items
[ i
].isMovable() ) {
9502 firstMovable
= items
[ i
];
9508 if ( items
[ i
].isMovable() ) {
9509 lastMovable
= items
[ i
];
9514 this.upButton
.setDisabled( !movable
|| selectedItem
=== firstMovable
);
9515 this.downButton
.setDisabled( !movable
|| selectedItem
=== lastMovable
);
9516 this.removeButton
.setDisabled( !removable
);
9520 * ToggleWidget is mixed into other classes to create widgets with an on/off state.
9521 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
9527 * @param {Object} [config] Configuration options
9528 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
9529 * By default, the toggle is in the 'off' state.
9531 OO
.ui
.ToggleWidget
= function OoUiToggleWidget( config
) {
9532 // Configuration initialization
9533 config
= config
|| {};
9539 this.$element
.addClass( 'oo-ui-toggleWidget' );
9540 this.setValue( !!config
.value
);
9548 * A change event is emitted when the on/off state of the toggle changes.
9550 * @param {boolean} value Value representing the new state of the toggle
9556 * Get the value representing the toggle’s state.
9558 * @return {boolean} The on/off state of the toggle
9560 OO
.ui
.ToggleWidget
.prototype.getValue = function () {
9565 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
9567 * @param {boolean} value The state of the toggle
9571 OO
.ui
.ToggleWidget
.prototype.setValue = function ( value
) {
9573 if ( this.value
!== value
) {
9575 this.emit( 'change', value
);
9576 this.$element
.toggleClass( 'oo-ui-toggleWidget-on', value
);
9577 this.$element
.toggleClass( 'oo-ui-toggleWidget-off', !value
);
9578 this.$element
.attr( 'aria-checked', value
.toString() );
9584 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
9585 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
9586 * removed, and cleared from the group.
9589 * // Example: A ButtonGroupWidget with two buttons
9590 * var button1 = new OO.ui.PopupButtonWidget( {
9591 * label : 'Select a category',
9594 * $content: $( '<p>List of categories...</p>' ),
9599 * var button2 = new OO.ui.ButtonWidget( {
9600 * label : 'Add item'
9602 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
9603 * items: [button1, button2]
9605 * $('body').append(buttonGroup.$element);
9608 * @extends OO.ui.Widget
9609 * @mixins OO.ui.GroupElement
9612 * @param {Object} [config] Configuration options
9613 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
9615 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
9616 // Configuration initialization
9617 config
= config
|| {};
9619 // Parent constructor
9620 OO
.ui
.ButtonGroupWidget
.super.call( this, config
);
9622 // Mixin constructors
9623 OO
.ui
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
9626 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
9627 if ( Array
.isArray( config
.items
) ) {
9628 this.addItems( config
.items
);
9634 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
9635 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.GroupElement
);
9638 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
9639 * feels, and functionality can be customized via the class’s configuration options
9640 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
9643 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
9646 * // A button widget
9647 * var button = new OO.ui.ButtonWidget( {
9648 * label : 'Button with Icon',
9650 * iconTitle : 'Remove'
9652 * $( 'body' ).append( button.$element );
9654 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
9657 * @extends OO.ui.Widget
9658 * @mixins OO.ui.ButtonElement
9659 * @mixins OO.ui.IconElement
9660 * @mixins OO.ui.IndicatorElement
9661 * @mixins OO.ui.LabelElement
9662 * @mixins OO.ui.TitledElement
9663 * @mixins OO.ui.FlaggedElement
9664 * @mixins OO.ui.TabIndexedElement
9667 * @param {Object} [config] Configuration options
9668 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
9669 * @cfg {string} [target] The frame or window in which to open the hyperlink.
9670 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
9672 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
9673 // Configuration initialization
9674 // FIXME: The `nofollow` alias is deprecated and will be removed (T89767)
9675 config
= $.extend( { noFollow
: config
&& config
.nofollow
}, config
);
9677 // Parent constructor
9678 OO
.ui
.ButtonWidget
.super.call( this, config
);
9680 // Mixin constructors
9681 OO
.ui
.ButtonElement
.call( this, config
);
9682 OO
.ui
.IconElement
.call( this, config
);
9683 OO
.ui
.IndicatorElement
.call( this, config
);
9684 OO
.ui
.LabelElement
.call( this, config
);
9685 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
9686 OO
.ui
.FlaggedElement
.call( this, config
);
9687 OO
.ui
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
9692 this.noFollow
= false;
9693 this.isHyperlink
= false;
9696 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
9698 .addClass( 'oo-ui-buttonWidget' )
9699 .append( this.$button
);
9700 this.setHref( config
.href
);
9701 this.setTarget( config
.target
);
9702 this.setNoFollow( config
.noFollow
);
9707 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
9708 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.ButtonElement
);
9709 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.IconElement
);
9710 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.IndicatorElement
);
9711 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.LabelElement
);
9712 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.TitledElement
);
9713 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.FlaggedElement
);
9714 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.TabIndexedElement
);
9721 OO
.ui
.ButtonWidget
.prototype.onMouseDown = function ( e
) {
9722 if ( !this.isDisabled() ) {
9723 // Remove the tab-index while the button is down to prevent the button from stealing focus
9724 this.$button
.removeAttr( 'tabindex' );
9727 return OO
.ui
.ButtonElement
.prototype.onMouseDown
.call( this, e
);
9733 OO
.ui
.ButtonWidget
.prototype.onMouseUp = function ( e
) {
9734 if ( !this.isDisabled() ) {
9735 // Restore the tab-index after the button is up to restore the button's accessibility
9736 this.$button
.attr( 'tabindex', this.tabIndex
);
9739 return OO
.ui
.ButtonElement
.prototype.onMouseUp
.call( this, e
);
9745 OO
.ui
.ButtonWidget
.prototype.onClick = function ( e
) {
9746 var ret
= OO
.ui
.ButtonElement
.prototype.onClick
.call( this, e
);
9747 if ( this.isHyperlink
) {
9756 OO
.ui
.ButtonWidget
.prototype.onKeyPress = function ( e
) {
9757 var ret
= OO
.ui
.ButtonElement
.prototype.onKeyPress
.call( this, e
);
9758 if ( this.isHyperlink
) {
9765 * Get hyperlink location.
9767 * @return {string} Hyperlink location
9769 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
9774 * Get hyperlink target.
9776 * @return {string} Hyperlink target
9778 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
9783 * Get search engine traversal hint.
9785 * @return {boolean} Whether search engines should avoid traversing this hyperlink
9787 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
9788 return this.noFollow
;
9792 * Set hyperlink location.
9794 * @param {string|null} href Hyperlink location, null to remove
9796 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
9797 href
= typeof href
=== 'string' ? href
: null;
9799 if ( href
!== this.href
) {
9801 if ( href
!== null ) {
9802 this.$button
.attr( 'href', href
);
9803 this.isHyperlink
= true;
9805 this.$button
.removeAttr( 'href' );
9806 this.isHyperlink
= false;
9814 * Set hyperlink target.
9816 * @param {string|null} target Hyperlink target, null to remove
9818 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
9819 target
= typeof target
=== 'string' ? target
: null;
9821 if ( target
!== this.target
) {
9822 this.target
= target
;
9823 if ( target
!== null ) {
9824 this.$button
.attr( 'target', target
);
9826 this.$button
.removeAttr( 'target' );
9834 * Set search engine traversal hint.
9836 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
9838 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
9839 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
9841 if ( noFollow
!== this.noFollow
) {
9842 this.noFollow
= noFollow
;
9844 this.$button
.attr( 'rel', 'nofollow' );
9846 this.$button
.removeAttr( 'rel' );
9854 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
9855 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
9856 * of the actions. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
9859 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
9862 * @extends OO.ui.ButtonWidget
9863 * @mixins OO.ui.PendingElement
9866 * @param {Object} [config] Configuration options
9867 * @cfg {string} [action] Symbolic action name
9868 * @cfg {string[]} [modes] Symbolic mode names
9869 * @cfg {boolean} [framed=false] Render button with a frame
9871 OO
.ui
.ActionWidget
= function OoUiActionWidget( config
) {
9872 // Configuration initialization
9873 config
= $.extend( { framed
: false }, config
);
9875 // Parent constructor
9876 OO
.ui
.ActionWidget
.super.call( this, config
);
9878 // Mixin constructors
9879 OO
.ui
.PendingElement
.call( this, config
);
9882 this.action
= config
.action
|| '';
9883 this.modes
= config
.modes
|| [];
9888 this.$element
.addClass( 'oo-ui-actionWidget' );
9893 OO
.inheritClass( OO
.ui
.ActionWidget
, OO
.ui
.ButtonWidget
);
9894 OO
.mixinClass( OO
.ui
.ActionWidget
, OO
.ui
.PendingElement
);
9905 * Check if action is available in a certain mode.
9907 * @param {string} mode Name of mode
9908 * @return {boolean} Has mode
9910 OO
.ui
.ActionWidget
.prototype.hasMode = function ( mode
) {
9911 return this.modes
.indexOf( mode
) !== -1;
9915 * Get symbolic action name.
9919 OO
.ui
.ActionWidget
.prototype.getAction = function () {
9924 * Get symbolic action name.
9928 OO
.ui
.ActionWidget
.prototype.getModes = function () {
9929 return this.modes
.slice();
9933 * Emit a resize event if the size has changed.
9937 OO
.ui
.ActionWidget
.prototype.propagateResize = function () {
9940 if ( this.isElementAttached() ) {
9941 width
= this.$element
.width();
9942 height
= this.$element
.height();
9944 if ( width
!== this.width
|| height
!== this.height
) {
9946 this.height
= height
;
9947 this.emit( 'resize' );
9957 OO
.ui
.ActionWidget
.prototype.setIcon = function () {
9959 OO
.ui
.IconElement
.prototype.setIcon
.apply( this, arguments
);
9960 this.propagateResize();
9968 OO
.ui
.ActionWidget
.prototype.setLabel = function () {
9970 OO
.ui
.LabelElement
.prototype.setLabel
.apply( this, arguments
);
9971 this.propagateResize();
9979 OO
.ui
.ActionWidget
.prototype.setFlags = function () {
9981 OO
.ui
.FlaggedElement
.prototype.setFlags
.apply( this, arguments
);
9982 this.propagateResize();
9990 OO
.ui
.ActionWidget
.prototype.clearFlags = function () {
9992 OO
.ui
.FlaggedElement
.prototype.clearFlags
.apply( this, arguments
);
9993 this.propagateResize();
9999 * Toggle visibility of button.
10001 * @param {boolean} [show] Show button, omit to toggle visibility
10004 OO
.ui
.ActionWidget
.prototype.toggle = function () {
10006 OO
.ui
.ActionWidget
.super.prototype.toggle
.apply( this, arguments
);
10007 this.propagateResize();
10013 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
10014 * which is used to display additional information or options.
10017 * // Example of a popup button.
10018 * var popupButton = new OO.ui.PopupButtonWidget( {
10019 * label: 'Popup button with options',
10022 * $content: $( '<p>Additional options here.</p>' ),
10027 * // Append the button to the DOM.
10028 * $( 'body' ).append( popupButton.$element );
10031 * @extends OO.ui.ButtonWidget
10032 * @mixins OO.ui.PopupElement
10035 * @param {Object} [config] Configuration options
10037 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
10038 // Parent constructor
10039 OO
.ui
.PopupButtonWidget
.super.call( this, config
);
10041 // Mixin constructors
10042 OO
.ui
.PopupElement
.call( this, config
);
10045 this.connect( this, { click
: 'onAction' } );
10049 .addClass( 'oo-ui-popupButtonWidget' )
10050 .attr( 'aria-haspopup', 'true' )
10051 .append( this.popup
.$element
);
10056 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
10057 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.PopupElement
);
10062 * Handle the button action being triggered.
10066 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
10067 this.popup
.toggle();
10071 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
10072 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
10073 * configured with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators},
10074 * {@link OO.ui.TitledElement titles}, {@link OO.ui.FlaggedElement styling flags},
10075 * and {@link OO.ui.LabelElement labels}. Please see
10076 * the [OOjs UI documentation][1] on MediaWiki for more information.
10079 * // Toggle buttons in the 'off' and 'on' state.
10080 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
10081 * label: 'Toggle Button off'
10083 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
10084 * label: 'Toggle Button on',
10087 * // Append the buttons to the DOM.
10088 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
10090 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
10093 * @extends OO.ui.ButtonWidget
10094 * @mixins OO.ui.ToggleWidget
10097 * @param {Object} [config] Configuration options
10098 * @cfg {boolean} [value=false] The toggle button’s initial on/off
10099 * state. By default, the button is in the 'off' state.
10101 OO
.ui
.ToggleButtonWidget
= function OoUiToggleButtonWidget( config
) {
10102 // Configuration initialization
10103 config
= config
|| {};
10105 // Parent constructor
10106 OO
.ui
.ToggleButtonWidget
.super.call( this, config
);
10108 // Mixin constructors
10109 OO
.ui
.ToggleWidget
.call( this, config
);
10112 this.connect( this, { click
: 'onAction' } );
10115 this.$element
.addClass( 'oo-ui-toggleButtonWidget' );
10120 OO
.inheritClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.ButtonWidget
);
10121 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.ToggleWidget
);
10128 * Handle the button action being triggered.
10130 OO
.ui
.ToggleButtonWidget
.prototype.onAction = function () {
10131 this.setValue( !this.value
);
10137 OO
.ui
.ToggleButtonWidget
.prototype.setValue = function ( value
) {
10139 if ( value
!== this.value
) {
10140 this.$button
.attr( 'aria-pressed', value
.toString() );
10141 this.setActive( value
);
10144 // Parent method (from mixin)
10145 OO
.ui
.ToggleWidget
.prototype.setValue
.call( this, value
);
10151 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
10152 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
10153 * users can interact with it.
10156 * // Example: A DropdownWidget with a menu that contains three options
10157 * var dropDown=new OO.ui.DropdownWidget( {
10158 * label: 'Dropdown menu: Select a menu option',
10161 * new OO.ui.MenuOptionWidget( {
10165 * new OO.ui.MenuOptionWidget( {
10169 * new OO.ui.MenuOptionWidget( {
10177 * $('body').append(dropDown.$element);
10179 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
10181 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10184 * @extends OO.ui.Widget
10185 * @mixins OO.ui.IconElement
10186 * @mixins OO.ui.IndicatorElement
10187 * @mixins OO.ui.LabelElement
10188 * @mixins OO.ui.TitledElement
10189 * @mixins OO.ui.TabIndexedElement
10192 * @param {Object} [config] Configuration options
10193 * @cfg {Object} [menu] Configuration options to pass to menu widget
10195 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
10196 // Configuration initialization
10197 config
= $.extend( { indicator
: 'down' }, config
);
10199 // Parent constructor
10200 OO
.ui
.DropdownWidget
.super.call( this, config
);
10202 // Properties (must be set before TabIndexedElement constructor call)
10203 this.$handle
= this.$( '<span>' );
10205 // Mixin constructors
10206 OO
.ui
.IconElement
.call( this, config
);
10207 OO
.ui
.IndicatorElement
.call( this, config
);
10208 OO
.ui
.LabelElement
.call( this, config
);
10209 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
10210 OO
.ui
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
10213 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( { widget
: this }, config
.menu
) );
10217 click
: this.onClick
.bind( this ),
10218 keypress
: this.onKeyPress
.bind( this )
10220 this.menu
.connect( this, { select
: 'onMenuSelect' } );
10224 .addClass( 'oo-ui-dropdownWidget-handle' )
10225 .append( this.$icon
, this.$label
, this.$indicator
);
10227 .addClass( 'oo-ui-dropdownWidget' )
10228 .append( this.$handle
, this.menu
.$element
);
10233 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
10234 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.IconElement
);
10235 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.IndicatorElement
);
10236 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.LabelElement
);
10237 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.TitledElement
);
10238 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.TabIndexedElement
);
10245 * @return {OO.ui.MenuSelectWidget} Menu of widget
10247 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
10252 * Handles menu select events.
10255 * @param {OO.ui.MenuOptionWidget} item Selected menu item
10257 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
10264 selectedLabel
= item
.getLabel();
10266 // If the label is a DOM element, clone it, because setLabel will append() it
10267 if ( selectedLabel
instanceof jQuery
) {
10268 selectedLabel
= selectedLabel
.clone();
10271 this.setLabel( selectedLabel
);
10275 * Handle mouse click events.
10278 * @param {jQuery.Event} e Mouse click event
10280 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
10281 if ( !this.isDisabled() && e
.which
=== 1 ) {
10282 this.menu
.toggle();
10288 * Handle key press events.
10291 * @param {jQuery.Event} e Key press event
10293 OO
.ui
.DropdownWidget
.prototype.onKeyPress = function ( e
) {
10294 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
10295 this.menu
.toggle();
10301 * IconWidget is a generic widget for {@link OO.ui.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
10302 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
10303 * for a list of icons included in the library.
10306 * // An icon widget with a label
10307 * var myIcon = new OO.ui.IconWidget({
10309 * iconTitle: 'Help'
10311 * // Create a label.
10312 * var iconLabel = new OO.ui.LabelWidget({
10315 * $('body').append(myIcon.$element, iconLabel.$element);
10317 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
10320 * @extends OO.ui.Widget
10321 * @mixins OO.ui.IconElement
10322 * @mixins OO.ui.TitledElement
10325 * @param {Object} [config] Configuration options
10327 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
10328 // Configuration initialization
10329 config
= config
|| {};
10331 // Parent constructor
10332 OO
.ui
.IconWidget
.super.call( this, config
);
10334 // Mixin constructors
10335 OO
.ui
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
10336 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
10339 this.$element
.addClass( 'oo-ui-iconWidget' );
10344 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
10345 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.IconElement
);
10346 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.TitledElement
);
10348 /* Static Properties */
10350 OO
.ui
.IconWidget
.static.tagName
= 'span';
10353 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
10354 * attention to the status of an item or to clarify the function of a control. For a list of
10355 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
10358 * // Example of an indicator widget
10359 * var indicator1 = new OO.ui.IndicatorWidget( {
10360 * indicator: 'alert'
10363 * // Create a fieldset layout to add a label
10364 * var fieldset = new OO.ui.FieldsetLayout( );
10365 * fieldset.addItems( [
10366 * new OO.ui.FieldLayout( indicator1, {label: 'An alert indicator:'} )
10368 * $( 'body' ).append( fieldset.$element );
10370 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
10373 * @extends OO.ui.Widget
10374 * @mixins OO.ui.IndicatorElement
10375 * @mixins OO.ui.TitledElement
10378 * @param {Object} [config] Configuration options
10380 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
10381 // Configuration initialization
10382 config
= config
|| {};
10384 // Parent constructor
10385 OO
.ui
.IndicatorWidget
.super.call( this, config
);
10387 // Mixin constructors
10388 OO
.ui
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
10389 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
10392 this.$element
.addClass( 'oo-ui-indicatorWidget' );
10397 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
10398 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.IndicatorElement
);
10399 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.TitledElement
);
10401 /* Static Properties */
10403 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
10406 * InputWidget is the base class for all input widgets, which
10407 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
10408 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
10409 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10411 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10415 * @extends OO.ui.Widget
10416 * @mixins OO.ui.FlaggedElement
10417 * @mixins OO.ui.TabIndexedElement
10420 * @param {Object} [config] Configuration options
10421 * @cfg {string} [name=''] HTML input name
10422 * @cfg {string} [value=''] Input value
10423 * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
10425 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
10426 // Configuration initialization
10427 config
= config
|| {};
10429 // Parent constructor
10430 OO
.ui
.InputWidget
.super.call( this, config
);
10433 this.$input
= this.getInputElement( config
);
10435 this.inputFilter
= config
.inputFilter
;
10437 // Mixin constructors
10438 OO
.ui
.FlaggedElement
.call( this, config
);
10439 OO
.ui
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
10442 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
10446 .attr( 'name', config
.name
)
10447 .prop( 'disabled', this.isDisabled() );
10448 this.$element
.addClass( 'oo-ui-inputWidget' ).append( this.$input
, $( '<span>' ) );
10449 this.setValue( config
.value
);
10454 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
10455 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.FlaggedElement
);
10456 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.TabIndexedElement
);
10462 * @param {string} value
10468 * Get input element.
10470 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
10471 * different circumstances. The element must have a `value` property (like form elements).
10474 * @param {Object} config Configuration options
10475 * @return {jQuery} Input element
10477 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
10478 return $( '<input>' );
10482 * Handle potentially value-changing events.
10484 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
10486 OO
.ui
.InputWidget
.prototype.onEdit = function () {
10488 if ( !this.isDisabled() ) {
10489 // Allow the stack to clear so the value will be updated
10490 setTimeout( function () {
10491 widget
.setValue( widget
.$input
.val() );
10497 * Get the value of the input.
10499 * @return {string} Input value
10501 OO
.ui
.InputWidget
.prototype.getValue = function () {
10502 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10503 // it, and we won't know unless they're kind enough to trigger a 'change' event.
10504 var value
= this.$input
.val();
10505 if ( this.value
!== value
) {
10506 this.setValue( value
);
10512 * Sets the direction of the current input, either RTL or LTR
10514 * @param {boolean} isRTL
10516 OO
.ui
.InputWidget
.prototype.setRTL = function ( isRTL
) {
10517 this.$input
.prop( 'dir', isRTL
? 'rtl' : 'ltr' );
10521 * Set the value of the input.
10523 * @param {string} value New value
10527 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
10528 value
= this.cleanUpValue( value
);
10529 // Update the DOM if it has changed. Note that with cleanUpValue, it
10530 // is possible for the DOM value to change without this.value changing.
10531 if ( this.$input
.val() !== value
) {
10532 this.$input
.val( value
);
10534 if ( this.value
!== value
) {
10535 this.value
= value
;
10536 this.emit( 'change', this.value
);
10542 * Clean up incoming value.
10544 * Ensures value is a string, and converts undefined and null to empty string.
10547 * @param {string} value Original value
10548 * @return {string} Cleaned up value
10550 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
10551 if ( value
=== undefined || value
=== null ) {
10553 } else if ( this.inputFilter
) {
10554 return this.inputFilter( String( value
) );
10556 return String( value
);
10561 * Simulate the behavior of clicking on a label bound to this input.
10563 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
10564 if ( !this.isDisabled() ) {
10565 if ( this.$input
.is( ':checkbox, :radio' ) ) {
10566 this.$input
.click();
10568 if ( this.$input
.is( ':input' ) ) {
10569 this.$input
[ 0 ].focus();
10577 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
10578 OO
.ui
.InputWidget
.super.prototype.setDisabled
.call( this, state
);
10579 if ( this.$input
) {
10580 this.$input
.prop( 'disabled', this.isDisabled() );
10590 OO
.ui
.InputWidget
.prototype.focus = function () {
10591 this.$input
[ 0 ].focus();
10600 OO
.ui
.InputWidget
.prototype.blur = function () {
10601 this.$input
[ 0 ].blur();
10606 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
10607 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
10608 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
10609 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
10610 * [OOjs UI documentation on MediaWiki] [1] for more information.
10613 * // A ButtonInputWidget rendered as an HTML button, the default.
10614 * var button = new OO.ui.ButtonInputWidget( {
10615 * label: 'Input button',
10619 * $( 'body' ).append( button.$element );
10621 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
10624 * @extends OO.ui.InputWidget
10625 * @mixins OO.ui.ButtonElement
10626 * @mixins OO.ui.IconElement
10627 * @mixins OO.ui.IndicatorElement
10628 * @mixins OO.ui.LabelElement
10629 * @mixins OO.ui.TitledElement
10630 * @mixins OO.ui.FlaggedElement
10633 * @param {Object} [config] Configuration options
10634 * @cfg {string} [type='button'] HTML tag `type` attribute, may be 'button', 'submit' or 'reset'
10635 * @cfg {boolean} [useInputTag=false] Whether to use `<input/>` rather than `<button/>`. Only useful
10636 * if you need IE 6 support in a form with multiple buttons. If you use this option, icons and
10637 * indicators will not be displayed, it won't be possible to have a non-plaintext label, and it
10638 * won't be possible to set a value (which will internally become identical to the label).
10640 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
10641 // Configuration initialization
10642 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
10644 // Properties (must be set before parent constructor, which calls #setValue)
10645 this.useInputTag
= config
.useInputTag
;
10647 // Parent constructor
10648 OO
.ui
.ButtonInputWidget
.super.call( this, config
);
10650 // Mixin constructors
10651 OO
.ui
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
10652 OO
.ui
.IconElement
.call( this, config
);
10653 OO
.ui
.IndicatorElement
.call( this, config
);
10654 OO
.ui
.LabelElement
.call( this, config
);
10655 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
10656 OO
.ui
.FlaggedElement
.call( this, config
);
10659 if ( !config
.useInputTag
) {
10660 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
10662 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
10667 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
10668 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.ButtonElement
);
10669 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.IconElement
);
10670 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.IndicatorElement
);
10671 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.LabelElement
);
10672 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.TitledElement
);
10673 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.FlaggedElement
);
10681 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
10682 var html
= '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + config
.type
+ '">';
10689 * Overridden to support setting the 'value' of `<input/>` elements.
10691 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
10692 * text; or null for no label
10695 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
10696 OO
.ui
.LabelElement
.prototype.setLabel
.call( this, label
);
10698 if ( this.useInputTag
) {
10699 if ( typeof label
=== 'function' ) {
10700 label
= OO
.ui
.resolveMsg( label
);
10702 if ( label
instanceof jQuery
) {
10703 label
= label
.text();
10708 this.$input
.val( label
);
10715 * Set the value of the input.
10717 * Overridden to disable for `<input/>` elements, which have value identical to the label.
10719 * @param {string} value New value
10722 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
10723 if ( !this.useInputTag
) {
10724 OO
.ui
.ButtonInputWidget
.super.prototype.setValue
.call( this, value
);
10730 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
10731 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
10732 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
10733 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
10736 * // An example of selected, unselected, and disabled checkbox inputs
10737 * var checkbox1=new OO.ui.CheckboxInputWidget({
10741 * var checkbox2=new OO.ui.CheckboxInputWidget({
10744 * var checkbox3=new OO.ui.CheckboxInputWidget( {
10748 * // Create a fieldset layout with fields for each checkbox.
10749 * var fieldset = new OO.ui.FieldsetLayout( {
10750 * label: 'Checkboxes'
10752 * fieldset.addItems( [
10753 * new OO.ui.FieldLayout( checkbox1, {label : 'Selected checkbox', align : 'inline'}),
10754 * new OO.ui.FieldLayout( checkbox2, {label : 'Unselected checkbox', align : 'inline'}),
10755 * new OO.ui.FieldLayout( checkbox3, {label : 'Disabled checkbox', align : 'inline'}),
10757 * $( 'body' ).append( fieldset.$element );
10759 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10762 * @extends OO.ui.InputWidget
10765 * @param {Object} [config] Configuration options
10766 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
10768 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
10769 // Configuration initialization
10770 config
= config
|| {};
10772 // Parent constructor
10773 OO
.ui
.CheckboxInputWidget
.super.call( this, config
);
10776 this.$element
.addClass( 'oo-ui-checkboxInputWidget' );
10777 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
10782 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
10790 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
10791 return $( '<input type="checkbox" />' );
10797 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
10799 if ( !this.isDisabled() ) {
10800 // Allow the stack to clear so the value will be updated
10801 setTimeout( function () {
10802 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
10808 * Set selection state of this checkbox.
10810 * @param {boolean} state `true` for selected
10813 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
10815 if ( this.selected
!== state
) {
10816 this.selected
= state
;
10817 this.$input
.prop( 'checked', this.selected
);
10818 this.emit( 'change', this.selected
);
10824 * Check if this checkbox is selected.
10826 * @return {boolean} Checkbox is selected
10828 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
10829 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10830 // it, and we won't know unless they're kind enough to trigger a 'change' event.
10831 var selected
= this.$input
.prop( 'checked' );
10832 if ( this.selected
!== selected
) {
10833 this.setSelected( selected
);
10835 return this.selected
;
10839 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
10840 * within a {@link OO.ui.FormLayout form}. The selected value is synchronized with the value
10841 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
10842 * more information about input widgets.
10845 * // Example: A DropdownInputWidget with three options
10846 * var dropDown=new OO.ui.DropdownInputWidget( {
10847 * label: 'Dropdown menu: Select a menu option',
10849 * { data: 'a', label: 'First' } ,
10850 * { data: 'b', label: 'Second'} ,
10851 * { data: 'c', label: 'Third' }
10854 * $('body').append(dropDown.$element);
10856 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10859 * @extends OO.ui.InputWidget
10862 * @param {Object} [config] Configuration options
10863 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10865 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
10866 // Configuration initialization
10867 config
= config
|| {};
10869 // Properties (must be done before parent constructor which calls #setDisabled)
10870 this.dropdownWidget
= new OO
.ui
.DropdownWidget();
10872 // Parent constructor
10873 OO
.ui
.DropdownInputWidget
.super.call( this, config
);
10876 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
10879 this.setOptions( config
.options
|| [] );
10881 .addClass( 'oo-ui-dropdownInputWidget' )
10882 .append( this.dropdownWidget
.$element
);
10887 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
10895 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
10896 return $( '<input type="hidden">' );
10900 * Handles menu select events.
10903 * @param {OO.ui.MenuOptionWidget} item Selected menu item
10905 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
10906 this.setValue( item
.getData() );
10912 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
10913 var item
= this.dropdownWidget
.getMenu().getItemFromData( value
);
10915 this.dropdownWidget
.getMenu().selectItem( item
);
10917 OO
.ui
.DropdownInputWidget
.super.prototype.setValue
.call( this, value
);
10924 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
10925 this.dropdownWidget
.setDisabled( state
);
10926 OO
.ui
.DropdownInputWidget
.super.prototype.setDisabled
.call( this, state
);
10931 * Set the options available for this input.
10933 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10936 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
10937 var value
= this.getValue();
10939 // Rebuild the dropdown menu
10940 this.dropdownWidget
.getMenu()
10942 .addItems( options
.map( function ( opt
) {
10943 return new OO
.ui
.MenuOptionWidget( {
10945 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
10949 // Restore the previous value, or reset to something sensible
10950 if ( this.dropdownWidget
.getMenu().getItemFromData( value
) ) {
10951 // Previous value is still available, ensure consistency with the dropdown
10952 this.setValue( value
);
10954 // No longer valid, reset
10955 if ( options
.length
) {
10956 this.setValue( options
[ 0 ].data
);
10966 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
10967 this.dropdownWidget
.getMenu().toggle( true );
10974 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
10975 this.dropdownWidget
.getMenu().toggle( false );
10980 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
10981 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
10982 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
10983 * please see the [OOjs UI documentation on MediaWiki][1].
10986 * // An example of selected, unselected, and disabled radio inputs
10987 * var radio1=new OO.ui.RadioInputWidget({
10991 * var radio2=new OO.ui.RadioInputWidget({
10994 * var radio3=new OO.ui.RadioInputWidget( {
10998 * // Create a fieldset layout with fields for each radio button.
10999 * var fieldset = new OO.ui.FieldsetLayout( {
11000 * label: 'Radio inputs'
11002 * fieldset.addItems( [
11003 * new OO.ui.FieldLayout( radio1, {label : 'Selected', align : 'inline'}),
11004 * new OO.ui.FieldLayout( radio2, {label : 'Unselected', align : 'inline'}),
11005 * new OO.ui.FieldLayout( radio3, {label : 'Disabled', align : 'inline'}),
11007 * $( 'body' ).append( fieldset.$element );
11009 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11012 * @extends OO.ui.InputWidget
11015 * @param {Object} [config] Configuration options
11016 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
11018 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
11019 // Configuration initialization
11020 config
= config
|| {};
11022 // Parent constructor
11023 OO
.ui
.RadioInputWidget
.super.call( this, config
);
11026 this.$element
.addClass( 'oo-ui-radioInputWidget' );
11027 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
11032 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
11040 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
11041 return $( '<input type="radio" />' );
11047 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
11048 // RadioInputWidget doesn't track its state.
11052 * Set selection state of this radio button.
11054 * @param {boolean} state `true` for selected
11057 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
11058 // RadioInputWidget doesn't track its state.
11059 this.$input
.prop( 'checked', state
);
11064 * Check if this radio button is selected.
11066 * @return {boolean} Radio is selected
11068 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
11069 return this.$input
.prop( 'checked' );
11073 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
11074 * size of the field as well as its presentation. In addition, these widgets can be configured
11075 * with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators}, an optional
11076 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
11077 * which modifies incoming values rather than validating them.
11078 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
11081 * // Example of a text input widget
11082 * var textInput=new OO.ui.TextInputWidget( {
11083 * value: 'Text input'
11085 * $('body').append(textInput.$element);
11087 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11090 * @extends OO.ui.InputWidget
11091 * @mixins OO.ui.IconElement
11092 * @mixins OO.ui.IndicatorElement
11093 * @mixins OO.ui.PendingElement
11094 * @mixins OO.ui.LabelElement
11097 * @param {Object} [config] Configuration options
11098 * @cfg {string} [type='text'] The value of the HTML `type` attribute
11099 * @cfg {string} [placeholder] Placeholder text
11100 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
11101 * instruct the browser to focus this widget.
11102 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
11103 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
11104 * @cfg {boolean} [multiline=false] Allow multiple lines of text
11105 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11106 * Use the #maxRows config to specify a maximum number of displayed rows.
11107 * @cfg {boolean} [maxRows=10] Maximum number of rows to display when #autosize is set to true.
11108 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
11109 * the value or placeholder text: `'before'` or `'after'`
11110 * @cfg {boolean} [required=false] Mark the field as required
11111 * @cfg {RegExp|string} [validate] Validation pattern, either a regular expression or the
11112 * symbolic name of a pattern defined by the class: 'non-empty' (the value cannot be an empty string)
11113 * or 'integer' (the value must contain only numbers).
11115 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
11116 // Configuration initialization
11117 config
= $.extend( {
11119 labelPosition
: 'after',
11123 // Parent constructor
11124 OO
.ui
.TextInputWidget
.super.call( this, config
);
11126 // Mixin constructors
11127 OO
.ui
.IconElement
.call( this, config
);
11128 OO
.ui
.IndicatorElement
.call( this, config
);
11129 OO
.ui
.PendingElement
.call( this, config
);
11130 OO
.ui
.LabelElement
.call( this, config
);
11133 this.readOnly
= false;
11134 this.multiline
= !!config
.multiline
;
11135 this.autosize
= !!config
.autosize
;
11136 this.maxRows
= config
.maxRows
;
11137 this.validate
= null;
11139 // Clone for resizing
11140 if ( this.autosize
) {
11141 this.$clone
= this.$input
11143 .insertAfter( this.$input
)
11144 .attr( 'aria-hidden', 'true' )
11145 .addClass( 'oo-ui-element-hidden' );
11148 this.setValidation( config
.validate
);
11149 this.setLabelPosition( config
.labelPosition
);
11153 keypress
: this.onKeyPress
.bind( this ),
11154 blur
: this.setValidityFlag
.bind( this )
11156 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
11157 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
11158 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
11159 this.on( 'labelChange', this.updatePosition
.bind( this ) );
11163 .addClass( 'oo-ui-textInputWidget' )
11164 .append( this.$icon
, this.$indicator
);
11165 this.setReadOnly( !!config
.readOnly
);
11166 if ( config
.placeholder
) {
11167 this.$input
.attr( 'placeholder', config
.placeholder
);
11169 if ( config
.maxLength
!== undefined ) {
11170 this.$input
.attr( 'maxlength', config
.maxLength
);
11172 if ( config
.autofocus
) {
11173 this.$input
.attr( 'autofocus', 'autofocus' );
11175 if ( config
.required
) {
11176 this.$input
.attr( 'required', 'true' );
11182 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
11183 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.IconElement
);
11184 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.IndicatorElement
);
11185 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.PendingElement
);
11186 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.LabelElement
);
11188 /* Static properties */
11190 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
11198 * An `enter` event is emitted when the user presses 'enter' inside the text box.
11200 * Not emitted if the input is multiline.
11208 * Handle icon mouse down events.
11211 * @param {jQuery.Event} e Mouse down event
11214 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
11215 if ( e
.which
=== 1 ) {
11216 this.$input
[ 0 ].focus();
11222 * Handle indicator mouse down events.
11225 * @param {jQuery.Event} e Mouse down event
11228 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
11229 if ( e
.which
=== 1 ) {
11230 this.$input
[ 0 ].focus();
11236 * Handle key press events.
11239 * @param {jQuery.Event} e Key press event
11240 * @fires enter If enter key is pressed and input is not multiline
11242 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
11243 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
11244 this.emit( 'enter', e
);
11249 * Handle element attach events.
11252 * @param {jQuery.Event} e Element attach event
11254 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
11255 // Any previously calculated size is now probably invalid if we reattached elsewhere
11256 this.valCache
= null;
11258 this.positionLabel();
11264 OO
.ui
.TextInputWidget
.prototype.onEdit = function () {
11268 return OO
.ui
.TextInputWidget
.super.prototype.onEdit
.call( this );
11274 OO
.ui
.TextInputWidget
.prototype.setValue = function ( value
) {
11276 OO
.ui
.TextInputWidget
.super.prototype.setValue
.call( this, value
);
11278 this.setValidityFlag();
11284 * Check if the input is {@link #readOnly read-only}.
11286 * @return {boolean}
11288 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
11289 return this.readOnly
;
11293 * Set the {@link #readOnly read-only} state of the input.
11295 * @param {boolean} state Make input read-only
11298 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
11299 this.readOnly
= !!state
;
11300 this.$input
.prop( 'readOnly', this.readOnly
);
11305 * Automatically adjust the size of the text input.
11307 * This only affects #multiline inputs that are {@link #autosize autosized}.
11311 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
11312 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
, idealHeight
;
11314 if ( this.multiline
&& this.autosize
&& this.$input
.val() !== this.valCache
) {
11316 .val( this.$input
.val() )
11317 .attr( 'rows', '' )
11318 // Set inline height property to 0 to measure scroll height
11319 .css( 'height', 0 );
11321 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11323 this.valCache
= this.$input
.val();
11325 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11327 // Remove inline height property to measure natural heights
11328 this.$clone
.css( 'height', '' );
11329 innerHeight
= this.$clone
.innerHeight();
11330 outerHeight
= this.$clone
.outerHeight();
11332 // Measure max rows height
11334 .attr( 'rows', this.maxRows
)
11335 .css( 'height', 'auto' )
11337 maxInnerHeight
= this.$clone
.innerHeight();
11339 // Difference between reported innerHeight and scrollHeight with no scrollbars present
11340 // Equals 1 on Blink-based browsers and 0 everywhere else
11341 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11342 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11344 this.$clone
.addClass( 'oo-ui-element-hidden' );
11346 // Only apply inline height when expansion beyond natural height is needed
11347 if ( idealHeight
> innerHeight
) {
11348 // Use the difference between the inner and outer height as a buffer
11349 this.$input
.css( 'height', idealHeight
+ ( outerHeight
- innerHeight
) );
11351 this.$input
.css( 'height', '' );
11361 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
11362 return config
.multiline
? $( '<textarea>' ) : $( '<input type="' + config
.type
+ '" />' );
11366 * Check if the input supports multiple lines.
11368 * @return {boolean}
11370 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
11371 return !!this.multiline
;
11375 * Check if the input automatically adjusts its size.
11377 * @return {boolean}
11379 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
11380 return !!this.autosize
;
11384 * Select the entire text of the input.
11388 OO
.ui
.TextInputWidget
.prototype.select = function () {
11389 this.$input
.select();
11394 * Set the validation pattern.
11396 * The validation pattern is either a regular expression or the symbolic name of a pattern
11397 * defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11398 * value must contain only numbers).
11400 * @param {RegExp|string|null} validate Regular expression or the symbolic name of a
11401 * pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11403 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
11404 if ( validate
instanceof RegExp
) {
11405 this.validate
= validate
;
11407 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
11412 * Sets the 'invalid' flag appropriately.
11414 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function () {
11416 this.isValid().done( function ( valid
) {
11417 widget
.setFlags( { invalid
: !valid
} );
11422 * Check if a value is valid.
11424 * This method returns a promise that resolves with a boolean `true` if the current value is
11425 * considered valid according to the supplied {@link #validate validation pattern}.
11427 * @return {jQuery.Deferred} A promise that resolves to a boolean `true` if the value is valid.
11429 OO
.ui
.TextInputWidget
.prototype.isValid = function () {
11430 return $.Deferred().resolve( !!this.getValue().match( this.validate
) ).promise();
11434 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11436 * @param {string} labelPosition Label position, 'before' or 'after'
11439 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
11440 this.labelPosition
= labelPosition
;
11441 this.updatePosition();
11446 * Deprecated alias of #setLabelPosition
11448 * @deprecated Use setLabelPosition instead.
11450 OO
.ui
.TextInputWidget
.prototype.setPosition
=
11451 OO
.ui
.TextInputWidget
.prototype.setLabelPosition
;
11454 * Update the position of the inline label.
11456 * This method is called by #setLabelPosition, and can also be called on its own if
11457 * something causes the label to be mispositioned.
11462 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
11463 var after
= this.labelPosition
=== 'after';
11466 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
11467 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
11469 if ( this.label
) {
11470 this.positionLabel();
11477 * Position the label by setting the correct padding on the input.
11482 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
11483 // Clear old values
11485 // Clear old values if present
11487 'padding-right': '',
11491 if ( this.label
) {
11492 this.$element
.append( this.$label
);
11494 this.$label
.detach();
11498 var after
= this.labelPosition
=== 'after',
11499 rtl
= this.$element
.css( 'direction' ) === 'rtl',
11500 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11502 this.$input
.css( property
, this.$label
.outerWidth( true ) );
11508 * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11509 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11510 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11512 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11513 * option, that option will appear to be selected.
11514 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11517 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
11520 * // Example: A ComboBoxWidget.
11521 * var comboBox=new OO.ui.ComboBoxWidget( {
11522 * label: 'ComboBoxWidget',
11523 * input: { value: 'Option One' },
11526 * new OO.ui.MenuOptionWidget( {
11527 * data: 'Option 1',
11528 * label: 'Option One' } ),
11529 * new OO.ui.MenuOptionWidget( {
11530 * data: 'Option 2',
11531 * label: 'Option Two' } ),
11532 * new OO.ui.MenuOptionWidget( {
11533 * data: 'Option 3',
11534 * label: 'Option Three'} ),
11535 * new OO.ui.MenuOptionWidget( {
11536 * data: 'Option 4',
11537 * label: 'Option Four' } ),
11538 * new OO.ui.MenuOptionWidget( {
11539 * data: 'Option 5',
11540 * label: 'Option Five' } )
11544 * $('body').append(comboBox.$element);
11546 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
11549 * @extends OO.ui.Widget
11550 * @mixins OO.ui.TabIndexedElement
11553 * @param {Object} [config] Configuration options
11554 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11555 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
11556 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11557 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11558 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11560 OO
.ui
.ComboBoxWidget
= function OoUiComboBoxWidget( config
) {
11561 // Configuration initialization
11562 config
= config
|| {};
11564 // Parent constructor
11565 OO
.ui
.ComboBoxWidget
.super.call( this, config
);
11567 // Properties (must be set before TabIndexedElement constructor call)
11568 this.$indicator
= this.$( '<span>' );
11570 // Mixin constructors
11571 OO
.ui
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$indicator
} ) );
11574 this.$overlay
= config
.$overlay
|| this.$element
;
11575 this.input
= new OO
.ui
.TextInputWidget( $.extend(
11578 $indicator
: this.$indicator
,
11579 disabled
: this.isDisabled()
11583 this.input
.$input
.eq( 0 ).attr( {
11585 'aria-autocomplete': 'list'
11587 this.menu
= new OO
.ui
.TextInputMenuSelectWidget( this.input
, $.extend(
11591 disabled
: this.isDisabled()
11597 this.$indicator
.on( {
11598 click
: this.onClick
.bind( this ),
11599 keypress
: this.onKeyPress
.bind( this )
11601 this.input
.connect( this, {
11602 change
: 'onInputChange',
11603 enter
: 'onInputEnter'
11605 this.menu
.connect( this, {
11606 choose
: 'onMenuChoose',
11607 add
: 'onMenuItemsChange',
11608 remove
: 'onMenuItemsChange'
11612 this.$element
.addClass( 'oo-ui-comboBoxWidget' ).append( this.input
.$element
);
11613 this.$overlay
.append( this.menu
.$element
);
11614 this.onMenuItemsChange();
11619 OO
.inheritClass( OO
.ui
.ComboBoxWidget
, OO
.ui
.Widget
);
11620 OO
.mixinClass( OO
.ui
.ComboBoxWidget
, OO
.ui
.TabIndexedElement
);
11625 * Get the combobox's menu.
11626 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
11628 OO
.ui
.ComboBoxWidget
.prototype.getMenu = function () {
11633 * Handle input change events.
11636 * @param {string} value New value
11638 OO
.ui
.ComboBoxWidget
.prototype.onInputChange = function ( value
) {
11639 var match
= this.menu
.getItemFromData( value
);
11641 this.menu
.selectItem( match
);
11642 if ( this.menu
.getHighlightedItem() ) {
11643 this.menu
.highlightItem( match
);
11646 if ( !this.isDisabled() ) {
11647 this.menu
.toggle( true );
11652 * Handle mouse click events.
11656 * @param {jQuery.Event} e Mouse click event
11658 OO
.ui
.ComboBoxWidget
.prototype.onClick = function ( e
) {
11659 if ( !this.isDisabled() && e
.which
=== 1 ) {
11660 this.menu
.toggle();
11661 this.input
.$input
[ 0 ].focus();
11667 * Handle key press events.
11671 * @param {jQuery.Event} e Key press event
11673 OO
.ui
.ComboBoxWidget
.prototype.onKeyPress = function ( e
) {
11674 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
11675 this.menu
.toggle();
11676 this.input
.$input
[ 0 ].focus();
11682 * Handle input enter events.
11686 OO
.ui
.ComboBoxWidget
.prototype.onInputEnter = function () {
11687 if ( !this.isDisabled() ) {
11688 this.menu
.toggle( false );
11693 * Handle menu choose events.
11696 * @param {OO.ui.OptionWidget} item Chosen item
11698 OO
.ui
.ComboBoxWidget
.prototype.onMenuChoose = function ( item
) {
11700 this.input
.setValue( item
.getData() );
11705 * Handle menu item change events.
11709 OO
.ui
.ComboBoxWidget
.prototype.onMenuItemsChange = function () {
11710 var match
= this.menu
.getItemFromData( this.input
.getValue() );
11711 this.menu
.selectItem( match
);
11712 if ( this.menu
.getHighlightedItem() ) {
11713 this.menu
.highlightItem( match
);
11715 this.$element
.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu
.isEmpty() );
11721 OO
.ui
.ComboBoxWidget
.prototype.setDisabled = function ( disabled
) {
11723 OO
.ui
.ComboBoxWidget
.super.prototype.setDisabled
.call( this, disabled
);
11725 if ( this.input
) {
11726 this.input
.setDisabled( this.isDisabled() );
11729 this.menu
.setDisabled( this.isDisabled() );
11736 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
11737 * be configured with a `label` option that is set to a string, a label node, or a function:
11739 * - String: a plaintext string
11740 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
11741 * label that includes a link or special styling, such as a gray color or additional graphical elements.
11742 * - Function: a function that will produce a string in the future. Functions are used
11743 * in cases where the value of the label is not currently defined.
11745 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
11746 * will come into focus when the label is clicked.
11749 * // Examples of LabelWidgets
11750 * var label1 = new OO.ui.LabelWidget({
11751 * label: 'plaintext label'
11753 * var label2 = new OO.ui.LabelWidget({
11754 * label: $( '<a href="default.html">jQuery label</a>' )
11756 * // Create a fieldset layout with fields for each example
11757 * var fieldset = new OO.ui.FieldsetLayout( );
11758 * fieldset.addItems( [
11759 * new OO.ui.FieldLayout( label1 ),
11760 * new OO.ui.FieldLayout( label2 )
11762 * $( 'body' ).append( fieldset.$element );
11766 * @extends OO.ui.Widget
11767 * @mixins OO.ui.LabelElement
11770 * @param {Object} [config] Configuration options
11771 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
11772 * Clicking the label will focus the specified input field.
11774 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
11775 // Configuration initialization
11776 config
= config
|| {};
11778 // Parent constructor
11779 OO
.ui
.LabelWidget
.super.call( this, config
);
11781 // Mixin constructors
11782 OO
.ui
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
11783 OO
.ui
.TitledElement
.call( this, config
);
11786 this.input
= config
.input
;
11789 if ( this.input
instanceof OO
.ui
.InputWidget
) {
11790 this.$element
.on( 'click', this.onClick
.bind( this ) );
11794 this.$element
.addClass( 'oo-ui-labelWidget' );
11799 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
11800 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.LabelElement
);
11801 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.TitledElement
);
11803 /* Static Properties */
11805 OO
.ui
.LabelWidget
.static.tagName
= 'span';
11810 * Handles label mouse click events.
11813 * @param {jQuery.Event} e Mouse click event
11815 OO
.ui
.LabelWidget
.prototype.onClick = function () {
11816 this.input
.simulateLabelClick();
11821 * OptionWidgets are special elements that can be selected and configured with data. The
11822 * data is often unique for each option, but it does not have to be. OptionWidgets are used
11823 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
11824 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
11826 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
11829 * @extends OO.ui.Widget
11830 * @mixins OO.ui.LabelElement
11831 * @mixins OO.ui.FlaggedElement
11834 * @param {Object} [config] Configuration options
11836 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
11837 // Configuration initialization
11838 config
= config
|| {};
11840 // Parent constructor
11841 OO
.ui
.OptionWidget
.super.call( this, config
);
11843 // Mixin constructors
11844 OO
.ui
.ItemWidget
.call( this );
11845 OO
.ui
.LabelElement
.call( this, config
);
11846 OO
.ui
.FlaggedElement
.call( this, config
);
11849 this.selected
= false;
11850 this.highlighted
= false;
11851 this.pressed
= false;
11855 .data( 'oo-ui-optionWidget', this )
11856 .attr( 'role', 'option' )
11857 .addClass( 'oo-ui-optionWidget' )
11858 .append( this.$label
);
11863 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
11864 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.ItemWidget
);
11865 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.LabelElement
);
11866 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.FlaggedElement
);
11868 /* Static Properties */
11870 OO
.ui
.OptionWidget
.static.selectable
= true;
11872 OO
.ui
.OptionWidget
.static.highlightable
= true;
11874 OO
.ui
.OptionWidget
.static.pressable
= true;
11876 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
11881 * Check if the option can be selected.
11883 * @return {boolean} Item is selectable
11885 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
11886 return this.constructor.static.selectable
&& !this.isDisabled();
11890 * Check if the option can be highlighted. A highlight indicates that the option
11891 * may be selected when a user presses enter or clicks. Disabled items cannot
11894 * @return {boolean} Item is highlightable
11896 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
11897 return this.constructor.static.highlightable
&& !this.isDisabled();
11901 * Check if the option can be pressed. The pressed state occurs when a user mouses
11902 * down on an item, but has not yet let go of the mouse.
11904 * @return {boolean} Item is pressable
11906 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
11907 return this.constructor.static.pressable
&& !this.isDisabled();
11911 * Check if the option is selected.
11913 * @return {boolean} Item is selected
11915 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
11916 return this.selected
;
11920 * Check if the option is highlighted. A highlight indicates that the
11921 * item may be selected when a user presses enter or clicks.
11923 * @return {boolean} Item is highlighted
11925 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
11926 return this.highlighted
;
11930 * Check if the option is pressed. The pressed state occurs when a user mouses
11931 * down on an item, but has not yet let go of the mouse. The item may appear
11932 * selected, but it will not be selected until the user releases the mouse.
11934 * @return {boolean} Item is pressed
11936 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
11937 return this.pressed
;
11941 * Set the option’s selected state. In general, all modifications to the selection
11942 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
11943 * method instead of this method.
11945 * @param {boolean} [state=false] Select option
11948 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
11949 if ( this.constructor.static.selectable
) {
11950 this.selected
= !!state
;
11952 .toggleClass( 'oo-ui-optionWidget-selected', state
)
11953 .attr( 'aria-selected', state
.toString() );
11954 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
11955 this.scrollElementIntoView();
11957 this.updateThemeClasses();
11963 * Set the option’s highlighted state. In general, all programmatic
11964 * modifications to the highlight should be handled by the
11965 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
11966 * method instead of this method.
11968 * @param {boolean} [state=false] Highlight option
11971 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
11972 if ( this.constructor.static.highlightable
) {
11973 this.highlighted
= !!state
;
11974 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
11975 this.updateThemeClasses();
11981 * Set the option’s pressed state. In general, all
11982 * programmatic modifications to the pressed state should be handled by the
11983 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
11984 * method instead of this method.
11986 * @param {boolean} [state=false] Press option
11989 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
11990 if ( this.constructor.static.pressable
) {
11991 this.pressed
= !!state
;
11992 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
11993 this.updateThemeClasses();
11999 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
12000 * with an {@link OO.ui.IconElement icon} and/or {@link OO.ui.IndicatorElement indicator}.
12001 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
12002 * options. For more information about options and selects, please see the
12003 * [OOjs UI documentation on MediaWiki][1].
12006 * // Decorated options in a select widget
12007 * var select=new OO.ui.SelectWidget( {
12009 * new OO.ui.DecoratedOptionWidget( {
12011 * label: 'Option with icon',
12014 * new OO.ui.DecoratedOptionWidget( {
12016 * label: 'Option with indicator',
12017 * indicator: 'next'
12021 * $('body').append(select.$element);
12023 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12026 * @extends OO.ui.OptionWidget
12027 * @mixins OO.ui.IconElement
12028 * @mixins OO.ui.IndicatorElement
12031 * @param {Object} [config] Configuration options
12033 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
12034 // Parent constructor
12035 OO
.ui
.DecoratedOptionWidget
.super.call( this, config
);
12037 // Mixin constructors
12038 OO
.ui
.IconElement
.call( this, config
);
12039 OO
.ui
.IndicatorElement
.call( this, config
);
12043 .addClass( 'oo-ui-decoratedOptionWidget' )
12044 .prepend( this.$icon
)
12045 .append( this.$indicator
);
12050 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
12051 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.IconElement
);
12052 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.IndicatorElement
);
12055 * ButtonOptionWidget is a special type of {@link OO.ui.ButtonElement button element} that
12056 * can be selected and configured with data. The class is
12057 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
12058 * [OOjs UI documentation on MediaWiki] [1] for more information.
12060 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
12063 * @extends OO.ui.DecoratedOptionWidget
12064 * @mixins OO.ui.ButtonElement
12065 * @mixins OO.ui.TabIndexedElement
12068 * @param {Object} [config] Configuration options
12070 OO
.ui
.ButtonOptionWidget
= function OoUiButtonOptionWidget( config
) {
12071 // Configuration initialization
12072 config
= $.extend( { tabIndex
: -1 }, config
);
12074 // Parent constructor
12075 OO
.ui
.ButtonOptionWidget
.super.call( this, config
);
12077 // Mixin constructors
12078 OO
.ui
.ButtonElement
.call( this, config
);
12079 OO
.ui
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
12082 this.$element
.addClass( 'oo-ui-buttonOptionWidget' );
12083 this.$button
.append( this.$element
.contents() );
12084 this.$element
.append( this.$button
);
12089 OO
.inheritClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
12090 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.ButtonElement
);
12091 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.TabIndexedElement
);
12093 /* Static Properties */
12095 // Allow button mouse down events to pass through so they can be handled by the parent select widget
12096 OO
.ui
.ButtonOptionWidget
.static.cancelButtonMouseDownEvents
= false;
12098 OO
.ui
.ButtonOptionWidget
.static.highlightable
= false;
12105 OO
.ui
.ButtonOptionWidget
.prototype.setSelected = function ( state
) {
12106 OO
.ui
.ButtonOptionWidget
.super.prototype.setSelected
.call( this, state
);
12108 if ( this.constructor.static.selectable
) {
12109 this.setActive( state
);
12116 * RadioOptionWidget is an option widget that looks like a radio button.
12117 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
12118 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
12120 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
12123 * @extends OO.ui.OptionWidget
12126 * @param {Object} [config] Configuration options
12128 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
12129 // Configuration initialization
12130 config
= config
|| {};
12132 // Properties (must be done before parent constructor which calls #setDisabled)
12133 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
12135 // Parent constructor
12136 OO
.ui
.RadioOptionWidget
.super.call( this, config
);
12140 .addClass( 'oo-ui-radioOptionWidget' )
12141 .prepend( this.radio
.$element
);
12146 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
12148 /* Static Properties */
12150 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
12152 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
12154 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
12156 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
12163 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
12164 OO
.ui
.RadioOptionWidget
.super.prototype.setSelected
.call( this, state
);
12166 this.radio
.setSelected( state
);
12174 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
12175 OO
.ui
.RadioOptionWidget
.super.prototype.setDisabled
.call( this, disabled
);
12177 this.radio
.setDisabled( this.isDisabled() );
12183 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
12184 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
12185 * the [OOjs UI documentation on MediaWiki] [1] for more information.
12187 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12190 * @extends OO.ui.DecoratedOptionWidget
12193 * @param {Object} [config] Configuration options
12195 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
12196 // Configuration initialization
12197 config
= $.extend( { icon
: 'check' }, config
);
12199 // Parent constructor
12200 OO
.ui
.MenuOptionWidget
.super.call( this, config
);
12204 .attr( 'role', 'menuitem' )
12205 .addClass( 'oo-ui-menuOptionWidget' );
12210 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
12212 /* Static Properties */
12214 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
12217 * Section to group one or more items in a OO.ui.MenuSelectWidget.
12220 * @extends OO.ui.DecoratedOptionWidget
12223 * @param {Object} [config] Configuration options
12225 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
12226 // Parent constructor
12227 OO
.ui
.MenuSectionOptionWidget
.super.call( this, config
);
12230 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' );
12235 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
12237 /* Static Properties */
12239 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
12241 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
12244 * Items for an OO.ui.OutlineSelectWidget.
12247 * @extends OO.ui.DecoratedOptionWidget
12250 * @param {Object} [config] Configuration options
12251 * @cfg {number} [level] Indentation level
12252 * @cfg {boolean} [movable] Allow modification from outline controls
12254 OO
.ui
.OutlineOptionWidget
= function OoUiOutlineOptionWidget( config
) {
12255 // Configuration initialization
12256 config
= config
|| {};
12258 // Parent constructor
12259 OO
.ui
.OutlineOptionWidget
.super.call( this, config
);
12263 this.movable
= !!config
.movable
;
12264 this.removable
= !!config
.removable
;
12267 this.$element
.addClass( 'oo-ui-outlineOptionWidget' );
12268 this.setLevel( config
.level
);
12273 OO
.inheritClass( OO
.ui
.OutlineOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
12275 /* Static Properties */
12277 OO
.ui
.OutlineOptionWidget
.static.highlightable
= false;
12279 OO
.ui
.OutlineOptionWidget
.static.scrollIntoViewOnSelect
= true;
12281 OO
.ui
.OutlineOptionWidget
.static.levelClass
= 'oo-ui-outlineOptionWidget-level-';
12283 OO
.ui
.OutlineOptionWidget
.static.levels
= 3;
12288 * Check if item is movable.
12290 * Movability is used by outline controls.
12292 * @return {boolean} Item is movable
12294 OO
.ui
.OutlineOptionWidget
.prototype.isMovable = function () {
12295 return this.movable
;
12299 * Check if item is removable.
12301 * Removability is used by outline controls.
12303 * @return {boolean} Item is removable
12305 OO
.ui
.OutlineOptionWidget
.prototype.isRemovable = function () {
12306 return this.removable
;
12310 * Get indentation level.
12312 * @return {number} Indentation level
12314 OO
.ui
.OutlineOptionWidget
.prototype.getLevel = function () {
12321 * Movability is used by outline controls.
12323 * @param {boolean} movable Item is movable
12326 OO
.ui
.OutlineOptionWidget
.prototype.setMovable = function ( movable
) {
12327 this.movable
= !!movable
;
12328 this.updateThemeClasses();
12333 * Set removability.
12335 * Removability is used by outline controls.
12337 * @param {boolean} movable Item is removable
12340 OO
.ui
.OutlineOptionWidget
.prototype.setRemovable = function ( removable
) {
12341 this.removable
= !!removable
;
12342 this.updateThemeClasses();
12347 * Set indentation level.
12349 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
12352 OO
.ui
.OutlineOptionWidget
.prototype.setLevel = function ( level
) {
12353 var levels
= this.constructor.static.levels
,
12354 levelClass
= this.constructor.static.levelClass
,
12357 this.level
= level
? Math
.max( 0, Math
.min( levels
- 1, level
) ) : 0;
12359 if ( this.level
=== i
) {
12360 this.$element
.addClass( levelClass
+ i
);
12362 this.$element
.removeClass( levelClass
+ i
);
12365 this.updateThemeClasses();
12371 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
12372 * By default, each popup has an anchor that points toward its origin.
12373 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
12376 * // A popup widget.
12377 * var popup=new OO.ui.PopupWidget({
12378 * $content: $( '<p>Hi there!</p>' ),
12383 * $('body').append(popup.$element);
12384 * // To display the popup, toggle the visibility to 'true'.
12385 * popup.toggle(true);
12387 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
12390 * @extends OO.ui.Widget
12391 * @mixins OO.ui.LabelElement
12394 * @param {Object} [config] Configuration options
12395 * @cfg {number} [width=320] Width of popup in pixels
12396 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
12397 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
12398 * @cfg {string} [align='center'] Alignment of the popup: `center`, `left`, or `right`.
12399 * If the popup is right-aligned, the right edge of the popup is aligned to the anchor.
12400 * For left-aligned popups, the left edge is aligned to the anchor.
12401 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
12402 * See the [OOjs UI docs on MediaWiki][3] for an example.
12403 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
12404 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
12405 * @cfg {jQuery} [$content] Content to append to the popup's body
12406 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
12407 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
12408 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
12410 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
12411 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
12413 * @cfg {boolean} [padded] Add padding to the popup's body
12415 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
12416 // Configuration initialization
12417 config
= config
|| {};
12419 // Parent constructor
12420 OO
.ui
.PopupWidget
.super.call( this, config
);
12422 // Properties (must be set before ClippableElement constructor call)
12423 this.$body
= $( '<div>' );
12425 // Mixin constructors
12426 OO
.ui
.LabelElement
.call( this, config
);
12427 OO
.ui
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$body
} ) );
12430 this.$popup
= $( '<div>' );
12431 this.$head
= $( '<div>' );
12432 this.$anchor
= $( '<div>' );
12433 // If undefined, will be computed lazily in updateDimensions()
12434 this.$container
= config
.$container
;
12435 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
12436 this.autoClose
= !!config
.autoClose
;
12437 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
12438 this.transitionTimeout
= null;
12439 this.anchor
= null;
12440 this.width
= config
.width
!== undefined ? config
.width
: 320;
12441 this.height
= config
.height
!== undefined ? config
.height
: null;
12442 this.align
= config
.align
|| 'center';
12443 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
12444 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
12445 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
12448 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
12451 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
12452 this.$body
.addClass( 'oo-ui-popupWidget-body' );
12453 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
12455 .addClass( 'oo-ui-popupWidget-head' )
12456 .append( this.$label
, this.closeButton
.$element
);
12457 if ( !config
.head
) {
12458 this.$head
.addClass( 'oo-ui-element-hidden' );
12461 .addClass( 'oo-ui-popupWidget-popup' )
12462 .append( this.$head
, this.$body
);
12464 .addClass( 'oo-ui-popupWidget' )
12465 .append( this.$popup
, this.$anchor
);
12466 // Move content, which was added to #$element by OO.ui.Widget, to the body
12467 if ( config
.$content
instanceof jQuery
) {
12468 this.$body
.append( config
.$content
);
12470 if ( config
.padded
) {
12471 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
12474 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
12475 // that reference properties not initialized at that time of parent class construction
12476 // TODO: Find a better way to handle post-constructor setup
12477 this.visible
= false;
12478 this.$element
.addClass( 'oo-ui-element-hidden' );
12483 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
12484 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.LabelElement
);
12485 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.ClippableElement
);
12490 * Handles mouse down events.
12493 * @param {MouseEvent} e Mouse down event
12495 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
12497 this.isVisible() &&
12498 !$.contains( this.$element
[ 0 ], e
.target
) &&
12499 ( !this.$autoCloseIgnore
|| !this.$autoCloseIgnore
.has( e
.target
).length
)
12501 this.toggle( false );
12506 * Bind mouse down listener.
12510 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
12511 // Capture clicks outside popup
12512 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
12516 * Handles close button click events.
12520 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
12521 if ( this.isVisible() ) {
12522 this.toggle( false );
12527 * Unbind mouse down listener.
12531 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
12532 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
12536 * Handles key down events.
12539 * @param {KeyboardEvent} e Key down event
12541 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
12543 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
12546 this.toggle( false );
12547 e
.preventDefault();
12548 e
.stopPropagation();
12553 * Bind key down listener.
12557 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
12558 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
12562 * Unbind key down listener.
12566 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
12567 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
12571 * Show, hide, or toggle the visibility of the anchor.
12573 * @param {boolean} [show] Show anchor, omit to toggle
12575 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
12576 show
= show
=== undefined ? !this.anchored
: !!show
;
12578 if ( this.anchored
!== show
) {
12580 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
12582 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
12584 this.anchored
= show
;
12589 * Check if the anchor is visible.
12591 * @return {boolean} Anchor is visible
12593 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
12594 return this.anchor
;
12600 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
12601 show
= show
=== undefined ? !this.isVisible() : !!show
;
12603 var change
= show
!== this.isVisible();
12606 OO
.ui
.PopupWidget
.super.prototype.toggle
.call( this, show
);
12610 if ( this.autoClose
) {
12611 this.bindMouseDownListener();
12612 this.bindKeyDownListener();
12614 this.updateDimensions();
12615 this.toggleClipping( true );
12617 this.toggleClipping( false );
12618 if ( this.autoClose
) {
12619 this.unbindMouseDownListener();
12620 this.unbindKeyDownListener();
12629 * Set the size of the popup.
12631 * Changing the size may also change the popup's position depending on the alignment.
12633 * @param {number} width Width in pixels
12634 * @param {number} height Height in pixels
12635 * @param {boolean} [transition=false] Use a smooth transition
12638 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
12639 this.width
= width
;
12640 this.height
= height
!== undefined ? height
: null;
12641 if ( this.isVisible() ) {
12642 this.updateDimensions( transition
);
12647 * Update the size and position.
12649 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
12650 * be called automatically.
12652 * @param {boolean} [transition=false] Use a smooth transition
12655 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
12656 var popupOffset
, originOffset
, containerLeft
, containerWidth
, containerRight
,
12657 popupLeft
, popupRight
, overlapLeft
, overlapRight
, anchorWidth
,
12660 if ( !this.$container
) {
12661 // Lazy-initialize $container if not specified in constructor
12662 this.$container
= $( this.getClosestScrollableElementContainer() );
12665 // Set height and width before measuring things, since it might cause our measurements
12666 // to change (e.g. due to scrollbars appearing or disappearing)
12669 height
: this.height
!== null ? this.height
: 'auto'
12672 // Compute initial popupOffset based on alignment
12673 popupOffset
= this.width
* ( { left
: 0, center
: -0.5, right
: -1 } )[ this.align
];
12675 // Figure out if this will cause the popup to go beyond the edge of the container
12676 originOffset
= this.$element
.offset().left
;
12677 containerLeft
= this.$container
.offset().left
;
12678 containerWidth
= this.$container
.innerWidth();
12679 containerRight
= containerLeft
+ containerWidth
;
12680 popupLeft
= popupOffset
- this.containerPadding
;
12681 popupRight
= popupOffset
+ this.containerPadding
+ this.width
+ this.containerPadding
;
12682 overlapLeft
= ( originOffset
+ popupLeft
) - containerLeft
;
12683 overlapRight
= containerRight
- ( originOffset
+ popupRight
);
12685 // Adjust offset to make the popup not go beyond the edge, if needed
12686 if ( overlapRight
< 0 ) {
12687 popupOffset
+= overlapRight
;
12688 } else if ( overlapLeft
< 0 ) {
12689 popupOffset
-= overlapLeft
;
12692 // Adjust offset to avoid anchor being rendered too close to the edge
12693 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
12694 // TODO: Find a measurement that works for CSS anchors and image anchors
12695 anchorWidth
= this.$anchor
[ 0 ].scrollWidth
* 2;
12696 if ( popupOffset
+ this.width
< anchorWidth
) {
12697 popupOffset
= anchorWidth
- this.width
;
12698 } else if ( -popupOffset
< anchorWidth
) {
12699 popupOffset
= -anchorWidth
;
12702 // Prevent transition from being interrupted
12703 clearTimeout( this.transitionTimeout
);
12704 if ( transition
) {
12705 // Enable transition
12706 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
12709 // Position body relative to anchor
12710 this.$popup
.css( 'margin-left', popupOffset
);
12712 if ( transition
) {
12713 // Prevent transitioning after transition is complete
12714 this.transitionTimeout
= setTimeout( function () {
12715 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
12718 // Prevent transitioning immediately
12719 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
12722 // Reevaluate clipping state since we've relocated and resized the popup
12729 * Progress bars visually display the status of an operation, such as a download,
12730 * and can be either determinate or indeterminate:
12732 * - **determinate** process bars show the percent of an operation that is complete.
12734 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
12735 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
12736 * not use percentages.
12738 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
12741 * // Examples of determinate and indeterminate progress bars.
12742 * var progressBar1=new OO.ui.ProgressBarWidget( {
12746 * var progressBar2=new OO.ui.ProgressBarWidget( {
12749 * // Create a FieldsetLayout to layout progress bars
12750 * var fieldset = new OO.ui.FieldsetLayout;
12751 * fieldset.addItems( [
12752 * new OO.ui.FieldLayout( progressBar1, {label : 'Determinate', align : 'top'}),
12753 * new OO.ui.FieldLayout( progressBar2, {label : 'Indeterminate', align : 'top'})
12755 * $( 'body' ).append( fieldset.$element );
12758 * @extends OO.ui.Widget
12761 * @param {Object} [config] Configuration options
12762 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
12763 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
12764 * By default, the progress bar is indeterminate.
12766 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
12767 // Configuration initialization
12768 config
= config
|| {};
12770 // Parent constructor
12771 OO
.ui
.ProgressBarWidget
.super.call( this, config
);
12774 this.$bar
= $( '<div>' );
12775 this.progress
= null;
12778 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
12779 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
12782 role
: 'progressbar',
12783 'aria-valuemin': 0,
12784 'aria-valuemax': 100
12786 .addClass( 'oo-ui-progressBarWidget' )
12787 .append( this.$bar
);
12792 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
12794 /* Static Properties */
12796 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
12801 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
12803 * @return {number|boolean} Progress percent
12805 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
12806 return this.progress
;
12810 * Set the percent of the process completed or `false` for an indeterminate process.
12812 * @param {number|boolean} progress Progress percent or `false` for indeterminate
12814 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
12815 this.progress
= progress
;
12817 if ( progress
!== false ) {
12818 this.$bar
.css( 'width', this.progress
+ '%' );
12819 this.$element
.attr( 'aria-valuenow', this.progress
);
12821 this.$bar
.css( 'width', '' );
12822 this.$element
.removeAttr( 'aria-valuenow' );
12824 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress
);
12830 * Search widgets combine a query input, placed above, and a results selection widget, placed below.
12831 * Results are cleared and populated each time the query is changed.
12834 * @extends OO.ui.Widget
12837 * @param {Object} [config] Configuration options
12838 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
12839 * @cfg {string} [value] Initial query value
12841 OO
.ui
.SearchWidget
= function OoUiSearchWidget( config
) {
12842 // Configuration initialization
12843 config
= config
|| {};
12845 // Parent constructor
12846 OO
.ui
.SearchWidget
.super.call( this, config
);
12849 this.query
= new OO
.ui
.TextInputWidget( {
12851 placeholder
: config
.placeholder
,
12852 value
: config
.value
12854 this.results
= new OO
.ui
.SelectWidget();
12855 this.$query
= $( '<div>' );
12856 this.$results
= $( '<div>' );
12859 this.query
.connect( this, {
12860 change
: 'onQueryChange',
12861 enter
: 'onQueryEnter'
12863 this.results
.connect( this, {
12864 highlight
: 'onResultsHighlight',
12865 select
: 'onResultsSelect'
12867 this.query
.$input
.on( 'keydown', this.onQueryKeydown
.bind( this ) );
12871 .addClass( 'oo-ui-searchWidget-query' )
12872 .append( this.query
.$element
);
12874 .addClass( 'oo-ui-searchWidget-results' )
12875 .append( this.results
.$element
);
12877 .addClass( 'oo-ui-searchWidget' )
12878 .append( this.$results
, this.$query
);
12883 OO
.inheritClass( OO
.ui
.SearchWidget
, OO
.ui
.Widget
);
12889 * @param {Object|null} item Item data or null if no item is highlighted
12894 * @param {Object|null} item Item data or null if no item is selected
12900 * Handle query key down events.
12902 * @param {jQuery.Event} e Key down event
12904 OO
.ui
.SearchWidget
.prototype.onQueryKeydown = function ( e
) {
12905 var highlightedItem
, nextItem
,
12906 dir
= e
.which
=== OO
.ui
.Keys
.DOWN
? 1 : ( e
.which
=== OO
.ui
.Keys
.UP
? -1 : 0 );
12909 highlightedItem
= this.results
.getHighlightedItem();
12910 if ( !highlightedItem
) {
12911 highlightedItem
= this.results
.getSelectedItem();
12913 nextItem
= this.results
.getRelativeSelectableItem( highlightedItem
, dir
);
12914 this.results
.highlightItem( nextItem
);
12915 nextItem
.scrollElementIntoView();
12920 * Handle select widget select events.
12922 * Clears existing results. Subclasses should repopulate items according to new query.
12924 * @param {string} value New value
12926 OO
.ui
.SearchWidget
.prototype.onQueryChange = function () {
12928 this.results
.clearItems();
12932 * Handle select widget enter key events.
12934 * Selects highlighted item.
12936 * @param {string} value New value
12938 OO
.ui
.SearchWidget
.prototype.onQueryEnter = function () {
12940 this.results
.selectItem( this.results
.getHighlightedItem() );
12944 * Handle select widget highlight events.
12946 * @param {OO.ui.OptionWidget} item Highlighted item
12949 OO
.ui
.SearchWidget
.prototype.onResultsHighlight = function ( item
) {
12950 this.emit( 'highlight', item
? item
.getData() : null );
12954 * Handle select widget select events.
12956 * @param {OO.ui.OptionWidget} item Selected item
12959 OO
.ui
.SearchWidget
.prototype.onResultsSelect = function ( item
) {
12960 this.emit( 'select', item
? item
.getData() : null );
12964 * Get the query input.
12966 * @return {OO.ui.TextInputWidget} Query input
12968 OO
.ui
.SearchWidget
.prototype.getQuery = function () {
12973 * Get the results list.
12975 * @return {OO.ui.SelectWidget} Select list
12977 OO
.ui
.SearchWidget
.prototype.getResults = function () {
12978 return this.results
;
12982 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
12983 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
12984 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
12987 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
12988 * information, please see the [OOjs UI documentation on MediaWiki][1].
12991 * // Example of a select widget with three options
12992 * var select=new OO.ui.SelectWidget( {
12994 * new OO.ui.OptionWidget( {
12996 * label: 'Option One',
12998 * new OO.ui.OptionWidget( {
13000 * label: 'Option Two',
13002 * new OO.ui.OptionWidget( {
13004 * label: 'Option Three',
13008 * $('body').append(select.$element);
13010 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13013 * @extends OO.ui.Widget
13014 * @mixins OO.ui.GroupElement
13017 * @param {Object} [config] Configuration options
13018 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
13019 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
13020 * the [OOjs UI documentation on MediaWiki] [2] for examples.
13021 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13023 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
13024 // Configuration initialization
13025 config
= config
|| {};
13027 // Parent constructor
13028 OO
.ui
.SelectWidget
.super.call( this, config
);
13030 // Mixin constructors
13031 OO
.ui
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
13034 this.pressed
= false;
13035 this.selecting
= null;
13036 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
13037 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
13038 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
13041 this.$element
.on( {
13042 mousedown
: this.onMouseDown
.bind( this ),
13043 mouseover
: this.onMouseOver
.bind( this ),
13044 mouseleave
: this.onMouseLeave
.bind( this )
13049 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
13050 .attr( 'role', 'listbox' );
13051 if ( Array
.isArray( config
.items
) ) {
13052 this.addItems( config
.items
);
13058 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
13060 // Need to mixin base class as well
13061 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.GroupElement
);
13062 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.GroupWidget
);
13069 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
13071 * @param {OO.ui.OptionWidget|null} item Highlighted item
13077 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
13078 * pressed state of an option.
13080 * @param {OO.ui.OptionWidget|null} item Pressed item
13086 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
13088 * @param {OO.ui.OptionWidget|null} item Selected item
13093 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
13094 * @param {OO.ui.OptionWidget|null} item Chosen item
13100 * An `add` event is emitted when options are added to the select with the #addItems method.
13102 * @param {OO.ui.OptionWidget[]} items Added items
13103 * @param {number} index Index of insertion point
13109 * A `remove` event is emitted when options are removed from the select with the #clearItems
13110 * or #removeItems methods.
13112 * @param {OO.ui.OptionWidget[]} items Removed items
13118 * Handle mouse down events.
13121 * @param {jQuery.Event} e Mouse down event
13123 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
13126 if ( !this.isDisabled() && e
.which
=== 1 ) {
13127 this.togglePressed( true );
13128 item
= this.getTargetItem( e
);
13129 if ( item
&& item
.isSelectable() ) {
13130 this.pressItem( item
);
13131 this.selecting
= item
;
13132 this.getElementDocument().addEventListener(
13134 this.onMouseUpHandler
,
13137 this.getElementDocument().addEventListener(
13139 this.onMouseMoveHandler
,
13148 * Handle mouse up events.
13151 * @param {jQuery.Event} e Mouse up event
13153 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
13156 this.togglePressed( false );
13157 if ( !this.selecting
) {
13158 item
= this.getTargetItem( e
);
13159 if ( item
&& item
.isSelectable() ) {
13160 this.selecting
= item
;
13163 if ( !this.isDisabled() && e
.which
=== 1 && this.selecting
) {
13164 this.pressItem( null );
13165 this.chooseItem( this.selecting
);
13166 this.selecting
= null;
13169 this.getElementDocument().removeEventListener(
13171 this.onMouseUpHandler
,
13174 this.getElementDocument().removeEventListener(
13176 this.onMouseMoveHandler
,
13184 * Handle mouse move events.
13187 * @param {jQuery.Event} e Mouse move event
13189 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
13192 if ( !this.isDisabled() && this.pressed
) {
13193 item
= this.getTargetItem( e
);
13194 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
13195 this.pressItem( item
);
13196 this.selecting
= item
;
13203 * Handle mouse over events.
13206 * @param {jQuery.Event} e Mouse over event
13208 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
13211 if ( !this.isDisabled() ) {
13212 item
= this.getTargetItem( e
);
13213 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
13219 * Handle mouse leave events.
13222 * @param {jQuery.Event} e Mouse over event
13224 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
13225 if ( !this.isDisabled() ) {
13226 this.highlightItem( null );
13232 * Handle key down events.
13235 * @param {jQuery.Event} e Key down event
13237 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
13240 currentItem
= this.getHighlightedItem() || this.getSelectedItem();
13242 if ( !this.isDisabled() && this.isVisible() ) {
13243 switch ( e
.keyCode
) {
13244 case OO
.ui
.Keys
.ENTER
:
13245 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
13246 // Was only highlighted, now let's select it. No-op if already selected.
13247 this.chooseItem( currentItem
);
13251 case OO
.ui
.Keys
.UP
:
13252 case OO
.ui
.Keys
.LEFT
:
13253 nextItem
= this.getRelativeSelectableItem( currentItem
, -1 );
13256 case OO
.ui
.Keys
.DOWN
:
13257 case OO
.ui
.Keys
.RIGHT
:
13258 nextItem
= this.getRelativeSelectableItem( currentItem
, 1 );
13261 case OO
.ui
.Keys
.ESCAPE
:
13262 case OO
.ui
.Keys
.TAB
:
13263 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
13264 currentItem
.setHighlighted( false );
13266 this.unbindKeyDownListener();
13267 // Don't prevent tabbing away / defocusing
13273 if ( nextItem
.constructor.static.highlightable
) {
13274 this.highlightItem( nextItem
);
13276 this.chooseItem( nextItem
);
13278 nextItem
.scrollElementIntoView();
13282 // Can't just return false, because e is not always a jQuery event
13283 e
.preventDefault();
13284 e
.stopPropagation();
13290 * Bind key down listener.
13294 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
13295 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
13299 * Unbind key down listener.
13303 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
13304 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
13308 * Get the closest item to a jQuery.Event.
13311 * @param {jQuery.Event} e
13312 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
13314 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
13315 var $item
= $( e
.target
).closest( '.oo-ui-optionWidget' );
13316 if ( $item
.length
) {
13317 return $item
.data( 'oo-ui-optionWidget' );
13323 * Get selected item.
13325 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
13327 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
13330 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13331 if ( this.items
[ i
].isSelected() ) {
13332 return this.items
[ i
];
13339 * Get highlighted item.
13341 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
13343 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
13346 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13347 if ( this.items
[ i
].isHighlighted() ) {
13348 return this.items
[ i
];
13355 * Toggle pressed state.
13357 * Press is a state that occurs when a user mouses down on an item, but
13358 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
13359 * until the user releases the mouse.
13361 * @param {boolean} pressed An option is being pressed
13363 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
13364 if ( pressed
=== undefined ) {
13365 pressed
= !this.pressed
;
13367 if ( pressed
!== this.pressed
) {
13369 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
13370 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
13371 this.pressed
= pressed
;
13376 * Highlight an option. If the `item` param is omitted, no options will be highlighted
13377 * and any existing highlight will be removed. The highlight is mutually exclusive.
13379 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
13383 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
13384 var i
, len
, highlighted
,
13387 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13388 highlighted
= this.items
[ i
] === item
;
13389 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
13390 this.items
[ i
].setHighlighted( highlighted
);
13395 this.emit( 'highlight', item
);
13402 * Programmatically select an option by its reference. If the `item` parameter is omitted,
13403 * all options will be deselected.
13405 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
13409 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
13410 var i
, len
, selected
,
13413 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13414 selected
= this.items
[ i
] === item
;
13415 if ( this.items
[ i
].isSelected() !== selected
) {
13416 this.items
[ i
].setSelected( selected
);
13421 this.emit( 'select', item
);
13430 * Press is a state that occurs when a user mouses down on an item, but has not
13431 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
13432 * releases the mouse.
13434 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
13438 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
13439 var i
, len
, pressed
,
13442 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13443 pressed
= this.items
[ i
] === item
;
13444 if ( this.items
[ i
].isPressed() !== pressed
) {
13445 this.items
[ i
].setPressed( pressed
);
13450 this.emit( 'press', item
);
13459 * Note that ‘choose’ should never be modified programmatically. A user can choose
13460 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
13461 * use the #selectItem method.
13463 * This method is identical to #selectItem, but may vary in subclasses that take additional action
13464 * when users choose an item with the keyboard or mouse.
13466 * @param {OO.ui.OptionWidget} item Item to choose
13470 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
13471 this.selectItem( item
);
13472 this.emit( 'choose', item
);
13478 * Get an option by its position relative to the specified item (or to the start of the option array,
13479 * if item is `null`). The direction in which to search through the option array is specified with a
13480 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
13481 * `null` if there are no options in the array.
13483 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
13484 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
13485 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
13487 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
) {
13488 var currentIndex
, nextIndex
, i
,
13489 increase
= direction
> 0 ? 1 : -1,
13490 len
= this.items
.length
;
13492 if ( item
instanceof OO
.ui
.OptionWidget
) {
13493 currentIndex
= $.inArray( item
, this.items
);
13494 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
13496 // If no item is selected and moving forward, start at the beginning.
13497 // If moving backward, start at the end.
13498 nextIndex
= direction
> 0 ? 0 : len
- 1;
13501 for ( i
= 0; i
< len
; i
++ ) {
13502 item
= this.items
[ nextIndex
];
13503 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() ) {
13506 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
13512 * Get the next selectable item or `null` if there are no selectable items.
13513 * Disabled options and menu-section markers and breaks are not selectable.
13515 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
13517 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
13520 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
13521 item
= this.items
[ i
];
13522 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() ) {
13531 * Add an array of options to the select. Optionally, an index number can be used to
13532 * specify an insertion point.
13534 * @param {OO.ui.OptionWidget[]} items Items to add
13535 * @param {number} [index] Index to insert items after
13539 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
13541 OO
.ui
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
13543 // Always provide an index, even if it was omitted
13544 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
13550 * Remove the specified array of options from the select. Options will be detached
13551 * from the DOM, not removed, so they can be reused later. To remove all options from
13552 * the select, you may wish to use the #clearItems method instead.
13554 * @param {OO.ui.OptionWidget[]} items Items to remove
13558 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
13561 // Deselect items being removed
13562 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
13564 if ( item
.isSelected() ) {
13565 this.selectItem( null );
13570 OO
.ui
.GroupWidget
.prototype.removeItems
.call( this, items
);
13572 this.emit( 'remove', items
);
13578 * Clear all options from the select. Options will be detached from the DOM, not removed,
13579 * so that they can be reused later. To remove a subset of options from the select, use
13580 * the #removeItems method.
13585 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
13586 var items
= this.items
.slice();
13589 OO
.ui
.GroupWidget
.prototype.clearItems
.call( this );
13592 this.selectItem( null );
13594 this.emit( 'remove', items
);
13600 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
13601 * button options and is used together with
13602 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
13603 * highlighting, choosing, and selecting mutually exclusive options. Please see
13604 * the [OOjs UI documentation on MediaWiki] [1] for more information.
13607 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
13608 * var option1 = new OO.ui.ButtonOptionWidget( {
13610 * label: 'Option 1',
13611 * title:'Button option 1'
13614 * var option2 = new OO.ui.ButtonOptionWidget( {
13616 * label: 'Option 2',
13617 * title:'Button option 2'
13620 * var option3 = new OO.ui.ButtonOptionWidget( {
13622 * label: 'Option 3',
13623 * title:'Button option 3'
13626 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
13627 * items: [option1, option2, option3]
13629 * $('body').append(buttonSelect.$element);
13631 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13634 * @extends OO.ui.SelectWidget
13635 * @mixins OO.ui.TabIndexedElement
13638 * @param {Object} [config] Configuration options
13640 OO
.ui
.ButtonSelectWidget
= function OoUiButtonSelectWidget( config
) {
13641 // Parent constructor
13642 OO
.ui
.ButtonSelectWidget
.super.call( this, config
);
13644 // Mixin constructors
13645 OO
.ui
.TabIndexedElement
.call( this, config
);
13648 this.$element
.on( {
13649 focus
: this.bindKeyDownListener
.bind( this ),
13650 blur
: this.unbindKeyDownListener
.bind( this )
13654 this.$element
.addClass( 'oo-ui-buttonSelectWidget' );
13659 OO
.inheritClass( OO
.ui
.ButtonSelectWidget
, OO
.ui
.SelectWidget
);
13660 OO
.mixinClass( OO
.ui
.ButtonSelectWidget
, OO
.ui
.TabIndexedElement
);
13663 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
13664 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
13665 * an interface for adding, removing and selecting options.
13666 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
13669 * // A RadioSelectWidget with RadioOptions.
13670 * var option1 = new OO.ui.RadioOptionWidget( {
13672 * label: 'Selected radio option'
13675 * var option2 = new OO.ui.RadioOptionWidget( {
13677 * label: 'Unselected radio option'
13680 * var radioSelect=new OO.ui.RadioSelectWidget( {
13681 * items: [option1, option2]
13684 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
13685 * radioSelect.selectItem( option1 );
13687 * $('body').append(radioSelect.$element);
13689 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13693 * @extends OO.ui.SelectWidget
13694 * @mixins OO.ui.TabIndexedElement
13697 * @param {Object} [config] Configuration options
13699 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
13700 // Parent constructor
13701 OO
.ui
.RadioSelectWidget
.super.call( this, config
);
13703 // Mixin constructors
13704 OO
.ui
.TabIndexedElement
.call( this, config
);
13707 this.$element
.on( {
13708 focus
: this.bindKeyDownListener
.bind( this ),
13709 blur
: this.unbindKeyDownListener
.bind( this )
13713 this.$element
.addClass( 'oo-ui-radioSelectWidget' );
13718 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
13719 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.TabIndexedElement
);
13722 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
13723 * is used together with OO.ui.MenuOptionWidget. See {@link OO.ui.DropdownWidget DropdownWidget} and
13724 * {@link OO.ui.ComboBoxWidget ComboBoxWidget} for examples of interfaces that contain menus.
13725 * MenuSelectWidgets themselves are not designed to be instantiated directly, rather subclassed
13726 * and customized to be opened, closed, and displayed as needed.
13728 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
13729 * mouse outside the menu.
13731 * Menus also have support for keyboard interaction:
13733 * - Enter/Return key: choose and select a menu option
13734 * - Up-arrow key: highlight the previous menu option
13735 * - Down-arrow key: highlight the next menu option
13736 * - Esc key: hide the menu
13738 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
13739 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13742 * @extends OO.ui.SelectWidget
13743 * @mixins OO.ui.ClippableElement
13746 * @param {Object} [config] Configuration options
13747 * @cfg {OO.ui.TextInputWidget} [input] Input to bind keyboard handlers to
13748 * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
13749 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
13751 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
13752 // Configuration initialization
13753 config
= config
|| {};
13755 // Parent constructor
13756 OO
.ui
.MenuSelectWidget
.super.call( this, config
);
13758 // Mixin constructors
13759 OO
.ui
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
13762 this.newItems
= null;
13763 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
13764 this.$input
= config
.input
? config
.input
.$input
: null;
13765 this.$widget
= config
.widget
? config
.widget
.$element
: null;
13766 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
13770 .addClass( 'oo-ui-menuSelectWidget' )
13771 .attr( 'role', 'menu' );
13773 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
13774 // that reference properties not initialized at that time of parent class construction
13775 // TODO: Find a better way to handle post-constructor setup
13776 this.visible
= false;
13777 this.$element
.addClass( 'oo-ui-element-hidden' );
13782 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
13783 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.ClippableElement
);
13788 * Handles document mouse down events.
13791 * @param {jQuery.Event} e Key down event
13793 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
13795 !OO
.ui
.contains( this.$element
[ 0 ], e
.target
, true ) &&
13796 ( !this.$widget
|| !OO
.ui
.contains( this.$widget
[ 0 ], e
.target
, true ) )
13798 this.toggle( false );
13805 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
13806 var currentItem
= this.getHighlightedItem() || this.getSelectedItem();
13808 if ( !this.isDisabled() && this.isVisible() ) {
13809 switch ( e
.keyCode
) {
13810 case OO
.ui
.Keys
.LEFT
:
13811 case OO
.ui
.Keys
.RIGHT
:
13812 // Do nothing if a text field is associated, arrow keys will be handled natively
13813 if ( !this.$input
) {
13814 OO
.ui
.MenuSelectWidget
.super.prototype.onKeyDown
.call( this, e
);
13817 case OO
.ui
.Keys
.ESCAPE
:
13818 case OO
.ui
.Keys
.TAB
:
13819 if ( currentItem
) {
13820 currentItem
.setHighlighted( false );
13822 this.toggle( false );
13823 // Don't prevent tabbing away, prevent defocusing
13824 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
13825 e
.preventDefault();
13826 e
.stopPropagation();
13830 OO
.ui
.MenuSelectWidget
.super.prototype.onKeyDown
.call( this, e
);
13839 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
13840 if ( this.$input
) {
13841 this.$input
.on( 'keydown', this.onKeyDownHandler
);
13843 OO
.ui
.MenuSelectWidget
.super.prototype.bindKeyDownListener
.call( this );
13850 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
13851 if ( this.$input
) {
13852 this.$input
.off( 'keydown', this.onKeyDownHandler
);
13854 OO
.ui
.MenuSelectWidget
.super.prototype.unbindKeyDownListener
.call( this );
13861 * This will close the menu, unlike #selectItem which only changes selection.
13863 * @param {OO.ui.OptionWidget} item Item to choose
13866 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
13867 OO
.ui
.MenuSelectWidget
.super.prototype.chooseItem
.call( this, item
);
13868 this.toggle( false );
13875 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
13879 OO
.ui
.MenuSelectWidget
.super.prototype.addItems
.call( this, items
, index
);
13882 if ( !this.newItems
) {
13883 this.newItems
= [];
13886 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
13888 if ( this.isVisible() ) {
13889 // Defer fitting label until item has been attached
13892 this.newItems
.push( item
);
13896 // Reevaluate clipping
13905 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
13907 OO
.ui
.MenuSelectWidget
.super.prototype.removeItems
.call( this, items
);
13909 // Reevaluate clipping
13918 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
13920 OO
.ui
.MenuSelectWidget
.super.prototype.clearItems
.call( this );
13922 // Reevaluate clipping
13931 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
13932 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
13935 change
= visible
!== this.isVisible();
13938 OO
.ui
.MenuSelectWidget
.super.prototype.toggle
.call( this, visible
);
13942 this.bindKeyDownListener();
13944 if ( this.newItems
&& this.newItems
.length
) {
13945 for ( i
= 0, len
= this.newItems
.length
; i
< len
; i
++ ) {
13946 this.newItems
[ i
].fitLabel();
13948 this.newItems
= null;
13950 this.toggleClipping( true );
13953 if ( this.autoHide
) {
13954 this.getElementDocument().addEventListener(
13955 'mousedown', this.onDocumentMouseDownHandler
, true
13959 this.unbindKeyDownListener();
13960 this.getElementDocument().removeEventListener(
13961 'mousedown', this.onDocumentMouseDownHandler
, true
13963 this.toggleClipping( false );
13971 * Menu for a text input widget.
13973 * This menu is specially designed to be positioned beneath a text input widget. The menu's position
13974 * is automatically calculated and maintained when the menu is toggled or the window is resized.
13977 * @extends OO.ui.MenuSelectWidget
13980 * @param {OO.ui.TextInputWidget} inputWidget Text input widget to provide menu for
13981 * @param {Object} [config] Configuration options
13982 * @cfg {jQuery} [$container=input.$element] Element to render menu under
13984 OO
.ui
.TextInputMenuSelectWidget
= function OoUiTextInputMenuSelectWidget( inputWidget
, config
) {
13985 // Allow passing positional parameters inside the config object
13986 if ( OO
.isPlainObject( inputWidget
) && config
=== undefined ) {
13987 config
= inputWidget
;
13988 inputWidget
= config
.inputWidget
;
13991 // Configuration initialization
13992 config
= config
|| {};
13994 // Parent constructor
13995 OO
.ui
.TextInputMenuSelectWidget
.super.call( this, config
);
13998 this.inputWidget
= inputWidget
;
13999 this.$container
= config
.$container
|| this.inputWidget
.$element
;
14000 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
14003 this.$element
.addClass( 'oo-ui-textInputMenuSelectWidget' );
14008 OO
.inheritClass( OO
.ui
.TextInputMenuSelectWidget
, OO
.ui
.MenuSelectWidget
);
14013 * Handle window resize event.
14015 * @param {jQuery.Event} e Window resize event
14017 OO
.ui
.TextInputMenuSelectWidget
.prototype.onWindowResize = function () {
14024 OO
.ui
.TextInputMenuSelectWidget
.prototype.toggle = function ( visible
) {
14025 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
14027 var change
= visible
!== this.isVisible();
14029 if ( change
&& visible
) {
14030 // Make sure the width is set before the parent method runs.
14031 // After this we have to call this.position(); again to actually
14032 // position ourselves correctly.
14037 OO
.ui
.TextInputMenuSelectWidget
.super.prototype.toggle
.call( this, visible
);
14040 if ( this.isVisible() ) {
14042 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler
);
14044 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler
);
14052 * Position the menu.
14056 OO
.ui
.TextInputMenuSelectWidget
.prototype.position = function () {
14057 var $container
= this.$container
,
14058 pos
= OO
.ui
.Element
.static.getRelativePosition( $container
, this.$element
.offsetParent() );
14060 // Position under input
14061 pos
.top
+= $container
.height();
14062 this.$element
.css( pos
);
14065 this.setIdealSize( $container
.width() );
14066 // We updated the position, so re-evaluate the clipping state
14073 * Structured list of items.
14075 * Use with OO.ui.OutlineOptionWidget.
14078 * @extends OO.ui.SelectWidget
14079 * @mixins OO.ui.TabIndexedElement
14082 * @param {Object} [config] Configuration options
14084 OO
.ui
.OutlineSelectWidget
= function OoUiOutlineSelectWidget( config
) {
14085 // Parent constructor
14086 OO
.ui
.OutlineSelectWidget
.super.call( this, config
);
14088 // Mixin constructors
14089 OO
.ui
.TabIndexedElement
.call( this, config
);
14092 this.$element
.on( {
14093 focus
: this.bindKeyDownListener
.bind( this ),
14094 blur
: this.unbindKeyDownListener
.bind( this )
14098 this.$element
.addClass( 'oo-ui-outlineSelectWidget' );
14103 OO
.inheritClass( OO
.ui
.OutlineSelectWidget
, OO
.ui
.SelectWidget
);
14104 OO
.mixinClass( OO
.ui
.OutlineSelectWidget
, OO
.ui
.TabIndexedElement
);
14107 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
14108 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
14109 * visually by a slider in the leftmost position.
14112 * // Toggle switches in the 'off' and 'on' position.
14113 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget({
14116 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget({
14120 * // Create a FieldsetLayout to layout and label switches
14121 * var fieldset = new OO.ui.FieldsetLayout( {
14122 * label: 'Toggle switches'
14124 * fieldset.addItems( [
14125 * new OO.ui.FieldLayout( toggleSwitch1, {label : 'Off', align : 'top'}),
14126 * new OO.ui.FieldLayout( toggleSwitch2, {label : 'On', align : 'top'})
14128 * $( 'body' ).append( fieldset.$element );
14131 * @extends OO.ui.Widget
14132 * @mixins OO.ui.ToggleWidget
14133 * @mixins OO.ui.TabIndexedElement
14136 * @param {Object} [config] Configuration options
14137 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
14138 * By default, the toggle switch is in the 'off' position.
14140 OO
.ui
.ToggleSwitchWidget
= function OoUiToggleSwitchWidget( config
) {
14141 // Parent constructor
14142 OO
.ui
.ToggleSwitchWidget
.super.call( this, config
);
14144 // Mixin constructors
14145 OO
.ui
.ToggleWidget
.call( this, config
);
14146 OO
.ui
.TabIndexedElement
.call( this, config
);
14149 this.dragging
= false;
14150 this.dragStart
= null;
14151 this.sliding
= false;
14152 this.$glow
= $( '<span>' );
14153 this.$grip
= $( '<span>' );
14156 this.$element
.on( {
14157 click
: this.onClick
.bind( this ),
14158 keypress
: this.onKeyPress
.bind( this )
14162 this.$glow
.addClass( 'oo-ui-toggleSwitchWidget-glow' );
14163 this.$grip
.addClass( 'oo-ui-toggleSwitchWidget-grip' );
14165 .addClass( 'oo-ui-toggleSwitchWidget' )
14166 .attr( 'role', 'checkbox' )
14167 .append( this.$glow
, this.$grip
);
14172 OO
.inheritClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.Widget
);
14173 OO
.mixinClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.ToggleWidget
);
14174 OO
.mixinClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.TabIndexedElement
);
14179 * Handle mouse click events.
14182 * @param {jQuery.Event} e Mouse click event
14184 OO
.ui
.ToggleSwitchWidget
.prototype.onClick = function ( e
) {
14185 if ( !this.isDisabled() && e
.which
=== 1 ) {
14186 this.setValue( !this.value
);
14192 * Handle key press events.
14195 * @param {jQuery.Event} e Key press event
14197 OO
.ui
.ToggleSwitchWidget
.prototype.onKeyPress = function ( e
) {
14198 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
14199 this.setValue( !this.value
);