jenkins-bot has submitted this change and it was merged. Change subject: Add IndexLayout ......................................................................
Add IndexLayout IndexLayout (new) is similar to BookletLayout but much simpler. It uses CardLayout (new) instead of PageLayout, each of which relates to generated TabOptionWidget (new) objects which are inside a new TabSelectWidget (new) object. The styling is gray background with white tabs that connect to the content below. The unselected tabs have hover background color change. The titles are bold like buttons. Tabs are left aligned and also an active state was added to the tabs. Bug: T97877 Change-Id: I3deaa165d1d18a83dff13f469caeda63f34e32b1 --- M build/modules.json M demos/pages/dialogs.js A src/layouts/CardLayout.js A src/layouts/IndexLayout.js M src/styles/core.less A src/styles/layouts/CardLayout.less A src/styles/layouts/IndexLayout.less M src/styles/theme.less A src/styles/widgets/TabOptionWidget.less A src/styles/widgets/TabSelectWidget.less M src/themes/apex/layouts.less M src/themes/apex/widgets.less M src/themes/blank/layouts.less M src/themes/blank/widgets.less M src/themes/mediawiki/layouts.less M src/themes/mediawiki/widgets.less M src/widgets/OutlineControlsWidget.js M src/widgets/OutlineSelectWidget.js A src/widgets/TabOptionWidget.js A src/widgets/TabSelectWidget.js 20 files changed, 855 insertions(+), 4 deletions(-) Approvals: Krinkle: Looks good to me, approved jenkins-bot: Verified diff --git a/build/modules.json b/build/modules.json index 7f44dda..5aa773f 100644 --- a/build/modules.json +++ b/build/modules.json @@ -47,7 +47,9 @@ "src/layouts/FormLayout.js", "src/layouts/MenuLayout.js", "src/layouts/BookletLayout.js", + "src/layouts/IndexLayout.js", "src/layouts/PanelLayout.js", + "src/layouts/CardLayout.js", "src/layouts/PageLayout.js", "src/layouts/StackLayout.js", @@ -87,6 +89,7 @@ "src/widgets/MenuOptionWidget.js", "src/widgets/MenuSectionOptionWidget.js", "src/widgets/OutlineOptionWidget.js", + "src/widgets/TabOptionWidget.js", "src/widgets/PopupWidget.js", "src/widgets/ProgressBarWidget.js", "src/widgets/SearchWidget.js", @@ -96,6 +99,7 @@ "src/widgets/MenuSelectWidget.js", "src/widgets/TextInputMenuSelectWidget.js", "src/widgets/OutlineSelectWidget.js", + "src/widgets/TabSelectWidget.js", "src/widgets/ToggleSwitchWidget.js", "src/outro.js.txt" diff --git a/demos/pages/dialogs.js b/demos/pages/dialogs.js index b2da609..67f2ffc 100644 --- a/demos/pages/dialogs.js +++ b/demos/pages/dialogs.js @@ -285,6 +285,52 @@ }, this ); }; + function SampleCard( name, config ) { + config = $.extend( { label: 'Sample card' }, config ); + OO.ui.CardLayout.call( this, name, config ); + this.label = config.label; + this.$element.text( this.label ); + } + OO.inheritClass( SampleCard, OO.ui.CardLayout ); + SampleCard.prototype.setupTabItem = function ( tabItem ) { + SampleCard.super.prototype.setupTabItem.call( this, tabItem ); + this.tabItem.setLabel( this.label ); + }; + + function IndexedDialog( config ) { + IndexedDialog.super.call( this, config ); + } + OO.inheritClass( IndexedDialog, OO.ui.ProcessDialog ); + IndexedDialog.static.title = 'Index dialog'; + IndexedDialog.static.actions = [ + { action: 'save', label: 'Done', flags: [ 'primary', 'progressive' ] }, + { action: 'cancel', label: 'Cancel', flags: 'safe' } + ]; + IndexedDialog.prototype.getBodyHeight = function () { + return 250; + }; + IndexedDialog.prototype.initialize = function () { + IndexedDialog.super.prototype.initialize.apply( this, arguments ); + this.indexLayout = new OO.ui.IndexLayout(); + this.cards = [ + new SampleCard( 'first', { label: 'One' } ), + new SampleCard( 'second', { label: 'Two' } ), + new SampleCard( 'third', { label: 'Three' } ), + new SampleCard( 'fourth', { label: 'Four' } ) + ]; + + this.indexLayout.addCards( this.cards ); + this.$body.append( this.indexLayout.$element ); + }; + IndexedDialog.prototype.getActionProcess = function ( action ) { + if ( action ) { + return new OO.ui.Process( function () { + this.close( { action: action } ); + }, this ); + } + return IndexedDialog.super.prototype.getActionProcess.call( this, action ); + }; + function MenuDialog( config ) { MenuDialog.super.call( this, config ); } @@ -438,6 +484,13 @@ } }, { + name: 'Indexed dialog', + dialogClass: IndexedDialog, + config: { + size: 'medium' + } + }, + { name: 'Menu dialog', dialogClass: MenuDialog, config: { diff --git a/src/layouts/CardLayout.js b/src/layouts/CardLayout.js new file mode 100644 index 0000000..16f2464 --- /dev/null +++ b/src/layouts/CardLayout.js @@ -0,0 +1,138 @@ +/** + * 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. + * + * 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 + * {@link OO.ui.IndexLayout IndexLayout} for an example. + * + * @class + * @extends OO.ui.PanelLayout + * + * @constructor + * @param {string} name Unique symbolic name of card + * @param {Object} [config] Configuration options + */ +OO.ui.CardLayout = function OoUiCardLayout( name, config ) { + // Allow passing positional parameters inside the config object + if ( OO.isPlainObject( name ) && config === undefined ) { + config = name; + name = config.name; + } + + // Configuration initialization + config = $.extend( { scrollable: true }, config ); + + // Parent constructor + OO.ui.CardLayout.super.call( this, config ); + + // Properties + this.name = name; + this.tabItem = null; + this.active = false; + + // Initialization + this.$element.addClass( 'oo-ui-cardLayout' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.CardLayout, 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. + * + * @event active + * @param {boolean} active Card is active + */ + +/* Methods */ + +/** + * Get the symbolic name of the card. + * + * @return {string} Symbolic name of card + */ +OO.ui.CardLayout.prototype.getName = function () { + return this.name; +}; + +/** + * Check if card 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. + * + * @return {boolean} Card is active + */ +OO.ui.CardLayout.prototype.isActive = function () { + return this.active; +}; + +/** + * Get tab item. + * + * The tab item allows users to access the card 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 () { + return this.tabItem; +}; + +/** + * Set or unset the tab item. + * + * Specify an {@link OO.ui.TabOptionWidget tab option} to set it, + * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab + * level), use #setupTabItem instead of this method. + * + * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear + * @chainable + */ +OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) { + this.tabItem = tabItem || null; + if ( tabItem ) { + this.setupTabItem(); + } + return this; +}; + +/** + * Set up the tab item. + * + * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset + * the tab item itself (with an {@link OO.ui.TabOptionWidget tab option} or `null`), use + * the #setTabItem method instead. + * + * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up + * @chainable + */ +OO.ui.CardLayout.prototype.setupTabItem = function () { + return this; +}; + +/** + * Set the card 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. + * + * @param {boolean} value Card is active + * @fires active + */ +OO.ui.CardLayout.prototype.setActive = function ( active ) { + active = !!active; + + if ( active !== this.active ) { + this.active = active; + this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active ); + this.emit( 'active', this.active ); + } +}; diff --git a/src/layouts/IndexLayout.js b/src/layouts/IndexLayout.js new file mode 100644 index 0000000..4234c23 --- /dev/null +++ b/src/layouts/IndexLayout.js @@ -0,0 +1,452 @@ +/** + * 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, + * 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. + * + * function CardOneLayout( name, config ) { + * CardOneLayout.super.call( this, name, config ); + * this.$element.append( '<p>First card</p>' ); + * } + * OO.inheritClass( CardOneLayout, OO.ui.CardLayout ); + * CardOneLayout.prototype.setupTabItem = function () { + * this.tabItem.setLabel( 'Card One' ); + * }; + * + * function CardTwoLayout( name, config ) { + * CardTwoLayout.super.call( this, name, config ); + * this.$element.append( '<p>Second card</p>' ); + * } + * OO.inheritClass( CardTwoLayout, OO.ui.CardLayout ); + * CardTwoLayout.prototype.setupTabItem = function () { + * this.tabItem.setLabel( 'Card Two' ); + * }; + * + * var card1 = new CardOneLayout( 'one' ), + * card2 = new CardTwoLayout( 'two' ); + * + * var index = new OO.ui.IndexLayout(); + * + * index.addCards ( [ card1, card2 ] ); + * $( 'body' ).append( index.$element ); + * + * @class + * @extends OO.ui.MenuLayout + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [continuous=false] Show all cards, one after another + * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed. + */ +OO.ui.IndexLayout = function OoUiIndexLayout( config ) { + // Configuration initialization + config = $.extend( {}, config, { menuPosition: 'top' } ); + + // Parent constructor + OO.ui.IndexLayout.super.call( this, config ); + + // Properties + this.currentCardName = null; + this.cards = {}; + this.ignoreFocus = false; + this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } ); + this.$content.append( this.stackLayout.$element ); + this.autoFocus = config.autoFocus === undefined || !!config.autoFocus; + + this.tabSelectWidget = new OO.ui.TabSelectWidget(); + this.tabPanel = new OO.ui.PanelLayout(); + this.$menu.append( this.tabPanel.$element ); + + this.toggleMenu( true ); + + // Events + this.stackLayout.connect( this, { set: 'onStackLayoutSet' } ); + this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' } ); + if ( this.autoFocus ) { + // Event 'focus' does not bubble, but 'focusin' does + this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) ); + } + + // Initialization + this.$element.addClass( 'oo-ui-indexLayout' ); + this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' ); + this.tabPanel.$element + .addClass( 'oo-ui-indexLayout-tabPanel' ) + .append( this.tabSelectWidget.$element ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout ); + +/* Events */ + +/** + * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout. + * @event set + * @param {OO.ui.CardLayout} card Current card + */ + +/** + * An 'add' event is emitted when cards are {@link #addCards added} to the index layout. + * + * @event add + * @param {OO.ui.CardLayout[]} card Added cards + * @param {number} index Index cards were added at + */ + +/** + * A 'remove' event is emitted when cards are {@link #clearCards cleared} or + * {@link #removeCards removed} from the index. + * + * @event remove + * @param {OO.ui.CardLayout[]} cards Removed cards + */ + +/* Methods */ + +/** + * Handle stack layout focus. + * + * @private + * @param {jQuery.Event} e Focusin 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 ); + break; + } + } +}; + +/** + * Handle stack layout set events. + * + * @private + * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel + */ +OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) { + var layout = this; + if ( card ) { + card.scrollElementIntoView( { complete: function () { + if ( layout.autoFocus ) { + layout.focus(); + } + } } ); + } +}; + +/** + * Focus the first input in the current card. + * + * 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. + * @param {number} [itemIndex] A specific item to focus on + */ +OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) { + var $input, card, + items = this.stackLayout.getItems(); + + if ( itemIndex !== undefined && items[ itemIndex ] ) { + card = items[ itemIndex ]; + } else { + card = this.stackLayout.getCurrentItem(); + } + + if ( !card ) { + this.selectFirstSelectableCard(); + card = this.stackLayout.getCurrentItem(); + } + if ( !card ) { + return; + } + // Only change the focus if is not already in the current card + if ( !card.$element.find( ':focus' ).length ) { + $input = card.$element.find( ':input:first' ); + if ( $input.length ) { + $input[ 0 ].focus(); + } + } +}; + +/** + * Find the first focusable input in the index layout and focus + * on it. + */ +OO.ui.IndexLayout.prototype.focusFirstFocusable = function () { + var i, len, + found = false, + items = this.stackLayout.getItems(), + checkAndFocus = function () { + if ( OO.ui.isFocusableElement( $( this ) ) ) { + $( this ).focus(); + found = true; + return false; + } + }; + + for ( i = 0, len = items.length; i < len; i++ ) { + if ( found ) { + break; + } + // Find all potentially focusable elements in the item + // and check if they are focusable + items[i].$element + .find( 'input, select, textarea, button, object' ) + .each( checkAndFocus ); + } +}; + +/** + * Handle tab widget select events. + * + * @private + * @param {OO.ui.OptionWidget|null} item Selected item + */ +OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) { + if ( item ) { + this.setCard( item.getData() ); + } +}; + +/** + * Get the card closest to the specified card. + * + * @param {OO.ui.CardLayout} card Card to use as a reference point + * @return {OO.ui.CardLayout|null} Card closest to the specified card + */ +OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) { + var next, prev, level, + cards = this.stackLayout.getItems(), + index = $.inArray( card, cards ); + + 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(); + if ( + prev && + level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel() + ) { + return prev; + } + if ( + next && + level === this.tabSelectWidget.getItemFromData( next.getName() ).getLevel() + ) { + return next; + } + } + return prev || next || null; +}; + +/** + * Get the tabs widget. + * + * @return {OO.ui.TabSelectWidget} Tabs widget + */ +OO.ui.IndexLayout.prototype.getTabs = function () { + return this.tabSelectWidget; +}; + +/** + * Get a card by its symbolic name. + * + * @param {string} name Symbolic name of card + * @return {OO.ui.CardLayout|undefined} Card, if found + */ +OO.ui.IndexLayout.prototype.getCard = function ( name ) { + return this.cards[ name ]; +}; + +/** + * Get the current card. + * + * @return {OO.ui.CardLayout|undefined} Current card, if found + */ +OO.ui.IndexLayout.prototype.getCurrentCard = function () { + var name = this.getCurrentCardName(); + return name ? this.getCard( name ) : undefined; +}; + +/** + * Get the symbolic name of the current card. + * + * @return {string|null} Symbolic name of the current card + */ +OO.ui.IndexLayout.prototype.getCurrentCardName = function () { + return this.currentCardName; +}; + +/** + * Add cards 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. + * + * @param {OO.ui.CardLayout[]} cards Cards 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(), + remove = [], + items = []; + + // Remove cards with same names + for ( i = 0, len = cards.length; i < len; i++ ) { + card = cards[ i ]; + name = card.getName(); + + if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) { + // Correct the insertion index + currentIndex = $.inArray( this.cards[ name ], stackLayoutCards ); + if ( currentIndex !== -1 && currentIndex + 1 < index ) { + index--; + } + remove.push( this.cards[ name ] ); + } + } + if ( remove.length ) { + this.removeCards( remove ); + } + + // Add new cards + for ( i = 0, len = cards.length; i < len; i++ ) { + card = cards[ i ]; + name = card.getName(); + this.cards[ card.getName() ] = card; + item = new OO.ui.TabOptionWidget( { data: name } ); + card.setTabItem( item ); + items.push( item ); + } + + if ( items.length ) { + this.tabSelectWidget.addItems( items, index ); + this.selectFirstSelectableCard(); + } + this.stackLayout.addItems( cards, index ); + this.emit( 'add', cards, index ); + + return this; +}; + +/** + * Remove the specified cards from the index layout. + * + * To remove all cards from the index, you may wish to use the #clearCards method instead. + * + * @param {OO.ui.CardLayout[]} cards An array of cards to remove + * @fires remove + * @chainable + */ +OO.ui.IndexLayout.prototype.removeCards = function ( cards ) { + var i, len, name, card, + items = []; + + for ( i = 0, len = cards.length; i < len; i++ ) { + card = cards[ i ]; + name = card.getName(); + delete this.cards[ name ]; + items.push( this.tabSelectWidget.getItemFromData( name ) ); + card.setTabItem( null ); + } + if ( items.length ) { + this.tabSelectWidget.removeItems( items ); + this.selectFirstSelectableCard(); + } + this.stackLayout.removeItems( cards ); + this.emit( 'remove', cards ); + + return this; +}; + +/** + * Clear all cards from the index layout. + * + * To remove only a subset of cards from the index, use the #removeCards method. + * + * @fires remove + * @chainable + */ +OO.ui.IndexLayout.prototype.clearCards = function () { + var i, len, + cards = this.stackLayout.getItems(); + + this.cards = {}; + this.currentCardName = null; + this.tabSelectWidget.clearItems(); + for ( i = 0, len = cards.length; i < len; i++ ) { + cards[ i ].setTabItem( null ); + } + this.stackLayout.clearItems(); + + this.emit( 'remove', cards ); + + return this; +}; + +/** + * Set the current card by symbolic name. + * + * @fires set + * @param {string} name Symbolic name of card + */ +OO.ui.IndexLayout.prototype.setCard = function ( name ) { + var selectedItem, + $focused, + card = this.cards[ name ]; + + if ( name !== this.currentCardName ) { + selectedItem = this.tabSelectWidget.getSelectedItem(); + if ( selectedItem && selectedItem.getData() !== name ) { + this.tabSelectWidget.selectItem( this.tabSelectWidget.getItemFromData( name ) ); + } + if ( card ) { + if ( this.currentCardName && this.cards[ this.currentCardName ] ) { + this.cards[ this.currentCardName ].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 + // this blur happens automatically + if ( this.autoFocus && !card.$element.find( ':input' ).length ) { + $focused = this.cards[ this.currentCardName ].$element.find( ':focus' ); + if ( $focused.length ) { + $focused[ 0 ].blur(); + } + } + } + this.currentCardName = name; + this.stackLayout.setItem( card ); + card.setActive( true ); + this.emit( 'set', card ); + } + } +}; + +/** + * Select the first selectable card. + * + * @chainable + */ +OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () { + if ( !this.tabSelectWidget.getSelectedItem() ) { + this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() ); + } + + return this; +}; diff --git a/src/styles/core.less b/src/styles/core.less index 585109e..fc1c91f 100644 --- a/src/styles/core.less +++ b/src/styles/core.less @@ -33,12 +33,14 @@ @import 'Layout.less'; @import 'layouts/BookletLayout.less'; +@import 'layouts/IndexLayout.less'; @import 'layouts/FieldLayout.less'; @import 'layouts/ActionFieldLayout.less'; @import 'layouts/FieldsetLayout.less'; @import 'layouts/FormLayout.less'; @import 'layouts/MenuLayout.less'; @import 'layouts/PanelLayout.less'; +@import 'layouts/CardLayout.less'; @import 'layouts/PageLayout.less'; @import 'layouts/StackLayout.less'; @@ -98,6 +100,9 @@ @import 'widgets/OutlineOptionWidget.less'; @import 'widgets/OutlineControlsWidget.less'; +@import 'widgets/TabSelectWidget.less'; +@import 'widgets/TabOptionWidget.less'; + @import 'widgets/ComboBoxWidget.less'; @import 'widgets/SearchWidget.less'; diff --git a/src/styles/layouts/CardLayout.less b/src/styles/layouts/CardLayout.less new file mode 100644 index 0000000..4fdb20b --- /dev/null +++ b/src/styles/layouts/CardLayout.less @@ -0,0 +1,5 @@ +@import '../common'; + +.oo-ui-cardLayout { + .theme-oo-ui-cardLayout(); +} diff --git a/src/styles/layouts/IndexLayout.less b/src/styles/layouts/IndexLayout.less new file mode 100644 index 0000000..63f3f14 --- /dev/null +++ b/src/styles/layouts/IndexLayout.less @@ -0,0 +1,13 @@ +@import '../common'; + +.oo-ui-indexLayout { + > .oo-ui-menuLayout-menu { + height: 3em; + } + + > .oo-ui-menuLayout-content { + top: 3em; + } + + .theme-oo-ui-indexLayout(); +} diff --git a/src/styles/theme.less b/src/styles/theme.less index b306c62..2cd4c0e 100644 --- a/src/styles/theme.less +++ b/src/styles/theme.less @@ -33,12 +33,14 @@ .theme-oo-ui-processDialog () {} .theme-oo-ui-bookletLayout () {} +.theme-oo-ui-indexLayout () {} .theme-oo-ui-fieldLayout () {} .theme-oo-ui-actionFieldLayout () {} .theme-oo-ui-fieldsetLayout () {} .theme-oo-ui-formLayout () {} .theme-oo-ui-menuLayout () {} .theme-oo-ui-panelLayout () {} +.theme-oo-ui-cardLayout () {} .theme-oo-ui-pageLayout () {} .theme-oo-ui-stackLayout () {} @@ -76,6 +78,7 @@ .theme-oo-ui-menuOptionWidget () {} .theme-oo-ui-menuSectionOptionWidget () {} .theme-oo-ui-outlineOptionWidget () {} +.theme-oo-ui-tabOptionWidget () {} .theme-oo-ui-popupWidget () {} .theme-oo-ui-searchWidget () {} .theme-oo-ui-selectWidget () {} @@ -84,4 +87,5 @@ .theme-oo-ui-menuSelectWidget () {} .theme-oo-ui-textInputMenuSelectWidget () {} .theme-oo-ui-outlineSelectWidget () {} +.theme-oo-ui-tabSelectWidget () {} .theme-oo-ui-toggleSwitchWidget () {} diff --git a/src/styles/widgets/TabOptionWidget.less b/src/styles/widgets/TabOptionWidget.less new file mode 100644 index 0000000..0b83154 --- /dev/null +++ b/src/styles/widgets/TabOptionWidget.less @@ -0,0 +1,8 @@ +@import '../common'; + +.oo-ui-tabOptionWidget { + display: inline-block; + vertical-align: bottom; + + .theme-oo-ui-tabOptionWidget(); +} diff --git a/src/styles/widgets/TabSelectWidget.less b/src/styles/widgets/TabSelectWidget.less new file mode 100644 index 0000000..b3a57c9 --- /dev/null +++ b/src/styles/widgets/TabSelectWidget.less @@ -0,0 +1,9 @@ +@import '../common'; + +.oo-ui-tabSelectWidget { + text-align: left; + white-space: nowrap; + overflow: hidden; + + .theme-oo-ui-tabSelectWidget(); +} diff --git a/src/themes/apex/layouts.less b/src/themes/apex/layouts.less index eb8cacf..b86aa01 100644 --- a/src/themes/apex/layouts.less +++ b/src/themes/apex/layouts.less @@ -18,6 +18,14 @@ } } +.theme-oo-ui-indexLayout () { + &-stackLayout { + > .oo-ui-panelLayout { + padding: 1.5em; + } + } +} + .theme-oo-ui-fieldLayout () { margin-bottom: 1em; @@ -120,6 +128,8 @@ } } +.theme-oo-ui-cardLayout () {} + .theme-oo-ui-pageLayout () {} .theme-oo-ui-stackLayout () {} diff --git a/src/themes/apex/widgets.less b/src/themes/apex/widgets.less index 503c214..d868635 100644 --- a/src/themes/apex/widgets.less +++ b/src/themes/apex/widgets.less @@ -525,6 +525,33 @@ } } +.theme-oo-ui-tabOptionWidget () { + padding: 0.5em 1em; + margin: 0.5em 0 0 0.75em; + border: 1px solid transparent; + border-bottom: none; + border-top-left-radius: 0.5em; + border-top-right-radius: 0.5em; + + &.oo-ui-indicatorElement .oo-ui-labelElement-label { + padding-right: 1.5em; + } + + &.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator { + opacity: 0.5; + } + + .oo-ui-selectWidget-pressed &.oo-ui-optionWidget-pressed { + background-color: transparent; + } + + .oo-ui-selectWidget-pressed &.oo-ui-optionWidget-selected, + .oo-ui-selectWidget-depressed &.oo-ui-optionWidget-selected { + background-color: #fff; + border-color: #ddd; + } +} + .theme-oo-ui-popupWidget () { &-popup { border: 1px solid #ccc; @@ -649,6 +676,11 @@ .theme-oo-ui-outlineSelectWidget () {} +.theme-oo-ui-tabSelectWidget () { + background-color: #eee; + box-shadow: inset 0 -0.015em 0.1em rgba(0, 0, 0, 0.1); +} + .theme-oo-ui-toggleSwitchWidget () { @travelDistance: 2em; height: 2em; diff --git a/src/themes/blank/layouts.less b/src/themes/blank/layouts.less index 3658625..93f677c 100644 --- a/src/themes/blank/layouts.less +++ b/src/themes/blank/layouts.less @@ -4,6 +4,8 @@ .theme-oo-ui-bookletLayout () {} +.theme-oo-ui-indexLayout () {} + .theme-oo-ui-fieldLayout () {} .theme-oo-ui-actionFieldLayout () {} @@ -16,6 +18,8 @@ .theme-oo-ui-panelLayout () {} +.theme-oo-ui-cardLayout () {} + .theme-oo-ui-pageLayout () {} .theme-oo-ui-stackLayout () {} diff --git a/src/themes/blank/widgets.less b/src/themes/blank/widgets.less index d81bbde..68c64cd 100644 --- a/src/themes/blank/widgets.less +++ b/src/themes/blank/widgets.less @@ -52,6 +52,8 @@ .theme-oo-ui-outlineOptionWidget () {} +.theme-oo-ui-tabOptionWidget () {} + .theme-oo-ui-popupWidget () {} .theme-oo-ui-searchWidget () {} @@ -68,6 +70,8 @@ .theme-oo-ui-outlineSelectWidget () {} +.theme-oo-ui-tabSelectWidget () {} + .theme-oo-ui-toggleSwitchWidget () {} .theme-oo-ui-progressBarWidget () {} diff --git a/src/themes/mediawiki/layouts.less b/src/themes/mediawiki/layouts.less index f2da23a..c4956f3 100644 --- a/src/themes/mediawiki/layouts.less +++ b/src/themes/mediawiki/layouts.less @@ -18,6 +18,14 @@ } } +.theme-oo-ui-indexLayout () { + &-stackLayout { + > .oo-ui-panelLayout { + padding: 1.5em; + } + } +} + .theme-oo-ui-fieldLayout () { margin-bottom: 1em; diff --git a/src/themes/mediawiki/widgets.less b/src/themes/mediawiki/widgets.less index c31235c..755f422 100644 --- a/src/themes/mediawiki/widgets.less +++ b/src/themes/mediawiki/widgets.less @@ -707,12 +707,46 @@ } } +.theme-oo-ui-tabOptionWidget () { + padding: 0.35em 1em; + margin: 0.5em 0 0 0.75em; + border: 1px solid transparent; + border-bottom: none; + border-top-left-radius: @border-radius; + border-top-right-radius: @border-radius; + color: #666; + font-weight: bold; + + &:hover { + background-color: rgba(255, 255, 255, 0.3); + } + + &:active { + background-color: rgba(255, 255, 255, 0.8); + } + + &.oo-ui-indicatorElement .oo-ui-labelElement-label { + padding-right: 1.5em; + } + + &.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator { + opacity: 0.5; + } + + .oo-ui-selectWidget-pressed &.oo-ui-optionWidget-selected, + .oo-ui-selectWidget-depressed &.oo-ui-optionWidget-selected, + &.oo-ui-optionWidget-selected:hover { + background-color: #fff; + color: #333; + } +} + .theme-oo-ui-popupWidget () { &-popup { border: 1px solid #aaa; border-radius: 0.2em; background-color: #fff; - box-shadow: inset 0 -0.2em 0 0 rgba(0,0,0,0.2); + box-shadow: inset 0 -0.2em 0 0 rgba(0, 0, 0, 0.2); } @anchor-size: 9px; @@ -841,13 +875,17 @@ border: 1px solid #aaa; border-radius: 0 0 0.2em 0.2em; padding-bottom: 0.25em; - box-shadow: inset 0 -0.2em 0 0 rgba(0,0,0,0.2), 0 0.1em 0 0 rgba(0,0,0,0.2); + box-shadow: inset 0 -0.2em 0 0 rgba(0, 0, 0, 0.2), 0 0.1em 0 0 rgba(0, 0, 0, 0.2); } .theme-oo-ui-textInputMenuSelectWidget () {} .theme-oo-ui-outlineSelectWidget () {} +.theme-oo-ui-tabSelectWidget () { + background-color: #ddd; +} + .theme-oo-ui-toggleSwitchWidget () { @travelDistance: 2em; height: 2em; diff --git a/src/widgets/OutlineControlsWidget.js b/src/widgets/OutlineControlsWidget.js index f92cf66..0219a44 100644 --- a/src/widgets/OutlineControlsWidget.js +++ b/src/widgets/OutlineControlsWidget.js @@ -1,7 +1,7 @@ /** * 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. - * ####Currently, this class is only used by {@link OO.ui.BookletLayout BookletLayouts}.#### + * ####Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.#### * * @class * @extends OO.ui.Widget diff --git a/src/widgets/OutlineSelectWidget.js b/src/widgets/OutlineSelectWidget.js index dd76823..cd7db54 100644 --- a/src/widgets/OutlineSelectWidget.js +++ b/src/widgets/OutlineSelectWidget.js @@ -2,7 +2,7 @@ * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options} * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget. * - * ####Currently, this class is only used by {@link OO.ui.BookletLayout BookletLayouts}.#### + * ####Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.#### * * @class * @extends OO.ui.SelectWidget diff --git a/src/widgets/TabOptionWidget.js b/src/widgets/TabOptionWidget.js new file mode 100644 index 0000000..7986646 --- /dev/null +++ b/src/widgets/TabOptionWidget.js @@ -0,0 +1,31 @@ +/** + * TabOptionWidget is an item in an {@link OO.ui.TabSelectWidget TabSelectWidget}. + * + * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain + * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.IndexLayout IndexLayout} + * for an example. + * + * @class + * @extends OO.ui.OptionWidget + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.TabOptionWidget.super.call( this, config ); + + // Initialization + this.$element.addClass( 'oo-ui-tabOptionWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget ); + +/* Static Properties */ + +OO.ui.TabOptionWidget.static.highlightable = false; diff --git a/src/widgets/TabSelectWidget.js b/src/widgets/TabSelectWidget.js new file mode 100644 index 0000000..ced3218 --- /dev/null +++ b/src/widgets/TabSelectWidget.js @@ -0,0 +1,33 @@ +/** + * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options} + * + * ####Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.#### + * + * @class + * @extends OO.ui.SelectWidget + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) { + // Parent constructor + OO.ui.TabSelectWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.TabIndexedElement.call( this, config ); + + // Events + this.$element.on( { + focus: this.bindKeyDownListener.bind( this ), + blur: this.unbindKeyDownListener.bind( this ) + } ); + + // Initialization + this.$element.addClass( 'oo-ui-tabSelectWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget ); +OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.TabIndexedElement ); -- To view, visit https://gerrit.wikimedia.org/r/208077 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I3deaa165d1d18a83dff13f469caeda63f34e32b1 Gerrit-PatchSet: 7 Gerrit-Project: oojs/ui Gerrit-Branch: master Gerrit-Owner: Trevor Parscal <tpars...@wikimedia.org> Gerrit-Reviewer: Bartosz DziewoĆski <matma....@gmail.com> Gerrit-Reviewer: Esanders <esand...@wikimedia.org> Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org> Gerrit-Reviewer: Krinkle <krinklem...@gmail.com> Gerrit-Reviewer: Nirzar <npangar...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits