diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2015-06-04 07:31:04 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2015-06-04 07:58:39 +0200 |
commit | f6d65e533c62f6deb21342d4901ece24497b433e (patch) | |
tree | f28adf0362d14bcd448f7b65a7aaf38650f923aa /vendor/oojs/oojs-ui/src/layouts | |
parent | c27b2e832fe25651ef2410fae85b41072aae7519 (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.js | 81 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/BookletLayout.js | 542 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/CardLayout.js | 138 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/FieldLayout.js | 160 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/FieldsetLayout.js | 86 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/FormLayout.js | 119 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/IndexLayout.js | 452 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/MenuLayout.js | 156 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/PageLayout.js | 138 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/PanelLayout.js | 55 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/StackLayout.js | 214 |
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' ); + } + } +}; |