/*!
- * OOjs UI v0.20.2
+ * OOjs UI v0.21.4
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-03-30T20:34:37Z
+ * Date: 2017-05-16T22:31:39Z
*/
( function ( OO ) {
if ( this.draggable !== isDraggable ) {
this.draggable = isDraggable;
- this.$element.toggleClass( 'oo-ui-draggableElement-undraggable', !this.draggable );
+ this.$handle.toggleClass( 'oo-ui-draggableElement-undraggable', !this.draggable );
}
};
OO.ui.mixin.RequestManager.prototype.getRequestCacheDataFromResponse = null;
/**
- * LookupElement is a mixin that creates a {@link OO.ui.FloatingMenuSelectWidget menu} of suggested values for
+ * LookupElement is a mixin that creates a {@link OO.ui.MenuSelectWidget menu} of suggested values for
* a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
* into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
* from the lookup menu, that value becomes the value of the input field.
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
+ * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning.
+ * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
* @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
* @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
* By default, the lookup menu is not generated and displayed until the user begins to type.
// Properties
this.$overlay = config.$overlay || this.$element;
- this.lookupMenu = new OO.ui.FloatingMenuSelectWidget( {
+ this.lookupMenu = new OO.ui.MenuSelectWidget( {
widget: this,
input: this,
- $container: config.$container || this.$element
+ $floatableContainer: config.$container || this.$element
} );
this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
} );
// Initialization
+ this.$input.attr( {
+ role: 'combobox',
+ 'aria-owns': this.lookupMenu.getElementId(),
+ 'aria-autocomplete': 'list'
+ } );
this.$element.addClass( 'oo-ui-lookupElement' );
this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
this.$overlay.append( this.lookupMenu.$element );
* Get lookup menu.
*
* @private
- * @return {OO.ui.FloatingMenuSelectWidget}
+ * @return {OO.ui.MenuSelectWidget}
*/
OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
return this.lookupMenu;
};
/**
- * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
- * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
- * rather extended to include the required content and functionality.
+ * TabPanelLayouts are used within {@link OO.ui.IndexLayout index layouts} to create tab panels that
+ * users can select and display from the index's optional {@link OO.ui.TabSelectWidget tab}
+ * navigation. TabPanels are usually not instantiated directly, rather extended to include the
+ * required content and functionality.
*
- * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
- * item is customized (with a label) using the #setupTabItem method. See
+ * Each tab panel must have a unique symbolic name, which is passed to the constructor. In addition,
+ * the tab panel's tab item is customized (with a label) using the #setupTabItem method. See
* {@link OO.ui.IndexLayout IndexLayout} for an example.
*
* @class
* @extends OO.ui.PanelLayout
*
* @constructor
- * @param {string} name Unique symbolic name of card
+ * @param {string} name Unique symbolic name of tab panel
* @param {Object} [config] Configuration options
- * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for card's tab
+ * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for tab panel's tab
*/
-OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
+OO.ui.TabPanelLayout = function OoUiTabPanelLayout( name, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( name ) && config === undefined ) {
config = name;
config = $.extend( { scrollable: true }, config );
// Parent constructor
- OO.ui.CardLayout.parent.call( this, config );
+ OO.ui.TabPanelLayout.parent.call( this, config );
// Properties
this.name = name;
this.active = false;
// Initialization
- this.$element.addClass( 'oo-ui-cardLayout' );
+ this.$element.addClass( 'oo-ui-tabPanelLayout' );
};
/* Setup */
-OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
+OO.inheritClass( OO.ui.TabPanelLayout, OO.ui.PanelLayout );
/* Events */
/**
- * An 'active' event is emitted when the card becomes active. Cards become active when they are
- * shown in a index layout that is configured to display only one card at a time.
+ * An 'active' event is emitted when the tab panel becomes active. Tab panels become active when they are
+ * shown in a index layout that is configured to display only one tab panel at a time.
*
* @event active
- * @param {boolean} active Card is active
+ * @param {boolean} active Tab panel is active
*/
/* Methods */
/**
- * Get the symbolic name of the card.
+ * Get the symbolic name of the tab panel.
*
- * @return {string} Symbolic name of card
+ * @return {string} Symbolic name of tab panel
*/
-OO.ui.CardLayout.prototype.getName = function () {
+OO.ui.TabPanelLayout.prototype.getName = function () {
return this.name;
};
/**
- * Check if card is active.
+ * Check if tab panel is active.
*
- * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
- * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
+ * Tab panels become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to
+ * display only one tab panel at a time. Additional CSS is applied to the tab panel's tab item to reflect the
+ * active state.
*
- * @return {boolean} Card is active
+ * @return {boolean} Tab panel is active
*/
-OO.ui.CardLayout.prototype.isActive = function () {
+OO.ui.TabPanelLayout.prototype.isActive = function () {
return this.active;
};
/**
* Get tab item.
*
- * The tab item allows users to access the card from the index's tab
+ * The tab item allows users to access the tab panel from the index's tab
* navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
*
* @return {OO.ui.TabOptionWidget|null} Tab option widget
*/
-OO.ui.CardLayout.prototype.getTabItem = function () {
+OO.ui.TabPanelLayout.prototype.getTabItem = function () {
return this.tabItem;
};
* @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
* @chainable
*/
-OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
+OO.ui.TabPanelLayout.prototype.setTabItem = function ( tabItem ) {
this.tabItem = tabItem || null;
if ( tabItem ) {
this.setupTabItem();
* @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
* @chainable
*/
-OO.ui.CardLayout.prototype.setupTabItem = function () {
+OO.ui.TabPanelLayout.prototype.setupTabItem = function () {
if ( this.label ) {
this.tabItem.setLabel( this.label );
}
};
/**
- * Set the card to its 'active' state.
+ * Set the tab panel to its 'active' state.
*
- * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
- * CSS is applied to the tab item to reflect the card's active state. Outside of the index
- * context, setting the active state on a card does nothing.
+ * Tab panels become active when they are shown in a index layout that is configured to display only
+ * one tab panel at a time. Additional CSS is applied to the tab item to reflect the tab panel's
+ * active state. Outside of the index context, setting the active state on a tab panel does nothing.
*
- * @param {boolean} active Card is active
+ * @param {boolean} active Tab panel is active
* @fires active
*/
-OO.ui.CardLayout.prototype.setActive = function ( active ) {
+OO.ui.TabPanelLayout.prototype.setActive = function ( active ) {
active = !!active;
if ( active !== this.active ) {
this.active = active;
- this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
+ this.$element.toggleClass( 'oo-ui-tabPanelLayout-active', this.active );
this.emit( 'active', this.active );
}
};
+/**
+ * The deprecated name for the TabPanelLayout, provided for backwards-compatibility.
+ *
+ * @class
+ * @extends OO.ui.TabPanelLayout
+ *
+ * @constructor
+ * @deprecated since v0.21.3
+ */
+OO.ui.CardLayout = function OoUiCardLayout() {
+ OO.ui.warnDeprecation( 'CardLayout has been renamed to TabPanel layout. Use that instead. See T155152' );
+ // Parent constructor
+ OO.ui.CardLayout.parent.apply( this, arguments );
+};
+
+OO.inheritClass( OO.ui.CardLayout, OO.ui.TabPanelLayout );
+
/**
* PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
* from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
};
/**
- * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
- * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
- * select which one to display. By default, only one card is displayed at a time. When a user
- * navigates to a new card, the index layout automatically focuses on the first focusable element,
+ * IndexLayouts contain {@link OO.ui.TabPanelLayout tab panel layouts} as well as
+ * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the tab panels and
+ * select which one to display. By default, only one tab panel is displayed at a time. When a user
+ * navigates to a new tab panel, the index layout automatically focuses on the first focusable element,
* unless the default setting is changed.
*
* TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
*
* @example
- * // Example of a IndexLayout that contains two CardLayouts.
+ * // Example of a IndexLayout that contains two TabPanelLayouts.
*
- * function CardOneLayout( name, config ) {
- * CardOneLayout.parent.call( this, name, config );
- * this.$element.append( '<p>First card</p>' );
+ * function TabPanelOneLayout( name, config ) {
+ * TabPanelOneLayout.parent.call( this, name, config );
+ * this.$element.append( '<p>First tab panel</p>' );
* }
- * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
- * CardOneLayout.prototype.setupTabItem = function () {
- * this.tabItem.setLabel( 'Card one' );
+ * OO.inheritClass( TabPanelOneLayout, OO.ui.TabPanelLayout );
+ * TabPanelOneLayout.prototype.setupTabItem = function () {
+ * this.tabItem.setLabel( 'Tab panel one' );
* };
*
- * var card1 = new CardOneLayout( 'one' ),
- * card2 = new OO.ui.CardLayout( 'two', { label: 'Card two' } );
+ * var tabPanel1 = new TabPanelOneLayout( 'one' ),
+ * tabPanel2 = new OO.ui.TabPanelLayout( 'two', { label: 'Tab panel two' } );
*
- * card2.$element.append( '<p>Second card</p>' );
+ * tabPanel2.$element.append( '<p>Second tab panel</p>' );
*
* var index = new OO.ui.IndexLayout();
*
- * index.addCards ( [ card1, card2 ] );
+ * index.addTabPanels ( [ tabPanel1, tabPanel2 ] );
* $( 'body' ).append( index.$element );
*
* @class
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {boolean} [continuous=false] Show all cards, one after another
+ * @cfg {boolean} [continuous=false] Show all tab panels, one after another
* @cfg {boolean} [expanded=true] Expand the content panel to fill the entire parent element.
- * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed. Disabled on mobile.
+ * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new tab panel is displayed. Disabled on mobile.
*/
OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
// Configuration initialization
OO.ui.IndexLayout.parent.call( this, config );
// Properties
- this.currentCardName = null;
- this.cards = {};
+ this.currentTabPanelName = null;
+ this.tabPanels = {};
+
+ Object.defineProperty( this, 'currentCardName', {
+ // TODO: read documentation
+ configurable: true,
+ enumerable: true,
+ get: function () {
+ OO.ui.warnDeprecation( 'IndexLayout\'s currentCardName property is deprecated. Use currentTabPanelName instead. See T155152' );
+ return this.currentTabPanelName;
+ },
+ set: function ( value ) {
+ OO.ui.warnDeprecation( 'IndexLayout\'s currentCardName property is deprecated. Use currentTabPanelName instead. See T155152' );
+ this.currentTabPanelName = value;
+ }
+ } );
+
+ Object.defineProperty( this, 'cards', {
+ // TODO: read documentation
+ configurable: true,
+ enumerable: true,
+ get: function () {
+ OO.ui.warnDeprecation( 'IndexLayout\'s cards property is deprecated. Use tabPanels instead. See T155152' );
+ return this.tabPanels;
+ },
+ set: function ( value ) {
+ OO.ui.warnDeprecation( 'IndexLayout\'s cards property is deprecated. Use tabPanels instead. See T155152' );
+ this.tabPanels = value;
+ }
+ } );
+
this.ignoreFocus = false;
this.stackLayout = new OO.ui.StackLayout( {
continuous: !!config.continuous,
/* Events */
/**
- * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
+ * A 'set' event is emitted when a tab panel is {@link #setTabPanel set} to be displayed by the index layout.
* @event set
- * @param {OO.ui.CardLayout} card Current card
+ * @param {OO.ui.TabPanelLayout} tabPanel Current tab panel
*/
/**
- * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
+ * An 'add' event is emitted when tab panels are {@link #addTabPanels added} to the index layout.
*
* @event add
- * @param {OO.ui.CardLayout[]} card Added cards
- * @param {number} index Index cards were added at
+ * @param {OO.ui.TabPanelLayout[]} tabPanel Added tab panels
+ * @param {number} index Index tab panels were added at
*/
/**
- * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
- * {@link #removeCards removed} from the index.
+ * A 'remove' event is emitted when tab panels are {@link #clearTabPanels cleared} or
+ * {@link #removeTabPanels removed} from the index.
*
* @event remove
- * @param {OO.ui.CardLayout[]} cards Removed cards
+ * @param {OO.ui.TabPanelLayout[]} tabPanel Removed tab panels
*/
/* Methods */
* Handle stack layout focus.
*
* @private
- * @param {jQuery.Event} e Focusin event
+ * @param {jQuery.Event} e Focusing event
*/
OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
var name, $target;
- // Find the card that an element was focused within
- $target = $( e.target ).closest( '.oo-ui-cardLayout' );
- for ( name in this.cards ) {
- // Check for card match, exclude current card to find only card changes
- if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
- this.setCard( name );
+ // Find the tab panel that an element was focused within
+ $target = $( e.target ).closest( '.oo-ui-tabPanelLayout' );
+ for ( name in this.tabPanels ) {
+ // Check for tab panel match, exclude current tab panel to find only tab panel changes
+ if ( this.tabPanels[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentTabPanelName ) {
+ this.setTabPanel( name );
break;
}
}
* Handle stack layout set events.
*
* @private
- * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
+ * @param {OO.ui.PanelLayout|null} tabPanel The tab panel that is now the current panel
*/
-OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
+OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( tabPanel ) {
var layout = this;
- if ( card ) {
- card.scrollElementIntoView().done( function () {
+ if ( tabPanel ) {
+ tabPanel.scrollElementIntoView().done( function () {
if ( layout.autoFocus && !OO.ui.isMobile() ) {
layout.focus();
}
};
/**
- * Focus the first input in the current card.
+ * Focus the first input in the current tab panel.
*
- * If no card is selected, the first selectable card will be selected.
- * If the focus is already in an element on the current card, nothing will happen.
+ * If no tab panel is selected, the first selectable tab panel will be selected.
+ * If the focus is already in an element on the current tab panel, nothing will happen.
*
* @param {number} [itemIndex] A specific item to focus on
*/
OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
- var card,
+ var tabPanel,
items = this.stackLayout.getItems();
if ( itemIndex !== undefined && items[ itemIndex ] ) {
- card = items[ itemIndex ];
+ tabPanel = items[ itemIndex ];
} else {
- card = this.stackLayout.getCurrentItem();
+ tabPanel = this.stackLayout.getCurrentItem();
}
- if ( !card ) {
- this.selectFirstSelectableCard();
- card = this.stackLayout.getCurrentItem();
+ if ( !tabPanel ) {
+ this.selectFirstSelectableTabPanel();
+ tabPanel = this.stackLayout.getCurrentItem();
}
- if ( !card ) {
+ if ( !tabPanel ) {
return;
}
// Only change the focus if is not already in the current page
- if ( !OO.ui.contains( card.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
- card.focus();
+ if ( !OO.ui.contains( tabPanel.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
+ tabPanel.focus();
}
};
*/
OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
if ( item ) {
- this.setCard( item.getData() );
+ this.setTabPanel( item.getData() );
}
};
/**
- * Get the card closest to the specified card.
+ * Get the tab panel closest to the specified tab panel.
*
- * @param {OO.ui.CardLayout} card Card to use as a reference point
- * @return {OO.ui.CardLayout|null} Card closest to the specified card
+ * @param {OO.ui.TabPanelLayout} tabPanel Tab panel to use as a reference point
+ * @return {OO.ui.TabPanelLayout|null} Tab panel closest to the specified
*/
-OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
+OO.ui.IndexLayout.prototype.getClosestTabPanel = function ( tabPanel ) {
var next, prev, level,
- cards = this.stackLayout.getItems(),
- index = cards.indexOf( card );
+ tabPanels = this.stackLayout.getItems(),
+ index = tabPanels.indexOf( tabPanel );
if ( index !== -1 ) {
- next = cards[ index + 1 ];
- prev = cards[ index - 1 ];
- // Prefer adjacent cards at the same level
- level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
+ next = tabPanels[ index + 1 ];
+ prev = tabPanels[ index - 1 ];
+ // Prefer adjacent tab panels at the same level
+ level = this.tabSelectWidget.getItemFromData( tabPanel.getName() ).getLevel();
if (
prev &&
level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
return prev || next || null;
};
+/**
+ * Get the tab panel closest to the specified tab panel.
+ *
+ * @param {OO.ui.TabPanelLayout} tabPanel Tab panel to use as a reference point
+ * @return {OO.ui.TabPanelLayout|null} Tab panel closest to the specified
+ * @deprecated since v0.21.3, use `getClosestTabPanel` instead
+ */
+OO.ui.IndexLayout.prototype.getClosestCard = function ( tabPanel ) {
+ OO.ui.warnDeprecation( 'IndexLayout\'s getClosestCard method is deprecated. Use getClosestTabPanel instead. See T155152' );
+ return this.getClosestTabPanel( tabPanel );
+};
+
/**
* Get the tabs widget.
*
};
/**
- * Get a card by its symbolic name.
+ * Get a tab panel by its symbolic name.
+ *
+ * @param {string} name Symbolic name of tab panel
+ * @return {OO.ui.TabPanelLayout|undefined} Tab panel, if found
+ */
+OO.ui.IndexLayout.prototype.getTabPanel = function ( name ) {
+ return this.tabPanels[ name ];
+};
+
+/**
+ * Get a tab panel by its symbolic name.
*
- * @param {string} name Symbolic name of card
- * @return {OO.ui.CardLayout|undefined} Card, if found
+ * @param {string} name Symbolic name of tab panel
+ * @return {OO.ui.TabPanelLayout|undefined} Tab panel, if found
+ * @deprecated since v0.21.3, use `getTabPanel` instead
*/
OO.ui.IndexLayout.prototype.getCard = function ( name ) {
- return this.cards[ name ];
+ OO.ui.warnDeprecation( 'IndexLayout\'s getCard method is deprecated. Use getTabPanel instead. See T155152' );
+ return this.getTabPanel( name );
};
/**
- * Get the current card.
+ * Get the current tab panel.
*
- * @return {OO.ui.CardLayout|undefined} Current card, if found
+ * @return {OO.ui.TabPanelLayout|undefined} Current tab panel, if found
+ */
+OO.ui.IndexLayout.prototype.getCurrentTabPanel = function () {
+ var name = this.getCurrentTabPanelName();
+ return name ? this.getTabPanel( name ) : undefined;
+};
+
+/**
+ * Get the current tab panel.
+ *
+ * @return {OO.ui.TabPanelLayout|undefined} Current tab panel, if found
+ * @deprecated since v0.21.3, use `getCurrentTabPanel` instead
*/
OO.ui.IndexLayout.prototype.getCurrentCard = function () {
- var name = this.getCurrentCardName();
- return name ? this.getCard( name ) : undefined;
+ OO.ui.warnDeprecation( 'IndexLayout\'s getCurrentCard method is deprecated. Use getCurrentTabPanel instead. See T155152' );
+ return this.getCurrentTabPanel();
+};
+
+/**
+ * Get the symbolic name of the current tab panel.
+ *
+ * @return {string|null} Symbolic name of the current tab panel
+ */
+OO.ui.IndexLayout.prototype.getCurrentTabPanelName = function () {
+ return this.currentTabPanelName;
};
/**
- * Get the symbolic name of the current card.
+ * Get the symbolic name of the current tab panel.
*
- * @return {string|null} Symbolic name of the current card
+ * @return {string|null} Symbolic name of the current tab panel
+ * @deprecated since v0.21.3, use `getCurrentTabPanelName` instead
*/
OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
- return this.currentCardName;
+ OO.ui.warnDeprecation( 'IndexLayout\'s getCurrentCardName method is deprecated. Use getCurrentTabPanelName instead. See T155152' );
+ return this.getCurrentTabPanelName();
};
/**
- * Add cards to the index layout
+ * Add tab panels to the index layout
*
- * When cards are added with the same names as existing cards, the existing cards will be
- * automatically removed before the new cards are added.
+ * When tab panels are added with the same names as existing tab panels, the existing tab panels
+ * will be automatically removed before the new tab panels are added.
*
- * @param {OO.ui.CardLayout[]} cards Cards to add
+ * @param {OO.ui.TabPanelLayout[]} tabPanels Tab panels to add
* @param {number} index Index of the insertion point
* @fires add
* @chainable
*/
-OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
- var i, len, name, card, item, currentIndex,
- stackLayoutCards = this.stackLayout.getItems(),
+OO.ui.IndexLayout.prototype.addTabPanels = function ( tabPanels, index ) {
+ var i, len, name, tabPanel, item, currentIndex,
+ stackLayoutTabPanels = this.stackLayout.getItems(),
remove = [],
items = [];
- // Remove cards with same names
- for ( i = 0, len = cards.length; i < len; i++ ) {
- card = cards[ i ];
- name = card.getName();
+ // Remove tab panels with same names
+ for ( i = 0, len = tabPanels.length; i < len; i++ ) {
+ tabPanel = tabPanels[ i ];
+ name = tabPanel.getName();
- if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
+ if ( Object.prototype.hasOwnProperty.call( this.tabPanels, name ) ) {
// Correct the insertion index
- currentIndex = stackLayoutCards.indexOf( this.cards[ name ] );
+ currentIndex = stackLayoutTabPanels.indexOf( this.tabPanels[ name ] );
if ( currentIndex !== -1 && currentIndex + 1 < index ) {
index--;
}
- remove.push( this.cards[ name ] );
+ remove.push( this.tabPanels[ name ] );
}
}
if ( remove.length ) {
- this.removeCards( remove );
+ this.removeTabPanels( remove );
}
- // Add new cards
- for ( i = 0, len = cards.length; i < len; i++ ) {
- card = cards[ i ];
- name = card.getName();
- this.cards[ card.getName() ] = card;
+ // Add new tab panels
+ for ( i = 0, len = tabPanels.length; i < len; i++ ) {
+ tabPanel = tabPanels[ i ];
+ name = tabPanel.getName();
+ this.tabPanels[ tabPanel.getName() ] = tabPanel;
item = new OO.ui.TabOptionWidget( { data: name } );
- card.setTabItem( item );
+ tabPanel.setTabItem( item );
items.push( item );
}
if ( items.length ) {
this.tabSelectWidget.addItems( items, index );
- this.selectFirstSelectableCard();
+ this.selectFirstSelectableTabPanel();
}
- this.stackLayout.addItems( cards, index );
- this.emit( 'add', cards, index );
+ this.stackLayout.addItems( tabPanels, index );
+ this.emit( 'add', tabPanels, index );
return this;
};
/**
- * Remove the specified cards from the index layout.
+ * Add tab panels to the index layout
+ *
+ * When tab panels are added with the same names as existing tab panels, the existing tab panels
+ * will be automatically removed before the new tab panels are added.
+ *
+ * @param {OO.ui.TabPanelLayout[]} tabPanels Tab panels to add
+ * @param {number} index Index of the insertion point
+ * @fires add
+ * @chainable
+ * @deprecated since v0.21.3, use `addTabPanels` instead
+ */
+OO.ui.IndexLayout.prototype.addCards = function ( tabPanels, index ) {
+ OO.ui.warnDeprecation( 'IndexLayout\'s addCards method is deprecated. Use addTabPanels instead. See T155152' );
+ return this.addTabPanels( tabPanels, index );
+};
+
+/**
+ * Remove the specified tab panels from the index layout.
*
- * To remove all cards from the index, you may wish to use the #clearCards method instead.
+ * To remove all tab panels from the index, you may wish to use the #clearTabPanels method instead.
*
- * @param {OO.ui.CardLayout[]} cards An array of cards to remove
+ * @param {OO.ui.TabPanelLayout[]} tabPanels An array of tab panels to remove
* @fires remove
* @chainable
*/
-OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
- var i, len, name, card,
+OO.ui.IndexLayout.prototype.removeTabPanels = function ( tabPanels ) {
+ var i, len, name, tabPanel,
items = [];
- for ( i = 0, len = cards.length; i < len; i++ ) {
- card = cards[ i ];
- name = card.getName();
- delete this.cards[ name ];
+ for ( i = 0, len = tabPanels.length; i < len; i++ ) {
+ tabPanel = tabPanels[ i ];
+ name = tabPanel.getName();
+ delete this.tabPanels[ name ];
items.push( this.tabSelectWidget.getItemFromData( name ) );
- card.setTabItem( null );
+ tabPanel.setTabItem( null );
}
if ( items.length ) {
this.tabSelectWidget.removeItems( items );
- this.selectFirstSelectableCard();
+ this.selectFirstSelectableTabPanel();
}
- this.stackLayout.removeItems( cards );
- this.emit( 'remove', cards );
+ this.stackLayout.removeItems( tabPanels );
+ this.emit( 'remove', tabPanels );
return this;
};
/**
- * Clear all cards from the index layout.
+ * Remove the specified tab panels from the index layout.
*
- * To remove only a subset of cards from the index, use the #removeCards method.
+ * To remove all tab panels from the index, you may wish to use the #clearTabPanels method instead.
*
+ * @param {OO.ui.TabPanelLayout[]} tabPanels An array of tab panels to remove
* @fires remove
* @chainable
+ * @deprecated since v0.21.3, use `removeTabPanels` instead
*/
-OO.ui.IndexLayout.prototype.clearCards = function () {
+OO.ui.IndexLayout.prototype.removeCards = function ( tabPanels ) {
+ OO.ui.warnDeprecation( 'IndexLayout\'s removeCards method is deprecated. Use removeTabPanels instead. See T155152.' );
+ return this.removeTabPanels( tabPanels );
+};
+
+/**
+ * Clear all tab panels from the index layout.
+ *
+ * To remove only a subset of tab panels from the index, use the #removeTabPanels method.
+ *
+ * @fires remove
+ * @chainable
+ */
+OO.ui.IndexLayout.prototype.clearTabPanels = function () {
var i, len,
- cards = this.stackLayout.getItems();
+ tabPanels = this.stackLayout.getItems();
- this.cards = {};
- this.currentCardName = null;
+ this.tabPanels = {};
+ this.currentTabPanelName = null;
this.tabSelectWidget.clearItems();
- for ( i = 0, len = cards.length; i < len; i++ ) {
- cards[ i ].setTabItem( null );
+ for ( i = 0, len = tabPanels.length; i < len; i++ ) {
+ tabPanels[ i ].setTabItem( null );
}
this.stackLayout.clearItems();
- this.emit( 'remove', cards );
+ this.emit( 'remove', tabPanels );
return this;
};
/**
- * Set the current card by symbolic name.
+ * Clear all tab panels from the index layout.
+ *
+ * To remove only a subset of tab panels from the index, use the #removeTabPanels method.
+ *
+ * @fires remove
+ * @chainable
+ * @deprecated since v0.21.3, use `clearTabPanels` instead
+ */
+OO.ui.IndexLayout.prototype.clearCards = function () {
+ OO.ui.warnDeprecation( 'IndexLayout\'s clearCards method is deprecated. Use clearTabPanels instead. See T155152.' );
+ return this.clearTabPanels();
+};
+
+/**
+ * Set the current tab panel by symbolic name.
*
* @fires set
- * @param {string} name Symbolic name of card
+ * @param {string} name Symbolic name of tab panel
*/
-OO.ui.IndexLayout.prototype.setCard = function ( name ) {
+OO.ui.IndexLayout.prototype.setTabPanel = function ( name ) {
var selectedItem,
$focused,
- card = this.cards[ name ],
- previousCard = this.currentCardName && this.cards[ this.currentCardName ];
+ tabPanel = this.tabPanels[ name ],
+ previousTabPanel = this.currentTabPanelName && this.tabPanels[ this.currentTabPanelName ];
- if ( name !== this.currentCardName ) {
+ if ( name !== this.currentTabPanelName ) {
selectedItem = this.tabSelectWidget.getSelectedItem();
if ( selectedItem && selectedItem.getData() !== name ) {
this.tabSelectWidget.selectItemByData( name );
}
- if ( card ) {
- if ( previousCard ) {
- previousCard.setActive( false );
- // Blur anything focused if the next card doesn't have anything focusable.
- // This is not needed if the next card has something focusable (because once it is focused
+ if ( tabPanel ) {
+ if ( previousTabPanel ) {
+ previousTabPanel.setActive( false );
+ // Blur anything focused if the next tab panel doesn't have anything focusable.
+ // This is not needed if the next tab panel has something focusable (because once it is focused
// this blur happens automatically). If the layout is non-continuous, this check is
- // meaningless because the next card is not visible yet and thus can't hold focus.
+ // meaningless because the next tab panel is not visible yet and thus can't hold focus.
if (
this.autoFocus &&
!OO.ui.isMobile() &&
this.stackLayout.continuous &&
- OO.ui.findFocusable( card.$element ).length !== 0
+ OO.ui.findFocusable( tabPanel.$element ).length !== 0
) {
- $focused = previousCard.$element.find( ':focus' );
+ $focused = previousTabPanel.$element.find( ':focus' );
if ( $focused.length ) {
$focused[ 0 ].blur();
}
}
}
- this.currentCardName = name;
- card.setActive( true );
- this.stackLayout.setItem( card );
- if ( !this.stackLayout.continuous && previousCard ) {
- // This should not be necessary, since any inputs on the previous card should have been
+ this.currentTabPanelName = name;
+ tabPanel.setActive( true );
+ this.stackLayout.setItem( tabPanel );
+ if ( !this.stackLayout.continuous && previousTabPanel ) {
+ // This should not be necessary, since any inputs on the previous tab panel should have been
// blurred when it was hidden, but browsers are not very consistent about this.
- $focused = previousCard.$element.find( ':focus' );
+ $focused = previousTabPanel.$element.find( ':focus' );
if ( $focused.length ) {
$focused[ 0 ].blur();
}
}
- this.emit( 'set', card );
+ this.emit( 'set', tabPanel );
}
}
};
/**
- * Select the first selectable card.
+ * Set the current tab panel by symbolic name.
+ *
+ * @fires set
+ * @param {string} name Symbolic name of tab panel
+ * @deprecated since v0.21.3, use `setTabPanel` instead
+ */
+OO.ui.IndexLayout.prototype.setCard = function ( name ) {
+ OO.ui.warnDeprecation( 'IndexLayout\'s setCard method is deprecated. Use setTabPanel instead. See T155152.' );
+ return this.setTabPanel( name );
+};
+
+/**
+ * Select the first selectable tab panel.
*
* @chainable
*/
-OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
+OO.ui.IndexLayout.prototype.selectFirstSelectableTabPanel = function () {
if ( !this.tabSelectWidget.getSelectedItem() ) {
this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
}
return this;
};
+/**
+ * Select the first selectable tab panel.
+ *
+ * @chainable
+ * @deprecated since v0.21.3, use `selectFirstSelectableTabPanel` instead
+ */
+OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
+ OO.ui.warnDeprecation( 'IndexLayout\'s selectFirstSelectableCard method is deprecated. Use selectFirestSelectableTabPanel instead. See T155152.' );
+ return this.selectFirstSelectableTabPanel();
+};
+
/**
* ToggleWidget implements basic behavior of widgets with an on/off state.
* Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
};
/**
- * Set the state of the toggle: `true` for 'on', `false' for 'off'.
+ * Set the state of the toggle: `true` for 'on', `false` for 'off'.
*
* @param {boolean} value The state of the toggle
* @fires change
this.emit( 'change', value );
this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
- this.$element.attr( 'aria-checked', value.toString() );
}
return this;
};
}
};
+/**
+ * @inheritdoc
+ */
+OO.ui.ToggleSwitchWidget.prototype.setValue = function ( value ) {
+ OO.ui.ToggleSwitchWidget.parent.prototype.setValue.call( this, value );
+ this.$element.attr( 'aria-checked', this.value.toString() );
+ return this;
+};
+
/**
* OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
* Controls include moving items up and down, removing items, and adding different kinds of items.
* TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
*
* Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
- * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
+ * {@link OO.ui.TabPanelLayout tab panel layouts}. See {@link OO.ui.IndexLayout IndexLayout}
* for an example.
*
* @class
}
};
-/**
- * Focuses the capsule
- */
-OO.ui.CapsuleItemWidget.prototype.focus = function () {
- this.$element.focus();
-};
-
/**
* CapsuleMultiselectWidgets are something like a {@link OO.ui.ComboBoxInputWidget combo box widget}
* that allows for selecting multiple values.
* @mixins OO.ui.mixin.IndicatorElement
* @mixins OO.ui.mixin.IconElement
* @uses OO.ui.CapsuleItemWidget
- * @uses OO.ui.FloatingMenuSelectWidget
+ * @uses OO.ui.MenuSelectWidget
*
* @constructor
* @param {Object} [config] Configuration options
* 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/OOjs_UI/Concepts#Overlays>.
*/
OO.ui.CapsuleMultiselectWidget = function OoUiCapsuleMultiselectWidget( config ) {
var $tabFocus;
this.allowArbitrary = config.allowArbitrary;
this.allowDuplicates = config.allowDuplicates;
this.$overlay = config.$overlay;
- this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
+ this.menu = new OO.ui.MenuSelectWidget( $.extend(
{
widget: this,
$input: this.$input,
- $container: this.$element,
+ $floatableContainer: this.$element,
filterFromInput: true,
disabled: this.isDisabled()
},
this.$input.prop( 'disabled', this.isDisabled() );
this.$input.attr( {
role: 'combobox',
+ 'aria-owns': this.menu.getElementId(),
'aria-autocomplete': 'list'
} );
}
OO.mixinClass( OO.ui.CapsuleMultiselectWidget, OO.ui.mixin.IndicatorElement );
OO.mixinClass( OO.ui.CapsuleMultiselectWidget, OO.ui.mixin.IconElement );
-/* Static Properties */
-
-OO.ui.CapsuleMultiselectWidget.static.supportsSimpleLabel = true;
-
/* Events */
/**
};
/**
- * Get the widget's input's id, or generate one, if it has an input.
- *
- * @return {string}
+ * @inheritdoc
*/
OO.ui.CapsuleMultiselectWidget.prototype.getInputId = function () {
- var id;
if ( !this.$input ) {
- return false;
- }
-
- id = this.$input.attr( 'id' );
- if ( id === undefined ) {
- id = OO.ui.generateElementId();
- this.$input.attr( 'id', id );
+ return null;
}
-
- return id;
+ return OO.ui.mixin.TabIndexedElement.prototype.getInputId.call( this );
};
/**
*/
OO.ui.CapsuleMultiselectWidget.prototype.onInputFocus = function () {
if ( !this.isDisabled() ) {
+ this.updateInputSize();
this.menu.toggle( true );
}
};
* Focus the widget
*
* @chainable
- * @return {OO.ui.CapsuleMultiselectWidget}
*/
OO.ui.CapsuleMultiselectWidget.prototype.focus = function () {
if ( !this.isDisabled() ) {
this.popup.toggle( true );
OO.ui.findFocusable( this.popup.$element ).focus();
} else {
- this.updateInputSize();
- this.menu.toggle( true );
- this.$input.focus();
+ OO.ui.mixin.TabIndexedElement.prototype.focus.call( this );
}
}
return this;
};
/**
- * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
- * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
- * OO.ui.mixin.IndicatorElement indicators}.
- * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
- *
- * @example
- * // Example of a file select widget
- * var selectFile = new OO.ui.SelectFileWidget();
- * $( 'body' ).append( selectFile.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
+ * TagItemWidgets are used within a {@link OO.ui.TagMultiselectWidget
+ * TagMultiselectWidget} to display the selected items.
*
* @class
* @extends OO.ui.Widget
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.PendingElement
+ * @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 options
- * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
- * @cfg {string} [placeholder] Text to display when no file is selected.
- * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
- * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
- * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be true.
- * @cfg {number} [thumbnailSizeLimit=20] File size limit in MiB above which to not try and show a
- * preview (for performance)
+ * @param {Object} [config] Configuration object
+ * @cfg {boolean} [valid=true] Item is valid
*/
-OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
- var dragHandler;
-
- // Configuration initialization
- config = $.extend( {
- accept: null,
- placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
- notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
- droppable: true,
- showDropTarget: false,
- thumbnailSizeLimit: 20
- }, config );
+OO.ui.TagItemWidget = function OoUiTagItemWidget( config ) {
+ config = config || {};
// Parent constructor
- OO.ui.SelectFileWidget.parent.call( this, config );
+ OO.ui.TagItemWidget.parent.call( this, config );
// Mixin constructors
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.IndicatorElement.call( this, config );
- OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$info } ) );
+ 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 );
- // Properties
- this.$info = $( '<span>' );
- this.showDropTarget = config.showDropTarget;
- this.thumbnailSizeLimit = config.thumbnailSizeLimit;
- this.isSupported = this.constructor.static.isSupported();
- this.currentFile = null;
- if ( Array.isArray( config.accept ) ) {
- this.accept = config.accept;
- } else {
- this.accept = null;
- }
- this.placeholder = config.placeholder;
- this.notsupported = config.notsupported;
- this.onFileSelectedHandler = this.onFileSelected.bind( this );
-
- this.selectButton = new OO.ui.ButtonWidget( {
- classes: [ 'oo-ui-selectFileWidget-selectButton' ],
- label: OO.ui.msg( 'ooui-selectfile-button-select' ),
- disabled: this.disabled || !this.isSupported
- } );
+ this.valid = config.valid === undefined ? true : !!config.valid;
- this.clearButton = new OO.ui.ButtonWidget( {
- classes: [ 'oo-ui-selectFileWidget-clearButton' ],
+ this.closeButton = new OO.ui.ButtonWidget( {
framed: false,
- icon: 'close',
- disabled: this.disabled
+ indicator: 'clear',
+ tabIndex: -1
} );
+ this.closeButton.setDisabled( this.isDisabled() );
// Events
- this.selectButton.$button.on( {
- keypress: this.onKeyPress.bind( this )
- } );
- this.clearButton.connect( this, {
- click: 'onClearClick'
- } );
- if ( config.droppable ) {
- dragHandler = this.onDragEnterOrOver.bind( this );
- this.$element.on( {
- dragenter: dragHandler,
- dragover: dragHandler,
- dragleave: this.onDragLeave.bind( this ),
- drop: this.onDrop.bind( this )
- } );
- }
+ 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.addInput();
- this.$label.addClass( 'oo-ui-selectFileWidget-label' );
- this.$info
- .addClass( 'oo-ui-selectFileWidget-info' )
- .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
-
- if ( config.droppable && config.showDropTarget ) {
- this.selectButton.setIcon( 'upload' );
- this.$thumbnail = $( '<div>' ).addClass( 'oo-ui-selectFileWidget-thumbnail' );
- this.setPendingElement( this.$thumbnail );
- this.$element
- .addClass( 'oo-ui-selectFileWidget-dropTarget oo-ui-selectFileWidget' )
- .on( {
- click: this.onDropTargetClick.bind( this )
- } )
- .append(
- this.$thumbnail,
- this.$info,
- this.selectButton.$element,
- $( '<span>' )
- .addClass( 'oo-ui-selectFileWidget-dropLabel' )
- .text( OO.ui.msg( 'ooui-selectfile-dragdrop-placeholder' ) )
- );
- } else {
- this.$element
- .addClass( 'oo-ui-selectFileWidget' )
- .append( this.$info, this.selectButton.$element );
- }
- this.updateUI();
+ this.$element
+ .addClass( 'oo-ui-tagItemWidget' )
+ .append( this.$label, this.closeButton.$element );
};
-/* Setup */
+/* Initialization */
-OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
-OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
+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 );
-/* Static Properties */
+/* Events */
/**
- * Check if this widget is supported
+ * @event remove
*
- * @static
- * @return {boolean}
+ * A remove action was performed on the item
*/
-OO.ui.SelectFileWidget.static.isSupported = function () {
- var $input;
- if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
- $input = $( '<input>' ).attr( 'type', 'file' );
- OO.ui.SelectFileWidget.static.isSupportedCache = $input[ 0 ].files !== undefined;
- }
- return OO.ui.SelectFileWidget.static.isSupportedCache;
-};
-
-OO.ui.SelectFileWidget.static.isSupportedCache = null;
-
-/* Events */
/**
- * @event change
- *
- * A change event is emitted when the on/off state of the toggle changes.
+ * @event navigate
+ * @param {string} direction Direction of the movement, forward or backwards
*
- * @param {File|null} value New value
+ * 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
+ */
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.TagItemWidget.prototype.setDisabled = function ( state ) {
+ // Parent method
+ OO.ui.TagItemWidget.parent.prototype.setDisabled.call( this, state );
+
+ if ( this.closeButton ) {
+ this.closeButton.setDisabled( state );
+ }
+ return this;
+};
+
+/**
+ * Handle removal of the item
+ *
+ * This is mainly for extensibility concerns, so other children
+ * of this class can change the behavior if they need to. This
+ * is called by both clicking the 'remove' button but also
+ * on keypress, which is harder to override if needed.
+ *
+ * @fires remove
+ */
+OO.ui.TagItemWidget.prototype.remove = function () {
+ if ( !this.isDisabled() ) {
+ this.emit( 'remove' );
+ }
+};
+
+/**
+ * Handle a keydown event on the widget
+ *
+ * @fires navigate
+ * @fires remove
+ * @param {jQuery.Event} e Key down event
+ * @return {boolean|undefined} false to stop the operation
+ */
+OO.ui.TagItemWidget.prototype.onKeyDown = function ( e ) {
+ var movement;
+
+ if ( e.keyCode === OO.ui.Keys.BACKSPACE || e.keyCode === OO.ui.Keys.DELETE ) {
+ this.remove();
+ return false;
+ } else if ( e.keyCode === OO.ui.Keys.ENTER ) {
+ this.select();
+ return false;
+ } else if (
+ e.keyCode === OO.ui.Keys.LEFT ||
+ e.keyCode === OO.ui.Keys.RIGHT
+ ) {
+ if ( OO.ui.Element.static.getDir( this.$element ) === 'rtl' ) {
+ movement = {
+ left: 'forwards',
+ right: 'backwards'
+ };
+ } else {
+ movement = {
+ left: 'backwards',
+ right: 'forwards'
+ };
+ }
+
+ this.emit(
+ 'navigate',
+ e.keyCode === OO.ui.Keys.LEFT ?
+ movement.left : movement.right
+ );
+ }
+};
+
+/**
+ * Select this item
+ *
+ * @fires select
+ */
+OO.ui.TagItemWidget.prototype.select = function () {
+ if ( !this.isDisabled() ) {
+ this.emit( 'select' );
+ }
+};
+
+/**
+ * Set the valid state of this item
+ *
+ * @param {boolean} [valid] Item is valid
+ * @fires valid
+ */
+OO.ui.TagItemWidget.prototype.toggleValid = function ( valid ) {
+ valid = valid === undefined ? !this.valid : !!valid;
+
+ if ( this.valid !== valid ) {
+ this.valid = valid;
+
+ this.setFlags( { invalid: !this.valid } );
+
+ this.emit( 'valid', this.valid );
+ }
+};
+
+/**
+ * Check whether the item is valid
+ *
+ * @return {boolean} Item is valid
+ */
+OO.ui.TagItemWidget.prototype.isValid = function () {
+ return this.valid;
+};
+
+/**
+ * A basic tag multiselect widget, similar in concept to {@link OO.ui.ComboBoxInputWidget combo box widget}
+ * that allows the user to add multiple values that are displayed in a tag area.
+ *
+ * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * This widget is a base widget; see {@link OO.ui.MenuTagMultiselectWidget MenuTagMultiselectWidget} and
+ * {@link OO.ui.PopupTagMultiselectWidget PopupTagMultiselectWidget} for the implementations that use
+ * a menu and a popup respectively.
+ *
+ * @example
+ * // Example: A basic TagMultiselectWidget.
+ * var widget = new OO.ui.TagMultiselectWidget( {
+ * inputPosition: 'outline',
+ * allowedValues: [ 'Option 1', 'Option 2', 'Option 3' ],
+ * selected: [ 'Option 1' ]
+ * } );
+ * $( 'body' ).append( widget.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.GroupWidget
+ * @mixins OO.ui.mixin.DraggableGroupElement
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.TabIndexedElement
+ * @mixins OO.ui.mixin.FlaggedElement
+ *
+ * @constructor
+ * @param {Object} config Configuration object
+ * @cfg {Object} [input] Configuration options for the input widget
+ * @cfg {OO.ui.InputWidget} [inputWidget] An optional input widget. If given, it will
+ * 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
+ * @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.
+ * @cfg {Object[]} [allowedValues] An array representing the allowed items
+ * by their datas.
+ * @cfg {boolean} [allowDuplicates=false] Allow duplicate items to be added
+ * @cfg {boolean} [allowDisplayInvalidTags=false] Allow the display of
+ * 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 {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
+ * pass the validity tests.
+ */
+OO.ui.TagMultiselectWidget = function OoUiTagMultiselectWidget( config ) {
+ var inputEvents,
+ rAF = window.requestAnimationFrame || setTimeout,
+ widget = this,
+ $tabFocus = $( '<span>' )
+ .addClass( 'oo-ui-tagMultiselectWidget-focusTrap' );
+
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.TagMultiselectWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.GroupWidget.call( this, config );
+ OO.ui.mixin.IndicatorElement.call( this, config );
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.TabIndexedElement.call( this, config );
+ OO.ui.mixin.FlaggedElement.call( this, config );
+ OO.ui.mixin.DraggableGroupElement.call( this, config );
+
+ this.toggleDraggable(
+ config.allowReordering === undefined ?
+ true : !!config.allowReordering
+ );
+
+ this.inputPosition = this.constructor.static.allowedInputPositions.indexOf( config.inputPosition ) > -1 ?
+ config.inputPosition : 'inline';
+ this.allowEditTags = config.allowEditTags === undefined ? true : !!config.allowEditTags;
+ this.allowArbitrary = !!config.allowArbitrary;
+ this.allowDuplicates = !!config.allowDuplicates;
+ this.allowedValues = config.allowedValues || [];
+ this.allowDisplayInvalidTags = config.allowDisplayInvalidTags;
+ this.hasInput = this.inputPosition !== 'none';
+ this.height = null;
+ this.valid = true;
+
+ this.$content = $( '<div>' )
+ .addClass( 'oo-ui-tagMultiselectWidget-content' );
+ this.$handle = $( '<div>' )
+ .addClass( 'oo-ui-tagMultiselectWidget-handle' )
+ .append(
+ this.$indicator,
+ this.$icon,
+ this.$content
+ .append(
+ this.$group
+ .addClass( 'oo-ui-tagMultiselectWidget-group' )
+ )
+ );
+
+ // Events
+ this.aggregate( {
+ remove: 'itemRemove',
+ navigate: 'itemNavigate',
+ select: 'itemSelect'
+ } );
+ this.connect( this, {
+ itemRemove: 'onTagRemove',
+ itemSelect: 'onTagSelect',
+ itemNavigate: 'onTagNavigate',
+ change: 'onChangeTags'
+ } );
+ this.$handle.on( {
+ mousedown: this.onMouseDown.bind( this )
+ } );
+
+ // Initialize
+ this.$element
+ .addClass( 'oo-ui-tagMultiselectWidget' )
+ .append( this.$handle );
+
+ if ( this.hasInput ) {
+ if ( config.inputWidget ) {
+ this.input = config.inputWidget;
+ } else {
+ this.input = new OO.ui.TextInputWidget( $.extend( {
+ placeholder: config.placeholder,
+ classes: [ 'oo-ui-tagMultiselectWidget-input' ]
+ }, config.input ) );
+ }
+ this.input.setDisabled( this.isDisabled() );
+
+ inputEvents = {
+ 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.onInputKeyDown.bind( this ),
+ keypress: this.onInputKeyPress.bind( this )
+ };
+
+ this.input.$input.on( inputEvents );
+
+ 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
+ this.input.$element.css( 'max-width', 'inherit' );
+ this.$element
+ .addClass( 'oo-ui-tagMultiselectWidget-outlined' )
+ .append( this.input.$element );
+ } else {
+ this.$element.addClass( 'oo-ui-tagMultiselectWidget-inlined' );
+ // HACK: When the widget is using 'inline' input, the
+ // behavior needs to only use the $input itself
+ // so we style and size it accordingly (otherwise
+ // the styling and sizing can get very convoluted
+ // when the wrapping divs and other elements)
+ // We are taking advantage of still being able to
+ // call the widget itself for operations like
+ // .getValue() and setDisabled() and .focus() but
+ // having only the $input attached to the DOM
+ this.$content.append( this.input.$input );
+ }
+ } else {
+ this.$content.append( $tabFocus );
+ }
+
+ this.setTabIndexedElement(
+ this.hasInput ?
+ this.input.$input :
+ $tabFocus
+ );
+
+ if ( config.selected ) {
+ this.setValue( config.selected );
+ }
+
+ // HACK: Input size needs to be calculated after everything
+ // else is rendered
+ rAF( function () {
+ if ( widget.hasInput ) {
+ widget.updateInputSize();
+ }
+ } );
+};
+
+/* Initialization */
+
+OO.inheritClass( OO.ui.TagMultiselectWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.GroupWidget );
+OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.DraggableGroupElement );
+OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.TabIndexedElement );
+OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.FlaggedElement );
+
+/* Static properties */
+
+/**
+ * Allowed input positions.
+ * - inline: The input is inside the tag list
+ * - outline: The input is under the tag list
+ * - none: There is no input
+ *
+ * @property {Array}
+ */
+OO.ui.TagMultiselectWidget.static.allowedInputPositions = [ 'inline', 'outline', 'none' ];
+
+/* Methods */
+
+/**
+ * Handle mouse down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse down event
+ * @return {boolean} False to prevent defaults
+ */
+OO.ui.TagMultiselectWidget.prototype.onMouseDown = function ( e ) {
+ if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
+ this.focus();
+ return false;
+ }
+};
+
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ * @return {boolean} Whether to prevent defaults
+ */
+OO.ui.TagMultiselectWidget.prototype.onInputKeyPress = function ( e ) {
+ var stopOrContinue,
+ withMetaKey = e.metaKey || e.ctrlKey;
+
+ if ( !this.isDisabled() ) {
+ if ( e.which === OO.ui.Keys.ENTER ) {
+ stopOrContinue = this.doInputEnter( e, withMetaKey );
+ }
+
+ // Make sure the input gets resized.
+ setTimeout( this.updateInputSize.bind( this ), 0 );
+ return stopOrContinue;
+ }
+};
+
+/**
+ * Handle key down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key down event
+ * @return {boolean}
+ */
+OO.ui.TagMultiselectWidget.prototype.onInputKeyDown = function ( e ) {
+ var movement, direction,
+ withMetaKey = e.metaKey || e.ctrlKey;
+
+ if ( !this.isDisabled() ) {
+ // 'keypress' event is not triggered for Backspace
+ if ( e.keyCode === OO.ui.Keys.BACKSPACE ) {
+ return this.doInputBackspace( e, withMetaKey );
+ } else if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
+ return this.doInputEscape( e );
+ } else if (
+ e.keyCode === OO.ui.Keys.LEFT ||
+ e.keyCode === OO.ui.Keys.RIGHT
+ ) {
+ if ( OO.ui.Element.static.getDir( this.$element ) === 'rtl' ) {
+ movement = {
+ left: 'forwards',
+ right: 'backwards'
+ };
+ } else {
+ movement = {
+ left: 'backwards',
+ right: 'forwards'
+ };
+ }
+ direction = e.keyCode === OO.ui.Keys.LEFT ?
+ movement.left : movement.right;
+
+ return this.doInputArrow( e, direction, withMetaKey );
+ }
+ }
+};
+
+/**
+ * Respond to input focus event
+ */
+OO.ui.TagMultiselectWidget.prototype.onInputFocus = function () {
+ this.$element.addClass( 'oo-ui-tagMultiselectWidget-focus' );
+};
+
+/**
+ * Respond to input blur event
+ */
+OO.ui.TagMultiselectWidget.prototype.onInputBlur = function () {
+ this.$element.removeClass( 'oo-ui-tagMultiselectWidget-focus' );
+};
+
+/**
+ * Perform an action after the enter key on the input
+ *
+ * @param {jQuery.Event} e Event data
+ * @param {boolean} [withMetaKey] Whether this key was pressed with
+ * a meta key like 'ctrl'
+ * @return {boolean} Whether to prevent defaults
+ */
+OO.ui.TagMultiselectWidget.prototype.doInputEnter = function () {
+ this.addTagFromInput();
+ return false;
+};
+
+/**
+ * Perform an action responding to the enter key on the input
+ *
+ * @param {jQuery.Event} e Event data
+ * @param {boolean} [withMetaKey] Whether this key was pressed with
+ * a meta key like 'ctrl'
+ * @return {boolean} Whether to prevent defaults
+ */
+OO.ui.TagMultiselectWidget.prototype.doInputBackspace = function ( e, withMetaKey ) {
+ var items, item;
+
+ if (
+ this.inputPosition === 'inline' &&
+ this.input.getValue() === '' &&
+ !this.isEmpty()
+ ) {
+ // Delete the last item
+ items = this.getItems();
+ item = items[ items.length - 1 ];
+ this.removeItems( [ item ] );
+ // If Ctrl/Cmd was pressed, delete item entirely.
+ // Otherwise put it into the text field for editing.
+ if ( !withMetaKey ) {
+ this.input.setValue( item.getData() );
+ }
+
+ return false;
+ }
+};
+
+/**
+ * Perform an action after the escape key on the input
+ *
+ * @param {jQuery.Event} e Event data
+ */
+OO.ui.TagMultiselectWidget.prototype.doInputEscape = function () {
+ this.clearInput();
+};
+
+/**
+ * Perform an action after the arrow key on the input, select the previous
+ * or next item from the input.
+ * See #getPreviousItem and #getNextItem
+ *
+ * @param {jQuery.Event} e Event data
+ * @param {string} direction Direction of the movement; forwards or backwards
+ * @param {boolean} [withMetaKey] Whether this key was pressed with
+ * a meta key like 'ctrl'
+ */
+OO.ui.TagMultiselectWidget.prototype.doInputArrow = function ( e, direction ) {
+ if (
+ this.inputPosition === 'inline' &&
+ !this.isEmpty()
+ ) {
+ if ( direction === 'backwards' ) {
+ // Get previous item
+ this.getPreviousItem().focus();
+ } else {
+ // Get next item
+ this.getNextItem().focus();
+ }
+ }
+};
+
+/**
+ * Respond to item select event
+ *
+ * @param {OO.ui.TagItemWidget} item Selected item
+ */
+OO.ui.TagMultiselectWidget.prototype.onTagSelect = function ( item ) {
+ if ( this.hasInput && this.allowEditTags ) {
+ if ( this.input.getValue() ) {
+ this.addTagFromInput();
+ }
+ // 1. Get the label of the tag into the input
+ this.input.setValue( item.getData() );
+ // 2. Remove the tag
+ this.removeItems( [ item ] );
+ // 3. Focus the input
+ this.focus();
+ }
+};
+
+/**
+ * Respond to change event, where items were added, removed, or cleared.
+ */
+OO.ui.TagMultiselectWidget.prototype.onChangeTags = function () {
+ this.toggleValid( this.checkValidity() );
+ if ( this.hasInput ) {
+ this.updateInputSize();
+ }
+ this.updateIfHeightChanged();
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.TagMultiselectWidget.prototype.setDisabled = function ( isDisabled ) {
+ // Parent method
+ OO.ui.TagMultiselectWidget.parent.prototype.setDisabled.call( this, isDisabled );
+
+ if ( this.hasInput && this.input ) {
+ this.input.setDisabled( !!isDisabled );
+ }
+
+ if ( this.items ) {
+ this.getItems().forEach( function ( item ) {
+ item.setDisabled( !!isDisabled );
+ } );
+ }
+};
+
+/**
+ * Respond to tag remove event
+ * @param {OO.ui.TagItemWidget} item Removed tag
+ */
+OO.ui.TagMultiselectWidget.prototype.onTagRemove = function ( item ) {
+ this.removeTagByData( item.getData() );
+};
+
+/**
+ * Respond to navigate event on the tag
+ *
+ * @param {OO.ui.TagItemWidget} item Removed tag
+ * @param {string} direction Direction of movement; 'forwards' or 'backwards'
+ */
+OO.ui.TagMultiselectWidget.prototype.onTagNavigate = function ( item, direction ) {
+ if ( direction === 'forwards' ) {
+ this.getNextItem( item ).focus();
+ } else {
+ this.getPreviousItem( item ).focus();
+ }
+};
+
+/**
+ * Add tag from input value
+ */
+OO.ui.TagMultiselectWidget.prototype.addTagFromInput = function () {
+ var val = this.input.getValue(),
+ isValid = this.isAllowedData( val );
+
+ if ( !val ) {
+ return;
+ }
+
+ if ( isValid || this.allowDisplayInvalidTags ) {
+ this.addTag( val );
+ this.clearInput();
+ this.focus();
+ }
+};
+
+/**
+ * Clear the input
+ */
+OO.ui.TagMultiselectWidget.prototype.clearInput = function () {
+ this.input.setValue( '' );
+};
+
+/**
+ * Check whether the given value is a duplicate of an existing
+ * tag already in the list.
+ *
+ * @param {string|Object} data Requested value
+ * @return {boolean} Value is duplicate
+ */
+OO.ui.TagMultiselectWidget.prototype.isDuplicateData = function ( data ) {
+ return !!this.getItemFromData( data );
+};
+
+/**
+ * Check whether a given value is allowed to be added
+ *
+ * @param {string|Object} data Requested value
+ * @return {boolean} Value is allowed
+ */
+OO.ui.TagMultiselectWidget.prototype.isAllowedData = function ( data ) {
+ if (
+ !this.allowDuplicates &&
+ this.isDuplicateData( data )
+ ) {
+ return false;
+ }
+
+ if ( this.allowArbitrary ) {
+ return true;
+ }
+
+ // Check with allowed values
+ if (
+ this.getAllowedValues().some( function ( value ) {
+ return data === value;
+ } )
+ ) {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Get the allowed values list
+ *
+ * @return {string[]} Allowed data values
+ */
+OO.ui.TagMultiselectWidget.prototype.getAllowedValues = function () {
+ return this.allowedValues;
+};
+
+/**
+ * Add a value to the allowed values list
+ *
+ * @param {string} value Allowed data value
+ */
+OO.ui.TagMultiselectWidget.prototype.addAllowedValue = function ( value ) {
+ if ( this.allowedValues.indexOf( value ) === -1 ) {
+ this.allowedValues.push( value );
+ }
+};
+
+/**
+ * Get the datas of the currently selected items
+ *
+ * @return {string[]|Object[]} Datas of currently selected items
+ */
+OO.ui.TagMultiselectWidget.prototype.getValue = function () {
+ return this.getItems()
+ .filter( function ( item ) {
+ return item.isValid();
+ } )
+ .map( function ( item ) {
+ return item.getData();
+ } );
+};
+
+/**
+ * Set the value of this widget by datas.
+ *
+ * @param {string|string[]|Object|Object[]} valueObject An object representing the data
+ * and label of the value. If the widget allows arbitrary values,
+ * the items will be added as-is. Otherwise, the data value will
+ * be checked against allowedValues.
+ * 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' }
+ * ]
+ * Value can also be added with plaintext array, for example:
+ * [ 'foo', 'bar', 'bla' ] or a single string, like 'foo'
+ */
+OO.ui.TagMultiselectWidget.prototype.setValue = function ( valueObject ) {
+ valueObject = Array.isArray( valueObject ) ? valueObject : [ valueObject ];
+
+ this.clearItems();
+ valueObject.forEach( function ( obj ) {
+ if ( typeof obj === 'string' ) {
+ this.addTag( obj );
+ } else {
+ this.addTag( obj.data, obj.label );
+ }
+ }.bind( this ) );
+};
+
+/**
+ * Add tag to the display area
+ *
+ * @param {string|Object} data Tag data
+ * @param {string} [label] Tag label. If no label is provided, the
+ * stringified version of the data will be used instead.
+ * @return {boolean} Item was added successfully
+ */
+OO.ui.TagMultiselectWidget.prototype.addTag = function ( data, label ) {
+ var newItemWidget,
+ isValid = this.isAllowedData( data );
+
+ if ( isValid || this.allowDisplayInvalidTags ) {
+ newItemWidget = this.createTagItemWidget( data, label );
+ newItemWidget.toggleValid( isValid );
+ this.addItems( [ newItemWidget ] );
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Remove tag by its data property.
+ *
+ * @param {string|Object} data Tag data
+ */
+OO.ui.TagMultiselectWidget.prototype.removeTagByData = function ( data ) {
+ var item = this.getItemFromData( data );
+
+ this.removeItems( [ item ] );
+};
+
+/**
+ * Construct a OO.ui.TagItemWidget (or a subclass thereof) from given label and data.
+ *
+ * @protected
+ * @param {string} data Item data
+ * @param {string} label The label text.
+ * @return {OO.ui.TagItemWidget}
+ */
+OO.ui.TagMultiselectWidget.prototype.createTagItemWidget = function ( data, label ) {
+ label = label || data;
+
+ return new OO.ui.TagItemWidget( { data: data, label: label } );
+};
+
+/**
+ * Given an item, returns the item after it. If the item is already the
+ * last item, return `this.input`. If no item is passed, returns the
+ * very first item.
+ *
+ * @protected
+ * @param {OO.ui.TagItemWidget} [item] Tag item
+ * @return {OO.ui.Widget} The next widget available.
+ */
+OO.ui.TagMultiselectWidget.prototype.getNextItem = function ( item ) {
+ var itemIndex = this.items.indexOf( item );
+
+ if ( item === undefined || itemIndex === -1 ) {
+ return this.items[ 0 ];
+ }
+
+ if ( itemIndex === this.items.length - 1 ) { // Last item
+ if ( this.hasInput ) {
+ return this.input;
+ } else {
+ // Return first item
+ return this.items[ 0 ];
+ }
+ } else {
+ return this.items[ itemIndex + 1 ];
+ }
+};
+
+/**
+ * Given an item, returns the item before it. If the item is already the
+ * first item, return `this.input`. If no item is passed, returns the
+ * very last item.
+ *
+ * @protected
+ * @param {OO.ui.TagItemWidget} [item] Tag item
+ * @return {OO.ui.Widget} The previous widget available.
+ */
+OO.ui.TagMultiselectWidget.prototype.getPreviousItem = function ( item ) {
+ var itemIndex = this.items.indexOf( item );
+
+ if ( item === undefined || itemIndex === -1 ) {
+ return this.items[ this.items.length - 1 ];
+ }
+
+ if ( itemIndex === 0 ) {
+ if ( this.hasInput ) {
+ return this.input;
+ } else {
+ // Return the last item
+ return this.items[ this.items.length - 1 ];
+ }
+ } else {
+ return this.items[ itemIndex - 1 ];
+ }
+};
+
+/**
+ * Update the dimensions of the text input field to encompass all available area.
+ * This is especially relevant for when the input is at the edge of a line
+ * and should get smaller. The usual operation (as an inline-block with min-width)
+ * does not work in that case, pushing the input downwards to the next line.
+ *
+ * @private
+ */
+OO.ui.TagMultiselectWidget.prototype.updateInputSize = function () {
+ var $lastItem, direction, contentWidth, currentWidth, bestWidth;
+ if ( this.inputPosition === 'inline' && !this.isDisabled() ) {
+ this.input.$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.getValue() === '' &&
+ this.input.$input.attr( 'placeholder' ) !== undefined
+ ) {
+ this.input.setValue( this.input.$input.attr( 'placeholder' ) );
+ this.contentWidthWithPlaceholder = this.input.$input[ 0 ].scrollWidth;
+ this.input.setValue( '' );
+
+ }
+
+ // Always keep the input wide enough for the placeholder text
+ contentWidth = Math.max(
+ this.input.$input[ 0 ].scrollWidth,
+ // undefined arguments in Math.max lead to NaN
+ ( this.contentWidthWithPlaceholder === undefined ) ?
+ 0 : this.contentWidthWithPlaceholder
+ );
+ currentWidth = this.input.$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.$input.width( Math.floor( bestWidth ) );
+ this.updateIfHeightChanged();
+ } else {
+ this.updateIfHeightChanged();
+ }
+};
+
+/**
+ * Determine if widget height changed, and if so,
+ * emit the resize event. This is useful for when there are either
+ * menus or popups attached to the bottom of the widget, to allow
+ * them to change their positioning in case the widget moved down
+ * or up.
+ *
+ * @private
+ */
+OO.ui.TagMultiselectWidget.prototype.updateIfHeightChanged = function () {
+ var height = this.$element.height();
+ if ( height !== this.height ) {
+ this.height = height;
+ this.emit( 'resize' );
+ }
+};
+
+/**
+ * Check whether all items in the widget are valid
+ *
+ * @return {boolean} Widget is valid
+ */
+OO.ui.TagMultiselectWidget.prototype.checkValidity = function () {
+ return this.getItems().every( function ( item ) {
+ return item.isValid();
+ } );
+};
+
+/**
+ * Set the valid state of this item
+ *
+ * @param {boolean} [valid] Item is valid
+ * @fires valid
+ */
+OO.ui.TagMultiselectWidget.prototype.toggleValid = function ( valid ) {
+ valid = valid === undefined ? !this.valid : !!valid;
+
+ if ( this.valid !== valid ) {
+ this.valid = valid;
+
+ this.setFlags( { invalid: !this.valid } );
+
+ this.emit( 'valid', this.valid );
+ }
+};
+
+/**
+ * Get the current valid state of the widget
+ *
+ * @return {boolean} Widget is valid
+ */
+OO.ui.TagMultiselectWidget.prototype.isValid = function () {
+ return this.valid;
+};
+
+/**
+ * PopupTagMultiselectWidget is a {@link OO.ui.TagMultiselectWidget OO.ui.TagMultiselectWidget} intended
+ * to use a popup. The popup can be configured to have a default input to insert values into the widget.
+ *
+ * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Example: A basic PopupTagMultiselectWidget.
+ * var widget = new OO.ui.PopupTagMultiselectWidget();
+ * $( 'body' ).append( widget.$element );
+ *
+ * // Example: A PopupTagMultiselectWidget with an external popup.
+ * var popupInput = new OO.ui.TextInputWidget(),
+ * widget = new OO.ui.PopupTagMultiselectWidget( {
+ * popupInput: popupInput,
+ * popup: {
+ * $content: popupInput.$element
+ * }
+ * } );
+ * $( 'body' ).append( widget.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
+ *
+ * @class
+ * @extends OO.ui.TagMultiselectWidget
+ * @mixins OO.ui.mixin.PopupElement
+ *
+ * @param {Object} config Configuration object
+ * @cfg {jQuery} [$overlay] An overlay for the popup.
+ * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
+ * @cfg {Object} [popup] Configuration options for the popup
+ * @cfg {OO.ui.InputWidget} [popupInput] An input widget inside the popup that will be
+ * focused when the popup is opened and will be used as replacement for the
+ * general input in the widget.
+ */
+OO.ui.PopupTagMultiselectWidget = function OoUiPopupTagMultiselectWidget( config ) {
+ var defaultInput,
+ defaultConfig = { popup: {} };
+
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.PopupTagMultiselectWidget.parent.call( this, $.extend( { inputPosition: 'none' }, config ) );
+
+ this.$overlay = config.$overlay || this.$element;
+
+ if ( !config.popup ) {
+ // For the default base implementation, we give a popup
+ // with an input widget inside it. For any other use cases
+ // the popup needs to be populated externally and the
+ // event handled to add tags separately and manually
+ defaultInput = new OO.ui.TextInputWidget();
+
+ defaultConfig.popupInput = defaultInput;
+ defaultConfig.popup.$content = defaultInput.$element;
+
+ this.$element.addClass( 'oo-ui-popupTagMultiselectWidget-defaultPopup' );
+ }
+
+ // Add overlay, and add that to the autoCloseIgnore
+ defaultConfig.popup.$overlay = this.$overlay;
+ defaultConfig.popup.$autoCloseIgnore = this.hasInput ?
+ this.input.$element.add( this.$overlay ) : this.$overlay;
+
+ // Allow extending any of the above
+ config = $.extend( defaultConfig, config );
+
+ // Mixin constructors
+ OO.ui.mixin.PopupElement.call( this, config );
+
+ if ( this.hasInput ) {
+ this.input.$input.on( 'focus', this.popup.toggle.bind( this.popup, true ) );
+ }
+
+ // Configuration options
+ this.popupInput = config.popupInput;
+ if ( this.popupInput ) {
+ this.popupInput.connect( this, {
+ enter: 'onPopupInputEnter'
+ } );
+ }
+
+ // Events
+ this.on( 'resize', this.popup.updateDimensions.bind( this.popup ) );
+ this.popup.connect( this, { toggle: 'onPopupToggle' } );
+ this.$tabIndexed
+ .on( 'focus', this.onFocus.bind( this ) );
+
+ // Initialize
+ this.$element
+ .append( this.popup.$element )
+ .addClass( 'oo-ui-popupTagMultiselectWidget' );
+};
+
+/* Initialization */
+
+OO.inheritClass( OO.ui.PopupTagMultiselectWidget, OO.ui.TagMultiselectWidget );
+OO.mixinClass( OO.ui.PopupTagMultiselectWidget, OO.ui.mixin.PopupElement );
+
+/* Methods */
+
+/**
+ * Focus event handler.
+ *
+ * @private
+ */
+OO.ui.PopupTagMultiselectWidget.prototype.onFocus = function () {
+ this.popup.toggle( true );
+};
+
+/**
+ * Respond to popup toggle event
+ *
+ * @param {boolean} isVisible Popup is visible
+ */
+OO.ui.PopupTagMultiselectWidget.prototype.onPopupToggle = function ( isVisible ) {
+ if ( isVisible && this.popupInput ) {
+ this.popupInput.focus();
+ }
+};
+
+/**
+ * Respond to popup input enter event
+ */
+OO.ui.PopupTagMultiselectWidget.prototype.onPopupInputEnter = function () {
+ if ( this.popupInput ) {
+ this.addTagByPopupValue( this.popupInput.getValue() );
+ this.popupInput.setValue( '' );
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupTagMultiselectWidget.prototype.onTagSelect = function ( item ) {
+ if ( this.popupInput && this.allowEditTags ) {
+ this.popupInput.setValue( item.getData() );
+ this.removeItems( [ item ] );
+
+ this.popup.toggle( true );
+ this.popupInput.focus();
+ } else {
+ // Parent
+ OO.ui.PopupTagMultiselectWidget.parent.prototype.onTagSelect.call( this, item );
+ }
+};
+
+/**
+ * Add a tag by the popup value.
+ * Whatever is responsible for setting the value in the popup should call
+ * this method to add a tag, or use the regular methods like #addTag or
+ * #setValue directly.
+ *
+ * @param {string} data The value of item
+ * @param {string} [label] The label of the tag. If not given, the data is used.
+ */
+OO.ui.PopupTagMultiselectWidget.prototype.addTagByPopupValue = function ( data, label ) {
+ this.addTag( data, label );
+};
+
+/**
+ * MenuTagMultiselectWidget is a {@link OO.ui.TagMultiselectWidget OO.ui.TagMultiselectWidget} intended
+ * to use a menu of selectable options.
+ *
+ * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Example: A basic MenuTagMultiselectWidget.
+ * var widget = new OO.ui.MenuTagMultiselectWidget( {
+ * inputPosition: 'outline',
+ * options: [
+ * { data: 'option1', label: 'Option 1' },
+ * { data: 'option2', label: 'Option 2' },
+ * { data: 'option3', label: 'Option 3' },
+ * ],
+ * selected: [ 'option1', 'option2' ]
+ * } );
+ * $( 'body' ).append( widget.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
+ *
+ * @class
+ * @extends OO.ui.TagMultiselectWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration object
+ * @cfg {Object} [menu] Configuration object for the menu widget
+ * @cfg {jQuery} [$overlay] An overlay for the menu.
+ * See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
+ * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
+ */
+OO.ui.MenuTagMultiselectWidget = function OoUiMenuTagMultiselectWidget( config ) {
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.MenuTagMultiselectWidget.parent.call( this, config );
+
+ this.$overlay = config.$overlay || this.$element;
+
+ this.menu = this.createMenuWidget( $.extend( {
+ widget: this,
+ input: this.hasInput ? this.input : null,
+ $input: this.hasInput ? this.input.$input : null,
+ filterFromInput: !!this.hasInput,
+ $autoCloseIgnore: this.hasInput ?
+ this.input.$element.add( this.$overlay ) : this.$overlay,
+ $floatableContainer: this.hasInput && this.inputPosition === 'outline' ?
+ this.input.$element : this.$element,
+ $overlay: this.$overlay,
+ disabled: this.isDisabled()
+ }, config.menu ) );
+ this.addOptions( config.options || [] );
+
+ // Events
+ this.menu.connect( this, {
+ choose: 'onMenuChoose',
+ toggle: 'onMenuToggle'
+ } );
+ if ( this.hasInput ) {
+ this.input.connect( this, { change: 'onInputChange' } );
+ }
+ this.connect( this, { resize: 'onResize' } );
+
+ // Initialization
+ this.$overlay
+ .append( this.menu.$element );
+ this.$element
+ .addClass( 'oo-ui-menuTagMultiselectWidget' );
+};
+
+/* Initialization */
+
+OO.inheritClass( OO.ui.MenuTagMultiselectWidget, OO.ui.TagMultiselectWidget );
+
+/* Methods */
+
+/**
+ * Respond to resize event
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.onResize = function () {
+ // Reposition the menu
+ this.menu.position();
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.onInputFocus = function () {
+ // Parent method
+ OO.ui.MenuTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
+
+ this.menu.toggle( true );
+};
+
+/**
+ * Respond to input change event
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.onInputChange = function () {
+ this.menu.toggle( true );
+};
+
+/**
+ * Respond to menu choose event
+ *
+ * @param {OO.ui.OptionWidget} menuItem Chosen menu item
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.onMenuChoose = function ( menuItem ) {
+ // Add tag
+ this.addTag( menuItem.getData(), menuItem.getLabel() );
+};
+
+/**
+ * Respond to menu toggle event. Reset item highlights on hide.
+ *
+ * @param {boolean} isVisible The menu is visible
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
+ if ( !isVisible ) {
+ this.menu.selectItem( null );
+ this.menu.highlightItem( null );
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
+ var menuItem = this.menu.getItemFromData( tagItem.getData() );
+ // Override the base behavior from TagMultiselectWidget; the base behavior
+ // in TagMultiselectWidget is to remove the tag to edit it in the input,
+ // but in our case, we want to utilize the menu selection behavior, and
+ // definitely not remove the item.
+
+ // Select the menu item
+ this.menu.selectItem( menuItem );
+
+ this.focus();
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.addTagFromInput = function () {
+ var inputValue = this.input.getValue(),
+ highlightedItem = this.menu.getHighlightedItem(),
+ item = this.menu.getItemFromData( inputValue );
+
+ // Override the parent method so we add from the menu
+ // rather than directly from the input
+
+ // Look for a highlighted item first
+ if ( highlightedItem ) {
+ this.addTag( highlightedItem.getData(), highlightedItem.getLabel() );
+ } else if ( item ) {
+ // Look for the element that fits the data
+ 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
+ this.addTag( inputValue );
+ }
+};
+
+/**
+ * Return the visible items in the menu. This is mainly used for when
+ * the menu is filtering results.
+ *
+ * @return {OO.ui.MenuOptionWidget[]} Visible results
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.getMenuVisibleItems = function () {
+ return this.menu.getItems().filter( function ( menuItem ) {
+ return menuItem.isVisible();
+ } );
+};
+
+/**
+ * Create the menu for this widget. This is in a separate method so that
+ * child classes can override this without polluting the constructor with
+ * unnecessary extra objects that will be overidden.
+ *
+ * @param {Object} menuConfig Configuration options
+ * @return {OO.ui.MenuSelectWidget} Menu widget
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
+ return new OO.ui.MenuSelectWidget( menuConfig );
+};
+
+/**
+ * Add options to the menu
+ *
+ * @param {Object[]} menuOptions Object defining options
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.addOptions = function ( menuOptions ) {
+ var widget = this,
+ items = menuOptions.map( function ( obj ) {
+ return widget.createMenuOptionWidget( obj.data, obj.label );
+ } );
+
+ this.menu.addItems( items );
+};
+
+/**
+ * Create a menu option widget.
+ *
+ * @param {string} data Item data
+ * @param {string} [label] Item label
+ * @return {OO.ui.OptionWidget} Option widget
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.createMenuOptionWidget = function ( data, label ) {
+ return new OO.ui.MenuOptionWidget( {
+ data: data,
+ label: label || data
+ } );
+};
+
+/**
+ * Get the menu
+ *
+ * @return {OO.ui.MenuSelectWidget} Menu
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.getMenu = function () {
+ return this.menu;
+};
+
+/**
+ * Get the allowed values list
+ *
+ * @return {string[]} Allowed data values
+ */
+OO.ui.MenuTagMultiselectWidget.prototype.getAllowedValues = function () {
+ var menuDatas = this.menu.getItems().map( function ( menuItem ) {
+ return menuItem.getData();
+ } );
+ return this.allowedValues.concat( menuDatas );
+};
+
+/**
+ * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
+ * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
+ * OO.ui.mixin.IndicatorElement indicators}.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * @example
+ * // Example of a file select widget
+ * var selectFile = new OO.ui.SelectFileWidget();
+ * $( 'body' ).append( selectFile.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.PendingElement
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
+ * @cfg {string} [placeholder] Text to display when no file is selected.
+ * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
+ * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
+ * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be true.
+ * @cfg {number} [thumbnailSizeLimit=20] File size limit in MiB above which to not try and show a
+ * preview (for performance)
+ */
+OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
+ var dragHandler;
+
+ // Configuration initialization
+ config = $.extend( {
+ accept: null,
+ placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
+ notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
+ droppable: true,
+ showDropTarget: false,
+ thumbnailSizeLimit: 20
+ }, config );
+
+ // Parent constructor
+ OO.ui.SelectFileWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.IndicatorElement.call( this, config );
+ OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$info } ) );
+ OO.ui.mixin.LabelElement.call( this, config );
+
+ // Properties
+ this.$info = $( '<span>' );
+ this.showDropTarget = config.showDropTarget;
+ this.thumbnailSizeLimit = config.thumbnailSizeLimit;
+ this.isSupported = this.constructor.static.isSupported();
+ this.currentFile = null;
+ if ( Array.isArray( config.accept ) ) {
+ this.accept = config.accept;
+ } else {
+ this.accept = null;
+ }
+ this.placeholder = config.placeholder;
+ this.notsupported = config.notsupported;
+ this.onFileSelectedHandler = this.onFileSelected.bind( this );
+
+ this.selectButton = new OO.ui.ButtonWidget( {
+ classes: [ 'oo-ui-selectFileWidget-selectButton' ],
+ label: OO.ui.msg( 'ooui-selectfile-button-select' ),
+ disabled: this.disabled || !this.isSupported
+ } );
+
+ this.clearButton = new OO.ui.ButtonWidget( {
+ classes: [ 'oo-ui-selectFileWidget-clearButton' ],
+ framed: false,
+ icon: 'close',
+ disabled: this.disabled
+ } );
+
+ // Events
+ this.selectButton.$button.on( {
+ keypress: this.onKeyPress.bind( this )
+ } );
+ this.clearButton.connect( this, {
+ click: 'onClearClick'
+ } );
+ if ( config.droppable ) {
+ dragHandler = this.onDragEnterOrOver.bind( this );
+ this.$element.on( {
+ dragenter: dragHandler,
+ dragover: dragHandler,
+ dragleave: this.onDragLeave.bind( this ),
+ drop: this.onDrop.bind( this )
+ } );
+ }
+
+ // Initialization
+ this.addInput();
+ this.$label.addClass( 'oo-ui-selectFileWidget-label' );
+ this.$info
+ .addClass( 'oo-ui-selectFileWidget-info' )
+ .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
+
+ if ( config.droppable && config.showDropTarget ) {
+ this.selectButton.setIcon( 'upload' );
+ this.$thumbnail = $( '<div>' ).addClass( 'oo-ui-selectFileWidget-thumbnail' );
+ this.setPendingElement( this.$thumbnail );
+ this.$element
+ .addClass( 'oo-ui-selectFileWidget-dropTarget oo-ui-selectFileWidget' )
+ .on( {
+ click: this.onDropTargetClick.bind( this )
+ } )
+ .append(
+ this.$thumbnail,
+ this.$info,
+ this.selectButton.$element,
+ $( '<span>' )
+ .addClass( 'oo-ui-selectFileWidget-dropLabel' )
+ .text( OO.ui.msg( 'ooui-selectfile-dragdrop-placeholder' ) )
+ );
+ } else {
+ this.$element
+ .addClass( 'oo-ui-selectFileWidget' )
+ .append( this.$info, this.selectButton.$element );
+ }
+ this.updateUI();
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
+OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
+
+/* Static Properties */
+
+/**
+ * Check if this widget is supported
+ *
+ * @static
+ * @return {boolean}
+ */
+OO.ui.SelectFileWidget.static.isSupported = function () {
+ var $input;
+ if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
+ $input = $( '<input>' ).attr( 'type', 'file' );
+ OO.ui.SelectFileWidget.static.isSupportedCache = $input[ 0 ].files !== undefined;
+ }
+ return OO.ui.SelectFileWidget.static.isSupportedCache;
+};
+
+OO.ui.SelectFileWidget.static.isSupportedCache = null;
+
+/* Events */
+
+/**
+ * @event change
+ *
+ * A change event is emitted when the on/off state of the toggle changes.
+ *
+ * @param {File|null} value New value
*/
/* Methods */
* @chainable
*/
OO.ui.SelectFileWidget.prototype.focus = function () {
- this.selectButton.$button[ 0 ].focus();
+ this.selectButton.focus();
+ return this;
+};
+
+/**
+ * Blur the widget.
+ *
+ * @chainable
+ */
+OO.ui.SelectFileWidget.prototype.blur = function () {
+ this.selectButton.blur();
return this;
};
* $( 'body' ).append( numberInput.$element );
*
* @class
- * @extends OO.ui.Widget
+ * @extends OO.ui.TextInputWidget
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
* @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
* @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
- * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
+ * @cfg {boolean} [allowInteger=false] Whether the field accepts only integer values.
* @cfg {number} [min=-Infinity] Minimum allowed value
* @cfg {number} [max=Infinity] Maximum allowed value
* @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
* @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
*/
OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
+ var $field = $( '<div>' )
+ .addClass( 'oo-ui-numberInputWidget-field' );
+
// Configuration initialization
config = $.extend( {
- isInteger: false,
+ allowInteger: false,
min: -Infinity,
max: Infinity,
step: 1,
showButtons: true
}, config );
+ // For backward compatibility
+ $.extend( config, config.input );
+ this.input = this;
+
// Parent constructor
- OO.ui.NumberInputWidget.parent.call( this, config );
+ OO.ui.NumberInputWidget.parent.call( this, $.extend( config, {
+ type: 'number'
+ } ) );
- // Properties
- this.input = new OO.ui.TextInputWidget( $.extend(
- {
- disabled: this.isDisabled(),
- type: 'number'
- },
- config.input
- ) );
if ( config.showButtons ) {
this.minusButton = new OO.ui.ButtonWidget( $.extend(
{
}
// Events
- this.input.connect( this, {
- change: this.emit.bind( this, 'change' ),
- enter: this.emit.bind( this, 'enter' )
- } );
- this.input.$input.on( {
+ this.$input.on( {
keydown: this.onKeyDown.bind( this ),
'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
} );
} );
}
- // Initialization
- this.setIsInteger( !!config.isInteger );
- this.setRange( config.min, config.max );
- this.setStep( config.step, config.pageStep );
-
- this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
- .append( this.input.$element );
- this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
+ // Build the field
+ $field.append( this.$input );
if ( config.showButtons ) {
- this.$field
+ $field
.prepend( this.minusButton.$element )
.append( this.plusButton.$element );
- this.$element.addClass( 'oo-ui-numberInputWidget-buttoned' );
}
- this.input.setValidation( this.validateNumber.bind( this ) );
-};
-/* Setup */
-
-OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
+ // Initialization
+ this.setAllowInteger( config.allowInteger || config.isInteger );
+ this.setRange( config.min, config.max );
+ this.setStep( config.step, config.pageStep );
+ // Set the validation method after we set allowInteger and range
+ // so that it doesn't immediately call setValidityFlag
+ this.setValidation( this.validateNumber.bind( this ) );
-/* Events */
+ this.$element
+ .addClass( 'oo-ui-numberInputWidget' )
+ .toggleClass( 'oo-ui-numberInputWidget-buttoned', config.showButtons )
+ .append( $field );
+};
-/**
- * A `change` event is emitted when the value of the input changes.
- *
- * @event change
- */
+/* Setup */
-/**
- * An `enter` event is emitted when the user presses 'enter' inside the text box.
- *
- * @event enter
- */
+OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.TextInputWidget );
/* Methods */
*
* @param {boolean} flag
*/
-OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
- this.isInteger = !!flag;
- this.input.setValidityFlag();
+OO.ui.NumberInputWidget.prototype.setAllowInteger = function ( flag ) {
+ this.allowInteger = !!flag;
+ this.setValidityFlag();
};
+// Backward compatibility
+OO.ui.NumberInputWidget.prototype.setIsInteger = OO.ui.NumberInputWidget.prototype.setAllowInteger;
/**
* Get whether only integers are allowed
*
* @return {boolean} Flag value
*/
-OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
- return this.isInteger;
+OO.ui.NumberInputWidget.prototype.getAllowInteger = function () {
+ return this.allowInteger;
};
+// Backward compatibility
+OO.ui.NumberInputWidget.prototype.getIsInteger = OO.ui.NumberInputWidget.prototype.getAllowInteger;
/**
* Set the range of allowed values
}
this.min = min;
this.max = max;
- this.input.setValidityFlag();
+ this.setValidityFlag();
};
/**
return [ this.step, this.pageStep ];
};
-/**
- * Get the current value of the widget
- *
- * @return {string}
- */
-OO.ui.NumberInputWidget.prototype.getValue = function () {
- return this.input.getValue();
-};
-
/**
* Get the current value of the widget as a number
*
* @return {number} May be NaN, or an invalid number
*/
OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
- return +this.input.getValue();
-};
-
-/**
- * Set the value of the widget
- *
- * @param {string} value Invalid values are allowed
- */
-OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
- this.input.setValue( value );
+ return +this.getValue();
};
/**
} else {
n = v + delta;
n = Math.max( Math.min( n, this.max ), this.min );
- if ( this.isInteger ) {
+ if ( this.allowInteger ) {
n = Math.round( n );
}
}
this.setValue( n );
}
};
-
/**
* Validate input
*
*/
OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
var n = +value;
+ if ( value === '' ) {
+ return !this.isRequired();
+ }
+
if ( isNaN( n ) || !isFinite( n ) ) {
return false;
}
- if ( this.isInteger && Math.floor( n ) !== n ) {
+ if ( this.allowInteger && Math.floor( n ) !== n ) {
return false;
}
OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
var delta = 0;
- if ( !this.isDisabled() && this.input.$input.is( ':focus' ) ) {
+ if ( !this.isDisabled() && this.$input.is( ':focus' ) ) {
// Standard 'wheel' event
if ( event.originalEvent.deltaMode !== undefined ) {
this.sawWheelEvent = true;
// Parent method
OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
- if ( this.input ) {
- this.input.setDisabled( this.isDisabled() );
- }
if ( this.minusButton ) {
this.minusButton.setDisabled( this.isDisabled() );
}
};
}( OO ) );
+
+//# sourceMappingURL=oojs-ui-widgets.js.map
\ No newline at end of file