diff options
Diffstat (limited to 'vendor/oojs/oojs-ui/src/layouts/BookletLayout.js')
-rw-r--r-- | vendor/oojs/oojs-ui/src/layouts/BookletLayout.js | 542 |
1 files changed, 542 insertions, 0 deletions
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; +}; |