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-02-19T01:33:11Z
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 ) ) {
122 * Message store for the default implementation of OO.ui.msg
124 * Environments that provide a localization system should not use this, but should override
125 * OO.ui.msg altogether.
130 // Tool tip for a button that moves items in a list down one place
131 'ooui-outline-control-move-down': 'Move item down',
132 // Tool tip for a button that moves items in a list up one place
133 'ooui-outline-control-move-up': 'Move item up',
134 // Tool tip for a button that removes items from a list
135 'ooui-outline-control-remove': 'Remove item',
136 // Label for the toolbar group that contains a list of all other available tools
137 'ooui-toolbar-more': 'More',
138 // Label for the fake tool that expands the full list of tools in a toolbar group
139 'ooui-toolgroup-expand': 'More',
140 // Label for the fake tool that collapses the full list of tools in a toolbar group
141 'ooui-toolgroup-collapse': 'Fewer',
142 // Default label for the accept button of a confirmation dialog
143 'ooui-dialog-message-accept': 'OK',
144 // Default label for the reject button of a confirmation dialog
145 'ooui-dialog-message-reject': 'Cancel',
146 // Title for process dialog error description
147 'ooui-dialog-process-error': 'Something went wrong',
148 // Label for process dialog dismiss error button, visible when describing errors
149 'ooui-dialog-process-dismiss': 'Dismiss',
150 // Label for process dialog retry action button, visible when describing only recoverable errors
151 'ooui-dialog-process-retry': 'Try again',
152 // Label for process dialog retry action button, visible when describing only warnings
153 'ooui-dialog-process-continue': 'Continue'
157 * Get a localized message.
159 * In environments that provide a localization system, this function should be overridden to
160 * return the message translated in the user's language. The default implementation always returns
163 * After the message key, message parameters may optionally be passed. In the default implementation,
164 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
165 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
166 * they support unnamed, ordered message parameters.
169 * @param {string} key Message key
170 * @param {Mixed...} [params] Message parameters
171 * @return {string} Translated message with parameters substituted
173 OO.ui.msg = function ( key ) {
174 var message = messages[ key ],
175 params = Array.prototype.slice.call( arguments, 1 );
176 if ( typeof message === 'string' ) {
177 // Perform $1 substitution
178 message = message.replace( /\$(\d+)/g, function ( unused, n ) {
179 var i = parseInt( n, 10 );
180 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
183 // Return placeholder if message not found
184 message = '[' + key + ']';
190 * Package a message and arguments for deferred resolution.
192 * Use this when you are statically specifying a message and the message may not yet be present.
194 * @param {string} key Message key
195 * @param {Mixed...} [params] Message parameters
196 * @return {Function} Function that returns the resolved message when executed
198 OO.ui.deferMsg = function () {
199 var args = arguments;
201 return OO.ui.msg.apply( OO.ui, args );
208 * If the message is a function it will be executed, otherwise it will pass through directly.
210 * @param {Function|string} msg Deferred message, or message text
211 * @return {string} Resolved message
213 OO.ui.resolveMsg = function ( msg ) {
214 if ( $.isFunction( msg ) ) {
223 * Element that can be marked as pending.
229 * @param {Object} [config] Configuration options
230 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
232 OO.ui.PendingElement = function OoUiPendingElement( config ) {
233 // Configuration initialization
234 config = config || {};
238 this.$pending = null;
241 this.setPendingElement( config.$pending || this.$element );
246 OO.initClass( OO.ui.PendingElement );
251 * Set the pending element (and clean up any existing one).
253 * @param {jQuery} $pending The element to set to pending.
255 OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) {
256 if ( this.$pending ) {
257 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
260 this.$pending = $pending;
261 if ( this.pending > 0 ) {
262 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
267 * Check if input is pending.
271 OO.ui.PendingElement.prototype.isPending = function () {
272 return !!this.pending;
276 * Increase the pending stack.
280 OO.ui.PendingElement.prototype.pushPending = function () {
281 if ( this.pending === 0 ) {
282 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
283 this.updateThemeClasses();
291 * Reduce the pending stack.
297 OO.ui.PendingElement.prototype.popPending = function () {
298 if ( this.pending === 1 ) {
299 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
300 this.updateThemeClasses();
302 this.pending = Math.max( 0, this.pending - 1 );
308 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget Action widgets} that comprise them.
309 * Actions can be made available for specific contexts (modes) and circumstances
310 * (abilities). Please see the [OOjs UI documentation on MediaWiki][1] for more information.
313 * // Example: An action set used in a process dialog
314 * function ProcessDialog( config ) {
315 * ProcessDialog.super.call( this, config );
317 * OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
318 * ProcessDialog.static.title = 'An action set in a process dialog';
319 * // An action set that uses modes ('edit' and 'help' mode, in this example).
320 * ProcessDialog.static.actions = [
321 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
322 * { action: 'help', modes: 'edit', label: 'Help' },
323 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
324 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
327 * ProcessDialog.prototype.initialize = function () {
328 * ProcessDialog.super.prototype.initialize.apply( this, arguments );
329 * this.panel1 = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } );
330 * 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>' );
331 * this.panel2 = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } );
332 * 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>' );
333 * this.stackLayout= new OO.ui.StackLayout( {
334 * items: [ this.panel1, this.panel2 ]
336 * this.$body.append( this.stackLayout.$element );
338 * ProcessDialog.prototype.getSetupProcess = function ( data ) {
339 * return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
340 * .next( function () {
341 * this.actions.setMode('edit');
344 * ProcessDialog.prototype.getActionProcess = function ( action ) {
345 * if ( action === 'help' ) {
346 * this.actions.setMode( 'help' );
347 * this.stackLayout.setItem( this.panel2 );
348 * } else if ( action === 'back' ) {
349 * this.actions.setMode( 'edit' );
350 * this.stackLayout.setItem( this.panel1 );
351 * } else if ( action === 'continue' ) {
353 * return new OO.ui.Process( function () {
357 * return ProcessDialog.super.prototype.getActionProcess.call( this, action );
359 * ProcessDialog.prototype.getBodyHeight = function () {
360 * return this.panel1.$element.outerHeight( true );
362 * var windowManager = new OO.ui.WindowManager();
363 * $( 'body' ).append( windowManager.$element );
364 * var processDialog = new ProcessDialog({
366 * windowManager.addWindows( [ processDialog ] );
367 * windowManager.openWindow( processDialog );
369 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
373 * @mixins OO.EventEmitter
376 * @param {Object} [config] Configuration options
378 OO.ui.ActionSet = function OoUiActionSet( config ) {
379 // Configuration initialization
380 config = config || {};
382 // Mixin constructors
383 OO.EventEmitter.call( this );
388 actions: 'getAction',
392 this.categorized = {};
395 this.organized = false;
396 this.changing = false;
397 this.changed = false;
402 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
404 /* Static Properties */
407 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
408 * header of a {@link OO.ui.ProcessDialog process dialog}.
409 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
411 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
418 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
424 * @param {OO.ui.ActionWidget} action Action that was clicked
429 * @param {OO.ui.ActionWidget} action Action that was resized
434 * @param {OO.ui.ActionWidget[]} added Actions added
439 * @param {OO.ui.ActionWidget[]} added Actions removed
449 * Handle action change events.
454 OO.ui.ActionSet.prototype.onActionChange = function () {
455 this.organized = false;
456 if ( this.changing ) {
459 this.emit( 'change' );
464 * Check if a action is one of the special actions.
466 * @param {OO.ui.ActionWidget} action Action to check
467 * @return {boolean} Action is special
469 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
472 for ( flag in this.special ) {
473 if ( action === this.special[ flag ] ) {
484 * @param {Object} [filters] Filters to use, omit to get all actions
485 * @param {string|string[]} [filters.actions] Actions that actions must have
486 * @param {string|string[]} [filters.flags] Flags that actions must have
487 * @param {string|string[]} [filters.modes] Modes that actions must have
488 * @param {boolean} [filters.visible] Actions must be visible
489 * @param {boolean} [filters.disabled] Actions must be disabled
490 * @return {OO.ui.ActionWidget[]} Actions matching all criteria
492 OO.ui.ActionSet.prototype.get = function ( filters ) {
493 var i, len, list, category, actions, index, match, matches;
498 // Collect category candidates
500 for ( category in this.categorized ) {
501 list = filters[ category ];
503 if ( !Array.isArray( list ) ) {
506 for ( i = 0, len = list.length; i < len; i++ ) {
507 actions = this.categorized[ category ][ list[ i ] ];
508 if ( Array.isArray( actions ) ) {
509 matches.push.apply( matches, actions );
514 // Remove by boolean filters
515 for ( i = 0, len = matches.length; i < len; i++ ) {
516 match = matches[ i ];
518 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
519 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
521 matches.splice( i, 1 );
527 for ( i = 0, len = matches.length; i < len; i++ ) {
528 match = matches[ i ];
529 index = matches.lastIndexOf( match );
530 while ( index !== i ) {
531 matches.splice( index, 1 );
533 index = matches.lastIndexOf( match );
538 return this.list.slice();
542 * Get special actions.
544 * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'.
545 * Special flags can be configured by changing #static-specialFlags in a subclass.
547 * @return {OO.ui.ActionWidget|null} Safe action
549 OO.ui.ActionSet.prototype.getSpecial = function () {
551 return $.extend( {}, this.special );
557 * Other actions include all non-special visible actions.
559 * @return {OO.ui.ActionWidget[]} Other actions
561 OO.ui.ActionSet.prototype.getOthers = function () {
563 return this.others.slice();
567 * Toggle actions based on their modes.
569 * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive
570 * visibility; matching actions will be shown, non-matching actions will be hidden.
572 * @param {string} mode Mode actions must have
577 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
580 this.changing = true;
581 for ( i = 0, len = this.list.length; i < len; i++ ) {
582 action = this.list[ i ];
583 action.toggle( action.hasMode( mode ) );
586 this.organized = false;
587 this.changing = false;
588 this.emit( 'change' );
594 * Change which actions are able to be performed.
596 * Actions with matching actions will be disabled/enabled. Other actions will not be changed.
598 * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values
599 * indicate actions are able to be performed
602 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
603 var i, len, action, item;
605 for ( i = 0, len = this.list.length; i < len; i++ ) {
606 item = this.list[ i ];
607 action = item.getAction();
608 if ( actions[ action ] !== undefined ) {
609 item.setDisabled( !actions[ action ] );
617 * Executes a function once per action.
619 * When making changes to multiple actions, use this method instead of iterating over the actions
620 * manually to defer emitting a change event until after all actions have been changed.
622 * @param {Object|null} actions Filters to use for which actions to iterate over; see #get
623 * @param {Function} callback Callback to run for each action; callback is invoked with three
624 * arguments: the action, the action's index, the list of actions being iterated over
627 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
628 this.changed = false;
629 this.changing = true;
630 this.get( filter ).forEach( callback );
631 this.changing = false;
632 if ( this.changed ) {
633 this.emit( 'change' );
642 * @param {OO.ui.ActionWidget[]} actions Actions to add
647 OO.ui.ActionSet.prototype.add = function ( actions ) {
650 this.changing = true;
651 for ( i = 0, len = actions.length; i < len; i++ ) {
652 action = actions[ i ];
653 action.connect( this, {
654 click: [ 'emit', 'click', action ],
655 resize: [ 'emit', 'resize', action ],
656 toggle: [ 'onActionChange' ]
658 this.list.push( action );
660 this.organized = false;
661 this.emit( 'add', actions );
662 this.changing = false;
663 this.emit( 'change' );
671 * @param {OO.ui.ActionWidget[]} actions Actions to remove
676 OO.ui.ActionSet.prototype.remove = function ( actions ) {
677 var i, len, index, action;
679 this.changing = true;
680 for ( i = 0, len = actions.length; i < len; i++ ) {
681 action = actions[ i ];
682 index = this.list.indexOf( action );
683 if ( index !== -1 ) {
684 action.disconnect( this );
685 this.list.splice( index, 1 );
688 this.organized = false;
689 this.emit( 'remove', actions );
690 this.changing = false;
691 this.emit( 'change' );
697 * Remove all actions.
703 OO.ui.ActionSet.prototype.clear = function () {
705 removed = this.list.slice();
707 this.changing = true;
708 for ( i = 0, len = this.list.length; i < len; i++ ) {
709 action = this.list[ i ];
710 action.disconnect( this );
715 this.organized = false;
716 this.emit( 'remove', removed );
717 this.changing = false;
718 this.emit( 'change' );
726 * This is called whenever organized information is requested. It will only reorganize the actions
727 * if something has changed since the last time it ran.
732 OO.ui.ActionSet.prototype.organize = function () {
733 var i, iLen, j, jLen, flag, action, category, list, item, special,
734 specialFlags = this.constructor.static.specialFlags;
736 if ( !this.organized ) {
737 this.categorized = {};
740 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
741 action = this.list[ i ];
742 if ( action.isVisible() ) {
743 // Populate categories
744 for ( category in this.categories ) {
745 if ( !this.categorized[ category ] ) {
746 this.categorized[ category ] = {};
748 list = action[ this.categories[ category ] ]();
749 if ( !Array.isArray( list ) ) {
752 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
754 if ( !this.categorized[ category ][ item ] ) {
755 this.categorized[ category ][ item ] = [];
757 this.categorized[ category ][ item ].push( action );
760 // Populate special/others
762 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
763 flag = specialFlags[ j ];
764 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
765 this.special[ flag ] = action;
771 this.others.push( action );
775 this.organized = true;
782 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
783 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
784 * connected to them and can't be interacted with.
790 * @param {Object} [config] Configuration options
791 * @cfg {string[]} [classes] CSS class names to add
792 * @cfg {string} [id] HTML id attribute
793 * @cfg {string} [text] Text to insert
794 * @cfg {jQuery} [$content] Content elements to append (after text)
795 * @cfg {Mixed} [data] Element data
797 OO.ui.Element = function OoUiElement( config ) {
798 // Configuration initialization
799 config = config || {};
803 this.data = config.data;
804 this.$element = $( document.createElement( this.getTagName() ) );
805 this.elementGroup = null;
806 this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
807 this.updateThemeClassesPending = false;
810 if ( Array.isArray( config.classes ) ) {
811 this.$element.addClass( config.classes.join( ' ' ) );
814 this.$element.attr( 'id', config.id );
817 this.$element.text( config.text );
819 if ( config.$content ) {
820 this.$element.append( config.$content );
826 OO.initClass( OO.ui.Element );
828 /* Static Properties */
833 * This may be ignored if #getTagName is overridden.
839 OO.ui.Element.static.tagName = 'div';
844 * Get a jQuery function within a specific document.
847 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
848 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
850 * @return {Function} Bound jQuery function
852 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
853 function wrapper( selector ) {
854 return $( selector, wrapper.context );
857 wrapper.context = this.getDocument( context );
860 wrapper.$iframe = $iframe;
867 * Get the document of an element.
870 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
871 * @return {HTMLDocument|null} Document object
873 OO.ui.Element.static.getDocument = function ( obj ) {
874 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
875 return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
876 // Empty jQuery selections might have a context
883 ( obj.nodeType === 9 && obj ) ||
888 * Get the window of an element or document.
891 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
892 * @return {Window} Window object
894 OO.ui.Element.static.getWindow = function ( obj ) {
895 var doc = this.getDocument( obj );
896 return doc.parentWindow || doc.defaultView;
900 * Get the direction of an element or document.
903 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
904 * @return {string} Text direction, either 'ltr' or 'rtl'
906 OO.ui.Element.static.getDir = function ( obj ) {
909 if ( obj instanceof jQuery ) {
912 isDoc = obj.nodeType === 9;
913 isWin = obj.document !== undefined;
914 if ( isDoc || isWin ) {
920 return $( obj ).css( 'direction' );
924 * Get the offset between two frames.
926 * TODO: Make this function not use recursion.
929 * @param {Window} from Window of the child frame
930 * @param {Window} [to=window] Window of the parent frame
931 * @param {Object} [offset] Offset to start with, used internally
932 * @return {Object} Offset object, containing left and top properties
934 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
935 var i, len, frames, frame, rect;
941 offset = { top: 0, left: 0 };
943 if ( from.parent === from ) {
947 // Get iframe element
948 frames = from.parent.document.getElementsByTagName( 'iframe' );
949 for ( i = 0, len = frames.length; i < len; i++ ) {
950 if ( frames[ i ].contentWindow === from ) {
956 // Recursively accumulate offset values
958 rect = frame.getBoundingClientRect();
959 offset.left += rect.left;
960 offset.top += rect.top;
962 this.getFrameOffset( from.parent, offset );
969 * Get the offset between two elements.
971 * The two elements may be in a different frame, but in that case the frame $element is in must
972 * be contained in the frame $anchor is in.
975 * @param {jQuery} $element Element whose position to get
976 * @param {jQuery} $anchor Element to get $element's position relative to
977 * @return {Object} Translated position coordinates, containing top and left properties
979 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
980 var iframe, iframePos,
981 pos = $element.offset(),
982 anchorPos = $anchor.offset(),
983 elementDocument = this.getDocument( $element ),
984 anchorDocument = this.getDocument( $anchor );
986 // If $element isn't in the same document as $anchor, traverse up
987 while ( elementDocument !== anchorDocument ) {
988 iframe = elementDocument.defaultView.frameElement;
990 throw new Error( '$element frame is not contained in $anchor frame' );
992 iframePos = $( iframe ).offset();
993 pos.left += iframePos.left;
994 pos.top += iframePos.top;
995 elementDocument = iframe.ownerDocument;
997 pos.left -= anchorPos.left;
998 pos.top -= anchorPos.top;
1003 * Get element border sizes.
1006 * @param {HTMLElement} el Element to measure
1007 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1009 OO.ui.Element.static.getBorders = function ( el ) {
1010 var doc = el.ownerDocument,
1011 win = doc.parentWindow || doc.defaultView,
1012 style = win && win.getComputedStyle ?
1013 win.getComputedStyle( el, null ) :
1016 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1017 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1018 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1019 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1030 * Get dimensions of an element or window.
1033 * @param {HTMLElement|Window} el Element to measure
1034 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1036 OO.ui.Element.static.getDimensions = function ( el ) {
1038 doc = el.ownerDocument || el.document,
1039 win = doc.parentWindow || doc.defaultView;
1041 if ( win === el || el === doc.documentElement ) {
1044 borders: { top: 0, left: 0, bottom: 0, right: 0 },
1046 top: $win.scrollTop(),
1047 left: $win.scrollLeft()
1049 scrollbar: { right: 0, bottom: 0 },
1053 bottom: $win.innerHeight(),
1054 right: $win.innerWidth()
1060 borders: this.getBorders( el ),
1062 top: $el.scrollTop(),
1063 left: $el.scrollLeft()
1066 right: $el.innerWidth() - el.clientWidth,
1067 bottom: $el.innerHeight() - el.clientHeight
1069 rect: el.getBoundingClientRect()
1075 * Get scrollable object parent
1077 * documentElement can't be used to get or set the scrollTop
1078 * property on Blink. Changing and testing its value lets us
1079 * use 'body' or 'documentElement' based on what is working.
1081 * https://code.google.com/p/chromium/issues/detail?id=303131
1084 * @param {HTMLElement} el Element to find scrollable parent for
1085 * @return {HTMLElement} Scrollable parent
1087 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1088 var scrollTop, body;
1090 if ( OO.ui.scrollableElement === undefined ) {
1091 body = el.ownerDocument.body;
1092 scrollTop = body.scrollTop;
1095 if ( body.scrollTop === 1 ) {
1096 body.scrollTop = scrollTop;
1097 OO.ui.scrollableElement = 'body';
1099 OO.ui.scrollableElement = 'documentElement';
1103 return el.ownerDocument[ OO.ui.scrollableElement ];
1107 * Get closest scrollable container.
1109 * Traverses up until either a scrollable element or the root is reached, in which case the window
1113 * @param {HTMLElement} el Element to find scrollable container for
1114 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1115 * @return {HTMLElement} Closest scrollable container
1117 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1119 props = [ 'overflow' ],
1120 $parent = $( el ).parent();
1122 if ( dimension === 'x' || dimension === 'y' ) {
1123 props.push( 'overflow-' + dimension );
1126 while ( $parent.length ) {
1127 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1128 return $parent[ 0 ];
1132 val = $parent.css( props[ i ] );
1133 if ( val === 'auto' || val === 'scroll' ) {
1134 return $parent[ 0 ];
1137 $parent = $parent.parent();
1139 return this.getDocument( el ).body;
1143 * Scroll element into view.
1146 * @param {HTMLElement} el Element to scroll into view
1147 * @param {Object} [config] Configuration options
1148 * @param {string} [config.duration] jQuery animation duration value
1149 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1150 * to scroll in both directions
1151 * @param {Function} [config.complete] Function to call when scrolling completes
1153 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1154 // Configuration initialization
1155 config = config || {};
1158 callback = typeof config.complete === 'function' && config.complete,
1159 sc = this.getClosestScrollableContainer( el, config.direction ),
1161 eld = this.getDimensions( el ),
1162 scd = this.getDimensions( sc ),
1163 $win = $( this.getWindow( el ) );
1165 // Compute the distances between the edges of el and the edges of the scroll viewport
1166 if ( $sc.is( 'html, body' ) ) {
1167 // If the scrollable container is the root, this is easy
1170 bottom: $win.innerHeight() - eld.rect.bottom,
1171 left: eld.rect.left,
1172 right: $win.innerWidth() - eld.rect.right
1175 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1177 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1178 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1179 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1180 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1184 if ( !config.direction || config.direction === 'y' ) {
1185 if ( rel.top < 0 ) {
1186 anim.scrollTop = scd.scroll.top + rel.top;
1187 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1188 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1191 if ( !config.direction || config.direction === 'x' ) {
1192 if ( rel.left < 0 ) {
1193 anim.scrollLeft = scd.scroll.left + rel.left;
1194 } else if ( rel.left > 0 && rel.right < 0 ) {
1195 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1198 if ( !$.isEmptyObject( anim ) ) {
1199 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1201 $sc.queue( function ( next ) {
1214 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1215 * and reserve space for them, because it probably doesn't.
1217 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1218 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1219 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1220 * and then reattach (or show) them back.
1223 * @param {HTMLElement} el Element to reconsider the scrollbars on
1225 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1226 var i, len, nodes = [];
1227 // Detach all children
1228 while ( el.firstChild ) {
1229 nodes.push( el.firstChild );
1230 el.removeChild( el.firstChild );
1233 void el.offsetHeight;
1234 // Reattach all children
1235 for ( i = 0, len = nodes.length; i < len; i++ ) {
1236 el.appendChild( nodes[ i ] );
1245 * @return {Mixed} Element data
1247 OO.ui.Element.prototype.getData = function () {
1254 * @param {Mixed} Element data
1257 OO.ui.Element.prototype.setData = function ( data ) {
1263 * Check if element supports one or more methods.
1265 * @param {string|string[]} methods Method or list of methods to check
1266 * @return {boolean} All methods are supported
1268 OO.ui.Element.prototype.supports = function ( methods ) {
1272 methods = Array.isArray( methods ) ? methods : [ methods ];
1273 for ( i = 0, len = methods.length; i < len; i++ ) {
1274 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1279 return methods.length === support;
1283 * Update the theme-provided classes.
1285 * @localdoc This is called in element mixins and widget classes any time state changes.
1286 * Updating is debounced, minimizing overhead of changing multiple attributes and
1287 * guaranteeing that theme updates do not occur within an element's constructor
1289 OO.ui.Element.prototype.updateThemeClasses = function () {
1290 if ( !this.updateThemeClassesPending ) {
1291 this.updateThemeClassesPending = true;
1292 setTimeout( this.debouncedUpdateThemeClassesHandler );
1299 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1300 OO.ui.theme.updateElementClasses( this );
1301 this.updateThemeClassesPending = false;
1305 * Get the HTML tag name.
1307 * Override this method to base the result on instance information.
1309 * @return {string} HTML tag name
1311 OO.ui.Element.prototype.getTagName = function () {
1312 return this.constructor.static.tagName;
1316 * Check if the element is attached to the DOM
1317 * @return {boolean} The element is attached to the DOM
1319 OO.ui.Element.prototype.isElementAttached = function () {
1320 return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1324 * Get the DOM document.
1326 * @return {HTMLDocument} Document object
1328 OO.ui.Element.prototype.getElementDocument = function () {
1329 // Don't cache this in other ways either because subclasses could can change this.$element
1330 return OO.ui.Element.static.getDocument( this.$element );
1334 * Get the DOM window.
1336 * @return {Window} Window object
1338 OO.ui.Element.prototype.getElementWindow = function () {
1339 return OO.ui.Element.static.getWindow( this.$element );
1343 * Get closest scrollable container.
1345 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1346 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1350 * Get group element is in.
1352 * @return {OO.ui.GroupElement|null} Group element, null if none
1354 OO.ui.Element.prototype.getElementGroup = function () {
1355 return this.elementGroup;
1359 * Set group element is in.
1361 * @param {OO.ui.GroupElement|null} group Group element, null if none
1364 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1365 this.elementGroup = group;
1370 * Scroll element into view.
1372 * @param {Object} [config] Configuration options
1374 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1375 return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1379 * Container for elements.
1383 * @extends OO.ui.Element
1384 * @mixins OO.EventEmitter
1387 * @param {Object} [config] Configuration options
1389 OO.ui.Layout = function OoUiLayout( config ) {
1390 // Configuration initialization
1391 config = config || {};
1393 // Parent constructor
1394 OO.ui.Layout.super.call( this, config );
1396 // Mixin constructors
1397 OO.EventEmitter.call( this );
1400 this.$element.addClass( 'oo-ui-layout' );
1405 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1406 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1409 * Widgets are compositions of one or more OOjs UI elements that users can both view
1410 * and interact with. All widgets can be configured and modified via a standard API,
1411 * and their state can change dynamically according to a model.
1415 * @extends OO.ui.Element
1416 * @mixins OO.EventEmitter
1419 * @param {Object} [config] Configuration options
1420 * @cfg {boolean} [disabled=false] Disable
1422 OO.ui.Widget = function OoUiWidget( config ) {
1423 // Initialize config
1424 config = $.extend( { disabled: false }, config );
1426 // Parent constructor
1427 OO.ui.Widget.super.call( this, config );
1429 // Mixin constructors
1430 OO.EventEmitter.call( this );
1433 this.visible = true;
1434 this.disabled = null;
1435 this.wasDisabled = null;
1438 this.$element.addClass( 'oo-ui-widget' );
1439 this.setDisabled( !!config.disabled );
1444 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1445 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1451 * @param {boolean} disabled Widget is disabled
1456 * @param {boolean} visible Widget is visible
1462 * Check if the widget is disabled.
1464 * @return {boolean} Button is disabled
1466 OO.ui.Widget.prototype.isDisabled = function () {
1467 return this.disabled;
1471 * Check if widget is visible.
1473 * @return {boolean} Widget is visible
1475 OO.ui.Widget.prototype.isVisible = function () {
1476 return this.visible;
1480 * Set the disabled state of the widget.
1482 * This should probably change the widgets' appearance and prevent it from being used.
1484 * @param {boolean} disabled Disable widget
1487 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1490 this.disabled = !!disabled;
1491 isDisabled = this.isDisabled();
1492 if ( isDisabled !== this.wasDisabled ) {
1493 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1494 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1495 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1496 this.emit( 'disable', isDisabled );
1497 this.updateThemeClasses();
1499 this.wasDisabled = isDisabled;
1505 * Toggle visibility of widget.
1507 * @param {boolean} [show] Make widget visible, omit to toggle visibility
1511 OO.ui.Widget.prototype.toggle = function ( show ) {
1512 show = show === undefined ? !this.visible : !!show;
1514 if ( show !== this.isVisible() ) {
1515 this.visible = show;
1516 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1517 this.emit( 'toggle', show );
1524 * Update the disabled state, in case of changes in parent widget.
1528 OO.ui.Widget.prototype.updateDisabled = function () {
1529 this.setDisabled( this.disabled );
1534 * A window is a container for elements that are in a child frame. They are used with
1535 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1536 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1537 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1538 * the window manager will choose a sensible fallback.
1540 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1541 * different processes are executed:
1543 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1544 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1547 * - {@link #getSetupProcess} method is called and its result executed
1548 * - {@link #getReadyProcess} method is called and its result executed
1550 * **opened**: The window is now open
1552 * **closing**: The closing stage begins when the window manager's
1553 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1554 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1556 * - {@link #getHoldProcess} method is called and its result executed
1557 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1559 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1560 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1561 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1562 * processing can complete. Always assume window processes are executed asynchronously.
1564 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1566 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1570 * @extends OO.ui.Element
1571 * @mixins OO.EventEmitter
1574 * @param {Object} [config] Configuration options
1575 * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large`, `larger` or
1576 * `full`; omit to use #static-size
1578 OO.ui.Window = function OoUiWindow( config ) {
1579 // Configuration initialization
1580 config = config || {};
1582 // Parent constructor
1583 OO.ui.Window.super.call( this, config );
1585 // Mixin constructors
1586 OO.EventEmitter.call( this );
1589 this.manager = null;
1590 this.size = config.size || this.constructor.static.size;
1591 this.$frame = $( '<div>' );
1592 this.$overlay = $( '<div>' );
1593 this.$content = $( '<div>' );
1596 this.$overlay.addClass( 'oo-ui-window-overlay' );
1598 .addClass( 'oo-ui-window-content' )
1599 .attr( 'tabIndex', 0 );
1601 .addClass( 'oo-ui-window-frame' )
1602 .append( this.$content );
1605 .addClass( 'oo-ui-window' )
1606 .append( this.$frame, this.$overlay );
1608 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1609 // that reference properties not initialized at that time of parent class construction
1610 // TODO: Find a better way to handle post-constructor setup
1611 this.visible = false;
1612 this.$element.addClass( 'oo-ui-element-hidden' );
1617 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1618 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1620 /* Static Properties */
1623 * Symbolic name of size.
1625 * Size is used if no size is configured during construction.
1629 * @property {string}
1631 OO.ui.Window.static.size = 'medium';
1636 * Handle mouse down events.
1638 * @param {jQuery.Event} e Mouse down event
1640 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1641 // Prevent clicking on the click-block from stealing focus
1642 if ( e.target === this.$element[ 0 ] ) {
1648 * Check if window has been initialized.
1650 * Initialization occurs when a window is added to a manager.
1652 * @return {boolean} Window has been initialized
1654 OO.ui.Window.prototype.isInitialized = function () {
1655 return !!this.manager;
1659 * Check if window is visible.
1661 * @return {boolean} Window is visible
1663 OO.ui.Window.prototype.isVisible = function () {
1664 return this.visible;
1668 * Check if window is opening.
1670 * This is a wrapper around OO.ui.WindowManager#isOpening.
1672 * @return {boolean} Window is opening
1674 OO.ui.Window.prototype.isOpening = function () {
1675 return this.manager.isOpening( this );
1679 * Check if window is closing.
1681 * This is a wrapper around OO.ui.WindowManager#isClosing.
1683 * @return {boolean} Window is closing
1685 OO.ui.Window.prototype.isClosing = function () {
1686 return this.manager.isClosing( this );
1690 * Check if window is opened.
1692 * This is a wrapper around OO.ui.WindowManager#isOpened.
1694 * @return {boolean} Window is opened
1696 OO.ui.Window.prototype.isOpened = function () {
1697 return this.manager.isOpened( this );
1701 * Get the window manager.
1703 * @return {OO.ui.WindowManager} Manager of window
1705 OO.ui.Window.prototype.getManager = function () {
1706 return this.manager;
1710 * Get the window size.
1712 * @return {string} Symbolic size name, e.g. `small`, `medium`, `large`, `larger`, `full`
1714 OO.ui.Window.prototype.getSize = function () {
1719 * Disable transitions on window's frame for the duration of the callback function, then enable them
1723 * @param {Function} callback Function to call while transitions are disabled
1725 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
1726 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1727 // Disable transitions first, otherwise we'll get values from when the window was animating.
1729 styleObj = this.$frame[ 0 ].style;
1730 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
1731 styleObj.MozTransition || styleObj.WebkitTransition;
1732 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1733 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
1735 // Force reflow to make sure the style changes done inside callback really are not transitioned
1736 this.$frame.height();
1737 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1738 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
1742 * Get the height of the dialog contents.
1744 * @return {number} Content height
1746 OO.ui.Window.prototype.getContentHeight = function () {
1749 bodyStyleObj = this.$body[ 0 ].style,
1750 frameStyleObj = this.$frame[ 0 ].style;
1752 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1753 // Disable transitions first, otherwise we'll get values from when the window was animating.
1754 this.withoutSizeTransitions( function () {
1755 var oldHeight = frameStyleObj.height,
1756 oldPosition = bodyStyleObj.position;
1757 frameStyleObj.height = '1px';
1758 // Force body to resize to new width
1759 bodyStyleObj.position = 'relative';
1760 bodyHeight = win.getBodyHeight();
1761 frameStyleObj.height = oldHeight;
1762 bodyStyleObj.position = oldPosition;
1766 // Add buffer for border
1767 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
1768 // Use combined heights of children
1769 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
1774 * Get the height of the dialog contents.
1776 * When this function is called, the dialog will temporarily have been resized
1777 * to height=1px, so .scrollHeight measurements can be taken accurately.
1779 * @return {number} Height of content
1781 OO.ui.Window.prototype.getBodyHeight = function () {
1782 return this.$body[ 0 ].scrollHeight;
1786 * Get the directionality of the frame
1788 * @return {string} Directionality, 'ltr' or 'rtl'
1790 OO.ui.Window.prototype.getDir = function () {
1795 * Get a process for setting up a window for use.
1797 * Each time the window is opened this process will set it up for use in a particular context, based
1798 * on the `data` argument.
1800 * When you override this method, you can add additional setup steps to the process the parent
1801 * method provides using the 'first' and 'next' methods.
1804 * @param {Object} [data] Window opening data
1805 * @return {OO.ui.Process} Setup process
1807 OO.ui.Window.prototype.getSetupProcess = function () {
1808 return new OO.ui.Process();
1812 * Get a process for readying a window for use.
1814 * Each time the window is open and setup, this process will ready it up for use in a particular
1815 * context, based on the `data` argument.
1817 * When you override this method, you can add additional setup steps to the process the parent
1818 * method provides using the 'first' and 'next' methods.
1821 * @param {Object} [data] Window opening data
1822 * @return {OO.ui.Process} Setup process
1824 OO.ui.Window.prototype.getReadyProcess = function () {
1825 return new OO.ui.Process();
1829 * Get a process for holding a window from use.
1831 * Each time the window is closed, this process will hold it from use in a particular context, based
1832 * on the `data` argument.
1834 * When you override this method, you can add additional setup steps to the process the parent
1835 * method provides using the 'first' and 'next' methods.
1838 * @param {Object} [data] Window closing data
1839 * @return {OO.ui.Process} Hold process
1841 OO.ui.Window.prototype.getHoldProcess = function () {
1842 return new OO.ui.Process();
1846 * Get a process for tearing down a window after use.
1848 * Each time the window is closed this process will tear it down and do something with the user's
1849 * interactions within the window, based on the `data` argument.
1851 * When you override this method, you can add additional teardown steps to the process the parent
1852 * method provides using the 'first' and 'next' methods.
1855 * @param {Object} [data] Window closing data
1856 * @return {OO.ui.Process} Teardown process
1858 OO.ui.Window.prototype.getTeardownProcess = function () {
1859 return new OO.ui.Process();
1863 * Toggle visibility of window.
1865 * @param {boolean} [show] Make window visible, omit to toggle visibility
1869 OO.ui.Window.prototype.toggle = function ( show ) {
1870 show = show === undefined ? !this.visible : !!show;
1872 if ( show !== this.isVisible() ) {
1873 this.visible = show;
1874 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1875 this.emit( 'toggle', show );
1882 * Set the window manager.
1884 * This will cause the window to initialize. Calling it more than once will cause an error.
1886 * @param {OO.ui.WindowManager} manager Manager for this window
1887 * @throws {Error} If called more than once
1890 OO.ui.Window.prototype.setManager = function ( manager ) {
1891 if ( this.manager ) {
1892 throw new Error( 'Cannot set window manager, window already has a manager' );
1895 this.manager = manager;
1902 * Set the window size.
1904 * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
1907 OO.ui.Window.prototype.setSize = function ( size ) {
1914 * Update the window size.
1916 * @throws {Error} If not attached to a manager
1919 OO.ui.Window.prototype.updateSize = function () {
1920 if ( !this.manager ) {
1921 throw new Error( 'Cannot update window size, must be attached to a manager' );
1924 this.manager.updateWindowSize( this );
1930 * Set window dimensions.
1932 * Properties are applied to the frame container.
1934 * @param {Object} dim CSS dimension properties
1935 * @param {string|number} [dim.width] Width
1936 * @param {string|number} [dim.minWidth] Minimum width
1937 * @param {string|number} [dim.maxWidth] Maximum width
1938 * @param {string|number} [dim.width] Height, omit to set based on height of contents
1939 * @param {string|number} [dim.minWidth] Minimum height
1940 * @param {string|number} [dim.maxWidth] Maximum height
1943 OO.ui.Window.prototype.setDimensions = function ( dim ) {
1946 styleObj = this.$frame[ 0 ].style;
1948 // Calculate the height we need to set using the correct width
1949 if ( dim.height === undefined ) {
1950 this.withoutSizeTransitions( function () {
1951 var oldWidth = styleObj.width;
1952 win.$frame.css( 'width', dim.width || '' );
1953 height = win.getContentHeight();
1954 styleObj.width = oldWidth;
1957 height = dim.height;
1961 width: dim.width || '',
1962 minWidth: dim.minWidth || '',
1963 maxWidth: dim.maxWidth || '',
1964 height: height || '',
1965 minHeight: dim.minHeight || '',
1966 maxHeight: dim.maxHeight || ''
1973 * Initialize window contents.
1975 * The first time the window is opened, #initialize is called so that changes to the window that
1976 * will persist between openings can be made. See #getSetupProcess for a way to make changes each
1977 * time the window opens.
1979 * @throws {Error} If not attached to a manager
1982 OO.ui.Window.prototype.initialize = function () {
1983 if ( !this.manager ) {
1984 throw new Error( 'Cannot initialize window, must be attached to a manager' );
1988 this.$head = $( '<div>' );
1989 this.$body = $( '<div>' );
1990 this.$foot = $( '<div>' );
1991 this.$innerOverlay = $( '<div>' );
1992 this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
1993 this.$document = $( this.getElementDocument() );
1996 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
1999 this.$head.addClass( 'oo-ui-window-head' );
2000 this.$body.addClass( 'oo-ui-window-body' );
2001 this.$foot.addClass( 'oo-ui-window-foot' );
2002 this.$innerOverlay.addClass( 'oo-ui-window-inner-overlay' );
2003 this.$content.append( this.$head, this.$body, this.$foot, this.$innerOverlay );
2011 * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager.
2012 * To do something each time the window opens, use #getSetupProcess or #getReadyProcess.
2014 * @param {Object} [data] Window opening data
2015 * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
2016 * first argument will be a promise which will be resolved when the window begins closing
2017 * @throws {Error} If not attached to a manager
2019 OO.ui.Window.prototype.open = function ( data ) {
2020 if ( !this.manager ) {
2021 throw new Error( 'Cannot open window, must be attached to a manager' );
2024 return this.manager.openWindow( this, data );
2030 * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager.
2031 * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess.
2033 * @param {Object} [data] Window closing data
2034 * @return {jQuery.Promise} Promise resolved when window is closed
2035 * @throws {Error} If not attached to a manager
2037 OO.ui.Window.prototype.close = function ( data ) {
2038 if ( !this.manager ) {
2039 throw new Error( 'Cannot close window, must be attached to a manager' );
2042 return this.manager.closeWindow( this, data );
2048 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2051 * @param {Object} [data] Window opening data
2052 * @return {jQuery.Promise} Promise resolved when window is setup
2054 OO.ui.Window.prototype.setup = function ( data ) {
2056 deferred = $.Deferred();
2058 this.toggle( true );
2060 this.getSetupProcess( data ).execute().done( function () {
2061 // Force redraw by asking the browser to measure the elements' widths
2062 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2063 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2067 return deferred.promise();
2073 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2076 * @param {Object} [data] Window opening data
2077 * @return {jQuery.Promise} Promise resolved when window is ready
2079 OO.ui.Window.prototype.ready = function ( data ) {
2081 deferred = $.Deferred();
2083 this.$content.focus();
2084 this.getReadyProcess( data ).execute().done( function () {
2085 // Force redraw by asking the browser to measure the elements' widths
2086 win.$element.addClass( 'oo-ui-window-ready' ).width();
2087 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2091 return deferred.promise();
2097 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2100 * @param {Object} [data] Window closing data
2101 * @return {jQuery.Promise} Promise resolved when window is held
2103 OO.ui.Window.prototype.hold = function ( data ) {
2105 deferred = $.Deferred();
2107 this.getHoldProcess( data ).execute().done( function () {
2108 // Get the focused element within the window's content
2109 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2111 // Blur the focused element
2112 if ( $focus.length ) {
2116 // Force redraw by asking the browser to measure the elements' widths
2117 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2118 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2122 return deferred.promise();
2128 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2131 * @param {Object} [data] Window closing data
2132 * @return {jQuery.Promise} Promise resolved when window is torn down
2134 OO.ui.Window.prototype.teardown = function ( data ) {
2137 return this.getTeardownProcess( data ).execute()
2138 .done( function () {
2139 // Force redraw by asking the browser to measure the elements' widths
2140 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2141 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2142 win.toggle( false );
2147 * The Dialog class serves as the base class for the other types of dialogs.
2148 * Unless extended to include controls, the rendered dialog box is a simple window
2149 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2150 * which opens, closes, and controls the presentation of the window. See the
2151 * [OOjs UI documentation on MediaWiki] [1] for more information.
2154 * // A simple dialog window.
2155 * function MyDialog( config ) {
2156 * MyDialog.super.call( this, config );
2158 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2159 * MyDialog.prototype.initialize = function () {
2160 * MyDialog.super.prototype.initialize.call( this );
2161 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2162 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2163 * this.$body.append( this.content.$element );
2165 * MyDialog.prototype.getBodyHeight = function () {
2166 * return this.content.$element.outerHeight( true );
2168 * var myDialog = new MyDialog( {
2171 * // Create and append a window manager, which opens and closes the window.
2172 * var windowManager = new OO.ui.WindowManager();
2173 * $( 'body' ).append( windowManager.$element );
2174 * windowManager.addWindows( [ myDialog ] );
2175 * // Open the window!
2176 * windowManager.openWindow( myDialog );
2178 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2182 * @extends OO.ui.Window
2183 * @mixins OO.ui.PendingElement
2186 * @param {Object} [config] Configuration options
2188 OO.ui.Dialog = function OoUiDialog( config ) {
2189 // Parent constructor
2190 OO.ui.Dialog.super.call( this, config );
2192 // Mixin constructors
2193 OO.ui.PendingElement.call( this );
2196 this.actions = new OO.ui.ActionSet();
2197 this.attachedActions = [];
2198 this.currentAction = null;
2199 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2202 this.actions.connect( this, {
2203 click: 'onActionClick',
2204 resize: 'onActionResize',
2205 change: 'onActionsChange'
2210 .addClass( 'oo-ui-dialog' )
2211 .attr( 'role', 'dialog' );
2216 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2217 OO.mixinClass( OO.ui.Dialog, OO.ui.PendingElement );
2219 /* Static Properties */
2222 * Symbolic name of dialog.
2227 * @property {string}
2229 OO.ui.Dialog.static.name = '';
2237 * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text
2239 OO.ui.Dialog.static.title = '';
2242 * List of OO.ui.ActionWidget configuration options.
2246 * @property {Object[]}
2248 OO.ui.Dialog.static.actions = [];
2251 * Close dialog when the escape key is pressed.
2256 * @property {boolean}
2258 OO.ui.Dialog.static.escapable = true;
2263 * Handle frame document key down events.
2265 * @param {jQuery.Event} e Key down event
2267 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2268 if ( e.which === OO.ui.Keys.ESCAPE ) {
2271 e.stopPropagation();
2276 * Handle action resized events.
2278 * @param {OO.ui.ActionWidget} action Action that was resized
2280 OO.ui.Dialog.prototype.onActionResize = function () {
2281 // Override in subclass
2285 * Handle action click events.
2287 * @param {OO.ui.ActionWidget} action Action that was clicked
2289 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2290 if ( !this.isPending() ) {
2291 this.currentAction = action;
2292 this.executeAction( action.getAction() );
2297 * Handle actions change event.
2299 OO.ui.Dialog.prototype.onActionsChange = function () {
2300 this.detachActions();
2301 if ( !this.isClosing() ) {
2302 this.attachActions();
2307 * Get set of actions.
2309 * @return {OO.ui.ActionSet}
2311 OO.ui.Dialog.prototype.getActions = function () {
2312 return this.actions;
2316 * Get a process for taking action.
2318 * When you override this method, you can add additional accept steps to the process the parent
2319 * method provides using the 'first' and 'next' methods.
2322 * @param {string} [action] Symbolic name of action
2323 * @return {OO.ui.Process} Action process
2325 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2326 return new OO.ui.Process()
2327 .next( function () {
2329 // An empty action always closes the dialog without data, which should always be
2330 // safe and make no changes
2339 * @param {Object} [data] Dialog opening data
2340 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
2341 * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
2342 * action item, omit to use #static-actions
2344 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2348 return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2349 .next( function () {
2352 config = this.constructor.static,
2353 actions = data.actions !== undefined ? data.actions : config.actions;
2355 this.title.setLabel(
2356 data.title !== undefined ? data.title : this.constructor.static.title
2358 for ( i = 0, len = actions.length; i < len; i++ ) {
2360 new OO.ui.ActionWidget( actions[ i ] )
2363 this.actions.add( items );
2365 if ( this.constructor.static.escapable ) {
2366 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2374 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2376 return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2377 .first( function () {
2378 if ( this.constructor.static.escapable ) {
2379 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2382 this.actions.clear();
2383 this.currentAction = null;
2390 OO.ui.Dialog.prototype.initialize = function () {
2392 OO.ui.Dialog.super.prototype.initialize.call( this );
2395 this.title = new OO.ui.LabelWidget();
2398 this.$content.addClass( 'oo-ui-dialog-content' );
2399 this.setPendingElement( this.$head );
2403 * Attach action actions.
2405 OO.ui.Dialog.prototype.attachActions = function () {
2406 // Remember the list of potentially attached actions
2407 this.attachedActions = this.actions.get();
2411 * Detach action actions.
2415 OO.ui.Dialog.prototype.detachActions = function () {
2418 // Detach all actions that may have been previously attached
2419 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2420 this.attachedActions[ i ].$element.detach();
2422 this.attachedActions = [];
2426 * Execute an action.
2428 * @param {string} action Symbolic name of action to execute
2429 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2431 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2433 return this.getActionProcess( action ).execute()
2434 .always( this.popPending.bind( this ) );
2438 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2439 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2440 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2441 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2442 * pertinent data and reused.
2444 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2445 * `opened`, and `closing`, which represent the primary stages of the cycle:
2447 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2448 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2450 * - an `opening` event is emitted with an `opening` promise
2451 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2452 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2453 * window and its result executed
2454 * - a `setup` progress notification is emitted from the `opening` promise
2455 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2456 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2457 * window and its result executed
2458 * - a `ready` progress notification is emitted from the `opening` promise
2459 * - the `opening` promise is resolved with an `opened` promise
2461 * **Opened**: the window is now open.
2463 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2464 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2465 * to close the window.
2467 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2468 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2469 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2470 * window and its result executed
2471 * - a `hold` progress notification is emitted from the `closing` promise
2472 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2473 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2474 * window and its result executed
2475 * - a `teardown` progress notification is emitted from the `closing` promise
2476 * - the `closing` promise is resolved. The window is now closed
2478 * See the [OOjs UI documentation on MediaWiki][1] for more information.
2480 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2483 * @extends OO.ui.Element
2484 * @mixins OO.EventEmitter
2487 * @param {Object} [config] Configuration options
2488 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2489 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2491 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2492 // Configuration initialization
2493 config = config || {};
2495 // Parent constructor
2496 OO.ui.WindowManager.super.call( this, config );
2498 // Mixin constructors
2499 OO.EventEmitter.call( this );
2502 this.factory = config.factory;
2503 this.modal = config.modal === undefined || !!config.modal;
2505 this.opening = null;
2507 this.closing = null;
2508 this.preparingToOpen = null;
2509 this.preparingToClose = null;
2510 this.currentWindow = null;
2511 this.$ariaHidden = null;
2512 this.onWindowResizeTimeout = null;
2513 this.onWindowResizeHandler = this.onWindowResize.bind( this );
2514 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2518 .addClass( 'oo-ui-windowManager' )
2519 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2524 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2525 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2530 * Window is opening.
2532 * Fired when the window begins to be opened.
2535 * @param {OO.ui.Window} win Window that's being opened
2536 * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
2537 * resolved the first argument will be a promise which will be resolved when the window begins
2538 * closing, the second argument will be the opening data; progress notifications will be fired on
2539 * the promise for `setup` and `ready` when those processes are completed respectively.
2540 * @param {Object} data Window opening data
2544 * Window is closing.
2546 * Fired when the window begins to be closed.
2549 * @param {OO.ui.Window} win Window that's being closed
2550 * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise
2551 * is resolved the first argument will be a the closing data; progress notifications will be fired
2552 * on the promise for `hold` and `teardown` when those processes are completed respectively.
2553 * @param {Object} data Window closing data
2557 * Window was resized.
2560 * @param {OO.ui.Window} win Window that was resized
2563 /* Static Properties */
2566 * Map of symbolic size names and CSS properties.
2570 * @property {Object}
2572 OO.ui.WindowManager.static.sizes = {
2586 // These can be non-numeric because they are never used in calculations
2593 * Symbolic name of default size.
2595 * Default size is used if the window's requested size is not recognized.
2599 * @property {string}
2601 OO.ui.WindowManager.static.defaultSize = 'medium';
2606 * Handle window resize events.
2608 * @param {jQuery.Event} e Window resize event
2610 OO.ui.WindowManager.prototype.onWindowResize = function () {
2611 clearTimeout( this.onWindowResizeTimeout );
2612 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
2616 * Handle window resize events.
2618 * @param {jQuery.Event} e Window resize event
2620 OO.ui.WindowManager.prototype.afterWindowResize = function () {
2621 if ( this.currentWindow ) {
2622 this.updateWindowSize( this.currentWindow );
2627 * Check if window is opening.
2629 * @return {boolean} Window is opening
2631 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
2632 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
2636 * Check if window is closing.
2638 * @return {boolean} Window is closing
2640 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
2641 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
2645 * Check if window is opened.
2647 * @return {boolean} Window is opened
2649 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
2650 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
2654 * Check if a window is being managed.
2656 * @param {OO.ui.Window} win Window to check
2657 * @return {boolean} Window is being managed
2659 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
2662 for ( name in this.windows ) {
2663 if ( this.windows[ name ] === win ) {
2672 * Get the number of milliseconds to wait between beginning opening and executing setup process.
2674 * @param {OO.ui.Window} win Window being opened
2675 * @param {Object} [data] Window opening data
2676 * @return {number} Milliseconds to wait
2678 OO.ui.WindowManager.prototype.getSetupDelay = function () {
2683 * Get the number of milliseconds to wait between finishing setup and executing ready process.
2685 * @param {OO.ui.Window} win Window being opened
2686 * @param {Object} [data] Window opening data
2687 * @return {number} Milliseconds to wait
2689 OO.ui.WindowManager.prototype.getReadyDelay = function () {
2694 * Get the number of milliseconds to wait between beginning closing and executing hold process.
2696 * @param {OO.ui.Window} win Window being closed
2697 * @param {Object} [data] Window closing data
2698 * @return {number} Milliseconds to wait
2700 OO.ui.WindowManager.prototype.getHoldDelay = function () {
2705 * Get the number of milliseconds to wait between finishing hold and executing teardown process.
2707 * @param {OO.ui.Window} win Window being closed
2708 * @param {Object} [data] Window closing data
2709 * @return {number} Milliseconds to wait
2711 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
2712 return this.modal ? 250 : 0;
2716 * Get managed window by symbolic name.
2718 * If window is not yet instantiated, it will be instantiated and added automatically.
2720 * @param {string} name Symbolic window name
2721 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2722 * @throws {Error} If the symbolic name is unrecognized by the factory
2723 * @throws {Error} If the symbolic name unrecognized as a managed window
2725 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
2726 var deferred = $.Deferred(),
2727 win = this.windows[ name ];
2729 if ( !( win instanceof OO.ui.Window ) ) {
2730 if ( this.factory ) {
2731 if ( !this.factory.lookup( name ) ) {
2732 deferred.reject( new OO.ui.Error(
2733 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
2736 win = this.factory.create( name, this );
2737 this.addWindows( [ win ] );
2738 deferred.resolve( win );
2741 deferred.reject( new OO.ui.Error(
2742 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
2746 deferred.resolve( win );
2749 return deferred.promise();
2753 * Get current window.
2755 * @return {OO.ui.Window|null} Currently opening/opened/closing window
2757 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
2758 return this.currentWindow;
2764 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
2765 * @param {Object} [data] Window opening data
2766 * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
2767 * for more details about the `opening` promise
2770 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
2772 opening = $.Deferred();
2774 // Argument handling
2775 if ( typeof win === 'string' ) {
2776 return this.getWindow( win ).then( function ( win ) {
2777 return manager.openWindow( win, data );
2782 if ( !this.hasWindow( win ) ) {
2783 opening.reject( new OO.ui.Error(
2784 'Cannot open window: window is not attached to manager'
2786 } else if ( this.preparingToOpen || this.opening || this.opened ) {
2787 opening.reject( new OO.ui.Error(
2788 'Cannot open window: another window is opening or open'
2793 if ( opening.state() !== 'rejected' ) {
2794 // If a window is currently closing, wait for it to complete
2795 this.preparingToOpen = $.when( this.closing );
2796 // Ensure handlers get called after preparingToOpen is set
2797 this.preparingToOpen.done( function () {
2798 if ( manager.modal ) {
2799 manager.toggleGlobalEvents( true );
2800 manager.toggleAriaIsolation( true );
2802 manager.currentWindow = win;
2803 manager.opening = opening;
2804 manager.preparingToOpen = null;
2805 manager.emit( 'opening', win, opening, data );
2806 setTimeout( function () {
2807 win.setup( data ).then( function () {
2808 manager.updateWindowSize( win );
2809 manager.opening.notify( { state: 'setup' } );
2810 setTimeout( function () {
2811 win.ready( data ).then( function () {
2812 manager.opening.notify( { state: 'ready' } );
2813 manager.opening = null;
2814 manager.opened = $.Deferred();
2815 opening.resolve( manager.opened.promise(), data );
2817 }, manager.getReadyDelay() );
2819 }, manager.getSetupDelay() );
2823 return opening.promise();
2829 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
2830 * @param {Object} [data] Window closing data
2831 * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
2832 * for more details about the `closing` promise
2833 * @throws {Error} If no window by that name is being managed
2836 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
2838 closing = $.Deferred(),
2841 // Argument handling
2842 if ( typeof win === 'string' ) {
2843 win = this.windows[ win ];
2844 } else if ( !this.hasWindow( win ) ) {
2850 closing.reject( new OO.ui.Error(
2851 'Cannot close window: window is not attached to manager'
2853 } else if ( win !== this.currentWindow ) {
2854 closing.reject( new OO.ui.Error(
2855 'Cannot close window: window already closed with different data'
2857 } else if ( this.preparingToClose || this.closing ) {
2858 closing.reject( new OO.ui.Error(
2859 'Cannot close window: window already closing with different data'
2864 if ( closing.state() !== 'rejected' ) {
2865 // If the window is currently opening, close it when it's done
2866 this.preparingToClose = $.when( this.opening );
2867 // Ensure handlers get called after preparingToClose is set
2868 this.preparingToClose.done( function () {
2869 manager.closing = closing;
2870 manager.preparingToClose = null;
2871 manager.emit( 'closing', win, closing, data );
2872 opened = manager.opened;
2873 manager.opened = null;
2874 opened.resolve( closing.promise(), data );
2875 setTimeout( function () {
2876 win.hold( data ).then( function () {
2877 closing.notify( { state: 'hold' } );
2878 setTimeout( function () {
2879 win.teardown( data ).then( function () {
2880 closing.notify( { state: 'teardown' } );
2881 if ( manager.modal ) {
2882 manager.toggleGlobalEvents( false );
2883 manager.toggleAriaIsolation( false );
2885 manager.closing = null;
2886 manager.currentWindow = null;
2887 closing.resolve( data );
2889 }, manager.getTeardownDelay() );
2891 }, manager.getHoldDelay() );
2895 return closing.promise();
2901 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
2902 * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
2903 * a statically configured symbolic name
2905 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
2906 var i, len, win, name, list;
2908 if ( Array.isArray( windows ) ) {
2909 // Convert to map of windows by looking up symbolic names from static configuration
2911 for ( i = 0, len = windows.length; i < len; i++ ) {
2912 name = windows[ i ].constructor.static.name;
2913 if ( typeof name !== 'string' ) {
2914 throw new Error( 'Cannot add window' );
2916 list[ name ] = windows[ i ];
2918 } else if ( $.isPlainObject( windows ) ) {
2923 for ( name in list ) {
2925 this.windows[ name ] = win.toggle( false );
2926 this.$element.append( win.$element );
2927 win.setManager( this );
2934 * Windows will be closed before they are removed.
2936 * @param {string[]} names Symbolic names of windows to remove
2937 * @return {jQuery.Promise} Promise resolved when window is closed and removed
2938 * @throws {Error} If windows being removed are not being managed
2940 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
2941 var i, len, win, name, cleanupWindow,
2944 cleanup = function ( name, win ) {
2945 delete manager.windows[ name ];
2946 win.$element.detach();
2949 for ( i = 0, len = names.length; i < len; i++ ) {
2951 win = this.windows[ name ];
2953 throw new Error( 'Cannot remove window' );
2955 cleanupWindow = cleanup.bind( null, name, win );
2956 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
2959 return $.when.apply( $, promises );
2963 * Remove all windows.
2965 * Windows will be closed before they are removed.
2967 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
2969 OO.ui.WindowManager.prototype.clearWindows = function () {
2970 return this.removeWindows( Object.keys( this.windows ) );
2976 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
2980 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
2981 // Bypass for non-current, and thus invisible, windows
2982 if ( win !== this.currentWindow ) {
2986 var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
2987 sizes = this.constructor.static.sizes,
2988 size = win.getSize();
2990 if ( !sizes[ size ] ) {
2991 size = this.constructor.static.defaultSize;
2993 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
2997 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
2998 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
2999 win.setDimensions( sizes[ size ] );
3001 this.emit( 'resize', win );
3007 * Bind or unbind global events for scrolling.
3009 * @param {boolean} [on] Bind global events
3012 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3013 on = on === undefined ? !!this.globalEvents : !!on;
3016 if ( !this.globalEvents ) {
3017 $( this.getElementWindow() ).on( {
3018 // Start listening for top-level window dimension changes
3019 'orientationchange resize': this.onWindowResizeHandler
3021 $( this.getElementDocument().body ).css( 'overflow', 'hidden' );
3022 this.globalEvents = true;
3024 } else if ( this.globalEvents ) {
3025 $( this.getElementWindow() ).off( {
3026 // Stop listening for top-level window dimension changes
3027 'orientationchange resize': this.onWindowResizeHandler
3029 $( this.getElementDocument().body ).css( 'overflow', '' );
3030 this.globalEvents = false;
3037 * Toggle screen reader visibility of content other than the window manager.
3039 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3042 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3043 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3046 if ( !this.$ariaHidden ) {
3047 // Hide everything other than the window manager from screen readers
3048 this.$ariaHidden = $( 'body' )
3050 .not( this.$element.parentsUntil( 'body' ).last() )
3051 .attr( 'aria-hidden', '' );
3053 } else if ( this.$ariaHidden ) {
3054 // Restore screen reader visibility
3055 this.$ariaHidden.removeAttr( 'aria-hidden' );
3056 this.$ariaHidden = null;
3063 * Destroy window manager.
3065 OO.ui.WindowManager.prototype.destroy = function () {
3066 this.toggleGlobalEvents( false );
3067 this.toggleAriaIsolation( false );
3068 this.clearWindows();
3069 this.$element.remove();
3076 * @param {string|jQuery} message Description of error
3077 * @param {Object} [config] Configuration options
3078 * @cfg {boolean} [recoverable=true] Error is recoverable
3079 * @cfg {boolean} [warning=false] Whether this error is a warning or not.
3081 OO.ui.Error = function OoUiElement( message, config ) {
3082 // Configuration initialization
3083 config = config || {};
3086 this.message = message instanceof jQuery ? message : String( message );
3087 this.recoverable = config.recoverable === undefined || !!config.recoverable;
3088 this.warning = !!config.warning;
3093 OO.initClass( OO.ui.Error );
3098 * Check if error can be recovered from.
3100 * @return {boolean} Error is recoverable
3102 OO.ui.Error.prototype.isRecoverable = function () {
3103 return this.recoverable;
3107 * Check if the error is a warning
3109 * @return {boolean} Error is warning
3111 OO.ui.Error.prototype.isWarning = function () {
3112 return this.warning;
3116 * Get error message as DOM nodes.
3118 * @return {jQuery} Error message in DOM nodes
3120 OO.ui.Error.prototype.getMessage = function () {
3121 return this.message instanceof jQuery ?
3122 this.message.clone() :
3123 $( '<div>' ).text( this.message ).contents();
3127 * Get error message as text.
3129 * @return {string} Error message
3131 OO.ui.Error.prototype.getMessageText = function () {
3132 return this.message instanceof jQuery ? this.message.text() : this.message;
3136 * A list of functions, called in sequence.
3138 * If a function added to a process returns boolean false the process will stop; if it returns an
3139 * object with a `promise` method the process will use the promise to either continue to the next
3140 * step when the promise is resolved or stop when the promise is rejected.
3145 * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
3146 * call, see #createStep for more information
3147 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3149 * @return {Object} Step object, with `callback` and `context` properties
3151 OO.ui.Process = function ( step, context ) {
3156 if ( step !== undefined ) {
3157 this.next( step, context );
3163 OO.initClass( OO.ui.Process );
3168 * Start the process.
3170 * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
3171 * any of the steps return boolean false or a promise which gets rejected; upon stopping the
3172 * process, the remaining steps will not be taken
3174 OO.ui.Process.prototype.execute = function () {
3175 var i, len, promise;
3178 * Continue execution.
3181 * @param {Array} step A function and the context it should be called in
3182 * @return {Function} Function that continues the process
3184 function proceed( step ) {
3185 return function () {
3186 // Execute step in the correct context
3188 result = step.callback.call( step.context );
3190 if ( result === false ) {
3191 // Use rejected promise for boolean false results
3192 return $.Deferred().reject( [] ).promise();
3194 if ( typeof result === 'number' ) {
3196 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3198 // Use a delayed promise for numbers, expecting them to be in milliseconds
3199 deferred = $.Deferred();
3200 setTimeout( deferred.resolve, result );
3201 return deferred.promise();
3203 if ( result instanceof OO.ui.Error ) {
3204 // Use rejected promise for error
3205 return $.Deferred().reject( [ result ] ).promise();
3207 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3208 // Use rejected promise for list of errors
3209 return $.Deferred().reject( result ).promise();
3211 // Duck-type the object to see if it can produce a promise
3212 if ( result && $.isFunction( result.promise ) ) {
3213 // Use a promise generated from the result
3214 return result.promise();
3216 // Use resolved promise for other results
3217 return $.Deferred().resolve().promise();
3221 if ( this.steps.length ) {
3222 // Generate a chain reaction of promises
3223 promise = proceed( this.steps[ 0 ] )();
3224 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3225 promise = promise.then( proceed( this.steps[ i ] ) );
3228 promise = $.Deferred().resolve().promise();
3235 * Create a process step.
3238 * @param {number|jQuery.Promise|Function} step
3240 * - Number of milliseconds to wait; or
3241 * - Promise to wait to be resolved; or
3242 * - Function to execute
3243 * - If it returns boolean false the process will stop
3244 * - If it returns an object with a `promise` method the process will use the promise to either
3245 * continue to the next step when the promise is resolved or stop when the promise is rejected
3246 * - If it returns a number, the process will wait for that number of milliseconds before
3248 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3250 * @return {Object} Step object, with `callback` and `context` properties
3252 OO.ui.Process.prototype.createStep = function ( step, context ) {
3253 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3255 callback: function () {
3261 if ( $.isFunction( step ) ) {
3267 throw new Error( 'Cannot create process step: number, promise or function expected' );
3271 * Add step to the beginning of the process.
3273 * @inheritdoc #createStep
3274 * @return {OO.ui.Process} this
3277 OO.ui.Process.prototype.first = function ( step, context ) {
3278 this.steps.unshift( this.createStep( step, context ) );
3283 * Add step to the end of the process.
3285 * @inheritdoc #createStep
3286 * @return {OO.ui.Process} this
3289 OO.ui.Process.prototype.next = function ( step, context ) {
3290 this.steps.push( this.createStep( step, context ) );
3295 * Factory for tools.
3298 * @extends OO.Factory
3301 OO.ui.ToolFactory = function OoUiToolFactory() {
3302 // Parent constructor
3303 OO.ui.ToolFactory.super.call( this );
3308 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3313 * Get tools from the factory
3315 * @param {Array} include Included tools
3316 * @param {Array} exclude Excluded tools
3317 * @param {Array} promote Promoted tools
3318 * @param {Array} demote Demoted tools
3319 * @return {string[]} List of tools
3321 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3322 var i, len, included, promoted, demoted,
3326 // Collect included and not excluded tools
3327 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3330 promoted = this.extract( promote, used );
3331 demoted = this.extract( demote, used );
3334 for ( i = 0, len = included.length; i < len; i++ ) {
3335 if ( !used[ included[ i ] ] ) {
3336 auto.push( included[ i ] );
3340 return promoted.concat( auto ).concat( demoted );
3344 * Get a flat list of names from a list of names or groups.
3346 * Tools can be specified in the following ways:
3348 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3349 * - All tools in a group: `{ group: 'group-name' }`
3350 * - All tools: `'*'`
3353 * @param {Array|string} collection List of tools
3354 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3355 * names will be added as properties
3356 * @return {string[]} List of extracted names
3358 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3359 var i, len, item, name, tool,
3362 if ( collection === '*' ) {
3363 for ( name in this.registry ) {
3364 tool = this.registry[ name ];
3366 // Only add tools by group name when auto-add is enabled
3367 tool.static.autoAddToCatchall &&
3368 // Exclude already used tools
3369 ( !used || !used[ name ] )
3373 used[ name ] = true;
3377 } else if ( Array.isArray( collection ) ) {
3378 for ( i = 0, len = collection.length; i < len; i++ ) {
3379 item = collection[ i ];
3380 // Allow plain strings as shorthand for named tools
3381 if ( typeof item === 'string' ) {
3382 item = { name: item };
3384 if ( OO.isPlainObject( item ) ) {
3386 for ( name in this.registry ) {
3387 tool = this.registry[ name ];
3389 // Include tools with matching group
3390 tool.static.group === item.group &&
3391 // Only add tools by group name when auto-add is enabled
3392 tool.static.autoAddToGroup &&
3393 // Exclude already used tools
3394 ( !used || !used[ name ] )
3398 used[ name ] = true;
3402 // Include tools with matching name and exclude already used tools
3403 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
3404 names.push( item.name );
3406 used[ item.name ] = true;
3416 * Factory for tool groups.
3419 * @extends OO.Factory
3422 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3423 // Parent constructor
3424 OO.Factory.call( this );
3427 defaultClasses = this.constructor.static.getDefaultClasses();
3429 // Register default toolgroups
3430 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3431 this.register( defaultClasses[ i ] );
3437 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3439 /* Static Methods */
3442 * Get a default set of classes to be registered on construction
3444 * @return {Function[]} Default classes
3446 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3449 OO.ui.ListToolGroup,
3461 * @param {Object} [config] Configuration options
3463 OO.ui.Theme = function OoUiTheme( config ) {
3464 // Configuration initialization
3465 config = config || {};
3470 OO.initClass( OO.ui.Theme );
3475 * Get a list of classes to be applied to a widget.
3477 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
3478 * otherwise state transitions will not work properly.
3480 * @param {OO.ui.Element} element Element for which to get classes
3481 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3483 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
3484 return { on: [], off: [] };
3488 * Update CSS classes provided by the theme.
3490 * For elements with theme logic hooks, this should be called any time there's a state change.
3492 * @param {OO.ui.Element} element Element for which to update classes
3493 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3495 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
3496 var classes = this.getElementClasses( element );
3499 .removeClass( classes.off.join( ' ' ) )
3500 .addClass( classes.on.join( ' ' ) );
3504 * Element supporting "sequential focus navigation" using the 'tabindex' attribute.
3510 * @param {Object} [config] Configuration options
3511 * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element
3512 * @cfg {number|null} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
3513 * prevent tab focusing, use null to suppress the `tabindex` attribute.
3515 OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) {
3516 // Configuration initialization
3517 config = $.extend( { tabIndex: 0 }, config );
3520 this.$tabIndexed = null;
3521 this.tabIndex = null;
3524 this.connect( this, { disable: 'onDisable' } );
3527 this.setTabIndex( config.tabIndex );
3528 this.setTabIndexedElement( config.$tabIndexed || this.$element );
3533 OO.initClass( OO.ui.TabIndexedElement );
3538 * Set the element with `tabindex` attribute.
3540 * If an element is already set, it will be cleaned up before setting up the new element.
3542 * @param {jQuery} $tabIndexed Element to set tab index on
3545 OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
3546 var tabIndex = this.tabIndex;
3547 // Remove attributes from old $tabIndexed
3548 this.setTabIndex( null );
3549 // Force update of new $tabIndexed
3550 this.$tabIndexed = $tabIndexed;
3551 this.tabIndex = tabIndex;
3552 return this.updateTabIndex();
3556 * Set tab index value.
3558 * @param {number|null} tabIndex Tab index value or null for no tab index
3561 OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
3562 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
3564 if ( this.tabIndex !== tabIndex ) {
3565 this.tabIndex = tabIndex;
3566 this.updateTabIndex();
3573 * Update the `tabindex` attribute, in case of changes to tab index or
3578 OO.ui.TabIndexedElement.prototype.updateTabIndex = function () {
3579 if ( this.$tabIndexed ) {
3580 if ( this.tabIndex !== null ) {
3581 // Do not index over disabled elements
3582 this.$tabIndexed.attr( {
3583 tabindex: this.isDisabled() ? -1 : this.tabIndex,
3584 // ChromeVox and NVDA do not seem to inherit this from parent elements
3585 'aria-disabled': this.isDisabled().toString()
3588 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
3595 * Handle disable events.
3597 * @param {boolean} disabled Element is disabled
3599 OO.ui.TabIndexedElement.prototype.onDisable = function () {
3600 this.updateTabIndex();
3604 * Get tab index value.
3606 * @return {number|null} Tab index value
3608 OO.ui.TabIndexedElement.prototype.getTabIndex = function () {
3609 return this.tabIndex;
3613 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
3614 * interface element that can be configured with access keys for accessibility.
3615 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
3617 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
3622 * @param {Object} [config] Configuration options
3623 * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
3624 * @cfg {boolean} [framed=true] Render button with a frame
3625 * @cfg {string} [accessKey] Button's access key
3627 OO.ui.ButtonElement = function OoUiButtonElement( config ) {
3628 // Configuration initialization
3629 config = config || {};
3632 this.$button = config.$button || $( '<a>' );
3634 this.accessKey = null;
3635 this.active = false;
3636 this.onMouseUpHandler = this.onMouseUp.bind( this );
3637 this.onMouseDownHandler = this.onMouseDown.bind( this );
3638 this.onKeyDownHandler = this.onKeyDown.bind( this );
3639 this.onKeyUpHandler = this.onKeyUp.bind( this );
3640 this.onClickHandler = this.onClick.bind( this );
3641 this.onKeyPressHandler = this.onKeyPress.bind( this );
3644 this.$element.addClass( 'oo-ui-buttonElement' );
3645 this.toggleFramed( config.framed === undefined || config.framed );
3646 this.setAccessKey( config.accessKey );
3647 this.setButtonElement( this.$button );
3652 OO.initClass( OO.ui.ButtonElement );
3654 /* Static Properties */
3657 * Cancel mouse down events.
3661 * @property {boolean}
3663 OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
3674 * Set the button element.
3676 * If an element is already set, it will be cleaned up before setting up the new element.
3678 * @param {jQuery} $button Element to use as button
3680 OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
3681 if ( this.$button ) {
3683 .removeClass( 'oo-ui-buttonElement-button' )
3684 .removeAttr( 'role accesskey' )
3686 mousedown: this.onMouseDownHandler,
3687 keydown: this.onKeyDownHandler,
3688 click: this.onClickHandler,
3689 keypress: this.onKeyPressHandler
3693 this.$button = $button
3694 .addClass( 'oo-ui-buttonElement-button' )
3695 .attr( { role: 'button', accesskey: this.accessKey } )
3697 mousedown: this.onMouseDownHandler,
3698 keydown: this.onKeyDownHandler,
3699 click: this.onClickHandler,
3700 keypress: this.onKeyPressHandler
3705 * Handles mouse down events.
3708 * @param {jQuery.Event} e Mouse down event
3710 OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
3711 if ( this.isDisabled() || e.which !== 1 ) {
3714 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3715 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
3716 // reliably remove the pressed class
3717 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
3718 // Prevent change of focus unless specifically configured otherwise
3719 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
3725 * Handles mouse up events.
3728 * @param {jQuery.Event} e Mouse up event
3730 OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
3731 if ( this.isDisabled() || e.which !== 1 ) {
3734 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
3735 // Stop listening for mouseup, since we only needed this once
3736 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
3740 * Handles mouse click events.
3743 * @param {jQuery.Event} e Mouse click event
3746 OO.ui.ButtonElement.prototype.onClick = function ( e ) {
3747 if ( !this.isDisabled() && e.which === 1 ) {
3748 this.emit( 'click' );
3754 * Handles key down events.
3757 * @param {jQuery.Event} e Key down event
3759 OO.ui.ButtonElement.prototype.onKeyDown = function ( e ) {
3760 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
3763 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3764 // Run the keyup handler no matter where the key is when the button is let go, so we can
3765 // reliably remove the pressed class
3766 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
3770 * Handles key up events.
3773 * @param {jQuery.Event} e Key up event
3775 OO.ui.ButtonElement.prototype.onKeyUp = function ( e ) {
3776 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
3779 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
3780 // Stop listening for keyup, since we only needed this once
3781 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
3785 * Handles key press events.
3788 * @param {jQuery.Event} e Key press event
3791 OO.ui.ButtonElement.prototype.onKeyPress = function ( e ) {
3792 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
3793 this.emit( 'click' );
3799 * Check if button has a frame.
3801 * @return {boolean} Button is framed
3803 OO.ui.ButtonElement.prototype.isFramed = function () {
3810 * @param {boolean} [framed] Make button framed, omit to toggle
3813 OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) {
3814 framed = framed === undefined ? !this.framed : !!framed;
3815 if ( framed !== this.framed ) {
3816 this.framed = framed;
3818 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
3819 .toggleClass( 'oo-ui-buttonElement-framed', framed );
3820 this.updateThemeClasses();
3829 * @param {string} accessKey Button's access key, use empty string to remove
3832 OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
3833 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
3835 if ( this.accessKey !== accessKey ) {
3836 if ( this.$button ) {
3837 if ( accessKey !== null ) {
3838 this.$button.attr( 'accesskey', accessKey );
3840 this.$button.removeAttr( 'accesskey' );
3843 this.accessKey = accessKey;
3852 * @param {boolean} [value] Make button active
3855 OO.ui.ButtonElement.prototype.setActive = function ( value ) {
3856 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
3861 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
3862 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
3863 * items from the group is done through the interface the class provides.
3864 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
3866 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
3872 * @param {Object} [config] Configuration options
3873 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
3875 OO.ui.GroupElement = function OoUiGroupElement( config ) {
3876 // Configuration initialization
3877 config = config || {};
3882 this.aggregateItemEvents = {};
3885 this.setGroupElement( config.$group || $( '<div>' ) );
3891 * Set the group element.
3893 * If an element is already set, items will be moved to the new element.
3895 * @param {jQuery} $group Element to use as group
3897 OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) {
3900 this.$group = $group;
3901 for ( i = 0, len = this.items.length; i < len; i++ ) {
3902 this.$group.append( this.items[ i ].$element );
3907 * Check if there are no items.
3909 * @return {boolean} Group is empty
3911 OO.ui.GroupElement.prototype.isEmpty = function () {
3912 return !this.items.length;
3918 * @return {OO.ui.Element[]} Items
3920 OO.ui.GroupElement.prototype.getItems = function () {
3921 return this.items.slice( 0 );
3925 * Get an item by its data.
3927 * Data is compared by a hash of its value. Only the first item with matching data will be returned.
3929 * @param {Object} data Item data to search for
3930 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
3932 OO.ui.GroupElement.prototype.getItemFromData = function ( data ) {
3934 hash = OO.getHash( data );
3936 for ( i = 0, len = this.items.length; i < len; i++ ) {
3937 item = this.items[ i ];
3938 if ( hash === OO.getHash( item.getData() ) ) {
3947 * Get items by their data.
3949 * Data is compared by a hash of its value. All items with matching data will be returned.
3951 * @param {Object} data Item data to search for
3952 * @return {OO.ui.Element[]} Items with equivalent data
3954 OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) {
3956 hash = OO.getHash( data ),
3959 for ( i = 0, len = this.items.length; i < len; i++ ) {
3960 item = this.items[ i ];
3961 if ( hash === OO.getHash( item.getData() ) ) {
3970 * Add an aggregate item event.
3972 * Aggregated events are listened to on each item and then emitted by the group under a new name,
3973 * and with an additional leading parameter containing the item that emitted the original event.
3974 * Other arguments that were emitted from the original event are passed through.
3976 * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
3977 * event, use null value to remove aggregation
3978 * @throws {Error} If aggregation already exists
3980 OO.ui.GroupElement.prototype.aggregate = function ( events ) {
3981 var i, len, item, add, remove, itemEvent, groupEvent;
3983 for ( itemEvent in events ) {
3984 groupEvent = events[ itemEvent ];
3986 // Remove existing aggregated event
3987 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
3988 // Don't allow duplicate aggregations
3990 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
3992 // Remove event aggregation from existing items
3993 for ( i = 0, len = this.items.length; i < len; i++ ) {
3994 item = this.items[ i ];
3995 if ( item.connect && item.disconnect ) {
3997 remove[ itemEvent ] = [ 'emit', groupEvent, item ];
3998 item.disconnect( this, remove );
4001 // Prevent future items from aggregating event
4002 delete this.aggregateItemEvents[ itemEvent ];
4005 // Add new aggregate event
4007 // Make future items aggregate event
4008 this.aggregateItemEvents[ itemEvent ] = groupEvent;
4009 // Add event aggregation to existing items
4010 for ( i = 0, len = this.items.length; i < len; i++ ) {
4011 item = this.items[ i ];
4012 if ( item.connect && item.disconnect ) {
4014 add[ itemEvent ] = [ 'emit', groupEvent, item ];
4015 item.connect( this, add );
4025 * Adding an existing item will move it.
4027 * @param {OO.ui.Element[]} items Items
4028 * @param {number} [index] Index to insert items at
4031 OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
4032 var i, len, item, event, events, currentIndex,
4035 for ( i = 0, len = items.length; i < len; i++ ) {
4038 // Check if item exists then remove it first, effectively "moving" it
4039 currentIndex = $.inArray( item, this.items );
4040 if ( currentIndex >= 0 ) {
4041 this.removeItems( [ item ] );
4042 // Adjust index to compensate for removal
4043 if ( currentIndex < index ) {
4048 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4050 for ( event in this.aggregateItemEvents ) {
4051 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
4053 item.connect( this, events );
4055 item.setElementGroup( this );
4056 itemElements.push( item.$element.get( 0 ) );
4059 if ( index === undefined || index < 0 || index >= this.items.length ) {
4060 this.$group.append( itemElements );
4061 this.items.push.apply( this.items, items );
4062 } else if ( index === 0 ) {
4063 this.$group.prepend( itemElements );
4064 this.items.unshift.apply( this.items, items );
4066 this.items[ index ].$element.before( itemElements );
4067 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4076 * Items will be detached, not removed, so they can be used later.
4078 * @param {OO.ui.Element[]} items Items to remove
4081 OO.ui.GroupElement.prototype.removeItems = function ( items ) {
4082 var i, len, item, index, remove, itemEvent;
4084 // Remove specific items
4085 for ( i = 0, len = items.length; i < len; i++ ) {
4087 index = $.inArray( item, this.items );
4088 if ( index !== -1 ) {
4090 item.connect && item.disconnect &&
4091 !$.isEmptyObject( this.aggregateItemEvents )
4094 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4095 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4097 item.disconnect( this, remove );
4099 item.setElementGroup( null );
4100 this.items.splice( index, 1 );
4101 item.$element.detach();
4111 * Items will be detached, not removed, so they can be used later.
4115 OO.ui.GroupElement.prototype.clearItems = function () {
4116 var i, len, item, remove, itemEvent;
4119 for ( i = 0, len = this.items.length; i < len; i++ ) {
4120 item = this.items[ i ];
4122 item.connect && item.disconnect &&
4123 !$.isEmptyObject( this.aggregateItemEvents )
4126 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4127 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4129 item.disconnect( this, remove );
4131 item.setElementGroup( null );
4132 item.$element.detach();
4140 * DraggableElement is a mixin class used to create elements that can be clicked
4141 * and dragged by a mouse to a new position within a group. This class must be used
4142 * in conjunction with OO.ui.DraggableGroupElement, which provides a container for
4143 * the draggable elements.
4150 OO.ui.DraggableElement = function OoUiDraggableElement() {
4154 // Initialize and events
4156 .attr( 'draggable', true )
4157 .addClass( 'oo-ui-draggableElement' )
4159 dragstart: this.onDragStart.bind( this ),
4160 dragover: this.onDragOver.bind( this ),
4161 dragend: this.onDragEnd.bind( this ),
4162 drop: this.onDrop.bind( this )
4166 OO.initClass( OO.ui.DraggableElement );
4172 * @param {OO.ui.DraggableElement} item Dragging item
4183 /* Static Properties */
4186 * @inheritdoc OO.ui.ButtonElement
4188 OO.ui.DraggableElement.static.cancelButtonMouseDownEvents = false;
4193 * Respond to dragstart event.
4194 * @param {jQuery.Event} event jQuery event
4197 OO.ui.DraggableElement.prototype.onDragStart = function ( e ) {
4198 var dataTransfer = e.originalEvent.dataTransfer;
4199 // Define drop effect
4200 dataTransfer.dropEffect = 'none';
4201 dataTransfer.effectAllowed = 'move';
4202 // We must set up a dataTransfer data property or Firefox seems to
4203 // ignore the fact the element is draggable.
4205 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4207 // The above is only for firefox. No need to set a catch clause
4208 // if it fails, move on.
4210 // Add dragging class
4211 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4213 this.emit( 'dragstart', this );
4218 * Respond to dragend event.
4221 OO.ui.DraggableElement.prototype.onDragEnd = function () {
4222 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4223 this.emit( 'dragend' );
4227 * Handle drop event.
4228 * @param {jQuery.Event} event jQuery event
4231 OO.ui.DraggableElement.prototype.onDrop = function ( e ) {
4233 this.emit( 'drop', e );
4237 * In order for drag/drop to work, the dragover event must
4238 * return false and stop propogation.
4240 OO.ui.DraggableElement.prototype.onDragOver = function ( e ) {
4246 * Store it in the DOM so we can access from the widget drag event
4247 * @param {number} Item index
4249 OO.ui.DraggableElement.prototype.setIndex = function ( index ) {
4250 if ( this.index !== index ) {
4252 this.$element.data( 'index', index );
4258 * @return {number} Item index
4260 OO.ui.DraggableElement.prototype.getIndex = function () {
4265 * DraggableGroupElement is a mixin class used to create a group element to
4266 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4267 * The class is used with OO.ui.DraggableElement.
4273 * @param {Object} [config] Configuration options
4274 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4275 * @cfg {string} [orientation] Item orientation, 'horizontal' or 'vertical'. Defaults to 'vertical'
4277 OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
4278 // Configuration initialization
4279 config = config || {};
4281 // Parent constructor
4282 OO.ui.GroupElement.call( this, config );
4285 this.orientation = config.orientation || 'vertical';
4286 this.dragItem = null;
4287 this.itemDragOver = null;
4289 this.sideInsertion = '';
4293 dragstart: 'itemDragStart',
4294 dragend: 'itemDragEnd',
4297 this.connect( this, {
4298 itemDragStart: 'onItemDragStart',
4299 itemDrop: 'onItemDrop',
4300 itemDragEnd: 'onItemDragEnd'
4303 dragover: $.proxy( this.onDragOver, this ),
4304 dragleave: $.proxy( this.onDragLeave, this )
4308 if ( Array.isArray( config.items ) ) {
4309 this.addItems( config.items );
4311 this.$placeholder = $( '<div>' )
4312 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4314 .addClass( 'oo-ui-draggableGroupElement' )
4315 .append( this.$status )
4316 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4317 .prepend( this.$placeholder );
4321 OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
4327 * @param {OO.ui.DraggableElement} item Reordered item
4328 * @param {number} [newIndex] New index for the item
4334 * Respond to item drag start event
4335 * @param {OO.ui.DraggableElement} item Dragged item
4337 OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4340 // Map the index of each object
4341 for ( i = 0, len = this.items.length; i < len; i++ ) {
4342 this.items[ i ].setIndex( i );
4345 if ( this.orientation === 'horizontal' ) {
4346 // Set the height of the indicator
4347 this.$placeholder.css( {
4348 height: item.$element.outerHeight(),
4352 // Set the width of the indicator
4353 this.$placeholder.css( {
4355 width: item.$element.outerWidth()
4358 this.setDragItem( item );
4362 * Respond to item drag end event
4364 OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
4365 this.unsetDragItem();
4370 * Handle drop event and switch the order of the items accordingly
4371 * @param {OO.ui.DraggableElement} item Dropped item
4374 OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
4375 var toIndex = item.getIndex();
4376 // Check if the dropped item is from the current group
4377 // TODO: Figure out a way to configure a list of legally droppable
4378 // elements even if they are not yet in the list
4379 if ( this.getDragItem() ) {
4380 // If the insertion point is 'after', the insertion index
4381 // is shifted to the right (or to the left in RTL, hence 'after')
4382 if ( this.sideInsertion === 'after' ) {
4385 // Emit change event
4386 this.emit( 'reorder', this.getDragItem(), toIndex );
4388 this.unsetDragItem();
4389 // Return false to prevent propogation
4394 * Handle dragleave event.
4396 OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
4397 // This means the item was dragged outside the widget
4400 .addClass( 'oo-ui-element-hidden' );
4404 * Respond to dragover event
4405 * @param {jQuery.Event} event Event details
4407 OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
4408 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
4409 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
4410 clientX = e.originalEvent.clientX,
4411 clientY = e.originalEvent.clientY;
4413 // Get the OptionWidget item we are dragging over
4414 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
4415 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
4416 if ( $optionWidget[ 0 ] ) {
4417 itemOffset = $optionWidget.offset();
4418 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
4419 itemPosition = $optionWidget.position();
4420 itemIndex = $optionWidget.data( 'index' );
4425 this.isDragging() &&
4426 itemIndex !== this.getDragItem().getIndex()
4428 if ( this.orientation === 'horizontal' ) {
4429 // Calculate where the mouse is relative to the item width
4430 itemSize = itemBoundingRect.width;
4431 itemMidpoint = itemBoundingRect.left + itemSize / 2;
4432 dragPosition = clientX;
4433 // Which side of the item we hover over will dictate
4434 // where the placeholder will appear, on the left or
4437 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
4438 top: itemPosition.top
4441 // Calculate where the mouse is relative to the item height
4442 itemSize = itemBoundingRect.height;
4443 itemMidpoint = itemBoundingRect.top + itemSize / 2;
4444 dragPosition = clientY;
4445 // Which side of the item we hover over will dictate
4446 // where the placeholder will appear, on the top or
4449 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
4450 left: itemPosition.left
4453 // Store whether we are before or after an item to rearrange
4454 // For horizontal layout, we need to account for RTL, as this is flipped
4455 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
4456 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
4458 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
4460 // Add drop indicator between objects
4463 .removeClass( 'oo-ui-element-hidden' );
4465 // This means the item was dragged outside the widget
4468 .addClass( 'oo-ui-element-hidden' );
4475 * Set a dragged item
4476 * @param {OO.ui.DraggableElement} item Dragged item
4478 OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
4479 this.dragItem = item;
4483 * Unset the current dragged item
4485 OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
4486 this.dragItem = null;
4487 this.itemDragOver = null;
4488 this.$placeholder.addClass( 'oo-ui-element-hidden' );
4489 this.sideInsertion = '';
4493 * Get the current dragged item
4494 * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
4496 OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
4497 return this.dragItem;
4501 * Check if there's an item being dragged.
4502 * @return {Boolean} Item is being dragged
4504 OO.ui.DraggableGroupElement.prototype.isDragging = function () {
4505 return this.getDragItem() !== null;
4509 * IconElement is often mixed into other classes to generate an icon.
4510 * Icons are graphics, about the size of normal text. They are used to aid the user
4511 * in locating a control or to convey information in a space-efficient way. See the
4512 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
4513 * included in the library.
4515 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4521 * @param {Object} [config] Configuration options
4522 * @cfg {jQuery} [$icon] Icon node, assigned to #$icon, omit to use a generated `<span>`
4523 * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
4524 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4526 * @cfg {string} [iconTitle] Icon title text or a function that returns text
4528 OO.ui.IconElement = function OoUiIconElement( config ) {
4529 // Configuration initialization
4530 config = config || {};
4535 this.iconTitle = null;
4538 this.setIcon( config.icon || this.constructor.static.icon );
4539 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
4540 this.setIconElement( config.$icon || $( '<span>' ) );
4545 OO.initClass( OO.ui.IconElement );
4547 /* Static Properties */
4550 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
4551 * for i18n purposes and contains a `default` icon name and additional names keyed by
4552 * language code. The `default` name is used when no icon is keyed by the user's language.
4554 * Example of an i18n map:
4556 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4558 * Note: the static property will be overridden if the #icon configuration is used.
4562 * @property {Object|string}
4564 OO.ui.IconElement.static.icon = null;
4567 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
4568 * function that returns title text, or `null` for no title.
4570 * The static property will be overridden if the #iconTitle configuration is used.
4574 * @property {string|Function|null}
4576 OO.ui.IconElement.static.iconTitle = null;
4581 * Set the icon element.
4583 * If an element is already set, it will be cleaned up before setting up the new element.
4585 * @param {jQuery} $icon Element to use as icon
4587 OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
4590 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
4591 .removeAttr( 'title' );
4595 .addClass( 'oo-ui-iconElement-icon' )
4596 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
4597 if ( this.iconTitle !== null ) {
4598 this.$icon.attr( 'title', this.iconTitle );
4605 * @param {Object|string|null} icon Symbolic icon name, or map of icon names keyed by language ID;
4606 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4607 * language, use null to remove icon
4610 OO.ui.IconElement.prototype.setIcon = function ( icon ) {
4611 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
4612 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
4614 if ( this.icon !== icon ) {
4616 if ( this.icon !== null ) {
4617 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
4619 if ( icon !== null ) {
4620 this.$icon.addClass( 'oo-ui-icon-' + icon );
4626 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
4627 this.updateThemeClasses();
4635 * @param {string|Function|null} icon Icon title text, a function that returns text or null
4639 OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
4640 iconTitle = typeof iconTitle === 'function' ||
4641 ( typeof iconTitle === 'string' && iconTitle.length ) ?
4642 OO.ui.resolveMsg( iconTitle ) : null;
4644 if ( this.iconTitle !== iconTitle ) {
4645 this.iconTitle = iconTitle;
4647 if ( this.iconTitle !== null ) {
4648 this.$icon.attr( 'title', iconTitle );
4650 this.$icon.removeAttr( 'title' );
4661 * @return {string} Icon name
4663 OO.ui.IconElement.prototype.getIcon = function () {
4670 * @return {string} Icon title text
4672 OO.ui.IconElement.prototype.getIconTitle = function () {
4673 return this.iconTitle;
4677 * IndicatorElement is often mixed into other classes to generate an indicator.
4678 * Indicators are small graphics that are generally used in two ways:
4680 * - To draw attention to the status of an item. For example, an indicator might be
4681 * used to show that an item in a list has errors that need to be resolved.
4682 * - To clarify the function of a control that acts in an exceptional way (a button
4683 * that opens a menu instead of performing an action directly, for example).
4685 * For a list of indicators included in the library, please see the
4686 * [OOjs UI documentation on MediaWiki] [1].
4688 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4694 * @param {Object} [config] Configuration options
4695 * @cfg {jQuery} [$indicator] Indicator node, assigned to #$indicator, omit to use a generated
4697 * @cfg {string} [indicator] Symbolic indicator name
4698 * @cfg {string} [indicatorTitle] Indicator title text or a function that returns text
4700 OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
4701 // Configuration initialization
4702 config = config || {};
4705 this.$indicator = null;
4706 this.indicator = null;
4707 this.indicatorTitle = null;
4710 this.setIndicator( config.indicator || this.constructor.static.indicator );
4711 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
4712 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
4717 OO.initClass( OO.ui.IndicatorElement );
4719 /* Static Properties */
4726 * @property {string|null} Symbolic indicator name
4728 OO.ui.IndicatorElement.static.indicator = null;
4735 * @property {string|Function|null} Indicator title text, a function that returns text or null for no
4738 OO.ui.IndicatorElement.static.indicatorTitle = null;
4743 * Set the indicator element.
4745 * If an element is already set, it will be cleaned up before setting up the new element.
4747 * @param {jQuery} $indicator Element to use as indicator
4749 OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
4750 if ( this.$indicator ) {
4752 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
4753 .removeAttr( 'title' );
4756 this.$indicator = $indicator
4757 .addClass( 'oo-ui-indicatorElement-indicator' )
4758 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
4759 if ( this.indicatorTitle !== null ) {
4760 this.$indicator.attr( 'title', this.indicatorTitle );
4765 * Set indicator name.
4767 * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
4770 OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) {
4771 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
4773 if ( this.indicator !== indicator ) {
4774 if ( this.$indicator ) {
4775 if ( this.indicator !== null ) {
4776 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
4778 if ( indicator !== null ) {
4779 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
4782 this.indicator = indicator;
4785 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
4786 this.updateThemeClasses();
4792 * Set indicator title.
4794 * @param {string|Function|null} indicator Indicator title text, a function that returns text or
4795 * null for no indicator title
4798 OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
4799 indicatorTitle = typeof indicatorTitle === 'function' ||
4800 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
4801 OO.ui.resolveMsg( indicatorTitle ) : null;
4803 if ( this.indicatorTitle !== indicatorTitle ) {
4804 this.indicatorTitle = indicatorTitle;
4805 if ( this.$indicator ) {
4806 if ( this.indicatorTitle !== null ) {
4807 this.$indicator.attr( 'title', indicatorTitle );
4809 this.$indicator.removeAttr( 'title' );
4818 * Get indicator name.
4820 * @return {string} Symbolic name of indicator
4822 OO.ui.IndicatorElement.prototype.getIndicator = function () {
4823 return this.indicator;
4827 * Get indicator title.
4829 * @return {string} Indicator title text
4831 OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
4832 return this.indicatorTitle;
4836 * Element containing a label.
4842 * @param {Object} [config] Configuration options
4843 * @cfg {jQuery} [$label] Label node, assigned to #$label, omit to use a generated `<span>`
4844 * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
4845 * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
4847 OO.ui.LabelElement = function OoUiLabelElement( config ) {
4848 // Configuration initialization
4849 config = config || {};
4854 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
4857 this.setLabel( config.label || this.constructor.static.label );
4858 this.setLabelElement( config.$label || $( '<span>' ) );
4863 OO.initClass( OO.ui.LabelElement );
4868 * @event labelChange
4869 * @param {string} value
4872 /* Static Properties */
4879 * @property {string|Function|null} Label text; a function that returns nodes or text; or null for
4882 OO.ui.LabelElement.static.label = null;
4887 * Set the label element.
4889 * If an element is already set, it will be cleaned up before setting up the new element.
4891 * @param {jQuery} $label Element to use as label
4893 OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) {
4894 if ( this.$label ) {
4895 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
4898 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
4899 this.setLabelContent( this.label );
4905 * An empty string will result in the label being hidden. A string containing only whitespace will
4906 * be converted to a single ` `.
4908 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4909 * text; or null for no label
4912 OO.ui.LabelElement.prototype.setLabel = function ( label ) {
4913 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
4914 label = ( typeof label === 'string' && label.length ) || label instanceof jQuery ? label : null;
4916 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
4918 if ( this.label !== label ) {
4919 if ( this.$label ) {
4920 this.setLabelContent( label );
4923 this.emit( 'labelChange' );
4932 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
4933 * text; or null for no label
4935 OO.ui.LabelElement.prototype.getLabel = function () {
4944 OO.ui.LabelElement.prototype.fitLabel = function () {
4945 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
4946 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
4953 * Set the content of the label.
4955 * Do not call this method until after the label element has been set by #setLabelElement.
4958 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4959 * text; or null for no label
4961 OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
4962 if ( typeof label === 'string' ) {
4963 if ( label.match( /^\s*$/ ) ) {
4964 // Convert whitespace only string to a single non-breaking space
4965 this.$label.html( ' ' );
4967 this.$label.text( label );
4969 } else if ( label instanceof jQuery ) {
4970 this.$label.empty().append( label );
4972 this.$label.empty();
4977 * Mixin that adds a menu showing suggested values for a OO.ui.TextInputWidget.
4979 * Subclasses that set the value of #lookupInput from #onLookupMenuItemChoose should
4980 * be aware that this will cause new suggestions to be looked up for the new value. If this is
4981 * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
4987 * @param {Object} [config] Configuration options
4988 * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
4989 * @cfg {jQuery} [$container=this.$element] Element to render menu under
4991 OO.ui.LookupElement = function OoUiLookupElement( config ) {
4992 // Configuration initialization
4993 config = config || {};
4996 this.$overlay = config.$overlay || this.$element;
4997 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
5000 $container: config.$container
5002 this.lookupCache = {};
5003 this.lookupQuery = null;
5004 this.lookupRequest = null;
5005 this.lookupsDisabled = false;
5006 this.lookupInputFocused = false;
5010 focus: this.onLookupInputFocus.bind( this ),
5011 blur: this.onLookupInputBlur.bind( this ),
5012 mousedown: this.onLookupInputMouseDown.bind( this )
5014 this.connect( this, { change: 'onLookupInputChange' } );
5015 this.lookupMenu.connect( this, {
5016 toggle: 'onLookupMenuToggle',
5017 choose: 'onLookupMenuItemChoose'
5021 this.$element.addClass( 'oo-ui-lookupElement' );
5022 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
5023 this.$overlay.append( this.lookupMenu.$element );
5029 * Handle input focus event.
5031 * @param {jQuery.Event} e Input focus event
5033 OO.ui.LookupElement.prototype.onLookupInputFocus = function () {
5034 this.lookupInputFocused = true;
5035 this.populateLookupMenu();
5039 * Handle input blur event.
5041 * @param {jQuery.Event} e Input blur event
5043 OO.ui.LookupElement.prototype.onLookupInputBlur = function () {
5044 this.closeLookupMenu();
5045 this.lookupInputFocused = false;
5049 * Handle input mouse down event.
5051 * @param {jQuery.Event} e Input mouse down event
5053 OO.ui.LookupElement.prototype.onLookupInputMouseDown = function () {
5054 // Only open the menu if the input was already focused.
5055 // This way we allow the user to open the menu again after closing it with Esc
5056 // by clicking in the input. Opening (and populating) the menu when initially
5057 // clicking into the input is handled by the focus handler.
5058 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
5059 this.populateLookupMenu();
5064 * Handle input change event.
5066 * @param {string} value New input value
5068 OO.ui.LookupElement.prototype.onLookupInputChange = function () {
5069 if ( this.lookupInputFocused ) {
5070 this.populateLookupMenu();
5075 * Handle the lookup menu being shown/hidden.
5077 * @param {boolean} visible Whether the lookup menu is now visible.
5079 OO.ui.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
5081 // When the menu is hidden, abort any active request and clear the menu.
5082 // This has to be done here in addition to closeLookupMenu(), because
5083 // MenuSelectWidget will close itself when the user presses Esc.
5084 this.abortLookupRequest();
5085 this.lookupMenu.clearItems();
5090 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5092 * @param {OO.ui.MenuOptionWidget|null} item Selected item
5094 OO.ui.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
5096 this.setValue( item.getData() );
5103 * @return {OO.ui.TextInputMenuSelectWidget}
5105 OO.ui.LookupElement.prototype.getLookupMenu = function () {
5106 return this.lookupMenu;
5110 * Disable or re-enable lookups.
5112 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5114 * @param {boolean} disabled Disable lookups
5116 OO.ui.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
5117 this.lookupsDisabled = !!disabled;
5121 * Open the menu. If there are no entries in the menu, this does nothing.
5125 OO.ui.LookupElement.prototype.openLookupMenu = function () {
5126 if ( !this.lookupMenu.isEmpty() ) {
5127 this.lookupMenu.toggle( true );
5133 * Close the menu, empty it, and abort any pending request.
5137 OO.ui.LookupElement.prototype.closeLookupMenu = function () {
5138 this.lookupMenu.toggle( false );
5139 this.abortLookupRequest();
5140 this.lookupMenu.clearItems();
5145 * Request menu items based on the input's current value, and when they arrive,
5146 * populate the menu with these items and show the menu.
5148 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5152 OO.ui.LookupElement.prototype.populateLookupMenu = function () {
5154 value = this.getValue();
5156 if ( this.lookupsDisabled ) {
5160 // If the input is empty, clear the menu
5161 if ( value === '' ) {
5162 this.closeLookupMenu();
5163 // Skip population if there is already a request pending for the current value
5164 } else if ( value !== this.lookupQuery ) {
5165 this.getLookupMenuItems()
5166 .done( function ( items ) {
5167 widget.lookupMenu.clearItems();
5168 if ( items.length ) {
5172 widget.initializeLookupMenuSelection();
5174 widget.lookupMenu.toggle( false );
5177 .fail( function () {
5178 widget.lookupMenu.clearItems();
5186 * Select and highlight the first selectable item in the menu.
5190 OO.ui.LookupElement.prototype.initializeLookupMenuSelection = function () {
5191 if ( !this.lookupMenu.getSelectedItem() ) {
5192 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
5194 this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
5198 * Get lookup menu items for the current query.
5200 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5201 * the done event. If the request was aborted to make way for a subsequent request, this promise
5202 * will not be rejected: it will remain pending forever.
5204 OO.ui.LookupElement.prototype.getLookupMenuItems = function () {
5206 value = this.getValue(),
5207 deferred = $.Deferred(),
5210 this.abortLookupRequest();
5211 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
5212 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
5215 this.lookupQuery = value;
5216 ourRequest = this.lookupRequest = this.getLookupRequest();
5218 .always( function () {
5219 // We need to pop pending even if this is an old request, otherwise
5220 // the widget will remain pending forever.
5221 // TODO: this assumes that an aborted request will fail or succeed soon after
5222 // being aborted, or at least eventually. It would be nice if we could popPending()
5223 // at abort time, but only if we knew that we hadn't already called popPending()
5224 // for that request.
5225 widget.popPending();
5227 .done( function ( data ) {
5228 // If this is an old request (and aborting it somehow caused it to still succeed),
5229 // ignore its success completely
5230 if ( ourRequest === widget.lookupRequest ) {
5231 widget.lookupQuery = null;
5232 widget.lookupRequest = null;
5233 widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( data );
5234 deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
5237 .fail( function () {
5238 // If this is an old request (or a request failing because it's being aborted),
5239 // ignore its failure completely
5240 if ( ourRequest === widget.lookupRequest ) {
5241 widget.lookupQuery = null;
5242 widget.lookupRequest = null;
5247 return deferred.promise();
5251 * Abort the currently pending lookup request, if any.
5253 OO.ui.LookupElement.prototype.abortLookupRequest = function () {
5254 var oldRequest = this.lookupRequest;
5256 // First unset this.lookupRequest to the fail handler will notice
5257 // that the request is no longer current
5258 this.lookupRequest = null;
5259 this.lookupQuery = null;
5265 * Get a new request object of the current lookup query value.
5268 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5270 OO.ui.LookupElement.prototype.getLookupRequest = function () {
5271 // Stub, implemented in subclass
5276 * Pre-process data returned by the request from #getLookupRequest.
5278 * The return value of this function will be cached, and any further queries for the given value
5279 * will use the cache rather than doing API requests.
5282 * @param {Mixed} data Response from server
5283 * @return {Mixed} Cached result data
5285 OO.ui.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
5286 // Stub, implemented in subclass
5291 * Get a list of menu option widgets from the (possibly cached) data returned by
5292 * #getLookupCacheDataFromResponse.
5295 * @param {Mixed} data Cached result data, usually an array
5296 * @return {OO.ui.MenuOptionWidget[]} Menu items
5298 OO.ui.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
5299 // Stub, implemented in subclass
5304 * Element containing an OO.ui.PopupWidget object.
5310 * @param {Object} [config] Configuration options
5311 * @cfg {Object} [popup] Configuration to pass to popup
5312 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5314 OO.ui.PopupElement = function OoUiPopupElement( config ) {
5315 // Configuration initialization
5316 config = config || {};
5319 this.popup = new OO.ui.PopupWidget( $.extend(
5320 { autoClose: true },
5322 { $autoCloseIgnore: this.$element }
5331 * @return {OO.ui.PopupWidget} Popup widget
5333 OO.ui.PopupElement.prototype.getPopup = function () {
5338 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
5339 * additional functionality to an element created by another class. The class provides
5340 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
5341 * which are used to customize the look and feel of a widget to better describe its
5342 * importance and functionality.
5344 * The library currently contains the following styling flags for general use:
5346 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
5347 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
5348 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
5350 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
5351 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
5353 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5359 * @param {Object} [config] Configuration options
5360 * @cfg {string|string[]} [flags] Flags describing importance and functionality, e.g. 'primary',
5361 * 'safe', 'progressive', 'destructive' or 'constructive'
5362 * @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element
5364 OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
5365 // Configuration initialization
5366 config = config || {};
5370 this.$flagged = null;
5373 this.setFlags( config.flags );
5374 this.setFlaggedElement( config.$flagged || this.$element );
5381 * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean
5382 * added/removed properties
5388 * Set the flagged element.
5390 * If an element is already set, it will be cleaned up before setting up the new element.
5392 * @param {jQuery} $flagged Element to add flags to
5394 OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
5395 var classNames = Object.keys( this.flags ).map( function ( flag ) {
5396 return 'oo-ui-flaggedElement-' + flag;
5399 if ( this.$flagged ) {
5400 this.$flagged.removeClass( classNames );
5403 this.$flagged = $flagged.addClass( classNames );
5407 * Check if a flag is set.
5409 * @param {string} flag Name of flag
5410 * @return {boolean} Has flag
5412 OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) {
5413 return flag in this.flags;
5417 * Get the names of all flags set.
5419 * @return {string[]} Flag names
5421 OO.ui.FlaggedElement.prototype.getFlags = function () {
5422 return Object.keys( this.flags );
5431 OO.ui.FlaggedElement.prototype.clearFlags = function () {
5432 var flag, className,
5435 classPrefix = 'oo-ui-flaggedElement-';
5437 for ( flag in this.flags ) {
5438 className = classPrefix + flag;
5439 changes[ flag ] = false;
5440 delete this.flags[ flag ];
5441 remove.push( className );
5444 if ( this.$flagged ) {
5445 this.$flagged.removeClass( remove.join( ' ' ) );
5448 this.updateThemeClasses();
5449 this.emit( 'flag', changes );
5455 * Add one or more flags.
5457 * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
5458 * keyed by flag name containing boolean set/remove instructions.
5462 OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) {
5463 var i, len, flag, className,
5467 classPrefix = 'oo-ui-flaggedElement-';
5469 if ( typeof flags === 'string' ) {
5470 className = classPrefix + flags;
5472 if ( !this.flags[ flags ] ) {
5473 this.flags[ flags ] = true;
5474 add.push( className );
5476 } else if ( Array.isArray( flags ) ) {
5477 for ( i = 0, len = flags.length; i < len; i++ ) {
5479 className = classPrefix + flag;
5481 if ( !this.flags[ flag ] ) {
5482 changes[ flag ] = true;
5483 this.flags[ flag ] = true;
5484 add.push( className );
5487 } else if ( OO.isPlainObject( flags ) ) {
5488 for ( flag in flags ) {
5489 className = classPrefix + flag;
5490 if ( flags[ flag ] ) {
5492 if ( !this.flags[ flag ] ) {
5493 changes[ flag ] = true;
5494 this.flags[ flag ] = true;
5495 add.push( className );
5499 if ( this.flags[ flag ] ) {
5500 changes[ flag ] = false;
5501 delete this.flags[ flag ];
5502 remove.push( className );
5508 if ( this.$flagged ) {
5510 .addClass( add.join( ' ' ) )
5511 .removeClass( remove.join( ' ' ) );
5514 this.updateThemeClasses();
5515 this.emit( 'flag', changes );
5521 * Element with a title.
5523 * Titles are rendered by the browser and are made visible when hovering the element. Titles are
5524 * not visible on touch devices.
5530 * @param {Object} [config] Configuration options
5531 * @cfg {jQuery} [$titled] Titled node, assigned to #$titled, omit to use #$element
5532 * @cfg {string|Function} [title] Title text or a function that returns text. If not provided, the
5533 * static property 'title' is used.
5535 OO.ui.TitledElement = function OoUiTitledElement( config ) {
5536 // Configuration initialization
5537 config = config || {};
5540 this.$titled = null;
5544 this.setTitle( config.title || this.constructor.static.title );
5545 this.setTitledElement( config.$titled || this.$element );
5550 OO.initClass( OO.ui.TitledElement );
5552 /* Static Properties */
5559 * @property {string|Function} Title text or a function that returns text
5561 OO.ui.TitledElement.static.title = null;
5566 * Set the titled element.
5568 * If an element is already set, it will be cleaned up before setting up the new element.
5570 * @param {jQuery} $titled Element to set title on
5572 OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) {
5573 if ( this.$titled ) {
5574 this.$titled.removeAttr( 'title' );
5577 this.$titled = $titled;
5579 this.$titled.attr( 'title', this.title );
5586 * @param {string|Function|null} title Title text, a function that returns text or null for no title
5589 OO.ui.TitledElement.prototype.setTitle = function ( title ) {
5590 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
5592 if ( this.title !== title ) {
5593 if ( this.$titled ) {
5594 if ( title !== null ) {
5595 this.$titled.attr( 'title', title );
5597 this.$titled.removeAttr( 'title' );
5609 * @return {string} Title string
5611 OO.ui.TitledElement.prototype.getTitle = function () {
5616 * Element that can be automatically clipped to visible boundaries.
5618 * Whenever the element's natural height changes, you have to call
5619 * #clip to make sure it's still clipping correctly.
5625 * @param {Object} [config] Configuration options
5626 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
5628 OO.ui.ClippableElement = function OoUiClippableElement( config ) {
5629 // Configuration initialization
5630 config = config || {};
5633 this.$clippable = null;
5634 this.clipping = false;
5635 this.clippedHorizontally = false;
5636 this.clippedVertically = false;
5637 this.$clippableContainer = null;
5638 this.$clippableScroller = null;
5639 this.$clippableWindow = null;
5640 this.idealWidth = null;
5641 this.idealHeight = null;
5642 this.onClippableContainerScrollHandler = this.clip.bind( this );
5643 this.onClippableWindowResizeHandler = this.clip.bind( this );
5646 this.setClippableElement( config.$clippable || this.$element );
5652 * Set clippable element.
5654 * If an element is already set, it will be cleaned up before setting up the new element.
5656 * @param {jQuery} $clippable Element to make clippable
5658 OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
5659 if ( this.$clippable ) {
5660 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
5661 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
5662 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5665 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
5672 * Do not turn clipping on until after the element is attached to the DOM and visible.
5674 * @param {boolean} [clipping] Enable clipping, omit to toggle
5677 OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
5678 clipping = clipping === undefined ? !this.clipping : !!clipping;
5680 if ( this.clipping !== clipping ) {
5681 this.clipping = clipping;
5683 this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
5684 // If the clippable container is the root, we have to listen to scroll events and check
5685 // jQuery.scrollTop on the window because of browser inconsistencies
5686 this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
5687 $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
5688 this.$clippableContainer;
5689 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
5690 this.$clippableWindow = $( this.getElementWindow() )
5691 .on( 'resize', this.onClippableWindowResizeHandler );
5692 // Initial clip after visible
5695 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
5696 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5698 this.$clippableContainer = null;
5699 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
5700 this.$clippableScroller = null;
5701 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
5702 this.$clippableWindow = null;
5710 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5712 * @return {boolean} Element will be clipped to the visible area
5714 OO.ui.ClippableElement.prototype.isClipping = function () {
5715 return this.clipping;
5719 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5721 * @return {boolean} Part of the element is being clipped
5723 OO.ui.ClippableElement.prototype.isClipped = function () {
5724 return this.clippedHorizontally || this.clippedVertically;
5728 * Check if the right of the element is being clipped by the nearest scrollable container.
5730 * @return {boolean} Part of the element is being clipped
5732 OO.ui.ClippableElement.prototype.isClippedHorizontally = function () {
5733 return this.clippedHorizontally;
5737 * Check if the bottom of the element is being clipped by the nearest scrollable container.
5739 * @return {boolean} Part of the element is being clipped
5741 OO.ui.ClippableElement.prototype.isClippedVertically = function () {
5742 return this.clippedVertically;
5746 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
5748 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
5749 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
5751 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
5752 this.idealWidth = width;
5753 this.idealHeight = height;
5755 if ( !this.clipping ) {
5756 // Update dimensions
5757 this.$clippable.css( { width: width, height: height } );
5759 // While clipping, idealWidth and idealHeight are not considered
5763 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
5764 * the element's natural height changes.
5766 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5767 * overlapped by, the visible area of the nearest scrollable container.
5771 OO.ui.ClippableElement.prototype.clip = function () {
5772 if ( !this.clipping ) {
5773 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
5777 var buffer = 7, // Chosen by fair dice roll
5778 cOffset = this.$clippable.offset(),
5779 $container = this.$clippableContainer.is( 'html, body' ) ?
5780 this.$clippableWindow : this.$clippableContainer,
5781 ccOffset = $container.offset() || { top: 0, left: 0 },
5782 ccHeight = $container.innerHeight() - buffer,
5783 ccWidth = $container.innerWidth() - buffer,
5784 cHeight = this.$clippable.outerHeight() + buffer,
5785 cWidth = this.$clippable.outerWidth() + buffer,
5786 scrollTop = this.$clippableScroller.scrollTop(),
5787 scrollLeft = this.$clippableScroller.scrollLeft(),
5788 desiredWidth = cOffset.left < 0 ?
5789 cWidth + cOffset.left :
5790 ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
5791 desiredHeight = cOffset.top < 0 ?
5792 cHeight + cOffset.top :
5793 ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
5794 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
5795 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
5796 clipWidth = desiredWidth < naturalWidth,
5797 clipHeight = desiredHeight < naturalHeight;
5800 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
5802 this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
5805 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
5807 this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
5810 // If we stopped clipping in at least one of the dimensions
5811 if ( !clipWidth || !clipHeight ) {
5812 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5815 this.clippedHorizontally = clipWidth;
5816 this.clippedVertically = clipHeight;
5822 * Generic toolbar tool.
5826 * @extends OO.ui.Widget
5827 * @mixins OO.ui.IconElement
5828 * @mixins OO.ui.FlaggedElement
5831 * @param {OO.ui.ToolGroup} toolGroup
5832 * @param {Object} [config] Configuration options
5833 * @cfg {string|Function} [title] Title text or a function that returns text
5835 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
5836 // Configuration initialization
5837 config = config || {};
5839 // Parent constructor
5840 OO.ui.Tool.super.call( this, config );
5842 // Mixin constructors
5843 OO.ui.IconElement.call( this, config );
5844 OO.ui.FlaggedElement.call( this, config );
5847 this.toolGroup = toolGroup;
5848 this.toolbar = this.toolGroup.getToolbar();
5849 this.active = false;
5850 this.$title = $( '<span>' );
5851 this.$accel = $( '<span>' );
5852 this.$link = $( '<a>' );
5856 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
5859 this.$title.addClass( 'oo-ui-tool-title' );
5861 .addClass( 'oo-ui-tool-accel' )
5863 // This may need to be changed if the key names are ever localized,
5864 // but for now they are essentially written in English
5869 .addClass( 'oo-ui-tool-link' )
5870 .append( this.$icon, this.$title, this.$accel )
5871 .prop( 'tabIndex', 0 )
5872 .attr( 'role', 'button' );
5874 .data( 'oo-ui-tool', this )
5876 'oo-ui-tool ' + 'oo-ui-tool-name-' +
5877 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
5879 .append( this.$link );
5880 this.setTitle( config.title || this.constructor.static.title );
5885 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
5886 OO.mixinClass( OO.ui.Tool, OO.ui.IconElement );
5887 OO.mixinClass( OO.ui.Tool, OO.ui.FlaggedElement );
5895 /* Static Properties */
5901 OO.ui.Tool.static.tagName = 'span';
5904 * Symbolic name of tool.
5909 * @property {string}
5911 OO.ui.Tool.static.name = '';
5919 * @property {string}
5921 OO.ui.Tool.static.group = '';
5926 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
5927 * is part of a list or menu tool group. If a trigger is associated with an action by the same name
5928 * as the tool, a description of its keyboard shortcut for the appropriate platform will be
5929 * appended to the title if the tool is part of a bar tool group.
5934 * @property {string|Function} Title text or a function that returns text
5936 OO.ui.Tool.static.title = '';
5939 * Tool can be automatically added to catch-all groups.
5943 * @property {boolean}
5945 OO.ui.Tool.static.autoAddToCatchall = true;
5948 * Tool can be automatically added to named groups.
5951 * @property {boolean}
5954 OO.ui.Tool.static.autoAddToGroup = true;
5957 * Check if this tool is compatible with given data.
5961 * @param {Mixed} data Data to check
5962 * @return {boolean} Tool can be used with data
5964 OO.ui.Tool.static.isCompatibleWith = function () {
5971 * Handle the toolbar state being updated.
5973 * This is an abstract method that must be overridden in a concrete subclass.
5977 OO.ui.Tool.prototype.onUpdateState = function () {
5979 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
5984 * Handle the tool being selected.
5986 * This is an abstract method that must be overridden in a concrete subclass.
5990 OO.ui.Tool.prototype.onSelect = function () {
5992 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
5997 * Check if the button is active.
5999 * @return {boolean} Button is active
6001 OO.ui.Tool.prototype.isActive = function () {
6006 * Make the button appear active or inactive.
6008 * @param {boolean} state Make button appear active
6010 OO.ui.Tool.prototype.setActive = function ( state ) {
6011 this.active = !!state;
6012 if ( this.active ) {
6013 this.$element.addClass( 'oo-ui-tool-active' );
6015 this.$element.removeClass( 'oo-ui-tool-active' );
6020 * Get the tool title.
6022 * @param {string|Function} title Title text or a function that returns text
6025 OO.ui.Tool.prototype.setTitle = function ( title ) {
6026 this.title = OO.ui.resolveMsg( title );
6032 * Get the tool title.
6034 * @return {string} Title text
6036 OO.ui.Tool.prototype.getTitle = function () {
6041 * Get the tool's symbolic name.
6043 * @return {string} Symbolic name of tool
6045 OO.ui.Tool.prototype.getName = function () {
6046 return this.constructor.static.name;
6052 OO.ui.Tool.prototype.updateTitle = function () {
6053 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
6054 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
6055 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
6058 this.$title.text( this.title );
6059 this.$accel.text( accel );
6061 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
6062 tooltipParts.push( this.title );
6064 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
6065 tooltipParts.push( accel );
6067 if ( tooltipParts.length ) {
6068 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
6070 this.$link.removeAttr( 'title' );
6077 OO.ui.Tool.prototype.destroy = function () {
6078 this.toolbar.disconnect( this );
6079 this.$element.remove();
6083 * Collection of tool groups.
6086 * @extends OO.ui.Element
6087 * @mixins OO.EventEmitter
6088 * @mixins OO.ui.GroupElement
6091 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
6092 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
6093 * @param {Object} [config] Configuration options
6094 * @cfg {boolean} [actions] Add an actions section opposite to the tools
6095 * @cfg {boolean} [shadow] Add a shadow below the toolbar
6097 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
6098 // Configuration initialization
6099 config = config || {};
6101 // Parent constructor
6102 OO.ui.Toolbar.super.call( this, config );
6104 // Mixin constructors
6105 OO.EventEmitter.call( this );
6106 OO.ui.GroupElement.call( this, config );
6109 this.toolFactory = toolFactory;
6110 this.toolGroupFactory = toolGroupFactory;
6113 this.$bar = $( '<div>' );
6114 this.$actions = $( '<div>' );
6115 this.initialized = false;
6119 .add( this.$bar ).add( this.$group ).add( this.$actions )
6120 .on( 'mousedown touchstart', this.onPointerDown.bind( this ) );
6123 this.$group.addClass( 'oo-ui-toolbar-tools' );
6124 if ( config.actions ) {
6125 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
6128 .addClass( 'oo-ui-toolbar-bar' )
6129 .append( this.$group, '<div style="clear:both"></div>' );
6130 if ( config.shadow ) {
6131 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
6133 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
6138 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
6139 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
6140 OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
6145 * Get the tool factory.
6147 * @return {OO.ui.ToolFactory} Tool factory
6149 OO.ui.Toolbar.prototype.getToolFactory = function () {
6150 return this.toolFactory;
6154 * Get the tool group factory.
6156 * @return {OO.Factory} Tool group factory
6158 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
6159 return this.toolGroupFactory;
6163 * Handles mouse down events.
6165 * @param {jQuery.Event} e Mouse down event
6167 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
6168 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
6169 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
6170 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
6176 * Sets up handles and preloads required information for the toolbar to work.
6177 * This must be called after it is attached to a visible document and before doing anything else.
6179 OO.ui.Toolbar.prototype.initialize = function () {
6180 this.initialized = true;
6186 * Tools can be specified in the following ways:
6188 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6189 * - All tools in a group: `{ group: 'group-name' }`
6190 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
6192 * @param {Object.<string,Array>} groups List of tool group configurations
6193 * @param {Array|string} [groups.include] Tools to include
6194 * @param {Array|string} [groups.exclude] Tools to exclude
6195 * @param {Array|string} [groups.promote] Tools to promote to the beginning
6196 * @param {Array|string} [groups.demote] Tools to demote to the end
6198 OO.ui.Toolbar.prototype.setup = function ( groups ) {
6199 var i, len, type, group,
6201 defaultType = 'bar';
6203 // Cleanup previous groups
6206 // Build out new groups
6207 for ( i = 0, len = groups.length; i < len; i++ ) {
6208 group = groups[ i ];
6209 if ( group.include === '*' ) {
6210 // Apply defaults to catch-all groups
6211 if ( group.type === undefined ) {
6212 group.type = 'list';
6214 if ( group.label === undefined ) {
6215 group.label = OO.ui.msg( 'ooui-toolbar-more' );
6218 // Check type has been registered
6219 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
6221 this.getToolGroupFactory().create( type, this, group )
6224 this.addItems( items );
6228 * Remove all tools and groups from the toolbar.
6230 OO.ui.Toolbar.prototype.reset = function () {
6235 for ( i = 0, len = this.items.length; i < len; i++ ) {
6236 this.items[ i ].destroy();
6242 * Destroys toolbar, removing event handlers and DOM elements.
6244 * Call this whenever you are done using a toolbar.
6246 OO.ui.Toolbar.prototype.destroy = function () {
6248 this.$element.remove();
6252 * Check if tool has not been used yet.
6254 * @param {string} name Symbolic name of tool
6255 * @return {boolean} Tool is available
6257 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
6258 return !this.tools[ name ];
6262 * Prevent tool from being used again.
6264 * @param {OO.ui.Tool} tool Tool to reserve
6266 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
6267 this.tools[ tool.getName() ] = tool;
6271 * Allow tool to be used again.
6273 * @param {OO.ui.Tool} tool Tool to release
6275 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
6276 delete this.tools[ tool.getName() ];
6280 * Get accelerator label for tool.
6282 * This is a stub that should be overridden to provide access to accelerator information.
6284 * @param {string} name Symbolic name of tool
6285 * @return {string|undefined} Tool accelerator label if available
6287 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
6292 * Collection of tools.
6294 * Tools can be specified in the following ways:
6296 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6297 * - All tools in a group: `{ group: 'group-name' }`
6298 * - All tools: `'*'`
6302 * @extends OO.ui.Widget
6303 * @mixins OO.ui.GroupElement
6306 * @param {OO.ui.Toolbar} toolbar
6307 * @param {Object} [config] Configuration options
6308 * @cfg {Array|string} [include=[]] List of tools to include
6309 * @cfg {Array|string} [exclude=[]] List of tools to exclude
6310 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
6311 * @cfg {Array|string} [demote=[]] List of tools to demote to the end
6313 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
6314 // Configuration initialization
6315 config = config || {};
6317 // Parent constructor
6318 OO.ui.ToolGroup.super.call( this, config );
6320 // Mixin constructors
6321 OO.ui.GroupElement.call( this, config );
6324 this.toolbar = toolbar;
6326 this.pressed = null;
6327 this.autoDisabled = false;
6328 this.include = config.include || [];
6329 this.exclude = config.exclude || [];
6330 this.promote = config.promote || [];
6331 this.demote = config.demote || [];
6332 this.onCapturedMouseUpHandler = this.onCapturedMouseUp.bind( this );
6336 'mousedown touchstart': this.onPointerDown.bind( this ),
6337 'mouseup touchend': this.onPointerUp.bind( this ),
6338 mouseover: this.onMouseOver.bind( this ),
6339 mouseout: this.onMouseOut.bind( this )
6341 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
6342 this.aggregate( { disable: 'itemDisable' } );
6343 this.connect( this, { itemDisable: 'updateDisabled' } );
6346 this.$group.addClass( 'oo-ui-toolGroup-tools' );
6348 .addClass( 'oo-ui-toolGroup' )
6349 .append( this.$group );
6355 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
6356 OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
6364 /* Static Properties */
6367 * Show labels in tooltips.
6371 * @property {boolean}
6373 OO.ui.ToolGroup.static.titleTooltips = false;
6376 * Show acceleration labels in tooltips.
6380 * @property {boolean}
6382 OO.ui.ToolGroup.static.accelTooltips = false;
6385 * Automatically disable the toolgroup when all tools are disabled
6389 * @property {boolean}
6391 OO.ui.ToolGroup.static.autoDisable = true;
6398 OO.ui.ToolGroup.prototype.isDisabled = function () {
6399 return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
6405 OO.ui.ToolGroup.prototype.updateDisabled = function () {
6406 var i, item, allDisabled = true;
6408 if ( this.constructor.static.autoDisable ) {
6409 for ( i = this.items.length - 1; i >= 0; i-- ) {
6410 item = this.items[ i ];
6411 if ( !item.isDisabled() ) {
6412 allDisabled = false;
6416 this.autoDisabled = allDisabled;
6418 OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
6422 * Handle mouse down events.
6424 * @param {jQuery.Event} e Mouse down event
6426 OO.ui.ToolGroup.prototype.onPointerDown = function ( e ) {
6427 // e.which is 0 for touch events, 1 for left mouse button
6428 if ( !this.isDisabled() && e.which <= 1 ) {
6429 this.pressed = this.getTargetTool( e );
6430 if ( this.pressed ) {
6431 this.pressed.setActive( true );
6432 this.getElementDocument().addEventListener(
6433 'mouseup', this.onCapturedMouseUpHandler, true
6441 * Handle captured mouse up events.
6443 * @param {Event} e Mouse up event
6445 OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
6446 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true );
6447 // onPointerUp may be called a second time, depending on where the mouse is when the button is
6448 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
6449 this.onPointerUp( e );
6453 * Handle mouse up events.
6455 * @param {jQuery.Event} e Mouse up event
6457 OO.ui.ToolGroup.prototype.onPointerUp = function ( e ) {
6458 var tool = this.getTargetTool( e );
6460 // e.which is 0 for touch events, 1 for left mouse button
6461 if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === tool ) {
6462 this.pressed.onSelect();
6465 this.pressed = null;
6470 * Handle mouse over events.
6472 * @param {jQuery.Event} e Mouse over event
6474 OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) {
6475 var tool = this.getTargetTool( e );
6477 if ( this.pressed && this.pressed === tool ) {
6478 this.pressed.setActive( true );
6483 * Handle mouse out events.
6485 * @param {jQuery.Event} e Mouse out event
6487 OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
6488 var tool = this.getTargetTool( e );
6490 if ( this.pressed && this.pressed === tool ) {
6491 this.pressed.setActive( false );
6496 * Get the closest tool to a jQuery.Event.
6498 * Only tool links are considered, which prevents other elements in the tool such as popups from
6499 * triggering tool group interactions.
6502 * @param {jQuery.Event} e
6503 * @return {OO.ui.Tool|null} Tool, `null` if none was found
6505 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
6507 $item = $( e.target ).closest( '.oo-ui-tool-link' );
6509 if ( $item.length ) {
6510 tool = $item.parent().data( 'oo-ui-tool' );
6513 return tool && !tool.isDisabled() ? tool : null;
6517 * Handle tool registry register events.
6519 * If a tool is registered after the group is created, we must repopulate the list to account for:
6521 * - a tool being added that may be included
6522 * - a tool already included being overridden
6524 * @param {string} name Symbolic name of tool
6526 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
6531 * Get the toolbar this group is in.
6533 * @return {OO.ui.Toolbar} Toolbar of group
6535 OO.ui.ToolGroup.prototype.getToolbar = function () {
6536 return this.toolbar;
6540 * Add and remove tools based on configuration.
6542 OO.ui.ToolGroup.prototype.populate = function () {
6543 var i, len, name, tool,
6544 toolFactory = this.toolbar.getToolFactory(),
6548 list = this.toolbar.getToolFactory().getTools(
6549 this.include, this.exclude, this.promote, this.demote
6552 // Build a list of needed tools
6553 for ( i = 0, len = list.length; i < len; i++ ) {
6557 toolFactory.lookup( name ) &&
6558 // Tool is available or is already in this group
6559 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
6561 tool = this.tools[ name ];
6563 // Auto-initialize tools on first use
6564 this.tools[ name ] = tool = toolFactory.create( name, this );
6567 this.toolbar.reserveTool( tool );
6569 names[ name ] = true;
6572 // Remove tools that are no longer needed
6573 for ( name in this.tools ) {
6574 if ( !names[ name ] ) {
6575 this.tools[ name ].destroy();
6576 this.toolbar.releaseTool( this.tools[ name ] );
6577 remove.push( this.tools[ name ] );
6578 delete this.tools[ name ];
6581 if ( remove.length ) {
6582 this.removeItems( remove );
6584 // Update emptiness state
6586 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
6588 this.$element.addClass( 'oo-ui-toolGroup-empty' );
6590 // Re-add tools (moving existing ones to new locations)
6591 this.addItems( add );
6592 // Disabled state may depend on items
6593 this.updateDisabled();
6597 * Destroy tool group.
6599 OO.ui.ToolGroup.prototype.destroy = function () {
6603 this.toolbar.getToolFactory().disconnect( this );
6604 for ( name in this.tools ) {
6605 this.toolbar.releaseTool( this.tools[ name ] );
6606 this.tools[ name ].disconnect( this ).destroy();
6607 delete this.tools[ name ];
6609 this.$element.remove();
6613 * Dialog for showing a message.
6616 * - Registers two actions by default (safe and primary).
6617 * - Renders action widgets in the footer.
6620 * @extends OO.ui.Dialog
6623 * @param {Object} [config] Configuration options
6625 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
6626 // Parent constructor
6627 OO.ui.MessageDialog.super.call( this, config );
6630 this.verticalActionLayout = null;
6633 this.$element.addClass( 'oo-ui-messageDialog' );
6638 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
6640 /* Static Properties */
6642 OO.ui.MessageDialog.static.name = 'message';
6644 OO.ui.MessageDialog.static.size = 'small';
6646 OO.ui.MessageDialog.static.verbose = false;
6651 * A confirmation dialog's title should describe what the progressive action will do. An alert
6652 * dialog's title should describe what event occurred.
6656 * @property {jQuery|string|Function|null}
6658 OO.ui.MessageDialog.static.title = null;
6661 * A confirmation dialog's message should describe the consequences of the progressive action. An
6662 * alert dialog's message should describe why the event occurred.
6666 * @property {jQuery|string|Function|null}
6668 OO.ui.MessageDialog.static.message = null;
6670 OO.ui.MessageDialog.static.actions = [
6671 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
6672 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
6680 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
6681 OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
6684 this.manager.connect( this, {
6694 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
6696 return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
6700 * Handle window resized events.
6702 OO.ui.MessageDialog.prototype.onResize = function () {
6704 dialog.fitActions();
6705 // Wait for CSS transition to finish and do it again :(
6706 setTimeout( function () {
6707 dialog.fitActions();
6712 * Toggle action layout between vertical and horizontal.
6714 * @param {boolean} [value] Layout actions vertically, omit to toggle
6717 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
6718 value = value === undefined ? !this.verticalActionLayout : !!value;
6720 if ( value !== this.verticalActionLayout ) {
6721 this.verticalActionLayout = value;
6723 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
6724 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
6733 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
6735 return new OO.ui.Process( function () {
6736 this.close( { action: action } );
6739 return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
6745 * @param {Object} [data] Dialog opening data
6746 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
6747 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
6748 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
6749 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
6752 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
6756 return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
6757 .next( function () {
6758 this.title.setLabel(
6759 data.title !== undefined ? data.title : this.constructor.static.title
6761 this.message.setLabel(
6762 data.message !== undefined ? data.message : this.constructor.static.message
6764 this.message.$element.toggleClass(
6765 'oo-ui-messageDialog-message-verbose',
6766 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
6774 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
6775 var bodyHeight, oldOverflow,
6776 $scrollable = this.container.$element;
6778 oldOverflow = $scrollable[ 0 ].style.overflow;
6779 $scrollable[ 0 ].style.overflow = 'hidden';
6781 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
6783 bodyHeight = this.text.$element.outerHeight( true );
6784 $scrollable[ 0 ].style.overflow = oldOverflow;
6792 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
6793 var $scrollable = this.container.$element;
6794 OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
6796 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
6797 // Need to do it after transition completes (250ms), add 50ms just in case.
6798 setTimeout( function () {
6799 var oldOverflow = $scrollable[ 0 ].style.overflow;
6800 $scrollable[ 0 ].style.overflow = 'hidden';
6802 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
6804 $scrollable[ 0 ].style.overflow = oldOverflow;
6813 OO.ui.MessageDialog.prototype.initialize = function () {
6815 OO.ui.MessageDialog.super.prototype.initialize.call( this );
6818 this.$actions = $( '<div>' );
6819 this.container = new OO.ui.PanelLayout( {
6820 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
6822 this.text = new OO.ui.PanelLayout( {
6823 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
6825 this.message = new OO.ui.LabelWidget( {
6826 classes: [ 'oo-ui-messageDialog-message' ]
6830 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
6831 this.$content.addClass( 'oo-ui-messageDialog-content' );
6832 this.container.$element.append( this.text.$element );
6833 this.text.$element.append( this.title.$element, this.message.$element );
6834 this.$body.append( this.container.$element );
6835 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
6836 this.$foot.append( this.$actions );
6842 OO.ui.MessageDialog.prototype.attachActions = function () {
6843 var i, len, other, special, others;
6846 OO.ui.MessageDialog.super.prototype.attachActions.call( this );
6848 special = this.actions.getSpecial();
6849 others = this.actions.getOthers();
6850 if ( special.safe ) {
6851 this.$actions.append( special.safe.$element );
6852 special.safe.toggleFramed( false );
6854 if ( others.length ) {
6855 for ( i = 0, len = others.length; i < len; i++ ) {
6856 other = others[ i ];
6857 this.$actions.append( other.$element );
6858 other.toggleFramed( false );
6861 if ( special.primary ) {
6862 this.$actions.append( special.primary.$element );
6863 special.primary.toggleFramed( false );
6866 if ( !this.isOpening() ) {
6867 // If the dialog is currently opening, this will be called automatically soon.
6868 // This also calls #fitActions.
6874 * Fit action actions into columns or rows.
6876 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
6878 OO.ui.MessageDialog.prototype.fitActions = function () {
6880 previous = this.verticalActionLayout,
6881 actions = this.actions.get();
6884 this.toggleVerticalActionLayout( false );
6885 for ( i = 0, len = actions.length; i < len; i++ ) {
6886 action = actions[ i ];
6887 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
6888 this.toggleVerticalActionLayout( true );
6893 // Move the body out of the way of the foot
6894 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
6896 if ( this.verticalActionLayout !== previous ) {
6897 // We changed the layout, window height might need to be updated.
6903 * Navigation dialog window.
6906 * - Show and hide errors.
6907 * - Retry an action.
6910 * - Renders header with dialog title and one action widget on either side
6911 * (a 'safe' button on the left, and a 'primary' button on the right, both of
6912 * which close the dialog).
6913 * - Displays any action widgets in the footer (none by default).
6914 * - Ability to dismiss errors.
6916 * Subclass responsibilities:
6917 * - Register a 'safe' action.
6918 * - Register a 'primary' action.
6919 * - Add content to the dialog.
6923 * @extends OO.ui.Dialog
6926 * @param {Object} [config] Configuration options
6928 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
6929 // Parent constructor
6930 OO.ui.ProcessDialog.super.call( this, config );
6933 this.$element.addClass( 'oo-ui-processDialog' );
6938 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
6943 * Handle dismiss button click events.
6947 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
6952 * Handle retry button click events.
6954 * Hides errors and then tries again.
6956 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
6958 this.executeAction( this.currentAction.getAction() );
6964 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
6965 if ( this.actions.isSpecial( action ) ) {
6968 return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
6974 OO.ui.ProcessDialog.prototype.initialize = function () {
6976 OO.ui.ProcessDialog.super.prototype.initialize.call( this );
6979 this.$navigation = $( '<div>' );
6980 this.$location = $( '<div>' );
6981 this.$safeActions = $( '<div>' );
6982 this.$primaryActions = $( '<div>' );
6983 this.$otherActions = $( '<div>' );
6984 this.dismissButton = new OO.ui.ButtonWidget( {
6985 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
6987 this.retryButton = new OO.ui.ButtonWidget();
6988 this.$errors = $( '<div>' );
6989 this.$errorsTitle = $( '<div>' );
6992 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
6993 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
6996 this.title.$element.addClass( 'oo-ui-processDialog-title' );
6998 .append( this.title.$element )
6999 .addClass( 'oo-ui-processDialog-location' );
7000 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
7001 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
7002 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
7004 .addClass( 'oo-ui-processDialog-errors-title' )
7005 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
7007 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
7008 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
7010 .addClass( 'oo-ui-processDialog-content' )
7011 .append( this.$errors );
7013 .addClass( 'oo-ui-processDialog-navigation' )
7014 .append( this.$safeActions, this.$location, this.$primaryActions );
7015 this.$head.append( this.$navigation );
7016 this.$foot.append( this.$otherActions );
7022 OO.ui.ProcessDialog.prototype.attachActions = function () {
7023 var i, len, other, special, others;
7026 OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
7028 special = this.actions.getSpecial();
7029 others = this.actions.getOthers();
7030 if ( special.primary ) {
7031 this.$primaryActions.append( special.primary.$element );
7032 special.primary.toggleFramed( true );
7034 if ( others.length ) {
7035 for ( i = 0, len = others.length; i < len; i++ ) {
7036 other = others[ i ];
7037 this.$otherActions.append( other.$element );
7038 other.toggleFramed( true );
7041 if ( special.safe ) {
7042 this.$safeActions.append( special.safe.$element );
7043 special.safe.toggleFramed( true );
7047 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
7053 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
7054 OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
7055 .fail( this.showErrors.bind( this ) );
7059 * Fit label between actions.
7063 OO.ui.ProcessDialog.prototype.fitLabel = function () {
7064 var width = Math.max(
7065 this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0,
7066 this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0
7068 this.$location.css( { paddingLeft: width, paddingRight: width } );
7074 * Handle errors that occurred during accept or reject processes.
7076 * @param {OO.ui.Error[]} errors Errors to be handled
7078 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
7084 for ( i = 0, len = errors.length; i < len; i++ ) {
7085 if ( !errors[ i ].isRecoverable() ) {
7086 recoverable = false;
7088 if ( errors[ i ].isWarning() ) {
7091 $item = $( '<div>' )
7092 .addClass( 'oo-ui-processDialog-error' )
7093 .append( errors[ i ].getMessage() );
7094 items.push( $item[ 0 ] );
7096 this.$errorItems = $( items );
7097 if ( recoverable ) {
7098 this.retryButton.clearFlags().setFlags( this.currentAction.getFlags() );
7100 this.currentAction.setDisabled( true );
7103 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
7105 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
7107 this.retryButton.toggle( recoverable );
7108 this.$errorsTitle.after( this.$errorItems );
7109 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
7115 OO.ui.ProcessDialog.prototype.hideErrors = function () {
7116 this.$errors.addClass( 'oo-ui-element-hidden' );
7117 this.$errorItems.remove();
7118 this.$errorItems = null;
7122 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
7123 * which is a widget that is specified by reference before any optional configuration settings.
7125 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
7127 * - **left**: The label is placed before the field-widget and aligned with the left margin.
7128 * A left-alignment is used for forms with many fields.
7129 * - **right**: The label is placed before the field-widget and aligned to the right margin.
7130 * A right-alignment is used for long but familiar forms which users tab through,
7131 * verifying the current field with a quick glance at the label.
7132 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
7133 * that users fill out from top to bottom.
7134 * - **inline**: The label is placed after the field-widget and aligned to the left.
7135 An inline-alignment is best used with checkboxes or radio buttons.
7137 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
7138 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
7140 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
7142 * @extends OO.ui.Layout
7143 * @mixins OO.ui.LabelElement
7146 * @param {OO.ui.Widget} fieldWidget Field widget
7147 * @param {Object} [config] Configuration options
7148 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7149 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7151 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
7152 var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
7154 // Configuration initialization
7155 config = $.extend( { align: 'left' }, config );
7157 // Parent constructor
7158 OO.ui.FieldLayout.super.call( this, config );
7160 // Mixin constructors
7161 OO.ui.LabelElement.call( this, config );
7164 this.fieldWidget = fieldWidget;
7165 this.$field = $( '<div>' );
7166 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
7168 if ( config.help ) {
7169 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7170 classes: [ 'oo-ui-fieldLayout-help' ],
7175 this.popupButtonWidget.getPopup().$body.append(
7177 .text( config.help )
7178 .addClass( 'oo-ui-fieldLayout-help-content' )
7180 this.$help = this.popupButtonWidget.$element;
7182 this.$help = $( [] );
7186 if ( hasInputWidget ) {
7187 this.$label.on( 'click', this.onLabelClick.bind( this ) );
7189 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
7193 .addClass( 'oo-ui-fieldLayout' )
7194 .append( this.$help, this.$body );
7195 this.$body.addClass( 'oo-ui-fieldLayout-body' );
7197 .addClass( 'oo-ui-fieldLayout-field' )
7198 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
7199 .append( this.fieldWidget.$element );
7201 this.setAlignment( config.align );
7206 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
7207 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
7212 * Handle field disable events.
7214 * @param {boolean} value Field is disabled
7216 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
7217 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
7221 * Handle label mouse click events.
7223 * @param {jQuery.Event} e Mouse click event
7225 OO.ui.FieldLayout.prototype.onLabelClick = function () {
7226 this.fieldWidget.simulateLabelClick();
7233 * @return {OO.ui.Widget} Field widget
7235 OO.ui.FieldLayout.prototype.getField = function () {
7236 return this.fieldWidget;
7240 * Set the field alignment mode.
7243 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
7246 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
7247 if ( value !== this.align ) {
7248 // Default to 'left'
7249 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
7253 if ( value === 'inline' ) {
7254 this.$body.append( this.$field, this.$label );
7256 this.$body.append( this.$label, this.$field );
7258 // Set classes. The following classes can be used here:
7259 // * oo-ui-fieldLayout-align-left
7260 // * oo-ui-fieldLayout-align-right
7261 // * oo-ui-fieldLayout-align-top
7262 // * oo-ui-fieldLayout-align-inline
7264 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
7266 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
7274 * Layout made of a field, a button, and an optional label.
7277 * @extends OO.ui.FieldLayout
7280 * @param {OO.ui.Widget} fieldWidget Field widget
7281 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
7282 * @param {Object} [config] Configuration options
7283 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7284 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7286 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
7287 // Configuration initialization
7288 config = $.extend( { align: 'left' }, config );
7290 // Parent constructor
7291 OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
7293 // Mixin constructors
7294 OO.ui.LabelElement.call( this, config );
7297 this.fieldWidget = fieldWidget;
7298 this.buttonWidget = buttonWidget;
7299 this.$button = $( '<div>' )
7300 .addClass( 'oo-ui-actionFieldLayout-button' )
7301 .append( this.buttonWidget.$element );
7302 this.$input = $( '<div>' )
7303 .addClass( 'oo-ui-actionFieldLayout-input' )
7304 .append( this.fieldWidget.$element );
7306 .addClass( 'oo-ui-actionFieldLayout' )
7307 .append( this.$input, this.$button );
7312 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
7315 * Layout made of a fieldset and optional legend.
7317 * Just add OO.ui.FieldLayout items.
7320 * @extends OO.ui.Layout
7321 * @mixins OO.ui.IconElement
7322 * @mixins OO.ui.LabelElement
7323 * @mixins OO.ui.GroupElement
7326 * @param {Object} [config] Configuration options
7327 * @cfg {OO.ui.FieldLayout[]} [items] Items to add
7329 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
7330 // Configuration initialization
7331 config = config || {};
7333 // Parent constructor
7334 OO.ui.FieldsetLayout.super.call( this, config );
7336 // Mixin constructors
7337 OO.ui.IconElement.call( this, config );
7338 OO.ui.LabelElement.call( this, config );
7339 OO.ui.GroupElement.call( this, config );
7341 if ( config.help ) {
7342 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7343 classes: [ 'oo-ui-fieldsetLayout-help' ],
7348 this.popupButtonWidget.getPopup().$body.append(
7350 .text( config.help )
7351 .addClass( 'oo-ui-fieldsetLayout-help-content' )
7353 this.$help = this.popupButtonWidget.$element;
7355 this.$help = $( [] );
7360 .addClass( 'oo-ui-fieldsetLayout' )
7361 .prepend( this.$help, this.$icon, this.$label, this.$group );
7362 if ( Array.isArray( config.items ) ) {
7363 this.addItems( config.items );
7369 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
7370 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
7371 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
7372 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
7375 * Layout with an HTML form.
7378 * @extends OO.ui.Layout
7381 * @param {Object} [config] Configuration options
7382 * @cfg {string} [method] HTML form `method` attribute
7383 * @cfg {string} [action] HTML form `action` attribute
7384 * @cfg {string} [enctype] HTML form `enctype` attribute
7386 OO.ui.FormLayout = function OoUiFormLayout( config ) {
7387 // Configuration initialization
7388 config = config || {};
7390 // Parent constructor
7391 OO.ui.FormLayout.super.call( this, config );
7394 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
7398 .addClass( 'oo-ui-formLayout' )
7400 method: config.method,
7401 action: config.action,
7402 enctype: config.enctype
7408 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
7416 /* Static Properties */
7418 OO.ui.FormLayout.static.tagName = 'form';
7423 * Handle form submit events.
7425 * @param {jQuery.Event} e Submit event
7428 OO.ui.FormLayout.prototype.onFormSubmit = function () {
7429 this.emit( 'submit' );
7434 * Layout made of proportionally sized columns and rows.
7437 * @extends OO.ui.Layout
7438 * @deprecated Use OO.ui.MenuLayout or plain CSS instead.
7441 * @param {OO.ui.PanelLayout[]} panels Panels in the grid
7442 * @param {Object} [config] Configuration options
7443 * @cfg {number[]} [widths] Widths of columns as ratios
7444 * @cfg {number[]} [heights] Heights of rows as ratios
7446 OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
7449 // Configuration initialization
7450 config = config || {};
7452 // Parent constructor
7453 OO.ui.GridLayout.super.call( this, config );
7461 this.$element.addClass( 'oo-ui-gridLayout' );
7462 for ( i = 0, len = panels.length; i < len; i++ ) {
7463 this.panels.push( panels[ i ] );
7464 this.$element.append( panels[ i ].$element );
7466 if ( config.widths || config.heights ) {
7467 this.layout( config.widths || [ 1 ], config.heights || [ 1 ] );
7469 // Arrange in columns by default
7470 widths = this.panels.map( function () { return 1; } );
7471 this.layout( widths, [ 1 ] );
7477 OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
7492 * Set grid dimensions.
7494 * @param {number[]} widths Widths of columns as ratios
7495 * @param {number[]} heights Heights of rows as ratios
7497 * @throws {Error} If grid is not large enough to fit all panels
7499 OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
7503 cols = widths.length,
7504 rows = heights.length;
7506 // Verify grid is big enough to fit panels
7507 if ( cols * rows < this.panels.length ) {
7508 throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' );
7511 // Sum up denominators
7512 for ( x = 0; x < cols; x++ ) {
7515 for ( y = 0; y < rows; y++ ) {
7521 for ( x = 0; x < cols; x++ ) {
7522 this.widths[ x ] = widths[ x ] / xd;
7524 for ( y = 0; y < rows; y++ ) {
7525 this.heights[ y ] = heights[ y ] / yd;
7529 this.emit( 'layout' );
7533 * Update panel positions and sizes.
7537 OO.ui.GridLayout.prototype.update = function () {
7538 var x, y, panel, width, height, dimensions,
7542 cols = this.widths.length,
7543 rows = this.heights.length;
7545 for ( y = 0; y < rows; y++ ) {
7546 height = this.heights[ y ];
7547 for ( x = 0; x < cols; x++ ) {
7548 width = this.widths[ x ];
7549 panel = this.panels[ i ];
7551 width: ( width * 100 ) + '%',
7552 height: ( height * 100 ) + '%',
7553 top: ( top * 100 ) + '%'
7556 if ( OO.ui.Element.static.getDir( document ) === 'rtl' ) {
7557 dimensions.right = ( left * 100 ) + '%';
7559 dimensions.left = ( left * 100 ) + '%';
7561 // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero
7562 if ( width === 0 || height === 0 ) {
7563 dimensions.visibility = 'hidden';
7565 dimensions.visibility = '';
7567 panel.$element.css( dimensions );
7575 this.emit( 'update' );
7579 * Get a panel at a given position.
7581 * The x and y position is affected by the current grid layout.
7583 * @param {number} x Horizontal position
7584 * @param {number} y Vertical position
7585 * @return {OO.ui.PanelLayout} The panel at the given position
7587 OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
7588 return this.panels[ ( x * this.widths.length ) + y ];
7592 * Layout with a content and menu area.
7594 * The menu area can be positioned at the top, after, bottom or before. The content area will fill
7595 * all remaining space.
7598 * @extends OO.ui.Layout
7601 * @param {Object} [config] Configuration options
7602 * @cfg {number|string} [menuSize='18em'] Size of menu in pixels or any CSS unit
7603 * @cfg {boolean} [showMenu=true] Show menu
7604 * @cfg {string} [position='before'] Position of menu, either `top`, `after`, `bottom` or `before`
7605 * @cfg {boolean} [collapse] Collapse the menu out of view
7607 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
7608 var positions = this.constructor.static.menuPositions;
7610 // Configuration initialization
7611 config = config || {};
7613 // Parent constructor
7614 OO.ui.MenuLayout.super.call( this, config );
7617 this.showMenu = config.showMenu !== false;
7618 this.menuSize = config.menuSize || '18em';
7619 this.menuPosition = positions[ config.menuPosition ] || positions.before;
7624 * @property {jQuery}
7626 this.$menu = $( '<div>' );
7630 * @property {jQuery}
7632 this.$content = $( '<div>' );
7635 this.toggleMenu( this.showMenu );
7638 .addClass( 'oo-ui-menuLayout-menu' )
7639 .css( this.menuPosition.sizeProperty, this.menuSize );
7640 this.$content.addClass( 'oo-ui-menuLayout-content' );
7642 .addClass( 'oo-ui-menuLayout ' + this.menuPosition.className )
7643 .append( this.$content, this.$menu );
7648 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
7650 /* Static Properties */
7652 OO.ui.MenuLayout.static.menuPositions = {
7654 sizeProperty: 'height',
7655 className: 'oo-ui-menuLayout-top'
7658 sizeProperty: 'width',
7659 className: 'oo-ui-menuLayout-after'
7662 sizeProperty: 'height',
7663 className: 'oo-ui-menuLayout-bottom'
7666 sizeProperty: 'width',
7667 className: 'oo-ui-menuLayout-before'
7676 * @param {boolean} showMenu Show menu, omit to toggle
7679 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
7680 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
7682 if ( this.showMenu !== showMenu ) {
7683 this.showMenu = showMenu;
7691 * Check if menu is visible
7693 * @return {boolean} Menu is visible
7695 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
7696 return this.showMenu;
7702 * @param {number|string} size Size of menu in pixels or any CSS unit
7705 OO.ui.MenuLayout.prototype.setMenuSize = function ( size ) {
7706 this.menuSize = size;
7713 * Update menu and content CSS based on current menu size and visibility
7715 * This method is called internally when size or position is changed.
7717 OO.ui.MenuLayout.prototype.updateSizes = function () {
7718 if ( this.showMenu ) {
7720 .css( this.menuPosition.sizeProperty, this.menuSize )
7721 .css( 'overflow', '' );
7722 // Set offsets on all sides. CSS resets all but one with
7723 // 'important' rules so directionality flips are supported
7724 this.$content.css( {
7726 right: this.menuSize,
7727 bottom: this.menuSize,
7732 .css( this.menuPosition.sizeProperty, 0 )
7733 .css( 'overflow', 'hidden' );
7734 this.$content.css( {
7746 * @return {number|string} Menu size
7748 OO.ui.MenuLayout.prototype.getMenuSize = function () {
7749 return this.menuSize;
7753 * Set menu position.
7755 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
7756 * @throws {Error} If position value is not supported
7759 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
7760 var positions = this.constructor.static.menuPositions;
7762 if ( !positions[ position ] ) {
7763 throw new Error( 'Cannot set position; unsupported position value: ' + position );
7766 this.$menu.css( this.menuPosition.sizeProperty, '' );
7767 this.$element.removeClass( this.menuPosition.className );
7769 this.menuPosition = positions[ position ];
7772 this.$element.addClass( this.menuPosition.className );
7778 * Get menu position.
7780 * @return {string} Menu position
7782 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
7783 return this.menuPosition;
7787 * Layout containing a series of pages.
7790 * @extends OO.ui.MenuLayout
7793 * @param {Object} [config] Configuration options
7794 * @cfg {boolean} [continuous=false] Show all pages, one after another
7795 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
7796 * @cfg {boolean} [outlined=false] Show an outline
7797 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
7799 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
7800 // Configuration initialization
7801 config = config || {};
7803 // Parent constructor
7804 OO.ui.BookletLayout.super.call( this, config );
7807 this.currentPageName = null;
7809 this.ignoreFocus = false;
7810 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
7811 this.$content.append( this.stackLayout.$element );
7812 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
7813 this.outlineVisible = false;
7814 this.outlined = !!config.outlined;
7815 if ( this.outlined ) {
7816 this.editable = !!config.editable;
7817 this.outlineControlsWidget = null;
7818 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
7819 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
7820 this.$menu.append( this.outlinePanel.$element );
7821 this.outlineVisible = true;
7822 if ( this.editable ) {
7823 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
7824 this.outlineSelectWidget
7828 this.toggleMenu( this.outlined );
7831 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
7832 if ( this.outlined ) {
7833 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
7835 if ( this.autoFocus ) {
7836 // Event 'focus' does not bubble, but 'focusin' does
7837 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
7841 this.$element.addClass( 'oo-ui-bookletLayout' );
7842 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
7843 if ( this.outlined ) {
7844 this.outlinePanel.$element
7845 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
7846 .append( this.outlineSelectWidget.$element );
7847 if ( this.editable ) {
7848 this.outlinePanel.$element
7849 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
7850 .append( this.outlineControlsWidget.$element );
7857 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
7863 * @param {OO.ui.PageLayout} page Current page
7868 * @param {OO.ui.PageLayout[]} page Added pages
7869 * @param {number} index Index pages were added at
7874 * @param {OO.ui.PageLayout[]} pages Removed pages
7880 * Handle stack layout focus.
7882 * @param {jQuery.Event} e Focusin event
7884 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
7887 // Find the page that an element was focused within
7888 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
7889 for ( name in this.pages ) {
7890 // Check for page match, exclude current page to find only page changes
7891 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
7892 this.setPage( name );
7899 * Handle stack layout set events.
7901 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
7903 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
7906 page.scrollElementIntoView( { complete: function () {
7907 if ( layout.autoFocus ) {
7915 * Focus the first input in the current page.
7917 * If no page is selected, the first selectable page will be selected.
7918 * If the focus is already in an element on the current page, nothing will happen.
7920 OO.ui.BookletLayout.prototype.focus = function () {
7921 var $input, page = this.stackLayout.getCurrentItem();
7922 if ( !page && this.outlined ) {
7923 this.selectFirstSelectablePage();
7924 page = this.stackLayout.getCurrentItem();
7929 // Only change the focus if is not already in the current page
7930 if ( !page.$element.find( ':focus' ).length ) {
7931 $input = page.$element.find( ':input:first' );
7932 if ( $input.length ) {
7933 $input[ 0 ].focus();
7939 * Handle outline widget select events.
7941 * @param {OO.ui.OptionWidget|null} item Selected item
7943 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
7945 this.setPage( item.getData() );
7950 * Check if booklet has an outline.
7954 OO.ui.BookletLayout.prototype.isOutlined = function () {
7955 return this.outlined;
7959 * Check if booklet has editing controls.
7963 OO.ui.BookletLayout.prototype.isEditable = function () {
7964 return this.editable;
7968 * Check if booklet has a visible outline.
7972 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
7973 return this.outlined && this.outlineVisible;
7977 * Hide or show the outline.
7979 * @param {boolean} [show] Show outline, omit to invert current state
7982 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
7983 if ( this.outlined ) {
7984 show = show === undefined ? !this.outlineVisible : !!show;
7985 this.outlineVisible = show;
7986 this.toggleMenu( show );
7993 * Get the outline widget.
7995 * @param {OO.ui.PageLayout} page Page to be selected
7996 * @return {OO.ui.PageLayout|null} Closest page to another
7998 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
7999 var next, prev, level,
8000 pages = this.stackLayout.getItems(),
8001 index = $.inArray( page, pages );
8003 if ( index !== -1 ) {
8004 next = pages[ index + 1 ];
8005 prev = pages[ index - 1 ];
8006 // Prefer adjacent pages at the same level
8007 if ( this.outlined ) {
8008 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
8011 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
8017 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
8023 return prev || next || null;
8027 * Get the outline widget.
8029 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
8031 OO.ui.BookletLayout.prototype.getOutline = function () {
8032 return this.outlineSelectWidget;
8036 * Get the outline controls widget. If the outline is not editable, null is returned.
8038 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
8040 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
8041 return this.outlineControlsWidget;
8045 * Get a page by name.
8047 * @param {string} name Symbolic name of page
8048 * @return {OO.ui.PageLayout|undefined} Page, if found
8050 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
8051 return this.pages[ name ];
8055 * Get the current page
8057 * @return {OO.ui.PageLayout|undefined} Current page, if found
8059 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
8060 var name = this.getCurrentPageName();
8061 return name ? this.getPage( name ) : undefined;
8065 * Get the current page name.
8067 * @return {string|null} Current page name
8069 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
8070 return this.currentPageName;
8074 * Add a page to the layout.
8076 * When pages are added with the same names as existing pages, the existing pages will be
8077 * automatically removed before the new pages are added.
8079 * @param {OO.ui.PageLayout[]} pages Pages to add
8080 * @param {number} index Index to insert pages after
8084 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
8085 var i, len, name, page, item, currentIndex,
8086 stackLayoutPages = this.stackLayout.getItems(),
8090 // Remove pages with same names
8091 for ( i = 0, len = pages.length; i < len; i++ ) {
8093 name = page.getName();
8095 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
8096 // Correct the insertion index
8097 currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
8098 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
8101 remove.push( this.pages[ name ] );
8104 if ( remove.length ) {
8105 this.removePages( remove );
8109 for ( i = 0, len = pages.length; i < len; i++ ) {
8111 name = page.getName();
8112 this.pages[ page.getName() ] = page;
8113 if ( this.outlined ) {
8114 item = new OO.ui.OutlineOptionWidget( { data: name } );
8115 page.setOutlineItem( item );
8120 if ( this.outlined && items.length ) {
8121 this.outlineSelectWidget.addItems( items, index );
8122 this.selectFirstSelectablePage();
8124 this.stackLayout.addItems( pages, index );
8125 this.emit( 'add', pages, index );
8131 * Remove a page from the layout.
8136 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
8137 var i, len, name, page,
8140 for ( i = 0, len = pages.length; i < len; i++ ) {
8142 name = page.getName();
8143 delete this.pages[ name ];
8144 if ( this.outlined ) {
8145 items.push( this.outlineSelectWidget.getItemFromData( name ) );
8146 page.setOutlineItem( null );
8149 if ( this.outlined && items.length ) {
8150 this.outlineSelectWidget.removeItems( items );
8151 this.selectFirstSelectablePage();
8153 this.stackLayout.removeItems( pages );
8154 this.emit( 'remove', pages );
8160 * Clear all pages from the layout.
8165 OO.ui.BookletLayout.prototype.clearPages = function () {
8167 pages = this.stackLayout.getItems();
8170 this.currentPageName = null;
8171 if ( this.outlined ) {
8172 this.outlineSelectWidget.clearItems();
8173 for ( i = 0, len = pages.length; i < len; i++ ) {
8174 pages[ i ].setOutlineItem( null );
8177 this.stackLayout.clearItems();
8179 this.emit( 'remove', pages );
8185 * Set the current page by name.
8188 * @param {string} name Symbolic name of page
8190 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
8193 page = this.pages[ name ];
8195 if ( name !== this.currentPageName ) {
8196 if ( this.outlined ) {
8197 selectedItem = this.outlineSelectWidget.getSelectedItem();
8198 if ( selectedItem && selectedItem.getData() !== name ) {
8199 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) );
8203 if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
8204 this.pages[ this.currentPageName ].setActive( false );
8205 // Blur anything focused if the next page doesn't have anything focusable - this
8206 // is not needed if the next page has something focusable because once it is focused
8207 // this blur happens automatically
8208 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
8209 $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
8210 if ( $focused.length ) {
8211 $focused[ 0 ].blur();
8215 this.currentPageName = name;
8216 this.stackLayout.setItem( page );
8217 page.setActive( true );
8218 this.emit( 'set', page );
8224 * Select the first selectable page.
8228 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
8229 if ( !this.outlineSelectWidget.getSelectedItem() ) {
8230 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
8237 * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
8240 * @extends OO.ui.Layout
8243 * @param {Object} [config] Configuration options
8244 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
8245 * @cfg {boolean} [padded=false] Pad the content from the edges
8246 * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
8248 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
8249 // Configuration initialization
8250 config = $.extend( {
8256 // Parent constructor
8257 OO.ui.PanelLayout.super.call( this, config );
8260 this.$element.addClass( 'oo-ui-panelLayout' );
8261 if ( config.scrollable ) {
8262 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
8264 if ( config.padded ) {
8265 this.$element.addClass( 'oo-ui-panelLayout-padded' );
8267 if ( config.expanded ) {
8268 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
8274 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
8277 * Page within an booklet layout.
8280 * @extends OO.ui.PanelLayout
8283 * @param {string} name Unique symbolic name of page
8284 * @param {Object} [config] Configuration options
8286 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
8287 // Configuration initialization
8288 config = $.extend( { scrollable: true }, config );
8290 // Parent constructor
8291 OO.ui.PageLayout.super.call( this, config );
8295 this.outlineItem = null;
8296 this.active = false;
8299 this.$element.addClass( 'oo-ui-pageLayout' );
8304 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
8310 * @param {boolean} active Page is active
8318 * @return {string} Symbolic name of page
8320 OO.ui.PageLayout.prototype.getName = function () {
8325 * Check if page is active.
8327 * @return {boolean} Page is active
8329 OO.ui.PageLayout.prototype.isActive = function () {
8336 * @return {OO.ui.OutlineOptionWidget|null} Outline item widget
8338 OO.ui.PageLayout.prototype.getOutlineItem = function () {
8339 return this.outlineItem;
8345 * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
8346 * outline item as desired; this method is called for setting (with an object) and unsetting
8347 * (with null) and overriding methods would have to check the value of `outlineItem` to avoid
8348 * operating on null instead of an OO.ui.OutlineOptionWidget object.
8350 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear
8353 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
8354 this.outlineItem = outlineItem || null;
8355 if ( outlineItem ) {
8356 this.setupOutlineItem();
8362 * Setup outline item.
8364 * @localdoc Subclasses should override this method to adjust the outline item as desired.
8366 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup
8369 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
8374 * Set page active state.
8376 * @param {boolean} Page is active
8379 OO.ui.PageLayout.prototype.setActive = function ( active ) {
8382 if ( active !== this.active ) {
8383 this.active = active;
8384 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
8385 this.emit( 'active', this.active );
8390 * Layout containing a series of mutually exclusive pages.
8393 * @extends OO.ui.PanelLayout
8394 * @mixins OO.ui.GroupElement
8397 * @param {Object} [config] Configuration options
8398 * @cfg {boolean} [continuous=false] Show all pages, one after another
8399 * @cfg {OO.ui.Layout[]} [items] Layouts to add
8401 OO.ui.StackLayout = function OoUiStackLayout( config ) {
8402 // Configuration initialization
8403 config = $.extend( { scrollable: true }, config );
8405 // Parent constructor
8406 OO.ui.StackLayout.super.call( this, config );
8408 // Mixin constructors
8409 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8412 this.currentItem = null;
8413 this.continuous = !!config.continuous;
8416 this.$element.addClass( 'oo-ui-stackLayout' );
8417 if ( this.continuous ) {
8418 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
8420 if ( Array.isArray( config.items ) ) {
8421 this.addItems( config.items );
8427 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
8428 OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
8434 * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
8440 * Get the current item.
8442 * @return {OO.ui.Layout|null}
8444 OO.ui.StackLayout.prototype.getCurrentItem = function () {
8445 return this.currentItem;
8449 * Unset the current item.
8452 * @param {OO.ui.StackLayout} layout
8455 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
8456 var prevItem = this.currentItem;
8457 if ( prevItem === null ) {
8461 this.currentItem = null;
8462 this.emit( 'set', null );
8468 * Adding an existing item (by value) will move it.
8470 * @param {OO.ui.Layout[]} items Items to add
8471 * @param {number} [index] Index to insert items after
8474 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
8475 // Update the visibility
8476 this.updateHiddenState( items, this.currentItem );
8479 OO.ui.GroupElement.prototype.addItems.call( this, items, index );
8481 if ( !this.currentItem && items.length ) {
8482 this.setItem( items[ 0 ] );
8491 * Items will be detached, not removed, so they can be used later.
8493 * @param {OO.ui.Layout[]} items Items to remove
8497 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
8499 OO.ui.GroupElement.prototype.removeItems.call( this, items );
8501 if ( $.inArray( this.currentItem, items ) !== -1 ) {
8502 if ( this.items.length ) {
8503 this.setItem( this.items[ 0 ] );
8505 this.unsetCurrentItem();
8515 * Items will be detached, not removed, so they can be used later.
8520 OO.ui.StackLayout.prototype.clearItems = function () {
8521 this.unsetCurrentItem();
8522 OO.ui.GroupElement.prototype.clearItems.call( this );
8530 * Any currently shown item will be hidden.
8532 * FIXME: If the passed item to show has not been added in the items list, then
8533 * this method drops it and unsets the current item.
8535 * @param {OO.ui.Layout} item Item to show
8539 OO.ui.StackLayout.prototype.setItem = function ( item ) {
8540 if ( item !== this.currentItem ) {
8541 this.updateHiddenState( this.items, item );
8543 if ( $.inArray( item, this.items ) !== -1 ) {
8544 this.currentItem = item;
8545 this.emit( 'set', item );
8547 this.unsetCurrentItem();
8555 * Update the visibility of all items in case of non-continuous view.
8557 * Ensure all items are hidden except for the selected one.
8558 * This method does nothing when the stack is continuous.
8560 * @param {OO.ui.Layout[]} items Item list iterate over
8561 * @param {OO.ui.Layout} [selectedItem] Selected item to show
8563 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
8566 if ( !this.continuous ) {
8567 for ( i = 0, len = items.length; i < len; i++ ) {
8568 if ( !selectedItem || selectedItem !== items[ i ] ) {
8569 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
8572 if ( selectedItem ) {
8573 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
8579 * Horizontal bar layout of tools as icon buttons.
8582 * @extends OO.ui.ToolGroup
8585 * @param {OO.ui.Toolbar} toolbar
8586 * @param {Object} [config] Configuration options
8588 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
8589 // Parent constructor
8590 OO.ui.BarToolGroup.super.call( this, toolbar, config );
8593 this.$element.addClass( 'oo-ui-barToolGroup' );
8598 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
8600 /* Static Properties */
8602 OO.ui.BarToolGroup.static.titleTooltips = true;
8604 OO.ui.BarToolGroup.static.accelTooltips = true;
8606 OO.ui.BarToolGroup.static.name = 'bar';
8609 * Popup list of tools with an icon and optional label.
8613 * @extends OO.ui.ToolGroup
8614 * @mixins OO.ui.IconElement
8615 * @mixins OO.ui.IndicatorElement
8616 * @mixins OO.ui.LabelElement
8617 * @mixins OO.ui.TitledElement
8618 * @mixins OO.ui.ClippableElement
8621 * @param {OO.ui.Toolbar} toolbar
8622 * @param {Object} [config] Configuration options
8623 * @cfg {string} [header] Text to display at the top of the pop-up
8625 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
8626 // Configuration initialization
8627 config = config || {};
8629 // Parent constructor
8630 OO.ui.PopupToolGroup.super.call( this, toolbar, config );
8632 // Mixin constructors
8633 OO.ui.IconElement.call( this, config );
8634 OO.ui.IndicatorElement.call( this, config );
8635 OO.ui.LabelElement.call( this, config );
8636 OO.ui.TitledElement.call( this, config );
8637 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
8640 this.active = false;
8641 this.dragging = false;
8642 this.onBlurHandler = this.onBlur.bind( this );
8643 this.$handle = $( '<span>' );
8647 'mousedown touchstart': this.onHandlePointerDown.bind( this ),
8648 'mouseup touchend': this.onHandlePointerUp.bind( this )
8653 .addClass( 'oo-ui-popupToolGroup-handle' )
8654 .append( this.$icon, this.$label, this.$indicator );
8655 // If the pop-up should have a header, add it to the top of the toolGroup.
8656 // Note: If this feature is useful for other widgets, we could abstract it into an
8657 // OO.ui.HeaderedElement mixin constructor.
8658 if ( config.header !== undefined ) {
8660 .prepend( $( '<span>' )
8661 .addClass( 'oo-ui-popupToolGroup-header' )
8662 .text( config.header )
8666 .addClass( 'oo-ui-popupToolGroup' )
8667 .prepend( this.$handle );
8672 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
8673 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconElement );
8674 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatorElement );
8675 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabelElement );
8676 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
8677 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
8679 /* Static Properties */
8686 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
8688 OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
8690 if ( this.isDisabled() && this.isElementAttached() ) {
8691 this.setActive( false );
8696 * Handle focus being lost.
8698 * The event is actually generated from a mouseup, so it is not a normal blur event object.
8700 * @param {jQuery.Event} e Mouse up event
8702 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
8703 // Only deactivate when clicking outside the dropdown element
8704 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
8705 this.setActive( false );
8712 OO.ui.PopupToolGroup.prototype.onPointerUp = function ( e ) {
8713 // e.which is 0 for touch events, 1 for left mouse button
8714 // Only close toolgroup when a tool was actually selected
8715 // FIXME: this duplicates logic from the parent class
8716 if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === this.getTargetTool( e ) ) {
8717 this.setActive( false );
8719 return OO.ui.PopupToolGroup.super.prototype.onPointerUp.call( this, e );
8723 * Handle mouse up events.
8725 * @param {jQuery.Event} e Mouse up event
8727 OO.ui.PopupToolGroup.prototype.onHandlePointerUp = function () {
8732 * Handle mouse down events.
8734 * @param {jQuery.Event} e Mouse down event
8736 OO.ui.PopupToolGroup.prototype.onHandlePointerDown = function ( e ) {
8737 // e.which is 0 for touch events, 1 for left mouse button
8738 if ( !this.isDisabled() && e.which <= 1 ) {
8739 this.setActive( !this.active );
8745 * Switch into active mode.
8747 * When active, mouseup events anywhere in the document will trigger deactivation.
8749 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
8751 if ( this.active !== value ) {
8752 this.active = value;
8754 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
8756 // Try anchoring the popup to the left first
8757 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
8758 this.toggleClipping( true );
8759 if ( this.isClippedHorizontally() ) {
8760 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
8761 this.toggleClipping( false );
8763 .removeClass( 'oo-ui-popupToolGroup-left' )
8764 .addClass( 'oo-ui-popupToolGroup-right' );
8765 this.toggleClipping( true );
8768 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
8769 this.$element.removeClass(
8770 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
8772 this.toggleClipping( false );
8778 * Drop down list layout of tools as labeled icon buttons.
8780 * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the
8781 * bottom of the main list. These are not automatically positioned at the bottom of the list; you
8782 * may want to use the 'promote' and 'demote' configuration options to achieve this.
8785 * @extends OO.ui.PopupToolGroup
8788 * @param {OO.ui.Toolbar} toolbar
8789 * @param {Object} [config] Configuration options
8790 * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always
8792 * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be
8793 * allowed to be collapsed.
8794 * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
8796 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
8797 // Configuration initialization
8798 config = config || {};
8800 // Properties (must be set before parent constructor, which calls #populate)
8801 this.allowCollapse = config.allowCollapse;
8802 this.forceExpand = config.forceExpand;
8803 this.expanded = config.expanded !== undefined ? config.expanded : false;
8804 this.collapsibleTools = [];
8806 // Parent constructor
8807 OO.ui.ListToolGroup.super.call( this, toolbar, config );
8810 this.$element.addClass( 'oo-ui-listToolGroup' );
8815 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
8817 /* Static Properties */
8819 OO.ui.ListToolGroup.static.accelTooltips = true;
8821 OO.ui.ListToolGroup.static.name = 'list';
8828 OO.ui.ListToolGroup.prototype.populate = function () {
8829 var i, len, allowCollapse = [];
8831 OO.ui.ListToolGroup.super.prototype.populate.call( this );
8833 // Update the list of collapsible tools
8834 if ( this.allowCollapse !== undefined ) {
8835 allowCollapse = this.allowCollapse;
8836 } else if ( this.forceExpand !== undefined ) {
8837 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
8840 this.collapsibleTools = [];
8841 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
8842 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
8843 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
8847 // Keep at the end, even when tools are added
8848 this.$group.append( this.getExpandCollapseTool().$element );
8850 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
8851 this.updateCollapsibleState();
8854 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
8855 if ( this.expandCollapseTool === undefined ) {
8856 var ExpandCollapseTool = function () {
8857 ExpandCollapseTool.super.apply( this, arguments );
8860 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
8862 ExpandCollapseTool.prototype.onSelect = function () {
8863 this.toolGroup.expanded = !this.toolGroup.expanded;
8864 this.toolGroup.updateCollapsibleState();
8865 this.setActive( false );
8867 ExpandCollapseTool.prototype.onUpdateState = function () {
8868 // Do nothing. Tool interface requires an implementation of this function.
8871 ExpandCollapseTool.static.name = 'more-fewer';
8873 this.expandCollapseTool = new ExpandCollapseTool( this );
8875 return this.expandCollapseTool;
8881 OO.ui.ListToolGroup.prototype.onPointerUp = function ( e ) {
8882 var ret = OO.ui.ListToolGroup.super.prototype.onPointerUp.call( this, e );
8884 // Do not close the popup when the user wants to show more/fewer tools
8885 if ( $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) {
8886 // Prevent the popup list from being hidden
8887 this.setActive( true );
8893 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
8896 this.getExpandCollapseTool()
8897 .setIcon( this.expanded ? 'collapse' : 'expand' )
8898 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
8900 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
8901 this.collapsibleTools[ i ].toggle( this.expanded );
8906 * Drop down menu layout of tools as selectable menu items.
8909 * @extends OO.ui.PopupToolGroup
8912 * @param {OO.ui.Toolbar} toolbar
8913 * @param {Object} [config] Configuration options
8915 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
8916 // Configuration initialization
8917 config = config || {};
8919 // Parent constructor
8920 OO.ui.MenuToolGroup.super.call( this, toolbar, config );
8923 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
8926 this.$element.addClass( 'oo-ui-menuToolGroup' );
8931 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
8933 /* Static Properties */
8935 OO.ui.MenuToolGroup.static.accelTooltips = true;
8937 OO.ui.MenuToolGroup.static.name = 'menu';
8942 * Handle the toolbar state being updated.
8944 * When the state changes, the title of each active item in the menu will be joined together and
8945 * used as a label for the group. The label will be empty if none of the items are active.
8947 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
8951 for ( name in this.tools ) {
8952 if ( this.tools[ name ].isActive() ) {
8953 labelTexts.push( this.tools[ name ].getTitle() );
8957 this.setLabel( labelTexts.join( ', ' ) || ' ' );
8961 * Tool that shows a popup when selected.
8965 * @extends OO.ui.Tool
8966 * @mixins OO.ui.PopupElement
8969 * @param {OO.ui.Toolbar} toolbar
8970 * @param {Object} [config] Configuration options
8972 OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
8973 // Parent constructor
8974 OO.ui.PopupTool.super.call( this, toolbar, config );
8976 // Mixin constructors
8977 OO.ui.PopupElement.call( this, config );
8981 .addClass( 'oo-ui-popupTool' )
8982 .append( this.popup.$element );
8987 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
8988 OO.mixinClass( OO.ui.PopupTool, OO.ui.PopupElement );
8993 * Handle the tool being selected.
8997 OO.ui.PopupTool.prototype.onSelect = function () {
8998 if ( !this.isDisabled() ) {
8999 this.popup.toggle();
9001 this.setActive( false );
9006 * Handle the toolbar state being updated.
9010 OO.ui.PopupTool.prototype.onUpdateState = function () {
9011 this.setActive( false );
9015 * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
9017 * Use together with OO.ui.ItemWidget to make disabled state inheritable.
9021 * @extends OO.ui.GroupElement
9024 * @param {Object} [config] Configuration options
9026 OO.ui.GroupWidget = function OoUiGroupWidget( config ) {
9027 // Parent constructor
9028 OO.ui.GroupWidget.super.call( this, config );
9033 OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
9038 * Set the disabled state of the widget.
9040 * This will also update the disabled state of child widgets.
9042 * @param {boolean} disabled Disable widget
9045 OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
9049 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
9050 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
9052 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
9054 for ( i = 0, len = this.items.length; i < len; i++ ) {
9055 this.items[ i ].updateDisabled();
9063 * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
9065 * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
9066 * allows bidirectional communication.
9068 * Use together with OO.ui.GroupWidget to make disabled state inheritable.
9075 OO.ui.ItemWidget = function OoUiItemWidget() {
9082 * Check if widget is disabled.
9084 * Checks parent if present, making disabled state inheritable.
9086 * @return {boolean} Widget is disabled
9088 OO.ui.ItemWidget.prototype.isDisabled = function () {
9089 return this.disabled ||
9090 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
9094 * Set group element is in.
9096 * @param {OO.ui.GroupElement|null} group Group element, null if none
9099 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
9101 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
9102 OO.ui.Element.prototype.setElementGroup.call( this, group );
9104 // Initialize item disabled states
9105 this.updateDisabled();
9111 * Mixin that adds a menu showing suggested values for a text input.
9113 * Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections.
9115 * Subclasses that set the value of #lookupInput from their `choose` or `select` handler should
9116 * be aware that this will cause new suggestions to be looked up for the new value. If this is
9117 * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
9121 * @deprecated Use OO.ui.LookupElement instead.
9124 * @param {OO.ui.TextInputWidget} input Input widget
9125 * @param {Object} [config] Configuration options
9126 * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
9127 * @cfg {jQuery} [$container=input.$element] Element to render menu under
9129 OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
9130 // Configuration initialization
9131 config = config || {};
9134 this.lookupInput = input;
9135 this.$overlay = config.$overlay || this.$element;
9136 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
9137 input: this.lookupInput,
9138 $container: config.$container
9140 this.lookupCache = {};
9141 this.lookupQuery = null;
9142 this.lookupRequest = null;
9143 this.lookupsDisabled = false;
9144 this.lookupInputFocused = false;
9147 this.lookupInput.$input.on( {
9148 focus: this.onLookupInputFocus.bind( this ),
9149 blur: this.onLookupInputBlur.bind( this ),
9150 mousedown: this.onLookupInputMouseDown.bind( this )
9152 this.lookupInput.connect( this, { change: 'onLookupInputChange' } );
9153 this.lookupMenu.connect( this, { toggle: 'onLookupMenuToggle' } );
9156 this.$element.addClass( 'oo-ui-lookupWidget' );
9157 this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
9158 this.$overlay.append( this.lookupMenu.$element );
9164 * Handle input focus event.
9166 * @param {jQuery.Event} e Input focus event
9168 OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
9169 this.lookupInputFocused = true;
9170 this.populateLookupMenu();
9174 * Handle input blur event.
9176 * @param {jQuery.Event} e Input blur event
9178 OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
9179 this.closeLookupMenu();
9180 this.lookupInputFocused = false;
9184 * Handle input mouse down event.
9186 * @param {jQuery.Event} e Input mouse down event
9188 OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
9189 // Only open the menu if the input was already focused.
9190 // This way we allow the user to open the menu again after closing it with Esc
9191 // by clicking in the input. Opening (and populating) the menu when initially
9192 // clicking into the input is handled by the focus handler.
9193 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
9194 this.populateLookupMenu();
9199 * Handle input change event.
9201 * @param {string} value New input value
9203 OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
9204 if ( this.lookupInputFocused ) {
9205 this.populateLookupMenu();
9210 * Handle the lookup menu being shown/hidden.
9211 * @param {boolean} visible Whether the lookup menu is now visible.
9213 OO.ui.LookupInputWidget.prototype.onLookupMenuToggle = function ( visible ) {
9215 // When the menu is hidden, abort any active request and clear the menu.
9216 // This has to be done here in addition to closeLookupMenu(), because
9217 // MenuSelectWidget will close itself when the user presses Esc.
9218 this.abortLookupRequest();
9219 this.lookupMenu.clearItems();
9226 * @return {OO.ui.TextInputMenuSelectWidget}
9228 OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
9229 return this.lookupMenu;
9233 * Disable or re-enable lookups.
9235 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
9237 * @param {boolean} disabled Disable lookups
9239 OO.ui.LookupInputWidget.prototype.setLookupsDisabled = function ( disabled ) {
9240 this.lookupsDisabled = !!disabled;
9244 * Open the menu. If there are no entries in the menu, this does nothing.
9248 OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
9249 if ( !this.lookupMenu.isEmpty() ) {
9250 this.lookupMenu.toggle( true );
9256 * Close the menu, empty it, and abort any pending request.
9260 OO.ui.LookupInputWidget.prototype.closeLookupMenu = function () {
9261 this.lookupMenu.toggle( false );
9262 this.abortLookupRequest();
9263 this.lookupMenu.clearItems();
9268 * Request menu items based on the input's current value, and when they arrive,
9269 * populate the menu with these items and show the menu.
9271 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
9275 OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
9277 value = this.lookupInput.getValue();
9279 if ( this.lookupsDisabled ) {
9283 // If the input is empty, clear the menu
9284 if ( value === '' ) {
9285 this.closeLookupMenu();
9286 // Skip population if there is already a request pending for the current value
9287 } else if ( value !== this.lookupQuery ) {
9288 this.getLookupMenuItems()
9289 .done( function ( items ) {
9290 widget.lookupMenu.clearItems();
9291 if ( items.length ) {
9295 widget.initializeLookupMenuSelection();
9297 widget.lookupMenu.toggle( false );
9300 .fail( function () {
9301 widget.lookupMenu.clearItems();
9309 * Select and highlight the first selectable item in the menu.
9313 OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
9314 if ( !this.lookupMenu.getSelectedItem() ) {
9315 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
9317 this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
9321 * Get lookup menu items for the current query.
9323 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
9324 * of the done event. If the request was aborted to make way for a subsequent request,
9325 * this promise will not be rejected: it will remain pending forever.
9327 OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
9329 value = this.lookupInput.getValue(),
9330 deferred = $.Deferred(),
9333 this.abortLookupRequest();
9334 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
9335 deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[ value ] ) );
9337 this.lookupInput.pushPending();
9338 this.lookupQuery = value;
9339 ourRequest = this.lookupRequest = this.getLookupRequest();
9341 .always( function () {
9342 // We need to pop pending even if this is an old request, otherwise
9343 // the widget will remain pending forever.
9344 // TODO: this assumes that an aborted request will fail or succeed soon after
9345 // being aborted, or at least eventually. It would be nice if we could popPending()
9346 // at abort time, but only if we knew that we hadn't already called popPending()
9347 // for that request.
9348 widget.lookupInput.popPending();
9350 .done( function ( data ) {
9351 // If this is an old request (and aborting it somehow caused it to still succeed),
9352 // ignore its success completely
9353 if ( ourRequest === widget.lookupRequest ) {
9354 widget.lookupQuery = null;
9355 widget.lookupRequest = null;
9356 widget.lookupCache[ value ] = widget.getLookupCacheItemFromData( data );
9357 deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[ value ] ) );
9360 .fail( function () {
9361 // If this is an old request (or a request failing because it's being aborted),
9362 // ignore its failure completely
9363 if ( ourRequest === widget.lookupRequest ) {
9364 widget.lookupQuery = null;
9365 widget.lookupRequest = null;
9370 return deferred.promise();
9374 * Abort the currently pending lookup request, if any.
9376 OO.ui.LookupInputWidget.prototype.abortLookupRequest = function () {
9377 var oldRequest = this.lookupRequest;
9379 // First unset this.lookupRequest to the fail handler will notice
9380 // that the request is no longer current
9381 this.lookupRequest = null;
9382 this.lookupQuery = null;
9388 * Get a new request object of the current lookup query value.
9391 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
9393 OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
9394 // Stub, implemented in subclass
9399 * Get a list of menu item widgets from the data stored by the lookup request's done handler.
9402 * @param {Mixed} data Cached result data, usually an array
9403 * @return {OO.ui.MenuOptionWidget[]} Menu items
9405 OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
9406 // Stub, implemented in subclass
9411 * Get lookup cache item from server response data.
9414 * @param {Mixed} data Response from server
9415 * @return {Mixed} Cached result data
9417 OO.ui.LookupInputWidget.prototype.getLookupCacheItemFromData = function () {
9418 // Stub, implemented in subclass
9423 * Set of controls for an OO.ui.OutlineSelectWidget.
9425 * Controls include moving items up and down, removing items, and adding different kinds of items.
9428 * @extends OO.ui.Widget
9429 * @mixins OO.ui.GroupElement
9430 * @mixins OO.ui.IconElement
9433 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
9434 * @param {Object} [config] Configuration options
9436 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
9437 // Configuration initialization
9438 config = $.extend( { icon: 'add' }, config );
9440 // Parent constructor
9441 OO.ui.OutlineControlsWidget.super.call( this, config );
9443 // Mixin constructors
9444 OO.ui.GroupElement.call( this, config );
9445 OO.ui.IconElement.call( this, config );
9448 this.outline = outline;
9449 this.$movers = $( '<div>' );
9450 this.upButton = new OO.ui.ButtonWidget( {
9453 title: OO.ui.msg( 'ooui-outline-control-move-up' )
9455 this.downButton = new OO.ui.ButtonWidget( {
9458 title: OO.ui.msg( 'ooui-outline-control-move-down' )
9460 this.removeButton = new OO.ui.ButtonWidget( {
9463 title: OO.ui.msg( 'ooui-outline-control-remove' )
9467 outline.connect( this, {
9468 select: 'onOutlineChange',
9469 add: 'onOutlineChange',
9470 remove: 'onOutlineChange'
9472 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
9473 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
9474 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
9477 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
9478 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
9480 .addClass( 'oo-ui-outlineControlsWidget-movers' )
9481 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
9482 this.$element.append( this.$icon, this.$group, this.$movers );
9487 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
9488 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
9489 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement );
9495 * @param {number} places Number of places to move
9505 * Handle outline change events.
9507 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
9508 var i, len, firstMovable, lastMovable,
9509 items = this.outline.getItems(),
9510 selectedItem = this.outline.getSelectedItem(),
9511 movable = selectedItem && selectedItem.isMovable(),
9512 removable = selectedItem && selectedItem.isRemovable();
9517 while ( ++i < len ) {
9518 if ( items[ i ].isMovable() ) {
9519 firstMovable = items[ i ];
9525 if ( items[ i ].isMovable() ) {
9526 lastMovable = items[ i ];
9531 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
9532 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
9533 this.removeButton.setDisabled( !removable );
9537 * Mixin for widgets with a boolean on/off state.
9543 * @param {Object} [config] Configuration options
9544 * @cfg {boolean} [value=false] Initial value
9546 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
9547 // Configuration initialization
9548 config = config || {};
9554 this.$element.addClass( 'oo-ui-toggleWidget' );
9555 this.setValue( !!config.value );
9562 * @param {boolean} value Changed value
9568 * Get the value of the toggle.
9572 OO.ui.ToggleWidget.prototype.getValue = function () {
9577 * Set the value of the toggle.
9579 * @param {boolean} value New value
9583 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
9585 if ( this.value !== value ) {
9587 this.emit( 'change', value );
9588 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
9589 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
9590 this.$element.attr( 'aria-checked', value.toString() );
9596 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
9597 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
9598 * removed, and cleared from the group.
9601 * // Example: A ButtonGroupWidget with two buttons
9602 * var button1 = new OO.ui.PopupButtonWidget( {
9603 * label : 'Select a category',
9606 * $content: $( '<p>List of categories...</p>' ),
9611 * var button2 = new OO.ui.ButtonWidget( {
9612 * label : 'Add item'
9614 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
9615 * items: [button1, button2]
9617 * $('body').append(buttonGroup.$element);
9620 * @extends OO.ui.Widget
9621 * @mixins OO.ui.GroupElement
9624 * @param {Object} [config] Configuration options
9625 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
9627 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
9628 // Configuration initialization
9629 config = config || {};
9631 // Parent constructor
9632 OO.ui.ButtonGroupWidget.super.call( this, config );
9634 // Mixin constructors
9635 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9638 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
9639 if ( Array.isArray( config.items ) ) {
9640 this.addItems( config.items );
9646 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
9647 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
9650 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
9651 * feels, and functionality can be customized via the class’s configuration options
9652 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
9655 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
9658 * // A button widget
9659 * var button = new OO.ui.ButtonWidget( {
9660 * label : 'Button with Icon',
9662 * iconTitle : 'Remove'
9664 * $( 'body' ).append( button.$element );
9666 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
9669 * @extends OO.ui.Widget
9670 * @mixins OO.ui.ButtonElement
9671 * @mixins OO.ui.IconElement
9672 * @mixins OO.ui.IndicatorElement
9673 * @mixins OO.ui.LabelElement
9674 * @mixins OO.ui.TitledElement
9675 * @mixins OO.ui.FlaggedElement
9676 * @mixins OO.ui.TabIndexedElement
9679 * @param {Object} [config] Configuration options
9680 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
9681 * @cfg {string} [target] The frame or window in which to open the hyperlink.
9682 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
9684 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
9685 // Configuration initialization
9686 // FIXME: The `nofollow` alias is deprecated and will be removed (T89767)
9687 config = $.extend( { noFollow: config && config.nofollow }, config );
9689 // Parent constructor
9690 OO.ui.ButtonWidget.super.call( this, config );
9692 // Mixin constructors
9693 OO.ui.ButtonElement.call( this, config );
9694 OO.ui.IconElement.call( this, config );
9695 OO.ui.IndicatorElement.call( this, config );
9696 OO.ui.LabelElement.call( this, config );
9697 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
9698 OO.ui.FlaggedElement.call( this, config );
9699 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
9704 this.noFollow = false;
9705 this.isHyperlink = false;
9708 this.$button.append( this.$icon, this.$label, this.$indicator );
9710 .addClass( 'oo-ui-buttonWidget' )
9711 .append( this.$button );
9712 this.setHref( config.href );
9713 this.setTarget( config.target );
9714 this.setNoFollow( config.noFollow );
9719 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
9720 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement );
9721 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement );
9722 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement );
9723 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
9724 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
9725 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
9726 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TabIndexedElement );
9733 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
9734 if ( !this.isDisabled() ) {
9735 // Remove the tab-index while the button is down to prevent the button from stealing focus
9736 this.$button.removeAttr( 'tabindex' );
9739 return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e );
9745 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
9746 if ( !this.isDisabled() ) {
9747 // Restore the tab-index after the button is up to restore the button's accessibility
9748 this.$button.attr( 'tabindex', this.tabIndex );
9751 return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e );
9757 OO.ui.ButtonWidget.prototype.onClick = function ( e ) {
9758 var ret = OO.ui.ButtonElement.prototype.onClick.call( this, e );
9759 if ( this.isHyperlink ) {
9768 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
9769 var ret = OO.ui.ButtonElement.prototype.onKeyPress.call( this, e );
9770 if ( this.isHyperlink ) {
9777 * Get hyperlink location.
9779 * @return {string} Hyperlink location
9781 OO.ui.ButtonWidget.prototype.getHref = function () {
9786 * Get hyperlink target.
9788 * @return {string} Hyperlink target
9790 OO.ui.ButtonWidget.prototype.getTarget = function () {
9795 * Get search engine traversal hint.
9797 * @return {boolean} Whether search engines should avoid traversing this hyperlink
9799 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
9800 return this.noFollow;
9804 * Set hyperlink location.
9806 * @param {string|null} href Hyperlink location, null to remove
9808 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
9809 href = typeof href === 'string' ? href : null;
9811 if ( href !== this.href ) {
9813 if ( href !== null ) {
9814 this.$button.attr( 'href', href );
9815 this.isHyperlink = true;
9817 this.$button.removeAttr( 'href' );
9818 this.isHyperlink = false;
9826 * Set hyperlink target.
9828 * @param {string|null} target Hyperlink target, null to remove
9830 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
9831 target = typeof target === 'string' ? target : null;
9833 if ( target !== this.target ) {
9834 this.target = target;
9835 if ( target !== null ) {
9836 this.$button.attr( 'target', target );
9838 this.$button.removeAttr( 'target' );
9846 * Set search engine traversal hint.
9848 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
9850 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
9851 noFollow = typeof noFollow === 'boolean' ? noFollow : true;
9853 if ( noFollow !== this.noFollow ) {
9854 this.noFollow = noFollow;
9856 this.$button.attr( 'rel', 'nofollow' );
9858 this.$button.removeAttr( 'rel' );
9866 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
9867 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
9868 * of the actions. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
9871 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
9874 * @extends OO.ui.ButtonWidget
9875 * @mixins OO.ui.PendingElement
9878 * @param {Object} [config] Configuration options
9879 * @cfg {string} [action] Symbolic action name
9880 * @cfg {string[]} [modes] Symbolic mode names
9881 * @cfg {boolean} [framed=false] Render button with a frame
9883 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
9884 // Configuration initialization
9885 config = $.extend( { framed: false }, config );
9887 // Parent constructor
9888 OO.ui.ActionWidget.super.call( this, config );
9890 // Mixin constructors
9891 OO.ui.PendingElement.call( this, config );
9894 this.action = config.action || '';
9895 this.modes = config.modes || [];
9900 this.$element.addClass( 'oo-ui-actionWidget' );
9905 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
9906 OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement );
9917 * Check if action is available in a certain mode.
9919 * @param {string} mode Name of mode
9920 * @return {boolean} Has mode
9922 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
9923 return this.modes.indexOf( mode ) !== -1;
9927 * Get symbolic action name.
9931 OO.ui.ActionWidget.prototype.getAction = function () {
9936 * Get symbolic action name.
9940 OO.ui.ActionWidget.prototype.getModes = function () {
9941 return this.modes.slice();
9945 * Emit a resize event if the size has changed.
9949 OO.ui.ActionWidget.prototype.propagateResize = function () {
9952 if ( this.isElementAttached() ) {
9953 width = this.$element.width();
9954 height = this.$element.height();
9956 if ( width !== this.width || height !== this.height ) {
9958 this.height = height;
9959 this.emit( 'resize' );
9969 OO.ui.ActionWidget.prototype.setIcon = function () {
9971 OO.ui.IconElement.prototype.setIcon.apply( this, arguments );
9972 this.propagateResize();
9980 OO.ui.ActionWidget.prototype.setLabel = function () {
9982 OO.ui.LabelElement.prototype.setLabel.apply( this, arguments );
9983 this.propagateResize();
9991 OO.ui.ActionWidget.prototype.setFlags = function () {
9993 OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments );
9994 this.propagateResize();
10002 OO.ui.ActionWidget.prototype.clearFlags = function () {
10004 OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments );
10005 this.propagateResize();
10011 * Toggle visibility of button.
10013 * @param {boolean} [show] Show button, omit to toggle visibility
10016 OO.ui.ActionWidget.prototype.toggle = function () {
10018 OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
10019 this.propagateResize();
10025 * Button that shows and hides a popup.
10028 * @extends OO.ui.ButtonWidget
10029 * @mixins OO.ui.PopupElement
10032 * @param {Object} [config] Configuration options
10034 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
10035 // Parent constructor
10036 OO.ui.PopupButtonWidget.super.call( this, config );
10038 // Mixin constructors
10039 OO.ui.PopupElement.call( this, config );
10042 this.connect( this, { click: 'onAction' } );
10046 .addClass( 'oo-ui-popupButtonWidget' )
10047 .attr( 'aria-haspopup', 'true' )
10048 .append( this.popup.$element );
10053 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
10054 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement );
10059 * Handle the button action being triggered.
10061 OO.ui.PopupButtonWidget.prototype.onAction = function () {
10062 this.popup.toggle();
10066 * Button that toggles on and off.
10069 * @extends OO.ui.ButtonWidget
10070 * @mixins OO.ui.ToggleWidget
10073 * @param {Object} [config] Configuration options
10074 * @cfg {boolean} [value=false] Initial value
10076 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
10077 // Configuration initialization
10078 config = config || {};
10080 // Parent constructor
10081 OO.ui.ToggleButtonWidget.super.call( this, config );
10083 // Mixin constructors
10084 OO.ui.ToggleWidget.call( this, config );
10087 this.connect( this, { click: 'onAction' } );
10090 this.$element.addClass( 'oo-ui-toggleButtonWidget' );
10095 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
10096 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
10101 * Handle the button action being triggered.
10103 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
10104 this.setValue( !this.value );
10110 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
10112 if ( value !== this.value ) {
10113 this.$button.attr( 'aria-pressed', value.toString() );
10114 this.setActive( value );
10117 // Parent method (from mixin)
10118 OO.ui.ToggleWidget.prototype.setValue.call( this, value );
10124 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
10125 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
10126 * users can interact with it.
10129 * // Example: A DropdownWidget with a menu that contains three options
10130 * var dropDown=new OO.ui.DropdownWidget( {
10131 * label: 'Dropdown menu: Select a menu option',
10134 * new OO.ui.MenuOptionWidget( {
10138 * new OO.ui.MenuOptionWidget( {
10142 * new OO.ui.MenuOptionWidget( {
10150 * $('body').append(dropDown.$element);
10152 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
10154 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10157 * @extends OO.ui.Widget
10158 * @mixins OO.ui.IconElement
10159 * @mixins OO.ui.IndicatorElement
10160 * @mixins OO.ui.LabelElement
10161 * @mixins OO.ui.TitledElement
10162 * @mixins OO.ui.TabIndexedElement
10165 * @param {Object} [config] Configuration options
10166 * @cfg {Object} [menu] Configuration options to pass to menu widget
10168 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
10169 // Configuration initialization
10170 config = $.extend( { indicator: 'down' }, config );
10172 // Parent constructor
10173 OO.ui.DropdownWidget.super.call( this, config );
10175 // Properties (must be set before TabIndexedElement constructor call)
10176 this.$handle = this.$( '<span>' );
10178 // Mixin constructors
10179 OO.ui.IconElement.call( this, config );
10180 OO.ui.IndicatorElement.call( this, config );
10181 OO.ui.LabelElement.call( this, config );
10182 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
10183 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
10186 this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
10190 click: this.onClick.bind( this ),
10191 keypress: this.onKeyPress.bind( this )
10193 this.menu.connect( this, { select: 'onMenuSelect' } );
10197 .addClass( 'oo-ui-dropdownWidget-handle' )
10198 .append( this.$icon, this.$label, this.$indicator );
10200 .addClass( 'oo-ui-dropdownWidget' )
10201 .append( this.$handle, this.menu.$element );
10206 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
10207 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement );
10208 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
10209 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
10210 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
10211 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TabIndexedElement );
10218 * @return {OO.ui.MenuSelectWidget} Menu of widget
10220 OO.ui.DropdownWidget.prototype.getMenu = function () {
10225 * Handles menu select events.
10228 * @param {OO.ui.MenuOptionWidget} item Selected menu item
10230 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
10237 selectedLabel = item.getLabel();
10239 // If the label is a DOM element, clone it, because setLabel will append() it
10240 if ( selectedLabel instanceof jQuery ) {
10241 selectedLabel = selectedLabel.clone();
10244 this.setLabel( selectedLabel );
10248 * Handle mouse click events.
10251 * @param {jQuery.Event} e Mouse click event
10253 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
10254 if ( !this.isDisabled() && e.which === 1 ) {
10255 this.menu.toggle();
10261 * Handle key press events.
10264 * @param {jQuery.Event} e Key press event
10266 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
10267 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
10268 this.menu.toggle();
10274 * IconWidget is a generic widget for {@link OO.ui.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
10275 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
10276 * for a list of icons included in the library.
10279 * // An icon widget with a label
10280 * var myIcon = new OO.ui.IconWidget({
10282 * iconTitle: 'Help'
10284 * // Create a label.
10285 * var iconLabel = new OO.ui.LabelWidget({
10288 * $('body').append(myIcon.$element, iconLabel.$element);
10290 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
10293 * @extends OO.ui.Widget
10294 * @mixins OO.ui.IconElement
10295 * @mixins OO.ui.TitledElement
10298 * @param {Object} [config] Configuration options
10300 OO.ui.IconWidget = function OoUiIconWidget( config ) {
10301 // Configuration initialization
10302 config = config || {};
10304 // Parent constructor
10305 OO.ui.IconWidget.super.call( this, config );
10307 // Mixin constructors
10308 OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
10309 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
10312 this.$element.addClass( 'oo-ui-iconWidget' );
10317 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
10318 OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
10319 OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
10321 /* Static Properties */
10323 OO.ui.IconWidget.static.tagName = 'span';
10326 * Indicator widget.
10328 * See OO.ui.IndicatorElement for more information.
10331 * @extends OO.ui.Widget
10332 * @mixins OO.ui.IndicatorElement
10333 * @mixins OO.ui.TitledElement
10336 * @param {Object} [config] Configuration options
10338 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
10339 // Configuration initialization
10340 config = config || {};
10342 // Parent constructor
10343 OO.ui.IndicatorWidget.super.call( this, config );
10345 // Mixin constructors
10346 OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
10347 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
10350 this.$element.addClass( 'oo-ui-indicatorWidget' );
10355 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
10356 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
10357 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
10359 /* Static Properties */
10361 OO.ui.IndicatorWidget.static.tagName = 'span';
10364 * InputWidget is the base class for all input widgets, which
10365 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
10366 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
10367 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10369 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10373 * @extends OO.ui.Widget
10374 * @mixins OO.ui.FlaggedElement
10375 * @mixins OO.ui.TabIndexedElement
10378 * @param {Object} [config] Configuration options
10379 * @cfg {string} [name=''] HTML input name
10380 * @cfg {string} [value=''] Input value
10381 * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
10383 OO.ui.InputWidget = function OoUiInputWidget( config ) {
10384 // Configuration initialization
10385 config = config || {};
10387 // Parent constructor
10388 OO.ui.InputWidget.super.call( this, config );
10391 this.$input = this.getInputElement( config );
10393 this.inputFilter = config.inputFilter;
10395 // Mixin constructors
10396 OO.ui.FlaggedElement.call( this, config );
10397 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
10400 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
10404 .attr( 'name', config.name )
10405 .prop( 'disabled', this.isDisabled() );
10406 this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
10407 this.setValue( config.value );
10412 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
10413 OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement );
10414 OO.mixinClass( OO.ui.InputWidget, OO.ui.TabIndexedElement );
10420 * @param {string} value
10426 * Get input element.
10428 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
10429 * different circumstances. The element must have a `value` property (like form elements).
10432 * @param {Object} config Configuration options
10433 * @return {jQuery} Input element
10435 OO.ui.InputWidget.prototype.getInputElement = function () {
10436 return $( '<input>' );
10440 * Handle potentially value-changing events.
10442 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
10444 OO.ui.InputWidget.prototype.onEdit = function () {
10446 if ( !this.isDisabled() ) {
10447 // Allow the stack to clear so the value will be updated
10448 setTimeout( function () {
10449 widget.setValue( widget.$input.val() );
10455 * Get the value of the input.
10457 * @return {string} Input value
10459 OO.ui.InputWidget.prototype.getValue = function () {
10460 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10461 // it, and we won't know unless they're kind enough to trigger a 'change' event.
10462 var value = this.$input.val();
10463 if ( this.value !== value ) {
10464 this.setValue( value );
10470 * Sets the direction of the current input, either RTL or LTR
10472 * @param {boolean} isRTL
10474 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
10475 this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
10479 * Set the value of the input.
10481 * @param {string} value New value
10485 OO.ui.InputWidget.prototype.setValue = function ( value ) {
10486 value = this.cleanUpValue( value );
10487 // Update the DOM if it has changed. Note that with cleanUpValue, it
10488 // is possible for the DOM value to change without this.value changing.
10489 if ( this.$input.val() !== value ) {
10490 this.$input.val( value );
10492 if ( this.value !== value ) {
10493 this.value = value;
10494 this.emit( 'change', this.value );
10500 * Clean up incoming value.
10502 * Ensures value is a string, and converts undefined and null to empty string.
10505 * @param {string} value Original value
10506 * @return {string} Cleaned up value
10508 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
10509 if ( value === undefined || value === null ) {
10511 } else if ( this.inputFilter ) {
10512 return this.inputFilter( String( value ) );
10514 return String( value );
10519 * Simulate the behavior of clicking on a label bound to this input.
10521 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
10522 if ( !this.isDisabled() ) {
10523 if ( this.$input.is( ':checkbox,:radio' ) ) {
10524 this.$input.click();
10525 } else if ( this.$input.is( ':input' ) ) {
10526 this.$input[ 0 ].focus();
10534 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
10535 OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
10536 if ( this.$input ) {
10537 this.$input.prop( 'disabled', this.isDisabled() );
10547 OO.ui.InputWidget.prototype.focus = function () {
10548 this.$input[ 0 ].focus();
10557 OO.ui.InputWidget.prototype.blur = function () {
10558 this.$input[ 0 ].blur();
10563 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
10564 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
10565 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
10566 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
10567 * [OOjs UI documentation on MediaWiki] [1] for more information.
10570 * // A ButtonInputWidget rendered as an HTML button, the default.
10571 * var button = new OO.ui.ButtonInputWidget( {
10572 * label: 'Input button',
10576 * $( 'body' ).append( button.$element );
10578 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
10581 * @extends OO.ui.InputWidget
10582 * @mixins OO.ui.ButtonElement
10583 * @mixins OO.ui.IconElement
10584 * @mixins OO.ui.IndicatorElement
10585 * @mixins OO.ui.LabelElement
10586 * @mixins OO.ui.TitledElement
10587 * @mixins OO.ui.FlaggedElement
10590 * @param {Object} [config] Configuration options
10591 * @cfg {string} [type='button'] HTML tag `type` attribute, may be 'button', 'submit' or 'reset'
10592 * @cfg {boolean} [useInputTag=false] Whether to use `<input/>` rather than `<button/>`. Only useful
10593 * if you need IE 6 support in a form with multiple buttons. If you use this option, icons and
10594 * indicators will not be displayed, it won't be possible to have a non-plaintext label, and it
10595 * won't be possible to set a value (which will internally become identical to the label).
10597 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
10598 // Configuration initialization
10599 config = $.extend( { type: 'button', useInputTag: false }, config );
10601 // Properties (must be set before parent constructor, which calls #setValue)
10602 this.useInputTag = config.useInputTag;
10604 // Parent constructor
10605 OO.ui.ButtonInputWidget.super.call( this, config );
10607 // Mixin constructors
10608 OO.ui.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
10609 OO.ui.IconElement.call( this, config );
10610 OO.ui.IndicatorElement.call( this, config );
10611 OO.ui.LabelElement.call( this, config );
10612 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
10613 OO.ui.FlaggedElement.call( this, config );
10616 if ( !config.useInputTag ) {
10617 this.$input.append( this.$icon, this.$label, this.$indicator );
10619 this.$element.addClass( 'oo-ui-buttonInputWidget' );
10624 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
10625 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.ButtonElement );
10626 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IconElement );
10627 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IndicatorElement );
10628 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement );
10629 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement );
10630 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
10638 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
10639 var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
10646 * Overridden to support setting the 'value' of `<input/>` elements.
10648 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
10649 * text; or null for no label
10652 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
10653 OO.ui.LabelElement.prototype.setLabel.call( this, label );
10655 if ( this.useInputTag ) {
10656 if ( typeof label === 'function' ) {
10657 label = OO.ui.resolveMsg( label );
10659 if ( label instanceof jQuery ) {
10660 label = label.text();
10665 this.$input.val( label );
10672 * Set the value of the input.
10674 * Overridden to disable for `<input/>` elements, which have value identical to the label.
10676 * @param {string} value New value
10679 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
10680 if ( !this.useInputTag ) {
10681 OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value );
10687 * Checkbox input widget.
10690 * @extends OO.ui.InputWidget
10693 * @param {Object} [config] Configuration options
10694 * @cfg {boolean} [selected=false] Whether the checkbox is initially selected
10696 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
10697 // Configuration initialization
10698 config = config || {};
10700 // Parent constructor
10701 OO.ui.CheckboxInputWidget.super.call( this, config );
10704 this.$element.addClass( 'oo-ui-checkboxInputWidget' );
10705 this.setSelected( config.selected !== undefined ? config.selected : false );
10710 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
10718 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
10719 return $( '<input type="checkbox" />' );
10725 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
10727 if ( !this.isDisabled() ) {
10728 // Allow the stack to clear so the value will be updated
10729 setTimeout( function () {
10730 widget.setSelected( widget.$input.prop( 'checked' ) );
10736 * Set selection state of this checkbox.
10738 * @param {boolean} state Whether the checkbox is selected
10741 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
10743 if ( this.selected !== state ) {
10744 this.selected = state;
10745 this.$input.prop( 'checked', this.selected );
10746 this.emit( 'change', this.selected );
10752 * Check if this checkbox is selected.
10754 * @return {boolean} Checkbox is selected
10756 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
10757 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10758 // it, and we won't know unless they're kind enough to trigger a 'change' event.
10759 var selected = this.$input.prop( 'checked' );
10760 if ( this.selected !== selected ) {
10761 this.setSelected( selected );
10763 return this.selected;
10767 * A OO.ui.DropdownWidget synchronized with a `<input type=hidden>` for form submission. Intended to
10768 * be used within a OO.ui.FormLayout.
10771 * @extends OO.ui.InputWidget
10774 * @param {Object} [config] Configuration options
10775 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10777 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
10778 // Configuration initialization
10779 config = config || {};
10781 // Properties (must be done before parent constructor which calls #setDisabled)
10782 this.dropdownWidget = new OO.ui.DropdownWidget();
10784 // Parent constructor
10785 OO.ui.DropdownInputWidget.super.call( this, config );
10788 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
10791 this.setOptions( config.options || [] );
10793 .addClass( 'oo-ui-dropdownInputWidget' )
10794 .append( this.dropdownWidget.$element );
10799 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
10807 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
10808 return $( '<input type="hidden">' );
10812 * Handles menu select events.
10814 * @param {OO.ui.MenuOptionWidget} item Selected menu item
10816 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
10817 this.setValue( item.getData() );
10823 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
10824 var item = this.dropdownWidget.getMenu().getItemFromData( value );
10826 this.dropdownWidget.getMenu().selectItem( item );
10828 OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value );
10835 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
10836 this.dropdownWidget.setDisabled( state );
10837 OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state );
10842 * Set the options available for this input.
10844 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10847 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
10848 var value = this.getValue();
10850 // Rebuild the dropdown menu
10851 this.dropdownWidget.getMenu()
10853 .addItems( options.map( function ( opt ) {
10854 return new OO.ui.MenuOptionWidget( {
10856 label: opt.label !== undefined ? opt.label : opt.data
10860 // Restore the previous value, or reset to something sensible
10861 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
10862 // Previous value is still available, ensure consistency with the dropdown
10863 this.setValue( value );
10865 // No longer valid, reset
10866 if ( options.length ) {
10867 this.setValue( options[ 0 ].data );
10877 OO.ui.DropdownInputWidget.prototype.focus = function () {
10878 this.dropdownWidget.getMenu().toggle( true );
10885 OO.ui.DropdownInputWidget.prototype.blur = function () {
10886 this.dropdownWidget.getMenu().toggle( false );
10891 * Radio input widget.
10893 * Radio buttons only make sense as a set, and you probably want to use the OO.ui.RadioSelectWidget
10894 * class instead of using this class directly.
10897 * @extends OO.ui.InputWidget
10900 * @param {Object} [config] Configuration options
10901 * @cfg {boolean} [selected=false] Whether the radio button is initially selected
10903 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
10904 // Configuration initialization
10905 config = config || {};
10907 // Parent constructor
10908 OO.ui.RadioInputWidget.super.call( this, config );
10911 this.$element.addClass( 'oo-ui-radioInputWidget' );
10912 this.setSelected( config.selected !== undefined ? config.selected : false );
10917 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
10925 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
10926 return $( '<input type="radio" />' );
10932 OO.ui.RadioInputWidget.prototype.onEdit = function () {
10933 // RadioInputWidget doesn't track its state.
10937 * Set selection state of this radio button.
10939 * @param {boolean} state Whether the button is selected
10942 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
10943 // RadioInputWidget doesn't track its state.
10944 this.$input.prop( 'checked', state );
10949 * Check if this radio button is selected.
10951 * @return {boolean} Radio is selected
10953 OO.ui.RadioInputWidget.prototype.isSelected = function () {
10954 return this.$input.prop( 'checked' );
10958 * Input widget with a text field.
10961 * @extends OO.ui.InputWidget
10962 * @mixins OO.ui.IconElement
10963 * @mixins OO.ui.IndicatorElement
10964 * @mixins OO.ui.PendingElement
10965 * @mixins OO.ui.LabelElement
10968 * @param {Object} [config] Configuration options
10969 * @cfg {string} [type='text'] HTML tag `type` attribute
10970 * @cfg {string} [placeholder] Placeholder text
10971 * @cfg {boolean} [autofocus=false] Ask the browser to focus this widget, using the 'autofocus' HTML
10973 * @cfg {boolean} [readOnly=false] Prevent changes
10974 * @cfg {number} [maxLength] Maximum allowed number of characters to input
10975 * @cfg {boolean} [multiline=false] Allow multiple lines of text
10976 * @cfg {boolean} [autosize=false] Automatically resize to fit content
10977 * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
10978 * @cfg {string} [labelPosition='after'] Label position, 'before' or 'after'
10979 * @cfg {boolean} [required=false] Mark the field as required
10980 * @cfg {RegExp|string} [validate] Regular expression to validate against (or symbolic name referencing
10981 * one, see #static-validationPatterns)
10983 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
10984 // Configuration initialization
10985 config = $.extend( {
10987 labelPosition: 'after',
10991 // Parent constructor
10992 OO.ui.TextInputWidget.super.call( this, config );
10994 // Mixin constructors
10995 OO.ui.IconElement.call( this, config );
10996 OO.ui.IndicatorElement.call( this, config );
10997 OO.ui.PendingElement.call( this, config );
10998 OO.ui.LabelElement.call( this, config );
11001 this.readOnly = false;
11002 this.multiline = !!config.multiline;
11003 this.autosize = !!config.autosize;
11004 this.maxRows = config.maxRows;
11005 this.validate = null;
11007 // Clone for resizing
11008 if ( this.autosize ) {
11009 this.$clone = this.$input
11011 .insertAfter( this.$input )
11012 .attr( 'aria-hidden', 'true' )
11013 .addClass( 'oo-ui-element-hidden' );
11016 this.setValidation( config.validate );
11017 this.setPosition( config.labelPosition );
11021 keypress: this.onKeyPress.bind( this ),
11022 blur: this.setValidityFlag.bind( this )
11024 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
11025 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
11026 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
11027 this.on( 'labelChange', this.updatePosition.bind( this ) );
11031 .addClass( 'oo-ui-textInputWidget' )
11032 .append( this.$icon, this.$indicator );
11033 this.setReadOnly( !!config.readOnly );
11034 if ( config.placeholder ) {
11035 this.$input.attr( 'placeholder', config.placeholder );
11037 if ( config.maxLength !== undefined ) {
11038 this.$input.attr( 'maxlength', config.maxLength );
11040 if ( config.autofocus ) {
11041 this.$input.attr( 'autofocus', 'autofocus' );
11043 if ( config.required ) {
11044 this.$input.attr( 'required', 'true' );
11050 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
11051 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
11052 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
11053 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
11054 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.LabelElement );
11056 /* Static properties */
11058 OO.ui.TextInputWidget.static.validationPatterns = {
11066 * User presses enter inside the text box.
11068 * Not called if input is multiline.
11074 * User clicks the icon.
11076 * @deprecated Fundamentally not accessible. Make the icon focusable, associate a label or tooltip,
11077 * and handle click/keypress events on it manually.
11082 * User clicks the indicator.
11084 * @deprecated Fundamentally not accessible. Make the indicator focusable, associate a label or
11085 * tooltip, and handle click/keypress events on it manually.
11092 * Handle icon mouse down events.
11094 * @param {jQuery.Event} e Mouse down event
11097 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
11098 if ( e.which === 1 ) {
11099 this.$input[ 0 ].focus();
11100 this.emit( 'icon' );
11106 * Handle indicator mouse down events.
11108 * @param {jQuery.Event} e Mouse down event
11111 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
11112 if ( e.which === 1 ) {
11113 this.$input[ 0 ].focus();
11114 this.emit( 'indicator' );
11120 * Handle key press events.
11122 * @param {jQuery.Event} e Key press event
11123 * @fires enter If enter key is pressed and input is not multiline
11125 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
11126 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
11127 this.emit( 'enter', e );
11132 * Handle element attach events.
11134 * @param {jQuery.Event} e Element attach event
11136 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
11137 // Any previously calculated size is now probably invalid if we reattached elsewhere
11138 this.valCache = null;
11140 this.positionLabel();
11146 OO.ui.TextInputWidget.prototype.onEdit = function () {
11150 return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
11156 OO.ui.TextInputWidget.prototype.setValue = function ( value ) {
11158 OO.ui.TextInputWidget.super.prototype.setValue.call( this, value );
11160 this.setValidityFlag();
11166 * Check if the widget is read-only.
11168 * @return {boolean}
11170 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
11171 return this.readOnly;
11175 * Set the read-only state of the widget.
11177 * This should probably change the widget's appearance and prevent it from being used.
11179 * @param {boolean} state Make input read-only
11182 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
11183 this.readOnly = !!state;
11184 this.$input.prop( 'readOnly', this.readOnly );
11189 * Automatically adjust the size of the text input.
11191 * This only affects multi-line inputs that are auto-sized.
11195 OO.ui.TextInputWidget.prototype.adjustSize = function () {
11196 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
11198 if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
11200 .val( this.$input.val() )
11201 .attr( 'rows', '' )
11202 // Set inline height property to 0 to measure scroll height
11203 .css( 'height', 0 );
11205 this.$clone.removeClass( 'oo-ui-element-hidden' );
11207 this.valCache = this.$input.val();
11209 scrollHeight = this.$clone[ 0 ].scrollHeight;
11211 // Remove inline height property to measure natural heights
11212 this.$clone.css( 'height', '' );
11213 innerHeight = this.$clone.innerHeight();
11214 outerHeight = this.$clone.outerHeight();
11216 // Measure max rows height
11218 .attr( 'rows', this.maxRows )
11219 .css( 'height', 'auto' )
11221 maxInnerHeight = this.$clone.innerHeight();
11223 // Difference between reported innerHeight and scrollHeight with no scrollbars present
11224 // Equals 1 on Blink-based browsers and 0 everywhere else
11225 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
11226 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
11228 this.$clone.addClass( 'oo-ui-element-hidden' );
11230 // Only apply inline height when expansion beyond natural height is needed
11231 if ( idealHeight > innerHeight ) {
11232 // Use the difference between the inner and outer height as a buffer
11233 this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
11235 this.$input.css( 'height', '' );
11245 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
11246 return config.multiline ? $( '<textarea>' ) : $( '<input type="' + config.type + '" />' );
11250 * Check if input supports multiple lines.
11252 * @return {boolean}
11254 OO.ui.TextInputWidget.prototype.isMultiline = function () {
11255 return !!this.multiline;
11259 * Check if input automatically adjusts its size.
11261 * @return {boolean}
11263 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
11264 return !!this.autosize;
11268 * Select the contents of the input.
11272 OO.ui.TextInputWidget.prototype.select = function () {
11273 this.$input.select();
11278 * Sets the validation pattern to use.
11279 * @param {RegExp|string|null} validate Regular expression (or symbolic name referencing
11280 * one, see #static-validationPatterns)
11282 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
11283 if ( validate instanceof RegExp ) {
11284 this.validate = validate;
11286 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
11291 * Sets the 'invalid' flag appropriately.
11293 OO.ui.TextInputWidget.prototype.setValidityFlag = function () {
11295 this.isValid().done( function ( valid ) {
11296 widget.setFlags( { invalid: !valid } );
11301 * Returns whether or not the current value is considered valid, according to the
11302 * supplied validation pattern.
11304 * @return {jQuery.Deferred}
11306 OO.ui.TextInputWidget.prototype.isValid = function () {
11307 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
11311 * Set the position of the inline label.
11313 * @param {string} labelPosition Label position, 'before' or 'after'
11316 OO.ui.TextInputWidget.prototype.setPosition = function ( labelPosition ) {
11317 this.labelPosition = labelPosition;
11318 this.updatePosition();
11323 * Update the position of the inline label.
11327 OO.ui.TextInputWidget.prototype.updatePosition = function () {
11328 var after = this.labelPosition === 'after';
11331 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
11332 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
11334 if ( this.label ) {
11335 this.positionLabel();
11342 * Position the label by setting the correct padding on the input.
11346 OO.ui.TextInputWidget.prototype.positionLabel = function () {
11347 // Clear old values
11349 // Clear old values if present
11351 'padding-right': '',
11355 if ( this.label ) {
11356 this.$element.append( this.$label );
11358 this.$label.detach();
11362 var after = this.labelPosition === 'after',
11363 rtl = this.$element.css( 'direction' ) === 'rtl',
11364 property = after === rtl ? 'padding-left' : 'padding-right';
11366 this.$input.css( property, this.$label.outerWidth( true ) );
11372 * Text input with a menu of optional values.
11375 * @extends OO.ui.Widget
11376 * @mixins OO.ui.TabIndexedElement
11379 * @param {Object} [config] Configuration options
11380 * @cfg {Object} [menu] Configuration options to pass to menu widget
11381 * @cfg {Object} [input] Configuration options to pass to input widget
11382 * @cfg {jQuery} [$overlay] Overlay layer; defaults to relative positioning
11384 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
11385 // Configuration initialization
11386 config = config || {};
11388 // Parent constructor
11389 OO.ui.ComboBoxWidget.super.call( this, config );
11391 // Properties (must be set before TabIndexedElement constructor call)
11392 this.$indicator = this.$( '<span>' );
11394 // Mixin constructors
11395 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
11398 this.$overlay = config.$overlay || this.$element;
11399 this.input = new OO.ui.TextInputWidget( $.extend(
11402 $indicator: this.$indicator,
11403 disabled: this.isDisabled()
11407 this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
11411 disabled: this.isDisabled()
11417 this.$indicator.on( {
11418 click: this.onClick.bind( this ),
11419 keypress: this.onKeyPress.bind( this )
11421 this.input.connect( this, {
11422 change: 'onInputChange',
11423 enter: 'onInputEnter'
11425 this.menu.connect( this, {
11426 choose: 'onMenuChoose',
11427 add: 'onMenuItemsChange',
11428 remove: 'onMenuItemsChange'
11432 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
11433 this.$overlay.append( this.menu.$element );
11434 this.onMenuItemsChange();
11439 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
11440 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.TabIndexedElement );
11445 * Get the combobox's menu.
11446 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
11448 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
11453 * Handle input change events.
11455 * @param {string} value New value
11457 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
11458 var match = this.menu.getItemFromData( value );
11460 this.menu.selectItem( match );
11461 if ( this.menu.getHighlightedItem() ) {
11462 this.menu.highlightItem( match );
11465 if ( !this.isDisabled() ) {
11466 this.menu.toggle( true );
11471 * Handle mouse click events.
11473 * @param {jQuery.Event} e Mouse click event
11475 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
11476 if ( !this.isDisabled() && e.which === 1 ) {
11477 this.menu.toggle();
11478 this.input.$input[ 0 ].focus();
11484 * Handle key press events.
11486 * @param {jQuery.Event} e Key press event
11488 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
11489 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
11490 this.menu.toggle();
11491 this.input.$input[ 0 ].focus();
11497 * Handle input enter events.
11499 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
11500 if ( !this.isDisabled() ) {
11501 this.menu.toggle( false );
11506 * Handle menu choose events.
11508 * @param {OO.ui.OptionWidget} item Chosen item
11510 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
11512 this.input.setValue( item.getData() );
11517 * Handle menu item change events.
11519 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
11520 var match = this.menu.getItemFromData( this.input.getValue() );
11521 this.menu.selectItem( match );
11522 if ( this.menu.getHighlightedItem() ) {
11523 this.menu.highlightItem( match );
11525 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
11531 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
11533 OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled );
11535 if ( this.input ) {
11536 this.input.setDisabled( this.isDisabled() );
11539 this.menu.setDisabled( this.isDisabled() );
11549 * @extends OO.ui.Widget
11550 * @mixins OO.ui.LabelElement
11553 * @param {Object} [config] Configuration options
11554 * @cfg {OO.ui.InputWidget} [input] Input widget this label is for
11556 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
11557 // Configuration initialization
11558 config = config || {};
11560 // Parent constructor
11561 OO.ui.LabelWidget.super.call( this, config );
11563 // Mixin constructors
11564 OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
11565 OO.ui.TitledElement.call( this, config );
11568 this.input = config.input;
11571 if ( this.input instanceof OO.ui.InputWidget ) {
11572 this.$element.on( 'click', this.onClick.bind( this ) );
11576 this.$element.addClass( 'oo-ui-labelWidget' );
11581 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
11582 OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement );
11583 OO.mixinClass( OO.ui.LabelWidget, OO.ui.TitledElement );
11585 /* Static Properties */
11587 OO.ui.LabelWidget.static.tagName = 'span';
11592 * Handles label mouse click events.
11594 * @param {jQuery.Event} e Mouse click event
11596 OO.ui.LabelWidget.prototype.onClick = function () {
11597 this.input.simulateLabelClick();
11602 * Generic option widget for use with OO.ui.SelectWidget.
11605 * @extends OO.ui.Widget
11606 * @mixins OO.ui.LabelElement
11607 * @mixins OO.ui.FlaggedElement
11610 * @param {Object} [config] Configuration options
11612 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
11613 // Configuration initialization
11614 config = config || {};
11616 // Parent constructor
11617 OO.ui.OptionWidget.super.call( this, config );
11619 // Mixin constructors
11620 OO.ui.ItemWidget.call( this );
11621 OO.ui.LabelElement.call( this, config );
11622 OO.ui.FlaggedElement.call( this, config );
11625 this.selected = false;
11626 this.highlighted = false;
11627 this.pressed = false;
11631 .data( 'oo-ui-optionWidget', this )
11632 .attr( 'role', 'option' )
11633 .addClass( 'oo-ui-optionWidget' )
11634 .append( this.$label );
11639 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
11640 OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
11641 OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement );
11642 OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement );
11644 /* Static Properties */
11646 OO.ui.OptionWidget.static.selectable = true;
11648 OO.ui.OptionWidget.static.highlightable = true;
11650 OO.ui.OptionWidget.static.pressable = true;
11652 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
11657 * Check if option can be selected.
11659 * @return {boolean} Item is selectable
11661 OO.ui.OptionWidget.prototype.isSelectable = function () {
11662 return this.constructor.static.selectable && !this.isDisabled();
11666 * Check if option can be highlighted.
11668 * @return {boolean} Item is highlightable
11670 OO.ui.OptionWidget.prototype.isHighlightable = function () {
11671 return this.constructor.static.highlightable && !this.isDisabled();
11675 * Check if option can be pressed.
11677 * @return {boolean} Item is pressable
11679 OO.ui.OptionWidget.prototype.isPressable = function () {
11680 return this.constructor.static.pressable && !this.isDisabled();
11684 * Check if option is selected.
11686 * @return {boolean} Item is selected
11688 OO.ui.OptionWidget.prototype.isSelected = function () {
11689 return this.selected;
11693 * Check if option is highlighted.
11695 * @return {boolean} Item is highlighted
11697 OO.ui.OptionWidget.prototype.isHighlighted = function () {
11698 return this.highlighted;
11702 * Check if option is pressed.
11704 * @return {boolean} Item is pressed
11706 OO.ui.OptionWidget.prototype.isPressed = function () {
11707 return this.pressed;
11711 * Set selected state.
11713 * @param {boolean} [state=false] Select option
11716 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
11717 if ( this.constructor.static.selectable ) {
11718 this.selected = !!state;
11720 .toggleClass( 'oo-ui-optionWidget-selected', state )
11721 .attr( 'aria-selected', state.toString() );
11722 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
11723 this.scrollElementIntoView();
11725 this.updateThemeClasses();
11731 * Set highlighted state.
11733 * @param {boolean} [state=false] Highlight option
11736 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
11737 if ( this.constructor.static.highlightable ) {
11738 this.highlighted = !!state;
11739 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
11740 this.updateThemeClasses();
11746 * Set pressed state.
11748 * @param {boolean} [state=false] Press option
11751 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
11752 if ( this.constructor.static.pressable ) {
11753 this.pressed = !!state;
11754 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
11755 this.updateThemeClasses();
11761 * Option widget with an option icon and indicator.
11763 * Use together with OO.ui.SelectWidget.
11766 * @extends OO.ui.OptionWidget
11767 * @mixins OO.ui.IconElement
11768 * @mixins OO.ui.IndicatorElement
11771 * @param {Object} [config] Configuration options
11773 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
11774 // Parent constructor
11775 OO.ui.DecoratedOptionWidget.super.call( this, config );
11777 // Mixin constructors
11778 OO.ui.IconElement.call( this, config );
11779 OO.ui.IndicatorElement.call( this, config );
11783 .addClass( 'oo-ui-decoratedOptionWidget' )
11784 .prepend( this.$icon )
11785 .append( this.$indicator );
11790 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
11791 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement );
11792 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
11795 * Option widget that looks like a button.
11797 * Use together with OO.ui.ButtonSelectWidget.
11800 * @extends OO.ui.DecoratedOptionWidget
11801 * @mixins OO.ui.ButtonElement
11802 * @mixins OO.ui.TabIndexedElement
11805 * @param {Object} [config] Configuration options
11807 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
11808 // Configuration initialization
11809 config = $.extend( { tabIndex: -1 }, config );
11811 // Parent constructor
11812 OO.ui.ButtonOptionWidget.super.call( this, config );
11814 // Mixin constructors
11815 OO.ui.ButtonElement.call( this, config );
11816 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
11819 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
11820 this.$button.append( this.$element.contents() );
11821 this.$element.append( this.$button );
11826 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
11827 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
11828 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.TabIndexedElement );
11830 /* Static Properties */
11832 // Allow button mouse down events to pass through so they can be handled by the parent select widget
11833 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
11835 OO.ui.ButtonOptionWidget.static.highlightable = false;
11842 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
11843 OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
11845 if ( this.constructor.static.selectable ) {
11846 this.setActive( state );
11853 * Option widget that looks like a radio button.
11855 * Use together with OO.ui.RadioSelectWidget.
11858 * @extends OO.ui.OptionWidget
11861 * @param {Object} [config] Configuration options
11863 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
11864 // Parent constructor
11865 OO.ui.RadioOptionWidget.super.call( this, config );
11868 this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
11872 .addClass( 'oo-ui-radioOptionWidget' )
11873 .prepend( this.radio.$element );
11878 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
11880 /* Static Properties */
11882 OO.ui.RadioOptionWidget.static.highlightable = false;
11884 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
11886 OO.ui.RadioOptionWidget.static.pressable = false;
11888 OO.ui.RadioOptionWidget.static.tagName = 'label';
11895 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
11896 OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
11898 this.radio.setSelected( state );
11904 * Item of an OO.ui.MenuSelectWidget.
11907 * @extends OO.ui.DecoratedOptionWidget
11910 * @param {Object} [config] Configuration options
11912 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
11913 // Configuration initialization
11914 config = $.extend( { icon: 'check' }, config );
11916 // Parent constructor
11917 OO.ui.MenuOptionWidget.super.call( this, config );
11921 .attr( 'role', 'menuitem' )
11922 .addClass( 'oo-ui-menuOptionWidget' );
11927 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
11929 /* Static Properties */
11931 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
11934 * Section to group one or more items in a OO.ui.MenuSelectWidget.
11937 * @extends OO.ui.DecoratedOptionWidget
11940 * @param {Object} [config] Configuration options
11942 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
11943 // Parent constructor
11944 OO.ui.MenuSectionOptionWidget.super.call( this, config );
11947 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
11952 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
11954 /* Static Properties */
11956 OO.ui.MenuSectionOptionWidget.static.selectable = false;
11958 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
11961 * Items for an OO.ui.OutlineSelectWidget.
11964 * @extends OO.ui.DecoratedOptionWidget
11967 * @param {Object} [config] Configuration options
11968 * @cfg {number} [level] Indentation level
11969 * @cfg {boolean} [movable] Allow modification from outline controls
11971 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
11972 // Configuration initialization
11973 config = config || {};
11975 // Parent constructor
11976 OO.ui.OutlineOptionWidget.super.call( this, config );
11980 this.movable = !!config.movable;
11981 this.removable = !!config.removable;
11984 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
11985 this.setLevel( config.level );
11990 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
11992 /* Static Properties */
11994 OO.ui.OutlineOptionWidget.static.highlightable = false;
11996 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
11998 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
12000 OO.ui.OutlineOptionWidget.static.levels = 3;
12005 * Check if item is movable.
12007 * Movability is used by outline controls.
12009 * @return {boolean} Item is movable
12011 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
12012 return this.movable;
12016 * Check if item is removable.
12018 * Removability is used by outline controls.
12020 * @return {boolean} Item is removable
12022 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
12023 return this.removable;
12027 * Get indentation level.
12029 * @return {number} Indentation level
12031 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
12038 * Movability is used by outline controls.
12040 * @param {boolean} movable Item is movable
12043 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
12044 this.movable = !!movable;
12045 this.updateThemeClasses();
12050 * Set removability.
12052 * Removability is used by outline controls.
12054 * @param {boolean} movable Item is removable
12057 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
12058 this.removable = !!removable;
12059 this.updateThemeClasses();
12064 * Set indentation level.
12066 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
12069 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
12070 var levels = this.constructor.static.levels,
12071 levelClass = this.constructor.static.levelClass,
12074 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
12076 if ( this.level === i ) {
12077 this.$element.addClass( levelClass + i );
12079 this.$element.removeClass( levelClass + i );
12082 this.updateThemeClasses();
12088 * Container for content that is overlaid and positioned absolutely.
12091 * @extends OO.ui.Widget
12092 * @mixins OO.ui.LabelElement
12095 * @param {Object} [config] Configuration options
12096 * @cfg {number} [width=320] Width of popup in pixels
12097 * @cfg {number} [height] Height of popup, omit to use automatic height
12098 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
12099 * @cfg {string} [align='center'] Alignment of popup to origin
12100 * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
12101 * @cfg {number} [containerPadding=10] How much padding to keep between popup and container
12102 * @cfg {jQuery} [$content] Content to append to the popup's body
12103 * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
12104 * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
12105 * @cfg {boolean} [head] Show label and close button at the top
12106 * @cfg {boolean} [padded] Add padding to the body
12108 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
12109 // Configuration initialization
12110 config = config || {};
12112 // Parent constructor
12113 OO.ui.PopupWidget.super.call( this, config );
12115 // Properties (must be set before ClippableElement constructor call)
12116 this.$body = $( '<div>' );
12118 // Mixin constructors
12119 OO.ui.LabelElement.call( this, config );
12120 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
12123 this.$popup = $( '<div>' );
12124 this.$head = $( '<div>' );
12125 this.$anchor = $( '<div>' );
12126 // If undefined, will be computed lazily in updateDimensions()
12127 this.$container = config.$container;
12128 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
12129 this.autoClose = !!config.autoClose;
12130 this.$autoCloseIgnore = config.$autoCloseIgnore;
12131 this.transitionTimeout = null;
12132 this.anchor = null;
12133 this.width = config.width !== undefined ? config.width : 320;
12134 this.height = config.height !== undefined ? config.height : null;
12135 this.align = config.align || 'center';
12136 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
12137 this.onMouseDownHandler = this.onMouseDown.bind( this );
12140 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
12143 this.toggleAnchor( config.anchor === undefined || config.anchor );
12144 this.$body.addClass( 'oo-ui-popupWidget-body' );
12145 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
12147 .addClass( 'oo-ui-popupWidget-head' )
12148 .append( this.$label, this.closeButton.$element );
12149 if ( !config.head ) {
12150 this.$head.addClass( 'oo-ui-element-hidden' );
12153 .addClass( 'oo-ui-popupWidget-popup' )
12154 .append( this.$head, this.$body );
12156 .addClass( 'oo-ui-popupWidget' )
12157 .append( this.$popup, this.$anchor );
12158 // Move content, which was added to #$element by OO.ui.Widget, to the body
12159 if ( config.$content instanceof jQuery ) {
12160 this.$body.append( config.$content );
12162 if ( config.padded ) {
12163 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
12166 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
12167 // that reference properties not initialized at that time of parent class construction
12168 // TODO: Find a better way to handle post-constructor setup
12169 this.visible = false;
12170 this.$element.addClass( 'oo-ui-element-hidden' );
12175 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
12176 OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
12177 OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
12182 * Handles mouse down events.
12184 * @param {jQuery.Event} e Mouse down event
12186 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
12188 this.isVisible() &&
12189 !$.contains( this.$element[ 0 ], e.target ) &&
12190 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
12192 this.toggle( false );
12197 * Bind mouse down listener.
12199 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
12200 // Capture clicks outside popup
12201 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
12205 * Handles close button click events.
12207 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
12208 if ( this.isVisible() ) {
12209 this.toggle( false );
12214 * Unbind mouse down listener.
12216 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
12217 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
12221 * Set whether to show a anchor.
12223 * @param {boolean} [show] Show anchor, omit to toggle
12225 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
12226 show = show === undefined ? !this.anchored : !!show;
12228 if ( this.anchored !== show ) {
12230 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
12232 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
12234 this.anchored = show;
12239 * Check if showing a anchor.
12241 * @return {boolean} anchor is visible
12243 OO.ui.PopupWidget.prototype.hasAnchor = function () {
12244 return this.anchor;
12250 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
12251 show = show === undefined ? !this.isVisible() : !!show;
12253 var change = show !== this.isVisible();
12256 OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
12260 if ( this.autoClose ) {
12261 this.bindMouseDownListener();
12263 this.updateDimensions();
12264 this.toggleClipping( true );
12266 this.toggleClipping( false );
12267 if ( this.autoClose ) {
12268 this.unbindMouseDownListener();
12277 * Set the size of the popup.
12279 * Changing the size may also change the popup's position depending on the alignment.
12281 * @param {number} width Width
12282 * @param {number} height Height
12283 * @param {boolean} [transition=false] Use a smooth transition
12286 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
12287 this.width = width;
12288 this.height = height !== undefined ? height : null;
12289 if ( this.isVisible() ) {
12290 this.updateDimensions( transition );
12295 * Update the size and position.
12297 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
12298 * be called automatically.
12300 * @param {boolean} [transition=false] Use a smooth transition
12303 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
12304 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
12305 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
12308 if ( !this.$container ) {
12309 // Lazy-initialize $container if not specified in constructor
12310 this.$container = $( this.getClosestScrollableElementContainer() );
12313 // Set height and width before measuring things, since it might cause our measurements
12314 // to change (e.g. due to scrollbars appearing or disappearing)
12317 height: this.height !== null ? this.height : 'auto'
12320 // Compute initial popupOffset based on alignment
12321 popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[ this.align ];
12323 // Figure out if this will cause the popup to go beyond the edge of the container
12324 originOffset = this.$element.offset().left;
12325 containerLeft = this.$container.offset().left;
12326 containerWidth = this.$container.innerWidth();
12327 containerRight = containerLeft + containerWidth;
12328 popupLeft = popupOffset - this.containerPadding;
12329 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
12330 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
12331 overlapRight = containerRight - ( originOffset + popupRight );
12333 // Adjust offset to make the popup not go beyond the edge, if needed
12334 if ( overlapRight < 0 ) {
12335 popupOffset += overlapRight;
12336 } else if ( overlapLeft < 0 ) {
12337 popupOffset -= overlapLeft;
12340 // Adjust offset to avoid anchor being rendered too close to the edge
12341 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
12342 // TODO: Find a measurement that works for CSS anchors and image anchors
12343 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
12344 if ( popupOffset + this.width < anchorWidth ) {
12345 popupOffset = anchorWidth - this.width;
12346 } else if ( -popupOffset < anchorWidth ) {
12347 popupOffset = -anchorWidth;
12350 // Prevent transition from being interrupted
12351 clearTimeout( this.transitionTimeout );
12352 if ( transition ) {
12353 // Enable transition
12354 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
12357 // Position body relative to anchor
12358 this.$popup.css( 'margin-left', popupOffset );
12360 if ( transition ) {
12361 // Prevent transitioning after transition is complete
12362 this.transitionTimeout = setTimeout( function () {
12363 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
12366 // Prevent transitioning immediately
12367 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
12370 // Reevaluate clipping state since we've relocated and resized the popup
12377 * Progress bar widget.
12380 * @extends OO.ui.Widget
12383 * @param {Object} [config] Configuration options
12384 * @cfg {number|boolean} [progress=false] Initial progress percent or false for indeterminate
12386 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
12387 // Configuration initialization
12388 config = config || {};
12390 // Parent constructor
12391 OO.ui.ProgressBarWidget.super.call( this, config );
12394 this.$bar = $( '<div>' );
12395 this.progress = null;
12398 this.setProgress( config.progress !== undefined ? config.progress : false );
12399 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
12402 role: 'progressbar',
12403 'aria-valuemin': 0,
12404 'aria-valuemax': 100
12406 .addClass( 'oo-ui-progressBarWidget' )
12407 .append( this.$bar );
12412 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
12414 /* Static Properties */
12416 OO.ui.ProgressBarWidget.static.tagName = 'div';
12421 * Get progress percent
12423 * @return {number} Progress percent
12425 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
12426 return this.progress;
12430 * Set progress percent
12432 * @param {number|boolean} progress Progress percent or false for indeterminate
12434 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
12435 this.progress = progress;
12437 if ( progress !== false ) {
12438 this.$bar.css( 'width', this.progress + '%' );
12439 this.$element.attr( 'aria-valuenow', this.progress );
12441 this.$bar.css( 'width', '' );
12442 this.$element.removeAttr( 'aria-valuenow' );
12444 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
12450 * Search widgets combine a query input, placed above, and a results selection widget, placed below.
12451 * Results are cleared and populated each time the query is changed.
12454 * @extends OO.ui.Widget
12457 * @param {Object} [config] Configuration options
12458 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
12459 * @cfg {string} [value] Initial query value
12461 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
12462 // Configuration initialization
12463 config = config || {};
12465 // Parent constructor
12466 OO.ui.SearchWidget.super.call( this, config );
12469 this.query = new OO.ui.TextInputWidget( {
12471 placeholder: config.placeholder,
12472 value: config.value
12474 this.results = new OO.ui.SelectWidget();
12475 this.$query = $( '<div>' );
12476 this.$results = $( '<div>' );
12479 this.query.connect( this, {
12480 change: 'onQueryChange',
12481 enter: 'onQueryEnter'
12483 this.results.connect( this, {
12484 highlight: 'onResultsHighlight',
12485 select: 'onResultsSelect'
12487 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
12491 .addClass( 'oo-ui-searchWidget-query' )
12492 .append( this.query.$element );
12494 .addClass( 'oo-ui-searchWidget-results' )
12495 .append( this.results.$element );
12497 .addClass( 'oo-ui-searchWidget' )
12498 .append( this.$results, this.$query );
12503 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
12509 * @param {Object|null} item Item data or null if no item is highlighted
12514 * @param {Object|null} item Item data or null if no item is selected
12520 * Handle query key down events.
12522 * @param {jQuery.Event} e Key down event
12524 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
12525 var highlightedItem, nextItem,
12526 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
12529 highlightedItem = this.results.getHighlightedItem();
12530 if ( !highlightedItem ) {
12531 highlightedItem = this.results.getSelectedItem();
12533 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
12534 this.results.highlightItem( nextItem );
12535 nextItem.scrollElementIntoView();
12540 * Handle select widget select events.
12542 * Clears existing results. Subclasses should repopulate items according to new query.
12544 * @param {string} value New value
12546 OO.ui.SearchWidget.prototype.onQueryChange = function () {
12548 this.results.clearItems();
12552 * Handle select widget enter key events.
12554 * Selects highlighted item.
12556 * @param {string} value New value
12558 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
12560 this.results.selectItem( this.results.getHighlightedItem() );
12564 * Handle select widget highlight events.
12566 * @param {OO.ui.OptionWidget} item Highlighted item
12569 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
12570 this.emit( 'highlight', item ? item.getData() : null );
12574 * Handle select widget select events.
12576 * @param {OO.ui.OptionWidget} item Selected item
12579 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
12580 this.emit( 'select', item ? item.getData() : null );
12584 * Get the query input.
12586 * @return {OO.ui.TextInputWidget} Query input
12588 OO.ui.SearchWidget.prototype.getQuery = function () {
12593 * Get the results list.
12595 * @return {OO.ui.SelectWidget} Select list
12597 OO.ui.SearchWidget.prototype.getResults = function () {
12598 return this.results;
12602 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
12603 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
12604 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
12607 * This class should be used together with OO.ui.OptionWidget.
12609 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
12611 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12614 * @extends OO.ui.Widget
12615 * @mixins OO.ui.GroupElement
12618 * @param {Object} [config] Configuration options
12619 * @cfg {OO.ui.OptionWidget[]} [items] Options to add
12621 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
12622 // Configuration initialization
12623 config = config || {};
12625 // Parent constructor
12626 OO.ui.SelectWidget.super.call( this, config );
12628 // Mixin constructors
12629 OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
12632 this.pressed = false;
12633 this.selecting = null;
12634 this.onMouseUpHandler = this.onMouseUp.bind( this );
12635 this.onMouseMoveHandler = this.onMouseMove.bind( this );
12636 this.onKeyDownHandler = this.onKeyDown.bind( this );
12639 this.$element.on( {
12640 mousedown: this.onMouseDown.bind( this ),
12641 mouseover: this.onMouseOver.bind( this ),
12642 mouseleave: this.onMouseLeave.bind( this )
12647 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
12648 .attr( 'role', 'listbox' );
12649 if ( Array.isArray( config.items ) ) {
12650 this.addItems( config.items );
12656 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
12658 // Need to mixin base class as well
12659 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
12660 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
12666 * @param {OO.ui.OptionWidget|null} item Highlighted item
12671 * @param {OO.ui.OptionWidget|null} item Pressed item
12676 * @param {OO.ui.OptionWidget|null} item Selected item
12681 * @param {OO.ui.OptionWidget|null} item Chosen item
12686 * @param {OO.ui.OptionWidget[]} items Added items
12687 * @param {number} index Index items were added at
12692 * @param {OO.ui.OptionWidget[]} items Removed items
12698 * Handle mouse down events.
12701 * @param {jQuery.Event} e Mouse down event
12703 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
12706 if ( !this.isDisabled() && e.which === 1 ) {
12707 this.togglePressed( true );
12708 item = this.getTargetItem( e );
12709 if ( item && item.isSelectable() ) {
12710 this.pressItem( item );
12711 this.selecting = item;
12712 this.getElementDocument().addEventListener(
12714 this.onMouseUpHandler,
12717 this.getElementDocument().addEventListener(
12719 this.onMouseMoveHandler,
12728 * Handle mouse up events.
12731 * @param {jQuery.Event} e Mouse up event
12733 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
12736 this.togglePressed( false );
12737 if ( !this.selecting ) {
12738 item = this.getTargetItem( e );
12739 if ( item && item.isSelectable() ) {
12740 this.selecting = item;
12743 if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
12744 this.pressItem( null );
12745 this.chooseItem( this.selecting );
12746 this.selecting = null;
12749 this.getElementDocument().removeEventListener(
12751 this.onMouseUpHandler,
12754 this.getElementDocument().removeEventListener(
12756 this.onMouseMoveHandler,
12764 * Handle mouse move events.
12767 * @param {jQuery.Event} e Mouse move event
12769 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
12772 if ( !this.isDisabled() && this.pressed ) {
12773 item = this.getTargetItem( e );
12774 if ( item && item !== this.selecting && item.isSelectable() ) {
12775 this.pressItem( item );
12776 this.selecting = item;
12783 * Handle mouse over events.
12786 * @param {jQuery.Event} e Mouse over event
12788 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
12791 if ( !this.isDisabled() ) {
12792 item = this.getTargetItem( e );
12793 this.highlightItem( item && item.isHighlightable() ? item : null );
12799 * Handle mouse leave events.
12802 * @param {jQuery.Event} e Mouse over event
12804 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
12805 if ( !this.isDisabled() ) {
12806 this.highlightItem( null );
12812 * Handle key down events.
12814 * @param {jQuery.Event} e Key down event
12816 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
12819 currentItem = this.getHighlightedItem() || this.getSelectedItem();
12821 if ( !this.isDisabled() && this.isVisible() ) {
12822 switch ( e.keyCode ) {
12823 case OO.ui.Keys.ENTER:
12824 if ( currentItem && currentItem.constructor.static.highlightable ) {
12825 // Was only highlighted, now let's select it. No-op if already selected.
12826 this.chooseItem( currentItem );
12830 case OO.ui.Keys.UP:
12831 case OO.ui.Keys.LEFT:
12832 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
12835 case OO.ui.Keys.DOWN:
12836 case OO.ui.Keys.RIGHT:
12837 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
12840 case OO.ui.Keys.ESCAPE:
12841 case OO.ui.Keys.TAB:
12842 if ( currentItem && currentItem.constructor.static.highlightable ) {
12843 currentItem.setHighlighted( false );
12845 this.unbindKeyDownListener();
12846 // Don't prevent tabbing away / defocusing
12852 if ( nextItem.constructor.static.highlightable ) {
12853 this.highlightItem( nextItem );
12855 this.chooseItem( nextItem );
12857 nextItem.scrollElementIntoView();
12861 // Can't just return false, because e is not always a jQuery event
12862 e.preventDefault();
12863 e.stopPropagation();
12869 * Bind key down listener.
12871 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
12872 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
12876 * Unbind key down listener.
12878 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
12879 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
12883 * Get the closest item to a jQuery.Event.
12886 * @param {jQuery.Event} e
12887 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
12889 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
12890 var $item = $( e.target ).closest( '.oo-ui-optionWidget' );
12891 if ( $item.length ) {
12892 return $item.data( 'oo-ui-optionWidget' );
12898 * Get selected item.
12900 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
12902 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
12905 for ( i = 0, len = this.items.length; i < len; i++ ) {
12906 if ( this.items[ i ].isSelected() ) {
12907 return this.items[ i ];
12914 * Get highlighted item.
12916 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
12918 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
12921 for ( i = 0, len = this.items.length; i < len; i++ ) {
12922 if ( this.items[ i ].isHighlighted() ) {
12923 return this.items[ i ];
12930 * Toggle pressed state.
12932 * @param {boolean} pressed An option is being pressed
12934 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
12935 if ( pressed === undefined ) {
12936 pressed = !this.pressed;
12938 if ( pressed !== this.pressed ) {
12940 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
12941 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
12942 this.pressed = pressed;
12947 * Highlight an item.
12949 * Highlighting is mutually exclusive.
12951 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
12955 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
12956 var i, len, highlighted,
12959 for ( i = 0, len = this.items.length; i < len; i++ ) {
12960 highlighted = this.items[ i ] === item;
12961 if ( this.items[ i ].isHighlighted() !== highlighted ) {
12962 this.items[ i ].setHighlighted( highlighted );
12967 this.emit( 'highlight', item );
12976 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
12980 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
12981 var i, len, selected,
12984 for ( i = 0, len = this.items.length; i < len; i++ ) {
12985 selected = this.items[ i ] === item;
12986 if ( this.items[ i ].isSelected() !== selected ) {
12987 this.items[ i ].setSelected( selected );
12992 this.emit( 'select', item );
13001 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
13005 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
13006 var i, len, pressed,
13009 for ( i = 0, len = this.items.length; i < len; i++ ) {
13010 pressed = this.items[ i ] === item;
13011 if ( this.items[ i ].isPressed() !== pressed ) {
13012 this.items[ i ].setPressed( pressed );
13017 this.emit( 'press', item );
13026 * Identical to #selectItem, but may vary in subclasses that want to take additional action when
13027 * an item is selected using the keyboard or mouse.
13029 * @param {OO.ui.OptionWidget} item Item to choose
13033 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
13034 this.selectItem( item );
13035 this.emit( 'choose', item );
13041 * Get an item relative to another one.
13043 * @param {OO.ui.OptionWidget|null} item Item to start at, null to get relative to list start
13044 * @param {number} direction Direction to move in, -1 to move backward, 1 to move forward
13045 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
13047 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
13048 var currentIndex, nextIndex, i,
13049 increase = direction > 0 ? 1 : -1,
13050 len = this.items.length;
13052 if ( item instanceof OO.ui.OptionWidget ) {
13053 currentIndex = $.inArray( item, this.items );
13054 nextIndex = ( currentIndex + increase + len ) % len;
13056 // If no item is selected and moving forward, start at the beginning.
13057 // If moving backward, start at the end.
13058 nextIndex = direction > 0 ? 0 : len - 1;
13061 for ( i = 0; i < len; i++ ) {
13062 item = this.items[ nextIndex ];
13063 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
13066 nextIndex = ( nextIndex + increase + len ) % len;
13072 * Get the next selectable item.
13074 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
13076 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
13079 for ( i = 0, len = this.items.length; i < len; i++ ) {
13080 item = this.items[ i ];
13081 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
13092 * @param {OO.ui.OptionWidget[]} items Items to add
13093 * @param {number} [index] Index to insert items after
13097 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
13099 OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
13101 // Always provide an index, even if it was omitted
13102 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
13110 * Items will be detached, not removed, so they can be used later.
13112 * @param {OO.ui.OptionWidget[]} items Items to remove
13116 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
13119 // Deselect items being removed
13120 for ( i = 0, len = items.length; i < len; i++ ) {
13122 if ( item.isSelected() ) {
13123 this.selectItem( null );
13128 OO.ui.GroupWidget.prototype.removeItems.call( this, items );
13130 this.emit( 'remove', items );
13138 * Items will be detached, not removed, so they can be used later.
13143 OO.ui.SelectWidget.prototype.clearItems = function () {
13144 var items = this.items.slice();
13147 OO.ui.GroupWidget.prototype.clearItems.call( this );
13150 this.selectItem( null );
13152 this.emit( 'remove', items );
13158 * Select widget containing button options.
13160 * Use together with OO.ui.ButtonOptionWidget.
13163 * @extends OO.ui.SelectWidget
13164 * @mixins OO.ui.TabIndexedElement
13167 * @param {Object} [config] Configuration options
13169 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
13170 // Parent constructor
13171 OO.ui.ButtonSelectWidget.super.call( this, config );
13173 // Mixin constructors
13174 OO.ui.TabIndexedElement.call( this, config );
13177 this.$element.on( {
13178 focus: this.bindKeyDownListener.bind( this ),
13179 blur: this.unbindKeyDownListener.bind( this )
13183 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
13188 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
13189 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.TabIndexedElement );
13192 * Select widget containing radio button options.
13194 * Use together with OO.ui.RadioOptionWidget.
13197 * @extends OO.ui.SelectWidget
13198 * @mixins OO.ui.TabIndexedElement
13201 * @param {Object} [config] Configuration options
13203 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
13204 // Parent constructor
13205 OO.ui.RadioSelectWidget.super.call( this, config );
13207 // Mixin constructors
13208 OO.ui.TabIndexedElement.call( this, config );
13211 this.$element.on( {
13212 focus: this.bindKeyDownListener.bind( this ),
13213 blur: this.unbindKeyDownListener.bind( this )
13217 this.$element.addClass( 'oo-ui-radioSelectWidget' );
13222 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
13223 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.TabIndexedElement );
13226 * Overlaid menu of options.
13228 * Menus are clipped to the visible viewport. They do not provide a control for opening or closing
13231 * Use together with OO.ui.MenuOptionWidget.
13234 * @extends OO.ui.SelectWidget
13235 * @mixins OO.ui.ClippableElement
13238 * @param {Object} [config] Configuration options
13239 * @cfg {OO.ui.TextInputWidget} [input] Input to bind keyboard handlers to
13240 * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
13241 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
13243 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
13244 // Configuration initialization
13245 config = config || {};
13247 // Parent constructor
13248 OO.ui.MenuSelectWidget.super.call( this, config );
13250 // Mixin constructors
13251 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
13254 this.newItems = null;
13255 this.autoHide = config.autoHide === undefined || !!config.autoHide;
13256 this.$input = config.input ? config.input.$input : null;
13257 this.$widget = config.widget ? config.widget.$element : null;
13258 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
13262 .addClass( 'oo-ui-menuSelectWidget' )
13263 .attr( 'role', 'menu' );
13265 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
13266 // that reference properties not initialized at that time of parent class construction
13267 // TODO: Find a better way to handle post-constructor setup
13268 this.visible = false;
13269 this.$element.addClass( 'oo-ui-element-hidden' );
13274 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
13275 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement );
13280 * Handles document mouse down events.
13282 * @param {jQuery.Event} e Key down event
13284 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
13286 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
13287 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
13289 this.toggle( false );
13296 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
13297 var currentItem = this.getHighlightedItem() || this.getSelectedItem();
13299 if ( !this.isDisabled() && this.isVisible() ) {
13300 switch ( e.keyCode ) {
13301 case OO.ui.Keys.LEFT:
13302 case OO.ui.Keys.RIGHT:
13303 // Do nothing if a text field is associated, arrow keys will be handled natively
13304 if ( !this.$input ) {
13305 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
13308 case OO.ui.Keys.ESCAPE:
13309 case OO.ui.Keys.TAB:
13310 if ( currentItem ) {
13311 currentItem.setHighlighted( false );
13313 this.toggle( false );
13314 // Don't prevent tabbing away, prevent defocusing
13315 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
13316 e.preventDefault();
13317 e.stopPropagation();
13321 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
13330 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
13331 if ( this.$input ) {
13332 this.$input.on( 'keydown', this.onKeyDownHandler );
13334 OO.ui.MenuSelectWidget.super.prototype.bindKeyDownListener.call( this );
13341 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
13342 if ( this.$input ) {
13343 this.$input.off( 'keydown', this.onKeyDownHandler );
13345 OO.ui.MenuSelectWidget.super.prototype.unbindKeyDownListener.call( this );
13352 * This will close the menu, unlike #selectItem which only changes selection.
13354 * @param {OO.ui.OptionWidget} item Item to choose
13357 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
13358 OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
13359 this.toggle( false );
13366 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
13370 OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
13373 if ( !this.newItems ) {
13374 this.newItems = [];
13377 for ( i = 0, len = items.length; i < len; i++ ) {
13379 if ( this.isVisible() ) {
13380 // Defer fitting label until item has been attached
13383 this.newItems.push( item );
13387 // Reevaluate clipping
13396 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
13398 OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
13400 // Reevaluate clipping
13409 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
13411 OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
13413 // Reevaluate clipping
13422 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
13423 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
13426 change = visible !== this.isVisible();
13429 OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
13433 this.bindKeyDownListener();
13435 if ( this.newItems && this.newItems.length ) {
13436 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
13437 this.newItems[ i ].fitLabel();
13439 this.newItems = null;
13441 this.toggleClipping( true );
13444 if ( this.autoHide ) {
13445 this.getElementDocument().addEventListener(
13446 'mousedown', this.onDocumentMouseDownHandler, true
13450 this.unbindKeyDownListener();
13451 this.getElementDocument().removeEventListener(
13452 'mousedown', this.onDocumentMouseDownHandler, true
13454 this.toggleClipping( false );
13462 * Menu for a text input widget.
13464 * This menu is specially designed to be positioned beneath a text input widget. The menu's position
13465 * is automatically calculated and maintained when the menu is toggled or the window is resized.
13468 * @extends OO.ui.MenuSelectWidget
13471 * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
13472 * @param {Object} [config] Configuration options
13473 * @cfg {jQuery} [$container=input.$element] Element to render menu under
13475 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( input, config ) {
13476 // Configuration initialization
13477 config = config || {};
13479 // Parent constructor
13480 OO.ui.TextInputMenuSelectWidget.super.call( this, config );
13483 this.input = input;
13484 this.$container = config.$container || this.input.$element;
13485 this.onWindowResizeHandler = this.onWindowResize.bind( this );
13488 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
13493 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
13498 * Handle window resize event.
13500 * @param {jQuery.Event} e Window resize event
13502 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
13509 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
13510 visible = visible === undefined ? !this.isVisible() : !!visible;
13512 var change = visible !== this.isVisible();
13514 if ( change && visible ) {
13515 // Make sure the width is set before the parent method runs.
13516 // After this we have to call this.position(); again to actually
13517 // position ourselves correctly.
13522 OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible );
13525 if ( this.isVisible() ) {
13527 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
13529 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
13537 * Position the menu.
13541 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
13542 var $container = this.$container,
13543 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
13545 // Position under input
13546 pos.top += $container.height();
13547 this.$element.css( pos );
13550 this.setIdealSize( $container.width() );
13551 // We updated the position, so re-evaluate the clipping state
13558 * Structured list of items.
13560 * Use with OO.ui.OutlineOptionWidget.
13563 * @extends OO.ui.SelectWidget
13564 * @mixins OO.ui.TabIndexedElement
13567 * @param {Object} [config] Configuration options
13569 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
13570 // Parent constructor
13571 OO.ui.OutlineSelectWidget.super.call( this, config );
13573 // Mixin constructors
13574 OO.ui.TabIndexedElement.call( this, config );
13577 this.$element.on( {
13578 focus: this.bindKeyDownListener.bind( this ),
13579 blur: this.unbindKeyDownListener.bind( this )
13583 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
13588 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
13589 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.TabIndexedElement );
13592 * Switch that slides on and off.
13595 * @extends OO.ui.Widget
13596 * @mixins OO.ui.ToggleWidget
13597 * @mixins OO.ui.TabIndexedElement
13600 * @param {Object} [config] Configuration options
13601 * @cfg {boolean} [value=false] Initial value
13603 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
13604 // Parent constructor
13605 OO.ui.ToggleSwitchWidget.super.call( this, config );
13607 // Mixin constructors
13608 OO.ui.ToggleWidget.call( this, config );
13609 OO.ui.TabIndexedElement.call( this, config );
13612 this.dragging = false;
13613 this.dragStart = null;
13614 this.sliding = false;
13615 this.$glow = $( '<span>' );
13616 this.$grip = $( '<span>' );
13619 this.$element.on( {
13620 click: this.onClick.bind( this ),
13621 keypress: this.onKeyPress.bind( this )
13625 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
13626 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
13628 .addClass( 'oo-ui-toggleSwitchWidget' )
13629 .attr( 'role', 'checkbox' )
13630 .append( this.$glow, this.$grip );
13635 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
13636 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
13637 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.TabIndexedElement );
13642 * Handle mouse click events.
13644 * @param {jQuery.Event} e Mouse click event
13646 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
13647 if ( !this.isDisabled() && e.which === 1 ) {
13648 this.setValue( !this.value );
13654 * Handle key press events.
13656 * @param {jQuery.Event} e Key press event
13658 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
13659 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
13660 this.setValue( !this.value );