From fa81c6fc9b057dfb63a82511f27304f0d8cc41fe Mon Sep 17 00:00:00 2001 From: "James D. Forrester" Date: Sun, 1 Feb 2015 19:30:18 -0800 Subject: [PATCH] Update OOjs UI to v0.6.5 Release notes: https://git.wikimedia.org/blob/oojs%2Fui.git/v0.6.5/History.md Change-Id: I1278a9d0f3b63977f293a79c73283593c65bf910 --- composer.json | 2 +- resources/lib/oojs-ui/oojs-ui-mediawiki.css | 4 +- resources/lib/oojs-ui/oojs-ui-mediawiki.js | 4 +- .../lib/oojs-ui/oojs-ui-mediawiki.svg.css | 4 +- resources/lib/oojs-ui/oojs-ui.js | 1684 +++++++++-------- 5 files changed, 851 insertions(+), 847 deletions(-) diff --git a/composer.json b/composer.json index a54719cd43..4d51a4508b 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require": { "cssjanus/cssjanus": "1.1.1", "leafo/lessphp": "0.5.0", - "oojs/oojs-ui": "0.6.4", + "oojs/oojs-ui": "0.6.5", "php": ">=5.3.3", "psr/log": "1.0.0", "wikimedia/cdb": "1.0.1", diff --git a/resources/lib/oojs-ui/oojs-ui-mediawiki.css b/resources/lib/oojs-ui/oojs-ui-mediawiki.css index dea35fdc09..e4143ce19c 100644 --- a/resources/lib/oojs-ui/oojs-ui-mediawiki.css +++ b/resources/lib/oojs-ui/oojs-ui-mediawiki.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.6.4 + * OOjs UI v0.6.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2015 OOjs Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2015-01-31T01:16:05Z + * Date: 2015-02-02T03:29:03Z */ .oo-ui-progressBarWidget-slide-frames from { margin-left: -40%; diff --git a/resources/lib/oojs-ui/oojs-ui-mediawiki.js b/resources/lib/oojs-ui/oojs-ui-mediawiki.js index 75ffcc18a6..3ef200b5b5 100644 --- a/resources/lib/oojs-ui/oojs-ui-mediawiki.js +++ b/resources/lib/oojs-ui/oojs-ui-mediawiki.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.6.4 + * OOjs UI v0.6.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2015 OOjs Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2015-01-31T01:15:57Z + * Date: 2015-02-02T03:28:54Z */ /** * @class diff --git a/resources/lib/oojs-ui/oojs-ui-mediawiki.svg.css b/resources/lib/oojs-ui/oojs-ui-mediawiki.svg.css index f4e7371aec..e99bb037e4 100644 --- a/resources/lib/oojs-ui/oojs-ui-mediawiki.svg.css +++ b/resources/lib/oojs-ui/oojs-ui-mediawiki.svg.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.6.4 + * OOjs UI v0.6.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2015 OOjs Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2015-01-31T01:16:05Z + * Date: 2015-02-02T03:29:03Z */ .oo-ui-progressBarWidget-slide-frames from { margin-left: -40%; diff --git a/resources/lib/oojs-ui/oojs-ui.js b/resources/lib/oojs-ui/oojs-ui.js index f013b06fb4..1c4adba322 100644 --- a/resources/lib/oojs-ui/oojs-ui.js +++ b/resources/lib/oojs-ui/oojs-ui.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.6.4 + * OOjs UI v0.6.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2015 OOjs Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2015-01-31T01:15:57Z + * Date: 2015-02-02T03:28:54Z */ ( function ( OO ) { @@ -3885,6 +3885,9 @@ OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) { return false; } this.$element.addClass( 'oo-ui-buttonElement-pressed' ); + // Run the mouseup handler no matter where the mouse is when the button is let go, so we can + // reliably remove the pressed class + this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true ); // Prevent change of focus unless specifically configured otherwise if ( this.constructor.static.cancelButtonMouseDownEvents ) { return false; @@ -3901,6 +3904,8 @@ OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) { return false; } this.$element.removeClass( 'oo-ui-buttonElement-pressed' ); + // Stop listening for mouseup, since we only needed this once + this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true ); }; /** @@ -7197,1131 +7202,1137 @@ OO.ui.ProcessDialog.prototype.hideErrors = function () { }; /** - * Layout containing a series of pages. + * Layout made of a field and optional label. + * + * Available label alignment modes include: + * - left: Label is before the field and aligned away from it, best for when the user will be + * scanning for a specific label in a form with many fields + * - right: Label is before the field and aligned toward it, best for forms the user is very + * familiar with and will tab through field checking quickly to verify which field they are in + * - top: Label is before the field and above it, best for when the user will need to fill out all + * fields from top to bottom in a form with few fields + * - inline: Label is after the field and aligned toward it, best for small boolean fields like + * checkboxes or radio buttons * * @class * @extends OO.ui.Layout + * @mixins OO.ui.LabelElement * * @constructor + * @param {OO.ui.Widget} fieldWidget Field widget * @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 changing to a page - * @cfg {boolean} [outlined=false] Show an outline - * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages + * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline' + * @cfg {string} [help] Explanatory text shown as a '?' icon. */ -OO.ui.BookletLayout = function OoUiBookletLayout( config ) { +OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) { + var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget; + // Configuration initialization - config = config || {}; + config = $.extend( { align: 'left' }, config ); + + // Properties (must be set before parent constructor, which calls #getTagName) + this.fieldWidget = fieldWidget; // Parent constructor - OO.ui.BookletLayout.super.call( this, config ); + OO.ui.FieldLayout.super.call( this, config ); + + // Mixin constructors + OO.ui.LabelElement.call( this, config ); // Properties - this.currentPageName = null; - this.pages = {}; - this.ignoreFocus = false; - this.stackLayout = new OO.ui.StackLayout( { $: this.$, continuous: !!config.continuous } ); - 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.$ } ); - this.outlinePanel = new OO.ui.PanelLayout( { $: this.$, scrollable: true } ); - this.gridLayout = new OO.ui.GridLayout( - [ this.outlinePanel, this.stackLayout ], - { $: this.$, widths: [ 1, 2 ] } + this.$field = this.$( '
' ); + this.$body = this.$( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' ); + this.align = null; + if ( config.help ) { + this.popupButtonWidget = new OO.ui.PopupButtonWidget( { + $: this.$, + classes: [ 'oo-ui-fieldLayout-help' ], + framed: false, + icon: 'info' + } ); + + this.popupButtonWidget.getPopup().$body.append( + this.$( '
' ) + .text( config.help ) + .addClass( 'oo-ui-fieldLayout-help-content' ) ); - this.outlineVisible = true; - if ( this.editable ) { - this.outlineControlsWidget = new OO.ui.OutlineControlsWidget( - this.outlineSelectWidget, { $: this.$ } - ); - } + this.$help = this.popupButtonWidget.$element; + } else { + this.$help = this.$( [] ); } // 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 ) ); + if ( hasInputWidget ) { + this.$label.on( 'click', this.onLabelClick.bind( this ) ); } + this.fieldWidget.connect( this, { disable: 'onFieldDisable' } ); // 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 ); - } - this.$element.append( this.gridLayout.$element ); - } else { - this.$element.append( this.stackLayout.$element ); - } + 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.BookletLayout, OO.ui.Layout ); - -/* Events */ - -/** - * @event set - * @param {OO.ui.PageLayout} page Current page - */ - -/** - * @event add - * @param {OO.ui.PageLayout[]} page Added pages - * @param {number} index Index pages were added at - */ - -/** - * @event remove - * @param {OO.ui.PageLayout[]} pages Removed pages - */ +OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout ); +OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement ); /* Methods */ /** - * Handle stack layout focus. + * Handle field disable events. * - * @param {jQuery.Event} e Focusin event + * @param {boolean} value Field is disabled */ -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; - } - } +OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) { + this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value ); }; /** - * Handle stack layout set events. + * Handle label mouse click events. * - * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel + * @param {jQuery.Event} e Mouse click event */ -OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) { - var layout = this; - if ( page ) { - page.scrollElementIntoView( { complete: function () { - if ( layout.autoFocus ) { - layout.focus(); - } - } } ); - } +OO.ui.FieldLayout.prototype.onLabelClick = function () { + this.fieldWidget.simulateLabelClick(); + return false; }; /** - * Focus the first input in the current page. + * Get the field. * - * 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. + * @return {OO.ui.Widget} Field widget */ -OO.ui.BookletLayout.prototype.focus = function () { - var $input, 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(); - } - } +OO.ui.FieldLayout.prototype.getField = function () { + return this.fieldWidget; }; /** - * Handle outline widget select events. + * Set the field alignment mode. * - * @param {OO.ui.OptionWidget|null} item Selected item + * @private + * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline' + * @chainable */ -OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) { - if ( item ) { - this.setPage( item.getData() ); +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; } -}; -/** - * Check if booklet has an outline. - * - * @return {boolean} - */ -OO.ui.BookletLayout.prototype.isOutlined = function () { - return this.outlined; + return this; }; /** - * Check if booklet has editing controls. + * Layout made of a field, a button, and an optional label. * - * @return {boolean} - */ -OO.ui.BookletLayout.prototype.isEditable = function () { - return this.editable; -}; - -/** - * Check if booklet has a visible outline. + * @class + * @extends OO.ui.FieldLayout * - * @return {boolean} + * @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 mode, either 'left', 'right', 'top' or 'inline' + * @cfg {string} [help] Explanatory text shown as a '?' icon. */ -OO.ui.BookletLayout.prototype.isOutlineVisible = function () { - return this.outlined && this.outlineVisible; -}; +OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) { + // Configuration initialization + config = $.extend( { align: 'left' }, config ); -/** - * 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.gridLayout.layout( show ? [ 1, 2 ] : [ 0, 1 ], [ 1 ] ); - } + // Properties (must be set before parent constructor, which calls #getTagName) + this.fieldWidget = fieldWidget; + this.buttonWidget = buttonWidget; - return this; + // Parent constructor + OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config ); + + // Mixin constructors + OO.ui.LabelElement.call( this, config ); + + // Properties + this.$button = this.$( '
' ) + .addClass( 'oo-ui-actionFieldLayout-button' ) + .append( this.buttonWidget.$element ); + + this.$input = this.$( '
' ) + .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 ); + /** - * Get the outline widget. + * Layout made of a fieldset and optional legend. * - * @param {OO.ui.PageLayout} page Page to be selected - * @return {OO.ui.PageLayout|null} Closest page to another + * Just add OO.ui.FieldLayout items. + * + * @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] Items to add */ -OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) { - var next, prev, level, - pages = this.stackLayout.getItems(), - index = $.inArray( page, pages ); +OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) { + // Configuration initialization + config = config || {}; - 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; - } - } + // 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( { + $: this.$, + classes: [ 'oo-ui-fieldsetLayout-help' ], + framed: false, + icon: 'info' + } ); + + this.popupButtonWidget.getPopup().$body.append( + this.$( '
' ) + .text( config.help ) + .addClass( 'oo-ui-fieldsetLayout-help-content' ) + ); + this.$help = this.popupButtonWidget.$element; + } else { + this.$help = this.$( [] ); } - return prev || next || null; -}; -/** - * Get the outline widget. - * - * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline - */ -OO.ui.BookletLayout.prototype.getOutline = function () { - return this.outlineSelectWidget; + // Initialization + this.$element + .addClass( 'oo-ui-fieldsetLayout' ) + .prepend( this.$help, this.$icon, this.$label, this.$group ); + if ( $.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 ); + /** - * Get the outline controls widget. If the outline is not editable, null is returned. + * Layout with an HTML form. * - * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget. + * @class + * @extends OO.ui.Layout + * + * @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 */ -OO.ui.BookletLayout.prototype.getOutlineControls = function () { - return this.outlineControlsWidget; +OO.ui.FormLayout = function OoUiFormLayout( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.FormLayout.super.call( this, config ); + + // 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 + } ); }; +/* Setup */ + +OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout ); + +/* Events */ + /** - * Get a page by name. - * - * @param {string} name Symbolic name of page - * @return {OO.ui.PageLayout|undefined} Page, if found + * @event submit */ -OO.ui.BookletLayout.prototype.getPage = function ( name ) { - return this.pages[ name ]; -}; + +/* Static Properties */ + +OO.ui.FormLayout.static.tagName = 'form'; + +/* Methods */ /** - * Get the current page name. + * Handle form submit events. * - * @return {string|null} Current page name + * @param {jQuery.Event} e Submit event + * @fires submit */ -OO.ui.BookletLayout.prototype.getCurrentPageName = function () { - return this.currentPageName; +OO.ui.FormLayout.prototype.onFormSubmit = function () { + this.emit( 'submit' ); + return false; }; /** - * Add a page to the layout. + * Layout made of proportionally sized columns and rows. * - * When pages are added with the same names as existing pages, the existing pages will be - * automatically removed before the new pages are added. + * @class + * @extends OO.ui.Layout * - * @param {OO.ui.PageLayout[]} pages Pages to add - * @param {number} index Index to insert pages after - * @fires add - * @chainable + * @constructor + * @param {OO.ui.PanelLayout[]} panels Panels in the grid + * @param {Object} [config] Configuration options + * @cfg {number[]} [widths] Widths of columns as ratios + * @cfg {number[]} [heights] Heights of rows as ratios */ -OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) { - var i, len, name, page, item, currentIndex, - stackLayoutPages = this.stackLayout.getItems(), - remove = [], - items = []; +OO.ui.GridLayout = function OoUiGridLayout( panels, config ) { + var i, len, widths; - // Remove pages with same names - for ( i = 0, len = pages.length; i < len; i++ ) { - page = pages[ i ]; - name = page.getName(); + // Configuration initialization + config = config || {}; - 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 ] ); - } + // Parent constructor + OO.ui.GridLayout.super.call( this, config ); + + // Properties + this.panels = []; + this.widths = []; + this.heights = []; + + // Initialization + this.$element.addClass( 'oo-ui-gridLayout' ); + for ( i = 0, len = panels.length; i < len; i++ ) { + this.panels.push( panels[ i ] ); + this.$element.append( panels[ i ].$element ); } - if ( remove.length ) { - this.removePages( remove ); + if ( config.widths || config.heights ) { + this.layout( config.widths || [ 1 ], config.heights || [ 1 ] ); + } else { + // Arrange in columns by default + widths = this.panels.map( function () { return 1; } ); + this.layout( widths, [ 1 ] ); } +}; - // 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( { $: this.$, data: name } ); - page.setOutlineItem( item ); - items.push( item ); - } - } +/* Setup */ - if ( this.outlined && items.length ) { - this.outlineSelectWidget.addItems( items, index ); - this.selectFirstSelectablePage(); - } - this.stackLayout.addItems( pages, index ); - this.emit( 'add', pages, index ); +OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout ); - return this; -}; +/* Events */ /** - * Remove a page from the layout. - * - * @fires remove - * @chainable + * @event layout */ -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 ); +/** + * @event update + */ - return this; -}; +/* Methods */ /** - * Clear all pages from the layout. + * Set grid dimensions. * - * @fires remove - * @chainable + * @param {number[]} widths Widths of columns as ratios + * @param {number[]} heights Heights of rows as ratios + * @fires layout + * @throws {Error} If grid is not large enough to fit all panels */ -OO.ui.BookletLayout.prototype.clearPages = function () { - var i, len, - pages = this.stackLayout.getItems(); +OO.ui.GridLayout.prototype.layout = function ( widths, heights ) { + var x, y, + xd = 0, + yd = 0, + cols = widths.length, + rows = heights.length; - this.pages = {}; - this.currentPageName = null; - if ( this.outlined ) { - this.outlineSelectWidget.clearItems(); - for ( i = 0, len = pages.length; i < len; i++ ) { - pages[ i ].setOutlineItem( null ); - } + // Verify grid is big enough to fit panels + if ( cols * rows < this.panels.length ) { + throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' ); } - this.stackLayout.clearItems(); - this.emit( 'remove', pages ); - - return this; + // Sum up denominators + for ( x = 0; x < cols; x++ ) { + xd += widths[ x ]; + } + for ( y = 0; y < rows; y++ ) { + yd += heights[ y ]; + } + // Store factors + this.widths = []; + this.heights = []; + for ( x = 0; x < cols; x++ ) { + this.widths[ x ] = widths[ x ] / xd; + } + for ( y = 0; y < rows; y++ ) { + this.heights[ y ] = heights[ y ] / yd; + } + // Synchronize view + this.update(); + this.emit( 'layout' ); }; /** - * Set the current page by name. + * Update panel positions and sizes. * - * @fires set - * @param {string} name Symbolic name of page + * @fires update */ -OO.ui.BookletLayout.prototype.setPage = function ( name ) { - var selectedItem, - $focused, - page = this.pages[ name ]; +OO.ui.GridLayout.prototype.update = function () { + var x, y, panel, width, height, dimensions, + i = 0, + top = 0, + left = 0, + cols = this.widths.length, + rows = this.heights.length; - if ( name !== this.currentPageName ) { - if ( this.outlined ) { - selectedItem = this.outlineSelectWidget.getSelectedItem(); - if ( selectedItem && selectedItem.getData() !== name ) { - this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( 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(); - } - } + for ( y = 0; y < rows; y++ ) { + height = this.heights[ y ]; + for ( x = 0; x < cols; x++ ) { + width = this.widths[ x ]; + panel = this.panels[ i ]; + dimensions = { + width: ( width * 100 ) + '%', + height: ( height * 100 ) + '%', + top: ( top * 100 ) + '%' + }; + // If RTL, reverse: + if ( OO.ui.Element.static.getDir( this.$.context ) === 'rtl' ) { + dimensions.right = ( left * 100 ) + '%'; + } else { + dimensions.left = ( left * 100 ) + '%'; } - this.currentPageName = name; - this.stackLayout.setItem( page ); - page.setActive( true ); - this.emit( 'set', page ); + // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero + if ( width === 0 || height === 0 ) { + dimensions.visibility = 'hidden'; + } else { + dimensions.visibility = ''; + } + panel.$element.css( dimensions ); + i++; + left += width; } + top += height; + left = 0; } + + this.emit( 'update' ); }; /** - * Select the first selectable page. + * Get a panel at a given position. * - * @chainable + * The x and y position is affected by the current grid layout. + * + * @param {number} x Horizontal position + * @param {number} y Vertical position + * @return {OO.ui.PanelLayout} The panel at the given position */ -OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () { - if ( !this.outlineSelectWidget.getSelectedItem() ) { - this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() ); - } - - return this; +OO.ui.GridLayout.prototype.getPanel = function ( x, y ) { + return this.panels[ ( x * this.widths.length ) + y ]; }; /** - * Layout made of a field and optional label. + * Layout with a content and menu area. * - * Available label alignment modes include: - * - left: Label is before the field and aligned away from it, best for when the user will be - * scanning for a specific label in a form with many fields - * - right: Label is before the field and aligned toward it, best for forms the user is very - * familiar with and will tab through field checking quickly to verify which field they are in - * - top: Label is before the field and above it, best for when the user will need to fill out all - * fields from top to bottom in a form with few fields - * - inline: Label is after the field and aligned toward it, best for small boolean fields like - * checkboxes or radio buttons + * The menu area can be positioned at the top, after, bottom or before. The content area will fill + * all remaining space. * * @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 mode, either 'left', 'right', 'top' or 'inline' - * @cfg {string} [help] Explanatory text shown as a '?' icon. + * @cfg {number|string} [menuSize='18em'] Size of menu in pixels or any CSS unit + * @cfg {boolean} [showMenu=true] Show menu + * @cfg {string} [position='before'] Position of menu, either `top`, `after`, `bottom` or `before` + * @cfg {boolean} [collapse] Collapse the menu out of view */ -OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) { - var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget; +OO.ui.MenuLayout = function OoUiMenuLayout( config ) { + var positions = this.constructor.static.menuPositions; // Configuration initialization - config = $.extend( { align: 'left' }, config ); - - // Properties (must be set before parent constructor, which calls #getTagName) - this.fieldWidget = fieldWidget; + config = config || {}; // Parent constructor - OO.ui.FieldLayout.super.call( this, config ); - - // Mixin constructors - OO.ui.LabelElement.call( this, config ); + OO.ui.MenuLayout.super.call( this, config ); // Properties - this.$field = this.$( '
' ); - this.$body = this.$( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' ); - this.align = null; - if ( config.help ) { - this.popupButtonWidget = new OO.ui.PopupButtonWidget( { - $: this.$, - classes: [ 'oo-ui-fieldLayout-help' ], - framed: false, - icon: 'info' - } ); + this.showMenu = config.showMenu !== false; + this.menuSize = config.menuSize || '18em'; + this.menuPosition = positions[ config.menuPosition ] || positions.before; - this.popupButtonWidget.getPopup().$body.append( - this.$( '
' ) - .text( config.help ) - .addClass( 'oo-ui-fieldLayout-help-content' ) - ); - this.$help = this.popupButtonWidget.$element; - } else { - this.$help = this.$( [] ); - } + /** + * Menu DOM node + * + * @property {jQuery} + */ + this.$menu = this.$( '
' ); + /** + * Content DOM node + * + * @property {jQuery} + */ + this.$content = this.$( '
' ); // Events - if ( hasInputWidget ) { - this.$label.on( 'click', this.onLabelClick.bind( this ) ); - } - this.fieldWidget.connect( this, { disable: 'onFieldDisable' } ); + this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) ); // Initialization + this.toggleMenu( this.showMenu ); + this.$menu + .addClass( 'oo-ui-menuLayout-menu' ) + .css( this.menuPosition.sizeProperty, this.menuSize ); + this.$content.addClass( 'oo-ui-menuLayout-content' ); 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 ); + .addClass( 'oo-ui-menuLayout ' + this.menuPosition.className ) + .append( this.$content, this.$menu ); }; /* Setup */ -OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout ); -OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement ); - -/* Methods */ - -/** - * Handle field disable events. - * - * @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. - * - * @param {jQuery.Event} e Mouse click event - */ -OO.ui.FieldLayout.prototype.onLabelClick = function () { - this.fieldWidget.simulateLabelClick(); - return false; -}; +OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout ); -/** - * Get the field. - * - * @return {OO.ui.Widget} Field widget - */ -OO.ui.FieldLayout.prototype.getField = function () { - return this.fieldWidget; -}; +/* Static Properties */ -/** - * 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; +OO.ui.MenuLayout.static.menuPositions = { + top: { + sizeProperty: 'height', + positionProperty: 'top', + className: 'oo-ui-menuLayout-top' + }, + after: { + sizeProperty: 'width', + positionProperty: 'right', + rtlPositionProperty: 'left', + className: 'oo-ui-menuLayout-after' + }, + bottom: { + sizeProperty: 'height', + positionProperty: 'bottom', + className: 'oo-ui-menuLayout-bottom' + }, + before: { + sizeProperty: 'width', + positionProperty: 'left', + rtlPositionProperty: 'right', + className: 'oo-ui-menuLayout-before' } - - return this; }; -/** - * Layout made of a field, a button, and an optional label. - * - * @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 mode, either 'left', 'right', 'top' or 'inline' - * @cfg {string} [help] Explanatory text shown as a '?' icon. - */ -OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) { - // Configuration initialization - config = $.extend( { align: 'left' }, config ); - - // Properties (must be set before parent constructor, which calls #getTagName) - this.fieldWidget = fieldWidget; - this.buttonWidget = buttonWidget; - - // Parent constructor - OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config ); - - // Mixin constructors - OO.ui.LabelElement.call( this, config ); - - // Properties - this.$button = this.$( '
' ) - .addClass( 'oo-ui-actionFieldLayout-button' ) - .append( this.buttonWidget.$element ); - - this.$input = this.$( '
' ) - .addClass( 'oo-ui-actionFieldLayout-input' ) - .append( this.fieldWidget.$element ); +/* Methods */ - this.$field - .addClass( 'oo-ui-actionFieldLayout' ) - .append( this.$input, this.$button ); +/** + * Handle DOM attachment events + */ +OO.ui.MenuLayout.prototype.onElementAttach = function () { + // getPositionProperty won't know about directionality until the layout is attached + if ( this.showMenu ) { + this.$content.css( this.getPositionProperty(), this.menuSize ); + } }; -/* Setup */ +/** + * Toggle menu. + * + * @param {boolean} showMenu Show menu, omit to toggle + * @chainable + */ +OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) { + showMenu = showMenu === undefined ? !this.showMenu : !!showMenu; -OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout ); + if ( this.showMenu !== showMenu ) { + this.showMenu = showMenu; + this.updateSizes(); + } + + return this; +}; /** - * Layout made of a fieldset and optional legend. - * - * Just add OO.ui.FieldLayout items. - * - * @class - * @extends OO.ui.Layout - * @mixins OO.ui.IconElement - * @mixins OO.ui.LabelElement - * @mixins OO.ui.GroupElement + * Check if menu is visible * - * @constructor - * @param {Object} [config] Configuration options - * @cfg {OO.ui.FieldLayout[]} [items] Items to add + * @return {boolean} Menu is visible */ -OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) { - // Configuration initialization - config = config || {}; - - // Parent constructor - OO.ui.FieldsetLayout.super.call( this, config ); +OO.ui.MenuLayout.prototype.isMenuVisible = function () { + return this.showMenu; +}; - // Mixin constructors - OO.ui.IconElement.call( this, config ); - OO.ui.LabelElement.call( this, config ); - OO.ui.GroupElement.call( this, config ); +/** + * Set menu size. + * + * @param {number|string} size Size of menu in pixels or any CSS unit + * @chainable + */ +OO.ui.MenuLayout.prototype.setMenuSize = function ( size ) { + this.menuSize = size; + this.updateSizes(); - if ( config.help ) { - this.popupButtonWidget = new OO.ui.PopupButtonWidget( { - $: this.$, - classes: [ 'oo-ui-fieldsetLayout-help' ], - framed: false, - icon: 'info' - } ); + return this; +}; - this.popupButtonWidget.getPopup().$body.append( - this.$( '
' ) - .text( config.help ) - .addClass( 'oo-ui-fieldsetLayout-help-content' ) - ); - this.$help = this.popupButtonWidget.$element; +/** + * Update menu and content CSS based on current menu size and visibility + */ +OO.ui.MenuLayout.prototype.updateSizes = function () { + if ( this.showMenu ) { + this.$menu + .css( this.menuPosition.sizeProperty, this.menuSize ) + .css( 'overflow', '' ); + this.$content.css( this.getPositionProperty(), this.menuSize ); } else { - this.$help = this.$( [] ); - } - - // Initialization - this.$element - .addClass( 'oo-ui-fieldsetLayout' ) - .prepend( this.$help, this.$icon, this.$label, this.$group ); - if ( $.isArray( config.items ) ) { - this.addItems( config.items ); + this.$menu + .css( this.menuPosition.sizeProperty, 0 ) + .css( 'overflow', 'hidden' ); + this.$content.css( this.getPositionProperty(), 0 ); } }; -/* 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 ); - /** - * Layout with an HTML form. - * - * @class - * @extends OO.ui.Layout + * Get menu size. * - * @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 + * @return {number|string} Menu size */ -OO.ui.FormLayout = function OoUiFormLayout( config ) { - // Configuration initialization - config = config || {}; +OO.ui.MenuLayout.prototype.getMenuSize = function () { + return this.menuSize; +}; - // Parent constructor - OO.ui.FormLayout.super.call( this, config ); +/** + * 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 ) { + var positionProperty, positions = this.constructor.static.menuPositions; - // Events - this.$element.on( 'submit', this.onFormSubmit.bind( this ) ); + if ( !positions[ position ] ) { + throw new Error( 'Cannot set position; unsupported position value: ' + position ); + } - // Initialization - this.$element - .addClass( 'oo-ui-formLayout' ) - .attr( { - method: config.method, - action: config.action, - enctype: config.enctype - } ); -}; + positionProperty = this.getPositionProperty(); + this.$menu.css( this.menuPosition.sizeProperty, '' ); + this.$content.css( positionProperty, '' ); + this.$element.removeClass( this.menuPosition.className ); -/* Setup */ + this.menuPosition = positions[ position ]; -OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout ); + this.updateSizes(); + this.$element.addClass( this.menuPosition.className ); -/* Events */ + return this; +}; /** - * @event submit + * Get menu position. + * + * @return {string} Menu position */ - -/* Static Properties */ - -OO.ui.FormLayout.static.tagName = 'form'; - -/* Methods */ +OO.ui.MenuLayout.prototype.getMenuPosition = function () { + return this.menuPosition; +}; /** - * Handle form submit events. + * Get the menu position property. * - * @param {jQuery.Event} e Submit event - * @fires submit + * @return {string} Menu position CSS property */ -OO.ui.FormLayout.prototype.onFormSubmit = function () { - this.emit( 'submit' ); - return false; +OO.ui.MenuLayout.prototype.getPositionProperty = function () { + if ( this.menuPosition.rtlPositionProperty && this.$element.css( 'direction' ) === 'rtl' ) { + return this.menuPosition.rtlPositionProperty; + } else { + return this.menuPosition.positionProperty; + } }; /** - * Layout made of proportionally sized columns and rows. + * Layout containing a series of pages. * * @class - * @extends OO.ui.Layout + * @extends OO.ui.MenuLayout * * @constructor - * @param {OO.ui.PanelLayout[]} panels Panels in the grid * @param {Object} [config] Configuration options - * @cfg {number[]} [widths] Widths of columns as ratios - * @cfg {number[]} [heights] Heights of rows as ratios + * @cfg {boolean} [continuous=false] Show all pages, one after another + * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page + * @cfg {boolean} [outlined=false] Show an outline + * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages */ -OO.ui.GridLayout = function OoUiGridLayout( panels, config ) { - var i, len, widths; - +OO.ui.BookletLayout = function OoUiBookletLayout( config ) { // Configuration initialization config = config || {}; // Parent constructor - OO.ui.GridLayout.super.call( this, config ); + OO.ui.BookletLayout.super.call( this, config ); // Properties - this.panels = []; - this.widths = []; - this.heights = []; + this.currentPageName = null; + this.pages = {}; + this.ignoreFocus = false; + this.stackLayout = new OO.ui.StackLayout( { $: this.$, 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.$ } ); + this.outlinePanel = new OO.ui.PanelLayout( { $: this.$, scrollable: true } ); + this.$menu.append( this.outlinePanel.$element ); + this.outlineVisible = true; + if ( this.editable ) { + this.outlineControlsWidget = new OO.ui.OutlineControlsWidget( + this.outlineSelectWidget, { $: this.$ } + ); + } + } + this.toggleMenu( this.outlined ); - // Initialization - this.$element.addClass( 'oo-ui-gridLayout' ); - for ( i = 0, len = panels.length; i < len; i++ ) { - this.panels.push( panels[ i ] ); - this.$element.append( panels[ i ].$element ); + // Events + this.stackLayout.connect( this, { set: 'onStackLayoutSet' } ); + if ( this.outlined ) { + this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } ); } - if ( config.widths || config.heights ) { - this.layout( config.widths || [ 1 ], config.heights || [ 1 ] ); - } else { - // Arrange in columns by default - widths = this.panels.map( function () { return 1; } ); - this.layout( widths, [ 1 ] ); + 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.GridLayout, OO.ui.Layout ); +OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout ); /* Events */ /** - * @event layout + * @event set + * @param {OO.ui.PageLayout} page Current page */ /** - * @event update + * @event add + * @param {OO.ui.PageLayout[]} page Added pages + * @param {number} index Index pages were added at + */ + +/** + * @event remove + * @param {OO.ui.PageLayout[]} pages Removed pages */ /* Methods */ /** - * Set grid dimensions. + * Handle stack layout focus. * - * @param {number[]} widths Widths of columns as ratios - * @param {number[]} heights Heights of rows as ratios - * @fires layout - * @throws {Error} If grid is not large enough to fit all panels + * @param {jQuery.Event} e Focusin event */ -OO.ui.GridLayout.prototype.layout = function ( widths, heights ) { - var x, y, - xd = 0, - yd = 0, - cols = widths.length, - rows = heights.length; +OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) { + var name, $target; - // Verify grid is big enough to fit panels - if ( cols * rows < this.panels.length ) { - throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' ); + // 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; + } } +}; - // Sum up denominators - for ( x = 0; x < cols; x++ ) { - xd += widths[ x ]; +/** + * Handle stack layout set events. + * + * @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(); + } + } } ); } - for ( y = 0; y < rows; y++ ) { - yd += heights[ y ]; +}; + +/** + * 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. + */ +OO.ui.BookletLayout.prototype.focus = function () { + var $input, page = this.stackLayout.getCurrentItem(); + if ( !page && this.outlined ) { + this.selectFirstSelectablePage(); + page = this.stackLayout.getCurrentItem(); } - // Store factors - this.widths = []; - this.heights = []; - for ( x = 0; x < cols; x++ ) { - this.widths[ x ] = widths[ x ] / xd; + if ( !page ) { + return; } - for ( y = 0; y < rows; y++ ) { - this.heights[ y ] = heights[ y ] / yd; + // 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(); + } } - // Synchronize view - this.update(); - this.emit( 'layout' ); }; /** - * Update panel positions and sizes. + * Handle outline widget select events. * - * @fires update + * @param {OO.ui.OptionWidget|null} item Selected item */ -OO.ui.GridLayout.prototype.update = function () { - var x, y, panel, width, height, dimensions, - i = 0, - top = 0, - left = 0, - cols = this.widths.length, - rows = this.heights.length; - - for ( y = 0; y < rows; y++ ) { - height = this.heights[ y ]; - for ( x = 0; x < cols; x++ ) { - width = this.widths[ x ]; - panel = this.panels[ i ]; - dimensions = { - width: ( width * 100 ) + '%', - height: ( height * 100 ) + '%', - top: ( top * 100 ) + '%' - }; - // If RTL, reverse: - if ( OO.ui.Element.static.getDir( this.$.context ) === 'rtl' ) { - dimensions.right = ( left * 100 ) + '%'; - } else { - dimensions.left = ( left * 100 ) + '%'; - } - // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero - if ( width === 0 || height === 0 ) { - dimensions.visibility = 'hidden'; - } else { - dimensions.visibility = ''; - } - panel.$element.css( dimensions ); - i++; - left += width; - } - top += height; - left = 0; +OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) { + if ( item ) { + this.setPage( item.getData() ); } - - this.emit( 'update' ); }; /** - * Get a panel at a given position. - * - * The x and y position is affected by the current grid layout. + * Check if booklet has an outline. * - * @param {number} x Horizontal position - * @param {number} y Vertical position - * @return {OO.ui.PanelLayout} The panel at the given position + * @return {boolean} */ -OO.ui.GridLayout.prototype.getPanel = function ( x, y ) { - return this.panels[ ( x * this.widths.length ) + y ]; +OO.ui.BookletLayout.prototype.isOutlined = function () { + return this.outlined; }; /** - * Layout with a content and menu area. - * - * The menu area can be positioned at the top, after, bottom or before. The content area will fill - * all remaining space. - * - * @class - * @extends OO.ui.Layout + * Check if booklet has editing controls. * - * @constructor - * @param {Object} [config] Configuration options - * @cfg {number|string} [menuSize='18em'] Size of menu in pixels or any CSS unit - * @cfg {boolean} [showMenu=true] Show menu - * @cfg {string} [position='before'] Position of menu, either `top`, `after`, `bottom` or `before` - * @cfg {boolean} [collapse] Collapse the menu out of view + * @return {boolean} */ -OO.ui.MenuLayout = function OoUiMenuLayout( config ) { - var positions = this.constructor.static.menuPositions; - - // Configuration initialization - config = config || {}; - - // Parent constructor - OO.ui.MenuLayout.super.call( this, config ); - - // Properties - this.showMenu = config.showMenu !== false; - this.menuSize = config.menuSize || '18em'; - this.menuPosition = positions[ config.menuPosition ] || positions.before; - - /** - * Menu DOM node - * - * @property {jQuery} - */ - this.$menu = this.$( '
' ); - /** - * Content DOM node - * - * @property {jQuery} - */ - this.$content = this.$( '
' ); - - // Events - this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) ); - - // Initialization - this.toggleMenu( this.showMenu ); - this.$menu - .addClass( 'oo-ui-menuLayout-menu' ) - .css( this.menuPosition.sizeProperty, this.menuSize ); - this.$content.addClass( 'oo-ui-menuLayout-content' ); - this.$element - .addClass( 'oo-ui-menuLayout ' + this.menuPosition.className ) - .append( this.$content, this.$menu ); +OO.ui.BookletLayout.prototype.isEditable = function () { + return this.editable; }; -/* Setup */ +/** + * Check if booklet has a visible outline. + * + * @return {boolean} + */ +OO.ui.BookletLayout.prototype.isOutlineVisible = function () { + return this.outlined && this.outlineVisible; +}; -OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout ); +/** + * 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.gridLayout.layout( show ? [ 1, 2 ] : [ 0, 1 ], [ 1 ] ); + } -/* Static Properties */ + return this; +}; -OO.ui.MenuLayout.static.menuPositions = { - top: { - sizeProperty: 'height', - positionProperty: 'top', - className: 'oo-ui-menuLayout-top' - }, - after: { - sizeProperty: 'width', - positionProperty: 'right', - rtlPositionProperty: 'left', - className: 'oo-ui-menuLayout-after' - }, - bottom: { - sizeProperty: 'height', - positionProperty: 'bottom', - className: 'oo-ui-menuLayout-bottom' - }, - before: { - sizeProperty: 'width', - positionProperty: 'left', - rtlPositionProperty: 'right', - className: 'oo-ui-menuLayout-before' +/** + * Get the outline widget. + * + * @param {OO.ui.PageLayout} page Page to be selected + * @return {OO.ui.PageLayout|null} Closest page to another + */ +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; }; -/* Methods */ - /** - * Handle DOM attachment events + * Get the outline widget. + * + * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline */ -OO.ui.MenuLayout.prototype.onElementAttach = function () { - // getPositionProperty won't know about directionality until the layout is attached - if ( this.showMenu ) { - this.$content.css( this.getPositionProperty(), this.menuSize ); - } +OO.ui.BookletLayout.prototype.getOutline = function () { + return this.outlineSelectWidget; }; /** - * Toggle menu. + * Get the outline controls widget. If the outline is not editable, null is returned. * - * @param {boolean} showMenu Show menu, omit to toggle - * @chainable + * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget. */ -OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) { - showMenu = showMenu === undefined ? !this.showMenu : !!showMenu; - - if ( this.showMenu !== showMenu ) { - this.showMenu = showMenu; - this.updateSizes(); - } - - return this; +OO.ui.BookletLayout.prototype.getOutlineControls = function () { + return this.outlineControlsWidget; }; /** - * Check if menu is visible + * Get a page by name. * - * @return {boolean} Menu is visible + * @param {string} name Symbolic name of page + * @return {OO.ui.PageLayout|undefined} Page, if found */ -OO.ui.MenuLayout.prototype.isMenuVisible = function () { - return this.showMenu; +OO.ui.BookletLayout.prototype.getPage = function ( name ) { + return this.pages[ name ]; }; /** - * Set menu size. + * Get the current page * - * @param {number|string} size Size of menu in pixels or any CSS unit - * @chainable + * @return {OO.ui.PageLayout|undefined} Current page, if found */ -OO.ui.MenuLayout.prototype.setMenuSize = function ( size ) { - this.menuSize = size; - this.updateSizes(); - - return this; +OO.ui.BookletLayout.prototype.getCurrentPage = function () { + var name = this.getCurrentPageName(); + return name ? this.getPage( name ) : undefined; }; /** - * Update menu and content CSS based on current menu size and visibility + * Get the current page name. + * + * @return {string|null} Current page name */ -OO.ui.MenuLayout.prototype.updateSizes = function () { - if ( this.showMenu ) { - this.$menu - .css( this.menuPosition.sizeProperty, this.menuSize ) - .css( 'overflow', '' ); - this.$content.css( this.getPositionProperty(), this.menuSize ); - } else { - this.$menu - .css( this.menuPosition.sizeProperty, 0 ) - .css( 'overflow', 'hidden' ); - this.$content.css( this.getPositionProperty(), 0 ); - } +OO.ui.BookletLayout.prototype.getCurrentPageName = function () { + return this.currentPageName; }; /** - * Get menu size. + * Add a page to the layout. * - * @return {number|string} Menu size + * 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 to insert pages after + * @fires add + * @chainable */ -OO.ui.MenuLayout.prototype.getMenuSize = function () { - return this.menuSize; +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( { $: this.$, 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; }; /** - * Set menu position. + * Remove a page from the layout. * - * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before` - * @throws {Error} If position value is not supported + * @fires remove * @chainable */ -OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) { - var positionProperty, positions = this.constructor.static.menuPositions; +OO.ui.BookletLayout.prototype.removePages = function ( pages ) { + var i, len, name, page, + items = []; - if ( !positions[ position ] ) { - throw new Error( 'Cannot set position; unsupported position value: ' + position ); + 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 ); - positionProperty = this.getPositionProperty(); - this.$menu.css( this.menuPosition.sizeProperty, '' ); - this.$content.css( positionProperty, '' ); - this.$element.removeClass( this.menuPosition.className ); + return this; +}; - this.menuPosition = positions[ position ]; +/** + * Clear all pages from the layout. + * + * @fires remove + * @chainable + */ +OO.ui.BookletLayout.prototype.clearPages = function () { + var i, len, + pages = this.stackLayout.getItems(); - this.updateSizes(); - this.$element.addClass( this.menuPosition.className ); + 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; }; /** - * Get menu position. + * Set the current page by name. * - * @return {string} Menu position + * @fires set + * @param {string} name Symbolic name of page */ -OO.ui.MenuLayout.prototype.getMenuPosition = function () { - return this.menuPosition; +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.selectItem( this.outlineSelectWidget.getItemFromData( 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 ); + } + } }; /** - * Get the menu position property. + * Select the first selectable page. * - * @return {string} Menu position CSS property + * @chainable */ -OO.ui.MenuLayout.prototype.getPositionProperty = function () { - if ( this.menuPosition.rtlPositionProperty && this.$element.css( 'direction' ) === 'rtl' ) { - return this.menuPosition.rtlPositionProperty; - } else { - return this.menuPosition.positionProperty; +OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () { + if ( !this.outlineSelectWidget.getSelectedItem() ) { + this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() ); } + + return this; }; /** @@ -9806,10 +9817,6 @@ OO.ui.ButtonWidget.prototype.onClick = function () { OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) { // Remove the tab-index while the button is down to prevent the button from stealing focus this.$button.removeAttr( 'tabindex' ); - // Run the mouseup handler no matter where the mouse is when the button is let go, so we can - // reliably reapply the tabindex and remove the pressed class - this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true ); - return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e ); }; @@ -9819,9 +9826,6 @@ OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) { OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) { // Restore the tab-index after the button is up to restore the button's accessibility this.$button.attr( 'tabindex', this.tabIndex ); - // Stop listening for mouseup, since we only needed this once - this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true ); - return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e ); }; -- 2.20.1