/*!
- * OOUI v0.28.2
+ * OOUI v0.29.6
* https://www.mediawiki.org/wiki/OOUI
*
* Copyright 2011–2018 OOUI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2018-09-11T23:05:15Z
+ * Date: 2018-12-05T00:15:55Z
*/
( function ( OO ) {
*
* @private
* @param {OO.ui.mixin.DraggableElement} item Dropped item
+ * @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.DraggableGroupElement.prototype.onItemDropOrDragEnd = function () {
var targetIndex, originalIndex,
*
* @private
* @chainable
+ * @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
if ( !this.lookupMenu.isEmpty() ) {
*
* @private
* @chainable
+ * @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
this.lookupMenu.toggle( false );
*
* @private
* @chainable
+ * @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
var widget = this,
*
* @param {boolean} readOnly Make input read-only
* @chainable
+ * @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.LookupElement.prototype.setReadOnly = function ( readOnly ) {
// Parent method
*
* @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
* @chainable
+ * @return {OO.ui.TabPanelLayout} The layout, for chaining
*/
OO.ui.TabPanelLayout.prototype.setTabItem = function ( tabItem ) {
this.tabItem = tabItem || null;
*
* @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
* @chainable
+ * @return {OO.ui.TabPanelLayout} The layout, for chaining
*/
OO.ui.TabPanelLayout.prototype.setupTabItem = function () {
this.$element.attr( 'aria-labelledby', this.tabItem.getElementId() );
*
* @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
* @chainable
+ * @return {OO.ui.PageLayout} The layout, for chaining
*/
OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
this.outlineItem = outlineItem || null;
*
* @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
* @chainable
+ * @return {OO.ui.PageLayout} The layout, for chaining
*/
OO.ui.PageLayout.prototype.setupOutlineItem = function () {
return this;
* by setting the #continuous option to 'true'.
*
* @example
- * // A stack layout with two panels, configured to be displayed continously
+ * // A stack layout with two panels, configured to be displayed continuously
* var myStack = new OO.ui.StackLayout( {
* items: [
* new OO.ui.PanelLayout( {
* @param {OO.ui.Layout[]} items Panels to add
* @param {number} [index] Index of the insertion point
* @chainable
+ * @return {OO.ui.StackLayout} The layout, for chaining
*/
OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
// Update the visibility
*
* @param {OO.ui.Layout[]} items Panels to remove
* @chainable
+ * @return {OO.ui.StackLayout} The layout, for chaining
* @fires set
*/
OO.ui.StackLayout.prototype.removeItems = function ( items ) {
* a subset of panels, use the #removeItems method.
*
* @chainable
+ * @return {OO.ui.StackLayout} The layout, for chaining
* @fires set
*/
OO.ui.StackLayout.prototype.clearItems = function () {
*
* @param {OO.ui.Layout} item Panel to show
* @chainable
+ * @return {OO.ui.StackLayout} The layout, for chaining
* @fires set
*/
OO.ui.StackLayout.prototype.setItem = function ( item ) {
return this;
};
+/**
+ * Reset the scroll offset of all panels, or the container if continuous
+ *
+ * @inheritdoc
+ */
+OO.ui.StackLayout.prototype.resetScroll = function () {
+ if ( this.continuous ) {
+ // Parent method
+ return OO.ui.StackLayout.parent.prototype.resetScroll.call( this );
+ }
+ // Reset each panel
+ this.getItems().forEach( function ( panel ) {
+ var hidden = panel.$element.hasClass( 'oo-ui-element-hidden' );
+ // Scroll can only be reset when panel is visible
+ panel.$element.removeClass( 'oo-ui-element-hidden' );
+ panel.resetScroll();
+ if ( hidden ) {
+ panel.$element.addClass( 'oo-ui-element-hidden' );
+ }
+ } );
+
+ return this;
+};
+
/**
* Update the visibility of all items in case of non-continuous view.
*
* and its size is customized with the #menuSize config. The content area will fill all remaining space.
*
* @example
- * var menuLayout = new OO.ui.MenuLayout( {
- * position: 'top'
- * } ),
+ * var menuLayout,
* menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
* contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
* select = new OO.ui.SelectWidget( {
* items: [
* new OO.ui.OptionWidget( {
* data: 'before',
- * label: 'Before',
+ * label: 'Before'
* } ),
* new OO.ui.OptionWidget( {
* data: 'after',
- * label: 'After',
+ * label: 'After'
* } ),
* new OO.ui.OptionWidget( {
* data: 'top',
- * label: 'Top',
+ * label: 'Top'
* } ),
* new OO.ui.OptionWidget( {
* data: 'bottom',
- * label: 'Bottom',
+ * label: 'Bottom'
* } )
* ]
* } ).on( 'select', function ( item ) {
* menuLayout.setMenuPosition( item.getData() );
* } );
*
+ * menuLayout = new OO.ui.MenuLayout( {
+ * position: 'top',
+ * menuPanel: menuPanel,
+ * contentPanel: contentPanel
+ * } )
* menuLayout.$menu.append(
* menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
* );
* may be omitted.
*
* .oo-ui-menuLayout-menu {
- * height: 200px;
* width: 200px;
+ * height: 200px;
* }
+ *
* .oo-ui-menuLayout-content {
* top: 200px;
* left: 200px;
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {OO.ui.PanelLayout} [menuPanel] Menu panel
+ * @cfg {OO.ui.PanelLayout} [contentPanel] Content panel
* @cfg {boolean} [expanded=true] Expand the layout to fill the entire parent element.
* @cfg {boolean} [showMenu=true] Show menu
* @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
// Parent constructor
OO.ui.MenuLayout.parent.call( this, config );
+ this.menuPanel = null;
+ this.contentPanel = null;
this.expanded = !!config.expanded;
/**
* Menu DOM node
} else {
this.$element.addClass( 'oo-ui-menuLayout-static' );
}
+ if ( config.menuPanel ) {
+ this.setMenuPanel( config.menuPanel );
+ }
+ if ( config.contentPanel ) {
+ this.setContentPanel( config.contentPanel );
+ }
this.setMenuPosition( config.menuPosition );
this.toggleMenu( config.showMenu );
};
*
* @param {boolean} showMenu Show menu, omit to toggle
* @chainable
+ * @return {OO.ui.MenuLayout} The layout, for chaining
*/
OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
* @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
* @throws {Error} If position value is not supported
* @chainable
+ * @return {OO.ui.MenuLayout} The layout, for chaining
*/
OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
return this.menuPosition;
};
+/**
+ * Set the menu panel.
+ *
+ * @param {OO.ui.PanelLayout} menuPanel Menu panel
+ */
+OO.ui.MenuLayout.prototype.setMenuPanel = function ( menuPanel ) {
+ this.menuPanel = menuPanel;
+ this.$menu.append( this.menuPanel.$element );
+};
+
+/**
+ * Set the content panel.
+ *
+ * @param {OO.ui.PanelLayout} contentPanel Content panel
+ */
+OO.ui.MenuLayout.prototype.setContentPanel = function ( contentPanel ) {
+ this.contentPanel = contentPanel;
+ this.$content.append( this.contentPanel.$element );
+};
+
+/**
+ * Clear the menu panel.
+ */
+OO.ui.MenuLayout.prototype.clearMenuPanel = function () {
+ this.menuPanel = null;
+ this.$menu.empty();
+};
+
+/**
+ * Clear the content panel.
+ */
+OO.ui.MenuLayout.prototype.clearContentPanel = function () {
+ this.contentPanel = null;
+ this.$content.empty();
+};
+
+/**
+ * Reset the scroll offset of all panels and the tab select widget
+ *
+ * @inheritdoc
+ */
+OO.ui.MenuLayout.prototype.resetScroll = function () {
+ if ( this.menuPanel ) {
+ this.menuPanel.resetScroll();
+ }
+ if ( this.contentPanel ) {
+ this.contentPanel.resetScroll();
+ }
+
+ return this;
+};
+
/**
* BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
* an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
* outlined: true
* } );
*
- * booklet.addPages ( [ page1, page2 ] );
+ * booklet.addPages( [ page1, page2 ] );
* $( 'body' ).append( booklet.$element );
*
* @class
continuous: !!config.continuous,
expanded: this.expanded
} );
- this.$content.append( this.stackLayout.$element );
+ this.setContentPanel( this.stackLayout );
this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
this.outlineVisible = false;
this.outlined = !!config.outlined;
expanded: this.expanded,
scrollable: true
} );
- this.$menu.append( this.outlinePanel.$element );
+ this.setMenuPanel( this.outlinePanel );
this.outlineVisible = true;
if ( this.editable ) {
this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
*
* @param {boolean} [show] Show outline, omit to invert current state
* @chainable
+ * @return {OO.ui.BookletLayout} The layout, for chaining
*/
OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
var booklet = this;
// outline controls are present, delay matches transition on `.oo-ui-menuLayout-menu`.
setTimeout( function () {
OO.ui.Element.static.reconsiderScrollbars( booklet.outlinePanel.$element[ 0 ] );
- }, 200 );
+ }, OO.ui.theme.getDialogTransitionDuration() );
}
}
* @param {number} index Index of the insertion point
* @fires add
* @chainable
+ * @return {OO.ui.BookletLayout} The layout, for chaining
*/
OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
var i, len, name, page, item, currentIndex,
* @param {OO.ui.PageLayout[]} pages An array of pages to remove
* @fires remove
* @chainable
+ * @return {OO.ui.BookletLayout} The layout, for chaining
*/
OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
var i, len, name, page,
*
* @fires remove
* @chainable
+ * @return {OO.ui.BookletLayout} The layout, for chaining
*/
OO.ui.BookletLayout.prototype.clearPages = function () {
var i, len,
}
};
+/**
+ * For outlined-continuous booklets, also reset the outlineSelectWidget to the first item.
+ *
+ * @inheritdoc
+ */
+OO.ui.BookletLayout.prototype.resetScroll = function () {
+ // Parent method
+ OO.ui.BookletLayout.parent.prototype.resetScroll.call( this );
+
+ if ( this.outlined && this.stackLayout.continuous && this.outlineSelectWidget.findFirstSelectableItem() ) {
+ this.scrolling = true;
+ this.outlineSelectWidget.selectItem( this.outlineSelectWidget.findFirstSelectableItem() );
+ this.scrolling = false;
+ }
+ return this;
+};
+
/**
* Select the first selectable page.
*
* @chainable
+ * @return {OO.ui.BookletLayout} The layout, for chaining
*/
OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
if ( !this.outlineSelectWidget.findSelectedItem() ) {
*
* var index = new OO.ui.IndexLayout();
*
- * index.addTabPanels ( [ tabPanel1, tabPanel2 ] );
+ * index.addTabPanels( [ tabPanel1, tabPanel2 ] );
* $( 'body' ).append( index.$element );
*
* @class
continuous: !!config.continuous,
expanded: this.expanded
} );
- this.$content.append( this.stackLayout.$element );
+ this.setContentPanel( this.stackLayout );
this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
this.tabSelectWidget = new OO.ui.TabSelectWidget();
this.tabPanel = new OO.ui.PanelLayout( {
expanded: this.expanded
} );
- this.$menu.append( this.tabPanel.$element );
+ this.setMenuPanel( this.tabPanel );
this.toggleMenu( true );
* @param {number} index Index of the insertion point
* @fires add
* @chainable
+ * @return {OO.ui.BookletLayout} The layout, for chaining
*/
OO.ui.IndexLayout.prototype.addTabPanels = function ( tabPanels, index ) {
var i, len, name, tabPanel, item, currentIndex,
* @param {OO.ui.TabPanelLayout[]} tabPanels An array of tab panels to remove
* @fires remove
* @chainable
+ * @return {OO.ui.BookletLayout} The layout, for chaining
*/
OO.ui.IndexLayout.prototype.removeTabPanels = function ( tabPanels ) {
var i, len, name, tabPanel,
*
* @fires remove
* @chainable
+ * @return {OO.ui.BookletLayout} The layout, for chaining
*/
OO.ui.IndexLayout.prototype.clearTabPanels = function () {
var i, len,
OO.ui.IndexLayout.prototype.setTabPanel = function ( name ) {
var selectedItem,
$focused,
- tabPanel = this.tabPanels[ name ],
- previousTabPanel = this.currentTabPanelName && this.tabPanels[ this.currentTabPanelName ];
+ previousTabPanel,
+ tabPanel = this.tabPanels[ name ];
if ( name !== this.currentTabPanelName ) {
+ previousTabPanel = this.getCurrentTabPanel();
selectedItem = this.tabSelectWidget.findSelectedItem();
if ( selectedItem && selectedItem.getData() !== name ) {
this.tabSelectWidget.selectItemByData( name );
* Select the first selectable tab panel.
*
* @chainable
+ * @return {OO.ui.BookletLayout} The layout, for chaining
*/
OO.ui.IndexLayout.prototype.selectFirstSelectableTabPanel = function () {
if ( !this.tabSelectWidget.findSelectedItem() ) {
* @param {boolean} value The state of the toggle
* @fires change
* @chainable
+ * @return {OO.ui.Widget} The widget, for chaining
*/
OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
value = !!value;
*
* @private
* @param {jQuery.Event} e Mouse click event
+ * @return {undefined/boolean} False to prevent default if event is handled
*/
OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
*
* @private
* @param {jQuery.Event} e Key press event
+ * @return {undefined/boolean} False to prevent default if event is handled
*/
OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
*
* @param {boolean} movable Item is movable
* @chainable
+ * @return {OO.ui.Widget} The widget, for chaining
*/
OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
this.movable = !!movable;
*
* @param {boolean} removable Item is removable
* @chainable
+ * @return {OO.ui.Widget} The widget, for chaining
*/
OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
this.removable = !!removable;
*
* @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
* @chainable
+ * @return {OO.ui.Widget} The widget, for chaining
*/
OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
var levels = this.constructor.static.levels,
// Events
this.$element.on( {
- focus: this.bindKeyDownListener.bind( this ),
- blur: this.unbindKeyDownListener.bind( this )
+ focus: this.bindDocumentKeyDownListener.bind( this ),
+ blur: this.unbindDocumentKeyDownListener.bind( this )
} );
// Initialization
// Events
this.$element.on( {
- focus: this.bindKeyDownListener.bind( this ),
- blur: this.unbindKeyDownListener.bind( this )
+ focus: this.bindDocumentKeyDownListener.bind( this ),
+ blur: this.unbindDocumentKeyDownListener.bind( this )
} );
// Initialization
// Events
this.$element.on( {
- focus: this.bindKeyDownListener.bind( this ),
- blur: this.unbindKeyDownListener.bind( this )
+ focus: this.bindDocumentKeyDownListener.bind( this ),
+ blur: this.unbindDocumentKeyDownListener.bind( this )
} );
// Initialization
OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
/**
- * CapsuleItemWidgets are used within a {@link OO.ui.CapsuleMultiselectWidget
- * CapsuleMultiselectWidget} to display the selected items.
+ * TagItemWidgets are used within a {@link OO.ui.TagMultiselectWidget
+ * TagMultiselectWidget} to display the selected items.
*
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.LabelElement
* @mixins OO.ui.mixin.FlaggedElement
* @mixins OO.ui.mixin.TabIndexedElement
+ * @mixins OO.ui.mixin.DraggableElement
*
* @constructor
- * @param {Object} [config] Configuration options
- * @deprecated
+ * @param {Object} [config] Configuration object
+ * @cfg {boolean} [valid=true] Item is valid
+ * @cfg {boolean} [fixed] Item is fixed. This means the item is
+ * always included in the values and cannot be removed.
*/
-OO.ui.CapsuleItemWidget = function OoUiCapsuleItemWidget( config ) {
- // Configuration initialization
+OO.ui.TagItemWidget = function OoUiTagItemWidget( config ) {
config = config || {};
// Parent constructor
- OO.ui.CapsuleItemWidget.parent.call( this, config );
+ OO.ui.TagItemWidget.parent.call( this, config );
// Mixin constructors
OO.ui.mixin.ItemWidget.call( this );
OO.ui.mixin.LabelElement.call( this, config );
OO.ui.mixin.FlaggedElement.call( this, config );
OO.ui.mixin.TabIndexedElement.call( this, config );
+ OO.ui.mixin.DraggableElement.call( this, config );
+
+ this.valid = config.valid === undefined ? true : !!config.valid;
+ this.fixed = !!config.fixed;
- // Events
this.closeButton = new OO.ui.ButtonWidget( {
framed: false,
icon: 'close',
tabIndex: -1,
title: OO.ui.msg( 'ooui-item-remove' )
- } ).on( 'click', this.onCloseClick.bind( this ) );
+ } );
+ this.closeButton.setDisabled( this.isDisabled() );
- this.on( 'disable', function ( disabled ) {
- this.closeButton.setDisabled( disabled );
- }.bind( this ) );
+ // Events
+ this.closeButton
+ .connect( this, { click: 'remove' } );
+ this.$element
+ .on( 'click', this.select.bind( this ) )
+ .on( 'keydown', this.onKeyDown.bind( this ) )
+ // Prevent propagation of mousedown; the tag item "lives" in the
+ // clickable area of the TagMultiselectWidget, which listens to
+ // mousedown to open the menu or popup. We want to prevent that
+ // for clicks specifically on the tag itself, so the actions taken
+ // are more deliberate. When the tag is clicked, it will emit the
+ // selection event (similar to how #OO.ui.MultioptionWidget emits 'change')
+ // and can be handled separately.
+ .on( 'mousedown', function ( e ) { e.stopPropagation(); } );
// Initialization
this.$element
- .on( {
- click: this.onClick.bind( this ),
- keydown: this.onKeyDown.bind( this )
- } )
- .addClass( 'oo-ui-capsuleItemWidget' )
+ .addClass( 'oo-ui-tagItemWidget' )
.append( this.$label, this.closeButton.$element );
};
-/* Setup */
-
-OO.inheritClass( OO.ui.CapsuleItemWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.ItemWidget );
-OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.FlaggedElement );
-OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.TabIndexedElement );
-
-/* Methods */
+/* Initialization */
-/**
- * Handle close icon clicks
- */
-OO.ui.CapsuleItemWidget.prototype.onCloseClick = function () {
- var element = this.getElementGroup();
+OO.inheritClass( OO.ui.TagItemWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.ItemWidget );
+OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.FlaggedElement );
+OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.TabIndexedElement );
+OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.DraggableElement );
- if ( element && $.isFunction( element.removeItems ) ) {
- element.removeItems( [ this ] );
- element.focus();
- }
-};
+/* Events */
/**
- * Handle click event for the entire capsule
+ * @event remove
+ *
+ * A remove action was performed on the item
*/
-OO.ui.CapsuleItemWidget.prototype.onClick = function () {
- var element = this.getElementGroup();
-
- if ( !this.isDisabled() && element && $.isFunction( element.editItem ) ) {
- element.editItem( this );
- }
-};
/**
- * Handle keyDown event for the entire capsule
+ * @event navigate
+ * @param {string} direction Direction of the movement, forward or backwards
*
- * @param {jQuery.Event} e Key down event
+ * A navigate action was performed on the item
*/
-OO.ui.CapsuleItemWidget.prototype.onKeyDown = function ( e ) {
- var element = this.getElementGroup();
-
- if ( e.keyCode === OO.ui.Keys.BACKSPACE || e.keyCode === OO.ui.Keys.DELETE ) {
- element.removeItems( [ this ] );
- element.focus();
- return false;
- } else if ( e.keyCode === OO.ui.Keys.ENTER ) {
- element.editItem( this );
- return false;
- } else if ( e.keyCode === OO.ui.Keys.LEFT ) {
- element.getPreviousItem( this ).focus();
- } else if ( e.keyCode === OO.ui.Keys.RIGHT ) {
- element.getNextItem( this ).focus();
- }
-};
/**
- * CapsuleMultiselectWidgets are something like a {@link OO.ui.ComboBoxInputWidget combo box widget}
- * that allows for selecting multiple values.
- *
- * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
- *
- * @example
- * // Example: A CapsuleMultiselectWidget.
- * var capsule = new OO.ui.CapsuleMultiselectWidget( {
- * label: 'CapsuleMultiselectWidget',
- * selected: [ 'Option 1', 'Option 3' ],
- * menu: {
- * items: [
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 1',
- * label: 'Option One'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 2',
- * label: 'Option Two'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 3',
- * label: 'Option Three'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 4',
- * label: 'Option Four'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 5',
- * label: 'Option Five'
- * } )
- * ]
- * }
- * } );
- * $( 'body' ).append( capsule.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.GroupElement
- * @mixins OO.ui.mixin.PopupElement
- * @mixins OO.ui.mixin.TabIndexedElement
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.IconElement
- * @uses OO.ui.CapsuleItemWidget
- * @uses OO.ui.MenuSelectWidget
+ * @event select
*
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [placeholder] Placeholder text
- * @cfg {boolean} [allowArbitrary=false] Allow data items to be added even if not present in the menu.
- * @cfg {boolean} [allowDuplicates=false] Allow duplicate items to be added.
- * @cfg {Object} [menu] (required) Configuration options to pass to the
- * {@link OO.ui.MenuSelectWidget menu select widget}.
- * @cfg {Object} [popup] Configuration options to pass to the {@link OO.ui.PopupWidget popup widget}.
- * If specified, this popup will be shown instead of the menu (but the menu
- * will still be used for item labels and allowArbitrary=false). The widgets
- * in the popup should use {@link #addItemsFromData} or {@link #addItems} as necessary.
- * @cfg {jQuery} [$overlay=this.$element] Render the menu or popup into a separate layer.
- * This configuration is useful in cases where the expanded menu is larger than
- * its containing `<div>`. The specified overlay layer is usually on top of
- * the containing `<div>` and has a larger area. By default, the menu uses
- * relative positioning.
- * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
- * @deprecated
+ * The tag widget was selected. This can occur when the widget
+ * is either clicked or enter was pressed on it.
*/
-OO.ui.CapsuleMultiselectWidget = function OoUiCapsuleMultiselectWidget( config ) {
- var $tabFocus;
-
- // Parent constructor
- OO.ui.CapsuleMultiselectWidget.parent.call( this, config );
-
- // Configuration initialization
- config = $.extend( {
- allowArbitrary: false,
- allowDuplicates: false
- }, config );
-
- // Properties (must be set before mixin constructor calls)
- this.$handle = $( '<div>' );
- this.$input = config.popup ? null : $( '<input>' );
- if ( config.placeholder !== undefined && config.placeholder !== '' ) {
- this.$input.attr( 'placeholder', config.placeholder );
- }
-
- // Mixin constructors
- OO.ui.mixin.GroupElement.call( this, config );
- if ( config.popup ) {
- config.popup = $.extend( {}, config.popup, {
- align: 'forwards',
- anchor: false
- } );
- OO.ui.mixin.PopupElement.call( this, config );
- $tabFocus = $( '<span>' );
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: $tabFocus } ) );
- } else {
- this.popup = null;
- $tabFocus = null;
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
- }
- OO.ui.mixin.IndicatorElement.call( this, config );
- OO.ui.mixin.IconElement.call( this, config );
-
- // Properties
- this.$content = $( '<div>' );
- this.allowArbitrary = config.allowArbitrary;
- this.allowDuplicates = config.allowDuplicates;
- this.$overlay = ( config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
- this.menu = new OO.ui.MenuSelectWidget( $.extend(
- {
- widget: this,
- $input: this.$input,
- $floatableContainer: this.$element,
- filterFromInput: true,
- disabled: this.isDisabled()
- },
- config.menu
- ) );
-
- // Events
- if ( this.popup ) {
- $tabFocus.on( {
- focus: this.focus.bind( this )
- } );
- this.popup.$element.on( 'focusout', this.onPopupFocusOut.bind( this ) );
- if ( this.popup.$autoCloseIgnore ) {
- this.popup.$autoCloseIgnore.on( 'focusout', this.onPopupFocusOut.bind( this ) );
- }
- this.popup.connect( this, {
- toggle: function ( visible ) {
- $tabFocus.toggle( !visible );
- }
- } );
- } else {
- this.$input.on( {
- focus: this.onInputFocus.bind( this ),
- blur: this.onInputBlur.bind( this ),
- 'propertychange change click mouseup keydown keyup input cut paste select focus':
- OO.ui.debounce( this.updateInputSize.bind( this ) ),
- keydown: this.onKeyDown.bind( this ),
- keypress: this.onKeyPress.bind( this )
- } );
- }
- this.menu.connect( this, {
- choose: 'onMenuChoose',
- toggle: 'onMenuToggle',
- add: 'onMenuItemsChange',
- remove: 'onMenuItemsChange'
- } );
- this.$handle.on( {
- mousedown: this.onMouseDown.bind( this )
- } );
-
- // Initialization
- if ( this.$input ) {
- this.$input.prop( 'disabled', this.isDisabled() );
- this.$input.attr( {
- role: 'combobox',
- 'aria-owns': this.menu.getElementId(),
- 'aria-autocomplete': 'list'
- } );
- }
- if ( config.data ) {
- this.setItemsFromData( config.data );
- }
- this.$content.addClass( 'oo-ui-capsuleMultiselectWidget-content' )
- .append( this.$group );
- this.$group.addClass( 'oo-ui-capsuleMultiselectWidget-group' );
- this.$handle.addClass( 'oo-ui-capsuleMultiselectWidget-handle' )
- .append( this.$indicator, this.$icon, this.$content );
- this.$element.addClass( 'oo-ui-capsuleMultiselectWidget' )
- .append( this.$handle );
- if ( this.popup ) {
- this.popup.$element.addClass( 'oo-ui-capsuleMultiselectWidget-popup' );
- this.$content.append( $tabFocus );
- this.$overlay.append( this.popup.$element );
- } else {
- this.$content.append( this.$input );
- this.$overlay.append( this.menu.$element );
- }
- if ( $tabFocus ) {
- $tabFocus.addClass( 'oo-ui-capsuleMultiselectWidget-focusTrap' );
- }
-
- // Input size needs to be calculated after everything else is rendered
- setTimeout( function () {
- if ( this.$input ) {
- this.updateInputSize();
- }
- }.bind( this ) );
-
- this.onMenuItemsChange();
-
- // Deprecation warning
- OO.ui.warnDeprecation( 'CapsuleMultiselectWidget: Deprecated widget. Use TagMultiselectWidget instead. See T183299.' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.CapsuleMultiselectWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.CapsuleMultiselectWidget, OO.ui.mixin.GroupElement );
-OO.mixinClass( OO.ui.CapsuleMultiselectWidget, OO.ui.mixin.PopupElement );
-OO.mixinClass( OO.ui.CapsuleMultiselectWidget, OO.ui.mixin.TabIndexedElement );
-OO.mixinClass( OO.ui.CapsuleMultiselectWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.CapsuleMultiselectWidget, OO.ui.mixin.IconElement );
-
-/* Events */
/**
- * @event change
- *
- * A change event is emitted when the set of selected items changes.
+ * @event valid
+ * @param {boolean} isValid Item is valid
*
- * @param {Mixed[]} datas Data of the now-selected items
+ * Item validity has changed
*/
/**
- * @event resize
+ * @event fixed
+ * @param {boolean} isFixed Item is fixed
*
- * A resize event is emitted when the widget's dimensions change to accomodate newly added items or
- * current user input.
+ * Item fixed state has changed
*/
/* Methods */
/**
- * Construct a OO.ui.CapsuleItemWidget (or a subclass thereof) from given label and data.
- * May return `null` if the given label and data are not valid.
+ * Set this item as fixed, meaning it cannot be removed
*
- * @protected
- * @param {Mixed} data Custom data of any type.
- * @param {string} label The label text.
- * @return {OO.ui.CapsuleItemWidget|null}
+ * @param {string} [state] Item is fixed
+ * @fires fixed
+ * @return {OO.ui.Widget} The widget, for chaining
*/
-OO.ui.CapsuleMultiselectWidget.prototype.createItemWidget = function ( data, label ) {
- if ( label === '' ) {
- return null;
- }
- return new OO.ui.CapsuleItemWidget( { data: data, label: label } );
-};
+OO.ui.TagItemWidget.prototype.setFixed = function ( state ) {
+ state = state === undefined ? !this.fixed : !!state;
-/**
- * @inheritdoc
- */
-OO.ui.CapsuleMultiselectWidget.prototype.getInputId = function () {
- if ( !this.$input ) {
- return null;
+ if ( this.fixed !== state ) {
+ this.fixed = state;
+ if ( this.closeButton ) {
+ this.closeButton.toggle( !this.fixed );
+ }
+
+ if ( !this.fixed && this.elementGroup && !this.elementGroup.isDraggable() ) {
+ // Only enable the state of the item if the
+ // entire group is draggable
+ this.toggleDraggable( !this.fixed );
+ }
+ this.$element.toggleClass( 'oo-ui-tagItemWidget-fixed', this.fixed );
+
+ this.emit( 'fixed', this.isFixed() );
}
- return OO.ui.mixin.TabIndexedElement.prototype.getInputId.call( this );
+ return this;
};
/**
- * Get the data of the items in the capsule
- *
- * @return {Mixed[]}
+ * Check whether the item is fixed
+ * @return {boolean}
*/
-OO.ui.CapsuleMultiselectWidget.prototype.getItemsData = function () {
- return this.getItems().map( function ( item ) {
- return item.data;
- } );
+OO.ui.TagItemWidget.prototype.isFixed = function () {
+ return this.fixed;
};
/**
- * Set the items in the capsule by providing data
- *
- * @chainable
- * @param {Mixed[]} datas
- * @return {OO.ui.CapsuleMultiselectWidget}
- */
-OO.ui.CapsuleMultiselectWidget.prototype.setItemsFromData = function ( datas ) {
- var widget = this,
- menu = this.menu,
- items = this.getItems();
-
- $.each( datas, function ( i, data ) {
- var j, label,
- item = menu.findItemFromData( data );
-
- if ( item ) {
- label = item.label;
- } else if ( widget.allowArbitrary ) {
- label = String( data );
- } else {
- return;
- }
-
- item = null;
- for ( j = 0; j < items.length; j++ ) {
- if ( items[ j ].data === data && items[ j ].label === label ) {
- item = items[ j ];
- items.splice( j, 1 );
- break;
- }
- }
- if ( !item ) {
- item = widget.createItemWidget( data, label );
- }
- if ( item ) {
- widget.addItems( [ item ], i );
- }
- } );
-
- if ( items.length ) {
- widget.removeItems( items );
- }
-
- return this;
-};
-
-/**
- * Add items to the capsule by providing their data
- *
- * @chainable
- * @param {Mixed[]} datas
- * @return {OO.ui.CapsuleMultiselectWidget}
- */
-OO.ui.CapsuleMultiselectWidget.prototype.addItemsFromData = function ( datas ) {
- var widget = this,
- menu = this.menu,
- items = [];
-
- $.each( datas, function ( i, data ) {
- var item;
-
- if ( !widget.findItemFromData( data ) || widget.allowDuplicates ) {
- item = menu.findItemFromData( data );
- if ( item ) {
- item = widget.createItemWidget( data, item.label );
- } else if ( widget.allowArbitrary ) {
- item = widget.createItemWidget( data, String( data ) );
- }
- if ( item ) {
- items.push( item );
- }
- }
- } );
-
- if ( items.length ) {
- this.addItems( items );
- }
-
- return this;
-};
-
-/**
- * Add items to the capsule by providing a label
- *
- * @param {string} label
- * @return {boolean} Whether the item was added or not
- */
-OO.ui.CapsuleMultiselectWidget.prototype.addItemFromLabel = function ( label ) {
- var item, items;
- item = this.menu.getItemFromLabel( label, true );
- if ( item ) {
- this.addItemsFromData( [ item.data ] );
- return true;
- } else if ( this.allowArbitrary ) {
- items = this.getItems();
- this.addItemsFromData( [ label ] );
- return !OO.compare( this.getItems(), items );
- }
- return false;
-};
-
-/**
- * Remove items by data
- *
- * @chainable
- * @param {Mixed[]} datas
- * @return {OO.ui.CapsuleMultiselectWidget}
- */
-OO.ui.CapsuleMultiselectWidget.prototype.removeItemsFromData = function ( datas ) {
- var widget = this,
- items = [];
-
- $.each( datas, function ( i, data ) {
- var item = widget.findItemFromData( data );
- if ( item ) {
- items.push( item );
- }
- } );
-
- if ( items.length ) {
- this.removeItems( items );
- }
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.CapsuleMultiselectWidget.prototype.addItems = function ( items ) {
- var same, i, l,
- oldItems = this.items.slice();
-
- OO.ui.mixin.GroupElement.prototype.addItems.call( this, items );
-
- if ( this.items.length !== oldItems.length ) {
- same = false;
- } else {
- same = true;
- for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
- same = same && this.items[ i ] === oldItems[ i ];
- }
- }
- if ( !same ) {
- this.emit( 'change', this.getItemsData() );
- this.updateInputSize();
- }
-
- return this;
-};
-
-/**
- * Removes the item from the list and copies its label to `this.$input`.
- *
- * @param {Object} item
- */
-OO.ui.CapsuleMultiselectWidget.prototype.editItem = function ( item ) {
- this.addItemFromLabel( this.$input.val() );
- this.clearInput();
- this.$input.val( item.label );
- this.updateInputSize();
- this.focus();
- this.menu.updateItemVisibility(); // Hack, we shouldn't be calling this method directly
- this.removeItems( [ item ] );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.CapsuleMultiselectWidget.prototype.removeItems = function ( items ) {
- var same, i, l,
- oldItems = this.items.slice();
-
- OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
-
- if ( this.items.length !== oldItems.length ) {
- same = false;
- } else {
- same = true;
- for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
- same = same && this.items[ i ] === oldItems[ i ];
- }
- }
- if ( !same ) {
- this.emit( 'change', this.getItemsData() );
- this.updateInputSize();
- }
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.CapsuleMultiselectWidget.prototype.clearItems = function () {
- if ( this.items.length ) {
- OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
- this.emit( 'change', this.getItemsData() );
- this.updateInputSize();
- }
- return this;
-};
-
-/**
- * Given an item, returns the item after it. If its the last item,
- * returns `this.$input`. If no item is passed, returns the very first
- * item.
- *
- * @param {OO.ui.CapsuleItemWidget} [item]
- * @return {OO.ui.CapsuleItemWidget|jQuery|boolean}
- */
-OO.ui.CapsuleMultiselectWidget.prototype.getNextItem = function ( item ) {
- var itemIndex;
-
- if ( item === undefined ) {
- return this.items[ 0 ];
- }
-
- itemIndex = this.items.indexOf( item );
- if ( itemIndex < 0 ) { // Item not in list
- return false;
- } else if ( itemIndex === this.items.length - 1 ) { // Last item
- return this.$input;
- } else {
- return this.items[ itemIndex + 1 ];
- }
-};
-
-/**
- * Given an item, returns the item before it. If its the first item,
- * returns `this.$input`. If no item is passed, returns the very last
- * item.
- *
- * @param {OO.ui.CapsuleItemWidget} [item]
- * @return {OO.ui.CapsuleItemWidget|jQuery|boolean}
- */
-OO.ui.CapsuleMultiselectWidget.prototype.getPreviousItem = function ( item ) {
- var itemIndex;
-
- if ( item === undefined ) {
- return this.items[ this.items.length - 1 ];
- }
-
- itemIndex = this.items.indexOf( item );
- if ( itemIndex < 0 ) { // Item not in list
- return false;
- } else if ( itemIndex === 0 ) { // First item
- return this.$input;
- } else {
- return this.items[ itemIndex - 1 ];
- }
-};
-
-/**
- * Get the capsule widget's menu.
- *
- * @return {OO.ui.MenuSelectWidget} Menu widget
- */
-OO.ui.CapsuleMultiselectWidget.prototype.getMenu = function () {
- return this.menu;
-};
-
-/**
- * Handle focus events
- *
- * @private
- * @param {jQuery.Event} event
- */
-OO.ui.CapsuleMultiselectWidget.prototype.onInputFocus = function () {
- if ( !this.isDisabled() ) {
- this.updateInputSize();
- this.menu.toggle( true );
- }
-};
-
-/**
- * Handle blur events
- *
- * @private
- * @param {jQuery.Event} event
- */
-OO.ui.CapsuleMultiselectWidget.prototype.onInputBlur = function () {
- this.addItemFromLabel( this.$input.val() );
- this.clearInput();
-};
-
-/**
- * Handles popup focus out events.
- *
- * @private
- * @param {jQuery.Event} e Focus out event
- */
-OO.ui.CapsuleMultiselectWidget.prototype.onPopupFocusOut = function () {
- var widget = this.popup;
-
- setTimeout( function () {
- if (
- widget.isVisible() &&
- !OO.ui.contains( widget.$element.add( widget.$autoCloseIgnore ).get(), document.activeElement, true )
- ) {
- widget.toggle( false );
- }
- } );
-};
-
-/**
- * Handle mouse down events.
- *
- * @private
- * @param {jQuery.Event} e Mouse down event
- */
-OO.ui.CapsuleMultiselectWidget.prototype.onMouseDown = function ( e ) {
- if ( e.which === OO.ui.MouseButtons.LEFT ) {
- this.focus();
- return false;
- } else {
- this.updateInputSize();
- }
-};
-
-/**
- * Handle key press events.
- *
- * @private
- * @param {jQuery.Event} e Key press event
- */
-OO.ui.CapsuleMultiselectWidget.prototype.onKeyPress = function ( e ) {
- if ( !this.isDisabled() ) {
- if ( e.which === OO.ui.Keys.ESCAPE ) {
- this.clearInput();
- return false;
- }
-
- if ( !this.popup ) {
- this.menu.toggle( true );
- if ( e.which === OO.ui.Keys.ENTER ) {
- if ( this.addItemFromLabel( this.$input.val() ) ) {
- this.clearInput();
- }
- return false;
- }
-
- // Make sure the input gets resized.
- setTimeout( this.updateInputSize.bind( this ), 0 );
- }
- }
-};
-
-/**
- * Handle key down events.
- *
- * @private
- * @param {jQuery.Event} e Key down event
- */
-OO.ui.CapsuleMultiselectWidget.prototype.onKeyDown = function ( e ) {
- if (
- !this.isDisabled() &&
- this.$input.val() === '' &&
- this.items.length
- ) {
- // 'keypress' event is not triggered for Backspace
- if ( e.keyCode === OO.ui.Keys.BACKSPACE ) {
- if ( e.metaKey || e.ctrlKey ) {
- this.removeItems( this.items.slice( -1 ) );
- } else {
- this.editItem( this.items[ this.items.length - 1 ] );
- }
- return false;
- } else if ( e.keyCode === OO.ui.Keys.LEFT ) {
- this.getPreviousItem().focus();
- } else if ( e.keyCode === OO.ui.Keys.RIGHT ) {
- this.getNextItem().focus();
- }
- }
-};
-
-/**
- * Update the dimensions of the text input field to encompass all available area.
- *
- * @private
- * @param {jQuery.Event} e Event of some sort
- */
-OO.ui.CapsuleMultiselectWidget.prototype.updateInputSize = function () {
- var $lastItem, direction, contentWidth, currentWidth, bestWidth;
- if ( this.$input && !this.isDisabled() ) {
- this.$input.css( 'width', '1em' );
- $lastItem = this.$group.children().last();
- direction = OO.ui.Element.static.getDir( this.$handle );
-
- // Get the width of the input with the placeholder text as
- // the value and save it so that we don't keep recalculating
- if (
- this.contentWidthWithPlaceholder === undefined &&
- this.$input.val() === '' &&
- this.$input.attr( 'placeholder' ) !== undefined
- ) {
- this.$input.val( this.$input.attr( 'placeholder' ) );
- this.contentWidthWithPlaceholder = this.$input[ 0 ].scrollWidth;
- this.$input.val( '' );
-
- }
-
- // Always keep the input wide enough for the placeholder text
- contentWidth = Math.max(
- this.$input[ 0 ].scrollWidth,
- // undefined arguments in Math.max lead to NaN
- ( this.contentWidthWithPlaceholder === undefined ) ?
- 0 : this.contentWidthWithPlaceholder
- );
- currentWidth = this.$input.width();
-
- if ( contentWidth < currentWidth ) {
- this.updateIfHeightChanged();
- // All is fine, don't perform expensive calculations
- return;
- }
-
- if ( $lastItem.length === 0 ) {
- bestWidth = this.$content.innerWidth();
- } else {
- bestWidth = direction === 'ltr' ?
- this.$content.innerWidth() - $lastItem.position().left - $lastItem.outerWidth() :
- $lastItem.position().left;
- }
-
- // Some safety margin for sanity, because I *really* don't feel like finding out where the few
- // pixels this is off by are coming from.
- bestWidth -= 10;
- if ( contentWidth > bestWidth ) {
- // This will result in the input getting shifted to the next line
- bestWidth = this.$content.innerWidth() - 10;
- }
- this.$input.width( Math.floor( bestWidth ) );
- this.updateIfHeightChanged();
- } else {
- this.updateIfHeightChanged();
- }
-};
-
-/**
- * Determine if widget height changed, and if so, update menu position and emit 'resize' event.
- *
- * @private
- */
-OO.ui.CapsuleMultiselectWidget.prototype.updateIfHeightChanged = function () {
- var height = this.$element.height();
- if ( height !== this.height ) {
- this.height = height;
- this.menu.position();
- if ( this.popup ) {
- this.popup.updateDimensions();
- }
- this.emit( 'resize' );
- }
-};
-
-/**
- * Handle menu choose events.
- *
- * @private
- * @param {OO.ui.OptionWidget} item Chosen item
- */
-OO.ui.CapsuleMultiselectWidget.prototype.onMenuChoose = function ( item ) {
- if ( item && item.isVisible() ) {
- this.addItemsFromData( [ item.getData() ] );
- this.clearInput();
- }
-};
-
-/**
- * Handle menu toggle events.
- *
- * @private
- * @param {boolean} isVisible Open state of the menu
- */
-OO.ui.CapsuleMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
- this.$element.toggleClass( 'oo-ui-capsuleMultiselectWidget-open', isVisible );
-};
-
-/**
- * Handle menu item change events.
- *
- * @private
- */
-OO.ui.CapsuleMultiselectWidget.prototype.onMenuItemsChange = function () {
- this.setItemsFromData( this.getItemsData() );
- this.$element.toggleClass( 'oo-ui-capsuleMultiselectWidget-empty', this.menu.isEmpty() );
-};
-
-/**
- * Clear the input field
- *
- * @private
- */
-OO.ui.CapsuleMultiselectWidget.prototype.clearInput = function () {
- if ( this.$input ) {
- this.$input.val( '' );
- this.updateInputSize();
- }
- if ( this.popup ) {
- this.popup.toggle( false );
- }
- this.menu.toggle( false );
- this.menu.selectItem();
- this.menu.highlightItem();
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.CapsuleMultiselectWidget.prototype.setDisabled = function ( disabled ) {
- var i, len;
-
- // Parent method
- OO.ui.CapsuleMultiselectWidget.parent.prototype.setDisabled.call( this, disabled );
-
- if ( this.$input ) {
- this.$input.prop( 'disabled', this.isDisabled() );
- }
- if ( this.menu ) {
- this.menu.setDisabled( this.isDisabled() );
- }
- if ( this.popup ) {
- this.popup.setDisabled( this.isDisabled() );
- }
-
- if ( this.items ) {
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[ i ].updateDisabled();
- }
- }
-
- return this;
-};
-
-/**
- * Focus the widget
- *
- * @chainable
- */
-OO.ui.CapsuleMultiselectWidget.prototype.focus = function () {
- if ( !this.isDisabled() ) {
- if ( this.popup ) {
- this.popup.setSize( this.$handle.outerWidth() );
- this.popup.toggle( true );
- OO.ui.findFocusable( this.popup.$element ).focus();
- } else {
- OO.ui.mixin.TabIndexedElement.prototype.focus.call( this );
- }
- }
- return this;
-};
-
-/**
- * TagItemWidgets are used within a {@link OO.ui.TagMultiselectWidget
- * TagMultiselectWidget} to display the selected items.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.ItemWidget
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.FlaggedElement
- * @mixins OO.ui.mixin.TabIndexedElement
- * @mixins OO.ui.mixin.DraggableElement
- *
- * @constructor
- * @param {Object} [config] Configuration object
- * @cfg {boolean} [valid=true] Item is valid
- * @cfg {boolean} [fixed] Item is fixed. This means the item is
- * always included in the values and cannot be removed.
- */
-OO.ui.TagItemWidget = function OoUiTagItemWidget( config ) {
- config = config || {};
-
- // Parent constructor
- OO.ui.TagItemWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.ItemWidget.call( this );
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.FlaggedElement.call( this, config );
- OO.ui.mixin.TabIndexedElement.call( this, config );
- OO.ui.mixin.DraggableElement.call( this, config );
-
- this.valid = config.valid === undefined ? true : !!config.valid;
- this.fixed = !!config.fixed;
-
- this.closeButton = new OO.ui.ButtonWidget( {
- framed: false,
- icon: 'close',
- tabIndex: -1,
- title: OO.ui.msg( 'ooui-item-remove' )
- } );
- this.closeButton.setDisabled( this.isDisabled() );
-
- // Events
- this.closeButton
- .connect( this, { click: 'remove' } );
- this.$element
- .on( 'click', this.select.bind( this ) )
- .on( 'keydown', this.onKeyDown.bind( this ) )
- // Prevent propagation of mousedown; the tag item "lives" in the
- // clickable area of the TagMultiselectWidget, which listens to
- // mousedown to open the menu or popup. We want to prevent that
- // for clicks specifically on the tag itself, so the actions taken
- // are more deliberate. When the tag is clicked, it will emit the
- // selection event (similar to how #OO.ui.MultioptionWidget emits 'change')
- // and can be handled separately.
- .on( 'mousedown', function ( e ) { e.stopPropagation(); } );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-tagItemWidget' )
- .append( this.$label, this.closeButton.$element );
-};
-
-/* Initialization */
-
-OO.inheritClass( OO.ui.TagItemWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.ItemWidget );
-OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.FlaggedElement );
-OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.TabIndexedElement );
-OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.DraggableElement );
-
-/* Events */
-
-/**
- * @event remove
- *
- * A remove action was performed on the item
- */
-
-/**
- * @event navigate
- * @param {string} direction Direction of the movement, forward or backwards
- *
- * A navigate action was performed on the item
- */
-
-/**
- * @event select
- *
- * The tag widget was selected. This can occur when the widget
- * is either clicked or enter was pressed on it.
- */
-
-/**
- * @event valid
- * @param {boolean} isValid Item is valid
- *
- * Item validity has changed
- */
-
-/**
- * @event fixed
- * @param {boolean} isFixed Item is fixed
- *
- * Item fixed state has changed
- */
-
-/* Methods */
-
-/**
- * Set this item as fixed, meaning it cannot be removed
- *
- * @param {string} [state] Item is fixed
- * @fires fixed
- */
-OO.ui.TagItemWidget.prototype.setFixed = function ( state ) {
- state = state === undefined ? !this.fixed : !!state;
-
- if ( this.fixed !== state ) {
- this.fixed = state;
- if ( this.closeButton ) {
- this.closeButton.toggle( !this.fixed );
- }
-
- if ( !this.fixed && this.elementGroup && !this.elementGroup.isDraggable() ) {
- // Only enable the state of the item if the
- // entire group is draggable
- this.toggleDraggable( !this.fixed );
- }
- this.$element.toggleClass( 'oo-ui-tagItemWidget-fixed', this.fixed );
-
- this.emit( 'fixed', this.isFixed() );
- }
- return this;
-};
-
-/**
- * Check whether the item is fixed
- */
-OO.ui.TagItemWidget.prototype.isFixed = function () {
- return this.fixed;
-};
-
-/**
- * @inheritdoc
+ * @inheritdoc
*/
OO.ui.TagItemWidget.prototype.setDisabled = function ( state ) {
if ( state && this.elementGroup && !this.elementGroup.isDisabled() ) {
OO.ui.TagItemWidget.prototype.onKeyDown = function ( e ) {
var movement;
- if ( !this.isDisabled() && !this.isFixed() && e.keyCode === OO.ui.Keys.BACKSPACE || e.keyCode === OO.ui.Keys.DELETE ) {
+ if ( !this.isDisabled() && !this.isFixed() && ( e.keyCode === OO.ui.Keys.BACKSPACE || e.keyCode === OO.ui.Keys.DELETE ) ) {
this.remove();
return false;
} else if ( e.keyCode === OO.ui.Keys.ENTER ) {
* replace the input widget used in the TagMultiselectWidget. If not given,
* TagMultiselectWidget creates its own.
* @cfg {boolean} [inputPosition='inline'] Position of the input. Options are:
- * - inline: The input is invisible, but exists inside the tag list, so
- * the user types into the tag groups to add tags.
- * - outline: The input is underneath the tag area.
- * - none: No input supplied
+ * - inline: The input is invisible, but exists inside the tag list, so
+ * the user types into the tag groups to add tags.
+ * - outline: The input is underneath the tag area.
+ * - none: No input supplied
* @cfg {boolean} [allowEditTags=true] Allow editing of the tags by clicking them
* @cfg {boolean} [allowArbitrary=false] Allow data items to be added even if
* not present in the menu.
* invalid tags. These tags will display with an invalid state, and
* the widget as a whole will have an invalid state if any invalid tags
* are present.
+ * @cfg {number} [tagLimit] An optional limit on the number of selected options.
+ * If 'tagLimit' is set and is reached, the input is disabled, not allowing any
+ * additions. If 'tagLimit' is unset or is 0, an unlimited number of items can be
+ * added.
* @cfg {boolean} [allowReordering=true] Allow reordering of the items
* @cfg {Object[]|String[]} [selected] A set of selected tags. If given,
* these will appear in the tag list on initialization, as long as they
this.allowedValues = config.allowedValues || [];
this.allowDisplayInvalidTags = config.allowDisplayInvalidTags;
this.hasInput = this.inputPosition !== 'none';
+ this.tagLimit = config.tagLimit;
this.height = null;
this.valid = true;
};
this.input.$input.on( inputEvents );
+ this.inputPlaceholder = this.input.$input.attr( 'placeholder' );
if ( this.inputPosition === 'outline' ) {
// Override max-height for the input widget
// in the case the widget is outline so it can
- // stretch all the way if the widet is wide
+ // stretch all the way if the widget is wide
this.input.$element.css( 'max-width', 'inherit' );
this.$element
.addClass( 'oo-ui-tagMultiselectWidget-outlined' )
*/
OO.ui.TagMultiselectWidget.prototype.onInputFocus = function () {
this.$element.addClass( 'oo-ui-tagMultiselectWidget-focus' );
+ // Reset validity
+ this.toggleValid( true );
};
/**
*/
OO.ui.TagMultiselectWidget.prototype.onInputBlur = function () {
this.$element.removeClass( 'oo-ui-tagMultiselectWidget-focus' );
+
+ // Set the widget as invalid if there's text in the input
+ this.addTagFromInput();
+ this.toggleValid( this.checkValidity() && ( !this.hasInput || !this.input.getValue() ) );
};
/**
* Respond to change event, where items were added, removed, or cleared.
*/
OO.ui.TagMultiselectWidget.prototype.onChangeTags = function () {
- this.toggleValid( this.checkValidity() );
+ var isUnderLimit = this.isUnderLimit();
+
+ // Reset validity
+ this.toggleValid(
+ this.checkValidity() &&
+ !( this.hasInput && this.input.getValue() )
+ );
+
if ( this.hasInput ) {
this.updateInputSize();
+ if ( !isUnderLimit ) {
+ // Clear the input
+ this.input.setValue( '' );
+ }
+ if ( this.inputPosition === 'outline' ) {
+ // Show/clear the placeholder and enable/disable the input
+ // based on whether we are/aren't under the specified limit
+ this.input.$input.attr( 'placeholder', isUnderLimit ? this.inputPlaceholder : '' );
+ this.input.setDisabled( !isUnderLimit );
+ } else {
+ // Show/hide the input
+ this.input.$input.toggleClass( 'oo-ui-element-hidden', !isUnderLimit );
+ }
}
this.updateIfHeightChanged();
};
OO.ui.TagMultiselectWidget.parent.prototype.setDisabled.call( this, isDisabled );
if ( this.hasInput && this.input ) {
- this.input.setDisabled( !!isDisabled );
+ this.input.setDisabled( !!isDisabled && !this.isUnderLimit() );
}
if ( this.items ) {
}
if ( isValid || this.allowDisplayInvalidTags ) {
- this.addTag( val );
this.clearInput();
- this.focus();
+ this.addTag( val );
}
};
* This object must contain at least a data key. Example:
* { data: 'foo', label: 'Foo item' }
* For multiple items, use an array of objects. For example:
- * [
- * { data: 'foo', label: 'Foo item' },
- * { data: 'bar', label: 'Bar item' }
- * ]
+ * [
+ * { data: 'foo', label: 'Foo item' },
+ * { data: 'bar', label: 'Bar item' }
+ * ]
* Value can also be added with plaintext array, for example:
* [ 'foo', 'bar', 'bla' ] or a single string, like 'foo'
*/
var newItemWidget,
isValid = this.isAllowedData( data );
- if ( isValid || this.allowDisplayInvalidTags ) {
+ if ( this.isUnderLimit() && ( isValid || this.allowDisplayInvalidTags ) ) {
newItemWidget = this.createTagItemWidget( data, label );
newItemWidget.toggleValid( isValid );
this.addItems( [ newItemWidget ] );
return true;
}
+
return false;
};
+/**
+ * Check whether the number of current tags is within the limit.
+ *
+ * @return {boolean} True if current tag count is within the limit or
+ * if 'tagLimit' is not set
+ */
+OO.ui.TagMultiselectWidget.prototype.isUnderLimit = function () {
+ return !this.tagLimit ||
+ this.getItemCount() < this.tagLimit;
+};
+
/**
* Remove tag by its data property.
*
// Some safety margin for sanity, because I *really* don't feel like finding out where the few
// pixels this is off by are coming from.
- bestWidth -= 10;
+ bestWidth -= 13;
if ( contentWidth > bestWidth ) {
// This will result in the input getting shifted to the next line
- bestWidth = this.$content.innerWidth() - 10;
+ bestWidth = this.$content.innerWidth() - 13;
}
this.input.$input.width( Math.floor( bestWidth ) );
this.updateIfHeightChanged();
this.menu.toggle( true );
};
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.onInputBlur = function () {
+ // Parent method
+ OO.ui.MenuTagMultiselectWidget.parent.prototype.onInputBlur.call( this );
+
+ this.menu.toggle( false );
+};
+
/**
* Respond to input change event
*/
*/
OO.ui.MenuTagMultiselectWidget.prototype.initializeMenuSelection = function () {
if ( !this.menu.findSelectedItem() ) {
- this.menu.highlightItem( this.menu.findFirstSelectableItem() );
+ this.menu.highlightItem(
+ this.allowArbitrary ?
+ null :
+ this.menu.findFirstSelectableItem()
+ );
}
};
* @inheritdoc
*/
OO.ui.MenuTagMultiselectWidget.prototype.addTagFromInput = function () {
- var inputValue = this.input.getValue(),
- validated = false,
- highlightedItem = this.menu.findHighlightedItem(),
- item = this.menu.findItemFromData( inputValue );
-
- if ( !inputValue ) {
- return;
- }
+ var val = this.input.getValue(),
+ // Look for a highlighted item first
+ // Then look for the element that fits the data
+ item = this.menu.findHighlightedItem() || this.menu.findItemFromData( val ),
+ data = item ? item.getData() : val,
+ isValid = this.isAllowedData( data );
// Override the parent method so we add from the menu
// rather than directly from the input
- // Look for a highlighted item first
- if ( highlightedItem ) {
- validated = this.addTag( highlightedItem.getData(), highlightedItem.getLabel() );
- } else if ( item ) {
- // Look for the element that fits the data
- validated = this.addTag( item.getData(), item.getLabel() );
- } else {
- // Otherwise, add the tag - the method will only add if the
- // tag is valid or if invalid tags are allowed
- validated = this.addTag( inputValue );
+ if ( !val ) {
+ return;
}
- if ( validated ) {
+ if ( isValid || this.allowDisplayInvalidTags ) {
this.clearInput();
- this.focus();
+ if ( item ) {
+ this.addTag( data, item.getLabel() );
+ } else {
+ this.addTag( val );
+ }
}
};
* Focusses the select file button.
*
* @chainable
+ * @return {OO.ui.Widget} The widget, for chaining
*/
OO.ui.SelectFileWidget.prototype.focus = function () {
this.selectButton.focus();
* Blur the widget.
*
* @chainable
+ * @return {OO.ui.Widget} The widget, for chaining
*/
OO.ui.SelectFileWidget.prototype.blur = function () {
this.selectButton.blur();
* Handle clear button click events.
*
* @private
+ * @return {undefined/boolean} False to prevent default if event is handled
*/
OO.ui.SelectFileWidget.prototype.onClearClick = function () {
this.setValue( null );
*
* @private
* @param {jQuery.Event} e Key press event
+ * @return {undefined/boolean} False to prevent default if event is handled
*/
OO.ui.SelectFileWidget.prototype.onKeyPress = function ( e ) {
if ( this.isSupported && !this.isDisabled() && this.$input &&
*
* @private
* @param {jQuery.Event} e Key press event
+ * @return {undefined/boolean} False to prevent default if event is handled
*/
OO.ui.SelectFileWidget.prototype.onDropTargetClick = function () {
if ( this.isSupported && !this.isDisabled() && this.$input ) {
*
* @private
* @param {jQuery.Event} e Drag event
+ * @return {undefined/boolean} False to prevent default if event is handled
*/
OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
var itemOrFile,
*
* @private
* @param {jQuery.Event} e Drop event
+ * @return {undefined/boolean} False to prevent default if event is handled
*/
OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
var file = null,