summaryrefslogtreecommitdiff
path: root/vendor/oojs/oojs-ui/src/layouts
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2015-06-04 07:31:04 +0200
committerPierre Schmitz <pierre@archlinux.de>2015-06-04 07:58:39 +0200
commitf6d65e533c62f6deb21342d4901ece24497b433e (patch)
treef28adf0362d14bcd448f7b65a7aaf38650f923aa /vendor/oojs/oojs-ui/src/layouts
parentc27b2e832fe25651ef2410fae85b41072aae7519 (diff)
Update to MediaWiki 1.25.1
Diffstat (limited to 'vendor/oojs/oojs-ui/src/layouts')
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/ActionFieldLayout.js81
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/BookletLayout.js542
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/CardLayout.js138
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/FieldLayout.js160
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/FieldsetLayout.js86
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/FormLayout.js119
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/IndexLayout.js452
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/MenuLayout.js156
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/PageLayout.js138
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/PanelLayout.js55
-rw-r--r--vendor/oojs/oojs-ui/src/layouts/StackLayout.js214
11 files changed, 2141 insertions, 0 deletions
diff --git a/vendor/oojs/oojs-ui/src/layouts/ActionFieldLayout.js b/vendor/oojs/oojs-ui/src/layouts/ActionFieldLayout.js
new file mode 100644
index 00000000..59640ed9
--- /dev/null
+++ b/vendor/oojs/oojs-ui/src/layouts/ActionFieldLayout.js
@@ -0,0 +1,81 @@
+/**
+ * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
+ * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
+ * is required and is specified before any optional configuration settings.
+ *
+ * Labels can be aligned in one of four ways:
+ *
+ * - **left**: The label is placed before the field-widget and aligned with the left margin.
+ * A left-alignment is used for forms with many fields.
+ * - **right**: The label is placed before the field-widget and aligned to the right margin.
+ * A right-alignment is used for long but familiar forms which users tab through,
+ * verifying the current field with a quick glance at the label.
+ * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
+ * that users fill out from top to bottom.
+ * - **inline**: The label is placed after the field-widget and aligned to the left.
+ * An inline-alignment is best used with checkboxes or radio buttons.
+ *
+ * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
+ * text is specified.
+ *
+ * @example
+ * // Example of an ActionFieldLayout
+ * var actionFieldLayout = new OO.ui.ActionFieldLayout(
+ * new OO.ui.TextInputWidget( {
+ * placeholder: 'Field widget'
+ * } ),
+ * new OO.ui.ButtonWidget( {
+ * label: 'Button'
+ * } ),
+ * {
+ * label: 'An ActionFieldLayout. This label is aligned top',
+ * align: 'top',
+ * help: 'This is help text'
+ * }
+ * );
+ *
+ * $( 'body' ).append( actionFieldLayout.$element );
+ *
+ *
+ * @class
+ * @extends OO.ui.FieldLayout
+ *
+ * @constructor
+ * @param {OO.ui.Widget} fieldWidget Field widget
+ * @param {OO.ui.ButtonWidget} buttonWidget Button widget
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
+ * @cfg {string} [help] Help text. When help text is specified, a help icon will appear in the
+ * upper-right corner of the rendered field.
+ */
+OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
+ config = fieldWidget;
+ fieldWidget = config.fieldWidget;
+ buttonWidget = config.buttonWidget;
+ }
+
+ // Configuration initialization
+ config = $.extend( { align: 'left' }, config );
+
+ // Parent constructor
+ OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
+
+ // Properties
+ this.fieldWidget = fieldWidget;
+ this.buttonWidget = buttonWidget;
+ this.$button = $( '<div>' )
+ .addClass( 'oo-ui-actionFieldLayout-button' )
+ .append( this.buttonWidget.$element );
+ this.$input = $( '<div>' )
+ .addClass( 'oo-ui-actionFieldLayout-input' )
+ .append( this.fieldWidget.$element );
+ this.$field
+ .addClass( 'oo-ui-actionFieldLayout' )
+ .append( this.$input, this.$button );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
diff --git a/vendor/oojs/oojs-ui/src/layouts/BookletLayout.js b/vendor/oojs/oojs-ui/src/layouts/BookletLayout.js
new file mode 100644
index 00000000..eebf57d6
--- /dev/null
+++ b/vendor/oojs/oojs-ui/src/layouts/BookletLayout.js
@@ -0,0 +1,542 @@
+/**
+ * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
+ * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
+ * through the pages and select which one to display. By default, only one page is
+ * displayed at a time and the outline is hidden. When a user navigates to a new page,
+ * the booklet layout automatically focuses on the first focusable element, unless the
+ * default setting is changed. Optionally, booklets can be configured to show
+ * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
+ *
+ * @example
+ * // Example of a BookletLayout that contains two PageLayouts.
+ *
+ * function PageOneLayout( name, config ) {
+ * PageOneLayout.super.call( this, name, config );
+ * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
+ * }
+ * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
+ * PageOneLayout.prototype.setupOutlineItem = function () {
+ * this.outlineItem.setLabel( 'Page One' );
+ * };
+ *
+ * function PageTwoLayout( name, config ) {
+ * PageTwoLayout.super.call( this, name, config );
+ * this.$element.append( '<p>Second page</p>' );
+ * }
+ * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
+ * PageTwoLayout.prototype.setupOutlineItem = function () {
+ * this.outlineItem.setLabel( 'Page Two' );
+ * };
+ *
+ * var page1 = new PageOneLayout( 'one' ),
+ * page2 = new PageTwoLayout( 'two' );
+ *
+ * var booklet = new OO.ui.BookletLayout( {
+ * outlined: true
+ * } );
+ *
+ * booklet.addPages ( [ page1, page2 ] );
+ * $( 'body' ).append( booklet.$element );
+ *
+ * @class
+ * @extends OO.ui.MenuLayout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [continuous=false] Show all pages, one after another
+ * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
+ * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
+ * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
+ */
+OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.BookletLayout.super.call( this, config );
+
+ // Properties
+ this.currentPageName = null;
+ this.pages = {};
+ 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.outlineVisible = false;
+ this.outlined = !!config.outlined;
+ if ( this.outlined ) {
+ this.editable = !!config.editable;
+ this.outlineControlsWidget = null;
+ this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
+ this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
+ this.$menu.append( this.outlinePanel.$element );
+ this.outlineVisible = true;
+ if ( this.editable ) {
+ this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
+ this.outlineSelectWidget
+ );
+ }
+ }
+ this.toggleMenu( this.outlined );
+
+ // Events
+ this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
+ if ( this.outlined ) {
+ this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
+ }
+ 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-bookletLayout' );
+ this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
+ if ( this.outlined ) {
+ this.outlinePanel.$element
+ .addClass( 'oo-ui-bookletLayout-outlinePanel' )
+ .append( this.outlineSelectWidget.$element );
+ if ( this.editable ) {
+ this.outlinePanel.$element
+ .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
+ .append( this.outlineControlsWidget.$element );
+ }
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
+
+/* Events */
+
+/**
+ * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
+ * @event set
+ * @param {OO.ui.PageLayout} page Current page
+ */
+
+/**
+ * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
+ *
+ * @event add
+ * @param {OO.ui.PageLayout[]} page Added pages
+ * @param {number} index Index pages were added at
+ */
+
+/**
+ * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
+ * {@link #removePages removed} from the booklet.
+ *
+ * @event remove
+ * @param {OO.ui.PageLayout[]} pages Removed pages
+ */
+
+/* Methods */
+
+/**
+ * Handle stack layout focus.
+ *
+ * @private
+ * @param {jQuery.Event} e Focusin event
+ */
+OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
+ var name, $target;
+
+ // Find the page that an element was focused within
+ $target = $( e.target ).closest( '.oo-ui-pageLayout' );
+ for ( name in this.pages ) {
+ // Check for page match, exclude current page to find only page changes
+ if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
+ this.setPage( name );
+ break;
+ }
+ }
+};
+
+/**
+ * Handle stack layout set events.
+ *
+ * @private
+ * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
+ */
+OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
+ var layout = this;
+ if ( page ) {
+ page.scrollElementIntoView( { complete: function () {
+ if ( layout.autoFocus ) {
+ layout.focus();
+ }
+ } } );
+ }
+};
+
+/**
+ * Focus the first input in the current page.
+ *
+ * If no page is selected, the first selectable page will be selected.
+ * If the focus is already in an element on the current page, nothing will happen.
+ * @param {number} [itemIndex] A specific item to focus on
+ */
+OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
+ var $input, page,
+ items = this.stackLayout.getItems();
+
+ if ( itemIndex !== undefined && items[ itemIndex ] ) {
+ page = items[ itemIndex ];
+ } else {
+ page = this.stackLayout.getCurrentItem();
+ }
+
+ if ( !page && this.outlined ) {
+ this.selectFirstSelectablePage();
+ page = this.stackLayout.getCurrentItem();
+ }
+ if ( !page ) {
+ return;
+ }
+ // Only change the focus if is not already in the current page
+ if ( !page.$element.find( ':focus' ).length ) {
+ $input = page.$element.find( ':input:first' );
+ if ( $input.length ) {
+ $input[ 0 ].focus();
+ }
+ }
+};
+
+/**
+ * Find the first focusable input in the booklet layout and focus
+ * on it.
+ */
+OO.ui.BookletLayout.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' )
+ /* jshint loopfunc:true */
+ .each( checkAndFocus );
+ }
+};
+
+/**
+ * Handle outline widget select events.
+ *
+ * @private
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
+ if ( item ) {
+ this.setPage( item.getData() );
+ }
+};
+
+/**
+ * Check if booklet has an outline.
+ *
+ * @return {boolean} Booklet has an outline
+ */
+OO.ui.BookletLayout.prototype.isOutlined = function () {
+ return this.outlined;
+};
+
+/**
+ * Check if booklet has editing controls.
+ *
+ * @return {boolean} Booklet is editable
+ */
+OO.ui.BookletLayout.prototype.isEditable = function () {
+ return this.editable;
+};
+
+/**
+ * Check if booklet has a visible outline.
+ *
+ * @return {boolean} Outline is visible
+ */
+OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
+ return this.outlined && this.outlineVisible;
+};
+
+/**
+ * Hide or show the outline.
+ *
+ * @param {boolean} [show] Show outline, omit to invert current state
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
+ if ( this.outlined ) {
+ show = show === undefined ? !this.outlineVisible : !!show;
+ this.outlineVisible = show;
+ this.toggleMenu( show );
+ }
+
+ return this;
+};
+
+/**
+ * Get the page closest to the specified page.
+ *
+ * @param {OO.ui.PageLayout} page Page to use as a reference point
+ * @return {OO.ui.PageLayout|null} Page closest to the specified page
+ */
+OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
+ var next, prev, level,
+ pages = this.stackLayout.getItems(),
+ index = $.inArray( page, pages );
+
+ if ( index !== -1 ) {
+ next = pages[ index + 1 ];
+ prev = pages[ index - 1 ];
+ // Prefer adjacent pages at the same level
+ if ( this.outlined ) {
+ level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
+ if (
+ prev &&
+ level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
+ ) {
+ return prev;
+ }
+ if (
+ next &&
+ level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
+ ) {
+ return next;
+ }
+ }
+ }
+ return prev || next || null;
+};
+
+/**
+ * Get the outline widget.
+ *
+ * If the booklet is not outlined, the method will return `null`.
+ *
+ * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
+ */
+OO.ui.BookletLayout.prototype.getOutline = function () {
+ return this.outlineSelectWidget;
+};
+
+/**
+ * Get the outline controls widget.
+ *
+ * If the outline is not editable, the method will return `null`.
+ *
+ * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
+ */
+OO.ui.BookletLayout.prototype.getOutlineControls = function () {
+ return this.outlineControlsWidget;
+};
+
+/**
+ * Get a page by its symbolic name.
+ *
+ * @param {string} name Symbolic name of page
+ * @return {OO.ui.PageLayout|undefined} Page, if found
+ */
+OO.ui.BookletLayout.prototype.getPage = function ( name ) {
+ return this.pages[ name ];
+};
+
+/**
+ * Get the current page.
+ *
+ * @return {OO.ui.PageLayout|undefined} Current page, if found
+ */
+OO.ui.BookletLayout.prototype.getCurrentPage = function () {
+ var name = this.getCurrentPageName();
+ return name ? this.getPage( name ) : undefined;
+};
+
+/**
+ * Get the symbolic name of the current page.
+ *
+ * @return {string|null} Symbolic name of the current page
+ */
+OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
+ return this.currentPageName;
+};
+
+/**
+ * Add pages to the booklet layout
+ *
+ * When pages are added with the same names as existing pages, the existing pages will be
+ * automatically removed before the new pages are added.
+ *
+ * @param {OO.ui.PageLayout[]} pages Pages to add
+ * @param {number} index Index of the insertion point
+ * @fires add
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
+ var i, len, name, page, item, currentIndex,
+ stackLayoutPages = this.stackLayout.getItems(),
+ remove = [],
+ items = [];
+
+ // Remove pages with same names
+ for ( i = 0, len = pages.length; i < len; i++ ) {
+ page = pages[ i ];
+ name = page.getName();
+
+ if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
+ // Correct the insertion index
+ currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
+ if ( currentIndex !== -1 && currentIndex + 1 < index ) {
+ index--;
+ }
+ remove.push( this.pages[ name ] );
+ }
+ }
+ if ( remove.length ) {
+ this.removePages( remove );
+ }
+
+ // Add new pages
+ for ( i = 0, len = pages.length; i < len; i++ ) {
+ page = pages[ i ];
+ name = page.getName();
+ this.pages[ page.getName() ] = page;
+ if ( this.outlined ) {
+ item = new OO.ui.OutlineOptionWidget( { data: name } );
+ page.setOutlineItem( item );
+ items.push( item );
+ }
+ }
+
+ if ( this.outlined && items.length ) {
+ this.outlineSelectWidget.addItems( items, index );
+ this.selectFirstSelectablePage();
+ }
+ this.stackLayout.addItems( pages, index );
+ this.emit( 'add', pages, index );
+
+ return this;
+};
+
+/**
+ * Remove the specified pages from the booklet layout.
+ *
+ * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
+ *
+ * @param {OO.ui.PageLayout[]} pages An array of pages to remove
+ * @fires remove
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
+ var i, len, name, page,
+ items = [];
+
+ for ( i = 0, len = pages.length; i < len; i++ ) {
+ page = pages[ i ];
+ name = page.getName();
+ delete this.pages[ name ];
+ if ( this.outlined ) {
+ items.push( this.outlineSelectWidget.getItemFromData( name ) );
+ page.setOutlineItem( null );
+ }
+ }
+ if ( this.outlined && items.length ) {
+ this.outlineSelectWidget.removeItems( items );
+ this.selectFirstSelectablePage();
+ }
+ this.stackLayout.removeItems( pages );
+ this.emit( 'remove', pages );
+
+ return this;
+};
+
+/**
+ * Clear all pages from the booklet layout.
+ *
+ * To remove only a subset of pages from the booklet, use the #removePages method.
+ *
+ * @fires remove
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.clearPages = function () {
+ var i, len,
+ pages = this.stackLayout.getItems();
+
+ this.pages = {};
+ this.currentPageName = null;
+ if ( this.outlined ) {
+ this.outlineSelectWidget.clearItems();
+ for ( i = 0, len = pages.length; i < len; i++ ) {
+ pages[ i ].setOutlineItem( null );
+ }
+ }
+ this.stackLayout.clearItems();
+
+ this.emit( 'remove', pages );
+
+ return this;
+};
+
+/**
+ * Set the current page by symbolic name.
+ *
+ * @fires set
+ * @param {string} name Symbolic name of page
+ */
+OO.ui.BookletLayout.prototype.setPage = function ( name ) {
+ var selectedItem,
+ $focused,
+ page = this.pages[ name ];
+
+ if ( name !== this.currentPageName ) {
+ if ( this.outlined ) {
+ selectedItem = this.outlineSelectWidget.getSelectedItem();
+ if ( selectedItem && selectedItem.getData() !== name ) {
+ this.outlineSelectWidget.selectItemByData( name );
+ }
+ }
+ if ( page ) {
+ if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
+ this.pages[ this.currentPageName ].setActive( false );
+ // Blur anything focused if the next page doesn't have anything focusable - this
+ // is not needed if the next page has something focusable because once it is focused
+ // this blur happens automatically
+ if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
+ $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
+ if ( $focused.length ) {
+ $focused[ 0 ].blur();
+ }
+ }
+ }
+ this.currentPageName = name;
+ this.stackLayout.setItem( page );
+ page.setActive( true );
+ this.emit( 'set', page );
+ }
+ }
+};
+
+/**
+ * Select the first selectable page.
+ *
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
+ if ( !this.outlineSelectWidget.getSelectedItem() ) {
+ this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
+ }
+
+ return this;
+};
diff --git a/vendor/oojs/oojs-ui/src/layouts/CardLayout.js b/vendor/oojs/oojs-ui/src/layouts/CardLayout.js
new file mode 100644
index 00000000..353aedc9
--- /dev/null
+++ b/vendor/oojs/oojs-ui/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 a {@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 a {@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/vendor/oojs/oojs-ui/src/layouts/FieldLayout.js b/vendor/oojs/oojs-ui/src/layouts/FieldLayout.js
new file mode 100644
index 00000000..86385741
--- /dev/null
+++ b/vendor/oojs/oojs-ui/src/layouts/FieldLayout.js
@@ -0,0 +1,160 @@
+/**
+ * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
+ * which is a widget that is specified by reference before any optional configuration settings.
+ *
+ * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
+ *
+ * - **left**: The label is placed before the field-widget and aligned with the left margin.
+ * A left-alignment is used for forms with many fields.
+ * - **right**: The label is placed before the field-widget and aligned to the right margin.
+ * A right-alignment is used for long but familiar forms which users tab through,
+ * verifying the current field with a quick glance at the label.
+ * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
+ * that users fill out from top to bottom.
+ * - **inline**: The label is placed after the field-widget and aligned to the left.
+ * An inline-alignment is best used with checkboxes or radio buttons.
+ *
+ * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.LabelElement
+ *
+ * @constructor
+ * @param {OO.ui.Widget} fieldWidget Field widget
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
+ * @cfg {string} [help] Help text. When help text is specified, a help icon will appear
+ * in the upper-right corner of the rendered field.
+ */
+OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
+ config = fieldWidget;
+ fieldWidget = config.fieldWidget;
+ }
+
+ var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
+
+ // Configuration initialization
+ config = $.extend( { align: 'left' }, config );
+
+ // Parent constructor
+ OO.ui.FieldLayout.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.LabelElement.call( this, config );
+
+ // Properties
+ this.fieldWidget = fieldWidget;
+ this.$field = $( '<div>' );
+ this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
+ this.align = null;
+ if ( config.help ) {
+ this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ classes: [ 'oo-ui-fieldLayout-help' ],
+ framed: false,
+ icon: 'info'
+ } );
+
+ this.popupButtonWidget.getPopup().$body.append(
+ $( '<div>' )
+ .text( config.help )
+ .addClass( 'oo-ui-fieldLayout-help-content' )
+ );
+ this.$help = this.popupButtonWidget.$element;
+ } else {
+ this.$help = $( [] );
+ }
+
+ // Events
+ if ( hasInputWidget ) {
+ this.$label.on( 'click', this.onLabelClick.bind( this ) );
+ }
+ this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-fieldLayout' )
+ .append( this.$help, this.$body );
+ this.$body.addClass( 'oo-ui-fieldLayout-body' );
+ this.$field
+ .addClass( 'oo-ui-fieldLayout-field' )
+ .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
+ .append( this.fieldWidget.$element );
+
+ this.setAlignment( config.align );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
+
+/* Methods */
+
+/**
+ * Handle field disable events.
+ *
+ * @private
+ * @param {boolean} value Field is disabled
+ */
+OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
+ this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
+};
+
+/**
+ * Handle label mouse click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.FieldLayout.prototype.onLabelClick = function () {
+ this.fieldWidget.simulateLabelClick();
+ return false;
+};
+
+/**
+ * Get the widget contained by the field.
+ *
+ * @return {OO.ui.Widget} Field widget
+ */
+OO.ui.FieldLayout.prototype.getField = function () {
+ return this.fieldWidget;
+};
+
+/**
+ * Set the field alignment mode.
+ *
+ * @private
+ * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
+ * @chainable
+ */
+OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
+ if ( value !== this.align ) {
+ // Default to 'left'
+ if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
+ value = 'left';
+ }
+ // Reorder elements
+ if ( value === 'inline' ) {
+ this.$body.append( this.$field, this.$label );
+ } else {
+ this.$body.append( this.$label, this.$field );
+ }
+ // Set classes. The following classes can be used here:
+ // * oo-ui-fieldLayout-align-left
+ // * oo-ui-fieldLayout-align-right
+ // * oo-ui-fieldLayout-align-top
+ // * oo-ui-fieldLayout-align-inline
+ if ( this.align ) {
+ this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
+ }
+ this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
+ this.align = value;
+ }
+
+ return this;
+};
diff --git a/vendor/oojs/oojs-ui/src/layouts/FieldsetLayout.js b/vendor/oojs/oojs-ui/src/layouts/FieldsetLayout.js
new file mode 100644
index 00000000..1a21e8e1
--- /dev/null
+++ b/vendor/oojs/oojs-ui/src/layouts/FieldsetLayout.js
@@ -0,0 +1,86 @@
+/**
+ * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
+ * which each contain an individual widget and, optionally, a label. Each Fieldset can be
+ * configured with a label as well. For more information and examples,
+ * please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Example of a fieldset layout
+ * var input1 = new OO.ui.TextInputWidget( {
+ * placeholder: 'A text input field'
+ * } );
+ *
+ * var input2 = new OO.ui.TextInputWidget( {
+ * placeholder: 'A text input field'
+ * } );
+ *
+ * var fieldset = new OO.ui.FieldsetLayout( {
+ * label: 'Example of a fieldset layout'
+ * } );
+ *
+ * fieldset.addItems( [
+ * new OO.ui.FieldLayout( input1, {
+ * label: 'Field One'
+ * } ),
+ * new OO.ui.FieldLayout( input2, {
+ * label: 'Field Two'
+ * } )
+ * ] );
+ * $( 'body' ).append( fieldset.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
+ *
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.LabelElement
+ * @mixins OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
+ */
+OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.FieldsetLayout.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.IconElement.call( this, config );
+ OO.ui.LabelElement.call( this, config );
+ OO.ui.GroupElement.call( this, config );
+
+ if ( config.help ) {
+ this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ classes: [ 'oo-ui-fieldsetLayout-help' ],
+ framed: false,
+ icon: 'info'
+ } );
+
+ this.popupButtonWidget.getPopup().$body.append(
+ $( '<div>' )
+ .text( config.help )
+ .addClass( 'oo-ui-fieldsetLayout-help-content' )
+ );
+ this.$help = this.popupButtonWidget.$element;
+ } else {
+ this.$help = $( [] );
+ }
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-fieldsetLayout' )
+ .prepend( this.$help, this.$icon, this.$label, this.$group );
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
diff --git a/vendor/oojs/oojs-ui/src/layouts/FormLayout.js b/vendor/oojs/oojs-ui/src/layouts/FormLayout.js
new file mode 100644
index 00000000..a131c169
--- /dev/null
+++ b/vendor/oojs/oojs-ui/src/layouts/FormLayout.js
@@ -0,0 +1,119 @@
+/**
+ * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
+ * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
+ * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
+ * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
+ * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
+ * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
+ * some fancier controls. Some controls have both regular and InputWidget variants, for example
+ * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
+ * often have simplified APIs to match the capabilities of HTML forms.
+ * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
+ *
+ * @example
+ * // Example of a form layout that wraps a fieldset layout
+ * var input1 = new OO.ui.TextInputWidget( {
+ * placeholder: 'Username'
+ * } );
+ * var input2 = new OO.ui.TextInputWidget( {
+ * placeholder: 'Password',
+ * type: 'password'
+ * } );
+ * var submit = new OO.ui.ButtonInputWidget( {
+ * label: 'Submit'
+ * } );
+ *
+ * var fieldset = new OO.ui.FieldsetLayout( {
+ * label: 'A form layout'
+ * } );
+ * fieldset.addItems( [
+ * new OO.ui.FieldLayout( input1, {
+ * label: 'Username',
+ * align: 'top'
+ * } ),
+ * new OO.ui.FieldLayout( input2, {
+ * label: 'Password',
+ * align: 'top'
+ * } ),
+ * new OO.ui.FieldLayout( submit )
+ * ] );
+ * var form = new OO.ui.FormLayout( {
+ * items: [ fieldset ],
+ * action: '/api/formhandler',
+ * method: 'get'
+ * } )
+ * $( 'body' ).append( form.$element );
+ *
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [method] HTML form `method` attribute
+ * @cfg {string} [action] HTML form `action` attribute
+ * @cfg {string} [enctype] HTML form `enctype` attribute
+ * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
+ */
+OO.ui.FormLayout = function OoUiFormLayout( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.FormLayout.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+
+ // Events
+ this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-formLayout' )
+ .attr( {
+ method: config.method,
+ action: config.action,
+ enctype: config.enctype
+ } );
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FormLayout, OO.ui.GroupElement );
+
+/* Events */
+
+/**
+ * A 'submit' event is emitted when the form is submitted.
+ *
+ * @event submit
+ */
+
+/* Static Properties */
+
+OO.ui.FormLayout.static.tagName = 'form';
+
+/* Methods */
+
+/**
+ * Handle form submit events.
+ *
+ * @private
+ * @param {jQuery.Event} e Submit event
+ * @fires submit
+ */
+OO.ui.FormLayout.prototype.onFormSubmit = function () {
+ if ( this.emit( 'submit' ) ) {
+ return false;
+ }
+};
diff --git a/vendor/oojs/oojs-ui/src/layouts/IndexLayout.js b/vendor/oojs/oojs-ui/src/layouts/IndexLayout.js
new file mode 100644
index 00000000..4cda00a9
--- /dev/null
+++ b/vendor/oojs/oojs-ui/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.selectItemByData( 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/vendor/oojs/oojs-ui/src/layouts/MenuLayout.js b/vendor/oojs/oojs-ui/src/layouts/MenuLayout.js
new file mode 100644
index 00000000..53937cc7
--- /dev/null
+++ b/vendor/oojs/oojs-ui/src/layouts/MenuLayout.js
@@ -0,0 +1,156 @@
+/**
+ * MenuLayouts combine a menu and a content {@link OO.ui.PanelLayout panel}. The menu is positioned relative to the content (after, before, top, or bottom)
+ * and its size is customized with the #menuSize config. The content area will fill all remaining space.
+ *
+ * @example
+ * var menuLayout = new OO.ui.MenuLayout( {
+ * position: 'top'
+ * } ),
+ * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
+ * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
+ * select = new OO.ui.SelectWidget( {
+ * items: [
+ * new OO.ui.OptionWidget( {
+ * data: 'before',
+ * label: 'Before',
+ * } ),
+ * new OO.ui.OptionWidget( {
+ * data: 'after',
+ * label: 'After',
+ * } ),
+ * new OO.ui.OptionWidget( {
+ * data: 'top',
+ * label: 'Top',
+ * } ),
+ * new OO.ui.OptionWidget( {
+ * data: 'bottom',
+ * label: 'Bottom',
+ * } )
+ * ]
+ * } ).on( 'select', function ( item ) {
+ * menuLayout.setMenuPosition( item.getData() );
+ * } );
+ *
+ * menuLayout.$menu.append(
+ * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
+ * );
+ * menuLayout.$content.append(
+ * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
+ * );
+ * $( 'body' ).append( menuLayout.$element );
+ *
+ * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
+ * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
+ * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
+ * may be omitted.
+ *
+ * .oo-ui-menuLayout-menu {
+ * height: 200px;
+ * width: 200px;
+ * }
+ * .oo-ui-menuLayout-content {
+ * top: 200px;
+ * left: 200px;
+ * right: 200px;
+ * bottom: 200px;
+ * }
+ *
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [showMenu=true] Show menu
+ * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
+ */
+OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
+ // Configuration initialization
+ config = $.extend( {
+ showMenu: true,
+ menuPosition: 'before'
+ }, config );
+
+ // Parent constructor
+ OO.ui.MenuLayout.super.call( this, config );
+
+ /**
+ * Menu DOM node
+ *
+ * @property {jQuery}
+ */
+ this.$menu = $( '<div>' );
+ /**
+ * Content DOM node
+ *
+ * @property {jQuery}
+ */
+ this.$content = $( '<div>' );
+
+ // Initialization
+ this.$menu
+ .addClass( 'oo-ui-menuLayout-menu' );
+ this.$content.addClass( 'oo-ui-menuLayout-content' );
+ this.$element
+ .addClass( 'oo-ui-menuLayout' )
+ .append( this.$content, this.$menu );
+ this.setMenuPosition( config.menuPosition );
+ this.toggleMenu( config.showMenu );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
+
+/* Methods */
+
+/**
+ * Toggle menu.
+ *
+ * @param {boolean} showMenu Show menu, omit to toggle
+ * @chainable
+ */
+OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
+ showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
+
+ if ( this.showMenu !== showMenu ) {
+ this.showMenu = showMenu;
+ this.$element
+ .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
+ .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
+ }
+
+ return this;
+};
+
+/**
+ * Check if menu is visible
+ *
+ * @return {boolean} Menu is visible
+ */
+OO.ui.MenuLayout.prototype.isMenuVisible = function () {
+ return this.showMenu;
+};
+
+/**
+ * Set menu position.
+ *
+ * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
+ * @throws {Error} If position value is not supported
+ * @chainable
+ */
+OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
+ this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
+ this.menuPosition = position;
+ this.$element.addClass( 'oo-ui-menuLayout-' + position );
+
+ return this;
+};
+
+/**
+ * Get menu position.
+ *
+ * @return {string} Menu position
+ */
+OO.ui.MenuLayout.prototype.getMenuPosition = function () {
+ return this.menuPosition;
+};
diff --git a/vendor/oojs/oojs-ui/src/layouts/PageLayout.js b/vendor/oojs/oojs-ui/src/layouts/PageLayout.js
new file mode 100644
index 00000000..4753b7db
--- /dev/null
+++ b/vendor/oojs/oojs-ui/src/layouts/PageLayout.js
@@ -0,0 +1,138 @@
+/**
+ * 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,
+ * rather extended to include the required content and functionality.
+ *
+ * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
+ * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
+ * {@link OO.ui.BookletLayout BookletLayout} for an example.
+ *
+ * @class
+ * @extends OO.ui.PanelLayout
+ *
+ * @constructor
+ * @param {string} name Unique symbolic name of page
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.PageLayout = function OoUiPageLayout( 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.PageLayout.super.call( this, config );
+
+ // Properties
+ this.name = name;
+ this.outlineItem = null;
+ this.active = false;
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-pageLayout' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
+
+/* Events */
+
+/**
+ * An 'active' event is emitted when the page becomes active. Pages become active when they are
+ * shown in a booklet layout that is configured to display only one page at a time.
+ *
+ * @event active
+ * @param {boolean} active Page is active
+ */
+
+/* Methods */
+
+/**
+ * Get the symbolic name of the page.
+ *
+ * @return {string} Symbolic name of page
+ */
+OO.ui.PageLayout.prototype.getName = function () {
+ return this.name;
+};
+
+/**
+ * Check if page is active.
+ *
+ * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
+ * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
+ *
+ * @return {boolean} Page is active
+ */
+OO.ui.PageLayout.prototype.isActive = function () {
+ return this.active;
+};
+
+/**
+ * Get outline item.
+ *
+ * The outline item allows users to access the page from the booklet's outline
+ * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
+ *
+ * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
+ */
+OO.ui.PageLayout.prototype.getOutlineItem = function () {
+ return this.outlineItem;
+};
+
+/**
+ * Set or unset the outline item.
+ *
+ * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
+ * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
+ * level), use #setupOutlineItem instead of this method.
+ *
+ * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
+ * @chainable
+ */
+OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
+ this.outlineItem = outlineItem || null;
+ if ( outlineItem ) {
+ this.setupOutlineItem();
+ }
+ return this;
+};
+
+/**
+ * Set up the outline item.
+ *
+ * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
+ * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
+ * the #setOutlineItem method instead.
+ *
+ * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
+ * @chainable
+ */
+OO.ui.PageLayout.prototype.setupOutlineItem = function () {
+ return this;
+};
+
+/**
+ * Set the page to its 'active' state.
+ *
+ * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
+ * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
+ * context, setting the active state on a page does nothing.
+ *
+ * @param {boolean} value Page is active
+ * @fires active
+ */
+OO.ui.PageLayout.prototype.setActive = function ( active ) {
+ active = !!active;
+
+ if ( active !== this.active ) {
+ this.active = active;
+ this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
+ this.emit( 'active', this.active );
+ }
+};
diff --git a/vendor/oojs/oojs-ui/src/layouts/PanelLayout.js b/vendor/oojs/oojs-ui/src/layouts/PanelLayout.js
new file mode 100644
index 00000000..9183f597
--- /dev/null
+++ b/vendor/oojs/oojs-ui/src/layouts/PanelLayout.js
@@ -0,0 +1,55 @@
+/**
+ * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
+ * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
+ *
+ * @example
+ * // Example of a panel layout
+ * var panel = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * framed: true,
+ * padded: true,
+ * $content: $( '<p>A panel layout with padding and a frame.</p>' )
+ * } );
+ * $( 'body' ).append( panel.$element );
+ *
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [scrollable=false] Allow vertical scrolling
+ * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
+ * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
+ * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
+ */
+OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
+ // Configuration initialization
+ config = $.extend( {
+ scrollable: false,
+ padded: false,
+ expanded: true,
+ framed: false
+ }, config );
+
+ // Parent constructor
+ OO.ui.PanelLayout.super.call( this, config );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-panelLayout' );
+ if ( config.scrollable ) {
+ this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
+ }
+ if ( config.padded ) {
+ this.$element.addClass( 'oo-ui-panelLayout-padded' );
+ }
+ if ( config.expanded ) {
+ this.$element.addClass( 'oo-ui-panelLayout-expanded' );
+ }
+ if ( config.framed ) {
+ this.$element.addClass( 'oo-ui-panelLayout-framed' );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
diff --git a/vendor/oojs/oojs-ui/src/layouts/StackLayout.js b/vendor/oojs/oojs-ui/src/layouts/StackLayout.js
new file mode 100644
index 00000000..58f35d31
--- /dev/null
+++ b/vendor/oojs/oojs-ui/src/layouts/StackLayout.js
@@ -0,0 +1,214 @@
+/**
+ * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
+ * at a time, though the stack layout can also be configured to show all contained panels, one after another,
+ * by setting the #continuous option to 'true'.
+ *
+ * @example
+ * // A stack layout with two panels, configured to be displayed continously
+ * var myStack = new OO.ui.StackLayout( {
+ * items: [
+ * new OO.ui.PanelLayout( {
+ * $content: $( '<p>Panel One</p>' ),
+ * padded: true,
+ * framed: true
+ * } ),
+ * new OO.ui.PanelLayout( {
+ * $content: $( '<p>Panel Two</p>' ),
+ * padded: true,
+ * framed: true
+ * } )
+ * ],
+ * continuous: true
+ * } );
+ * $( 'body' ).append( myStack.$element );
+ *
+ * @class
+ * @extends OO.ui.PanelLayout
+ * @mixins OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
+ * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
+ */
+OO.ui.StackLayout = function OoUiStackLayout( config ) {
+ // Configuration initialization
+ config = $.extend( { scrollable: true }, config );
+
+ // Parent constructor
+ OO.ui.StackLayout.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+
+ // Properties
+ this.currentItem = null;
+ this.continuous = !!config.continuous;
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-stackLayout' );
+ if ( this.continuous ) {
+ this.$element.addClass( 'oo-ui-stackLayout-continuous' );
+ }
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
+OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
+
+/* Events */
+
+/**
+ * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
+ * {@link #clearItems cleared} or {@link #setItem displayed}.
+ *
+ * @event set
+ * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
+ */
+
+/* Methods */
+
+/**
+ * Get the current panel.
+ *
+ * @return {OO.ui.Layout|null}
+ */
+OO.ui.StackLayout.prototype.getCurrentItem = function () {
+ return this.currentItem;
+};
+
+/**
+ * Unset the current item.
+ *
+ * @private
+ * @param {OO.ui.StackLayout} layout
+ * @fires set
+ */
+OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
+ var prevItem = this.currentItem;
+ if ( prevItem === null ) {
+ return;
+ }
+
+ this.currentItem = null;
+ this.emit( 'set', null );
+};
+
+/**
+ * Add panel layouts to the stack layout.
+ *
+ * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
+ * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
+ * by the index.
+ *
+ * @param {OO.ui.Layout[]} items Panels to add
+ * @param {number} [index] Index of the insertion point
+ * @chainable
+ */
+OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
+ // Update the visibility
+ this.updateHiddenState( items, this.currentItem );
+
+ // Mixin method
+ OO.ui.GroupElement.prototype.addItems.call( this, items, index );
+
+ if ( !this.currentItem && items.length ) {
+ this.setItem( items[ 0 ] );
+ }
+
+ return this;
+};
+
+/**
+ * Remove the specified panels from the stack layout.
+ *
+ * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
+ * you may wish to use the #clearItems method instead.
+ *
+ * @param {OO.ui.Layout[]} items Panels to remove
+ * @chainable
+ * @fires set
+ */
+OO.ui.StackLayout.prototype.removeItems = function ( items ) {
+ // Mixin method
+ OO.ui.GroupElement.prototype.removeItems.call( this, items );
+
+ if ( $.inArray( this.currentItem, items ) !== -1 ) {
+ if ( this.items.length ) {
+ this.setItem( this.items[ 0 ] );
+ } else {
+ this.unsetCurrentItem();
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Clear all panels from the stack layout.
+ *
+ * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
+ * a subset of panels, use the #removeItems method.
+ *
+ * @chainable
+ * @fires set
+ */
+OO.ui.StackLayout.prototype.clearItems = function () {
+ this.unsetCurrentItem();
+ OO.ui.GroupElement.prototype.clearItems.call( this );
+
+ return this;
+};
+
+/**
+ * Show the specified panel.
+ *
+ * If another panel is currently displayed, it will be hidden.
+ *
+ * @param {OO.ui.Layout} item Panel to show
+ * @chainable
+ * @fires set
+ */
+OO.ui.StackLayout.prototype.setItem = function ( item ) {
+ if ( item !== this.currentItem ) {
+ this.updateHiddenState( this.items, item );
+
+ if ( $.inArray( item, this.items ) !== -1 ) {
+ this.currentItem = item;
+ this.emit( 'set', item );
+ } else {
+ this.unsetCurrentItem();
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Update the visibility of all items in case of non-continuous view.
+ *
+ * Ensure all items are hidden except for the selected one.
+ * This method does nothing when the stack is continuous.
+ *
+ * @private
+ * @param {OO.ui.Layout[]} items Item list iterate over
+ * @param {OO.ui.Layout} [selectedItem] Selected item to show
+ */
+OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
+ var i, len;
+
+ if ( !this.continuous ) {
+ for ( i = 0, len = items.length; i < len; i++ ) {
+ if ( !selectedItem || selectedItem !== items[ i ] ) {
+ items[ i ].$element.addClass( 'oo-ui-element-hidden' );
+ }
+ }
+ if ( selectedItem ) {
+ selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
+ }
+ }
+};