/*!
- * 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 ) {
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;
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 );
};
/**
};
/**
- * 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.$( '<div>' );
+ 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.$( '<div>' )
+ .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.$( '<div>' )
+ .addClass( 'oo-ui-actionFieldLayout-button' )
+ .append( this.buttonWidget.$element );
+
+ this.$input = this.$( '<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 );
+
/**
- * 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.$( '<div>' )
+ .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.$( '<div>' );
- 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.$( '<div>' )
- .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.$( '<div>' );
+ /**
+ * Content DOM node
+ *
+ * @property {jQuery}
+ */
+ this.$content = this.$( '<div>' );
// 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.$( '<div>' )
- .addClass( 'oo-ui-actionFieldLayout-button' )
- .append( this.buttonWidget.$element );
-
- this.$input = this.$( '<div>' )
- .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.$( '<div>' )
- .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.$( '<div>' );
- /**
- * Content DOM node
- *
- * @property {jQuery}
- */
- this.$content = this.$( '<div>' );
-
- // 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;
};
/**
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 );
};
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 );
};