/*!
- * OOjs UI v0.1.0-pre (0d358b167a)
+ * OOjs UI v0.6.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2014 OOjs Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2014-10-17T23:41:06Z
+ * Date: 2014-12-16T21:00:55Z
*/
( function ( OO ) {
return undefined;
};
+/**
+ * Check if a node is contained within another node
+ *
+ * Similar to jQuery#contains except a list of containers can be supplied
+ * and a boolean argument allows you to include the container in the match list
+ *
+ * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
+ * @param {HTMLElement} contained Node to find
+ * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
+ * @return {boolean} The node is in the list of target nodes
+ */
+OO.ui.contains = function ( containers, contained, matchContainers ) {
+ var i;
+ if ( !Array.isArray( containers ) ) {
+ containers = [ containers ];
+ }
+ for ( i = containers.length - 1; i >= 0; i-- ) {
+ if ( ( matchContainers && contained === containers[i] ) || $.contains( containers[i], contained ) ) {
+ return true;
+ }
+ }
+ return false;
+};
+
( function () {
/**
* Message store for the default implementation of OO.ui.msg
'ooui-outline-control-remove': 'Remove item',
// Label for the toolbar group that contains a list of all other available tools
'ooui-toolbar-more': 'More',
+ // Label for the fake tool that expands the full list of tools in a toolbar group
+ 'ooui-toolgroup-expand': 'More',
+ // Label for the fake tool that collapses the full list of tools in a toolbar group
+ 'ooui-toolgroup-collapse': 'Fewer',
// Default label for the accept button of a confirmation dialog
'ooui-dialog-message-accept': 'OK',
// Default label for the reject button of a confirmation dialog
'ooui-dialog-process-error': 'Something went wrong',
// Label for process dialog dismiss error button, visible when describing errors
'ooui-dialog-process-dismiss': 'Dismiss',
- // Label for process dialog retry action button, visible when describing recoverable errors
- 'ooui-dialog-process-retry': 'Try again'
+ // Label for process dialog retry action button, visible when describing only recoverable errors
+ 'ooui-dialog-process-retry': 'Try again',
+ // Label for process dialog retry action button, visible when describing only warnings
+ 'ooui-dialog-process-continue': 'Continue'
};
/**
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
*/
OO.ui.PendingElement = function OoUiPendingElement( config ) {
- // Config initialisation
+ // Configuration initialization
config = config || {};
// Properties
* @param {Object} [config] Configuration options
*/
OO.ui.ActionSet = function OoUiActionSet( config ) {
- // Configuration intialization
+ // Configuration initialization
config = config || {};
// Mixin constructors
/**
* Organize actions.
*
- * This is called whenver organized information is requested. It will only reorganize the actions
+ * This is called whenever organized information is requested. It will only reorganize the actions
* if something has changed since the last time it ran.
*
* @private
for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
action = this.list[i];
if ( action.isVisible() ) {
- // Populate catgeories
+ // Populate categories
for ( category in this.categories ) {
if ( !this.categorized[category] ) {
this.categorized[category] = {};
* @constructor
* @param {Object} [config] Configuration options
* @cfg {Function} [$] jQuery for the frame the widget is in
- * @cfg {string[]} [classes] CSS class names
+ * @cfg {string[]} [classes] CSS class names to add
* @cfg {string} [text] Text to insert
* @cfg {jQuery} [$content] Content elements to append (after text)
+ * @cfg {Mixed} [data] Element data
*/
OO.ui.Element = function OoUiElement( config ) {
// Configuration initialization
config = config || {};
// Properties
- this.$ = config.$ || OO.ui.Element.getJQuery( document );
+ this.$ = config.$ || OO.ui.Element.static.getJQuery( document );
+ this.data = config.data;
this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
this.elementGroup = null;
this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
* not in an iframe
* @return {Function} Bound jQuery function
*/
-OO.ui.Element.getJQuery = function ( context, $iframe ) {
+OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
function wrapper( selector ) {
return $( selector, wrapper.context );
}
* @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
* @return {HTMLDocument|null} Document object
*/
-OO.ui.Element.getDocument = function ( obj ) {
+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 ) ||
// Empty jQuery selections might have a context
* @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
* @return {Window} Window object
*/
-OO.ui.Element.getWindow = function ( obj ) {
+OO.ui.Element.static.getWindow = function ( obj ) {
var doc = this.getDocument( obj );
return doc.parentWindow || doc.defaultView;
};
*
* @static
* @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
- * @return {string} Text direction, either `ltr` or `rtl`
+ * @return {string} Text direction, either 'ltr' or 'rtl'
*/
-OO.ui.Element.getDir = function ( obj ) {
+OO.ui.Element.static.getDir = function ( obj ) {
var isDoc, isWin;
if ( obj instanceof jQuery ) {
* @param {Object} [offset] Offset to start with, used internally
* @return {Object} Offset object, containing left and top properties
*/
-OO.ui.Element.getFrameOffset = function ( from, to, offset ) {
+OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
var i, len, frames, frame, rect;
if ( !to ) {
* @param {jQuery} $anchor Element to get $element's position relative to
* @return {Object} Translated position coordinates, containing top and left properties
*/
-OO.ui.Element.getRelativePosition = function ( $element, $anchor ) {
+OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
var iframe, iframePos,
pos = $element.offset(),
anchorPos = $anchor.offset(),
* @param {HTMLElement} el Element to measure
* @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
*/
-OO.ui.Element.getBorders = function ( el ) {
+OO.ui.Element.static.getBorders = function ( el ) {
var doc = el.ownerDocument,
win = doc.parentWindow || doc.defaultView,
style = win && win.getComputedStyle ?
* @param {HTMLElement|Window} el Element to measure
* @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
*/
-OO.ui.Element.getDimensions = function ( el ) {
+OO.ui.Element.static.getDimensions = function ( el ) {
var $el, $win,
doc = el.ownerDocument || el.document,
win = doc.parentWindow || doc.defaultView;
}
};
+/**
+ * Get scrollable object parent
+ *
+ * documentElement can't be used to get or set the scrollTop
+ * property on Blink. Changing and testing its value lets us
+ * use 'body' or 'documentElement' based on what is working.
+ *
+ * https://code.google.com/p/chromium/issues/detail?id=303131
+ *
+ * @static
+ * @param {HTMLElement} el Element to find scrollable parent for
+ * @return {HTMLElement} Scrollable parent
+ */
+OO.ui.Element.static.getRootScrollableElement = function ( el ) {
+ var scrollTop, body;
+
+ if ( OO.ui.scrollableElement === undefined ) {
+ body = el.ownerDocument.body;
+ scrollTop = body.scrollTop;
+ body.scrollTop = 1;
+
+ if ( body.scrollTop === 1 ) {
+ body.scrollTop = scrollTop;
+ OO.ui.scrollableElement = 'body';
+ } else {
+ OO.ui.scrollableElement = 'documentElement';
+ }
+ }
+
+ return el.ownerDocument[ OO.ui.scrollableElement ];
+};
+
/**
* Get closest scrollable container.
*
* @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
* @return {HTMLElement} Closest scrollable container
*/
-OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) {
+OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
var i, val,
props = [ 'overflow' ],
$parent = $( el ).parent();
}
while ( $parent.length ) {
- if ( $parent[0] === el.ownerDocument.body ) {
+ if ( $parent[0] === this.getRootScrollableElement( el ) ) {
return $parent[0];
}
i = props.length;
*
* @static
* @param {HTMLElement} el Element to scroll into view
- * @param {Object} [config={}] Configuration config
+ * @param {Object} [config] Configuration options
* @param {string} [config.duration] jQuery animation duration value
* @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
* to scroll in both directions
* @param {Function} [config.complete] Function to call when scrolling completes
*/
-OO.ui.Element.scrollIntoView = function ( el, config ) {
+OO.ui.Element.static.scrollIntoView = function ( el, config ) {
// Configuration initialization
config = config || {};
$win = $( this.getWindow( el ) );
// Compute the distances between the edges of el and the edges of the scroll viewport
- if ( $sc.is( 'body' ) ) {
- // If the scrollable container is the <body> this is easy
+ if ( $sc.is( 'html, body' ) ) {
+ // If the scrollable container is the root, this is easy
rel = {
top: eld.rect.top,
bottom: $win.innerHeight() - eld.rect.bottom,
}
};
+/* Methods */
+
/**
- * Bind a handler for an event on a DOM element.
+ * Get element data.
*
- * Used to be for working around a jQuery bug (jqbug.com/14180),
- * but obsolete as of jQuery 1.11.0.
- *
- * @static
- * @deprecated Use jQuery#on instead.
- * @param {HTMLElement|jQuery} el DOM element
- * @param {string} event Event to bind
- * @param {Function} callback Callback to call when the event fires
+ * @return {Mixed} Element data
*/
-OO.ui.Element.onDOMEvent = function ( el, event, callback ) {
- $( el ).on( event, callback );
+OO.ui.Element.prototype.getData = function () {
+ return this.data;
};
/**
- * Unbind a handler bound with #static-method-onDOMEvent.
+ * Set element data.
*
- * @deprecated Use jQuery#off instead.
- * @static
- * @param {HTMLElement|jQuery} el DOM element
- * @param {string} event Event to unbind
- * @param {Function} [callback] Callback to unbind
+ * @param {Mixed} Element data
+ * @chainable
*/
-OO.ui.Element.offDOMEvent = function ( el, event, callback ) {
- $( el ).off( event, callback );
+OO.ui.Element.prototype.setData = function ( data ) {
+ this.data = data;
+ return this;
};
-/* Methods */
-
/**
* Check if element supports one or more methods.
*
* @param {string|string[]} methods Method or list of methods to check
- * @return boolean All methods are supported
+ * @return {boolean} All methods are supported
*/
OO.ui.Element.prototype.supports = function ( methods ) {
var i, len,
/**
* Update the theme-provided classes.
*
- * @localdoc This is called in element mixins and widget classes anytime state changes.
+ * @localdoc This is called in element mixins and widget classes any time state changes.
* Updating is debounced, minimizing overhead of changing multiple attributes and
* guaranteeing that theme updates do not occur within an element's constructor
*/
* @return {HTMLDocument} Document object
*/
OO.ui.Element.prototype.getElementDocument = function () {
- return OO.ui.Element.getDocument( this.$element );
+ // 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 );
};
/**
* @return {Window} Window object
*/
OO.ui.Element.prototype.getElementWindow = function () {
- return OO.ui.Element.getWindow( this.$element );
+ return OO.ui.Element.static.getWindow( this.$element );
};
/**
* Get closest scrollable container.
*/
OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
- return OO.ui.Element.getClosestScrollableContainer( this.$element[0] );
+ return OO.ui.Element.static.getClosestScrollableContainer( this.$element[0] );
};
/**
/**
* Scroll element into view.
*
- * @param {Object} [config={}]
+ * @param {Object} [config] Configuration options
*/
OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
- return OO.ui.Element.scrollIntoView( this.$element[0], config );
-};
-
-/**
- * Bind a handler for an event on this.$element
- *
- * @deprecated Use jQuery#on instead.
- * @param {string} event
- * @param {Function} callback
- */
-OO.ui.Element.prototype.onDOMEvent = function ( event, callback ) {
- OO.ui.Element.onDOMEvent( this.$element, event, callback );
-};
-
-/**
- * Unbind a handler bound with #offDOMEvent
- *
- * @deprecated Use jQuery#off instead.
- * @param {string} event
- * @param {Function} callback
- */
-OO.ui.Element.prototype.offDOMEvent = function ( event, callback ) {
- OO.ui.Element.offDOMEvent( this.$element, event, callback );
+ return OO.ui.Element.static.scrollIntoView( this.$element[0], config );
};
/**
* @param {Object} [config] Configuration options
*/
OO.ui.Layout = function OoUiLayout( config ) {
- // Initialize config
+ // Configuration initialization
config = config || {};
// Parent constructor
/**
* Check if the widget is disabled.
*
- * @param {boolean} Button is disabled
+ * @return {boolean} Button is disabled
*/
OO.ui.Widget.prototype.isDisabled = function () {
return this.disabled;
*
* 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 asynchonous
- * processing can complete. Always assume window processes are executed asychronously. See
+ * {@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.
*
* @param {Object} [config] Configuration options
* @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to
* use #static-size
- * @fires initialize
*/
OO.ui.Window = function OoUiWindow( config ) {
// Configuration initialization
this.$frame.addClass( 'oo-ui-window-frame' );
this.$overlay.addClass( 'oo-ui-window-overlay' );
- // NOTE: Additional intitialization will occur when #setManager is called
+ // NOTE: Additional initialization will occur when #setManager is called
};
/* Setup */
return this.size;
};
+/**
+ * Disable transitions on window's frame for the duration of the callback function, then enable them
+ * back.
+ *
+ * @private
+ * @param {Function} callback Function to call while transitions are disabled
+ */
+OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
+ // 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;
+ oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
+ styleObj.MozTransition || styleObj.WebkitTransition;
+ styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
+ styleObj.MozTransition = styleObj.WebkitTransition = 'none';
+ callback();
+ // Force reflow to make sure the style changes done inside callback really are not transitioned
+ this.$frame.height();
+ styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
+ styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
+};
+
/**
* Get the height of the dialog contents.
*
* @return {number} Content height
*/
OO.ui.Window.prototype.getContentHeight = function () {
- // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements
- var bodyHeight, oldHeight = this.$frame[0].style.height;
- this.$frame[0].style.height = '1px';
- bodyHeight = this.getBodyHeight();
- this.$frame[0].style.height = oldHeight;
+ var bodyHeight,
+ win = this,
+ 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.
+ this.withoutSizeTransitions( function () {
+ var oldHeight = frameStyleObj.height, oldPosition = bodyStyleObj.position;
+ frameStyleObj.height = '1px';
+ // Force body to resize to new width
+ bodyStyleObj.position = 'relative';
+ bodyHeight = win.getBodyHeight();
+ frameStyleObj.height = oldHeight;
+ bodyStyleObj.position = oldPosition;
+ } );
return Math.round(
// Add buffer for border
/**
* Toggle visibility of window.
*
- * If the window is isolated and hasn't fully loaded yet, the visiblity property will be used
+ * 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 visible
+ * @fires toggle
* @chainable
*/
OO.ui.Window.prototype.toggle = function ( show ) {
} else {
this.$content = this.$( '<div>' );
this.$document = $( this.getElementDocument() );
- this.$content.addClass( 'oo-ui-window-content' );
+ 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.getDir( this.$iframe || this.$content ) || 'ltr';
+ this.dir = OO.ui.Element.static.getDir( this.$iframe || this.$content ) || 'ltr';
return this;
};
* @chainable
*/
OO.ui.Window.prototype.setDimensions = function ( dim ) {
- // Apply width before height so height is not based on wrapping content using the wrong width
+ var height,
+ win = this,
+ styleObj = this.$frame[0].style;
+
+ // Calculate the height we need to set using the correct width
+ if ( dim.height === undefined ) {
+ this.withoutSizeTransitions( function () {
+ var oldWidth = styleObj.width;
+ win.$frame.css( 'width', dim.width || '' );
+ height = win.getContentHeight();
+ styleObj.width = oldWidth;
+ } );
+ } else {
+ height = dim.height;
+ }
+
this.$frame.css( {
width: dim.width || '',
minWidth: dim.minWidth || '',
- maxWidth: dim.maxWidth || ''
- } );
- this.$frame.css( {
- height: ( dim.height !== undefined ? dim.height : this.getContentHeight() ) || '',
+ maxWidth: dim.maxWidth || '',
+ height: height || '',
minHeight: dim.minHeight || '',
maxHeight: dim.maxHeight || ''
} );
+
return this;
};
/**
* Setup window.
*
- * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
+ * This is called by OO.ui.WindowManager during window opening, and should not be called directly
* by other systems.
*
* @param {Object} [data] Window opening data
/**
* Ready window.
*
- * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
+ * This is called by OO.ui.WindowManager during window opening, and should not be called directly
* by other systems.
*
* @param {Object} [data] Window opening data
/**
* Hold window.
*
- * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
+ * This is called by OO.ui.WindowManager during window closing, and should not be called directly
* by other systems.
*
* @param {Object} [data] Window closing data
this.getHoldProcess( data ).execute().done( function () {
// Get the focused element within the window's content
- var $focus = win.$content.find( OO.ui.Element.getDocument( win.$content ).activeElement );
+ var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
// Blur the focused element
if ( $focus.length ) {
/**
* Teardown window.
*
- * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
+ * This is called by OO.ui.WindowManager during window closing, and should not be called directly
* by other systems.
*
* @param {Object} [data] Window closing data
this.getTeardownProcess( data ).execute().done( function () {
// Force redraw by asking the browser to measure the elements' widths
- win.$element.removeClass( 'oo-ui-window-setup' ).width();
+ 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;
/**
* Load the frame contents.
*
- * Once the iframe's stylesheets are loaded, the `load` event will be emitted and 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.
+ * 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...
*
* All this stylesheet injection and polling magic is in #transplantStyles.
*
* @return {jQuery.Promise} Promise resolved when loading is complete
- * @fires load
*/
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();
doc.close();
// Properties
- this.$ = OO.ui.Element.getJQuery( doc, this.$iframe );
+ this.$ = OO.ui.Element.static.getJQuery( doc, this.$iframe );
this.$content = this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 );
this.$document = this.$( doc );
this.actions = new OO.ui.ActionSet();
this.attachedActions = [];
this.currentAction = null;
+ this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
// Events
this.actions.connect( this, {
OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
if ( e.which === OO.ui.Keys.ESCAPE ) {
this.close();
- return false;
+ e.preventDefault();
+ e.stopPropagation();
}
};
);
}
this.actions.add( items );
+
+ if ( this.constructor.static.escapable ) {
+ this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
+ }
}, this );
};
// Parent method
return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
.first( function () {
+ if ( this.constructor.static.escapable ) {
+ this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
+ }
+
this.actions.clear();
this.currentAction = null;
}, this );
// Properties
this.title = new OO.ui.LabelWidget( { $: this.$ } );
- // Events
- if ( this.constructor.static.escapable ) {
- this.$document.on( 'keydown', this.onDocumentKeyDown.bind( this ) );
- }
-
// Initialization
this.$content.addClass( 'oo-ui-dialog-content' );
this.setPendingElement( this.$head );
*
* @param {jQuery.Event} e Mouse wheel event
*/
-OO.ui.WindowManager.prototype.onWindowMouseWheel = function ( e ) {
- // Kill all events in the parent window if the child window is isolated,
- // or if the event didn't come from the child window
- return !( this.shouldIsolate() || !$.contains( this.getCurrentWindow().$frame[0], e.target ) );
+OO.ui.WindowManager.prototype.onWindowMouseWheel = function () {
+ // Kill all events in the parent window if the child window is isolated
+ return !this.shouldIsolate();
};
/**
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,
- // or if the event didn't come from the child window
- return !( this.shouldIsolate() || !$.contains( this.getCurrentWindow().$frame[0], e.target ) );
+ // Kill all events in the parent window if the child window is isolated
+ return !this.shouldIsolate();
}
};
// Window opening
if ( opening.state() !== 'rejected' ) {
- // Begin loading the window if it's not loading or loaded already - may take noticable time
- // and we want to do this in paralell with any other preparatory actions
- if ( !win.isLoading() && !win.isLoaded() ) {
- // Finish initializing the window (must be done after manager is attached to DOM)
+ if ( !win.getManager() ) {
win.setManager( this );
- preparing.push( win.load() );
}
+ preparing.push( win.load() );
if ( this.closing ) {
// If a window is currently closing, wait for it to complete
* @throws {Error} If windows being removed are not being managed
*/
OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
- var i, len, win, name,
+ var i, len, win, name, cleanupWindow,
manager = this,
promises = [],
cleanup = function ( name, win ) {
if ( !win ) {
throw new Error( 'Cannot remove window' );
}
- promises.push( this.closeWindow( name ).then( cleanup.bind( null, name, win ) ) );
+ cleanupWindow = cleanup.bind( null, name, win );
+ promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
}
return $.when.apply( $, promises );
return;
}
- var viewport = OO.ui.Element.getDimensions( win.getElementWindow() ),
+ var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
sizes = this.constructor.static.sizes,
size = win.getSize();
// 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.globalEvents = true;
}
} else if ( this.globalEvents ) {
// Stop listening for top-level window dimension changes
'orientationchange resize': this.onWindowResizeHandler
} );
+ if ( !this.shouldIsolate() ) {
+ $( this.getElementDocument().body ).css( 'overflow', '' );
+ }
this.globalEvents = false;
}
.attr( 'aria-hidden', '' );
}
} else if ( this.$ariaHidden ) {
- // Restore screen reader visiblity
+ // Restore screen reader visibility
this.$ariaHidden.removeAttr( 'aria-hidden' );
this.$ariaHidden = null;
}
* @param {string|jQuery} message Description of error
* @param {Object} [config] Configuration options
* @cfg {boolean} [recoverable=true] Error is recoverable
+ * @cfg {boolean} [warning=false] Whether this error is a warning or not.
*/
OO.ui.Error = function OoUiElement( message, config ) {
// Configuration initialization
// Properties
this.message = message instanceof jQuery ? message : String( message );
this.recoverable = config.recoverable === undefined || !!config.recoverable;
+ this.warning = !!config.warning;
};
/* Setup */
return this.recoverable;
};
+/**
+ * Check if the error is a warning
+ *
+ * @return {boolean} Error is warning
+ */
+OO.ui.Error.prototype.isWarning = function () {
+ return this.warning;
+};
+
/**
* Get error message as DOM nodes.
*
/* Methods */
-/** */
+/**
+ * Get tools from the factory
+ *
+ * @param {Array} include Included tools
+ * @param {Array} exclude Excluded tools
+ * @param {Array} promote Promoted tools
+ * @param {Array} demote Demoted tools
+ * @return {string[]} List of tools
+ */
OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
var i, len, included, promoted, demoted,
auto = [],
* @param {Object} [config] Configuration options
*/
OO.ui.Theme = function OoUiTheme( config ) {
- // Initialize config
+ // Configuration initialization
config = config || {};
};
/**
* Get a list of classes to be applied to a widget.
*
- * @localdoc The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or
- * removes, otherwise state transitions will not work properly.
+ * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
+ * otherwise state transitions will not work properly.
*
* @param {OO.ui.Element} element Element for which to get classes
* @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
/**
* Update CSS classes provided by the theme.
*
- * For elements with theme logic hooks, this should be called anytime there's a state change.
+ * For elements with theme logic hooks, this should be called any time there's a state change.
*
- * @param {OO.ui.Element} Element for which to update classes
+ * @param {OO.ui.Element} element Element for which to update classes
* @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
*/
OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
* @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 null to have no tabIndex
+ * @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
*/
OO.ui.ButtonElement = function OoUiButtonElement( config ) {
this.$button
.removeClass( 'oo-ui-buttonElement-button' )
.removeAttr( 'role accesskey tabindex' )
- .off( this.onMouseDownHandler );
+ .off( 'mousedown', this.onMouseDownHandler );
}
this.$button = $button
if ( this.isDisabled() || e.which !== 1 ) {
return false;
}
- // Restore the tab-index after the button is up to restore the button's accesssibility
+ // Restore the tab-index after the button is up to restore the button's accessibility
this.$button.attr( 'tabindex', this.tabIndex );
this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
// Stop listening for mouseup, since we only needed this once
* @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
*/
OO.ui.GroupElement = function OoUiGroupElement( config ) {
- // Configuration
+ // Configuration initialization
config = config || {};
// Properties
return this.items.slice( 0 );
};
+/**
+ * Get an item by its data.
+ *
+ * Data is compared by a hash of its value. Only the first item with matching data will be returned.
+ *
+ * @param {Object} data Item data to search for
+ * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
+ */
+OO.ui.GroupElement.prototype.getItemFromData = function ( data ) {
+ var i, len, item,
+ hash = OO.getHash( data );
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ item = this.items[i];
+ if ( hash === OO.getHash( item.getData() ) ) {
+ return item;
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Get items by their data.
+ *
+ * Data is compared by a hash of its value. All items with matching data will be returned.
+ *
+ * @param {Object} data Item data to search for
+ * @return {OO.ui.Element[]} Items with equivalent data
+ */
+OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) {
+ var i, len, item,
+ hash = OO.getHash( data ),
+ items = [];
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ item = this.items[i];
+ if ( hash === OO.getHash( item.getData() ) ) {
+ items.push( item );
+ }
+ }
+
+ return items;
+};
+
/**
* Add an aggregate item event.
*
groupEvent = events[itemEvent];
// Remove existing aggregated event
- if ( itemEvent in this.aggregateItemEvents ) {
+ if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
// Don't allow duplicate aggregations
if ( groupEvent ) {
throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
/**
* Add items.
*
- * Adding an existing item (by value) will move it.
+ * Adding an existing item will move it.
*
* @param {OO.ui.Element[]} items Items
* @param {number} [index] Index to insert items at
!$.isEmptyObject( this.aggregateItemEvents )
) {
remove = {};
- if ( itemEvent in this.aggregateItemEvents ) {
+ if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
}
item.disconnect( this, remove );
!$.isEmptyObject( this.aggregateItemEvents )
) {
remove = {};
- if ( itemEvent in this.aggregateItemEvents ) {
+ if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
}
item.disconnect( this, remove );
};
/**
- * Element containing an icon.
- *
- * 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.
+ * A mixin for an element that can be dragged and dropped.
+ * Use in conjunction with DragGroupWidget
*
* @abstract
* @class
*
* @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$icon] Icon node, assigned to #$icon, omit to use a generated `<span>`
- * @cfg {Object|string} [icon=''] 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
- * @cfg {string} [iconTitle] Icon title text or a function that returns text
*/
-OO.ui.IconElement = function OoUiIconElement( config ) {
- // Config intialization
- config = config || {};
-
+OO.ui.DraggableElement = function OoUiDraggableElement() {
// Properties
- this.$icon = null;
- this.icon = null;
- this.iconTitle = null;
+ this.index = null;
- // Initialization
- this.setIcon( config.icon || this.constructor.static.icon );
- this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
- this.setIconElement( config.$icon || this.$( '<span>' ) );
+ // Initialize and events
+ this.$element
+ .attr( 'draggable', true )
+ .addClass( 'oo-ui-draggableElement' )
+ .on( {
+ dragstart: this.onDragStart.bind( this ),
+ dragover: this.onDragOver.bind( this ),
+ dragend: this.onDragEnd.bind( this ),
+ drop: this.onDrop.bind( this )
+ } );
};
-/* Setup */
-
-OO.initClass( OO.ui.IconElement );
+/* Events */
-/* Static Properties */
+/**
+ * @event dragstart
+ * @param {OO.ui.DraggableElement} item Dragging item
+ */
/**
- * Icon.
- *
- * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
- *
- * 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' }
- *
- * @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
+ * @event dragend
*/
-OO.ui.IconElement.static.icon = null;
/**
- * Icon title.
- *
- * @static
- * @inheritable
- * @property {string|Function|null} Icon title text, a function that returns text or null for no
- * icon title
+ * @event drop
*/
-OO.ui.IconElement.static.iconTitle = null;
/* Methods */
/**
- * Set the icon element.
- *
- * If an element is already set, it will be cleaned up before setting up the new element.
- *
- * @param {jQuery} $icon Element to use as icon
+ * Respond to dragstart event.
+ * @param {jQuery.Event} event jQuery event
+ * @fires dragstart
*/
-OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
- if ( this.$icon ) {
- this.$icon
- .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
- .removeAttr( 'title' );
- }
-
- this.$icon = $icon
- .addClass( 'oo-ui-iconElement-icon' )
- .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
- if ( this.iconTitle !== null ) {
- this.$icon.attr( 'title', this.iconTitle );
+OO.ui.DraggableElement.prototype.onDragStart = function ( e ) {
+ var dataTransfer = e.originalEvent.dataTransfer;
+ // Define drop effect
+ dataTransfer.dropEffect = 'none';
+ dataTransfer.effectAllowed = 'move';
+ // We must set up a dataTransfer data property or Firefox seems to
+ // ignore the fact the element is draggable.
+ try {
+ dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
+ } catch ( err ) {
+ // The above is only for firefox. No need to set a catch clause
+ // if it fails, move on.
}
+ // Add dragging class
+ this.$element.addClass( 'oo-ui-draggableElement-dragging' );
+ // Emit event
+ this.emit( 'dragstart', this );
+ return true;
};
/**
- * Set icon.
- *
- * @param {Object|string|null} icon 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, use null to remove icon
- * @chainable
+ * Respond to dragend event.
+ * @fires dragend
*/
-OO.ui.IconElement.prototype.setIcon = function ( icon ) {
- icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
- icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
-
- if ( this.icon !== icon ) {
- if ( this.$icon ) {
- if ( this.icon !== null ) {
- this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
- }
- if ( icon !== null ) {
- this.$icon.addClass( 'oo-ui-icon-' + icon );
- }
- }
- this.icon = icon;
- }
-
- this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
- this.updateThemeClasses();
+OO.ui.DraggableElement.prototype.onDragEnd = function () {
+ this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
+ this.emit( 'dragend' );
+};
- return this;
+/**
+ * Handle drop event.
+ * @param {jQuery.Event} event jQuery event
+ * @fires drop
+ */
+OO.ui.DraggableElement.prototype.onDrop = function ( e ) {
+ e.preventDefault();
+ this.emit( 'drop', e );
};
/**
- * Set icon title.
- *
- * @param {string|Function|null} icon Icon title text, a function that returns text or null
- * for no icon title
- * @chainable
+ * In order for drag/drop to work, the dragover event must
+ * return false and stop propogation.
*/
-OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
- iconTitle = typeof iconTitle === 'function' ||
- ( typeof iconTitle === 'string' && iconTitle.length ) ?
- OO.ui.resolveMsg( iconTitle ) : null;
+OO.ui.DraggableElement.prototype.onDragOver = function ( e ) {
+ e.preventDefault();
+};
- if ( this.iconTitle !== iconTitle ) {
- this.iconTitle = iconTitle;
- if ( this.$icon ) {
- if ( this.iconTitle !== null ) {
- this.$icon.attr( 'title', iconTitle );
- } else {
- this.$icon.removeAttr( 'title' );
- }
- }
+/**
+ * Set item index.
+ * Store it in the DOM so we can access from the widget drag event
+ * @param {number} Item index
+ */
+OO.ui.DraggableElement.prototype.setIndex = function ( index ) {
+ if ( this.index !== index ) {
+ this.index = index;
+ this.$element.data( 'index', index );
}
-
- return this;
};
/**
- * Get icon.
- *
- * @return {string} Icon
+ * Get item index
+ * @return {number} Item index
*/
-OO.ui.IconElement.prototype.getIcon = function () {
- return this.icon;
+OO.ui.DraggableElement.prototype.getIndex = function () {
+ return this.index;
};
/**
- * Element containing an indicator.
- *
- * 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.
+ * Element containing a sequence of child elements that can be dragged
+ * and dropped.
*
* @abstract
* @class
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {jQuery} [$indicator] Indicator node, assigned to #$indicator, omit to use a generated
- * `<span>`
- * @cfg {string} [indicator] Symbolic indicator name
+ * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
+ * @cfg {string} [orientation] Item orientation, 'horizontal' or 'vertical'. Defaults to 'vertical'
+ */
+OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.GroupElement.call( this, config );
+
+ // Properties
+ this.orientation = config.orientation || 'vertical';
+ this.dragItem = null;
+ this.itemDragOver = null;
+ this.itemKeys = {};
+ this.sideInsertion = '';
+
+ // Events
+ this.aggregate( {
+ dragstart: 'itemDragStart',
+ dragend: 'itemDragEnd',
+ drop: 'itemDrop'
+ } );
+ this.connect( this, {
+ itemDragStart: 'onItemDragStart',
+ itemDrop: 'onItemDrop',
+ itemDragEnd: 'onItemDragEnd'
+ } );
+ this.$element.on( {
+ dragover: $.proxy( this.onDragOver, this ),
+ dragleave: $.proxy( this.onDragLeave, this )
+ } );
+
+ // Initialize
+ if ( $.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+ this.$placeholder = $( '<div>' )
+ .addClass( 'oo-ui-draggableGroupElement-placeholder' );
+ this.$element
+ .addClass( 'oo-ui-draggableGroupElement' )
+ .append( this.$status )
+ .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
+ .prepend( this.$placeholder );
+};
+
+/* Setup */
+OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
+
+/* Events */
+
+/**
+ * @event reorder
+ * @param {OO.ui.DraggableElement} item Reordered item
+ * @param {number} [newIndex] New index for the item
+ */
+
+/* Methods */
+
+/**
+ * Respond to item drag start event
+ * @param {OO.ui.DraggableElement} item Dragged item
+ */
+OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
+ var i, len;
+
+ // Map the index of each object
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ this.items[i].setIndex( i );
+ }
+
+ if ( this.orientation === 'horizontal' ) {
+ // Set the height of the indicator
+ this.$placeholder.css( {
+ height: item.$element.outerHeight(),
+ width: 2
+ } );
+ } else {
+ // Set the width of the indicator
+ this.$placeholder.css( {
+ height: 2,
+ width: item.$element.outerWidth()
+ } );
+ }
+ this.setDragItem( item );
+};
+
+/**
+ * Respond to item drag end event
+ */
+OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
+ this.unsetDragItem();
+ return false;
+};
+
+/**
+ * Handle drop event and switch the order of the items accordingly
+ * @param {OO.ui.DraggableElement} item Dropped item
+ * @fires reorder
+ */
+OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
+ var toIndex = item.getIndex();
+ // Check if the dropped item is from the current group
+ // TODO: Figure out a way to configure a list of legally droppable
+ // elements even if they are not yet in the list
+ if ( this.getDragItem() ) {
+ // If the insertion point is 'after', the insertion index
+ // is shifted to the right (or to the left in RTL, hence 'after')
+ if ( this.sideInsertion === 'after' ) {
+ toIndex++;
+ }
+ // Emit change event
+ this.emit( 'reorder', this.getDragItem(), toIndex );
+ }
+ // Return false to prevent propogation
+ return false;
+};
+
+/**
+ * Handle dragleave event.
+ */
+OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
+ // This means the item was dragged outside the widget
+ this.$placeholder
+ .css( 'left', 0 )
+ .hide();
+};
+
+/**
+ * Respond to dragover event
+ * @param {jQuery.Event} event Event details
+ */
+OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
+ var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
+ itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
+ clientX = e.originalEvent.clientX,
+ clientY = e.originalEvent.clientY;
+
+ // Get the OptionWidget item we are dragging over
+ dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
+ $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
+ if ( $optionWidget[0] ) {
+ itemOffset = $optionWidget.offset();
+ itemBoundingRect = $optionWidget[0].getBoundingClientRect();
+ itemPosition = $optionWidget.position();
+ itemIndex = $optionWidget.data( 'index' );
+ }
+
+ if (
+ itemOffset &&
+ this.isDragging() &&
+ itemIndex !== this.getDragItem().getIndex()
+ ) {
+ if ( this.orientation === 'horizontal' ) {
+ // Calculate where the mouse is relative to the item width
+ itemSize = itemBoundingRect.width;
+ itemMidpoint = itemBoundingRect.left + itemSize / 2;
+ dragPosition = clientX;
+ // Which side of the item we hover over will dictate
+ // where the placeholder will appear, on the left or
+ // on the right
+ cssOutput = {
+ left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
+ top: itemPosition.top
+ };
+ } else {
+ // Calculate where the mouse is relative to the item height
+ itemSize = itemBoundingRect.height;
+ itemMidpoint = itemBoundingRect.top + itemSize / 2;
+ dragPosition = clientY;
+ // Which side of the item we hover over will dictate
+ // where the placeholder will appear, on the top or
+ // on the bottom
+ cssOutput = {
+ top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
+ left: itemPosition.left
+ };
+ }
+ // Store whether we are before or after an item to rearrange
+ // For horizontal layout, we need to account for RTL, as this is flipped
+ if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
+ this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
+ } else {
+ 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();
+ }
+ } else {
+ // This means the item was dragged outside the widget
+ this.$placeholder
+ .css( 'left', 0 )
+ .hide();
+ }
+ // Prevent default
+ e.preventDefault();
+};
+
+/**
+ * Set a dragged item
+ * @param {OO.ui.DraggableElement} item Dragged item
+ */
+OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
+ this.dragItem = item;
+};
+
+/**
+ * Unset the current dragged item
+ */
+OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
+ this.dragItem = null;
+ this.itemDragOver = null;
+ this.$placeholder.hide();
+ this.sideInsertion = '';
+};
+
+/**
+ * Get the current dragged item
+ * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
+ */
+OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
+ return this.dragItem;
+};
+
+/**
+ * Check if there's an item being dragged.
+ * @return {Boolean} Item is being dragged
+ */
+OO.ui.DraggableGroupElement.prototype.isDragging = function () {
+ return this.getDragItem() !== null;
+};
+
+/**
+ * Element containing an icon.
+ *
+ * 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.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$icon] Icon node, assigned to #$icon, omit to use a generated `<span>`
+ * @cfg {Object|string} [icon=''] 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
+ * @cfg {string} [iconTitle] Icon title text or a function that returns text
+ */
+OO.ui.IconElement = function OoUiIconElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$icon = null;
+ this.icon = null;
+ this.iconTitle = null;
+
+ // Initialization
+ this.setIcon( config.icon || this.constructor.static.icon );
+ this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
+ this.setIconElement( config.$icon || this.$( '<span>' ) );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.IconElement );
+
+/* Static Properties */
+
+/**
+ * Icon.
+ *
+ * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
+ *
+ * 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' }
+ *
+ * @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
+ */
+OO.ui.IconElement.static.icon = null;
+
+/**
+ * Icon title.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null} Icon title text, a function that returns text or null for no
+ * icon title
+ */
+OO.ui.IconElement.static.iconTitle = null;
+
+/* Methods */
+
+/**
+ * Set the icon element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $icon Element to use as icon
+ */
+OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
+ if ( this.$icon ) {
+ this.$icon
+ .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
+ .removeAttr( 'title' );
+ }
+
+ this.$icon = $icon
+ .addClass( 'oo-ui-iconElement-icon' )
+ .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
+ if ( this.iconTitle !== null ) {
+ this.$icon.attr( 'title', this.iconTitle );
+ }
+};
+
+/**
+ * Set icon name.
+ *
+ * @param {Object|string|null} icon 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, use null to remove icon
+ * @chainable
+ */
+OO.ui.IconElement.prototype.setIcon = function ( icon ) {
+ icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
+ icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
+
+ if ( this.icon !== icon ) {
+ if ( this.$icon ) {
+ if ( this.icon !== null ) {
+ this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
+ }
+ if ( icon !== null ) {
+ this.$icon.addClass( 'oo-ui-icon-' + icon );
+ }
+ }
+ this.icon = icon;
+ }
+
+ this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
+ this.updateThemeClasses();
+
+ return this;
+};
+
+/**
+ * Set icon title.
+ *
+ * @param {string|Function|null} icon Icon title text, a function that returns text or null
+ * for no icon title
+ * @chainable
+ */
+OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
+ iconTitle = typeof iconTitle === 'function' ||
+ ( typeof iconTitle === 'string' && iconTitle.length ) ?
+ OO.ui.resolveMsg( iconTitle ) : null;
+
+ if ( this.iconTitle !== iconTitle ) {
+ this.iconTitle = iconTitle;
+ if ( this.$icon ) {
+ if ( this.iconTitle !== null ) {
+ this.$icon.attr( 'title', iconTitle );
+ } else {
+ this.$icon.removeAttr( 'title' );
+ }
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Get icon name.
+ *
+ * @return {string} Icon name
+ */
+OO.ui.IconElement.prototype.getIcon = function () {
+ return this.icon;
+};
+
+/**
+ * Get icon title.
+ *
+ * @return {string} Icon title text
+ */
+OO.ui.IconElement.prototype.getIconTitle = function () {
+ return this.iconTitle;
+};
+
+/**
+ * Element containing an indicator.
+ *
+ * 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.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$indicator] Indicator node, assigned to #$indicator, omit to use a generated
+ * `<span>`
+ * @cfg {string} [indicator] Symbolic indicator name
* @cfg {string} [indicatorTitle] Indicator title text or a function that returns text
*/
OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
- // Config intialization
+ // Configuration initialization
config = config || {};
// Properties
*
* @static
* @inheritable
- * @property {string|null} Symbolic indicator name or null for no indicator
+ * @property {string|null} Symbolic indicator name
*/
OO.ui.IndicatorElement.static.indicator = null;
};
/**
- * Set indicator.
+ * Set indicator name.
*
* @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
* @chainable
};
/**
- * Get indicator.
+ * Get indicator name.
*
- * @return {string} title Symbolic name of indicator
+ * @return {string} Symbolic name of indicator
*/
OO.ui.IndicatorElement.prototype.getIndicator = function () {
return this.indicator;
* @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
*/
OO.ui.LabelElement = function OoUiLabelElement( config ) {
- // Config intialization
+ // Configuration initialization
config = config || {};
// Properties
* Set the label.
*
* An empty string will result in the label being hidden. A string containing only whitespace will
- * be converted to a single
+ * be converted to a single ` `.
*
* @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
* text; or null for no label
/**
* Get the label.
*
- * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
+ * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
* text; or null for no label
*/
OO.ui.LabelElement.prototype.getLabel = function () {
} else {
this.$label.empty();
}
- this.$label.css( 'display', !label ? 'none' : '' );
};
/**
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
+ * @cfg {string|string[]} [flags] Flags describing importance and functionality, e.g. 'primary',
+ * 'safe', 'progressive', 'destructive' or 'constructive'
* @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element
*/
OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
- // Config initialization
+ // Configuration initialization
config = config || {};
// Properties
/**
* Get the names of all flags set.
*
- * @return {string[]} flags Flag names
+ * @return {string[]} Flag names
*/
OO.ui.FlaggedElement.prototype.getFlags = function () {
return Object.keys( this.flags );
* @constructor
* @param {Object} [config] Configuration options
* @cfg {jQuery} [$titled] Titled node, assigned to #$titled, omit to use #$element
- * @cfg {string|Function} [title] Title text or a function that returns text
+ * @cfg {string|Function} [title] Title text or a function that returns text. If not provided, the
+ * static property 'title' is used.
*/
OO.ui.TitledElement = function OoUiTitledElement( config ) {
- // Config intialization
+ // Configuration initialization
config = config || {};
// Properties
this.clipping = clipping;
if ( clipping ) {
this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
- // If the clippable container is the body, we have to listen to scroll events and check
+ // 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( 'body' ) ?
- this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) :
+ this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
+ this.$( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
this.$clippableContainer;
this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
this.$clippableWindow = this.$( this.getElementWindow() )
return this;
}
- var buffer = 10,
+ var buffer = 7, // Chosen by fair dice roll
cOffset = this.$clippable.offset(),
- $container = this.$clippableContainer.is( 'body' ) ?
+ $container = this.$clippableContainer.is( 'html, body' ) ?
this.$clippableWindow : this.$clippableContainer,
ccOffset = $container.offset() || { top: 0, left: 0 },
ccHeight = $container.innerHeight() - buffer,
ccWidth = $container.innerWidth() - buffer,
+ cHeight = this.$clippable.outerHeight() + buffer,
+ cWidth = this.$clippable.outerWidth() + buffer,
scrollTop = this.$clippableScroller.scrollTop(),
scrollLeft = this.$clippableScroller.scrollLeft(),
- desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
- desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
+ desiredWidth = cOffset.left < 0 ?
+ cWidth + cOffset.left :
+ ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
+ desiredHeight = cOffset.top < 0 ?
+ cHeight + cOffset.top :
+ ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
naturalWidth = this.$clippable.prop( 'scrollWidth' ),
naturalHeight = this.$clippable.prop( 'scrollHeight' ),
clipWidth = desiredWidth < naturalWidth,
* @cfg {string|Function} [title] Title text or a function that returns text
*/
OO.ui.Tool = function OoUiTool( toolGroup, config ) {
- // Config intialization
+ // Configuration initialization
config = config || {};
// Parent constructor
this.toolbar = this.toolGroup.getToolbar();
this.active = false;
this.$title = this.$( '<span>' );
+ this.$accel = this.$( '<span>' );
this.$link = this.$( '<a>' );
this.title = null;
// Initialization
this.$title.addClass( 'oo-ui-tool-title' );
+ this.$accel
+ .addClass( 'oo-ui-tool-accel' )
+ .prop( {
+ // This may need to be changed if the key names are ever localized,
+ // but for now they are essentially written in English
+ dir: 'ltr',
+ lang: 'en'
+ } );
this.$link
.addClass( 'oo-ui-tool-link' )
- .append( this.$icon, this.$title )
+ .append( this.$icon, this.$title, this.$accel )
.prop( 'tabIndex', 0 )
.attr( 'role', 'button' );
this.$element
/**
* Check if the button is active.
*
- * @param {boolean} Button is active
+ * @return {boolean} Button is active
*/
OO.ui.Tool.prototype.isActive = function () {
return this.active;
accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
tooltipParts = [];
- this.$title.empty()
- .text( this.title )
- .append(
- this.$( '<span>' )
- .addClass( 'oo-ui-tool-accel' )
- .text( accel )
- );
+ this.$title.text( this.title );
+ this.$accel.text( accel );
if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
tooltipParts.push( this.title );
// Initialization
this.$group.addClass( 'oo-ui-toolbar-tools' );
- this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group );
if ( config.actions ) {
- this.$actions.addClass( 'oo-ui-toolbar-actions' );
- this.$bar.append( this.$actions );
+ this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
}
- this.$bar.append( '<div style="clear:both"></div>' );
+ this.$bar
+ .addClass( 'oo-ui-toolbar-bar' )
+ .append( this.$group, '<div style="clear:both"></div>' );
if ( config.shadow ) {
this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
}
* Dialog title.
*
* A confirmation dialog's title should describe what the progressive action will do. An alert
- * dialog's title should describe what event occured.
+ * dialog's title should describe what event occurred.
*
* @static
* inheritable
/**
* A confirmation dialog's message should describe the consequences of the progressive action. An
- * alert dialog's message should describe why the event occured.
+ * alert dialog's message should describe why the event occurred.
*
* @static
* inheritable
/* Methods */
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
+ OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
+
+ // Events
+ this.manager.connect( this, {
+ resize: 'onResize'
+ } );
+
+ return this;
+};
+
/**
* @inheritdoc
*/
OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
this.fitActions();
- return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
+ return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
+};
+
+/**
+ * Handle window resized events.
+ */
+OO.ui.MessageDialog.prototype.onResize = function () {
+ var dialog = this;
+ dialog.fitActions();
+ // Wait for CSS transition to finish and do it again :(
+ setTimeout( function () {
+ dialog.fitActions();
+ }, 300 );
};
/**
/**
* @inheritdoc
*/
-OO.ui.MessageDialog.prototype.getBodyHeight = function () {
- return Math.round( this.text.$element.outerHeight( true ) );
+OO.ui.MessageDialog.prototype.getBodyHeight = function () {
+ var bodyHeight, oldOverflow,
+ $scrollable = this.container.$element;
+
+ oldOverflow = $scrollable[0].style.overflow;
+ $scrollable[0].style.overflow = 'hidden';
+
+ // Force… ugh… something to happen
+ $scrollable.contents().hide();
+ $scrollable.height();
+ $scrollable.contents().show();
+
+ bodyHeight = Math.round( this.text.$element.outerHeight( true ) );
+ $scrollable[0].style.overflow = oldOverflow;
+
+ return bodyHeight;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
+ var $scrollable = this.container.$element;
+ OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
+
+ // 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';
+
+ // Force… ugh… something to happen
+ $scrollable.contents().hide();
+ $scrollable.height();
+ $scrollable.contents().show();
+
+ $scrollable[0].style.overflow = oldOverflow;
+ }, 300 );
+
+ return this;
};
/**
special.primary.toggleFramed( false );
}
- this.fitActions();
if ( !this.isOpening() ) {
+ // If the dialog is currently opening, this will be called automatically soon.
+ // This also calls #fitActions.
this.manager.updateWindowSize( this );
}
- this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
};
/**
*/
OO.ui.MessageDialog.prototype.fitActions = function () {
var i, len, action,
+ previous = this.verticalActionLayout,
actions = this.actions.get();
// Detect clipping
break;
}
}
+
+ 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.$,
label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
} );
- this.retryButton = new OO.ui.ButtonWidget( {
- $: this.$,
- label: OO.ui.msg( 'ooui-dialog-process-retry' )
- } );
+ this.retryButton = new OO.ui.ButtonWidget( { $: this.$ } );
this.$errors = this.$( '<div>' );
this.$errorsTitle = this.$( '<div>' );
};
/**
- * Handle errors that occured durring accept or reject processes.
+ * Handle errors that occurred during accept or reject processes.
*
* @param {OO.ui.Error[]} errors Errors to be handled
*/
OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
var i, len, $item,
items = [],
- recoverable = true;
+ recoverable = true,
+ warning = false;
for ( i = 0, len = errors.length; i < len; i++ ) {
if ( !errors[i].isRecoverable() ) {
recoverable = false;
}
+ if ( errors[i].isWarning() ) {
+ warning = true;
+ }
$item = this.$( '<div>' )
.addClass( 'oo-ui-processDialog-error' )
.append( errors[i].getMessage() );
} else {
this.currentAction.setDisabled( true );
}
+ if ( warning ) {
+ this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
+ } else {
+ this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
+ }
this.retryButton.toggle( recoverable );
this.$errorsTitle.after( this.$errorItems );
this.$errors.show().scrollTop( 0 );
* @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
*/
OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
- // Initialize configuration
+ // Configuration initialization
config = config || {};
// Parent constructor
if ( this.outlined ) {
this.editable = !!config.editable;
this.outlineControlsWidget = null;
- this.outlineWidget = new OO.ui.OutlineWidget( { $: this.$ } );
+ 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.outlineVisible = true;
if ( this.editable ) {
this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
- this.outlineWidget, { $: this.$ }
+ this.outlineSelectWidget, { $: this.$ }
);
}
}
// Events
this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
if ( this.outlined ) {
- this.outlineWidget.connect( this, { select: 'onOutlineWidgetSelect' } );
+ this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
}
if ( this.autoFocus ) {
// Event 'focus' does not bubble, but 'focusin' does
- this.stackLayout.onDOMEvent( 'focusin', this.onStackLayoutFocus.bind( this ) );
+ this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
}
// Initialization
if ( this.outlined ) {
this.outlinePanel.$element
.addClass( 'oo-ui-bookletLayout-outlinePanel' )
- .append( this.outlineWidget.$element );
+ .append( this.outlineSelectWidget.$element );
if ( this.editable ) {
this.outlinePanel.$element
.addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
* @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
*/
OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
- var $input, layout = this;
+ var layout = this;
if ( page ) {
page.scrollElementIntoView( { complete: function () {
if ( layout.autoFocus ) {
- // Set focus to the first input if nothing on the page is focused yet
- if ( !page.$element.find( ':focus' ).length ) {
- $input = page.$element.find( ':input:first' );
- if ( $input.length ) {
- $input[0].focus();
- }
- }
+ layout.focus();
}
} } );
}
};
+/**
+ * Focus the first input in the current page.
+ *
+ * If no page is selected, the first selectable page will be selected.
+ * If the focus is already in an element on the current page, nothing will happen.
+ */
+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();
+ }
+ }
+};
+
/**
* Handle outline widget select events.
*
* @param {OO.ui.OptionWidget|null} item Selected item
*/
-OO.ui.BookletLayout.prototype.onOutlineWidgetSelect = function ( item ) {
+OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
if ( item ) {
this.setPage( item.getData() );
}
prev = pages[index - 1];
// Prefer adjacent pages at the same level
if ( this.outlined ) {
- level = this.outlineWidget.getItemFromData( page.getName() ).getLevel();
+ level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
if (
prev &&
- level === this.outlineWidget.getItemFromData( prev.getName() ).getLevel()
+ level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
) {
return prev;
}
if (
next &&
- level === this.outlineWidget.getItemFromData( next.getName() ).getLevel()
+ level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
) {
return next;
}
/**
* Get the outline widget.
*
- * @return {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline
+ * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
*/
OO.ui.BookletLayout.prototype.getOutline = function () {
- return this.outlineWidget;
+ return this.outlineSelectWidget;
};
/**
*
* @return {string|null} Current page name
*/
-OO.ui.BookletLayout.prototype.getPageName = function () {
+OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
return this.currentPageName;
};
name = page.getName();
this.pages[page.getName()] = page;
if ( this.outlined ) {
- item = new OO.ui.OutlineItemWidget( name, page, { $: this.$ } );
+ item = new OO.ui.OutlineOptionWidget( { $: this.$, data: name } );
page.setOutlineItem( item );
items.push( item );
}
}
if ( this.outlined && items.length ) {
- this.outlineWidget.addItems( items, index );
- this.updateOutlineWidget();
+ this.outlineSelectWidget.addItems( items, index );
+ this.selectFirstSelectablePage();
}
this.stackLayout.addItems( pages, index );
this.emit( 'add', pages, index );
name = page.getName();
delete this.pages[name];
if ( this.outlined ) {
- items.push( this.outlineWidget.getItemFromData( name ) );
+ items.push( this.outlineSelectWidget.getItemFromData( name ) );
page.setOutlineItem( null );
}
}
if ( this.outlined && items.length ) {
- this.outlineWidget.removeItems( items );
- this.updateOutlineWidget();
+ this.outlineSelectWidget.removeItems( items );
+ this.selectFirstSelectablePage();
}
this.stackLayout.removeItems( pages );
this.emit( 'remove', pages );
this.pages = {};
this.currentPageName = null;
if ( this.outlined ) {
- this.outlineWidget.clearItems();
+ this.outlineSelectWidget.clearItems();
for ( i = 0, len = pages.length; i < len; i++ ) {
pages[i].setOutlineItem( null );
}
if ( name !== this.currentPageName ) {
if ( this.outlined ) {
- selectedItem = this.outlineWidget.getSelectedItem();
+ selectedItem = this.outlineSelectWidget.getSelectedItem();
if ( selectedItem && selectedItem.getData() !== name ) {
- this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) );
+ this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) );
}
}
if ( page ) {
};
/**
- * Call this after adding or removing items from the OutlineWidget.
+ * Select the first selectable page.
*
* @chainable
*/
-OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
- // Auto-select first item when nothing is selected anymore
- if ( !this.outlineWidget.getSelectedItem() ) {
- this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() );
+OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
+ if ( !this.outlineSelectWidget.getSelectedItem() ) {
+ this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
}
return this;
/**
* Layout made of a field and optional label.
*
- * @class
- * @extends OO.ui.Layout
- * @mixins OO.ui.LabelElement
- *
* 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
* - inline: Label is after the field and aligned toward it, best for small boolean fields like
* checkboxes or radio buttons
*
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.LabelElement
+ *
* @constructor
* @param {OO.ui.Widget} fieldWidget Field widget
* @param {Object} [config] Configuration options
* @cfg {string} [help] Explanatory text shown as a '?' icon.
*/
OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
- // Config initialization
+ var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
+
+ // Configuration initialization
config = $.extend( { align: 'left' }, config );
+ // Properties (must be set before parent constructor, which calls #getTagName)
+ this.fieldWidget = fieldWidget;
+
// Parent constructor
OO.ui.FieldLayout.super.call( this, config );
// Properties
this.$field = this.$( '<div>' );
- this.fieldWidget = fieldWidget;
+ this.$body = this.$( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
this.align = null;
if ( config.help ) {
this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
}
// Events
- if ( this.fieldWidget instanceof OO.ui.InputWidget ) {
+ if ( hasInputWidget ) {
this.$label.on( 'click', this.onLabelClick.bind( this ) );
}
this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
// Initialization
- this.$element.addClass( 'oo-ui-fieldLayout' );
+ 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 );
};
/**
* Set the field alignment mode.
*
+ * @private
* @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
* @chainable
*/
}
// Reorder elements
if ( value === 'inline' ) {
- this.$element.append( this.$field, this.$label, this.$help );
+ this.$body.append( this.$field, this.$label );
} else {
- this.$element.append( this.$help, this.$label, this.$field );
+ this.$body.append( this.$label, this.$field );
}
// Set classes. The following classes can be used here:
// * oo-ui-fieldLayout-align-left
*
* @class
* @extends OO.ui.Layout
- * @mixins OO.ui.LabelElement
* @mixins OO.ui.IconElement
+ * @mixins OO.ui.LabelElement
* @mixins OO.ui.GroupElement
*
* @constructor
* @cfg {OO.ui.FieldLayout[]} [items] Items to add
*/
OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
- // Config initialization
+ // Configuration initialization
config = config || {};
// Parent constructor
*
* @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.FormLayout = function OoUiFormLayout( config ) {
// Configuration initialization
this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
// Initialization
- this.$element.addClass( 'oo-ui-formLayout' );
+ this.$element
+ .addClass( 'oo-ui-formLayout' )
+ .attr( {
+ method: config.method,
+ action: config.action,
+ enctype: config.enctype
+ } );
};
/* Setup */
OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
var i, len, widths;
- // Config initialization
+ // Configuration initialization
config = config || {};
// Parent constructor
width = this.widths[x];
panel = this.panels[i];
dimensions = {
- width: Math.round( width * 100 ) + '%',
- height: Math.round( height * 100 ) + '%',
- top: Math.round( top * 100 ) + '%'
+ width: ( width * 100 ) + '%',
+ height: ( height * 100 ) + '%',
+ top: ( top * 100 ) + '%'
};
// If RTL, reverse:
- if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) {
- dimensions.right = Math.round( left * 100 ) + '%';
+ if ( OO.ui.Element.static.getDir( this.$.context ) === 'rtl' ) {
+ dimensions.right = ( left * 100 ) + '%';
} else {
- dimensions.left = Math.round( left * 100 ) + '%';
+ dimensions.left = ( left * 100 ) + '%';
}
// HACK: Work around IE bug by setting visibility: hidden; if width or height is zero
if ( width === 0 || height === 0 ) {
dimensions.visibility = 'hidden';
+ } else {
+ dimensions.visibility = '';
}
panel.$element.css( dimensions );
i++;
*
* @param {number} x Horizontal position
* @param {number} y Vertical position
- * @return {OO.ui.PanelLayout} The panel at the given postion
+ * @return {OO.ui.PanelLayout} The panel at the given position
*/
OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
return this.panels[ ( x * this.widths.length ) + y ];
* @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
*/
OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
- // Config initialization
+ // Configuration initialization
config = $.extend( {
scrollable: false,
padded: false,
* @constructor
* @param {string} name Unique symbolic name of page
* @param {Object} [config] Configuration options
- * @param {string} [outlineItem] Outline item widget
*/
OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
// Configuration initialization
// Properties
this.name = name;
- this.outlineItem = config.outlineItem || null;
+ this.outlineItem = null;
this.active = false;
// Initialization
/**
* Get outline item.
*
- * @return {OO.ui.OutlineItemWidget|null} Outline item widget
+ * @return {OO.ui.OutlineOptionWidget|null} Outline item widget
*/
OO.ui.PageLayout.prototype.getOutlineItem = function () {
return this.outlineItem;
* @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
* outline item as desired; this method is called for setting (with an object) and unsetting
* (with null) and overriding methods would have to check the value of `outlineItem` to avoid
- * operating on null instead of an OO.ui.OutlineItemWidget object.
+ * operating on null instead of an OO.ui.OutlineOptionWidget object.
*
- * @param {OO.ui.OutlineItemWidget|null} outlineItem Outline item widget, null to clear
+ * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear
* @chainable
*/
OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
*
* @localdoc Subclasses should override this method to adjust the outline item as desired.
*
- * @param {OO.ui.OutlineItemWidget} outlineItem Outline item widget to setup
+ * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup
* @chainable
*/
OO.ui.PageLayout.prototype.setupOutlineItem = function () {
* @constructor
* @param {Object} [config] Configuration options
* @cfg {boolean} [continuous=false] Show all pages, one after another
- * @cfg {string} [icon=''] Symbolic icon name
* @cfg {OO.ui.Layout[]} [items] Layouts to add
*/
OO.ui.StackLayout = function OoUiStackLayout( config ) {
- // Config initialization
+ // Configuration initialization
config = $.extend( { scrollable: true }, config );
// Parent constructor
*/
OO.ui.PopupToolGroup.prototype.onPointerUp = function ( e ) {
// e.which is 0 for touch events, 1 for left mouse button
- if ( !this.isDisabled() && e.which <= 1 ) {
+ // Only close toolgroup when a tool was actually selected
+ // FIXME: this duplicates logic from the parent class
+ if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === this.getTargetTool( e ) ) {
this.setActive( false );
}
return OO.ui.PopupToolGroup.super.prototype.onPointerUp.call( this, e );
/**
* Drop down list layout of tools as labeled icon buttons.
*
+ * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the
+ * bottom of the main list. These are not automatically positioned at the bottom of the list; you
+ * may want to use the 'promote' and 'demote' configuration options to achieve this.
+ *
* @class
* @extends OO.ui.PopupToolGroup
*
* @constructor
* @param {OO.ui.Toolbar} toolbar
* @param {Object} [config] Configuration options
+ * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always
+ * shown.
+ * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be
+ * allowed to be collapsed.
+ * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
*/
OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties (must be set before parent constructor, which calls #populate)
+ this.allowCollapse = config.allowCollapse;
+ this.forceExpand = config.forceExpand;
+ this.expanded = config.expanded !== undefined ? config.expanded : false;
+ this.collapsibleTools = [];
+
// Parent constructor
OO.ui.ListToolGroup.super.call( this, toolbar, config );
OO.ui.ListToolGroup.static.name = 'list';
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ListToolGroup.prototype.populate = function () {
+ var i, len, allowCollapse = [];
+
+ OO.ui.ListToolGroup.super.prototype.populate.call( this );
+
+ // Update the list of collapsible tools
+ if ( this.allowCollapse !== undefined ) {
+ allowCollapse = this.allowCollapse;
+ } else if ( this.forceExpand !== undefined ) {
+ allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
+ }
+
+ this.collapsibleTools = [];
+ for ( i = 0, len = allowCollapse.length; i < len; 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.updateCollapsibleState();
+};
+
+OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
+ if ( this.expandCollapseTool === undefined ) {
+ var ExpandCollapseTool = function () {
+ ExpandCollapseTool.super.apply( this, arguments );
+ };
+
+ OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
+
+ ExpandCollapseTool.prototype.onSelect = function () {
+ this.toolGroup.expanded = !this.toolGroup.expanded;
+ this.toolGroup.updateCollapsibleState();
+ this.setActive( false );
+ };
+ ExpandCollapseTool.prototype.onUpdateState = function () {
+ // Do nothing. Tool interface requires an implementation of this function.
+ };
+
+ ExpandCollapseTool.static.name = 'more-fewer';
+
+ this.expandCollapseTool = new ExpandCollapseTool( this );
+ }
+ return this.expandCollapseTool;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ListToolGroup.prototype.onPointerUp = function ( e ) {
+ 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 ) {
+ // Prevent the popup list from being hidden
+ this.setActive( true );
+ }
+
+ return ret;
+};
+
+OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
+ var i, len;
+
+ this.getExpandCollapseTool()
+ .setIcon( this.expanded ? 'collapse' : 'expand' )
+ .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 );
+ }
+};
+
/**
* Drop down menu layout of tools as selectable menu items.
*
* Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
*
* Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
- * allows bidrectional communication.
+ * allows bidirectional communication.
*
* Use together with OO.ui.GroupWidget to make disabled state inheritable.
*
*
* Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections.
*
+ * Subclasses that set the value of #lookupInput from their `choose` or `select` handler should
+ * be aware that this will cause new suggestions to be looked up for the new value. If this is
+ * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
+ *
* @class
* @abstract
*
* @constructor
* @param {OO.ui.TextInputWidget} input Input widget
* @param {Object} [config] Configuration options
- * @cfg {jQuery} [$overlay] Overlay layer; defaults to the current window's overlay.
+ * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
+ * @cfg {jQuery} [$container=input.$element] Element to render menu under
*/
OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
- // Config intialization
+ // Configuration initialization
config = config || {};
// Properties
this.lookupInput = input;
- this.$overlay = config.$overlay || ( this.$.$iframe || this.$element ).closest( '.oo-ui-window' ).children( '.oo-ui-window-overlay' );
- if ( this.$overlay.length === 0 ) {
- this.$overlay = this.$( 'body' );
- }
- this.lookupMenu = new OO.ui.TextInputMenuWidget( this, {
- $: OO.ui.Element.getJQuery( this.$overlay ),
+ 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.lookupCache = {};
this.lookupQuery = null;
this.lookupRequest = null;
- this.populating = false;
+ this.lookupsDisabled = false;
+ this.lookupInputFocused = false;
// Events
- this.$overlay.append( this.lookupMenu.$element );
-
this.lookupInput.$input.on( {
focus: this.onLookupInputFocus.bind( this ),
blur: this.onLookupInputBlur.bind( this ),
mousedown: this.onLookupInputMouseDown.bind( this )
} );
this.lookupInput.connect( this, { change: 'onLookupInputChange' } );
+ this.lookupMenu.connect( this, { toggle: 'onLookupMenuToggle' } );
// Initialization
this.$element.addClass( 'oo-ui-lookupWidget' );
this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
+ this.$overlay.append( this.lookupMenu.$element );
};
/* Methods */
* @param {jQuery.Event} e Input focus event
*/
OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
- this.openLookupMenu();
+ this.lookupInputFocused = true;
+ this.populateLookupMenu();
};
/**
* @param {jQuery.Event} e Input blur event
*/
OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
- this.lookupMenu.toggle( false );
+ this.closeLookupMenu();
+ this.lookupInputFocused = false;
};
/**
* @param {jQuery.Event} e Input mouse down event
*/
OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
- this.openLookupMenu();
+ // Only open the menu if the input was already focused.
+ // This way we allow the user to open the menu again after closing it with Esc
+ // by clicking in the input. Opening (and populating) the menu when initially
+ // clicking into the input is handled by the focus handler.
+ if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
+ this.populateLookupMenu();
+ }
};
/**
* @param {string} value New input value
*/
OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
- this.openLookupMenu();
+ if ( this.lookupInputFocused ) {
+ this.populateLookupMenu();
+ }
+};
+
+/**
+ * Handle the lookup menu being shown/hidden.
+ * @param {boolean} visible Whether the lookup menu is now visible.
+ */
+OO.ui.LookupInputWidget.prototype.onLookupMenuToggle = function ( visible ) {
+ if ( !visible ) {
+ // When the menu is hidden, abort any active request and clear the menu.
+ // This has to be done here in addition to closeLookupMenu(), because
+ // MenuSelectWidget will close itself when the user presses Esc.
+ this.abortLookupRequest();
+ this.lookupMenu.clearItems();
+ }
};
/**
* Get lookup menu.
*
- * @return {OO.ui.TextInputMenuWidget}
+ * @return {OO.ui.TextInputMenuSelectWidget}
*/
OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
return this.lookupMenu;
};
/**
- * Open the menu.
+ * Disable or re-enable lookups.
+ *
+ * When lookups are disabled, calls to #populateLookupMenu will be ignored.
+ *
+ * @param {boolean} disabled Disable lookups
+ */
+OO.ui.LookupInputWidget.prototype.setLookupsDisabled = function ( disabled ) {
+ this.lookupsDisabled = !!disabled;
+};
+
+/**
+ * Open the menu. If there are no entries in the menu, this does nothing.
*
* @chainable
*/
OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
- var value = this.lookupInput.getValue();
-
- if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) {
- this.populateLookupMenu();
+ if ( !this.lookupMenu.isEmpty() ) {
this.lookupMenu.toggle( true );
- } else {
- this.lookupMenu
- .clearItems()
- .toggle( false );
}
+ return this;
+};
+/**
+ * Close the menu, empty it, and abort any pending request.
+ *
+ * @chainable
+ */
+OO.ui.LookupInputWidget.prototype.closeLookupMenu = function () {
+ this.lookupMenu.toggle( false );
+ this.abortLookupRequest();
+ this.lookupMenu.clearItems();
return this;
};
/**
- * Populate lookup menu with current information.
+ * Request menu items based on the input's current value, and when they arrive,
+ * populate the menu with these items and show the menu.
+ *
+ * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
*
* @chainable
*/
OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
- var widget = this;
+ var widget = this,
+ value = this.lookupInput.getValue();
+
+ if ( this.lookupsDisabled ) {
+ return;
+ }
- if ( !this.populating ) {
- this.populating = true;
+ // If the input is empty, clear the menu
+ if ( value === '' ) {
+ this.closeLookupMenu();
+ // Skip population if there is already a request pending for the current value
+ } else if ( value !== this.lookupQuery ) {
this.getLookupMenuItems()
.done( function ( items ) {
widget.lookupMenu.clearItems();
.addItems( items )
.toggle( true );
widget.initializeLookupMenuSelection();
- widget.openLookupMenu();
} else {
- widget.lookupMenu.toggle( true );
+ widget.lookupMenu.toggle( false );
}
- widget.populating = false;
} )
.fail( function () {
widget.lookupMenu.clearItems();
- widget.populating = false;
} );
}
};
/**
- * Set selection in the lookup menu with current information.
+ * Select and highlight the first selectable item in the menu.
*
* @chainable
*/
* Get lookup menu items for the current query.
*
* @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
- * of the done event
+ * of the done event. If the request was aborted to make way for a subsequent request,
+ * this promise will not be rejected: it will remain pending forever.
*/
OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
var widget = this,
value = this.lookupInput.getValue(),
- deferred = $.Deferred();
+ deferred = $.Deferred(),
+ ourRequest;
- if ( value && value !== this.lookupQuery ) {
- // Abort current request if query has changed
- if ( this.lookupRequest ) {
- this.lookupRequest.abort();
- this.lookupQuery = null;
- this.lookupRequest = null;
- }
- if ( value in this.lookupCache ) {
- deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
- } else {
- this.lookupQuery = value;
- this.lookupRequest = this.getLookupRequest()
- .always( function () {
+ this.abortLookupRequest();
+ if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
+ deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
+ } else {
+ this.lookupInput.pushPending();
+ this.lookupQuery = value;
+ ourRequest = this.lookupRequest = this.getLookupRequest();
+ ourRequest
+ .always( function () {
+ // We need to pop pending even if this is an old request, otherwise
+ // the widget will remain pending forever.
+ // TODO: this assumes that an aborted request will fail or succeed soon after
+ // being aborted, or at least eventually. It would be nice if we could popPending()
+ // at abort time, but only if we knew that we hadn't already called popPending()
+ // for that request.
+ widget.lookupInput.popPending();
+ } )
+ .done( function ( data ) {
+ // If this is an old request (and aborting it somehow caused it to still succeed),
+ // ignore its success completely
+ if ( ourRequest === widget.lookupRequest ) {
widget.lookupQuery = null;
widget.lookupRequest = null;
- } )
- .done( function ( data ) {
widget.lookupCache[value] = widget.getLookupCacheItemFromData( data );
deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[value] ) );
- } )
- .fail( function () {
+ }
+ } )
+ .fail( function () {
+ // If this is an old request (or a request failing because it's being aborted),
+ // ignore its failure completely
+ if ( ourRequest === widget.lookupRequest ) {
+ widget.lookupQuery = null;
+ widget.lookupRequest = null;
deferred.reject();
- } );
- this.pushPending();
- this.lookupRequest.always( function () {
- widget.popPending();
+ }
} );
- }
}
return deferred.promise();
};
+/**
+ * Abort the currently pending lookup request, if any.
+ */
+OO.ui.LookupInputWidget.prototype.abortLookupRequest = function () {
+ var oldRequest = this.lookupRequest;
+ if ( oldRequest ) {
+ // First unset this.lookupRequest to the fail handler will notice
+ // that the request is no longer current
+ this.lookupRequest = null;
+ this.lookupQuery = null;
+ oldRequest.abort();
+ }
+};
+
/**
* Get a new request object of the current lookup query value.
*
* @abstract
- * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method
+ * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
*/
OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
// Stub, implemented in subclass
};
/**
- * Handle successful lookup request.
- *
- * Overriding methods should call #populateLookupMenu when results are available and cache results
- * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects.
+ * Get a list of menu item widgets from the data stored by the lookup request's done handler.
*
* @abstract
- * @param {Mixed} data Response from server
+ * @param {Mixed} data Cached result data, usually an array
+ * @return {OO.ui.MenuOptionWidget[]} Menu items
*/
-OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
+OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
// Stub, implemented in subclass
+ return [];
};
/**
- * Get a list of menu item widgets from the data stored by the lookup request's done handler.
+ * Get lookup cache item from server response data.
*
* @abstract
- * @param {Mixed} data Cached result data, usually an array
- * @return {OO.ui.MenuItemWidget[]} Menu items
+ * @param {Mixed} data Response from server
+ * @return {Mixed} Cached result data
*/
-OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
+OO.ui.LookupInputWidget.prototype.getLookupCacheItemFromData = function () {
// Stub, implemented in subclass
return [];
};
/**
- * Set of controls for an OO.ui.OutlineWidget.
+ * Set of controls for an OO.ui.OutlineSelectWidget.
*
* Controls include moving items up and down, removing items, and adding different kinds of items.
*
* @mixins OO.ui.IconElement
*
* @constructor
- * @param {OO.ui.OutlineWidget} outline Outline to control
+ * @param {OO.ui.OutlineSelectWidget} outline Outline to control
* @param {Object} [config] Configuration options
*/
OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {OO.ui.ButtonWidget} [items] Buttons to add
+ * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
*/
OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
// Parent constructor
OO.ui.ButtonGroupWidget.super.call( this, config );
*/
OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
// Configuration initialization
- config = $.extend( { target: '_blank' }, config );
+ config = config || {};
// Parent constructor
OO.ui.ButtonWidget.super.call( this, config );
*/
OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
- this.onClick();
+ this.emit( 'click' );
if ( this.isHyperlink ) {
return true;
}
* @cfg {boolean} [framed=false] Render button with a frame
*/
OO.ui.ActionWidget = function OoUiActionWidget( config ) {
- // Config intialization
+ // Configuration initialization
config = $.extend( { framed: false }, config );
// Parent constructor
// Parent method (from mixin)
OO.ui.ToggleWidget.prototype.setValue.call( this, value );
- return this;
-};
-
-/**
- * Icon widget.
- *
- * See OO.ui.IconElement for more information.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IconElement
- * @mixins OO.ui.TitledElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.IconWidget = function OoUiIconWidget( config ) {
- // Config intialization
- config = config || {};
-
- // Parent constructor
- OO.ui.IconWidget.super.call( this, config );
-
- // Mixin constructors
- OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
- OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
-
- // Initialization
- this.$element.addClass( 'oo-ui-iconWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
-
-/* Static Properties */
-
-OO.ui.IconWidget.static.tagName = 'span';
-
-/**
- * Indicator widget.
- *
- * See OO.ui.IndicatorElement for more information.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IndicatorElement
- * @mixins OO.ui.TitledElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
- // Config intialization
- config = config || {};
-
- // Parent constructor
- OO.ui.IndicatorWidget.super.call( this, config );
-
- // Mixin constructors
- OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
- OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
-
- // Initialization
- this.$element.addClass( 'oo-ui-indicatorWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
-
-/* Static Properties */
-
-OO.ui.IndicatorWidget.static.tagName = 'span';
+ return this;
+};
/**
- * Inline menu of options.
+ * Dropdown menu of options.
*
- * Inline menus provide a control for accessing a menu and compose a menu within the widget, which
+ * Dropdown menus provide a control for accessing a menu and compose a menu within the widget, which
* can be accessed using the #getMenu method.
*
- * Use with OO.ui.MenuItemWidget.
+ * Use with OO.ui.MenuOptionWidget.
*
* @class
* @extends OO.ui.Widget
* @param {Object} [config] Configuration options
* @cfg {Object} [menu] Configuration options to pass to menu widget
*/
-OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
+OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
// Configuration initialization
config = $.extend( { indicator: 'down' }, config );
// Parent constructor
- OO.ui.InlineMenuWidget.super.call( this, config );
+ OO.ui.DropdownWidget.super.call( this, config );
// Mixin constructors
OO.ui.IconElement.call( this, config );
OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
// Properties
- this.menu = new OO.ui.MenuWidget( $.extend( { $: this.$, widget: this }, config.menu ) );
+ this.menu = new OO.ui.MenuSelectWidget( $.extend( { $: this.$, widget: this }, config.menu ) );
this.$handle = this.$( '<span>' );
// Events
// Initialization
this.$handle
- .addClass( 'oo-ui-inlineMenuWidget-handle' )
+ .addClass( 'oo-ui-dropdownWidget-handle' )
.append( this.$icon, this.$label, this.$indicator );
this.$element
- .addClass( 'oo-ui-inlineMenuWidget' )
+ .addClass( 'oo-ui-dropdownWidget' )
.append( this.$handle, this.menu.$element );
};
/* Setup */
-OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatorElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabelElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement );
+OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
/* Methods */
/**
* Get the menu.
*
- * @return {OO.ui.MenuWidget} Menu of widget
+ * @return {OO.ui.MenuSelectWidget} Menu of widget
*/
-OO.ui.InlineMenuWidget.prototype.getMenu = function () {
+OO.ui.DropdownWidget.prototype.getMenu = function () {
return this.menu;
};
/**
* Handles menu select events.
*
- * @param {OO.ui.MenuItemWidget} item Selected menu item
+ * @param {OO.ui.MenuOptionWidget} item Selected menu item
*/
-OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) {
+OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
var selectedLabel;
if ( !item ) {
*
* @param {jQuery.Event} e Mouse click event
*/
-OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
+OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
// Skip clicks within the menu
if ( $.contains( this.menu.$element[0], e.target ) ) {
return;
return false;
};
+/**
+ * Icon widget.
+ *
+ * See OO.ui.IconElement for more information.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.TitledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.IconWidget = function OoUiIconWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.IconWidget.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
+ OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-iconWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
+
+/* Static Properties */
+
+OO.ui.IconWidget.static.tagName = 'span';
+
+/**
+ * Indicator widget.
+ *
+ * See OO.ui.IndicatorElement for more information.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IndicatorElement
+ * @mixins OO.ui.TitledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.IndicatorWidget.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
+ OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-indicatorWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
+
+/* Static Properties */
+
+OO.ui.IndicatorWidget.static.tagName = 'span';
+
/**
* Base class for input widgets.
*
* @param {Object} [config] Configuration options
* @cfg {string} [name=''] HTML input name
* @cfg {string} [value=''] Input value
- * @cfg {boolean} [readOnly=false] Prevent changes
* @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
*/
OO.ui.InputWidget = function OoUiInputWidget( config ) {
- // Config intialization
- config = $.extend( { readOnly: false }, config );
+ // Configuration initialization
+ config = config || {};
// Parent constructor
OO.ui.InputWidget.super.call( this, config );
// Properties
this.$input = this.getInputElement( config );
this.value = '';
- this.readOnly = false;
this.inputFilter = config.inputFilter;
// Events
this.$input
.attr( 'name', config.name )
.prop( 'disabled', this.isDisabled() );
- this.setReadOnly( config.readOnly );
- this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input );
+ this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
this.setValue( config.value );
};
/**
* @event change
- * @param value
+ * @param {string} value
*/
/* Methods */
/**
* Get input element.
*
+ * @private
* @param {Object} [config] Configuration options
* @return {jQuery} Input element
*/
* @chainable
*/
OO.ui.InputWidget.prototype.setValue = function ( value ) {
- value = this.sanitizeValue( value );
+ value = this.cleanUpValue( value );
+ // Update the DOM if it has changed. Note that with cleanUpValue, it
+ // is possible for the DOM value to change without this.value changing.
+ if ( this.$input.val() !== value ) {
+ this.$input.val( value );
+ }
if ( this.value !== value ) {
this.value = value;
this.emit( 'change', this.value );
}
- // Update the DOM if it has changed. Note that with sanitizeValue, it
- // is possible for the DOM value to change without this.value changing.
- if ( this.$input.val() !== this.value ) {
- this.$input.val( this.value );
- }
return this;
};
/**
- * Sanitize incoming value.
+ * Clean up incoming value.
*
- * Ensures value is a string, and converts undefined and null to empty strings.
+ * Ensures value is a string, and converts undefined and null to empty string.
*
+ * @private
* @param {string} value Original value
- * @return {string} Sanitized value
+ * @return {string} Cleaned up value
*/
-OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) {
+OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
if ( value === undefined || value === null ) {
return '';
} else if ( this.inputFilter ) {
};
/**
- * Check if the widget is read-only.
+ * @inheritdoc
+ */
+OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
+ OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
+ if ( this.$input ) {
+ this.$input.prop( 'disabled', this.isDisabled() );
+ }
+ return this;
+};
+
+/**
+ * Focus the input.
*
- * @return {boolean}
+ * @chainable
*/
-OO.ui.InputWidget.prototype.isReadOnly = function () {
- return this.readOnly;
+OO.ui.InputWidget.prototype.focus = function () {
+ this.$input[0].focus();
+ return this;
};
/**
- * Set the read-only state of the widget.
+ * Blur the input.
*
- * This should probably change the widgets's appearance and prevent it from being used.
+ * @chainable
+ */
+OO.ui.InputWidget.prototype.blur = function () {
+ this.$input[0].blur();
+ return this;
+};
+
+/**
+ * A button that is an input widget. Intended to be used within a OO.ui.FormLayout.
*
- * @param {boolean} state Make input read-only
+ * @class
+ * @extends OO.ui.InputWidget
+ * @mixins OO.ui.ButtonElement
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.IndicatorElement
+ * @mixins OO.ui.LabelElement
+ * @mixins OO.ui.TitledElement
+ * @mixins OO.ui.FlaggedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [type='button'] HTML tag `type` attribute, may be 'button', 'submit' or 'reset'
+ * @cfg {boolean} [useInputTag=false] Whether to use `<input/>` rather than `<button/>`. Only useful
+ * if you need IE 6 support in a form with multiple buttons. If you use this option, icons and
+ * indicators will not be displayed, it won't be possible to have a non-plaintext label, and it
+ * won't be possible to set a value (which will internally become identical to the label).
+ */
+OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
+ // Configuration initialization
+ config = $.extend( { type: 'button', useInputTag: false }, config );
+
+ // Properties (must be set before parent constructor, which calls #setValue)
+ this.useInputTag = config.useInputTag;
+
+ // Parent constructor
+ OO.ui.ButtonInputWidget.super.call( this, config );
+
+ // Mixin constructors
+ OO.ui.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
+ 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.$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 );
+ }
+ this.$element.addClass( 'oo-ui-buttonInputWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.ButtonElement );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IconElement );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IndicatorElement );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
+
+/* Events */
+
+/**
+ * @event click
+ */
+
+/* Methods */
+
+/**
+ * Get input element.
+ *
+ * @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 );
+};
+
+/**
+ * Set label value.
+ *
+ * Overridden to support setting the 'value' of `<input/>` elements.
+ *
+ * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
+ * text; or null for no label
* @chainable
*/
-OO.ui.InputWidget.prototype.setReadOnly = function ( state ) {
- this.readOnly = !!state;
- this.$input.prop( 'readOnly', this.readOnly );
+OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
+ OO.ui.LabelElement.prototype.setLabel.call( this, label );
+
+ if ( this.useInputTag ) {
+ if ( typeof label === 'function' ) {
+ label = OO.ui.resolveMsg( label );
+ }
+ if ( label instanceof jQuery ) {
+ label = label.text();
+ }
+ if ( !label ) {
+ label = '';
+ }
+ this.$input.val( label );
+ }
+
+ return this;
+};
+
+/**
+ * Set the value of the input.
+ *
+ * Overridden to disable for `<input/>` elements, which have value identical to the label.
+ *
+ * @param {string} value New value
+ * @chainable
+ */
+OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
+ if ( !this.useInputTag ) {
+ OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value );
+ }
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.
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [selected=false] Whether the checkbox is initially selected
+ */
+OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
+ // Parent constructor
+ OO.ui.CheckboxInputWidget.super.call( this, config );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-checkboxInputWidget' );
+ this.setSelected( config.selected !== undefined ? config.selected : false );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
+
+/* Methods */
+
+/**
+ * Get input element.
+ *
+ * @private
+ * @return {jQuery} Input element
+ */
+OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
+ return this.$( '<input type="checkbox" />' );
+};
+
/**
* @inheritdoc
*/
-OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
- OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
- if ( this.$input ) {
- this.$input.prop( 'disabled', this.isDisabled() );
+OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
+ var widget = this;
+ if ( !this.isDisabled() ) {
+ // Allow the stack to clear so the value will be updated
+ setTimeout( function () {
+ widget.setSelected( widget.$input.prop( 'checked' ) );
+ } );
}
- return this;
};
/**
- * Focus the input.
+ * Set selection state of this checkbox.
*
+ * @param {boolean} state Whether the checkbox is selected
* @chainable
*/
-OO.ui.InputWidget.prototype.focus = function () {
- this.$input[0].focus();
+OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
+ state = !!state;
+ if ( this.selected !== state ) {
+ this.selected = state;
+ this.$input.prop( 'checked', this.selected );
+ this.emit( 'change', this.selected );
+ }
return this;
};
/**
- * Blur the input.
+ * Check if this checkbox is selected.
*
- * @chainable
+ * @return {boolean} Checkbox is selected
*/
-OO.ui.InputWidget.prototype.blur = function () {
- this.$input[0].blur();
- return this;
+OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
+ return this.selected;
};
/**
- * Checkbox input widget.
+ * Radio input widget.
+ *
+ * Radio buttons only make sense as a set, and you probably want to use the OO.ui.RadioSelectWidget
+ * class instead of using this class directly.
*
* @class
* @extends OO.ui.InputWidget
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {boolean} [selected=false] Whether the radio button is initially selected
*/
-OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
+OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
// Parent constructor
- OO.ui.CheckboxInputWidget.super.call( this, config );
+ OO.ui.RadioInputWidget.super.call( this, config );
// Initialization
- this.$element.addClass( 'oo-ui-checkboxInputWidget' );
+ this.$element.addClass( 'oo-ui-radioInputWidget' );
+ this.setSelected( config.selected !== undefined ? config.selected : false );
};
/* Setup */
-OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
+OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
/* Methods */
/**
* Get input element.
*
+ * @private
* @return {jQuery} Input element
*/
-OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
- return this.$( '<input type="checkbox" />' );
+OO.ui.RadioInputWidget.prototype.getInputElement = function () {
+ return this.$( '<input type="radio" />' );
};
/**
- * Get checked state of the checkbox
- *
- * @return {boolean} If the checkbox is checked
+ * @inheritdoc
*/
-OO.ui.CheckboxInputWidget.prototype.getValue = function () {
- return this.value;
+OO.ui.RadioInputWidget.prototype.onEdit = function () {
+ // RadioInputWidget doesn't track its state.
};
/**
- * Set checked state of the checkbox
+ * Set selection state of this radio button.
*
- * @param {boolean} value New value
+ * @param {boolean} state Whether the button is selected
+ * @chainable
*/
-OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) {
- value = !!value;
- if ( this.value !== value ) {
- this.value = value;
- this.$input.prop( 'checked', this.value );
- this.emit( 'change', this.value );
- }
+OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
+ // RadioInputWidget doesn't track its state.
+ this.$input.prop( 'checked', state );
+ return this;
};
/**
- * @inheritdoc
+ * Check if this radio button is selected.
+ *
+ * @return {boolean} Radio is selected
*/
-OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
- var widget = this;
- if ( !this.isDisabled() ) {
- // Allow the stack to clear so the value will be updated
- setTimeout( function () {
- widget.setValue( widget.$input.prop( 'checked' ) );
- } );
- }
+OO.ui.RadioInputWidget.prototype.isSelected = function () {
+ return this.$input.prop( 'checked' );
};
/**
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {string} [type='text'] HTML tag `type` attribute
* @cfg {string} [placeholder] Placeholder text
+ * @cfg {boolean} [readOnly=false] Prevent changes
* @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
*/
OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
// Configuration initialization
- config = config || {};
+ config = $.extend( { readOnly: false }, config );
// Parent constructor
OO.ui.TextInputWidget.super.call( this, config );
OO.ui.PendingElement.call( this, config );
// Properties
+ this.readOnly = false;
this.multiline = !!config.multiline;
this.autosize = !!config.autosize;
this.maxRows = config.maxRows !== undefined ? config.maxRows : 10;
this.validate = null;
+ // Clone for resizing
+ if ( this.autosize ) {
+ this.$clone = this.$input
+ .clone()
+ .insertAfter( this.$input )
+ .hide();
+ }
+
this.setValidation( config.validate );
// Events
this.$element
.addClass( 'oo-ui-textInputWidget' )
.append( this.$icon, this.$indicator );
+ this.setReadOnly( config.readOnly );
if ( config.placeholder ) {
this.$input.attr( 'placeholder', config.placeholder );
}
*/
OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
- this.emit( 'enter' );
+ this.emit( 'enter', e );
}
};
return this;
};
+/**
+ * Check if the widget is read-only.
+ *
+ * @return {boolean}
+ */
+OO.ui.TextInputWidget.prototype.isReadOnly = function () {
+ return this.readOnly;
+};
+
+/**
+ * Set the read-only state of the widget.
+ *
+ * This should probably change the widget's appearance and prevent it from being used.
+ *
+ * @param {boolean} state Make input read-only
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
+ this.readOnly = !!state;
+ this.$input.prop( 'readOnly', this.readOnly );
+ return this;
+};
+
/**
* Automatically adjust the size of the text input.
*
* @chainable
*/
OO.ui.TextInputWidget.prototype.adjustSize = function () {
- var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
+ var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
- if ( this.multiline && this.autosize ) {
- $clone = this.$input.clone()
+ if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
+ this.$clone
.val( this.$input.val() )
+ .attr( 'rows', '' )
// Set inline height property to 0 to measure scroll height
- .css( { height: 0 } )
- .insertAfter( this.$input );
- scrollHeight = $clone[0].scrollHeight;
+ .css( 'height', 0 );
+
+ this.$clone[0].style.display = 'block';
+
+ this.valCache = this.$input.val();
+
+ scrollHeight = this.$clone[0].scrollHeight;
+
// Remove inline height property to measure natural heights
- $clone.css( 'height', '' );
- innerHeight = $clone.innerHeight();
- outerHeight = $clone.outerHeight();
+ this.$clone.css( 'height', '' );
+ innerHeight = this.$clone.innerHeight();
+ outerHeight = this.$clone.outerHeight();
+
// Measure max rows height
- $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' ).val( '' );
- maxInnerHeight = $clone.innerHeight();
+ this.$clone
+ .attr( 'rows', this.maxRows )
+ .css( 'height', 'auto' )
+ .val( '' );
+ maxInnerHeight = this.$clone.innerHeight();
+
// Difference between reported innerHeight and scrollHeight with no scrollbars present
// Equals 1 on Blink-based browsers and 0 everywhere else
- measurementError = maxInnerHeight - $clone[0].scrollHeight;
- $clone.remove();
+ measurementError = maxInnerHeight - this.$clone[0].scrollHeight;
idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
+
+ this.$clone[0].style.display = 'none';
+
// Only apply inline height when expansion beyond natural height is needed
if ( idealHeight > innerHeight ) {
// Use the difference between the inner and outer height as a buffer
/**
* Get input element.
*
+ * @private
* @param {Object} [config] Configuration options
* @return {jQuery} Input element
*/
OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
- return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="text" />' );
-};
+ // Configuration initialization
+ config = config || {};
-/* Methods */
+ var type = config.type || 'text';
+
+ return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="' + type + '" />' );
+};
/**
* Check if input supports multiple lines.
/**
* Sets the validation pattern to use.
- * @param validate {RegExp|string|null} Regular expression (or symbolic name referencing
+ * @param {RegExp|string|null} validate Regular expression (or symbolic name referencing
* one, see #static-validationPatterns)
*/
OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
* @param {Object} [config] Configuration options
* @cfg {Object} [menu] Configuration options to pass to menu widget
* @cfg {Object} [input] Configuration options to pass to input widget
- * @cfg {jQuery} [$overlay] Overlay layer; defaults to the current window's overlay.
+ * @cfg {jQuery} [$overlay] Overlay layer; defaults to relative positioning
*/
OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
// Configuration initialization
OO.ui.ComboBoxWidget.super.call( this, config );
// Properties
- this.$overlay = config.$overlay || ( this.$.$iframe || this.$element ).closest( '.oo-ui-window' ).children( '.oo-ui-window-overlay' );
- if ( this.$overlay.length === 0 ) {
- this.$overlay = this.$( 'body' );
- }
+ this.$overlay = config.$overlay || this.$element;
this.input = new OO.ui.TextInputWidget( $.extend(
{ $: this.$, indicator: 'down', disabled: this.isDisabled() },
config.input
) );
- this.menu = new OO.ui.TextInputMenuWidget( this.input, $.extend(
- { $: this.$, widget: this, input: this.input, disabled: this.isDisabled() },
+ this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
+ {
+ $: OO.ui.Element.static.getJQuery( this.$overlay ),
+ widget: this,
+ input: this.input,
+ disabled: this.isDisabled()
+ },
config.menu
) );
/* Methods */
+/**
+ * Get the combobox's menu.
+ * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
+ */
+OO.ui.ComboBoxWidget.prototype.getMenu = function () {
+ return this.menu;
+};
+
/**
* Handle input change events.
*
* Handle menu item change events.
*/
OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
+ var match = this.menu.getItemFromData( this.input.getValue() );
+ this.menu.selectItem( match );
this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
};
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {OO.ui.InputWidget} [input] Input widget this label is for
*/
OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
- // Config intialization
+ // Configuration initialization
config = config || {};
// Parent constructor
* @mixins OO.ui.FlaggedElement
*
* @constructor
- * @param {Mixed} data Option data
* @param {Object} [config] Configuration options
- * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling
*/
-OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
- // Config intialization
+OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
+ // Configuration initialization
config = config || {};
// Parent constructor
OO.ui.FlaggedElement.call( this, config );
// Properties
- this.data = data;
this.selected = false;
this.highlighted = false;
this.pressed = false;
// Initialization
this.$element
.data( 'oo-ui-optionWidget', this )
- .attr( 'rel', config.rel )
.attr( 'role', 'option' )
.addClass( 'oo-ui-optionWidget' )
.append( this.$label );
- this.$element
- .prepend( this.$icon )
- .append( this.$indicator );
};
/* Setup */
return deferred.promise();
};
-/**
- * Get option data.
- *
- * @return {Mixed} Option data
- */
-OO.ui.OptionWidget.prototype.getData = function () {
- return this.data;
-};
-
/**
* Option widget with an option icon and indicator.
*
* @mixins OO.ui.IndicatorElement
*
* @constructor
- * @param {Mixed} data Option data
* @param {Object} [config] Configuration options
*/
-OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( data, config ) {
+OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
// Parent constructor
- OO.ui.DecoratedOptionWidget.super.call( this, data, config );
+ OO.ui.DecoratedOptionWidget.super.call( this, config );
// Mixin constructors
OO.ui.IconElement.call( this, config );
* @mixins OO.ui.ButtonElement
*
* @constructor
- * @param {Mixed} data Option data
* @param {Object} [config] Configuration options
*/
-OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
+OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
// Parent constructor
- OO.ui.ButtonOptionWidget.super.call( this, data, config );
+ OO.ui.ButtonOptionWidget.super.call( this, config );
// Mixin constructors
OO.ui.ButtonElement.call( this, config );
};
/**
- * Item of an OO.ui.MenuWidget.
+ * Option widget that looks like a radio button.
+ *
+ * Use together with OO.ui.RadioSelectWidget.
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
+ // Parent constructor
+ OO.ui.RadioOptionWidget.super.call( this, config );
+
+ // Properties
+ this.radio = new OO.ui.RadioInputWidget( { value: config.data } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-radioOptionWidget' )
+ .prepend( this.radio.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
+
+/* Static Properties */
+
+OO.ui.RadioOptionWidget.static.highlightable = false;
+
+OO.ui.RadioOptionWidget.static.pressable = false;
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
+ OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
+
+ this.radio.setSelected( state );
+
+ return this;
+};
+
+/**
+ * Item of an OO.ui.MenuSelectWidget.
*
* @class
* @extends OO.ui.DecoratedOptionWidget
*
* @constructor
- * @param {Mixed} data Item data
* @param {Object} [config] Configuration options
*/
-OO.ui.MenuItemWidget = function OoUiMenuItemWidget( data, config ) {
+OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
// Configuration initialization
config = $.extend( { icon: 'check' }, config );
// Parent constructor
- OO.ui.MenuItemWidget.super.call( this, data, config );
+ OO.ui.MenuOptionWidget.super.call( this, config );
// Initialization
this.$element
.attr( 'role', 'menuitem' )
- .addClass( 'oo-ui-menuItemWidget' );
+ .addClass( 'oo-ui-menuOptionWidget' );
};
/* Setup */
-OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.DecoratedOptionWidget );
+OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
/**
- * Section to group one or more items in a OO.ui.MenuWidget.
+ * Section to group one or more items in a OO.ui.MenuSelectWidget.
*
* @class
* @extends OO.ui.DecoratedOptionWidget
*
* @constructor
- * @param {Mixed} data Item data
* @param {Object} [config] Configuration options
*/
-OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) {
+OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
// Parent constructor
- OO.ui.MenuSectionItemWidget.super.call( this, data, config );
+ OO.ui.MenuSectionOptionWidget.super.call( this, config );
// Initialization
- this.$element.addClass( 'oo-ui-menuSectionItemWidget' );
+ this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
};
/* Setup */
-OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.DecoratedOptionWidget );
+OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
/* Static Properties */
-OO.ui.MenuSectionItemWidget.static.selectable = false;
+OO.ui.MenuSectionOptionWidget.static.selectable = false;
-OO.ui.MenuSectionItemWidget.static.highlightable = false;
+OO.ui.MenuSectionOptionWidget.static.highlightable = false;
/**
- * Items for an OO.ui.OutlineWidget.
+ * Items for an OO.ui.OutlineSelectWidget.
*
* @class
* @extends OO.ui.DecoratedOptionWidget
*
* @constructor
- * @param {Mixed} data Item data
* @param {Object} [config] Configuration options
* @cfg {number} [level] Indentation level
* @cfg {boolean} [movable] Allow modification from outline controls
*/
-OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) {
- // Config intialization
+OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
+ // Configuration initialization
config = config || {};
// Parent constructor
- OO.ui.OutlineItemWidget.super.call( this, data, config );
+ OO.ui.OutlineOptionWidget.super.call( this, config );
// Properties
this.level = 0;
this.removable = !!config.removable;
// Initialization
- this.$element.addClass( 'oo-ui-outlineItemWidget' );
+ this.$element.addClass( 'oo-ui-outlineOptionWidget' );
this.setLevel( config.level );
};
/* Setup */
-OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.DecoratedOptionWidget );
+OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
/* Static Properties */
-OO.ui.OutlineItemWidget.static.highlightable = false;
+OO.ui.OutlineOptionWidget.static.highlightable = false;
-OO.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true;
+OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
-OO.ui.OutlineItemWidget.static.levelClass = 'oo-ui-outlineItemWidget-level-';
+OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
-OO.ui.OutlineItemWidget.static.levels = 3;
+OO.ui.OutlineOptionWidget.static.levels = 3;
/* Methods */
/**
* Check if item is movable.
*
- * Movablilty is used by outline controls.
+ * Movability is used by outline controls.
*
* @return {boolean} Item is movable
*/
-OO.ui.OutlineItemWidget.prototype.isMovable = function () {
+OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
return this.movable;
};
/**
* Check if item is removable.
*
- * Removablilty is used by outline controls.
+ * Removability is used by outline controls.
*
* @return {boolean} Item is removable
*/
-OO.ui.OutlineItemWidget.prototype.isRemovable = function () {
+OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
return this.removable;
};
*
* @return {number} Indentation level
*/
-OO.ui.OutlineItemWidget.prototype.getLevel = function () {
+OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
return this.level;
};
/**
* Set movability.
*
- * Movablilty is used by outline controls.
+ * Movability is used by outline controls.
*
* @param {boolean} movable Item is movable
* @chainable
*/
-OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
+OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
this.movable = !!movable;
this.updateThemeClasses();
return this;
/**
* Set removability.
*
- * Removablilty is used by outline controls.
+ * Removability is used by outline controls.
*
* @param {boolean} movable Item is removable
* @chainable
*/
-OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) {
+OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
this.removable = !!removable;
this.updateThemeClasses();
return this;
* @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
* @chainable
*/
-OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) {
+OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
var levels = this.constructor.static.levels,
levelClass = this.constructor.static.levelClass,
i = levels;
* @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
* @cfg {string} [align='center'] Alignment of popup to origin
* @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
+ * @cfg {number} [containerPadding=10] How much padding to keep between popup and container
* @cfg {jQuery} [$content] Content to append to the popup's body
* @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
* @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
* @cfg {boolean} [padded] Add padding to the body
*/
OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
- // Config intialization
+ // Configuration initialization
config = config || {};
// Parent constructor
this.$head = this.$( '<div>' );
this.$body = this.$( '<div>' );
this.$anchor = this.$( '<div>' );
- this.$container = config.$container; // If undefined, will be computed lazily in updateDimensions()
+ // If undefined, will be computed lazily in updateDimensions()
+ this.$container = config.$container;
+ this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
this.autoClose = !!config.autoClose;
this.$autoCloseIgnore = config.$autoCloseIgnore;
this.transitionTimeout = null;
OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
-/* Events */
-
-/**
- * @event hide
- */
-
-/**
- * @event show
- */
-
/* Methods */
/**
OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
- widget = this,
- padding = 10;
+ widget = this;
if ( !this.$container ) {
// Lazy-initialize $container if not specified in constructor
containerLeft = Math.round( this.$container.offset().left );
containerWidth = this.$container.innerWidth();
containerRight = containerLeft + containerWidth;
- popupLeft = popupOffset - padding;
- popupRight = popupOffset + padding + this.width + padding;
+ popupLeft = popupOffset - this.containerPadding;
+ popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
overlapLeft = ( originOffset + popupLeft ) - containerLeft;
overlapRight = containerRight - ( originOffset + popupRight );
}
// Adjust offset to avoid anchor being rendered too close to the edge
- anchorWidth = this.$anchor.width();
- if ( this.align === 'right' ) {
- popupOffset += anchorWidth;
- } else if ( this.align === 'left' ) {
- popupOffset -= anchorWidth;
+ // $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;
+ if ( popupOffset + this.width < anchorWidth ) {
+ popupOffset = anchorWidth - this.width;
+ } else if ( -popupOffset < anchorWidth ) {
+ popupOffset = -anchorWidth;
}
// Prevent transition from being interrupted
return this;
};
+/**
+ * Progress bar widget.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number|boolean} [progress=false] Initial progress percent or false for indeterminate
+ */
+OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.ProgressBarWidget.super.call( this, config );
+
+ // Properties
+ this.$bar = this.$( '<div>' );
+ this.progress = null;
+
+ // Initialization
+ this.setProgress( config.progress !== undefined ? config.progress : false );
+ this.$bar.addClass( 'oo-ui-progressBarWidget-bar');
+ this.$element
+ .attr( {
+ role: 'progressbar',
+ 'aria-valuemin': 0,
+ 'aria-valuemax': 100
+ } )
+ .addClass( 'oo-ui-progressBarWidget' )
+ .append( this.$bar );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
+
+/* Static Properties */
+
+OO.ui.ProgressBarWidget.static.tagName = 'div';
+
+/* Methods */
+
+/**
+ * Get progress percent
+ *
+ * @return {number} Progress percent
+ */
+OO.ui.ProgressBarWidget.prototype.getProgress = function () {
+ return this.progress;
+};
+
+/**
+ * Set progress percent
+ *
+ * @param {number|boolean} progress Progress percent or false for indeterminate
+ */
+OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
+ this.progress = progress;
+
+ if ( progress !== false ) {
+ this.$bar.css( 'width', this.progress + '%' );
+ this.$element.attr( 'aria-valuenow', this.progress );
+ } else {
+ this.$bar.css( 'width', '' );
+ this.$element.removeAttr( 'aria-valuenow' );
+ }
+ this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
+};
+
/**
* Search widget.
*
* @cfg {string} [value] Initial query value
*/
OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
- // Configuration intialization
+ // Configuration initialization
config = config || {};
// Parent constructor
/**
* Generic selection of options.
*
- * Items can contain any rendering, and are uniquely identified by a hash of their data. Any widget
- * that provides options, from which the user must choose one, should be built on this class.
+ * Items can contain any rendering. Any widget that provides options, from which the user must
+ * choose one, should be built on this class.
*
* Use together with OO.ui.OptionWidget.
*
* @cfg {OO.ui.OptionWidget[]} [items] Options to add
*/
OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
- // Config intialization
+ // Configuration initialization
config = config || {};
// Parent constructor
// Properties
this.pressed = false;
this.selecting = null;
- this.hashes = {};
this.onMouseUpHandler = this.onMouseUp.bind( this );
this.onMouseMoveHandler = this.onMouseMove.bind( this );
return null;
};
-/**
- * Get an existing item with equivilant data.
- *
- * @param {Object} data Item data to search for
- * @return {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists
- */
-OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
- var hash = OO.getHash( data );
-
- if ( hash in this.hashes ) {
- return this.hashes[hash];
- }
-
- return null;
-};
-
/**
* Toggle pressed state.
*
/**
* Get an item relative to another one.
*
- * @param {OO.ui.OptionWidget} item Item to start at
- * @param {number} direction Direction to move in
+ * @param {OO.ui.OptionWidget|null} item Item to start at, null to get relative to list start
+ * @param {number} direction Direction to move in, -1 to move backward, 1 to move forward
* @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
*/
OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
- var inc = direction > 0 ? 1 : -1,
- len = this.items.length,
- index = item instanceof OO.ui.OptionWidget ?
- $.inArray( item, this.items ) : ( inc > 0 ? -1 : 0 ),
- stopAt = Math.max( Math.min( index, len - 1 ), 0 ),
- i = inc > 0 ?
- // Default to 0 instead of -1, if nothing is selected let's start at the beginning
- Math.max( index, -1 ) :
- // Default to n-1 instead of -1, if nothing is selected let's start at the end
- Math.min( index, len );
-
- while ( len !== 0 ) {
- i = ( i + inc + len ) % len;
- item = this.items[i];
+ var currentIndex, nextIndex, i,
+ increase = direction > 0 ? 1 : -1,
+ len = this.items.length;
+
+ if ( item instanceof OO.ui.OptionWidget ) {
+ currentIndex = $.inArray( item, this.items );
+ nextIndex = ( currentIndex + increase + len ) % len;
+ } else {
+ // If no item is selected and moving forward, start at the beginning.
+ // If moving backward, start at the end.
+ nextIndex = direction > 0 ? 0 : len - 1;
+ }
+
+ for ( i = 0; i < len; i++ ) {
+ item = this.items[nextIndex];
if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
return item;
}
- // Stop iterating when we've looped all the way around
- if ( i === stopAt ) {
- break;
- }
+ nextIndex = ( nextIndex + increase + len ) % len;
}
return null;
};
/**
* Get the next selectable item.
*
- * @return {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
+ * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
*/
OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
var i, len, item;
/**
* Add items.
*
- * When items are added with the same values as existing items, the existing items will be
- * automatically removed before the new items are added.
- *
* @param {OO.ui.OptionWidget[]} items Items to add
* @param {number} [index] Index to insert items after
* @fires add
* @chainable
*/
OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
- var i, len, item, hash,
- remove = [];
-
- for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
- hash = OO.getHash( item.getData() );
- if ( hash in this.hashes ) {
- // Remove item with same value
- remove.push( this.hashes[hash] );
- }
- this.hashes[hash] = item;
- }
- if ( remove.length ) {
- this.removeItems( remove );
- }
-
// Mixin method
OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
* @chainable
*/
OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
- var i, len, item, hash;
+ var i, len, item;
+ // Deselect items being removed
for ( i = 0, len = items.length; i < len; i++ ) {
item = items[i];
- hash = OO.getHash( item.getData() );
- if ( hash in this.hashes ) {
- // Remove existing item
- delete this.hashes[hash];
- }
if ( item.isSelected() ) {
this.selectItem( null );
}
OO.ui.SelectWidget.prototype.clearItems = function () {
var items = this.items.slice();
- // Clear all items
- this.hashes = {};
// Mixin method
OO.ui.GroupWidget.prototype.clearItems.call( this );
+
+ // Clear selection
this.selectItem( null );
this.emit( 'remove', items );
OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
+/**
+ * Select widget containing radio button options.
+ *
+ * Use together with OO.ui.RadioOptionWidget.
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
+ // Parent constructor
+ OO.ui.RadioSelectWidget.super.call( this, config );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-radioSelectWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
+
/**
* Overlaid menu of options.
*
* Menus are clipped to the visible viewport. They do not provide a control for opening or closing
* the menu.
*
- * Use together with OO.ui.MenuItemWidget.
+ * Use together with OO.ui.MenuOptionWidget.
*
* @class
* @extends OO.ui.SelectWidget
* @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.MenuWidget = function OoUiMenuWidget( config ) {
- // Config intialization
+OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
+ // Configuration initialization
config = config || {};
// Parent constructor
- OO.ui.MenuWidget.super.call( this, config );
+ OO.ui.MenuSelectWidget.super.call( this, config );
// Mixin constructors
OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
this.$element
.hide()
.attr( 'role', 'menu' )
- .addClass( 'oo-ui-menuWidget' );
+ .addClass( 'oo-ui-menuSelectWidget' );
};
/* Setup */
-OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget );
-OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
+OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement );
/* Methods */
*
* @param {jQuery.Event} e Key down event
*/
-OO.ui.MenuWidget.prototype.onDocumentMouseDown = function ( e ) {
- if ( !$.contains( this.$element[0], e.target ) && ( !this.$widget || !$.contains( this.$widget[0], e.target ) ) ) {
+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 ) )
+ ) {
this.toggle( false );
}
};
*
* @param {jQuery.Event} e Key down event
*/
-OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
+OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
var nextItem,
handled = false,
highlightItem = this.getHighlightedItem();
/**
* Bind key down listener.
*/
-OO.ui.MenuWidget.prototype.bindKeyDownListener = function () {
+OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
if ( this.$input ) {
this.$input.on( 'keydown', this.onKeyDownHandler );
} else {
/**
* Unbind key down listener.
*/
-OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
+OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
if ( this.$input ) {
this.$input.off( 'keydown' );
} else {
* @param {OO.ui.OptionWidget} item Item to choose
* @chainable
*/
-OO.ui.MenuWidget.prototype.chooseItem = function ( item ) {
+OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
var widget = this;
// Parent method
- OO.ui.MenuWidget.super.prototype.chooseItem.call( this, item );
+ OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
if ( item && !this.flashing ) {
this.flashing = true;
/**
* @inheritdoc
*/
-OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
+OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
var i, len, item;
// Parent method
- OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index );
+ OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
// Auto-initialize
if ( !this.newItems ) {
/**
* @inheritdoc
*/
-OO.ui.MenuWidget.prototype.removeItems = function ( items ) {
+OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
// Parent method
- OO.ui.MenuWidget.super.prototype.removeItems.call( this, items );
+ OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
// Reevaluate clipping
this.clip();
/**
* @inheritdoc
*/
-OO.ui.MenuWidget.prototype.clearItems = function () {
+OO.ui.MenuSelectWidget.prototype.clearItems = function () {
// Parent method
- OO.ui.MenuWidget.super.prototype.clearItems.call( this );
+ OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
// Reevaluate clipping
this.clip();
/**
* @inheritdoc
*/
-OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
+OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
var i, len,
widgetDoc = this.$widget ? this.$widget[0].ownerDocument : null;
// Parent method
- OO.ui.MenuWidget.super.prototype.toggle.call( this, visible );
+ OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
if ( change ) {
if ( visible ) {
* menu is toggled or the window is resized.
*
* @class
- * @extends OO.ui.MenuWidget
+ * @extends OO.ui.MenuSelectWidget
*
* @constructor
* @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
* @param {Object} [config] Configuration options
* @cfg {jQuery} [$container=input.$element] Element to render menu under
*/
-OO.ui.TextInputMenuWidget = function OoUiTextInputMenuWidget( input, config ) {
+OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( input, config ) {
+ // Configuration initialization
+ config = config || {};
+
// Parent constructor
- OO.ui.TextInputMenuWidget.super.call( this, config );
+ OO.ui.TextInputMenuSelectWidget.super.call( this, config );
// Properties
this.input = input;
this.onWindowResizeHandler = this.onWindowResize.bind( this );
// Initialization
- this.$element.addClass( 'oo-ui-textInputMenuWidget' );
+ this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
};
/* Setup */
-OO.inheritClass( OO.ui.TextInputMenuWidget, OO.ui.MenuWidget );
+OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
/* Methods */
*
* @param {jQuery.Event} e Window resize event
*/
-OO.ui.TextInputMenuWidget.prototype.onWindowResize = function () {
+OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
this.position();
};
/**
* @inheritdoc
*/
-OO.ui.TextInputMenuWidget.prototype.toggle = function ( visible ) {
+OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
visible = visible === undefined ? !this.isVisible() : !!visible;
var change = visible !== this.isVisible();
}
// Parent method
- OO.ui.TextInputMenuWidget.super.prototype.toggle.call( this, visible );
+ OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible );
if ( change ) {
if ( this.isVisible() ) {
*
* @chainable
*/
-OO.ui.TextInputMenuWidget.prototype.position = function () {
+OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
var $container = this.$container,
- pos = OO.ui.Element.getRelativePosition( $container, this.$element.offsetParent() );
+ pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
// Position under input
pos.top += $container.height();
/**
* Structured list of items.
*
- * Use with OO.ui.OutlineItemWidget.
+ * Use with OO.ui.OutlineOptionWidget.
*
* @class
* @extends OO.ui.SelectWidget
* @constructor
* @param {Object} [config] Configuration options
*/
-OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) {
- // Config intialization
+OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
+ // Configuration initialization
config = config || {};
// Parent constructor
- OO.ui.OutlineWidget.super.call( this, config );
+ OO.ui.OutlineSelectWidget.super.call( this, config );
// Initialization
- this.$element.addClass( 'oo-ui-outlineWidget' );
+ this.$element.addClass( 'oo-ui-outlineSelectWidget' );
};
/* Setup */
-OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget );
+OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
/**
* Switch that slides on and off.