/*!
- * OOjs UI v0.6.3
+ * OOjs UI v0.8.0
* 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-16T00:05:04Z
+ * Date: 2015-02-19T01:33:11Z
*/
( function ( OO ) {
var i, len, langs;
// Requested language
- if ( obj[lang] ) {
- return obj[lang];
+ if ( obj[ lang ] ) {
+ return obj[ lang ];
}
// Known user language
langs = OO.ui.getUserLanguages();
for ( i = 0, len = langs.length; i < len; i++ ) {
- lang = langs[i];
- if ( obj[lang] ) {
- return obj[lang];
+ lang = langs[ i ];
+ if ( obj[ lang ] ) {
+ return obj[ lang ];
}
}
// Fallback language
- if ( obj[fallback] ) {
- return obj[fallback];
+ if ( obj[ fallback ] ) {
+ return obj[ fallback ];
}
// First existing language
for ( lang in obj ) {
- return obj[lang];
+ return obj[ lang ];
}
return undefined;
containers = [ containers ];
}
for ( i = containers.length - 1; i >= 0; i-- ) {
- if ( ( matchContainers && contained === containers[i] ) || $.contains( containers[i], contained ) ) {
+ if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
return true;
}
}
* @return {string} Translated message with parameters substituted
*/
OO.ui.msg = function ( key ) {
- var message = messages[key],
+ var message = messages[ key ],
params = Array.prototype.slice.call( arguments, 1 );
if ( typeof message === 'string' ) {
// Perform $1 substitution
message = message.replace( /\$(\d+)/g, function ( unused, n ) {
var i = parseInt( n, 10 );
- return params[i - 1] !== undefined ? params[i - 1] : '$' + n;
+ return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
} );
} else {
// Return placeholder if message not found
};
/**
- * List of actions.
+ * ActionSets manage the behavior of the {@link OO.ui.ActionWidget Action widgets} that comprise them.
+ * Actions can be made available for specific contexts (modes) and circumstances
+ * (abilities). Please see the [OOjs UI documentation on MediaWiki][1] for more information.
+ *
+ * @example
+ * // Example: An action set used in a process dialog
+ * function ProcessDialog( config ) {
+ * ProcessDialog.super.call( this, config );
+ * }
+ * OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
+ * ProcessDialog.static.title = 'An action set in a process dialog';
+ * // An action set that uses modes ('edit' and 'help' mode, in this example).
+ * ProcessDialog.static.actions = [
+ * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
+ * { action: 'help', modes: 'edit', label: 'Help' },
+ * { modes: 'edit', label: 'Cancel', flags: 'safe' },
+ * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
+ * ];
+ *
+ * ProcessDialog.prototype.initialize = function () {
+ * ProcessDialog.super.prototype.initialize.apply( this, arguments );
+ * this.panel1 = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } );
+ * this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode. </p>' );
+ * this.panel2 = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } );
+ * this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode</p>' );
+ * this.stackLayout= new OO.ui.StackLayout( {
+ * items: [ this.panel1, this.panel2 ]
+ * });
+ * this.$body.append( this.stackLayout.$element );
+ * };
+ * ProcessDialog.prototype.getSetupProcess = function ( data ) {
+ * return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
+ * .next( function () {
+ * this.actions.setMode('edit');
+ * }, this );
+ * };
+ * ProcessDialog.prototype.getActionProcess = function ( action ) {
+ * if ( action === 'help' ) {
+ * this.actions.setMode( 'help' );
+ * this.stackLayout.setItem( this.panel2 );
+ * } else if ( action === 'back' ) {
+ * this.actions.setMode( 'edit' );
+ * this.stackLayout.setItem( this.panel1 );
+ * } else if ( action === 'continue' ) {
+ * var dialog = this;
+ * return new OO.ui.Process( function () {
+ * dialog.close();
+ * } );
+ * }
+ * return ProcessDialog.super.prototype.getActionProcess.call( this, action );
+ * };
+ * ProcessDialog.prototype.getBodyHeight = function () {
+ * return this.panel1.$element.outerHeight( true );
+ * };
+ * var windowManager = new OO.ui.WindowManager();
+ * $( 'body' ).append( windowManager.$element );
+ * var processDialog = new ProcessDialog({
+ * size: 'medium'});
+ * windowManager.addWindows( [ processDialog ] );
+ * windowManager.openWindow( processDialog );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
*
* @abstract
* @class
/* Static Properties */
/**
- * Symbolic name of dialog.
+ * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
+ * header of a {@link OO.ui.ProcessDialog process dialog}.
+ * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
+ *
+ * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
*
* @abstract
* @static
/**
* Handle action change events.
*
+ * @private
* @fires change
*/
OO.ui.ActionSet.prototype.onActionChange = function () {
var flag;
for ( flag in this.special ) {
- if ( action === this.special[flag] ) {
+ if ( action === this.special[ flag ] ) {
return true;
}
}
// Collect category candidates
matches = [];
for ( category in this.categorized ) {
- list = filters[category];
+ list = filters[ category ];
if ( list ) {
if ( !Array.isArray( list ) ) {
list = [ list ];
}
for ( i = 0, len = list.length; i < len; i++ ) {
- actions = this.categorized[category][list[i]];
+ actions = this.categorized[ category ][ list[ i ] ];
if ( Array.isArray( actions ) ) {
matches.push.apply( matches, actions );
}
}
// Remove by boolean filters
for ( i = 0, len = matches.length; i < len; i++ ) {
- match = matches[i];
+ match = matches[ i ];
if (
( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
}
// Remove duplicates
for ( i = 0, len = matches.length; i < len; i++ ) {
- match = matches[i];
+ match = matches[ i ];
index = matches.lastIndexOf( match );
while ( index !== i ) {
matches.splice( index, 1 );
this.changing = true;
for ( i = 0, len = this.list.length; i < len; i++ ) {
- action = this.list[i];
+ action = this.list[ i ];
action.toggle( action.hasMode( mode ) );
}
var i, len, action, item;
for ( i = 0, len = this.list.length; i < len; i++ ) {
- item = this.list[i];
+ item = this.list[ i ];
action = item.getAction();
- if ( actions[action] !== undefined ) {
- item.setDisabled( !actions[action] );
+ if ( actions[ action ] !== undefined ) {
+ item.setDisabled( !actions[ action ] );
}
}
this.changing = true;
for ( i = 0, len = actions.length; i < len; i++ ) {
- action = actions[i];
+ action = actions[ i ];
action.connect( this, {
click: [ 'emit', 'click', action ],
resize: [ 'emit', 'resize', action ],
this.changing = true;
for ( i = 0, len = actions.length; i < len; i++ ) {
- action = actions[i];
+ action = actions[ i ];
index = this.list.indexOf( action );
if ( index !== -1 ) {
action.disconnect( this );
this.changing = true;
for ( i = 0, len = this.list.length; i < len; i++ ) {
- action = this.list[i];
+ action = this.list[ i ];
action.disconnect( this );
}
this.special = {};
this.others = [];
for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
- action = this.list[i];
+ action = this.list[ i ];
if ( action.isVisible() ) {
// Populate categories
for ( category in this.categories ) {
- if ( !this.categorized[category] ) {
- this.categorized[category] = {};
+ if ( !this.categorized[ category ] ) {
+ this.categorized[ category ] = {};
}
- list = action[this.categories[category]]();
+ list = action[ this.categories[ category ] ]();
if ( !Array.isArray( list ) ) {
list = [ list ];
}
for ( j = 0, jLen = list.length; j < jLen; j++ ) {
- item = list[j];
- if ( !this.categorized[category][item] ) {
- this.categorized[category][item] = [];
+ item = list[ j ];
+ if ( !this.categorized[ category ][ item ] ) {
+ this.categorized[ category ][ item ] = [];
}
- this.categorized[category][item].push( action );
+ this.categorized[ category ][ item ].push( action );
}
}
// Populate special/others
special = false;
for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
- flag = specialFlags[j];
- if ( !this.special[flag] && action.hasFlag( flag ) ) {
- this.special[flag] = action;
+ flag = specialFlags[ j ];
+ if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
+ this.special[ flag ] = action;
special = true;
break;
}
};
/**
- * DOM element abstraction.
+ * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
+ * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
+ * connected to them and can't be interacted with.
*
* @abstract
* @class
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {Function} [$] jQuery for the frame the widget is in
* @cfg {string[]} [classes] CSS class names to add
* @cfg {string} [id] HTML id attribute
* @cfg {string} [text] Text to insert
config = config || {};
// Properties
- this.$ = config.$ || OO.ui.Element.static.getJQuery( document );
+ this.$ = $;
this.data = config.data;
- this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
+ this.$element = $( document.createElement( this.getTagName() ) );
this.elementGroup = null;
this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
this.updateThemeClassesPending = false;
// Initialization
- if ( $.isArray( config.classes ) ) {
+ if ( Array.isArray( config.classes ) ) {
this.$element.addClass( config.classes.join( ' ' ) );
}
if ( config.id ) {
*/
OO.ui.Element.static.getDocument = function ( obj ) {
// jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
- return ( obj[0] && obj[0].ownerDocument ) ||
+ return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
// Empty jQuery selections might have a context
obj.context ||
// HTMLElement
var isDoc, isWin;
if ( obj instanceof jQuery ) {
- obj = obj[0];
+ obj = obj[ 0 ];
}
isDoc = obj.nodeType === 9;
isWin = obj.document !== undefined;
// Get iframe element
frames = from.parent.document.getElementsByTagName( 'iframe' );
for ( i = 0, len = frames.length; i < len; i++ ) {
- if ( frames[i].contentWindow === from ) {
- frame = frames[i];
+ if ( frames[ i ].contentWindow === from ) {
+ frame = frames[ i ];
break;
}
}
}
while ( $parent.length ) {
- if ( $parent[0] === this.getRootScrollableElement( el ) ) {
- return $parent[0];
+ if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
+ return $parent[ 0 ];
}
i = props.length;
while ( i-- ) {
- val = $parent.css( props[i] );
+ val = $parent.css( props[ i ] );
if ( val === 'auto' || val === 'scroll' ) {
- return $parent[0];
+ return $parent[ 0 ];
}
}
$parent = $parent.parent();
}
};
+/**
+ * Force the browser to reconsider whether it really needs to render scrollbars inside the element
+ * and reserve space for them, because it probably doesn't.
+ *
+ * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
+ * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
+ * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
+ * and then reattach (or show) them back.
+ *
+ * @static
+ * @param {HTMLElement} el Element to reconsider the scrollbars on
+ */
+OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
+ var i, len, nodes = [];
+ // Detach all children
+ while ( el.firstChild ) {
+ nodes.push( el.firstChild );
+ el.removeChild( el.firstChild );
+ }
+ // Force reflow
+ void el.offsetHeight;
+ // Reattach all children
+ for ( i = 0, len = nodes.length; i < len; i++ ) {
+ el.appendChild( nodes[ i ] );
+ }
+};
+
/* Methods */
/**
var i, len,
support = 0;
- methods = $.isArray( methods ) ? methods : [ methods ];
+ methods = Array.isArray( methods ) ? methods : [ methods ];
for ( i = 0, len = methods.length; i < len; i++ ) {
- if ( $.isFunction( this[methods[i]] ) ) {
+ if ( $.isFunction( this[ methods[ i ] ] ) ) {
support++;
}
}
* @return {boolean} The element is attached to the DOM
*/
OO.ui.Element.prototype.isElementAttached = function () {
- return $.contains( this.getElementDocument(), this.$element[0] );
+ return $.contains( this.getElementDocument(), this.$element[ 0 ] );
};
/**
* @return {HTMLDocument} Document object
*/
OO.ui.Element.prototype.getElementDocument = function () {
- // Don't use this.$.context because subclasses can rebind this.$
// Don't cache this in other ways either because subclasses could can change this.$element
return OO.ui.Element.static.getDocument( this.$element );
};
* Get closest scrollable container.
*/
OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
- return OO.ui.Element.static.getClosestScrollableContainer( this.$element[0] );
+ return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
};
/**
* @param {Object} [config] Configuration options
*/
OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
- return OO.ui.Element.static.scrollIntoView( this.$element[0], config );
+ return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
};
/**
OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
/**
- * User interface control.
+ * Widgets are compositions of one or more OOjs UI elements that users can both view
+ * and interact with. All widgets can be configured and modified via a standard API,
+ * and their state can change dynamically according to a model.
*
* @abstract
* @class
if ( isDisabled !== this.wasDisabled ) {
this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
+ this.$element.attr( 'aria-disabled', isDisabled.toString() );
this.emit( 'disable', isDisabled );
this.updateThemeClasses();
}
if ( show !== this.isVisible() ) {
this.visible = show;
- this.$element.toggle( show );
+ this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
this.emit( 'toggle', show );
}
};
/**
- * Container for elements in a child frame.
+ * A window is a container for elements that are in a child frame. They are used with
+ * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
+ * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
+ * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
+ * the window manager will choose a sensible fallback.
*
- * Use together with OO.ui.WindowManager.
- *
- * @abstract
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
+ * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
+ * different processes are executed:
*
- * When a window is opened, the setup and ready processes are executed. Similarly, the hold and
- * teardown processes are executed when the window is closed.
+ * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
+ * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
+ * the window.
*
- * - {@link OO.ui.WindowManager#openWindow} or {@link #open} methods are used to start opening
- * - Window manager begins opening window
* - {@link #getSetupProcess} method is called and its result executed
* - {@link #getReadyProcess} method is called and its result executed
- * - Window is now open
*
- * - {@link OO.ui.WindowManager#closeWindow} or {@link #close} methods are used to start closing
- * - Window manager begins closing window
+ * **opened**: The window is now open
+ *
+ * **closing**: The closing stage begins when the window manager's
+ * {@link OO.ui.WindowManager#closeWindow closeWindow}
+ * or the window's {@link #close} methods are used, and the window manager begins to close the window.
+ *
* - {@link #getHoldProcess} method is called and its result executed
- * - {@link #getTeardownProcess} method is called and its result executed
- * - Window is now closed
+ * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
+ *
+ * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
+ * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
+ * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
+ * processing can complete. Always assume window processes are executed asynchronously.
*
- * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding
- * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and
- * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchronous
- * processing can complete. Always assume window processes are executed asynchronously. See
- * OO.ui.Process for more details about how to work with processes. Some events, as well as the
- * #open and #close methods, provide promises which are resolved when the window enters a new state.
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
*
- * Sizing of windows is specified using symbolic names which are interpreted by the window manager.
- * If the requested size is not recognized, the window manager will choose a sensible fallback.
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
*
* @constructor
* @param {Object} [config] Configuration options
// Properties
this.manager = null;
- this.initialized = false;
- this.visible = false;
- this.opening = null;
- this.closing = null;
- this.opened = null;
- this.timing = null;
- this.loading = null;
this.size = config.size || this.constructor.static.size;
- this.$frame = this.$( '<div>' );
- this.$overlay = this.$( '<div>' );
+ this.$frame = $( '<div>' );
+ this.$overlay = $( '<div>' );
+ this.$content = $( '<div>' );
// Initialization
+ this.$overlay.addClass( 'oo-ui-window-overlay' );
+ this.$content
+ .addClass( 'oo-ui-window-content' )
+ .attr( 'tabIndex', 0 );
+ this.$frame
+ .addClass( 'oo-ui-window-frame' )
+ .append( this.$content );
+
this.$element
.addClass( 'oo-ui-window' )
.append( this.$frame, this.$overlay );
- this.$frame.addClass( 'oo-ui-window-frame' );
- this.$overlay.addClass( 'oo-ui-window-overlay' );
- // NOTE: Additional initialization will occur when #setManager is called
+ // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
+ // that reference properties not initialized at that time of parent class construction
+ // TODO: Find a better way to handle post-constructor setup
+ this.visible = false;
+ this.$element.addClass( 'oo-ui-element-hidden' );
};
/* Setup */
*/
OO.ui.Window.static.size = 'medium';
-/* Static Methods */
-
-/**
- * Transplant the CSS styles from as parent document to a frame's document.
- *
- * This loops over the style sheets in the parent document, and copies their nodes to the
- * frame's document. It then polls the document to see when all styles have loaded, and once they
- * have, resolves the promise.
- *
- * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
- * and resolve the promise anyway. This protects against cases like a display: none; iframe in
- * Firefox, where the styles won't load until the iframe becomes visible.
- *
- * For details of how we arrived at the strategy used in this function, see #load.
- *
- * @static
- * @inheritable
- * @param {HTMLDocument} parentDoc Document to transplant styles from
- * @param {HTMLDocument} frameDoc Document to transplant styles to
- * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
- * @return {jQuery.Promise} Promise resolved when styles have loaded
- */
-OO.ui.Window.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) {
- var i, numSheets, styleNode, styleText, newNode, timeoutID, pollNodeId, $pendingPollNodes,
- $pollNodes = $( [] ),
- // Fake font-family value
- fontFamily = 'oo-ui-frame-transplantStyles-loaded',
- nextIndex = parentDoc.oouiFrameTransplantStylesNextIndex || 0,
- deferred = $.Deferred();
-
- for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
- styleNode = parentDoc.styleSheets[i].ownerNode;
- if ( styleNode.disabled ) {
- continue;
- }
-
- if ( styleNode.nodeName.toLowerCase() === 'link' ) {
- // External stylesheet; use @import
- styleText = '@import url(' + styleNode.href + ');';
- } else {
- // Internal stylesheet; just copy the text
- // For IE10 we need to fall back to .cssText, BUT that's undefined in
- // other browsers, so fall back to '' rather than 'undefined'
- styleText = styleNode.textContent || parentDoc.styleSheets[i].cssText || '';
- }
-
- // Create a node with a unique ID that we're going to monitor to see when the CSS
- // has loaded
- if ( styleNode.oouiFrameTransplantStylesId ) {
- // If we're nesting transplantStyles operations and this node already has
- // a CSS rule to wait for loading, reuse it
- pollNodeId = styleNode.oouiFrameTransplantStylesId;
- } else {
- // Otherwise, create a new ID
- pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + nextIndex;
- nextIndex++;
-
- // Add #pollNodeId { font-family: ... } to the end of the stylesheet / after the @import
- // The font-family rule will only take effect once the @import finishes
- styleText += '\n' + '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
- }
-
- // Create a node with id=pollNodeId
- $pollNodes = $pollNodes.add( $( '<div>', frameDoc )
- .attr( 'id', pollNodeId )
- .appendTo( frameDoc.body )
- );
-
- // Add our modified CSS as a <style> tag
- newNode = frameDoc.createElement( 'style' );
- newNode.textContent = styleText;
- newNode.oouiFrameTransplantStylesId = pollNodeId;
- frameDoc.head.appendChild( newNode );
- }
- frameDoc.oouiFrameTransplantStylesNextIndex = nextIndex;
-
- // Poll every 100ms until all external stylesheets have loaded
- $pendingPollNodes = $pollNodes;
- timeoutID = setTimeout( function pollExternalStylesheets() {
- while (
- $pendingPollNodes.length > 0 &&
- $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
- ) {
- $pendingPollNodes = $pendingPollNodes.slice( 1 );
- }
-
- if ( $pendingPollNodes.length === 0 ) {
- // We're done!
- if ( timeoutID !== null ) {
- timeoutID = null;
- $pollNodes.remove();
- deferred.resolve();
- }
- } else {
- timeoutID = setTimeout( pollExternalStylesheets, 100 );
- }
- }, 100 );
- // ...but give up after a while
- if ( timeout !== 0 ) {
- setTimeout( function () {
- if ( timeoutID ) {
- clearTimeout( timeoutID );
- timeoutID = null;
- $pollNodes.remove();
- deferred.reject();
- }
- }, timeout || 5000 );
- }
-
- return deferred.promise();
-};
-
/* Methods */
/**
*/
OO.ui.Window.prototype.onMouseDown = function ( e ) {
// Prevent clicking on the click-block from stealing focus
- if ( e.target === this.$element[0] ) {
+ if ( e.target === this.$element[ 0 ] ) {
return false;
}
};
/**
* Check if window has been initialized.
*
+ * Initialization occurs when a window is added to a manager.
+ *
* @return {boolean} Window has been initialized
*/
OO.ui.Window.prototype.isInitialized = function () {
- return this.initialized;
+ return !!this.manager;
};
/**
return this.visible;
};
-/**
- * Check if window is loading.
- *
- * @return {boolean} Window is loading
- */
-OO.ui.Window.prototype.isLoading = function () {
- return this.loading && this.loading.state() === 'pending';
-};
-
-/**
- * Check if window is loaded.
- *
- * @return {boolean} Window is loaded
- */
-OO.ui.Window.prototype.isLoaded = function () {
- return this.loading && this.loading.state() === 'resolved';
-};
-
/**
* Check if window is opening.
*
// Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
// Disable transitions first, otherwise we'll get values from when the window was animating.
var oldTransition,
- styleObj = this.$frame[0].style;
+ styleObj = this.$frame[ 0 ].style;
oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
styleObj.MozTransition || styleObj.WebkitTransition;
styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
OO.ui.Window.prototype.getContentHeight = function () {
var bodyHeight,
win = this,
- bodyStyleObj = this.$body[0].style,
- frameStyleObj = this.$frame[0].style;
+ bodyStyleObj = this.$body[ 0 ].style,
+ frameStyleObj = this.$frame[ 0 ].style;
// Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
// Disable transitions first, otherwise we'll get values from when the window was animating.
* @return {number} Height of content
*/
OO.ui.Window.prototype.getBodyHeight = function () {
- return this.$body[0].scrollHeight;
+ return this.$body[ 0 ].scrollHeight;
};
/**
/**
* Toggle visibility of window.
*
- * If the window is isolated and hasn't fully loaded yet, the visibility property will be used
- * instead of display.
- *
* @param {boolean} [show] Make window visible, omit to toggle visibility
* @fires toggle
* @chainable
if ( show !== this.isVisible() ) {
this.visible = show;
-
- if ( this.isolated && !this.isLoaded() ) {
- // Hide the window using visibility instead of display until loading is complete
- // Can't use display: none; because that prevents the iframe from loading in Firefox
- this.$element.css( 'visibility', show ? 'visible' : 'hidden' );
- } else {
- this.$element.toggle( show ).css( 'visibility', '' );
- }
+ this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
this.emit( 'toggle', show );
}
/**
* Set the window manager.
*
- * This must be called before initialize. Calling it more than once will cause an error.
+ * This will cause the window to initialize. Calling it more than once will cause an error.
*
* @param {OO.ui.WindowManager} manager Manager for this window
* @throws {Error} If called more than once
throw new Error( 'Cannot set window manager, window already has a manager' );
}
- // Properties
this.manager = manager;
- this.isolated = manager.shouldIsolate();
-
- // Initialization
- if ( this.isolated ) {
- this.$iframe = this.$( '<iframe>' );
- this.$iframe.attr( { frameborder: 0, scrolling: 'no' } );
- this.$frame.append( this.$iframe );
- this.$ = function () {
- throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
- };
- // WARNING: Do not use this.$ again until #initialize is called
- } else {
- this.$content = this.$( '<div>' );
- this.$document = $( this.getElementDocument() );
- this.$content.addClass( 'oo-ui-window-content' ).attr( 'tabIndex', 0 );
- this.$frame.append( this.$content );
- }
- this.toggle( false );
-
- // Figure out directionality:
- this.dir = OO.ui.Element.static.getDir( this.$iframe || this.$content ) || 'ltr';
+ this.initialize();
return this;
};
*/
OO.ui.Window.prototype.setSize = function ( size ) {
this.size = size;
+ this.updateSize();
+ return this;
+};
+
+/**
+ * Update the window size.
+ *
+ * @throws {Error} If not attached to a manager
+ * @chainable
+ */
+OO.ui.Window.prototype.updateSize = function () {
+ if ( !this.manager ) {
+ throw new Error( 'Cannot update window size, must be attached to a manager' );
+ }
+
this.manager.updateWindowSize( this );
+
return this;
};
OO.ui.Window.prototype.setDimensions = function ( dim ) {
var height,
win = this,
- styleObj = this.$frame[0].style;
+ styleObj = this.$frame[ 0 ].style;
// Calculate the height we need to set using the correct width
if ( dim.height === undefined ) {
/**
* Initialize window contents.
*
- * The first time the window is opened, #initialize is called when it's safe to begin populating
- * its contents. See #getSetupProcess for a way to make changes each time the window opens.
- *
- * Once this method is called, this.$ can be used to create elements within the frame.
+ * The first time the window is opened, #initialize is called so that changes to the window that
+ * will persist between openings can be made. See #getSetupProcess for a way to make changes each
+ * time the window opens.
*
* @throws {Error} If not attached to a manager
* @chainable
}
// Properties
- this.$head = this.$( '<div>' );
- this.$body = this.$( '<div>' );
- this.$foot = this.$( '<div>' );
- this.$innerOverlay = this.$( '<div>' );
+ this.$head = $( '<div>' );
+ this.$body = $( '<div>' );
+ this.$foot = $( '<div>' );
+ this.$innerOverlay = $( '<div>' );
+ this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
+ this.$document = $( this.getElementDocument() );
// Events
this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
* @param {Object} [data] Window opening data
* @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
* first argument will be a promise which will be resolved when the window begins closing
+ * @throws {Error} If not attached to a manager
*/
OO.ui.Window.prototype.open = function ( data ) {
+ if ( !this.manager ) {
+ throw new Error( 'Cannot open window, must be attached to a manager' );
+ }
+
return this.manager.openWindow( this, data );
};
*
* @param {Object} [data] Window closing data
* @return {jQuery.Promise} Promise resolved when window is closed
+ * @throws {Error} If not attached to a manager
*/
OO.ui.Window.prototype.close = function ( data ) {
+ if ( !this.manager ) {
+ throw new Error( 'Cannot close window, must be attached to a manager' );
+ }
+
return this.manager.closeWindow( this, data );
};
var win = this,
deferred = $.Deferred();
- this.$element.show();
- this.visible = true;
+ this.toggle( true );
+
this.getSetupProcess( data ).execute().done( function () {
// Force redraw by asking the browser to measure the elements' widths
- win.$element.addClass( 'oo-ui-window-setup' ).width();
+ win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
win.$content.addClass( 'oo-ui-window-content-setup' ).width();
deferred.resolve();
} );
// Blur the focused element
if ( $focus.length ) {
- $focus[0].blur();
+ $focus[ 0 ].blur();
}
// Force redraw by asking the browser to measure the elements' widths
* @return {jQuery.Promise} Promise resolved when window is torn down
*/
OO.ui.Window.prototype.teardown = function ( data ) {
- var win = this,
- deferred = $.Deferred();
-
- this.getTeardownProcess( data ).execute().done( function () {
- // Force redraw by asking the browser to measure the elements' widths
- win.$element.removeClass( 'oo-ui-window-load oo-ui-window-setup' ).width();
- win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
- win.$element.hide();
- win.visible = false;
- deferred.resolve();
- } );
-
- return deferred.promise();
-};
-
-/**
- * Load the frame contents.
- *
- * Once the iframe's stylesheets are loaded the returned promise will be resolved. Calling while
- * loading will return a promise but not trigger a new loading cycle. Calling after loading is
- * complete will return a promise that's already been resolved.
- *
- * Sounds simple right? Read on...
- *
- * When you create a dynamic iframe using open/write/close, the window.load event for the
- * iframe is triggered when you call close, and there's no further load event to indicate that
- * everything is actually loaded.
- *
- * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
- * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
- * are added to document.styleSheets immediately, and the only way you can determine whether they've
- * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
- * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
- *
- * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>`
- * tags. Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets
- * until the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the
- * `@import` has finished. And because the contents of the `<style>` tag are from the same origin,
- * accessing .cssRules is allowed.
- *
- * However, now that we control the styles we're injecting, we might as well do away with
- * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
- * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
- * and wait for its font-family to change to someValue. Because `@import` is blocking, the
- * font-family rule is not applied until after the `@import` finishes.
- *
- * All this stylesheet injection and polling magic is in #transplantStyles.
- *
- * @return {jQuery.Promise} Promise resolved when loading is complete
- */
-OO.ui.Window.prototype.load = function () {
- var sub, doc, loading,
- win = this;
-
- this.$element.addClass( 'oo-ui-window-load' );
-
- // Non-isolated windows are already "loaded"
- if ( !this.loading && !this.isolated ) {
- this.loading = $.Deferred().resolve();
- this.initialize();
- // Set initialized state after so sub-classes aren't confused by it being set by calling
- // their parent initialize method
- this.initialized = true;
- }
-
- // Return existing promise if already loading or loaded
- if ( this.loading ) {
- return this.loading.promise();
- }
-
- // Load the frame
- loading = this.loading = $.Deferred();
- sub = this.$iframe.prop( 'contentWindow' );
- doc = sub.document;
-
- // Initialize contents
- doc.open();
- doc.write(
- '<!doctype html>' +
- '<html>' +
- '<body class="oo-ui-window-isolated oo-ui-' + this.dir + '"' +
- ' style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
- '<div class="oo-ui-window-content"></div>' +
- '</body>' +
- '</html>'
- );
- doc.close();
-
- // Properties
- this.$ = OO.ui.Element.static.getJQuery( doc, this.$iframe );
- this.$content = this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 );
- this.$document = this.$( doc );
-
- // Initialization
- this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
- .always( function () {
- // Initialize isolated windows
- win.initialize();
- // Set initialized state after so sub-classes aren't confused by it being set by calling
- // their parent initialize method
- win.initialized = true;
- // Undo the visibility: hidden; hack and apply display: none;
- // We can do this safely now that the iframe has initialized
- // (don't do this from within #initialize because it has to happen
- // after the all subclasses have been handled as well).
- win.toggle( win.isVisible() );
-
- loading.resolve();
+ var win = this;
+
+ return this.getTeardownProcess( data ).execute()
+ .done( function () {
+ // Force redraw by asking the browser to measure the elements' widths
+ win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
+ win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
+ win.toggle( false );
} );
-
- return loading.promise();
};
/**
- * Base class for all dialogs.
- *
- * Logic:
- * - Manage the window (open and close, etc.).
- * - Store the internal name and display title.
- * - A stack to track one or more pending actions.
- * - Manage a set of actions that can be performed.
- * - Configure and create action widgets.
- *
- * User interface:
- * - Close the dialog with Escape key.
- * - Visually lock the dialog while an action is in
- * progress (aka "pending").
- *
- * Subclass responsibilities:
- * - Display the title somewhere.
- * - Add content to the dialog.
- * - Provide a UI to close the dialog.
- * - Display the action widgets somewhere.
+ * The Dialog class serves as the base class for the other types of dialogs.
+ * Unless extended to include controls, the rendered dialog box is a simple window
+ * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
+ * which opens, closes, and controls the presentation of the window. See the
+ * [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * @example
+ * // A simple dialog window.
+ * function MyDialog( config ) {
+ * MyDialog.super.call( this, config );
+ * }
+ * OO.inheritClass( MyDialog, OO.ui.Dialog );
+ * MyDialog.prototype.initialize = function () {
+ * MyDialog.super.prototype.initialize.call( this );
+ * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
+ * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
+ * this.$body.append( this.content.$element );
+ * };
+ * MyDialog.prototype.getBodyHeight = function () {
+ * return this.content.$element.outerHeight( true );
+ * };
+ * var myDialog = new MyDialog( {
+ * size: 'medium'
+ * } );
+ * // Create and append a window manager, which opens and closes the window.
+ * var windowManager = new OO.ui.WindowManager();
+ * $( 'body' ).append( windowManager.$element );
+ * windowManager.addWindows( [ myDialog ] );
+ * // Open the window!
+ * windowManager.openWindow( myDialog );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
*
* @abstract
* @class
);
for ( i = 0, len = actions.length; i < len; i++ ) {
items.push(
- new OO.ui.ActionWidget( $.extend( { $: this.$ }, actions[i] ) )
+ new OO.ui.ActionWidget( actions[ i ] )
);
}
this.actions.add( items );
OO.ui.Dialog.super.prototype.initialize.call( this );
// Properties
- this.title = new OO.ui.LabelWidget( { $: this.$ } );
+ this.title = new OO.ui.LabelWidget();
// Initialization
this.$content.addClass( 'oo-ui-dialog-content' );
// Detach all actions that may have been previously attached
for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
- this.attachedActions[i].$element.detach();
+ this.attachedActions[ i ].$element.detach();
}
this.attachedActions = [];
};
};
/**
- * Collection of windows.
+ * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
+ * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
+ * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
+ * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
+ * pertinent data and reused.
+ *
+ * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
+ * `opened`, and `closing`, which represent the primary stages of the cycle:
+ *
+ * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
+ * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
+ *
+ * - an `opening` event is emitted with an `opening` promise
+ * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
+ * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
+ * window and its result executed
+ * - a `setup` progress notification is emitted from the `opening` promise
+ * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
+ * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
+ * window and its result executed
+ * - a `ready` progress notification is emitted from the `opening` promise
+ * - the `opening` promise is resolved with an `opened` promise
+ *
+ * **Opened**: the window is now open.
+ *
+ * **Closing**: the closing stage begins when the window manager's #closeWindow or the
+ * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
+ * to close the window.
+ *
+ * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
+ * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
+ * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
+ * window and its result executed
+ * - a `hold` progress notification is emitted from the `closing` promise
+ * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
+ * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
+ * window and its result executed
+ * - a `teardown` progress notification is emitted from the `closing` promise
+ * - the `closing` promise is resolved. The window is now closed
+ *
+ * See the [OOjs UI documentation on MediaWiki][1] for more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
*
* @class
* @extends OO.ui.Element
* @mixins OO.EventEmitter
*
- * Managed windows are mutually exclusive. If a window is opened while there is a current window
- * already opening or opened, the current window will be closed without data. Empty closing data
- * should always result in the window being closed without causing constructive or destructive
- * action.
- *
- * As a window is opened and closed, it passes through several stages and the manager emits several
- * corresponding events.
- *
- * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening
- * - {@link #event-opening} is emitted with `opening` promise
- * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution
- * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed
- * - `setup` progress notification is emitted from opening promise
- * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution
- * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed
- * - `ready` progress notification is emitted from opening promise
- * - `opening` promise is resolved with `opened` promise
- * - Window is now open
- *
- * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing
- * - `opened` promise is resolved with `closing` promise
- * - {@link #event-closing} is emitted with `closing` promise
- * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution
- * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed
- * - `hold` progress notification is emitted from opening promise
- * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution
- * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed
- * - `teardown` progress notification is emitted from opening promise
- * - Closing promise is resolved
- * - Window is now closed
- *
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {boolean} [isolate] Configure managed windows to isolate their content using inline frames
* @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
* @cfg {boolean} [modal=true] Prevent interaction outside the dialog
*/
// Properties
this.factory = config.factory;
this.modal = config.modal === undefined || !!config.modal;
- this.isolate = !!config.isolate;
this.windows = {};
this.opening = null;
this.opened = null;
this.closing = null;
this.preparingToOpen = null;
this.preparingToClose = null;
- this.size = null;
this.currentWindow = null;
this.$ariaHidden = null;
- this.requestedSize = null;
this.onWindowResizeTimeout = null;
this.onWindowResizeHandler = this.onWindowResize.bind( this );
this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
- this.onWindowMouseWheelHandler = this.onWindowMouseWheel.bind( this );
- this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
// Initialization
this.$element
}
};
-/**
- * Handle window mouse wheel events.
- *
- * @param {jQuery.Event} e Mouse wheel event
- */
-OO.ui.WindowManager.prototype.onWindowMouseWheel = function () {
- // Kill all events in the parent window if the child window is isolated
- return !this.shouldIsolate();
-};
-
-/**
- * Handle document key down events.
- *
- * @param {jQuery.Event} e Key down event
- */
-OO.ui.WindowManager.prototype.onDocumentKeyDown = function ( e ) {
- switch ( e.which ) {
- case OO.ui.Keys.PAGEUP:
- case OO.ui.Keys.PAGEDOWN:
- case OO.ui.Keys.END:
- case OO.ui.Keys.HOME:
- case OO.ui.Keys.LEFT:
- case OO.ui.Keys.UP:
- case OO.ui.Keys.RIGHT:
- case OO.ui.Keys.DOWN:
- // Kill all events in the parent window if the child window is isolated
- return !this.shouldIsolate();
- }
-};
-
/**
* Check if window is opening.
*
return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
};
-/**
- * Check if window contents should be isolated.
- *
- * Window content isolation is done using inline frames.
- *
- * @return {boolean} Window contents should be isolated
- */
-OO.ui.WindowManager.prototype.shouldIsolate = function () {
- return this.isolate;
-};
-
/**
* Check if a window is being managed.
*
var name;
for ( name in this.windows ) {
- if ( this.windows[name] === win ) {
+ if ( this.windows[ name ] === win ) {
return true;
}
}
*/
OO.ui.WindowManager.prototype.getWindow = function ( name ) {
var deferred = $.Deferred(),
- win = this.windows[name];
+ win = this.windows[ name ];
if ( !( win instanceof OO.ui.Window ) ) {
if ( this.factory ) {
'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
) );
} else {
- win = this.factory.create( name, this, { $: this.$ } );
+ win = this.factory.create( name, this );
this.addWindows( [ win ] );
deferred.resolve( win );
}
*/
OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
var manager = this,
- preparing = [],
opening = $.Deferred();
// Argument handling
// Window opening
if ( opening.state() !== 'rejected' ) {
- if ( !win.getManager() ) {
- win.setManager( this );
- }
- preparing.push( win.load() );
-
- if ( this.closing ) {
- // If a window is currently closing, wait for it to complete
- preparing.push( this.closing );
- }
-
- this.preparingToOpen = $.when.apply( $, preparing );
+ // If a window is currently closing, wait for it to complete
+ this.preparingToOpen = $.when( this.closing );
// Ensure handlers get called after preparingToOpen is set
this.preparingToOpen.done( function () {
if ( manager.modal ) {
*/
OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
var manager = this,
- preparing = [],
closing = $.Deferred(),
opened;
// Argument handling
if ( typeof win === 'string' ) {
- win = this.windows[win];
+ win = this.windows[ win ];
} else if ( !this.hasWindow( win ) ) {
win = null;
}
// Window closing
if ( closing.state() !== 'rejected' ) {
- if ( this.opening ) {
- // If the window is currently opening, close it when it's done
- preparing.push( this.opening );
- }
-
- this.preparingToClose = $.when.apply( $, preparing );
+ // If the window is currently opening, close it when it's done
+ this.preparingToClose = $.when( this.opening );
// Ensure handlers get called after preparingToClose is set
this.preparingToClose.done( function () {
manager.closing = closing;
OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
var i, len, win, name, list;
- if ( $.isArray( windows ) ) {
+ if ( Array.isArray( windows ) ) {
// Convert to map of windows by looking up symbolic names from static configuration
list = {};
for ( i = 0, len = windows.length; i < len; i++ ) {
- name = windows[i].constructor.static.name;
+ name = windows[ i ].constructor.static.name;
if ( typeof name !== 'string' ) {
throw new Error( 'Cannot add window' );
}
- list[name] = windows[i];
+ list[ name ] = windows[ i ];
}
} else if ( $.isPlainObject( windows ) ) {
list = windows;
// Add windows
for ( name in list ) {
- win = list[name];
- this.windows[name] = win;
+ win = list[ name ];
+ this.windows[ name ] = win.toggle( false );
this.$element.append( win.$element );
+ win.setManager( this );
}
};
manager = this,
promises = [],
cleanup = function ( name, win ) {
- delete manager.windows[name];
+ delete manager.windows[ name ];
win.$element.detach();
};
for ( i = 0, len = names.length; i < len; i++ ) {
- name = names[i];
- win = this.windows[name];
+ name = names[ i ];
+ win = this.windows[ name ];
if ( !win ) {
throw new Error( 'Cannot remove window' );
}
sizes = this.constructor.static.sizes,
size = win.getSize();
- if ( !sizes[size] ) {
+ if ( !sizes[ size ] ) {
size = this.constructor.static.defaultSize;
}
- if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[size].width ) {
+ if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
size = 'full';
}
this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
- win.setDimensions( sizes[size] );
+ win.setDimensions( sizes[ size ] );
this.emit( 'resize', win );
if ( on ) {
if ( !this.globalEvents ) {
- this.$( this.getElementDocument() ).on( {
- // Prevent scrolling by keys in top-level window
- keydown: this.onDocumentKeyDownHandler
- } );
- this.$( this.getElementWindow() ).on( {
- // Prevent scrolling by wheel in top-level window
- mousewheel: this.onWindowMouseWheelHandler,
+ $( this.getElementWindow() ).on( {
// Start listening for top-level window dimension changes
'orientationchange resize': this.onWindowResizeHandler
} );
- // Disable window scrolling in isolated windows
- if ( !this.shouldIsolate() ) {
- $( this.getElementDocument().body ).css( 'overflow', 'hidden' );
- }
+ $( this.getElementDocument().body ).css( 'overflow', 'hidden' );
this.globalEvents = true;
}
} else if ( this.globalEvents ) {
- // Unbind global events
- this.$( this.getElementDocument() ).off( {
- // Allow scrolling by keys in top-level window
- keydown: this.onDocumentKeyDownHandler
- } );
- this.$( this.getElementWindow() ).off( {
- // Allow scrolling by wheel in top-level window
- mousewheel: this.onWindowMouseWheelHandler,
+ $( this.getElementWindow() ).off( {
// Stop listening for top-level window dimension changes
'orientationchange resize': this.onWindowResizeHandler
} );
- if ( !this.shouldIsolate() ) {
- $( this.getElementDocument().body ).css( 'overflow', '' );
- }
+ $( this.getElementDocument().body ).css( 'overflow', '' );
this.globalEvents = false;
}
// Use rejected promise for error
return $.Deferred().reject( [ result ] ).promise();
}
- if ( $.isArray( result ) && result.length && result[0] instanceof OO.ui.Error ) {
+ if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
// Use rejected promise for list of errors
return $.Deferred().reject( result ).promise();
}
if ( this.steps.length ) {
// Generate a chain reaction of promises
- promise = proceed( this.steps[0] )();
+ promise = proceed( this.steps[ 0 ] )();
for ( i = 1, len = this.steps.length; i < len; i++ ) {
- promise = promise.then( proceed( this.steps[i] ) );
+ promise = promise.then( proceed( this.steps[ i ] ) );
}
} else {
promise = $.Deferred().resolve().promise();
// Auto
for ( i = 0, len = included.length; i < len; i++ ) {
- if ( !used[included[i]] ) {
- auto.push( included[i] );
+ if ( !used[ included[ i ] ] ) {
+ auto.push( included[ i ] );
}
}
if ( collection === '*' ) {
for ( name in this.registry ) {
- tool = this.registry[name];
+ tool = this.registry[ name ];
if (
// Only add tools by group name when auto-add is enabled
tool.static.autoAddToCatchall &&
// Exclude already used tools
- ( !used || !used[name] )
+ ( !used || !used[ name ] )
) {
names.push( name );
if ( used ) {
- used[name] = true;
+ used[ name ] = true;
}
}
}
- } else if ( $.isArray( collection ) ) {
+ } else if ( Array.isArray( collection ) ) {
for ( i = 0, len = collection.length; i < len; i++ ) {
- item = collection[i];
+ item = collection[ i ];
// Allow plain strings as shorthand for named tools
if ( typeof item === 'string' ) {
item = { name: item };
if ( OO.isPlainObject( item ) ) {
if ( item.group ) {
for ( name in this.registry ) {
- tool = this.registry[name];
+ tool = this.registry[ name ];
if (
// Include tools with matching group
tool.static.group === item.group &&
// Only add tools by group name when auto-add is enabled
tool.static.autoAddToGroup &&
// Exclude already used tools
- ( !used || !used[name] )
+ ( !used || !used[ name ] )
) {
names.push( name );
if ( used ) {
- used[name] = true;
+ used[ name ] = true;
}
}
}
// Include tools with matching name and exclude already used tools
- } else if ( item.name && ( !used || !used[item.name] ) ) {
+ } else if ( item.name && ( !used || !used[ item.name ] ) ) {
names.push( item.name );
if ( used ) {
- used[item.name] = true;
+ used[ item.name ] = true;
}
}
}
// Register default toolgroups
for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
- this.register( defaultClasses[i] );
+ this.register( defaultClasses[ i ] );
}
};
};
/**
- * Element with a button.
- *
- * Buttons are used for controls which can be clicked. They can be configured to use tab indexing
- * and access keys for accessibility purposes.
+ * Element supporting "sequential focus navigation" using the 'tabindex' attribute.
*
* @abstract
* @class
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
- * @cfg {boolean} [framed=true] Render button with a frame
- * @cfg {number} [tabIndex=0] Button's tab index. Use 0 to use default ordering, use -1 to prevent
- * tab focusing.
- * @cfg {string} [accessKey] Button's access key
+ * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element
+ * @cfg {number|null} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
+ * prevent tab focusing, use null to suppress the `tabindex` attribute.
*/
-OO.ui.ButtonElement = function OoUiButtonElement( config ) {
+OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) {
// Configuration initialization
- config = config || {};
+ config = $.extend( { tabIndex: 0 }, config );
// Properties
- this.$button = null;
- this.framed = null;
+ this.$tabIndexed = null;
this.tabIndex = null;
- this.accessKey = null;
- this.active = false;
- this.onMouseUpHandler = this.onMouseUp.bind( this );
- this.onMouseDownHandler = this.onMouseDown.bind( this );
+
+ // Events
+ this.connect( this, { disable: 'onDisable' } );
// Initialization
- this.$element.addClass( 'oo-ui-buttonElement' );
- this.toggleFramed( config.framed === undefined || config.framed );
- this.setTabIndex( config.tabIndex || 0 );
- this.setAccessKey( config.accessKey );
- this.setButtonElement( config.$button || this.$( '<a>' ) );
+ this.setTabIndex( config.tabIndex );
+ this.setTabIndexedElement( config.$tabIndexed || this.$element );
};
/* Setup */
-OO.initClass( OO.ui.ButtonElement );
-
-/* Static Properties */
-
-/**
- * Cancel mouse down events.
- *
- * @static
- * @inheritable
- * @property {boolean}
- */
-OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
+OO.initClass( OO.ui.TabIndexedElement );
/* Methods */
/**
- * Set the button element.
+ * Set the element with `tabindex` attribute.
*
* If an element is already set, it will be cleaned up before setting up the new element.
*
- * @param {jQuery} $button Element to use as button
+ * @param {jQuery} $tabIndexed Element to set tab index on
+ * @chainable
*/
-OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
- if ( this.$button ) {
- this.$button
- .removeClass( 'oo-ui-buttonElement-button' )
- .removeAttr( 'role accesskey tabindex' )
- .off( 'mousedown', this.onMouseDownHandler );
- }
-
- this.$button = $button
- .addClass( 'oo-ui-buttonElement-button' )
- .attr( { role: 'button', accesskey: this.accessKey, tabindex: this.tabIndex } )
- .on( 'mousedown', this.onMouseDownHandler );
+OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
+ var tabIndex = this.tabIndex;
+ // Remove attributes from old $tabIndexed
+ this.setTabIndex( null );
+ // Force update of new $tabIndexed
+ this.$tabIndexed = $tabIndexed;
+ this.tabIndex = tabIndex;
+ return this.updateTabIndex();
};
/**
- * Handles mouse down events.
+ * Set tab index value.
*
- * @param {jQuery.Event} e Mouse down event
+ * @param {number|null} tabIndex Tab index value or null for no tab index
+ * @chainable
*/
-OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
- if ( this.isDisabled() || e.which !== 1 ) {
- return false;
- }
- // Remove the tab-index while the button is down to prevent the button from stealing focus
- this.$button.removeAttr( 'tabindex' );
- 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 reapply the tabindex and 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;
+OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
+ tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
+
+ if ( this.tabIndex !== tabIndex ) {
+ this.tabIndex = tabIndex;
+ this.updateTabIndex();
}
+
+ return this;
};
/**
- * Handles mouse up events.
+ * Update the `tabindex` attribute, in case of changes to tab index or
+ * disabled state.
*
- * @param {jQuery.Event} e Mouse up event
+ * @chainable
*/
-OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
+OO.ui.TabIndexedElement.prototype.updateTabIndex = function () {
+ if ( this.$tabIndexed ) {
+ if ( this.tabIndex !== null ) {
+ // Do not index over disabled elements
+ this.$tabIndexed.attr( {
+ tabindex: this.isDisabled() ? -1 : this.tabIndex,
+ // ChromeVox and NVDA do not seem to inherit this from parent elements
+ 'aria-disabled': this.isDisabled().toString()
+ } );
+ } else {
+ this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
+ }
+ }
+ return this;
+};
+
+/**
+ * Handle disable events.
+ *
+ * @param {boolean} disabled Element is disabled
+ */
+OO.ui.TabIndexedElement.prototype.onDisable = function () {
+ this.updateTabIndex();
+};
+
+/**
+ * Get tab index value.
+ *
+ * @return {number|null} Tab index value
+ */
+OO.ui.TabIndexedElement.prototype.getTabIndex = function () {
+ return this.tabIndex;
+};
+
+/**
+ * ButtonElement is often mixed into other classes to generate a button, which is a clickable
+ * interface element that can be configured with access keys for accessibility.
+ * See the [OOjs UI documentation on MediaWiki] [1] for examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
+ * @cfg {boolean} [framed=true] Render button with a frame
+ * @cfg {string} [accessKey] Button's access key
+ */
+OO.ui.ButtonElement = function OoUiButtonElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$button = config.$button || $( '<a>' );
+ this.framed = null;
+ this.accessKey = null;
+ this.active = false;
+ this.onMouseUpHandler = this.onMouseUp.bind( this );
+ this.onMouseDownHandler = this.onMouseDown.bind( this );
+ this.onKeyDownHandler = this.onKeyDown.bind( this );
+ this.onKeyUpHandler = this.onKeyUp.bind( this );
+ this.onClickHandler = this.onClick.bind( this );
+ this.onKeyPressHandler = this.onKeyPress.bind( this );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-buttonElement' );
+ this.toggleFramed( config.framed === undefined || config.framed );
+ this.setAccessKey( config.accessKey );
+ this.setButtonElement( this.$button );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.ButtonElement );
+
+/* Static Properties */
+
+/**
+ * Cancel mouse down events.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
+
+/* Events */
+
+/**
+ * @event click
+ */
+
+/* Methods */
+
+/**
+ * Set the button element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $button Element to use as button
+ */
+OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
+ if ( this.$button ) {
+ this.$button
+ .removeClass( 'oo-ui-buttonElement-button' )
+ .removeAttr( 'role accesskey' )
+ .off( {
+ mousedown: this.onMouseDownHandler,
+ keydown: this.onKeyDownHandler,
+ click: this.onClickHandler,
+ keypress: this.onKeyPressHandler
+ } );
+ }
+
+ this.$button = $button
+ .addClass( 'oo-ui-buttonElement-button' )
+ .attr( { role: 'button', accesskey: this.accessKey } )
+ .on( {
+ mousedown: this.onMouseDownHandler,
+ keydown: this.onKeyDownHandler,
+ click: this.onClickHandler,
+ keypress: this.onKeyPressHandler
+ } );
+};
+
+/**
+ * Handles mouse down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
if ( this.isDisabled() || e.which !== 1 ) {
+ return;
+ }
+ 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;
}
- // Restore the tab-index after the button is up to restore the button's accessibility
- this.$button.attr( 'tabindex', this.tabIndex );
+};
+
+/**
+ * Handles mouse up events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
+ if ( this.isDisabled() || e.which !== 1 ) {
+ return;
+ }
this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
// Stop listening for mouseup, since we only needed this once
this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
};
+/**
+ * Handles mouse click events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse click event
+ * @fires click
+ */
+OO.ui.ButtonElement.prototype.onClick = function ( e ) {
+ if ( !this.isDisabled() && e.which === 1 ) {
+ this.emit( 'click' );
+ }
+ return false;
+};
+
+/**
+ * Handles key down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.ButtonElement.prototype.onKeyDown = function ( e ) {
+ if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
+ return;
+ }
+ this.$element.addClass( 'oo-ui-buttonElement-pressed' );
+ // Run the keyup handler no matter where the key is when the button is let go, so we can
+ // reliably remove the pressed class
+ this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
+};
+
+/**
+ * Handles key up events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key up event
+ */
+OO.ui.ButtonElement.prototype.onKeyUp = function ( e ) {
+ if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
+ return;
+ }
+ this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
+ // Stop listening for keyup, since we only needed this once
+ this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
+};
+
+/**
+ * Handles key press events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key press event
+ * @fires click
+ */
+OO.ui.ButtonElement.prototype.onKeyPress = function ( e ) {
+ if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+ this.emit( 'click' );
+ }
+ return false;
+};
+
/**
* Check if button has a frame.
*
return this;
};
-/**
- * Set tab index.
- *
- * @param {number|null} tabIndex Button's tab index, use null to remove
- * @chainable
- */
-OO.ui.ButtonElement.prototype.setTabIndex = function ( tabIndex ) {
- tabIndex = typeof tabIndex === 'number' && tabIndex >= 0 ? tabIndex : null;
-
- if ( this.tabIndex !== tabIndex ) {
- if ( this.$button ) {
- if ( tabIndex !== null ) {
- this.$button.attr( 'tabindex', tabIndex );
- } else {
- this.$button.removeAttr( 'tabindex' );
- }
- }
- this.tabIndex = tabIndex;
- }
-
- return this;
-};
-
/**
* Set access key.
*
};
/**
- * Element containing a sequence of child elements.
+ * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
+ * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
+ * items from the group is done through the interface the class provides.
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
*
* @abstract
* @class
this.aggregateItemEvents = {};
// Initialization
- this.setGroupElement( config.$group || this.$( '<div>' ) );
+ this.setGroupElement( config.$group || $( '<div>' ) );
};
/* Methods */
this.$group = $group;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.$group.append( this.items[i].$element );
+ this.$group.append( this.items[ i ].$element );
}
};
hash = OO.getHash( data );
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( hash === OO.getHash( item.getData() ) ) {
return item;
}
items = [];
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( hash === OO.getHash( item.getData() ) ) {
items.push( item );
}
var i, len, item, add, remove, itemEvent, groupEvent;
for ( itemEvent in events ) {
- groupEvent = events[itemEvent];
+ groupEvent = events[ itemEvent ];
// Remove existing aggregated event
if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
}
// Remove event aggregation from existing items
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( item.connect && item.disconnect ) {
remove = {};
- remove[itemEvent] = [ 'emit', groupEvent, item ];
+ remove[ itemEvent ] = [ 'emit', groupEvent, item ];
item.disconnect( this, remove );
}
}
// Prevent future items from aggregating event
- delete this.aggregateItemEvents[itemEvent];
+ delete this.aggregateItemEvents[ itemEvent ];
}
// Add new aggregate event
if ( groupEvent ) {
// Make future items aggregate event
- this.aggregateItemEvents[itemEvent] = groupEvent;
+ this.aggregateItemEvents[ itemEvent ] = groupEvent;
// Add event aggregation to existing items
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( item.connect && item.disconnect ) {
add = {};
- add[itemEvent] = [ 'emit', groupEvent, item ];
+ add[ itemEvent ] = [ 'emit', groupEvent, item ];
item.connect( this, add );
}
}
itemElements = [];
for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
+ item = items[ i ];
// Check if item exists then remove it first, effectively "moving" it
currentIndex = $.inArray( item, this.items );
if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
events = {};
for ( event in this.aggregateItemEvents ) {
- events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
+ events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
}
item.connect( this, events );
}
this.$group.prepend( itemElements );
this.items.unshift.apply( this.items, items );
} else {
- this.items[index].$element.before( itemElements );
+ this.items[ index ].$element.before( itemElements );
this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
}
// Remove specific items
for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
+ item = items[ i ];
index = $.inArray( item, this.items );
if ( index !== -1 ) {
if (
) {
remove = {};
if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
- remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
+ remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
}
item.disconnect( this, remove );
}
// Remove all items
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if (
item.connect && item.disconnect &&
!$.isEmptyObject( this.aggregateItemEvents )
) {
remove = {};
if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
- remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
+ remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
}
item.disconnect( this, remove );
}
};
/**
- * A mixin for an element that can be dragged and dropped.
- * Use in conjunction with DragGroupWidget
+ * DraggableElement is a mixin class used to create elements that can be clicked
+ * and dragged by a mouse to a new position within a group. This class must be used
+ * in conjunction with OO.ui.DraggableGroupElement, which provides a container for
+ * the draggable elements.
*
* @abstract
* @class
} );
};
+OO.initClass( OO.ui.DraggableElement );
+
/* Events */
/**
* @event drop
*/
+/* Static Properties */
+
+/**
+ * @inheritdoc OO.ui.ButtonElement
+ */
+OO.ui.DraggableElement.static.cancelButtonMouseDownEvents = false;
+
/* Methods */
/**
};
/**
- * Element containing a sequence of child elements that can be dragged
- * and dropped.
+ * DraggableGroupElement is a mixin class used to create a group element to
+ * contain draggable elements, which are items that can be clicked and dragged by a mouse.
+ * The class is used with OO.ui.DraggableElement.
*
* @abstract
* @class
} );
// Initialize
- if ( $.isArray( config.items ) ) {
+ if ( Array.isArray( config.items ) ) {
this.addItems( config.items );
}
this.$placeholder = $( '<div>' )
// Map the index of each object
for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[i].setIndex( i );
+ this.items[ i ].setIndex( i );
}
if ( this.orientation === 'horizontal' ) {
// Emit change event
this.emit( 'reorder', this.getDragItem(), toIndex );
}
+ this.unsetDragItem();
// Return false to prevent propogation
return false;
};
// This means the item was dragged outside the widget
this.$placeholder
.css( 'left', 0 )
- .hide();
+ .addClass( 'oo-ui-element-hidden' );
};
/**
// Get the OptionWidget item we are dragging over
dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
$optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
- if ( $optionWidget[0] ) {
+ if ( $optionWidget[ 0 ] ) {
itemOffset = $optionWidget.offset();
- itemBoundingRect = $optionWidget[0].getBoundingClientRect();
+ itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
itemPosition = $optionWidget.position();
itemIndex = $optionWidget.data( 'index' );
}
this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
}
// Add drop indicator between objects
- if ( this.sideInsertion ) {
- this.$placeholder
- .css( cssOutput )
- .show();
- } else {
- this.$placeholder
- .css( {
- left: 0,
- top: 0
- } )
- .hide();
- }
+ this.$placeholder
+ .css( cssOutput )
+ .removeClass( 'oo-ui-element-hidden' );
} else {
// This means the item was dragged outside the widget
this.$placeholder
.css( 'left', 0 )
- .hide();
+ .addClass( 'oo-ui-element-hidden' );
}
// Prevent default
e.preventDefault();
OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
this.dragItem = null;
this.itemDragOver = null;
- this.$placeholder.hide();
+ this.$placeholder.addClass( 'oo-ui-element-hidden' );
this.sideInsertion = '';
};
};
/**
- * Element containing an icon.
+ * IconElement is often mixed into other classes to generate an icon.
+ * Icons are graphics, about the size of normal text. They are used to aid the user
+ * in locating a control or to convey information in a space-efficient way. See the
+ * [OOjs UI documentation on MediaWiki] [1] for a list of icons
+ * included in the library.
*
- * Icons are graphics, about the size of normal text. They can be used to aid the user in locating
- * a control or convey information in a more space efficient way. Icons should rarely be used
- * without labels; such as in a toolbar where space is at a premium or within a context where the
- * meaning is very clear to the user.
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
*
* @abstract
* @class
// Initialization
this.setIcon( config.icon || this.constructor.static.icon );
this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
- this.setIconElement( config.$icon || this.$( '<span>' ) );
+ this.setIconElement( config.$icon || $( '<span>' ) );
};
/* Setup */
/* Static Properties */
/**
- * Icon.
+ * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
+ * for i18n purposes and contains a `default` icon name and additional names keyed by
+ * language code. The `default` name is used when no icon is keyed by the user's language.
*
- * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
+ * Example of an i18n map:
*
- * For i18n purposes, this property can be an object containing a `default` icon name property and
- * additional icon names keyed by language code.
- *
- * Example of i18n icon definition:
* { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
*
+ * Note: the static property will be overridden if the #icon configuration is used.
+ *
* @static
* @inheritable
- * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
- * use the 'default' key to specify the icon to be used when there is no icon in the user's
- * language
+ * @property {Object|string}
*/
OO.ui.IconElement.static.icon = null;
/**
- * Icon title.
+ * The icon title, displayed when users move the mouse over the icon. The value can be text, a
+ * function that returns title text, or `null` for no title.
+ *
+ * The static property will be overridden if the #iconTitle configuration is used.
*
* @static
* @inheritable
- * @property {string|Function|null} Icon title text, a function that returns text or null for no
- * icon title
+ * @property {string|Function|null}
*/
OO.ui.IconElement.static.iconTitle = null;
};
/**
- * Element containing an indicator.
+ * IndicatorElement is often mixed into other classes to generate an indicator.
+ * Indicators are small graphics that are generally used in two ways:
+ *
+ * - To draw attention to the status of an item. For example, an indicator might be
+ * used to show that an item in a list has errors that need to be resolved.
+ * - To clarify the function of a control that acts in an exceptional way (a button
+ * that opens a menu instead of performing an action directly, for example).
*
- * Indicators are graphics, smaller than normal text. They can be used to describe unique status or
- * behavior. Indicators should only be used in exceptional cases; such as a button that opens a menu
- * instead of performing an action directly, or an item in a list which has errors that need to be
- * resolved.
+ * For a list of indicators included in the library, please see the
+ * [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
*
* @abstract
* @class
// Initialization
this.setIndicator( config.indicator || this.constructor.static.indicator );
this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
- this.setIndicatorElement( config.$indicator || this.$( '<span>' ) );
+ this.setIndicatorElement( config.$indicator || $( '<span>' ) );
};
/* Setup */
// Initialization
this.setLabel( config.label || this.constructor.static.label );
- this.setLabelElement( config.$label || this.$( '<span>' ) );
+ this.setLabelElement( config.$label || $( '<span>' ) );
};
/* Setup */
OO.initClass( OO.ui.LabelElement );
+/* Events */
+
+/**
+ * @event labelChange
+ * @param {string} value
+ */
+
/* Static Properties */
/**
label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
label = ( typeof label === 'string' && label.length ) || label instanceof jQuery ? label : null;
+ this.$element.toggleClass( 'oo-ui-labelElement', !!label );
+
if ( this.label !== label ) {
if ( this.$label ) {
this.setLabelContent( label );
}
this.label = label;
+ this.emit( 'labelChange' );
}
- this.$element.toggleClass( 'oo-ui-labelElement', !!this.label );
-
return this;
};
// Properties
this.$overlay = config.$overlay || this.$element;
this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
- $: OO.ui.Element.static.getJQuery( this.$overlay ),
+ widget: this,
+ input: this,
$container: config.$container
} );
this.lookupCache = {};
this.abortLookupRequest();
if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
- deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[value] ) );
+ deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
} else {
this.pushPending();
this.lookupQuery = value;
if ( ourRequest === widget.lookupRequest ) {
widget.lookupQuery = null;
widget.lookupRequest = null;
- widget.lookupCache[value] = widget.getLookupCacheDataFromResponse( data );
- deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[value] ) );
+ widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( data );
+ deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
}
} )
.fail( function () {
* @constructor
* @param {Object} [config] Configuration options
* @cfg {Object} [popup] Configuration to pass to popup
- * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus
+ * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
*/
OO.ui.PopupElement = function OoUiPopupElement( config ) {
// Configuration initialization
this.popup = new OO.ui.PopupWidget( $.extend(
{ autoClose: true },
config.popup,
- { $: this.$, $autoCloseIgnore: this.$element }
+ { $autoCloseIgnore: this.$element }
) );
};
};
/**
- * Element with named flags that can be added, removed, listed and checked.
+ * The FlaggedElement class is an attribute mixin, meaning that it is used to add
+ * additional functionality to an element created by another class. The class provides
+ * a ‘flags’ property assigned the name (or an array of names) of styling flags,
+ * which are used to customize the look and feel of a widget to better describe its
+ * importance and functionality.
+ *
+ * The library currently contains the following styling flags for general use:
+ *
+ * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
+ * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
+ * - **constructive**: Constructive styling is applied to convey that the widget will create something.
+ *
+ * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
*
- * A flag, when set, adds a CSS class on the `$element` by combining `oo-ui-flaggedElement-` with
- * the flag name. Flags are primarily useful for styling.
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
*
* @abstract
* @class
for ( flag in this.flags ) {
className = classPrefix + flag;
- changes[flag] = false;
- delete this.flags[flag];
+ changes[ flag ] = false;
+ delete this.flags[ flag ];
remove.push( className );
}
if ( typeof flags === 'string' ) {
className = classPrefix + flags;
// Set
- if ( !this.flags[flags] ) {
- this.flags[flags] = true;
+ if ( !this.flags[ flags ] ) {
+ this.flags[ flags ] = true;
add.push( className );
}
- } else if ( $.isArray( flags ) ) {
+ } else if ( Array.isArray( flags ) ) {
for ( i = 0, len = flags.length; i < len; i++ ) {
- flag = flags[i];
+ flag = flags[ i ];
className = classPrefix + flag;
// Set
- if ( !this.flags[flag] ) {
- changes[flag] = true;
- this.flags[flag] = true;
+ if ( !this.flags[ flag ] ) {
+ changes[ flag ] = true;
+ this.flags[ flag ] = true;
add.push( className );
}
}
} else if ( OO.isPlainObject( flags ) ) {
for ( flag in flags ) {
className = classPrefix + flag;
- if ( flags[flag] ) {
+ if ( flags[ flag ] ) {
// Set
- if ( !this.flags[flag] ) {
- changes[flag] = true;
- this.flags[flag] = true;
+ if ( !this.flags[ flag ] ) {
+ changes[ flag ] = true;
+ this.flags[ flag ] = true;
add.push( className );
}
} else {
// Remove
- if ( this.flags[flag] ) {
- changes[flag] = false;
- delete this.flags[flag];
+ if ( this.flags[ flag ] ) {
+ changes[ flag ] = false;
+ delete this.flags[ flag ];
remove.push( className );
}
}
OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
if ( this.$clippable ) {
this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
- this.$clippable.css( { width: '', height: '' } );
- this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
- this.$clippable.css( { overflowX: '', overflowY: '' } );
+ this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
}
this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
if ( this.clipping !== clipping ) {
this.clipping = clipping;
if ( clipping ) {
- this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
+ this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
// If the clippable container is the root, we have to listen to scroll events and check
// jQuery.scrollTop on the window because of browser inconsistencies
this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
- this.$( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
+ $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
this.$clippableContainer;
this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
- this.$clippableWindow = this.$( this.getElementWindow() )
+ this.$clippableWindow = $( this.getElementWindow() )
.on( 'resize', this.onClippableWindowResizeHandler );
// Initial clip after visible
this.clip();
} else {
- this.$clippable.css( { width: '', height: '' } );
- this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
- this.$clippable.css( { overflowX: '', overflowY: '' } );
+ this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
this.$clippableContainer = null;
this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
if ( clipWidth ) {
this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
} else {
- this.$clippable.css( 'width', this.idealWidth || '' );
- this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
- this.$clippable.css( 'overflowX', '' );
+ this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
}
if ( clipHeight ) {
this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
} else {
- this.$clippable.css( 'height', this.idealHeight || '' );
- this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
- this.$clippable.css( 'overflowY', '' );
+ this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
+ }
+
+ // If we stopped clipping in at least one of the dimensions
+ if ( !clipWidth || !clipHeight ) {
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
}
this.clippedHorizontally = clipWidth;
this.toolGroup = toolGroup;
this.toolbar = this.toolGroup.getToolbar();
this.active = false;
- this.$title = this.$( '<span>' );
- this.$accel = this.$( '<span>' );
- this.$link = this.$( '<a>' );
+ this.$title = $( '<span>' );
+ this.$accel = $( '<span>' );
+ this.$link = $( '<a>' );
this.title = null;
// Events
this.toolGroupFactory = toolGroupFactory;
this.groups = [];
this.tools = {};
- this.$bar = this.$( '<div>' );
- this.$actions = this.$( '<div>' );
+ this.$bar = $( '<div>' );
+ this.$actions = $( '<div>' );
this.initialized = false;
// Events
* @param {jQuery.Event} e Mouse down event
*/
OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
- var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ),
+ var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
$closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
- if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) {
+ if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
return false;
}
};
// Build out new groups
for ( i = 0, len = groups.length; i < len; i++ ) {
- group = groups[i];
+ group = groups[ i ];
if ( group.include === '*' ) {
// Apply defaults to catch-all groups
if ( group.type === undefined ) {
// Check type has been registered
type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
items.push(
- this.getToolGroupFactory().create( type, this, $.extend( { $: this.$ }, group ) )
+ this.getToolGroupFactory().create( type, this, group )
);
}
this.addItems( items );
this.groups = [];
this.tools = {};
for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[i].destroy();
+ this.items[ i ].destroy();
}
this.clearItems();
};
* @return {boolean} Tool is available
*/
OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
- return !this.tools[name];
+ return !this.tools[ name ];
};
/**
* @param {OO.ui.Tool} tool Tool to reserve
*/
OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
- this.tools[tool.getName()] = tool;
+ this.tools[ tool.getName() ] = tool;
};
/**
* @param {OO.ui.Tool} tool Tool to release
*/
OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
- delete this.tools[tool.getName()];
+ delete this.tools[ tool.getName() ];
};
/**
if ( this.constructor.static.autoDisable ) {
for ( i = this.items.length - 1; i >= 0; i-- ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( !item.isDisabled() ) {
allDisabled = false;
break;
*/
OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
var tool,
- $item = this.$( e.target ).closest( '.oo-ui-tool-link' );
+ $item = $( e.target ).closest( '.oo-ui-tool-link' );
if ( $item.length ) {
tool = $item.parent().data( 'oo-ui-tool' );
// Build a list of needed tools
for ( i = 0, len = list.length; i < len; i++ ) {
- name = list[i];
+ name = list[ i ];
if (
// Tool exists
toolFactory.lookup( name ) &&
// Tool is available or is already in this group
- ( this.toolbar.isToolAvailable( name ) || this.tools[name] )
+ ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
) {
- tool = this.tools[name];
+ tool = this.tools[ name ];
if ( !tool ) {
// Auto-initialize tools on first use
- this.tools[name] = tool = toolFactory.create( name, this );
+ this.tools[ name ] = tool = toolFactory.create( name, this );
tool.updateTitle();
}
this.toolbar.reserveTool( tool );
add.push( tool );
- names[name] = true;
+ names[ name ] = true;
}
}
// Remove tools that are no longer needed
for ( name in this.tools ) {
- if ( !names[name] ) {
- this.tools[name].destroy();
- this.toolbar.releaseTool( this.tools[name] );
- remove.push( this.tools[name] );
- delete this.tools[name];
+ if ( !names[ name ] ) {
+ this.tools[ name ].destroy();
+ this.toolbar.releaseTool( this.tools[ name ] );
+ remove.push( this.tools[ name ] );
+ delete this.tools[ name ];
}
}
if ( remove.length ) {
this.clearItems();
this.toolbar.getToolFactory().disconnect( this );
for ( name in this.tools ) {
- this.toolbar.releaseTool( this.tools[name] );
- this.tools[name].disconnect( this ).destroy();
- delete this.tools[name];
+ this.toolbar.releaseTool( this.tools[ name ] );
+ this.tools[ name ].disconnect( this ).destroy();
+ delete this.tools[ name ];
}
this.$element.remove();
};
var bodyHeight, oldOverflow,
$scrollable = this.container.$element;
- oldOverflow = $scrollable[0].style.overflow;
- $scrollable[0].style.overflow = 'hidden';
+ oldOverflow = $scrollable[ 0 ].style.overflow;
+ $scrollable[ 0 ].style.overflow = 'hidden';
- // Force… ugh… something to happen
- $scrollable.contents().hide();
- $scrollable.height();
- $scrollable.contents().show();
+ OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
bodyHeight = this.text.$element.outerHeight( true );
- $scrollable[0].style.overflow = oldOverflow;
+ $scrollable[ 0 ].style.overflow = oldOverflow;
return bodyHeight;
};
// Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
// Need to do it after transition completes (250ms), add 50ms just in case.
setTimeout( function () {
- var oldOverflow = $scrollable[0].style.overflow;
- $scrollable[0].style.overflow = 'hidden';
+ var oldOverflow = $scrollable[ 0 ].style.overflow;
+ $scrollable[ 0 ].style.overflow = 'hidden';
- // Force… ugh… something to happen
- $scrollable.contents().hide();
- $scrollable.height();
- $scrollable.contents().show();
+ OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
- $scrollable[0].style.overflow = oldOverflow;
+ $scrollable[ 0 ].style.overflow = oldOverflow;
}, 300 );
return this;
OO.ui.MessageDialog.super.prototype.initialize.call( this );
// Properties
- this.$actions = this.$( '<div>' );
+ this.$actions = $( '<div>' );
this.container = new OO.ui.PanelLayout( {
- $: this.$, scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
+ scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
} );
this.text = new OO.ui.PanelLayout( {
- $: this.$, padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
+ padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
} );
this.message = new OO.ui.LabelWidget( {
- $: this.$, classes: [ 'oo-ui-messageDialog-message' ]
+ classes: [ 'oo-ui-messageDialog-message' ]
} );
// Initialization
}
if ( others.length ) {
for ( i = 0, len = others.length; i < len; i++ ) {
- other = others[i];
+ other = others[ i ];
this.$actions.append( other.$element );
other.toggleFramed( false );
}
if ( !this.isOpening() ) {
// If the dialog is currently opening, this will be called automatically soon.
// This also calls #fitActions.
- this.manager.updateWindowSize( this );
+ this.updateSize();
}
};
// Detect clipping
this.toggleVerticalActionLayout( false );
for ( i = 0, len = actions.length; i < len; i++ ) {
- action = actions[i];
+ action = actions[ i ];
if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
this.toggleVerticalActionLayout( true );
break;
}
}
+ // Move the body out of the way of the foot
+ this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
+
if ( this.verticalActionLayout !== previous ) {
- this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
// We changed the layout, window height might need to be updated.
- this.manager.updateWindowSize( this );
+ this.updateSize();
}
};
OO.ui.ProcessDialog.super.prototype.initialize.call( this );
// Properties
- this.$navigation = this.$( '<div>' );
- this.$location = this.$( '<div>' );
- this.$safeActions = this.$( '<div>' );
- this.$primaryActions = this.$( '<div>' );
- this.$otherActions = this.$( '<div>' );
+ this.$navigation = $( '<div>' );
+ this.$location = $( '<div>' );
+ this.$safeActions = $( '<div>' );
+ this.$primaryActions = $( '<div>' );
+ this.$otherActions = $( '<div>' );
this.dismissButton = new OO.ui.ButtonWidget( {
- $: this.$,
label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
} );
- this.retryButton = new OO.ui.ButtonWidget( { $: this.$ } );
- this.$errors = this.$( '<div>' );
- this.$errorsTitle = this.$( '<div>' );
+ this.retryButton = new OO.ui.ButtonWidget();
+ this.$errors = $( '<div>' );
+ this.$errorsTitle = $( '<div>' );
// Events
this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
.addClass( 'oo-ui-processDialog-errors-title' )
.text( OO.ui.msg( 'ooui-dialog-process-error' ) );
this.$errors
- .addClass( 'oo-ui-processDialog-errors' )
+ .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
.append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
this.$content
.addClass( 'oo-ui-processDialog-content' )
}
if ( others.length ) {
for ( i = 0, len = others.length; i < len; i++ ) {
- other = others[i];
+ other = others[ i ];
this.$otherActions.append( other.$element );
other.toggleFramed( true );
}
warning = false;
for ( i = 0, len = errors.length; i < len; i++ ) {
- if ( !errors[i].isRecoverable() ) {
+ if ( !errors[ i ].isRecoverable() ) {
recoverable = false;
}
- if ( errors[i].isWarning() ) {
+ if ( errors[ i ].isWarning() ) {
warning = true;
}
- $item = this.$( '<div>' )
+ $item = $( '<div>' )
.addClass( 'oo-ui-processDialog-error' )
- .append( errors[i].getMessage() );
- items.push( $item[0] );
+ .append( errors[ i ].getMessage() );
+ items.push( $item[ 0 ] );
}
- this.$errorItems = this.$( items );
+ this.$errorItems = $( items );
if ( recoverable ) {
this.retryButton.clearFlags().setFlags( this.currentAction.getFlags() );
} else {
}
this.retryButton.toggle( recoverable );
this.$errorsTitle.after( this.$errorItems );
- this.$errors.show().scrollTop( 0 );
+ this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
};
/**
* Hide errors.
*/
OO.ui.ProcessDialog.prototype.hideErrors = function () {
- this.$errors.hide();
+ this.$errors.addClass( 'oo-ui-element-hidden' );
this.$errorItems.remove();
this.$errorItems = null;
};
/**
- * Layout containing a series of pages.
+ * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
+ * which is a widget that is specified by reference before any optional configuration settings.
+ *
+ * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
+ *
+ * - **left**: The label is placed before the field-widget and aligned with the left margin.
+ * A left-alignment is used for forms with many fields.
+ * - **right**: The label is placed before the field-widget and aligned to the right margin.
+ * A right-alignment is used for long but familiar forms which users tab through,
+ * verifying the current field with a quick glance at the label.
+ * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
+ * that users fill out from top to bottom.
+ * - **inline**: The label is placed after the field-widget and aligned to the left.
+ An inline-alignment is best used with checkboxes or radio buttons.
+ *
+ * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
*
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
* @class
* @extends OO.ui.Layout
+ * @mixins OO.ui.LabelElement
*
* @constructor
+ * @param {OO.ui.Widget} fieldWidget Field widget
* @param {Object} [config] Configuration options
- * @cfg {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 );
// 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.fieldWidget = fieldWidget;
+ this.$field = $( '<div>' );
+ this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
+ this.align = null;
+ if ( config.help ) {
+ this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ classes: [ 'oo-ui-fieldLayout-help' ],
+ framed: false,
+ icon: 'info'
+ } );
+
+ this.popupButtonWidget.getPopup().$body.append(
+ $( '<div>' )
+ .text( config.help )
+ .addClass( 'oo-ui-fieldLayout-help-content' )
);
- this.outlineVisible = true;
- if ( this.editable ) {
- this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
- this.outlineSelectWidget, { $: this.$ }
- );
- }
+ this.$help = this.popupButtonWidget.$element;
+ } else {
+ this.$help = $( [] );
}
// 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 );
+OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
-/* Events */
+/* Methods */
/**
- * @event set
- * @param {OO.ui.PageLayout} page Current page
+ * 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 );
+};
/**
- * @event add
- * @param {OO.ui.PageLayout[]} page Added pages
- * @param {number} index Index pages were added at
+ * Handle label mouse click events.
+ *
+ * @param {jQuery.Event} e Mouse click event
*/
+OO.ui.FieldLayout.prototype.onLabelClick = function () {
+ this.fieldWidget.simulateLabelClick();
+ return false;
+};
/**
- * @event remove
- * @param {OO.ui.PageLayout[]} pages Removed pages
+ * Get the field.
+ *
+ * @return {OO.ui.Widget} Field widget
*/
-
-/* Methods */
+OO.ui.FieldLayout.prototype.getField = function () {
+ return this.fieldWidget;
+};
/**
- * Handle stack layout focus.
+ * Set the field alignment mode.
*
- * @param {jQuery.Event} e Focusin event
+ * @private
+ * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
+ * @chainable
*/
-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.setAlignment = function ( value ) {
+ if ( value !== this.align ) {
+ // Default to 'left'
+ if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
+ value = 'left';
}
+ // Reorder elements
+ if ( value === 'inline' ) {
+ this.$body.append( this.$field, this.$label );
+ } else {
+ this.$body.append( this.$label, this.$field );
+ }
+ // Set classes. The following classes can be used here:
+ // * oo-ui-fieldLayout-align-left
+ // * oo-ui-fieldLayout-align-right
+ // * oo-ui-fieldLayout-align-top
+ // * oo-ui-fieldLayout-align-inline
+ if ( this.align ) {
+ this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
+ }
+ this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
+ this.align = value;
}
+
+ return this;
};
/**
- * Handle stack layout set events.
+ * Layout made of a field, a button, and an optional label.
*
- * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
+ * @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.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
- var layout = this;
- if ( page ) {
- page.scrollElementIntoView( { complete: function () {
- if ( layout.autoFocus ) {
- layout.focus();
- }
- } } );
- }
+OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
+ // Configuration initialization
+ config = $.extend( { align: 'left' }, config );
+
+ // Parent constructor
+ OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
+
+ // Mixin constructors
+ OO.ui.LabelElement.call( this, config );
+
+ // Properties
+ this.fieldWidget = fieldWidget;
+ this.buttonWidget = buttonWidget;
+ this.$button = $( '<div>' )
+ .addClass( 'oo-ui-actionFieldLayout-button' )
+ .append( this.buttonWidget.$element );
+ this.$input = $( '<div>' )
+ .addClass( 'oo-ui-actionFieldLayout-input' )
+ .append( this.fieldWidget.$element );
+ this.$field
+ .addClass( 'oo-ui-actionFieldLayout' )
+ .append( this.$input, this.$button );
};
+/* Setup */
+
+OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
+
/**
- * Focus the first input in the current page.
+ * Layout made of a fieldset and optional legend.
*
- * 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.
+ * 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.focus = function () {
- var $input, page = this.stackLayout.getCurrentItem();
- if ( !page && this.outlined ) {
- this.selectFirstSelectablePage();
- page = this.stackLayout.getCurrentItem();
- if ( !page ) {
- return;
- }
+OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.FieldsetLayout.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.IconElement.call( this, config );
+ OO.ui.LabelElement.call( this, config );
+ OO.ui.GroupElement.call( this, config );
+
+ if ( config.help ) {
+ this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ classes: [ 'oo-ui-fieldsetLayout-help' ],
+ framed: false,
+ icon: 'info'
+ } );
+
+ this.popupButtonWidget.getPopup().$body.append(
+ $( '<div>' )
+ .text( config.help )
+ .addClass( 'oo-ui-fieldsetLayout-help-content' )
+ );
+ this.$help = this.popupButtonWidget.$element;
+ } else {
+ this.$help = $( [] );
}
- // 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();
- }
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-fieldsetLayout' )
+ .prepend( this.$help, this.$icon, this.$label, this.$group );
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
}
};
+/* Setup */
+
+OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
+
/**
- * Handle outline widget select events.
+ * Layout with an HTML form.
*
- * @param {OO.ui.OptionWidget|null} item Selected item
+ * @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.onOutlineSelectWidgetSelect = function ( item ) {
- if ( item ) {
- this.setPage( item.getData() );
- }
+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 */
+
/**
- * Check if booklet has an outline.
- *
- * @return {boolean}
+ * @event submit
*/
-OO.ui.BookletLayout.prototype.isOutlined = function () {
- return this.outlined;
-};
+
+/* Static Properties */
+
+OO.ui.FormLayout.static.tagName = 'form';
+
+/* Methods */
/**
- * Check if booklet has editing controls.
+ * Handle form submit events.
*
- * @return {boolean}
+ * @param {jQuery.Event} e Submit event
+ * @fires submit
*/
-OO.ui.BookletLayout.prototype.isEditable = function () {
- return this.editable;
+OO.ui.FormLayout.prototype.onFormSubmit = function () {
+ this.emit( 'submit' );
+ return false;
};
/**
- * Check if booklet has a visible outline.
+ * Layout made of proportionally sized columns and rows.
*
- * @return {boolean}
+ * @class
+ * @extends OO.ui.Layout
+ * @deprecated Use OO.ui.MenuLayout or plain CSS instead.
+ *
+ * @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.isOutlineVisible = function () {
- return this.outlined && this.outlineVisible;
+OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
+ var i, len, widths;
+
+ // Configuration initialization
+ config = config || {};
+
+ // 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 ( 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 ] );
+ }
};
+/* Setup */
+
+OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
+
+/* Events */
+
/**
- * Hide or show the outline.
+ * @event layout
+ */
+
+/**
+ * @event update
+ */
+
+/* Methods */
+
+/**
+ * Set grid dimensions.
*
- * @param {boolean} [show] Show outline, omit to invert current state
- * @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.toggleOutline = function ( show ) {
- if ( this.outlined ) {
- show = show === undefined ? !this.outlineVisible : !!show;
- this.outlineVisible = show;
- this.gridLayout.layout( show ? [ 1, 2 ] : [ 0, 1 ], [ 1 ] );
+OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
+ var x, y,
+ xd = 0,
+ yd = 0,
+ cols = widths.length,
+ rows = heights.length;
+
+ // 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' );
}
- 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' );
};
/**
- * Get the outline widget.
+ * Update panel positions and sizes.
*
- * @param {OO.ui.PageLayout} page Page to be selected
- * @return {OO.ui.PageLayout|null} Closest page to another
+ * @fires update
*/
-OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
- var next, prev, level,
- pages = this.stackLayout.getItems(),
- index = $.inArray( page, pages );
+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 ( 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;
+ 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( document ) === 'rtl' ) {
+ dimensions.right = ( left * 100 ) + '%';
+ } else {
+ dimensions.left = ( left * 100 ) + '%';
}
- if (
- next &&
- level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
- ) {
- return next;
+ // 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;
}
- 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;
+ this.emit( 'update' );
};
/**
- * Get the outline controls widget. If the outline is not editable, null is returned.
+ * Get a panel at a given position.
*
- * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
- */
-OO.ui.BookletLayout.prototype.getOutlineControls = function () {
- return this.outlineControlsWidget;
-};
-
-/**
- * Get a page by name.
+ * The x and y position is affected by the current grid layout.
*
- * @param {string} name Symbolic name of page
- * @return {OO.ui.PageLayout|undefined} Page, if found
+ * @param {number} x Horizontal position
+ * @param {number} y Vertical position
+ * @return {OO.ui.PanelLayout} The panel at the given position
*/
-OO.ui.BookletLayout.prototype.getPage = function ( name ) {
- return this.pages[name];
+OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
+ return this.panels[ ( x * this.widths.length ) + y ];
};
/**
- * Get the current page name.
+ * Layout with a content and menu area.
*
- * @return {string|null} Current page name
- */
-OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
- return this.currentPageName;
-};
-
-/**
- * Add a page to the layout.
+ * The menu area can be positioned at the top, after, bottom or before. The content area will fill
+ * all remaining space.
*
- * 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 {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
*/
-OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
- var i, len, name, page, item, currentIndex,
- stackLayoutPages = this.stackLayout.getItems(),
- remove = [],
- items = [];
+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 );
- // Remove pages with same names
- for ( i = 0, len = pages.length; i < len; i++ ) {
- page = pages[i];
- name = page.getName();
+ // Properties
+ this.showMenu = config.showMenu !== false;
+ this.menuSize = config.menuSize || '18em';
+ this.menuPosition = positions[ config.menuPosition ] || positions.before;
- 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 );
- }
+ /**
+ * Menu DOM node
+ *
+ * @property {jQuery}
+ */
+ this.$menu = $( '<div>' );
+ /**
+ * Content DOM node
+ *
+ * @property {jQuery}
+ */
+ this.$content = $( '<div>' );
- // 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 );
- }
- }
+ // Initialization
+ this.toggleMenu( this.showMenu );
+ this.updateSizes();
+ 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 );
+};
- if ( this.outlined && items.length ) {
- this.outlineSelectWidget.addItems( items, index );
- this.selectFirstSelectablePage();
- }
- this.stackLayout.addItems( pages, index );
- this.emit( 'add', pages, index );
+/* Setup */
- return this;
+OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
+
+/* Static Properties */
+
+OO.ui.MenuLayout.static.menuPositions = {
+ top: {
+ sizeProperty: 'height',
+ className: 'oo-ui-menuLayout-top'
+ },
+ after: {
+ sizeProperty: 'width',
+ className: 'oo-ui-menuLayout-after'
+ },
+ bottom: {
+ sizeProperty: 'height',
+ className: 'oo-ui-menuLayout-bottom'
+ },
+ before: {
+ sizeProperty: 'width',
+ className: 'oo-ui-menuLayout-before'
+ }
};
+/* Methods */
+
/**
- * Remove a page from the layout.
+ * Toggle menu.
*
- * @fires remove
+ * @param {boolean} showMenu Show menu, omit to toggle
* @chainable
*/
-OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
- var i, len, name, page,
- items = [];
+OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
+ showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
- 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();
+ if ( this.showMenu !== showMenu ) {
+ this.showMenu = showMenu;
+ this.updateSizes();
}
- this.stackLayout.removeItems( pages );
- this.emit( 'remove', pages );
return this;
};
/**
- * Clear all pages from the layout.
+ * Check if menu is visible
*
- * @fires remove
- * @chainable
+ * @return {boolean} Menu is visible
*/
-OO.ui.BookletLayout.prototype.clearPages = function () {
- var i, len,
- pages = this.stackLayout.getItems();
-
- this.pages = {};
- this.currentPageName = null;
- if ( this.outlined ) {
- this.outlineSelectWidget.clearItems();
- for ( i = 0, len = pages.length; i < len; i++ ) {
- pages[i].setOutlineItem( null );
- }
- }
- this.stackLayout.clearItems();
+OO.ui.MenuLayout.prototype.isMenuVisible = function () {
+ return this.showMenu;
+};
- this.emit( 'remove', pages );
+/**
+ * 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();
return this;
};
/**
- * Set the current page by name.
+ * Update menu and content CSS based on current menu size and visibility
*
- * @fires set
- * @param {string} name Symbolic name of page
+ * This method is called internally when size or position is changed.
*/
-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 );
- }
+OO.ui.MenuLayout.prototype.updateSizes = function () {
+ if ( this.showMenu ) {
+ this.$menu
+ .css( this.menuPosition.sizeProperty, this.menuSize )
+ .css( 'overflow', '' );
+ // Set offsets on all sides. CSS resets all but one with
+ // 'important' rules so directionality flips are supported
+ this.$content.css( {
+ top: this.menuSize,
+ right: this.menuSize,
+ bottom: this.menuSize,
+ left: this.menuSize
+ } );
+ } else {
+ this.$menu
+ .css( this.menuPosition.sizeProperty, 0 )
+ .css( 'overflow', 'hidden' );
+ this.$content.css( {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ } );
}
};
/**
- * Select the first selectable page.
+ * Get menu size.
+ *
+ * @return {number|string} Menu size
+ */
+OO.ui.MenuLayout.prototype.getMenuSize = function () {
+ return this.menuSize;
+};
+
+/**
+ * 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.BookletLayout.prototype.selectFirstSelectablePage = function () {
- if ( !this.outlineSelectWidget.getSelectedItem() ) {
- this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
+OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
+ var positions = this.constructor.static.menuPositions;
+
+ if ( !positions[ position ] ) {
+ throw new Error( 'Cannot set position; unsupported position value: ' + position );
}
+ this.$menu.css( this.menuPosition.sizeProperty, '' );
+ this.$element.removeClass( this.menuPosition.className );
+
+ this.menuPosition = positions[ position ];
+
+ this.updateSizes();
+ this.$element.addClass( this.menuPosition.className );
+
return this;
};
/**
- * Layout made of a field and optional label.
+ * Get menu position.
*
- * 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
+ * @return {string} Menu position
+ */
+OO.ui.MenuLayout.prototype.getMenuPosition = function () {
+ return this.menuPosition;
+};
+
+/**
+ * Layout containing a series of pages.
*
* @class
- * @extends OO.ui.Layout
- * @mixins OO.ui.LabelElement
+ * @extends OO.ui.MenuLayout
*
* @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 {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.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
- var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
-
+OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
// 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.BookletLayout.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.popupButtonWidget.getPopup().$body.append(
- this.$( '<div>' )
- .text( config.help )
- .addClass( 'oo-ui-fieldLayout-help-content' )
- );
- this.$help = this.popupButtonWidget.$element;
- } else {
- this.$help = this.$( [] );
+ this.currentPageName = null;
+ this.pages = {};
+ this.ignoreFocus = false;
+ this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
+ this.$content.append( this.stackLayout.$element );
+ this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
+ this.outlineVisible = false;
+ this.outlined = !!config.outlined;
+ if ( this.outlined ) {
+ this.editable = !!config.editable;
+ this.outlineControlsWidget = null;
+ this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
+ this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
+ this.$menu.append( this.outlinePanel.$element );
+ this.outlineVisible = true;
+ if ( this.editable ) {
+ this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
+ this.outlineSelectWidget
+ );
+ }
}
+ this.toggleMenu( this.outlined );
// Events
- if ( hasInputWidget ) {
- this.$label.on( 'click', this.onLabelClick.bind( this ) );
+ 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 ) );
}
- this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
// Initialization
- this.$element
- .addClass( 'oo-ui-fieldLayout' )
- .append( this.$help, this.$body );
- this.$body.addClass( 'oo-ui-fieldLayout-body' );
- this.$field
- .addClass( 'oo-ui-fieldLayout-field' )
- .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
- .append( this.fieldWidget.$element );
+ this.$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.setAlignment( config.align );
+/* Setup */
+
+OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
+
+/* 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
+ */
+
+/* Methods */
+
+/**
+ * Handle stack layout focus.
+ *
+ * @param {jQuery.Event} e Focusin event
+ */
+OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
+ var name, $target;
+
+ // Find the page that an element was focused within
+ $target = $( e.target ).closest( '.oo-ui-pageLayout' );
+ for ( name in this.pages ) {
+ // Check for page match, exclude current page to find only page changes
+ if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
+ this.setPage( name );
+ break;
+ }
+ }
};
-/* Setup */
+/**
+ * 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();
+ }
+ } } );
+ }
+};
-OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
+/**
+ * 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();
+ }
+ 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();
+ }
+ }
+};
-/* Methods */
+/**
+ * Handle outline widget select events.
+ *
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
+ if ( item ) {
+ this.setPage( item.getData() );
+ }
+};
/**
- * Handle field disable events.
+ * Check if booklet has an outline.
*
- * @param {boolean} value Field is disabled
+ * @return {boolean}
*/
-OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
- this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
+OO.ui.BookletLayout.prototype.isOutlined = function () {
+ return this.outlined;
};
/**
- * Handle label mouse click events.
+ * Check if booklet has editing controls.
*
- * @param {jQuery.Event} e Mouse click event
+ * @return {boolean}
*/
-OO.ui.FieldLayout.prototype.onLabelClick = function () {
- this.fieldWidget.simulateLabelClick();
- return false;
+OO.ui.BookletLayout.prototype.isEditable = function () {
+ return this.editable;
};
/**
- * Get the field.
+ * Check if booklet has a visible outline.
*
- * @return {OO.ui.Widget} Field widget
+ * @return {boolean}
*/
-OO.ui.FieldLayout.prototype.getField = function () {
- return this.fieldWidget;
+OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
+ return this.outlined && this.outlineVisible;
};
/**
- * Set the field alignment mode.
+ * Hide or show the outline.
*
- * @private
- * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
+ * @param {boolean} [show] Show outline, omit to invert current state
* @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.BookletLayout.prototype.toggleOutline = function ( show ) {
+ if ( this.outlined ) {
+ show = show === undefined ? !this.outlineVisible : !!show;
+ this.outlineVisible = show;
+ this.toggleMenu( show );
}
return this;
};
/**
- * Layout made of a field, a button, and an optional label.
- *
- * @class
- * @extends OO.ui.FieldLayout
+ * Get the outline widget.
*
- * @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.
+ * @param {OO.ui.PageLayout} page Page to be selected
+ * @return {OO.ui.PageLayout|null} Closest page to another
*/
-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 );
+OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
+ var next, prev, level,
+ pages = this.stackLayout.getItems(),
+ index = $.inArray( page, pages );
- this.$field
- .addClass( 'oo-ui-actionFieldLayout' )
- .append( this.$input, this.$button );
+ 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;
};
-/* Setup */
-
-OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
-
/**
- * 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
+ * Get the outline widget.
*
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {OO.ui.FieldLayout[]} [items] Items to add
+ * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
*/
-OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.FieldsetLayout.super.call( this, config );
-
- // Mixin constructors
- OO.ui.IconElement.call( this, config );
- OO.ui.LabelElement.call( this, config );
- OO.ui.GroupElement.call( this, config );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-fieldsetLayout' )
- .prepend( this.$icon, this.$label, this.$group );
- if ( $.isArray( config.items ) ) {
- this.addItems( config.items );
- }
+OO.ui.BookletLayout.prototype.getOutline = function () {
+ return this.outlineSelectWidget;
};
-/* 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 the outline controls widget. If the outline is not editable, null is returned.
*
- * @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 {OO.ui.OutlineControlsWidget|null} The outline controls widget.
*/
-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
- } );
+OO.ui.BookletLayout.prototype.getOutlineControls = function () {
+ return this.outlineControlsWidget;
};
-/* Setup */
-
-OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
-
-/* Events */
-
/**
- * @event submit
+ * Get a page by name.
+ *
+ * @param {string} name Symbolic name of page
+ * @return {OO.ui.PageLayout|undefined} Page, if found
*/
+OO.ui.BookletLayout.prototype.getPage = function ( name ) {
+ return this.pages[ name ];
+};
-/* Static Properties */
-
-OO.ui.FormLayout.static.tagName = 'form';
-
-/* Methods */
+/**
+ * Get the current page
+ *
+ * @return {OO.ui.PageLayout|undefined} Current page, if found
+ */
+OO.ui.BookletLayout.prototype.getCurrentPage = function () {
+ var name = this.getCurrentPageName();
+ return name ? this.getPage( name ) : undefined;
+};
/**
- * Handle form submit events.
+ * Get the current page name.
*
- * @param {jQuery.Event} e Submit event
- * @fires submit
+ * @return {string|null} Current page name
*/
-OO.ui.FormLayout.prototype.onFormSubmit = function () {
- this.emit( 'submit' );
- return false;
+OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
+ return this.currentPageName;
};
/**
- * Layout made of proportionally sized columns and rows.
+ * Add a page to the layout.
*
- * @class
- * @extends OO.ui.Layout
+ * When pages are added with the same names as existing pages, the existing pages will be
+ * automatically removed before the new pages are added.
*
- * @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
+ * @param {OO.ui.PageLayout[]} pages Pages to add
+ * @param {number} index Index to insert pages after
+ * @fires add
+ * @chainable
*/
-OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
- var i, len, widths;
-
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.GridLayout.super.call( this, config );
+OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
+ var i, len, name, page, item, currentIndex,
+ stackLayoutPages = this.stackLayout.getItems(),
+ remove = [],
+ items = [];
- // Properties
- this.panels = [];
- this.widths = [];
- this.heights = [];
+ // Remove pages with same names
+ for ( i = 0, len = pages.length; i < len; i++ ) {
+ page = pages[ i ];
+ name = page.getName();
- // 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 ( 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 ( 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 ( remove.length ) {
+ this.removePages( remove );
}
-};
-/* Setup */
+ // Add new pages
+ for ( i = 0, len = pages.length; i < len; i++ ) {
+ page = pages[ i ];
+ name = page.getName();
+ this.pages[ page.getName() ] = page;
+ if ( this.outlined ) {
+ item = new OO.ui.OutlineOptionWidget( { data: name } );
+ page.setOutlineItem( item );
+ items.push( item );
+ }
+ }
-OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
+ if ( this.outlined && items.length ) {
+ this.outlineSelectWidget.addItems( items, index );
+ this.selectFirstSelectablePage();
+ }
+ this.stackLayout.addItems( pages, index );
+ this.emit( 'add', pages, index );
-/* Events */
+ return this;
+};
/**
- * @event layout
+ * Remove a page from the layout.
+ *
+ * @fires remove
+ * @chainable
*/
+OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
+ var i, len, name, page,
+ items = [];
-/**
- * @event update
- */
+ 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 );
-/* Methods */
+ return this;
+};
/**
- * Set grid dimensions.
+ * Clear all pages from the layout.
*
- * @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
+ * @fires remove
+ * @chainable
*/
-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.clearPages = function () {
+ var i, len,
+ pages = this.stackLayout.getItems();
- // 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.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();
- // 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' );
+ this.emit( 'remove', pages );
+
+ return this;
};
/**
- * Update panel positions and sizes.
+ * Set the current page by name.
*
- * @fires update
+ * @fires set
+ * @param {string} name Symbolic name of page
*/
-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;
+OO.ui.BookletLayout.prototype.setPage = function ( name ) {
+ var selectedItem,
+ $focused,
+ page = this.pages[ name ];
- 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 ) + '%';
+ if ( name !== this.currentPageName ) {
+ if ( this.outlined ) {
+ selectedItem = this.outlineSelectWidget.getSelectedItem();
+ if ( selectedItem && selectedItem.getData() !== name ) {
+ this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) );
}
- // 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 = '';
+ }
+ 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();
+ }
+ }
}
- panel.$element.css( dimensions );
- i++;
- left += width;
+ this.currentPageName = name;
+ this.stackLayout.setItem( page );
+ page.setActive( true );
+ this.emit( 'set', page );
}
- top += height;
- left = 0;
}
-
- this.emit( 'update' );
};
/**
- * Get a panel at a given position.
- *
- * The x and y position is affected by the current grid layout.
+ * Select the first selectable page.
*
- * @param {number} x Horizontal position
- * @param {number} y Vertical position
- * @return {OO.ui.PanelLayout} The panel at the given position
+ * @chainable
*/
-OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
- return this.panels[ ( x * this.widths.length ) + y ];
+OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
+ if ( !this.outlineSelectWidget.getSelectedItem() ) {
+ this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
+ }
+
+ return this;
};
/**
if ( this.continuous ) {
this.$element.addClass( 'oo-ui-stackLayout-continuous' );
}
- if ( $.isArray( config.items ) ) {
+ if ( Array.isArray( config.items ) ) {
this.addItems( config.items );
}
};
* @chainable
*/
OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
+ // Update the visibility
+ this.updateHiddenState( items, this.currentItem );
+
// Mixin method
OO.ui.GroupElement.prototype.addItems.call( this, items, index );
if ( !this.currentItem && items.length ) {
- this.setItem( items[0] );
+ this.setItem( items[ 0 ] );
}
return this;
if ( $.inArray( this.currentItem, items ) !== -1 ) {
if ( this.items.length ) {
- this.setItem( this.items[0] );
+ this.setItem( this.items[ 0 ] );
} else {
this.unsetCurrentItem();
}
* @fires set
*/
OO.ui.StackLayout.prototype.setItem = function ( item ) {
- var i, len;
-
if ( item !== this.currentItem ) {
- if ( !this.continuous ) {
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[i].$element.css( 'display', '' );
- }
- }
+ this.updateHiddenState( this.items, item );
+
if ( $.inArray( item, this.items ) !== -1 ) {
- if ( !this.continuous ) {
- item.$element.css( 'display', 'block' );
- }
this.currentItem = item;
this.emit( 'set', item );
} else {
return this;
};
+/**
+ * Update the visibility of all items in case of non-continuous view.
+ *
+ * Ensure all items are hidden except for the selected one.
+ * This method does nothing when the stack is continuous.
+ *
+ * @param {OO.ui.Layout[]} items Item list iterate over
+ * @param {OO.ui.Layout} [selectedItem] Selected item to show
+ */
+OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
+ var i, len;
+
+ if ( !this.continuous ) {
+ for ( i = 0, len = items.length; i < len; i++ ) {
+ if ( !selectedItem || selectedItem !== items[ i ] ) {
+ items[ i ].$element.addClass( 'oo-ui-element-hidden' );
+ }
+ }
+ if ( selectedItem ) {
+ selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
+ }
+ }
+};
+
/**
* Horizontal bar layout of tools as icon buttons.
*
this.active = false;
this.dragging = false;
this.onBlurHandler = this.onBlur.bind( this );
- this.$handle = this.$( '<span>' );
+ this.$handle = $( '<span>' );
// Events
this.$handle.on( {
// OO.ui.HeaderedElement mixin constructor.
if ( config.header !== undefined ) {
this.$group
- .prepend( this.$( '<span>' )
+ .prepend( $( '<span>' )
.addClass( 'oo-ui-popupToolGroup-header' )
.text( config.header )
);
*/
OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
// Only deactivate when clicking outside the dropdown element
- if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) {
+ if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
this.setActive( false );
}
};
this.collapsibleTools = [];
for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
- if ( this.tools[ allowCollapse[i] ] !== undefined ) {
- this.collapsibleTools.push( this.tools[ allowCollapse[i] ] );
+ if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
+ this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
}
}
// Keep at the end, even when tools are added
this.$group.append( this.getExpandCollapseTool().$element );
- this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
-
- // Calling jQuery's .hide() and then .show() on a detached element caches the default value of its
- // 'display' attribute and restores it, and the tool uses a <span> and can be hidden and re-shown.
- // Is this a jQuery bug? http://jsfiddle.net/gtj4hu3h/
- if ( this.getExpandCollapseTool().$element.css( 'display' ) === 'inline' ) {
- this.getExpandCollapseTool().$element.css( 'display', 'block' );
- }
-
+ this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
this.updateCollapsibleState();
};
var ret = OO.ui.ListToolGroup.super.prototype.onPointerUp.call( this, e );
// Do not close the popup when the user wants to show more/fewer tools
- if ( this.$( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) {
+ if ( $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) {
// Prevent the popup list from being hidden
this.setActive( true );
}
.setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
- this.collapsibleTools[i].toggle( this.expanded );
+ this.collapsibleTools[ i ].toggle( this.expanded );
}
};
labelTexts = [];
for ( name in this.tools ) {
- if ( this.tools[name].isActive() ) {
- labelTexts.push( this.tools[name].getTitle() );
+ if ( this.tools[ name ].isActive() ) {
+ labelTexts.push( this.tools[ name ].getTitle() );
}
}
// During construction, #setDisabled is called before the OO.ui.GroupElement constructor
if ( this.items ) {
for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[i].updateDisabled();
+ this.items[ i ].updateDisabled();
}
}
*
* @class
* @abstract
- * @deprecated Use LookupElement instead.
+ * @deprecated Use OO.ui.LookupElement instead.
*
* @constructor
* @param {OO.ui.TextInputWidget} input Input widget
this.lookupInput = input;
this.$overlay = config.$overlay || this.$element;
this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
- $: OO.ui.Element.static.getJQuery( this.$overlay ),
input: this.lookupInput,
$container: config.$container
} );
this.abortLookupRequest();
if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
- deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
+ deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[ value ] ) );
} else {
this.lookupInput.pushPending();
this.lookupQuery = value;
if ( ourRequest === widget.lookupRequest ) {
widget.lookupQuery = null;
widget.lookupRequest = null;
- widget.lookupCache[value] = widget.getLookupCacheItemFromData( data );
- deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[value] ) );
+ widget.lookupCache[ value ] = widget.getLookupCacheItemFromData( data );
+ deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[ value ] ) );
}
} )
.fail( function () {
// Properties
this.outline = outline;
- this.$movers = this.$( '<div>' );
+ this.$movers = $( '<div>' );
this.upButton = new OO.ui.ButtonWidget( {
- $: this.$,
framed: false,
icon: 'collapse',
title: OO.ui.msg( 'ooui-outline-control-move-up' )
} );
this.downButton = new OO.ui.ButtonWidget( {
- $: this.$,
framed: false,
icon: 'expand',
title: OO.ui.msg( 'ooui-outline-control-move-down' )
} );
this.removeButton = new OO.ui.ButtonWidget( {
- $: this.$,
framed: false,
icon: 'remove',
title: OO.ui.msg( 'ooui-outline-control-remove' )
i = -1;
len = items.length;
while ( ++i < len ) {
- if ( items[i].isMovable() ) {
- firstMovable = items[i];
+ if ( items[ i ].isMovable() ) {
+ firstMovable = items[ i ];
break;
}
}
i = len;
while ( i-- ) {
- if ( items[i].isMovable() ) {
- lastMovable = items[i];
+ if ( items[ i ].isMovable() ) {
+ lastMovable = items[ i ];
break;
}
}
this.emit( 'change', value );
this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
+ this.$element.attr( 'aria-checked', value.toString() );
}
return this;
};
/**
- * Group widget for multiple related buttons.
- *
- * Use together with OO.ui.ButtonWidget.
+ * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
+ * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
+ * removed, and cleared from the group.
+ *
+ * @example
+ * // Example: A ButtonGroupWidget with two buttons
+ * var button1 = new OO.ui.PopupButtonWidget( {
+ * label : 'Select a category',
+ * icon : 'menu',
+ * popup : {
+ * $content: $( '<p>List of categories...</p>' ),
+ * padded: true,
+ * align: 'left'
+ * }
+ * } );
+ * var button2 = new OO.ui.ButtonWidget( {
+ * label : 'Add item'
+ * });
+ * var buttonGroup = new OO.ui.ButtonGroupWidget( {
+ * items: [button1, button2]
+ * } );
+ * $('body').append(buttonGroup.$element);
*
* @class
* @extends OO.ui.Widget
// Initialization
this.$element.addClass( 'oo-ui-buttonGroupWidget' );
- if ( $.isArray( config.items ) ) {
+ if ( Array.isArray( config.items ) ) {
this.addItems( config.items );
}
};
OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
/**
- * Generic widget for buttons.
+ * ButtonWidget is a generic widget for buttons. A wide variety of looks,
+ * feels, and functionality can be customized via the class’s configuration options
+ * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
+ * and examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
+ *
+ * @example
+ * // A button widget
+ * var button = new OO.ui.ButtonWidget( {
+ * label : 'Button with Icon',
+ * icon : 'remove',
+ * iconTitle : 'Remove'
+ * } );
+ * $( 'body' ).append( button.$element );
+ *
+ * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
*
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.LabelElement
* @mixins OO.ui.TitledElement
* @mixins OO.ui.FlaggedElement
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {string} [href] Hyperlink to visit when clicked
- * @cfg {string} [target] Target to open hyperlink in
+ * @cfg {string} [href] Hyperlink to visit when the button is clicked.
+ * @cfg {string} [target] The frame or window in which to open the hyperlink.
+ * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
*/
OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
// Configuration initialization
- config = config || {};
+ // FIXME: The `nofollow` alias is deprecated and will be removed (T89767)
+ config = $.extend( { noFollow: config && config.nofollow }, config );
// Parent constructor
OO.ui.ButtonWidget.super.call( this, config );
OO.ui.LabelElement.call( this, config );
OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
OO.ui.FlaggedElement.call( this, config );
+ OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
// Properties
this.href = null;
this.target = null;
+ this.noFollow = false;
this.isHyperlink = false;
- // Events
- this.$button.on( {
- click: this.onClick.bind( this ),
- keypress: this.onKeyPress.bind( this )
- } );
-
// Initialization
this.$button.append( this.$icon, this.$label, this.$indicator );
this.$element
.append( this.$button );
this.setHref( config.href );
this.setTarget( config.target );
+ this.setNoFollow( config.noFollow );
};
/* Setup */
OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TabIndexedElement );
-/* Events */
+/* Methods */
/**
- * @event click
+ * @inheritdoc
*/
+OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
+ if ( !this.isDisabled() ) {
+ // Remove the tab-index while the button is down to prevent the button from stealing focus
+ this.$button.removeAttr( 'tabindex' );
+ }
-/* Methods */
+ return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e );
+};
/**
- * Handles mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
- * @fires click
+ * @inheritdoc
*/
-OO.ui.ButtonWidget.prototype.onClick = function () {
+OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
if ( !this.isDisabled() ) {
- this.emit( 'click' );
- if ( this.isHyperlink ) {
- return true;
- }
+ // Restore the tab-index after the button is up to restore the button's accessibility
+ this.$button.attr( 'tabindex', this.tabIndex );
}
- return false;
+
+ return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e );
};
/**
- * Handles keypress events.
- *
- * @param {jQuery.Event} e Keypress event
- * @fires click
+ * @inheritdoc
+ */
+OO.ui.ButtonWidget.prototype.onClick = function ( e ) {
+ var ret = OO.ui.ButtonElement.prototype.onClick.call( this, e );
+ if ( this.isHyperlink ) {
+ return true;
+ }
+ return ret;
+};
+
+/**
+ * @inheritdoc
*/
OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
- if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
- this.emit( 'click' );
- if ( this.isHyperlink ) {
- return true;
- }
+ var ret = OO.ui.ButtonElement.prototype.onKeyPress.call( this, e );
+ if ( this.isHyperlink ) {
+ return true;
}
- return false;
+ return ret;
};
/**
return this.target;
};
+/**
+ * Get search engine traversal hint.
+ *
+ * @return {boolean} Whether search engines should avoid traversing this hyperlink
+ */
+OO.ui.ButtonWidget.prototype.getNoFollow = function () {
+ return this.noFollow;
+};
+
/**
* Set hyperlink location.
*
};
/**
- * Button widget that executes an action and is managed by an OO.ui.ActionSet.
+ * Set search engine traversal hint.
+ *
+ * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
+ */
+OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
+ noFollow = typeof noFollow === 'boolean' ? noFollow : true;
+
+ if ( noFollow !== this.noFollow ) {
+ this.noFollow = noFollow;
+ if ( noFollow ) {
+ this.$button.attr( 'rel', 'nofollow' );
+ } else {
+ this.$button.removeAttr( 'rel' );
+ }
+ }
+
+ return this;
+};
+
+/**
+ * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
+ * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
+ * of the actions. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
+ * and examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
*
* @class
* @extends OO.ui.ButtonWidget
// Mixin constructors
OO.ui.PopupElement.call( this, config );
+ // Events
+ this.connect( this, { click: 'onAction' } );
+
// Initialization
this.$element
.addClass( 'oo-ui-popupButtonWidget' )
+ .attr( 'aria-haspopup', 'true' )
.append( this.popup.$element );
};
/* Methods */
/**
- * Handles mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
+ * Handle the button action being triggered.
*/
-OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
- // Skip clicks within the popup
- if ( $.contains( this.popup.$element[0], e.target ) ) {
- return;
- }
-
- if ( !this.isDisabled() ) {
- this.popup.toggle();
- // Parent method
- OO.ui.PopupButtonWidget.super.prototype.onClick.call( this );
- }
- return false;
+OO.ui.PopupButtonWidget.prototype.onAction = function () {
+ this.popup.toggle();
};
/**
// Mixin constructors
OO.ui.ToggleWidget.call( this, config );
+ // Events
+ this.connect( this, { click: 'onAction' } );
+
// Initialization
this.$element.addClass( 'oo-ui-toggleButtonWidget' );
};
/* Methods */
/**
- * @inheritdoc
+ * Handle the button action being triggered.
*/
-OO.ui.ToggleButtonWidget.prototype.onClick = function () {
- if ( !this.isDisabled() ) {
- this.setValue( !this.value );
- }
-
- // Parent method
- return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this );
+OO.ui.ToggleButtonWidget.prototype.onAction = function () {
+ this.setValue( !this.value );
};
/**
OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
value = !!value;
if ( value !== this.value ) {
+ this.$button.attr( 'aria-pressed', value.toString() );
this.setActive( value );
}
};
/**
- * Dropdown menu of options.
+ * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
+ * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
+ * users can interact with it.
+ *
+ * @example
+ * // Example: A DropdownWidget with a menu that contains three options
+ * var dropDown=new OO.ui.DropdownWidget( {
+ * label: 'Dropdown menu: Select a menu option',
+ * menu: {
+ * items: [
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'a',
+ * label: 'First'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'b',
+ * label: 'Second'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'c',
+ * label: 'Third'
+ * } )
+ * ]
+ * }
+ * } );
+ *
+ * $('body').append(dropDown.$element);
*
- * Dropdown menus provide a control for accessing a menu and compose a menu within the widget, which
- * can be accessed using the #getMenu method.
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
*
- * Use with OO.ui.MenuOptionWidget.
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
*
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.IndicatorElement
* @mixins OO.ui.LabelElement
* @mixins OO.ui.TitledElement
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
// Parent constructor
OO.ui.DropdownWidget.super.call( this, config );
+ // Properties (must be set before TabIndexedElement constructor call)
+ this.$handle = this.$( '<span>' );
+
// Mixin constructors
OO.ui.IconElement.call( this, config );
OO.ui.IndicatorElement.call( this, config );
OO.ui.LabelElement.call( this, config );
OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
+ OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
// Properties
- this.menu = new OO.ui.MenuSelectWidget( $.extend( { $: this.$, widget: this }, config.menu ) );
- this.$handle = this.$( '<span>' );
+ this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
// Events
- this.$element.on( { click: this.onClick.bind( this ) } );
+ this.$handle.on( {
+ click: this.onClick.bind( this ),
+ keypress: this.onKeyPress.bind( this )
+ } );
this.menu.connect( this, { select: 'onMenuSelect' } );
// Initialization
OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TabIndexedElement );
/* Methods */
/**
* Handles menu select events.
*
+ * @private
* @param {OO.ui.MenuOptionWidget} item Selected menu item
*/
OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
};
/**
- * Handles mouse click events.
+ * Handle mouse click events.
*
+ * @private
* @param {jQuery.Event} e Mouse click event
*/
OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
- // Skip clicks within the menu
- if ( $.contains( this.menu.$element[0], e.target ) ) {
- return;
+ if ( !this.isDisabled() && e.which === 1 ) {
+ this.menu.toggle();
}
+ return false;
+};
- if ( !this.isDisabled() ) {
- if ( this.menu.isVisible() ) {
- this.menu.toggle( false );
- } else {
- this.menu.toggle( true );
- }
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
+ if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+ this.menu.toggle();
}
return false;
};
/**
- * Icon widget.
+ * IconWidget is a generic widget for {@link OO.ui.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
+ * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
+ * for a list of icons included in the library.
*
- * See OO.ui.IconElement for more information.
+ * @example
+ * // An icon widget with a label
+ * var myIcon = new OO.ui.IconWidget({
+ * icon: 'help',
+ * iconTitle: 'Help'
+ * });
+ * // Create a label.
+ * var iconLabel = new OO.ui.LabelWidget({
+ * label: 'Help'
+ * });
+ * $('body').append(myIcon.$element, iconLabel.$element);
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
*
* @class
* @extends OO.ui.Widget
OO.ui.IndicatorWidget.static.tagName = 'span';
/**
- * Base class for input widgets.
+ * InputWidget is the base class for all input widgets, which
+ * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
+ * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
+ * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
*
* @abstract
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.FlaggedElement
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
// Parent constructor
OO.ui.InputWidget.super.call( this, config );
- // Mixin constructors
- OO.ui.FlaggedElement.call( this, config );
-
// Properties
this.$input = this.getInputElement( config );
this.value = '';
this.inputFilter = config.inputFilter;
+ // Mixin constructors
+ OO.ui.FlaggedElement.call( this, config );
+ OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
+
// Events
this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement );
+OO.mixinClass( OO.ui.InputWidget, OO.ui.TabIndexedElement );
/* Events */
/**
* Get input element.
*
+ * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
+ * different circumstances. The element must have a `value` property (like form elements).
+ *
* @private
- * @param {Object} [config] Configuration options
+ * @param {Object} config Configuration options
* @return {jQuery} Input element
*/
OO.ui.InputWidget.prototype.getInputElement = function () {
- return this.$( '<input>' );
+ return $( '<input>' );
};
/**
* @return {string} Input value
*/
OO.ui.InputWidget.prototype.getValue = function () {
+ // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
+ // it, and we won't know unless they're kind enough to trigger a 'change' event.
+ var value = this.$input.val();
+ if ( this.value !== value ) {
+ this.setValue( value );
+ }
return this.value;
};
* @param {boolean} isRTL
*/
OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
- if ( isRTL ) {
- this.$input.removeClass( 'oo-ui-ltr' );
- this.$input.addClass( 'oo-ui-rtl' );
- } else {
- this.$input.removeClass( 'oo-ui-rtl' );
- this.$input.addClass( 'oo-ui-ltr' );
- }
+ this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
};
/**
if ( this.$input.is( ':checkbox,:radio' ) ) {
this.$input.click();
} else if ( this.$input.is( ':input' ) ) {
- this.$input[0].focus();
+ this.$input[ 0 ].focus();
}
}
};
* @chainable
*/
OO.ui.InputWidget.prototype.focus = function () {
- this.$input[0].focus();
+ this.$input[ 0 ].focus();
return this;
};
* @chainable
*/
OO.ui.InputWidget.prototype.blur = function () {
- this.$input[0].blur();
+ this.$input[ 0 ].blur();
return this;
};
/**
- * A button that is an input widget. Intended to be used within a OO.ui.FormLayout.
+ * ButtonInputWidget is used to submit HTML forms and is intended to be used within
+ * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
+ * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
+ * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
+ * [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * @example
+ * // A ButtonInputWidget rendered as an HTML button, the default.
+ * var button = new OO.ui.ButtonInputWidget( {
+ * label: 'Input button',
+ * icon: 'check',
+ * value: 'check'
+ * } );
+ * $( 'body' ).append( button.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
*
* @class
* @extends OO.ui.InputWidget
OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
OO.ui.FlaggedElement.call( this, config );
- // Events
- this.$input.on( {
- click: this.onClick.bind( this ),
- keypress: this.onKeyPress.bind( this )
- } );
-
// Initialization
if ( !config.useInputTag ) {
this.$input.append( this.$icon, this.$label, this.$indicator );
OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement );
OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
-/* Events */
-
-/**
- * @event click
- */
-
/* Methods */
/**
- * Get input element.
- *
+ * @inheritdoc
* @private
- * @param {Object} [config] Configuration options
- * @return {jQuery} Input element
*/
OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
- // Configuration initialization
- config = config || {};
-
var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
-
- return this.$( html );
+ return $( html );
};
/**
return this;
};
-/**
- * Handles mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
- * @fires click
- */
-OO.ui.ButtonInputWidget.prototype.onClick = function () {
- if ( !this.isDisabled() ) {
- this.emit( 'click' );
- }
- return false;
-};
-
-/**
- * Handles keypress events.
- *
- * @param {jQuery.Event} e Keypress event
- * @fires click
- */
-OO.ui.ButtonInputWidget.prototype.onKeyPress = function ( e ) {
- if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
- this.emit( 'click' );
- }
- return false;
-};
-
/**
* Checkbox input widget.
*
* @cfg {boolean} [selected=false] Whether the checkbox is initially selected
*/
OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
// Parent constructor
OO.ui.CheckboxInputWidget.super.call( this, config );
/* Methods */
/**
- * Get input element.
- *
+ * @inheritdoc
* @private
- * @return {jQuery} Input element
*/
OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
- return this.$( '<input type="checkbox" />' );
+ return $( '<input type="checkbox" />' );
};
/**
*
* @return {boolean} Checkbox is selected
*/
-OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
- return this.selected;
+OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
+ // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
+ // it, and we won't know unless they're kind enough to trigger a 'change' event.
+ var selected = this.$input.prop( 'checked' );
+ if ( this.selected !== selected ) {
+ this.setSelected( selected );
+ }
+ return this.selected;
+};
+
+/**
+ * A OO.ui.DropdownWidget synchronized with a `<input type=hidden>` for form submission. Intended to
+ * be used within a OO.ui.FormLayout.
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
+ */
+OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties (must be done before parent constructor which calls #setDisabled)
+ this.dropdownWidget = new OO.ui.DropdownWidget();
+
+ // Parent constructor
+ OO.ui.DropdownInputWidget.super.call( this, config );
+
+ // Events
+ this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
+
+ // Initialization
+ this.setOptions( config.options || [] );
+ this.$element
+ .addClass( 'oo-ui-dropdownInputWidget' )
+ .append( this.dropdownWidget.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ * @private
+ */
+OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
+ return $( '<input type="hidden">' );
+};
+
+/**
+ * Handles menu select events.
+ *
+ * @param {OO.ui.MenuOptionWidget} item Selected menu item
+ */
+OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
+ this.setValue( item.getData() );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
+ var item = this.dropdownWidget.getMenu().getItemFromData( value );
+ if ( item ) {
+ this.dropdownWidget.getMenu().selectItem( item );
+ }
+ OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value );
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
+ this.dropdownWidget.setDisabled( state );
+ OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state );
+ return this;
+};
+
+/**
+ * Set the options available for this input.
+ *
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @chainable
+ */
+OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
+ var value = this.getValue();
+
+ // Rebuild the dropdown menu
+ this.dropdownWidget.getMenu()
+ .clearItems()
+ .addItems( options.map( function ( opt ) {
+ return new OO.ui.MenuOptionWidget( {
+ data: opt.data,
+ label: opt.label !== undefined ? opt.label : opt.data
+ } );
+ } ) );
+
+ // Restore the previous value, or reset to something sensible
+ if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
+ // Previous value is still available, ensure consistency with the dropdown
+ this.setValue( value );
+ } else {
+ // No longer valid, reset
+ if ( options.length ) {
+ this.setValue( options[ 0 ].data );
+ }
+ }
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.focus = function () {
+ this.dropdownWidget.getMenu().toggle( true );
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.blur = function () {
+ this.dropdownWidget.getMenu().toggle( false );
+ return this;
};
/**
* @cfg {boolean} [selected=false] Whether the radio button is initially selected
*/
OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
// Parent constructor
OO.ui.RadioInputWidget.super.call( this, config );
/* Methods */
/**
- * Get input element.
- *
+ * @inheritdoc
* @private
- * @return {jQuery} Input element
*/
OO.ui.RadioInputWidget.prototype.getInputElement = function () {
- return this.$( '<input type="radio" />' );
+ return $( '<input type="radio" />' );
};
/**
* @mixins OO.ui.IconElement
* @mixins OO.ui.IndicatorElement
* @mixins OO.ui.PendingElement
+ * @mixins OO.ui.LabelElement
*
* @constructor
* @param {Object} [config] Configuration options
* @cfg {boolean} [autofocus=false] Ask the browser to focus this widget, using the 'autofocus' HTML
* attribute
* @cfg {boolean} [readOnly=false] Prevent changes
+ * @cfg {number} [maxLength] Maximum allowed number of characters to input
* @cfg {boolean} [multiline=false] Allow multiple lines of text
* @cfg {boolean} [autosize=false] Automatically resize to fit content
* @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
- * @cfg {RegExp|string} [validate] Regular expression (or symbolic name referencing
+ * @cfg {string} [labelPosition='after'] Label position, 'before' or 'after'
+ * @cfg {boolean} [required=false] Mark the field as required
+ * @cfg {RegExp|string} [validate] Regular expression to validate against (or symbolic name referencing
* one, see #static-validationPatterns)
*/
OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
// Configuration initialization
- config = $.extend( { readOnly: false }, config );
+ config = $.extend( {
+ type: 'text',
+ labelPosition: 'after',
+ maxRows: 10
+ }, config );
// Parent constructor
OO.ui.TextInputWidget.super.call( this, config );
OO.ui.IconElement.call( this, config );
OO.ui.IndicatorElement.call( this, config );
OO.ui.PendingElement.call( this, config );
+ OO.ui.LabelElement.call( this, config );
// Properties
this.readOnly = false;
this.multiline = !!config.multiline;
this.autosize = !!config.autosize;
- this.maxRows = config.maxRows !== undefined ? config.maxRows : 10;
+ this.maxRows = config.maxRows;
this.validate = null;
// Clone for resizing
this.$clone = this.$input
.clone()
.insertAfter( this.$input )
- .hide();
+ .attr( 'aria-hidden', 'true' )
+ .addClass( 'oo-ui-element-hidden' );
}
this.setValidation( config.validate );
+ this.setPosition( config.labelPosition );
// Events
this.$input.on( {
this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
+ this.on( 'labelChange', this.updatePosition.bind( this ) );
// Initialization
this.$element
.addClass( 'oo-ui-textInputWidget' )
.append( this.$icon, this.$indicator );
- this.setReadOnly( config.readOnly );
+ this.setReadOnly( !!config.readOnly );
if ( config.placeholder ) {
this.$input.attr( 'placeholder', config.placeholder );
}
+ if ( config.maxLength !== undefined ) {
+ this.$input.attr( 'maxlength', config.maxLength );
+ }
if ( config.autofocus ) {
this.$input.attr( 'autofocus', 'autofocus' );
}
- this.$element.attr( 'role', 'textbox' );
+ if ( config.required ) {
+ this.$input.attr( 'required', 'true' );
+ }
};
/* Setup */
OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.LabelElement );
/* Static properties */
/**
* User clicks the icon.
*
+ * @deprecated Fundamentally not accessible. Make the icon focusable, associate a label or tooltip,
+ * and handle click/keypress events on it manually.
* @event icon
*/
/**
* User clicks the indicator.
*
+ * @deprecated Fundamentally not accessible. Make the indicator focusable, associate a label or
+ * tooltip, and handle click/keypress events on it manually.
* @event indicator
*/
*/
OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
if ( e.which === 1 ) {
- this.$input[0].focus();
+ this.$input[ 0 ].focus();
this.emit( 'icon' );
return false;
}
*/
OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
if ( e.which === 1 ) {
- this.$input[0].focus();
+ this.$input[ 0 ].focus();
this.emit( 'indicator' );
return false;
}
* @param {jQuery.Event} e Element attach event
*/
OO.ui.TextInputWidget.prototype.onElementAttach = function () {
+ // Any previously calculated size is now probably invalid if we reattached elsewhere
+ this.valCache = null;
this.adjustSize();
+ this.positionLabel();
};
/**
// Set inline height property to 0 to measure scroll height
.css( 'height', 0 );
- this.$clone[0].style.display = 'block';
+ this.$clone.removeClass( 'oo-ui-element-hidden' );
this.valCache = this.$input.val();
- scrollHeight = this.$clone[0].scrollHeight;
+ scrollHeight = this.$clone[ 0 ].scrollHeight;
// Remove inline height property to measure natural heights
this.$clone.css( 'height', '' );
// Difference between reported innerHeight and scrollHeight with no scrollbars present
// Equals 1 on Blink-based browsers and 0 everywhere else
- measurementError = maxInnerHeight - this.$clone[0].scrollHeight;
+ measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
- this.$clone[0].style.display = 'none';
+ this.$clone.addClass( 'oo-ui-element-hidden' );
// Only apply inline height when expansion beyond natural height is needed
if ( idealHeight > innerHeight ) {
};
/**
- * Get input element.
- *
+ * @inheritdoc
* @private
- * @param {Object} [config] Configuration options
- * @return {jQuery} Input element
*/
OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
- // Configuration initialization
- config = config || {};
-
- var type = config.type || 'text';
-
- return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="' + type + '" />' );
+ return config.multiline ? $( '<textarea>' ) : $( '<input type="' + config.type + '" />' );
};
/**
if ( validate instanceof RegExp ) {
this.validate = validate;
} else {
- this.validate = this.constructor.static.validationPatterns[validate] || /.*/;
+ this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
}
};
return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
};
+/**
+ * Set the position of the inline label.
+ *
+ * @param {string} labelPosition Label position, 'before' or 'after'
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.setPosition = function ( labelPosition ) {
+ this.labelPosition = labelPosition;
+ this.updatePosition();
+ return this;
+};
+
+/**
+ * Update the position of the inline label.
+ *
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.updatePosition = function () {
+ var after = this.labelPosition === 'after';
+
+ this.$element
+ .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
+ .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
+
+ if ( this.label ) {
+ this.positionLabel();
+ }
+
+ return this;
+};
+
+/**
+ * Position the label by setting the correct padding on the input.
+ *
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.positionLabel = function () {
+ // Clear old values
+ this.$input
+ // Clear old values if present
+ .css( {
+ 'padding-right': '',
+ 'padding-left': ''
+ } );
+
+ if ( this.label ) {
+ this.$element.append( this.$label );
+ } else {
+ this.$label.detach();
+ return;
+ }
+
+ var after = this.labelPosition === 'after',
+ rtl = this.$element.css( 'direction' ) === 'rtl',
+ property = after === rtl ? 'padding-left' : 'padding-right';
+
+ this.$input.css( property, this.$label.outerWidth( true ) );
+
+ return this;
+};
+
/**
* Text input with a menu of optional values.
*
* @class
* @extends OO.ui.Widget
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
// Parent constructor
OO.ui.ComboBoxWidget.super.call( this, config );
+ // Properties (must be set before TabIndexedElement constructor call)
+ this.$indicator = this.$( '<span>' );
+
+ // Mixin constructors
+ OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
+
// Properties
this.$overlay = config.$overlay || this.$element;
this.input = new OO.ui.TextInputWidget( $.extend(
- { $: this.$, indicator: 'down', disabled: this.isDisabled() },
+ {
+ indicator: 'down',
+ $indicator: this.$indicator,
+ disabled: this.isDisabled()
+ },
config.input
) );
this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
{
- $: OO.ui.Element.static.getJQuery( this.$overlay ),
widget: this,
input: this.input,
disabled: this.isDisabled()
) );
// Events
+ this.$indicator.on( {
+ click: this.onClick.bind( this ),
+ keypress: this.onKeyPress.bind( this )
+ } );
this.input.connect( this, {
change: 'onInputChange',
- indicator: 'onInputIndicator',
enter: 'onInputEnter'
} );
this.menu.connect( this, {
/* Setup */
OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.TabIndexedElement );
/* Methods */
var match = this.menu.getItemFromData( value );
this.menu.selectItem( match );
+ if ( this.menu.getHighlightedItem() ) {
+ this.menu.highlightItem( match );
+ }
if ( !this.isDisabled() ) {
this.menu.toggle( true );
};
/**
- * Handle input indicator events.
+ * Handle mouse click events.
+ *
+ * @param {jQuery.Event} e Mouse click event
*/
-OO.ui.ComboBoxWidget.prototype.onInputIndicator = function () {
- if ( !this.isDisabled() ) {
+OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
+ if ( !this.isDisabled() && e.which === 1 ) {
+ this.menu.toggle();
+ this.input.$input[ 0 ].focus();
+ }
+ return false;
+};
+
+/**
+ * Handle key press events.
+ *
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
+ if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
this.menu.toggle();
+ this.input.$input[ 0 ].focus();
}
+ return false;
};
/**
OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
var match = this.menu.getItemFromData( this.input.getValue() );
this.menu.selectItem( match );
+ if ( this.menu.getHighlightedItem() ) {
+ this.menu.highlightItem( match );
+ }
this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
};
OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
if ( this.constructor.static.selectable ) {
this.selected = !!state;
- this.$element.toggleClass( 'oo-ui-optionWidget-selected', state );
+ this.$element
+ .toggleClass( 'oo-ui-optionWidget-selected', state )
+ .attr( 'aria-selected', state.toString() );
if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
this.scrollElementIntoView();
}
return this;
};
-/**
- * Make the option's highlight flash.
- *
- * While flashing, the visual style of the pressed state is removed if present.
- *
- * @return {jQuery.Promise} Promise resolved when flashing is done
- */
-OO.ui.OptionWidget.prototype.flash = function () {
- var widget = this,
- $element = this.$element,
- deferred = $.Deferred();
-
- if ( !this.isDisabled() && this.constructor.static.pressable ) {
- $element.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
- setTimeout( function () {
- // Restore original classes
- $element
- .toggleClass( 'oo-ui-optionWidget-highlighted', widget.highlighted )
- .toggleClass( 'oo-ui-optionWidget-pressed', widget.pressed );
-
- setTimeout( function () {
- deferred.resolve();
- }, 100 );
-
- }, 100 );
- }
-
- return deferred.promise();
-};
-
/**
* Option widget with an option icon and indicator.
*
* @class
* @extends OO.ui.DecoratedOptionWidget
* @mixins OO.ui.ButtonElement
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
*/
OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
+ // Configuration initialization
+ config = $.extend( { tabIndex: -1 }, config );
+
// Parent constructor
OO.ui.ButtonOptionWidget.super.call( this, config );
// Mixin constructors
OO.ui.ButtonElement.call( this, config );
+ OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
// Initialization
this.$element.addClass( 'oo-ui-buttonOptionWidget' );
OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
+OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.TabIndexedElement );
/* Static Properties */
// Allow button mouse down events to pass through so they can be handled by the parent select widget
OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
+OO.ui.ButtonOptionWidget.static.highlightable = false;
+
/* Methods */
/**
OO.ui.RadioOptionWidget.super.call( this, config );
// Properties
- this.radio = new OO.ui.RadioInputWidget( { value: config.data } );
+ this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
// Initialization
this.$element
OO.ui.RadioOptionWidget.static.highlightable = false;
+OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
+
OO.ui.RadioOptionWidget.static.pressable = false;
+OO.ui.RadioOptionWidget.static.tagName = 'label';
+
/* Methods */
/**
OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
+/* Static Properties */
+
+OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
+
/**
* Section to group one or more items in a OO.ui.MenuSelectWidget.
*
// Parent constructor
OO.ui.PopupWidget.super.call( this, config );
+ // Properties (must be set before ClippableElement constructor call)
+ this.$body = $( '<div>' );
+
// Mixin constructors
OO.ui.LabelElement.call( this, config );
- OO.ui.ClippableElement.call( this, config );
+ OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
// Properties
- this.visible = false;
- this.$popup = this.$( '<div>' );
- this.$head = this.$( '<div>' );
- this.$body = this.$( '<div>' );
- this.$anchor = this.$( '<div>' );
+ this.$popup = $( '<div>' );
+ this.$head = $( '<div>' );
+ this.$anchor = $( '<div>' );
// If undefined, will be computed lazily in updateDimensions()
this.$container = config.$container;
this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
this.width = config.width !== undefined ? config.width : 320;
this.height = config.height !== undefined ? config.height : null;
this.align = config.align || 'center';
- this.closeButton = new OO.ui.ButtonWidget( { $: this.$, framed: false, icon: 'close' } );
+ this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
this.onMouseDownHandler = this.onMouseDown.bind( this );
// Events
.addClass( 'oo-ui-popupWidget-head' )
.append( this.$label, this.closeButton.$element );
if ( !config.head ) {
- this.$head.hide();
+ this.$head.addClass( 'oo-ui-element-hidden' );
}
this.$popup
.addClass( 'oo-ui-popupWidget-popup' )
.append( this.$head, this.$body );
this.$element
- .hide()
.addClass( 'oo-ui-popupWidget' )
.append( this.$popup, this.$anchor );
// Move content, which was added to #$element by OO.ui.Widget, to the body
if ( config.padded ) {
this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
}
- this.setClippableElement( this.$body );
+
+ // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
+ // that reference properties not initialized at that time of parent class construction
+ // TODO: Find a better way to handle post-constructor setup
+ this.visible = false;
+ this.$element.addClass( 'oo-ui-element-hidden' );
};
/* Setup */
OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
if (
this.isVisible() &&
- !$.contains( this.$element[0], e.target ) &&
+ !$.contains( this.$element[ 0 ], e.target ) &&
( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
) {
this.toggle( false );
if ( !this.$container ) {
// Lazy-initialize $container if not specified in constructor
- this.$container = this.$( this.getClosestScrollableElementContainer() );
+ this.$container = $( this.getClosestScrollableElementContainer() );
}
// Set height and width before measuring things, since it might cause our measurements
} );
// Compute initial popupOffset based on alignment
- popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[this.align];
+ popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[ this.align ];
// Figure out if this will cause the popup to go beyond the edge of the container
originOffset = this.$element.offset().left;
// Adjust offset to avoid anchor being rendered too close to the edge
// $anchor.width() doesn't work with the pure CSS anchor (returns 0)
// TODO: Find a measurement that works for CSS anchors and image anchors
- anchorWidth = this.$anchor[0].scrollWidth * 2;
+ anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
if ( popupOffset + this.width < anchorWidth ) {
popupOffset = anchorWidth - this.width;
} else if ( -popupOffset < anchorWidth ) {
OO.ui.ProgressBarWidget.super.call( this, config );
// Properties
- this.$bar = this.$( '<div>' );
+ this.$bar = $( '<div>' );
this.progress = null;
// Initialization
// Properties
this.query = new OO.ui.TextInputWidget( {
- $: this.$,
icon: 'search',
placeholder: config.placeholder,
value: config.value
} );
- this.results = new OO.ui.SelectWidget( { $: this.$ } );
- this.$query = this.$( '<div>' );
- this.$results = this.$( '<div>' );
+ this.results = new OO.ui.SelectWidget();
+ this.$query = $( '<div>' );
+ this.$results = $( '<div>' );
// Events
this.query.connect( this, {
};
/**
- * Generic selection of options.
+ * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
+ * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
+ * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
+ * menu selects}.
*
- * Items can contain any rendering. Any widget that provides options, from which the user must
- * choose one, should be built on this class.
+ * This class should be used together with OO.ui.OptionWidget.
*
- * Use together with OO.ui.OptionWidget.
+ * For more information, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
*
* @class
* @extends OO.ui.Widget
this.selecting = null;
this.onMouseUpHandler = this.onMouseUp.bind( this );
this.onMouseMoveHandler = this.onMouseMove.bind( this );
+ this.onKeyDownHandler = this.onKeyDown.bind( this );
// Events
this.$element.on( {
} );
// Initialization
- this.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
- if ( $.isArray( config.items ) ) {
+ this.$element
+ .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
+ .attr( 'role', 'listbox' );
+ if ( Array.isArray( config.items ) ) {
this.addItems( config.items );
}
};
return false;
};
+/**
+ * Handle key down events.
+ *
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
+ var nextItem,
+ handled = false,
+ currentItem = this.getHighlightedItem() || this.getSelectedItem();
+
+ if ( !this.isDisabled() && this.isVisible() ) {
+ switch ( e.keyCode ) {
+ case OO.ui.Keys.ENTER:
+ if ( currentItem && currentItem.constructor.static.highlightable ) {
+ // Was only highlighted, now let's select it. No-op if already selected.
+ this.chooseItem( currentItem );
+ handled = true;
+ }
+ break;
+ case OO.ui.Keys.UP:
+ case OO.ui.Keys.LEFT:
+ nextItem = this.getRelativeSelectableItem( currentItem, -1 );
+ handled = true;
+ break;
+ case OO.ui.Keys.DOWN:
+ case OO.ui.Keys.RIGHT:
+ nextItem = this.getRelativeSelectableItem( currentItem, 1 );
+ handled = true;
+ break;
+ case OO.ui.Keys.ESCAPE:
+ case OO.ui.Keys.TAB:
+ if ( currentItem && currentItem.constructor.static.highlightable ) {
+ currentItem.setHighlighted( false );
+ }
+ this.unbindKeyDownListener();
+ // Don't prevent tabbing away / defocusing
+ handled = false;
+ break;
+ }
+
+ if ( nextItem ) {
+ if ( nextItem.constructor.static.highlightable ) {
+ this.highlightItem( nextItem );
+ } else {
+ this.chooseItem( nextItem );
+ }
+ nextItem.scrollElementIntoView();
+ }
+
+ if ( handled ) {
+ // Can't just return false, because e is not always a jQuery event
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+};
+
+/**
+ * Bind key down listener.
+ */
+OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
+ this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
+};
+
+/**
+ * Unbind key down listener.
+ */
+OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
+ this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
+};
+
/**
* Get the closest item to a jQuery.Event.
*
* @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
*/
OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
- var $item = this.$( e.target ).closest( '.oo-ui-optionWidget' );
+ var $item = $( e.target ).closest( '.oo-ui-optionWidget' );
if ( $item.length ) {
return $item.data( 'oo-ui-optionWidget' );
}
var i, len;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- if ( this.items[i].isSelected() ) {
- return this.items[i];
+ if ( this.items[ i ].isSelected() ) {
+ return this.items[ i ];
}
}
return null;
var i, len;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- if ( this.items[i].isHighlighted() ) {
- return this.items[i];
+ if ( this.items[ i ].isHighlighted() ) {
+ return this.items[ i ];
}
}
return null;
changed = false;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- highlighted = this.items[i] === item;
- if ( this.items[i].isHighlighted() !== highlighted ) {
- this.items[i].setHighlighted( highlighted );
+ highlighted = this.items[ i ] === item;
+ if ( this.items[ i ].isHighlighted() !== highlighted ) {
+ this.items[ i ].setHighlighted( highlighted );
changed = true;
}
}
changed = false;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- selected = this.items[i] === item;
- if ( this.items[i].isSelected() !== selected ) {
- this.items[i].setSelected( selected );
+ selected = this.items[ i ] === item;
+ if ( this.items[ i ].isSelected() !== selected ) {
+ this.items[ i ].setSelected( selected );
changed = true;
}
}
changed = false;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- pressed = this.items[i] === item;
- if ( this.items[i].isPressed() !== pressed ) {
- this.items[i].setPressed( pressed );
+ pressed = this.items[ i ] === item;
+ if ( this.items[ i ].isPressed() !== pressed ) {
+ this.items[ i ].setPressed( pressed );
changed = true;
}
}
}
for ( i = 0; i < len; i++ ) {
- item = this.items[nextIndex];
+ item = this.items[ nextIndex ];
if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
return item;
}
var i, len, item;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
return item;
}
// Deselect items being removed
for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
+ item = items[ i ];
if ( item.isSelected() ) {
this.selectItem( null );
}
*
* @class
* @extends OO.ui.SelectWidget
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
// Parent constructor
OO.ui.ButtonSelectWidget.super.call( this, config );
+ // Mixin constructors
+ OO.ui.TabIndexedElement.call( this, config );
+
+ // Events
+ this.$element.on( {
+ focus: this.bindKeyDownListener.bind( this ),
+ blur: this.unbindKeyDownListener.bind( this )
+ } );
+
// Initialization
this.$element.addClass( 'oo-ui-buttonSelectWidget' );
};
/* Setup */
OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.TabIndexedElement );
/**
* Select widget containing radio button options.
*
* @class
* @extends OO.ui.SelectWidget
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
// Parent constructor
OO.ui.RadioSelectWidget.super.call( this, config );
+ // Mixin constructors
+ OO.ui.TabIndexedElement.call( this, config );
+
+ // Events
+ this.$element.on( {
+ focus: this.bindKeyDownListener.bind( this ),
+ blur: this.unbindKeyDownListener.bind( this )
+ } );
+
// Initialization
this.$element.addClass( 'oo-ui-radioSelectWidget' );
};
/* Setup */
OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.TabIndexedElement );
/**
* Overlaid menu of options.
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
+ * @cfg {OO.ui.TextInputWidget} [input] Input to bind keyboard handlers to
* @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
* @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
*/
OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
// Properties
- this.flashing = false;
- this.visible = false;
this.newItems = null;
this.autoHide = config.autoHide === undefined || !!config.autoHide;
this.$input = config.input ? config.input.$input : null;
this.$widget = config.widget ? config.widget.$element : null;
- this.$previousFocus = null;
- this.isolated = !config.input;
- this.onKeyDownHandler = this.onKeyDown.bind( this );
this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
// Initialization
this.$element
- .hide()
- .attr( 'role', 'menu' )
- .addClass( 'oo-ui-menuSelectWidget' );
+ .addClass( 'oo-ui-menuSelectWidget' )
+ .attr( 'role', 'menu' );
+
+ // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
+ // that reference properties not initialized at that time of parent class construction
+ // TODO: Find a better way to handle post-constructor setup
+ this.visible = false;
+ this.$element.addClass( 'oo-ui-element-hidden' );
};
/* Setup */
*/
OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
if (
- !OO.ui.contains( this.$element[0], e.target, true ) &&
- ( !this.$widget || !OO.ui.contains( this.$widget[0], e.target, true ) )
+ !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
+ ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
) {
this.toggle( false );
}
};
/**
- * Handles key down events.
- *
- * @param {jQuery.Event} e Key down event
+ * @inheritdoc
*/
OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
- var nextItem,
- handled = false,
- highlightItem = this.getHighlightedItem();
+ var currentItem = this.getHighlightedItem() || this.getSelectedItem();
if ( !this.isDisabled() && this.isVisible() ) {
- if ( !highlightItem ) {
- highlightItem = this.getSelectedItem();
- }
switch ( e.keyCode ) {
- case OO.ui.Keys.ENTER:
- this.chooseItem( highlightItem );
- handled = true;
- break;
- case OO.ui.Keys.UP:
- nextItem = this.getRelativeSelectableItem( highlightItem, -1 );
- handled = true;
- break;
- case OO.ui.Keys.DOWN:
- nextItem = this.getRelativeSelectableItem( highlightItem, 1 );
- handled = true;
+ case OO.ui.Keys.LEFT:
+ case OO.ui.Keys.RIGHT:
+ // Do nothing if a text field is associated, arrow keys will be handled natively
+ if ( !this.$input ) {
+ OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
+ }
break;
case OO.ui.Keys.ESCAPE:
- if ( highlightItem ) {
- highlightItem.setHighlighted( false );
+ case OO.ui.Keys.TAB:
+ if ( currentItem ) {
+ currentItem.setHighlighted( false );
}
this.toggle( false );
- handled = true;
+ // Don't prevent tabbing away, prevent defocusing
+ if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
break;
- }
-
- if ( nextItem ) {
- this.highlightItem( nextItem );
- nextItem.scrollElementIntoView();
- }
-
- if ( handled ) {
- e.preventDefault();
- e.stopPropagation();
- return false;
+ default:
+ OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
+ return;
}
}
};
/**
- * Bind key down listener.
+ * @inheritdoc
*/
OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
if ( this.$input ) {
this.$input.on( 'keydown', this.onKeyDownHandler );
} else {
- // Capture menu navigation keys
- this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
+ OO.ui.MenuSelectWidget.super.prototype.bindKeyDownListener.call( this );
}
};
/**
- * Unbind key down listener.
+ * @inheritdoc
*/
OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
if ( this.$input ) {
- this.$input.off( 'keydown' );
+ this.$input.off( 'keydown', this.onKeyDownHandler );
} else {
- this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
+ OO.ui.MenuSelectWidget.super.prototype.unbindKeyDownListener.call( this );
}
};
/**
* Choose an item.
*
- * This will close the menu when done, unlike selectItem which only changes selection.
+ * This will close the menu, unlike #selectItem which only changes selection.
*
* @param {OO.ui.OptionWidget} item Item to choose
* @chainable
*/
OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
- var widget = this;
-
- // Parent method
OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
-
- if ( item && !this.flashing ) {
- this.flashing = true;
- item.flash().done( function () {
- widget.toggle( false );
- widget.flashing = false;
- } );
- } else {
- this.toggle( false );
- }
-
+ this.toggle( false );
return this;
};
}
for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
+ item = items[ i ];
if ( this.isVisible() ) {
// Defer fitting label until item has been attached
item.fitLabel();
visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
var i, len,
- change = visible !== this.isVisible(),
- elementDoc = this.getElementDocument(),
- widgetDoc = this.$widget ? this.$widget[0].ownerDocument : null;
+ change = visible !== this.isVisible();
// Parent method
OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
if ( visible ) {
this.bindKeyDownListener();
- // Change focus to enable keyboard navigation
- if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
- this.$previousFocus = this.$( ':focus' );
- this.$input[0].focus();
- }
if ( this.newItems && this.newItems.length ) {
for ( i = 0, len = this.newItems.length; i < len; i++ ) {
- this.newItems[i].fitLabel();
+ this.newItems[ i ].fitLabel();
}
this.newItems = null;
}
// Auto-hide
if ( this.autoHide ) {
- elementDoc.addEventListener(
+ this.getElementDocument().addEventListener(
'mousedown', this.onDocumentMouseDownHandler, true
);
- // Support $widget being in a different document
- if ( widgetDoc && widgetDoc !== elementDoc ) {
- widgetDoc.addEventListener(
- 'mousedown', this.onDocumentMouseDownHandler, true
- );
- }
}
} else {
this.unbindKeyDownListener();
- if ( this.isolated && this.$previousFocus ) {
- this.$previousFocus[0].focus();
- this.$previousFocus = null;
- }
- elementDoc.removeEventListener(
+ this.getElementDocument().removeEventListener(
'mousedown', this.onDocumentMouseDownHandler, true
);
- // Support $widget being in a different document
- if ( widgetDoc && widgetDoc !== elementDoc ) {
- widgetDoc.removeEventListener(
- 'mousedown', this.onDocumentMouseDownHandler, true
- );
- }
this.toggleClipping( false );
}
}
/**
* Menu for a text input widget.
*
- * This menu is specially designed to be positioned beneath the text input widget. Even if the input
- * is in a different frame, the menu's position is automatically calculated and maintained when the
- * menu is toggled or the window is resized.
+ * This menu is specially designed to be positioned beneath a text input widget. The menu's position
+ * is automatically calculated and maintained when the menu is toggled or the window is resized.
*
* @class
* @extends OO.ui.MenuSelectWidget
if ( change ) {
if ( this.isVisible() ) {
this.position();
- this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
+ $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
} else {
- this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
+ $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
}
}
*
* @class
* @extends OO.ui.SelectWidget
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
*/
OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
- // Configuration initialization
- config = config || {};
-
// Parent constructor
OO.ui.OutlineSelectWidget.super.call( this, config );
+ // Mixin constructors
+ OO.ui.TabIndexedElement.call( this, config );
+
+ // Events
+ this.$element.on( {
+ focus: this.bindKeyDownListener.bind( this ),
+ blur: this.unbindKeyDownListener.bind( this )
+ } );
+
// Initialization
this.$element.addClass( 'oo-ui-outlineSelectWidget' );
};
/* Setup */
OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.TabIndexedElement );
/**
* Switch that slides on and off.
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.ToggleWidget
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
// Mixin constructors
OO.ui.ToggleWidget.call( this, config );
+ OO.ui.TabIndexedElement.call( this, config );
// Properties
this.dragging = false;
this.dragStart = null;
this.sliding = false;
- this.$glow = this.$( '<span>' );
- this.$grip = this.$( '<span>' );
+ this.$glow = $( '<span>' );
+ this.$grip = $( '<span>' );
// Events
- this.$element.on( 'click', this.onClick.bind( this ) );
+ this.$element.on( {
+ click: this.onClick.bind( this ),
+ keypress: this.onKeyPress.bind( this )
+ } );
// Initialization
this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
this.$element
.addClass( 'oo-ui-toggleSwitchWidget' )
+ .attr( 'role', 'checkbox' )
.append( this.$glow, this.$grip );
};
OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
+OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.TabIndexedElement );
/* Methods */
/**
- * Handle mouse down events.
+ * Handle mouse click events.
*
- * @param {jQuery.Event} e Mouse down event
+ * @param {jQuery.Event} e Mouse click event
*/
OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
if ( !this.isDisabled() && e.which === 1 ) {
this.setValue( !this.value );
}
+ return false;
+};
+
+/**
+ * Handle key press events.
+ *
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
+ if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+ this.setValue( !this.value );
+ }
+ return false;
};
}( OO ) );