/*!
- * OOjs UI v0.2.4
+ * OOjs UI v0.6.4
* https://www.mediawiki.org/wiki/OOjs_UI
*
- * Copyright 2011–2014 OOjs Team and other contributors.
+ * Copyright 2011–2015 OOjs Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2014-12-02T18:45:19Z
+ * Date: 2015-01-31T01:15:57Z
*/
( function ( OO ) {
var i, len, langs;
// Requested language
- if ( obj[lang] ) {
- return obj[lang];
+ if ( obj[ lang ] ) {
+ return obj[ lang ];
}
// Known user language
langs = OO.ui.getUserLanguages();
for ( i = 0, len = langs.length; i < len; i++ ) {
- lang = langs[i];
- if ( obj[lang] ) {
- return obj[lang];
+ lang = langs[ i ];
+ if ( obj[ lang ] ) {
+ return obj[ lang ];
}
}
// Fallback language
- if ( obj[fallback] ) {
- return obj[fallback];
+ if ( obj[ fallback ] ) {
+ return obj[ fallback ];
}
// First existing language
for ( lang in obj ) {
- return obj[lang];
+ return obj[ lang ];
}
return undefined;
containers = [ containers ];
}
for ( i = containers.length - 1; i >= 0; i-- ) {
- if ( ( matchContainers && contained === containers[i] ) || $.contains( containers[i], contained ) ) {
+ if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
return true;
}
}
* @return {string} Translated message with parameters substituted
*/
OO.ui.msg = function ( key ) {
- var message = messages[key], params = Array.prototype.slice.call( arguments, 1 );
+ var message = messages[ key ],
+ params = Array.prototype.slice.call( arguments, 1 );
if ( typeof message === 'string' ) {
// Perform $1 substitution
message = message.replace( /\$(\d+)/g, function ( unused, n ) {
var i = parseInt( n, 10 );
- return params[i - 1] !== undefined ? params[i - 1] : '$' + n;
+ return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
} );
} else {
// Return placeholder if message not found
var flag;
for ( flag in this.special ) {
- if ( action === this.special[flag] ) {
+ if ( action === this.special[ flag ] ) {
return true;
}
}
// Collect category candidates
matches = [];
for ( category in this.categorized ) {
- list = filters[category];
+ list = filters[ category ];
if ( list ) {
if ( !Array.isArray( list ) ) {
list = [ list ];
}
for ( i = 0, len = list.length; i < len; i++ ) {
- actions = this.categorized[category][list[i]];
+ actions = this.categorized[ category ][ list[ i ] ];
if ( Array.isArray( actions ) ) {
matches.push.apply( matches, actions );
}
}
// Remove by boolean filters
for ( i = 0, len = matches.length; i < len; i++ ) {
- match = matches[i];
+ match = matches[ i ];
if (
( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
}
// Remove duplicates
for ( i = 0, len = matches.length; i < len; i++ ) {
- match = matches[i];
+ match = matches[ i ];
index = matches.lastIndexOf( match );
while ( index !== i ) {
matches.splice( index, 1 );
this.changing = true;
for ( i = 0, len = this.list.length; i < len; i++ ) {
- action = this.list[i];
+ action = this.list[ i ];
action.toggle( action.hasMode( mode ) );
}
var i, len, action, item;
for ( i = 0, len = this.list.length; i < len; i++ ) {
- item = this.list[i];
+ item = this.list[ i ];
action = item.getAction();
- if ( actions[action] !== undefined ) {
- item.setDisabled( !actions[action] );
+ if ( actions[ action ] !== undefined ) {
+ item.setDisabled( !actions[ action ] );
}
}
this.changing = true;
for ( i = 0, len = actions.length; i < len; i++ ) {
- action = actions[i];
+ action = actions[ i ];
action.connect( this, {
click: [ 'emit', 'click', action ],
resize: [ 'emit', 'resize', action ],
this.changing = true;
for ( i = 0, len = actions.length; i < len; i++ ) {
- action = actions[i];
+ action = actions[ i ];
index = this.list.indexOf( action );
if ( index !== -1 ) {
action.disconnect( this );
this.changing = true;
for ( i = 0, len = this.list.length; i < len; i++ ) {
- action = this.list[i];
+ action = this.list[ i ];
action.disconnect( this );
}
this.special = {};
this.others = [];
for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
- action = this.list[i];
+ action = this.list[ i ];
if ( action.isVisible() ) {
// Populate categories
for ( category in this.categories ) {
- if ( !this.categorized[category] ) {
- this.categorized[category] = {};
+ if ( !this.categorized[ category ] ) {
+ this.categorized[ category ] = {};
}
- list = action[this.categories[category]]();
+ list = action[ this.categories[ category ] ]();
if ( !Array.isArray( list ) ) {
list = [ list ];
}
for ( j = 0, jLen = list.length; j < jLen; j++ ) {
- item = list[j];
- if ( !this.categorized[category][item] ) {
- this.categorized[category][item] = [];
+ item = list[ j ];
+ if ( !this.categorized[ category ][ item ] ) {
+ this.categorized[ category ][ item ] = [];
}
- this.categorized[category][item].push( action );
+ this.categorized[ category ][ item ].push( action );
}
}
// Populate special/others
special = false;
for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
- flag = specialFlags[j];
- if ( !this.special[flag] && action.hasFlag( flag ) ) {
- this.special[flag] = action;
+ flag = specialFlags[ j ];
+ if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
+ this.special[ flag ] = action;
special = true;
break;
}
* @param {Object} [config] Configuration options
* @cfg {Function} [$] jQuery for the frame the widget is in
* @cfg {string[]} [classes] CSS class names to add
+ * @cfg {string} [id] HTML id attribute
* @cfg {string} [text] Text to insert
* @cfg {jQuery} [$content] Content elements to append (after text)
* @cfg {Mixed} [data] Element data
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;
if ( $.isArray( config.classes ) ) {
this.$element.addClass( config.classes.join( ' ' ) );
}
+ if ( config.id ) {
+ this.$element.attr( 'id', config.id );
+ }
if ( config.text ) {
this.$element.text( config.text );
}
* 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 ) ||
+ return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
// Empty jQuery selections might have a context
obj.context ||
// HTMLElement
* @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;
};
* @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
* @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 ) {
- obj = obj[0];
+ obj = obj[ 0 ];
}
isDoc = obj.nodeType === 9;
isWin = obj.document !== undefined;
* @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 ) {
// Get iframe element
frames = from.parent.document.getElementsByTagName( 'iframe' );
for ( i = 0, len = frames.length; i < len; i++ ) {
- if ( frames[i].contentWindow === from ) {
- frame = frames[i];
+ if ( frames[ i ].contentWindow === from ) {
+ frame = frames[ i ];
break;
}
}
* @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 ?
right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
return {
- top: Math.round( top ),
- left: Math.round( left ),
- bottom: Math.round( bottom ),
- right: Math.round( right )
+ top: top,
+ left: left,
+ bottom: bottom,
+ right: right
};
};
* @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 ) {
- return $parent[0];
+ if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
+ return $parent[ 0 ];
}
i = props.length;
while ( i-- ) {
- val = $parent.css( props[i] );
+ val = $parent.css( props[ i ] );
if ( val === 'auto' || val === 'scroll' ) {
- return $parent[0];
+ return $parent[ 0 ];
}
}
$parent = $parent.parent();
* 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,
};
/**
- * Bind a handler for an event on a DOM element.
- *
- * Used to be for working around a jQuery bug (jqbug.com/14180),
- * but obsolete as of jQuery 1.11.0.
+ * Force the browser to reconsider whether it really needs to render scrollbars inside the element
+ * and reserve space for them, because it probably doesn't.
*
- * @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
- */
-OO.ui.Element.onDOMEvent = function ( el, event, callback ) {
- $( el ).on( event, callback );
-};
-
-/**
- * Unbind a handler bound with #static-method-onDOMEvent.
+ * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
+ * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
+ * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
+ * and then reattach (or show) them back.
*
- * @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 {HTMLElement} el Element to reconsider the scrollbars on
*/
-OO.ui.Element.offDOMEvent = function ( el, event, callback ) {
- $( el ).off( event, callback );
+OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
+ var i, len, nodes = [];
+ // Detach all children
+ while ( el.firstChild ) {
+ nodes.push( el.firstChild );
+ el.removeChild( el.firstChild );
+ }
+ // Force reflow
+ void el.offsetHeight;
+ // Reattach all children
+ for ( i = 0, len = nodes.length; i < len; i++ ) {
+ el.appendChild( nodes[ i ] );
+ }
};
/* Methods */
methods = $.isArray( methods ) ? methods : [ methods ];
for ( i = 0, len = methods.length; i < len; i++ ) {
- if ( $.isFunction( this[methods[i]] ) ) {
+ if ( $.isFunction( this[ methods[ i ] ] ) ) {
support++;
}
}
* @return {boolean} The element is attached to the DOM
*/
OO.ui.Element.prototype.isElementAttached = function () {
- return $.contains( this.getElementDocument(), this.$element[0] );
+ return $.contains( this.getElementDocument(), this.$element[ 0 ] );
};
/**
* @return {HTMLDocument} Document object
*/
OO.ui.Element.prototype.getElementDocument = function () {
- 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 ] );
};
/**
* @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 );
};
/**
if ( isDisabled !== this.wasDisabled ) {
this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
+ this.$element.attr( 'aria-disabled', isDisabled.toString() );
this.emit( 'disable', isDisabled );
this.updateThemeClasses();
}
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to
- * use #static-size
+ * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large`, `larger` or
+ * `full`; omit to use #static-size
*/
OO.ui.Window = function OoUiWindow( config ) {
// Configuration initialization
deferred = $.Deferred();
for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
- styleNode = parentDoc.styleSheets[i].ownerNode;
+ styleNode = parentDoc.styleSheets[ i ].ownerNode;
if ( styleNode.disabled ) {
continue;
}
// Internal stylesheet; just copy the text
// For IE10 we need to fall back to .cssText, BUT that's undefined in
// other browsers, so fall back to '' rather than 'undefined'
- styleText = styleNode.textContent || parentDoc.styleSheets[i].cssText || '';
+ styleText = styleNode.textContent || parentDoc.styleSheets[ i ].cssText || '';
}
// Create a node with a unique ID that we're going to monitor to see when the CSS
*/
OO.ui.Window.prototype.onMouseDown = function ( e ) {
// Prevent clicking on the click-block from stealing focus
- if ( e.target === this.$element[0] ) {
+ if ( e.target === this.$element[ 0 ] ) {
return false;
}
};
/**
* Get the window size.
*
- * @return {string} Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
+ * @return {string} Symbolic size name, e.g. `small`, `medium`, `large`, `larger`, `full`
*/
OO.ui.Window.prototype.getSize = function () {
return this.size;
// Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
// Disable transitions first, otherwise we'll get values from when the window was animating.
var oldTransition,
- styleObj = this.$frame[0].style;
+ styleObj = this.$frame[ 0 ].style;
oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
styleObj.MozTransition || styleObj.WebkitTransition;
styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
OO.ui.Window.prototype.getContentHeight = function () {
var bodyHeight,
win = this,
- styleObj = this.$frame[0].style;
+ bodyStyleObj = this.$body[ 0 ].style,
+ frameStyleObj = this.$frame[ 0 ].style;
// Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
// Disable transitions first, otherwise we'll get values from when the window was animating.
this.withoutSizeTransitions( function () {
- var oldHeight = styleObj.height;
- styleObj.height = '1px';
+ var oldHeight = frameStyleObj.height,
+ oldPosition = bodyStyleObj.position;
+ frameStyleObj.height = '1px';
+ // Force body to resize to new width
+ bodyStyleObj.position = 'relative';
bodyHeight = win.getBodyHeight();
- styleObj.height = oldHeight;
+ frameStyleObj.height = oldHeight;
+ bodyStyleObj.position = oldPosition;
} );
- return Math.round(
+ return (
// Add buffer for border
( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
// Use combined heights of children
* @return {number} Height of content
*/
OO.ui.Window.prototype.getBodyHeight = function () {
- return this.$body[0].scrollHeight;
+ return this.$body[ 0 ].scrollHeight;
};
/**
} 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;
};
*/
OO.ui.Window.prototype.setSize = function ( size ) {
this.size = size;
+ this.updateSize();
+ return this;
+};
+
+/**
+ * Update the window size.
+ *
+ * @chainable
+ */
+OO.ui.Window.prototype.updateSize = function () {
this.manager.updateWindowSize( this );
return this;
};
OO.ui.Window.prototype.setDimensions = function ( dim ) {
var height,
win = this,
- styleObj = this.$frame[0].style;
+ styleObj = this.$frame[ 0 ].style;
// Calculate the height we need to set using the correct width
if ( dim.height === undefined ) {
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 ) {
- $focus[0].blur();
+ $focus[ 0 ].blur();
}
// Force redraw by asking the browser to measure the elements' widths
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 );
// Initialization
- this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
+ this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[ 0 ] )
.always( function () {
// Initialize isolated windows
win.initialize();
OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
if ( e.which === OO.ui.Keys.ESCAPE ) {
this.close();
- return false;
+ e.preventDefault();
+ e.stopPropagation();
}
};
);
for ( i = 0, len = actions.length; i < len; i++ ) {
items.push(
- new OO.ui.ActionWidget( $.extend( { $: this.$ }, actions[i] ) )
+ new OO.ui.ActionWidget( $.extend( { $: this.$ }, actions[ i ] ) )
);
}
this.actions.add( items );
// Detach all actions that may have been previously attached
for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
- this.attachedActions[i].$element.detach();
+ this.attachedActions[ i ].$element.detach();
}
this.attachedActions = [];
};
large: {
width: 700
},
+ larger: {
+ width: 900
+ },
full: {
// These can be non-numeric because they are never used in calculations
width: '100%',
*
* @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();
}
};
var name;
for ( name in this.windows ) {
- if ( this.windows[name] === win ) {
+ if ( this.windows[ name ] === win ) {
return true;
}
}
*/
OO.ui.WindowManager.prototype.getWindow = function ( name ) {
var deferred = $.Deferred(),
- win = this.windows[name];
+ win = this.windows[ name ];
if ( !( win instanceof OO.ui.Window ) ) {
if ( this.factory ) {
// Argument handling
if ( typeof win === 'string' ) {
- win = this.windows[win];
+ win = this.windows[ win ];
} else if ( !this.hasWindow( win ) ) {
win = null;
}
// Convert to map of windows by looking up symbolic names from static configuration
list = {};
for ( i = 0, len = windows.length; i < len; i++ ) {
- name = windows[i].constructor.static.name;
+ name = windows[ i ].constructor.static.name;
if ( typeof name !== 'string' ) {
throw new Error( 'Cannot add window' );
}
- list[name] = windows[i];
+ list[ name ] = windows[ i ];
}
} else if ( $.isPlainObject( windows ) ) {
list = windows;
// Add windows
for ( name in list ) {
- win = list[name];
- this.windows[name] = win;
+ win = list[ name ];
+ this.windows[ name ] = win;
this.$element.append( win.$element );
}
};
*
* Windows will be closed before they are removed.
*
- * @param {string} name Symbolic name of window to remove
+ * @param {string[]} names Symbolic names of windows to remove
* @return {jQuery.Promise} Promise resolved when window is closed and removed
* @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 ) {
- delete manager.windows[name];
+ delete manager.windows[ name ];
win.$element.detach();
};
for ( i = 0, len = names.length; i < len; i++ ) {
- name = names[i];
- win = this.windows[name];
+ name = names[ i ];
+ win = this.windows[ name ];
if ( !win ) {
throw new Error( 'Cannot remove window' );
}
- 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();
- if ( !sizes[size] ) {
+ if ( !sizes[ size ] ) {
size = this.constructor.static.defaultSize;
}
- if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[size].width ) {
+ if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
size = 'full';
}
this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
- win.setDimensions( sizes[size] );
+ win.setDimensions( sizes[ size ] );
this.emit( 'resize', win );
// 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;
}
/**
* Destroy window manager.
- *
- * Windows will not be closed, only removed from the DOM.
*/
OO.ui.WindowManager.prototype.destroy = function () {
this.toggleGlobalEvents( false );
this.toggleAriaIsolation( false );
+ this.clearWindows();
this.$element.remove();
};
/**
- * @abstract
* @class
*
* @constructor
// Use rejected promise for error
return $.Deferred().reject( [ result ] ).promise();
}
- if ( $.isArray( result ) && result.length && result[0] instanceof OO.ui.Error ) {
+ if ( $.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
// Use rejected promise for list of errors
return $.Deferred().reject( result ).promise();
}
if ( this.steps.length ) {
// Generate a chain reaction of promises
- promise = proceed( this.steps[0] )();
+ promise = proceed( this.steps[ 0 ] )();
for ( i = 1, len = this.steps.length; i < len; i++ ) {
- promise = promise.then( proceed( this.steps[i] ) );
+ promise = promise.then( proceed( this.steps[ i ] ) );
}
} else {
promise = $.Deferred().resolve().promise();
// Auto
for ( i = 0, len = included.length; i < len; i++ ) {
- if ( !used[included[i]] ) {
- auto.push( included[i] );
+ if ( !used[ included[ i ] ] ) {
+ auto.push( included[ i ] );
}
}
if ( collection === '*' ) {
for ( name in this.registry ) {
- tool = this.registry[name];
+ tool = this.registry[ name ];
if (
// Only add tools by group name when auto-add is enabled
tool.static.autoAddToCatchall &&
// Exclude already used tools
- ( !used || !used[name] )
+ ( !used || !used[ name ] )
) {
names.push( name );
if ( used ) {
- used[name] = true;
+ used[ name ] = true;
}
}
}
} else if ( $.isArray( collection ) ) {
for ( i = 0, len = collection.length; i < len; i++ ) {
- item = collection[i];
+ item = collection[ i ];
// Allow plain strings as shorthand for named tools
if ( typeof item === 'string' ) {
item = { name: item };
if ( OO.isPlainObject( item ) ) {
if ( item.group ) {
for ( name in this.registry ) {
- tool = this.registry[name];
+ tool = this.registry[ name ];
if (
// Include tools with matching group
tool.static.group === item.group &&
// Only add tools by group name when auto-add is enabled
tool.static.autoAddToGroup &&
// Exclude already used tools
- ( !used || !used[name] )
+ ( !used || !used[ name ] )
) {
names.push( name );
if ( used ) {
- used[name] = true;
+ used[ name ] = true;
}
}
}
// Include tools with matching name and exclude already used tools
- } else if ( item.name && ( !used || !used[item.name] ) ) {
+ } else if ( item.name && ( !used || !used[ item.name ] ) ) {
names.push( item.name );
if ( used ) {
- used[item.name] = true;
+ used[ item.name ] = true;
}
}
}
// Register default toolgroups
for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
- this.register( defaultClasses[i] );
+ this.register( defaultClasses[ i ] );
}
};
.addClass( classes.on.join( ' ' ) );
};
+/**
+ * Element supporting "sequential focus navigation" using the 'tabindex' attribute.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element
+ * @cfg {number|Function} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
+ * prevent tab focusing. (default: 0)
+ */
+OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$tabIndexed = null;
+ this.tabIndex = null;
+
+ // Initialization
+ this.setTabIndex( config.tabIndex || 0 );
+ this.setTabIndexedElement( config.$tabIndexed || this.$element );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.TabIndexedElement );
+
+/* Methods */
+
+/**
+ * Set the element with 'tabindex' attribute.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $tabIndexed Element to set tab index on
+ */
+OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
+ if ( this.$tabIndexed ) {
+ this.$tabIndexed.removeAttr( 'tabindex' );
+ }
+
+ this.$tabIndexed = $tabIndexed;
+ if ( this.tabIndex !== null ) {
+ this.$tabIndexed.attr( 'tabindex', this.tabIndex );
+ }
+};
+
+/**
+ * Set tab index value.
+ *
+ * @param {number|null} tabIndex Tab index value or null for no tabIndex
+ * @chainable
+ */
+OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
+ tabIndex = typeof tabIndex === 'number' && tabIndex >= 0 ? tabIndex : null;
+
+ if ( this.tabIndex !== tabIndex ) {
+ if ( this.$tabIndexed ) {
+ if ( tabIndex !== null ) {
+ this.$tabIndexed.attr( 'tabindex', tabIndex );
+ } else {
+ this.$tabIndexed.removeAttr( 'tabindex' );
+ }
+ }
+ this.tabIndex = tabIndex;
+ }
+
+ return this;
+};
+
+/**
+ * Get tab index value.
+ *
+ * @return {number} Tab index value
+ */
+OO.ui.TabIndexedElement.prototype.getTabIndex = function () {
+ return this.tabIndex;
+};
+
/**
* Element with a button.
*
* @param {Object} [config] Configuration options
* @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
* @cfg {boolean} [framed=true] Render button with a frame
- * @cfg {number} [tabIndex=0] Button's tab index. Use 0 to use default ordering, use -1 to prevent
- * tab focusing.
* @cfg {string} [accessKey] Button's access key
*/
OO.ui.ButtonElement = function OoUiButtonElement( config ) {
config = config || {};
// Properties
- this.$button = null;
+ this.$button = config.$button || this.$( '<a>' );
this.framed = null;
- this.tabIndex = null;
this.accessKey = null;
this.active = false;
this.onMouseUpHandler = this.onMouseUp.bind( this );
// Initialization
this.$element.addClass( 'oo-ui-buttonElement' );
this.toggleFramed( config.framed === undefined || config.framed );
- this.setTabIndex( config.tabIndex || 0 );
this.setAccessKey( config.accessKey );
- this.setButtonElement( config.$button || this.$( '<a>' ) );
+ this.setButtonElement( this.$button );
};
/* Setup */
if ( this.$button ) {
this.$button
.removeClass( 'oo-ui-buttonElement-button' )
- .removeAttr( 'role accesskey tabindex' )
- .off( this.onMouseDownHandler );
+ .removeAttr( 'role accesskey' )
+ .off( 'mousedown', this.onMouseDownHandler );
}
this.$button = $button
.addClass( 'oo-ui-buttonElement-button' )
- .attr( { role: 'button', accesskey: this.accessKey, tabindex: this.tabIndex } )
+ .attr( { role: 'button', accesskey: this.accessKey } )
.on( 'mousedown', this.onMouseDownHandler );
};
if ( this.isDisabled() || e.which !== 1 ) {
return false;
}
- // Remove the tab-index while the button is down to prevent the button from stealing focus
- this.$button.removeAttr( 'tabindex' );
this.$element.addClass( 'oo-ui-buttonElement-pressed' );
- // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
- // reliably reapply the tabindex and remove the pressed class
- this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
// Prevent change of focus unless specifically configured otherwise
if ( this.constructor.static.cancelButtonMouseDownEvents ) {
return false;
if ( this.isDisabled() || e.which !== 1 ) {
return false;
}
- // 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
- this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
};
/**
return this;
};
-/**
- * Set tab index.
- *
- * @param {number|null} tabIndex Button's tab index, use null to remove
- * @chainable
- */
-OO.ui.ButtonElement.prototype.setTabIndex = function ( tabIndex ) {
- tabIndex = typeof tabIndex === 'number' && tabIndex >= 0 ? tabIndex : null;
-
- if ( this.tabIndex !== tabIndex ) {
- if ( this.$button ) {
- if ( tabIndex !== null ) {
- this.$button.attr( 'tabindex', tabIndex );
- } else {
- this.$button.removeAttr( 'tabindex' );
- }
- }
- this.tabIndex = tabIndex;
- }
-
- return this;
-};
-
/**
* Set access key.
*
this.$group = $group;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.$group.append( this.items[i].$element );
+ this.$group.append( this.items[ i ].$element );
}
};
hash = OO.getHash( data );
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( hash === OO.getHash( item.getData() ) ) {
return item;
}
items = [];
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( hash === OO.getHash( item.getData() ) ) {
items.push( item );
}
var i, len, item, add, remove, itemEvent, groupEvent;
for ( itemEvent in events ) {
- groupEvent = events[itemEvent];
+ groupEvent = events[ itemEvent ];
// Remove existing aggregated event
if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
}
// Remove event aggregation from existing items
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( item.connect && item.disconnect ) {
remove = {};
- remove[itemEvent] = [ 'emit', groupEvent, item ];
+ remove[ itemEvent ] = [ 'emit', groupEvent, item ];
item.disconnect( this, remove );
}
}
// Prevent future items from aggregating event
- delete this.aggregateItemEvents[itemEvent];
+ delete this.aggregateItemEvents[ itemEvent ];
}
// Add new aggregate event
if ( groupEvent ) {
// Make future items aggregate event
- this.aggregateItemEvents[itemEvent] = groupEvent;
+ this.aggregateItemEvents[ itemEvent ] = groupEvent;
// Add event aggregation to existing items
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( item.connect && item.disconnect ) {
add = {};
- add[itemEvent] = [ 'emit', groupEvent, item ];
+ add[ itemEvent ] = [ 'emit', groupEvent, item ];
item.connect( this, add );
}
}
itemElements = [];
for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
+ item = items[ i ];
// Check if item exists then remove it first, effectively "moving" it
currentIndex = $.inArray( item, this.items );
if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
events = {};
for ( event in this.aggregateItemEvents ) {
- events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
+ events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
}
item.connect( this, events );
}
this.$group.prepend( itemElements );
this.items.unshift.apply( this.items, items );
} else {
- this.items[index].$element.before( itemElements );
+ this.items[ index ].$element.before( itemElements );
this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
}
// Remove specific items
for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
+ item = items[ i ];
index = $.inArray( item, this.items );
if ( index !== -1 ) {
if (
) {
remove = {};
if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
- remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
+ remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
}
item.disconnect( this, remove );
}
// Remove all items
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if (
item.connect && item.disconnect &&
!$.isEmptyObject( this.aggregateItemEvents )
) {
remove = {};
if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
- remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
+ remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
}
item.disconnect( this, remove );
}
};
/**
- * 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 ) {
- // Configuration initialization
- 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 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
+ * 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 item index
+ * @return {number} Item index
+ */
+OO.ui.DraggableElement.prototype.getIndex = function () {
+ return this.index;
};
/**
- * Get icon name.
+ * Element containing a sequence of child elements that can be dragged
+ * and dropped.
*
- * @return {string} Icon name
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @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.IconElement.prototype.getIcon = function () {
- return this.icon;
+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 */
+
/**
- * Element containing an indicator.
+ * @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.
*
- * 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.
+ * 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} [$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
+ * @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.IndicatorElement = function OoUiIndicatorElement( config ) {
+OO.ui.IconElement = function OoUiIconElement( config ) {
// Configuration initialization
config = config || {};
// Properties
- this.$indicator = null;
- this.indicator = null;
- this.indicatorTitle = null;
+ this.$icon = null;
+ this.icon = null;
+ this.iconTitle = null;
// Initialization
- this.setIndicator( config.indicator || this.constructor.static.indicator );
- this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
- this.setIndicatorElement( config.$indicator || this.$( '<span>' ) );
+ 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.IndicatorElement );
+OO.initClass( OO.ui.IconElement );
/* Static Properties */
/**
- * indicator.
+ * 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 {string|null} Symbolic indicator name
+ * @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.IndicatorElement.static.indicator = null;
+OO.ui.IconElement.static.icon = null;
/**
- * Indicator title.
+ * Icon title.
*
* @static
* @inheritable
- * @property {string|Function|null} Indicator title text, a function that returns text or null for no
- * indicator title
+ * @property {string|Function|null} Icon title text, a function that returns text or null for no
+ * icon title
*/
-OO.ui.IndicatorElement.static.indicatorTitle = null;
+OO.ui.IconElement.static.iconTitle = null;
/* Methods */
/**
- * Set the indicator element.
+ * Set the icon element.
*
* If an element is already set, it will be cleaned up before setting up the new element.
*
- * @param {jQuery} $indicator Element to use as indicator
+ * @param {jQuery} $icon Element to use as icon
*/
-OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
- if ( this.$indicator ) {
- this.$indicator
- .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
+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.$indicator = $indicator
- .addClass( 'oo-ui-indicatorElement-indicator' )
- .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
- if ( this.indicatorTitle !== null ) {
- this.$indicatorTitle.attr( 'title', this.indicatorTitle );
+ 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 indicator name.
+ * Set icon name.
*
- * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
+ * @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.IndicatorElement.prototype.setIndicator = function ( indicator ) {
- indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
+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.indicator !== indicator ) {
- if ( this.$indicator ) {
- if ( this.indicator !== null ) {
- this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
+ if ( this.icon !== icon ) {
+ if ( this.$icon ) {
+ if ( this.icon !== null ) {
+ this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
}
- if ( indicator !== null ) {
- this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
+ if ( icon !== null ) {
+ this.$icon.addClass( 'oo-ui-icon-' + icon );
}
}
- this.indicator = indicator;
+ this.icon = icon;
}
- this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
+ this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
this.updateThemeClasses();
return this;
};
/**
- * Set indicator title.
+ * Set icon title.
*
- * @param {string|Function|null} indicator Indicator title text, a function that returns text or
- * null for no indicator title
+ * @param {string|Function|null} icon Icon title text, a function that returns text or null
+ * for no icon title
* @chainable
*/
-OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
- indicatorTitle = typeof indicatorTitle === 'function' ||
- ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
- OO.ui.resolveMsg( indicatorTitle ) : null;
+OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
+ iconTitle = typeof iconTitle === 'function' ||
+ ( typeof iconTitle === 'string' && iconTitle.length ) ?
+ OO.ui.resolveMsg( iconTitle ) : null;
- if ( this.indicatorTitle !== indicatorTitle ) {
- this.indicatorTitle = indicatorTitle;
- if ( this.$indicator ) {
- if ( this.indicatorTitle !== null ) {
- this.$indicator.attr( 'title', indicatorTitle );
+ if ( this.iconTitle !== iconTitle ) {
+ this.iconTitle = iconTitle;
+ if ( this.$icon ) {
+ if ( this.iconTitle !== null ) {
+ this.$icon.attr( 'title', iconTitle );
} else {
- this.$indicator.removeAttr( 'title' );
+ this.$icon.removeAttr( 'title' );
}
}
}
};
/**
- * Get indicator name.
+ * Get icon name.
*
- * @return {string} Symbolic name of indicator
+ * @return {string} Icon name
*/
-OO.ui.IndicatorElement.prototype.getIndicator = function () {
- return this.indicator;
+OO.ui.IconElement.prototype.getIcon = function () {
+ return this.icon;
};
/**
- * Get indicator title.
+ * Get icon title.
*
- * @return {string} Indicator title text
+ * @return {string} Icon title text
*/
-OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
- return this.indicatorTitle;
+OO.ui.IconElement.prototype.getIconTitle = function () {
+ return this.iconTitle;
};
/**
- * Element containing a label.
+ * 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} [$label] Label node, assigned to #$label, omit to use a generated `<span>`
- * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
- * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
- */
+ * @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 ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$indicator = null;
+ this.indicator = null;
+ this.indicatorTitle = null;
+
+ // Initialization
+ this.setIndicator( config.indicator || this.constructor.static.indicator );
+ this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
+ this.setIndicatorElement( config.$indicator || this.$( '<span>' ) );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.IndicatorElement );
+
+/* Static Properties */
+
+/**
+ * indicator.
+ *
+ * @static
+ * @inheritable
+ * @property {string|null} Symbolic indicator name
+ */
+OO.ui.IndicatorElement.static.indicator = null;
+
+/**
+ * Indicator title.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null} Indicator title text, a function that returns text or null for no
+ * indicator title
+ */
+OO.ui.IndicatorElement.static.indicatorTitle = null;
+
+/* Methods */
+
+/**
+ * Set the indicator element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $indicator Element to use as indicator
+ */
+OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
+ if ( this.$indicator ) {
+ this.$indicator
+ .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
+ .removeAttr( 'title' );
+ }
+
+ this.$indicator = $indicator
+ .addClass( 'oo-ui-indicatorElement-indicator' )
+ .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
+ if ( this.indicatorTitle !== null ) {
+ this.$indicator.attr( 'title', this.indicatorTitle );
+ }
+};
+
+/**
+ * Set indicator name.
+ *
+ * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
+ * @chainable
+ */
+OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) {
+ indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
+
+ if ( this.indicator !== indicator ) {
+ if ( this.$indicator ) {
+ if ( this.indicator !== null ) {
+ this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
+ }
+ if ( indicator !== null ) {
+ this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
+ }
+ }
+ this.indicator = indicator;
+ }
+
+ this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
+ this.updateThemeClasses();
+
+ return this;
+};
+
+/**
+ * Set indicator title.
+ *
+ * @param {string|Function|null} indicator Indicator title text, a function that returns text or
+ * null for no indicator title
+ * @chainable
+ */
+OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
+ indicatorTitle = typeof indicatorTitle === 'function' ||
+ ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
+ OO.ui.resolveMsg( indicatorTitle ) : null;
+
+ if ( this.indicatorTitle !== indicatorTitle ) {
+ this.indicatorTitle = indicatorTitle;
+ if ( this.$indicator ) {
+ if ( this.indicatorTitle !== null ) {
+ this.$indicator.attr( 'title', indicatorTitle );
+ } else {
+ this.$indicator.removeAttr( 'title' );
+ }
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Get indicator name.
+ *
+ * @return {string} Symbolic name of indicator
+ */
+OO.ui.IndicatorElement.prototype.getIndicator = function () {
+ return this.indicator;
+};
+
+/**
+ * Get indicator title.
+ *
+ * @return {string} Indicator title text
+ */
+OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
+ return this.indicatorTitle;
+};
+
+/**
+ * Element containing a label.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$label] Label node, assigned to #$label, omit to use a generated `<span>`
+ * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
+ * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
+ */
OO.ui.LabelElement = function OoUiLabelElement( config ) {
// Configuration initialization
config = config || {};
OO.initClass( OO.ui.LabelElement );
+/* Events */
+
+/**
+ * @event labelChange
+ * @param {string} value
+ */
+
/* Static Properties */
/**
if ( this.$label ) {
this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
}
-
- this.$label = $label.addClass( 'oo-ui-labelElement-label' );
- this.setLabelContent( this.label );
+
+ this.$label = $label.addClass( 'oo-ui-labelElement-label' );
+ this.setLabelContent( this.label );
+};
+
+/**
+ * Set the label.
+ *
+ * An empty string will result in the label being hidden. A string containing only whitespace will
+ * 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
+ * @chainable
+ */
+OO.ui.LabelElement.prototype.setLabel = function ( label ) {
+ label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
+ label = ( typeof label === 'string' && label.length ) || label instanceof jQuery ? label : null;
+
+ this.$element.toggleClass( 'oo-ui-labelElement', !!label );
+
+ if ( this.label !== label ) {
+ if ( this.$label ) {
+ this.setLabelContent( label );
+ }
+ this.label = label;
+ this.emit( 'labelChange' );
+ }
+
+ return this;
+};
+
+/**
+ * Get the label.
+ *
+ * @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 () {
+ return this.label;
+};
+
+/**
+ * Fit the label.
+ *
+ * @chainable
+ */
+OO.ui.LabelElement.prototype.fitLabel = function () {
+ if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
+ this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
+ }
+
+ return this;
+};
+
+/**
+ * Set the content of the label.
+ *
+ * Do not call this method until after the label element has been set by #setLabelElement.
+ *
+ * @private
+ * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
+ * text; or null for no label
+ */
+OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
+ if ( typeof label === 'string' ) {
+ if ( label.match( /^\s*$/ ) ) {
+ // Convert whitespace only string to a single non-breaking space
+ this.$label.html( ' ' );
+ } else {
+ this.$label.text( label );
+ }
+ } else if ( label instanceof jQuery ) {
+ this.$label.empty().append( label );
+ } else {
+ this.$label.empty();
+ }
+};
+
+/**
+ * Mixin that adds a menu showing suggested values for a OO.ui.TextInputWidget.
+ *
+ * Subclasses that set the value of #lookupInput from #onLookupMenuItemChoose 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 {Object} [config] Configuration options
+ * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
+ * @cfg {jQuery} [$container=this.$element] Element to render menu under
+ */
+OO.ui.LookupElement = function OoUiLookupElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$overlay = config.$overlay || this.$element;
+ this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
+ $: OO.ui.Element.static.getJQuery( this.$overlay ),
+ $container: config.$container
+ } );
+ this.lookupCache = {};
+ this.lookupQuery = null;
+ this.lookupRequest = null;
+ this.lookupsDisabled = false;
+ this.lookupInputFocused = false;
+
+ // Events
+ this.$input.on( {
+ focus: this.onLookupInputFocus.bind( this ),
+ blur: this.onLookupInputBlur.bind( this ),
+ mousedown: this.onLookupInputMouseDown.bind( this )
+ } );
+ this.connect( this, { change: 'onLookupInputChange' } );
+ this.lookupMenu.connect( this, {
+ toggle: 'onLookupMenuToggle',
+ choose: 'onLookupMenuItemChoose'
+ } );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-lookupElement' );
+ this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
+ this.$overlay.append( this.lookupMenu.$element );
+};
+
+/* Methods */
+
+/**
+ * Handle input focus event.
+ *
+ * @param {jQuery.Event} e Input focus event
+ */
+OO.ui.LookupElement.prototype.onLookupInputFocus = function () {
+ this.lookupInputFocused = true;
+ this.populateLookupMenu();
+};
+
+/**
+ * Handle input blur event.
+ *
+ * @param {jQuery.Event} e Input blur event
+ */
+OO.ui.LookupElement.prototype.onLookupInputBlur = function () {
+ this.closeLookupMenu();
+ this.lookupInputFocused = false;
+};
+
+/**
+ * Handle input mouse down event.
+ *
+ * @param {jQuery.Event} e Input mouse down event
+ */
+OO.ui.LookupElement.prototype.onLookupInputMouseDown = function () {
+ // 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();
+ }
+};
+
+/**
+ * Handle input change event.
+ *
+ * @param {string} value New input value
+ */
+OO.ui.LookupElement.prototype.onLookupInputChange = function () {
+ if ( this.lookupInputFocused ) {
+ this.populateLookupMenu();
+ }
+};
+
+/**
+ * Handle the lookup menu being shown/hidden.
+ *
+ * @param {boolean} visible Whether the lookup menu is now visible.
+ */
+OO.ui.LookupElement.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();
+ }
+};
+
+/**
+ * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
+ *
+ * @param {OO.ui.MenuOptionWidget|null} item Selected item
+ */
+OO.ui.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
+ if ( item ) {
+ this.setValue( item.getData() );
+ }
+};
+
+/**
+ * Get lookup menu.
+ *
+ * @return {OO.ui.TextInputMenuSelectWidget}
+ */
+OO.ui.LookupElement.prototype.getLookupMenu = function () {
+ return this.lookupMenu;
+};
+
+/**
+ * Disable or re-enable lookups.
+ *
+ * When lookups are disabled, calls to #populateLookupMenu will be ignored.
+ *
+ * @param {boolean} disabled Disable lookups
+ */
+OO.ui.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
+ this.lookupsDisabled = !!disabled;
+};
+
+/**
+ * Open the menu. If there are no entries in the menu, this does nothing.
+ *
+ * @chainable
+ */
+OO.ui.LookupElement.prototype.openLookupMenu = function () {
+ if ( !this.lookupMenu.isEmpty() ) {
+ this.lookupMenu.toggle( true );
+ }
+ return this;
+};
+
+/**
+ * Close the menu, empty it, and abort any pending request.
+ *
+ * @chainable
+ */
+OO.ui.LookupElement.prototype.closeLookupMenu = function () {
+ this.lookupMenu.toggle( false );
+ this.abortLookupRequest();
+ this.lookupMenu.clearItems();
+ return this;
+};
+
+/**
+ * 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.LookupElement.prototype.populateLookupMenu = function () {
+ var widget = this,
+ value = this.getValue();
+
+ if ( this.lookupsDisabled ) {
+ return;
+ }
+
+ // 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();
+ if ( items.length ) {
+ widget.lookupMenu
+ .addItems( items )
+ .toggle( true );
+ widget.initializeLookupMenuSelection();
+ } else {
+ widget.lookupMenu.toggle( false );
+ }
+ } )
+ .fail( function () {
+ widget.lookupMenu.clearItems();
+ } );
+ }
+
+ return this;
+};
+
+/**
+ * Select and highlight the first selectable item in the menu.
+ *
+ * @chainable
+ */
+OO.ui.LookupElement.prototype.initializeLookupMenuSelection = function () {
+ if ( !this.lookupMenu.getSelectedItem() ) {
+ this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
+ }
+ this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
+};
+
+/**
+ * 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. 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.LookupElement.prototype.getLookupMenuItems = function () {
+ var widget = this,
+ value = this.getValue(),
+ deferred = $.Deferred(),
+ ourRequest;
+
+ this.abortLookupRequest();
+ if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
+ deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
+ } else {
+ this.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.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;
+ widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( data );
+ deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
+ }
+ } )
+ .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();
+ }
+ } );
+ }
+ return deferred.promise();
};
/**
- * Set the label.
- *
- * An empty string will result in the label being hidden. A string containing only whitespace will
- * 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
- * @chainable
+ * Abort the currently pending lookup request, if any.
*/
-OO.ui.LabelElement.prototype.setLabel = function ( label ) {
- label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
- label = ( typeof label === 'string' && label.length ) || label instanceof jQuery ? label : null;
-
- if ( this.label !== label ) {
- if ( this.$label ) {
- this.setLabelContent( label );
- }
- this.label = label;
+OO.ui.LookupElement.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();
}
-
- this.$element.toggleClass( 'oo-ui-labelElement', !!this.label );
-
- return this;
};
/**
- * Get the label.
+ * Get a new request object of the current lookup query value.
*
- * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
- * text; or null for no label
+ * @abstract
+ * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
*/
-OO.ui.LabelElement.prototype.getLabel = function () {
- return this.label;
+OO.ui.LookupElement.prototype.getLookupRequest = function () {
+ // Stub, implemented in subclass
+ return null;
};
/**
- * Fit the label.
+ * Pre-process data returned by the request from #getLookupRequest.
*
- * @chainable
+ * The return value of this function will be cached, and any further queries for the given value
+ * will use the cache rather than doing API requests.
+ *
+ * @abstract
+ * @param {Mixed} data Response from server
+ * @return {Mixed} Cached result data
*/
-OO.ui.LabelElement.prototype.fitLabel = function () {
- if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
- this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
- }
-
- return this;
+OO.ui.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
+ // Stub, implemented in subclass
+ return [];
};
/**
- * Set the content of the label.
- *
- * Do not call this method until after the label element has been set by #setLabelElement.
+ * Get a list of menu option widgets from the (possibly cached) data returned by
+ * #getLookupCacheDataFromResponse.
*
- * @private
- * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
- * text; or null for no label
+ * @abstract
+ * @param {Mixed} data Cached result data, usually an array
+ * @return {OO.ui.MenuOptionWidget[]} Menu items
*/
-OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
- if ( typeof label === 'string' ) {
- if ( label.match( /^\s*$/ ) ) {
- // Convert whitespace only string to a single non-breaking space
- this.$label.html( ' ' );
- } else {
- this.$label.text( label );
- }
- } else if ( label instanceof jQuery ) {
- this.$label.empty().append( label );
- } else {
- this.$label.empty();
- }
+OO.ui.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
+ // Stub, implemented in subclass
+ return [];
};
/**
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {string|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 ) {
for ( flag in this.flags ) {
className = classPrefix + flag;
- changes[flag] = false;
- delete this.flags[flag];
+ changes[ flag ] = false;
+ delete this.flags[ flag ];
remove.push( className );
}
if ( typeof flags === 'string' ) {
className = classPrefix + flags;
// Set
- if ( !this.flags[flags] ) {
- this.flags[flags] = true;
+ if ( !this.flags[ flags ] ) {
+ this.flags[ flags ] = true;
add.push( className );
}
} else if ( $.isArray( flags ) ) {
for ( i = 0, len = flags.length; i < len; i++ ) {
- flag = flags[i];
+ flag = flags[ i ];
className = classPrefix + flag;
// Set
- if ( !this.flags[flag] ) {
- changes[flag] = true;
- this.flags[flag] = true;
+ if ( !this.flags[ flag ] ) {
+ changes[ flag ] = true;
+ this.flags[ flag ] = true;
add.push( className );
}
}
} else if ( OO.isPlainObject( flags ) ) {
for ( flag in flags ) {
className = classPrefix + flag;
- if ( flags[flag] ) {
+ if ( flags[ flag ] ) {
// Set
- if ( !this.flags[flag] ) {
- changes[flag] = true;
- this.flags[flag] = true;
+ if ( !this.flags[ flag ] ) {
+ changes[ flag ] = true;
+ this.flags[ flag ] = true;
add.push( className );
}
} else {
// Remove
- if ( this.flags[flag] ) {
- changes[flag] = false;
- delete this.flags[flag];
+ if ( this.flags[ flag ] ) {
+ changes[ flag ] = false;
+ delete this.flags[ flag ];
remove.push( className );
}
}
OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
if ( this.$clippable ) {
this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
- this.$clippable.css( { width: '', height: '' } );
- this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
- this.$clippable.css( { overflowX: '', overflowY: '' } );
+ this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
}
this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
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() )
// Initial clip after visible
this.clip();
} else {
- this.$clippable.css( { width: '', height: '' } );
- this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
- this.$clippable.css( { overflowX: '', overflowY: '' } );
+ this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
this.$clippableContainer = null;
this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
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,
if ( clipWidth ) {
this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
} else {
- this.$clippable.css( 'width', this.idealWidth || '' );
- this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
- this.$clippable.css( 'overflowX', '' );
+ this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
}
if ( clipHeight ) {
this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
} else {
- this.$clippable.css( 'height', this.idealHeight || '' );
- this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
- this.$clippable.css( 'overflowY', '' );
+ this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
+ }
+
+ // If we stopped clipping in at least one of the dimensions
+ if ( !clipWidth || !clipHeight ) {
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
}
this.clippedHorizontally = clipWidth;
this.toolbar = this.toolGroup.getToolbar();
this.active = false;
this.$title = this.$( '<span>' );
- this.$titleText = this.$( '<span>' );
this.$accel = this.$( '<span>' );
this.$link = this.$( '<a>' );
this.title = null;
this.toolbar.connect( this, { updateState: 'onUpdateState' } );
// Initialization
- this.$titleText.addClass( 'oo-ui-tool-title-text' );
+ this.$title.addClass( 'oo-ui-tool-title' );
this.$accel
.addClass( 'oo-ui-tool-accel' )
.prop( {
dir: 'ltr',
lang: 'en'
} );
- this.$title
- .addClass( 'oo-ui-tool-title' )
- .append( this.$titleText, this.$accel );
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
accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
tooltipParts = [];
- this.$titleText.text( this.title );
+ this.$title.text( this.title );
this.$accel.text( accel );
if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ),
$closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
- if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) {
+ if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
return false;
}
};
/**
* Sets up handles and preloads required information for the toolbar to work.
- * This must be called immediately after it is attached to a visible document.
+ * This must be called after it is attached to a visible document and before doing anything else.
*/
OO.ui.Toolbar.prototype.initialize = function () {
this.initialized = true;
// Build out new groups
for ( i = 0, len = groups.length; i < len; i++ ) {
- group = groups[i];
+ group = groups[ i ];
if ( group.include === '*' ) {
// Apply defaults to catch-all groups
if ( group.type === undefined ) {
this.groups = [];
this.tools = {};
for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[i].destroy();
+ this.items[ i ].destroy();
}
this.clearItems();
};
* @return {boolean} Tool is available
*/
OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
- return !this.tools[name];
+ return !this.tools[ name ];
};
/**
* @param {OO.ui.Tool} tool Tool to reserve
*/
OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
- this.tools[tool.getName()] = tool;
+ this.tools[ tool.getName() ] = tool;
};
/**
* @param {OO.ui.Tool} tool Tool to release
*/
OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
- delete this.tools[tool.getName()];
+ delete this.tools[ tool.getName() ];
};
/**
if ( this.constructor.static.autoDisable ) {
for ( i = this.items.length - 1; i >= 0; i-- ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( !item.isDisabled() ) {
allDisabled = false;
break;
// Build a list of needed tools
for ( i = 0, len = list.length; i < len; i++ ) {
- name = list[i];
+ name = list[ i ];
if (
// Tool exists
toolFactory.lookup( name ) &&
// Tool is available or is already in this group
- ( this.toolbar.isToolAvailable( name ) || this.tools[name] )
+ ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
) {
- tool = this.tools[name];
+ tool = this.tools[ name ];
if ( !tool ) {
// Auto-initialize tools on first use
- this.tools[name] = tool = toolFactory.create( name, this );
+ this.tools[ name ] = tool = toolFactory.create( name, this );
tool.updateTitle();
}
this.toolbar.reserveTool( tool );
add.push( tool );
- names[name] = true;
+ names[ name ] = true;
}
}
// Remove tools that are no longer needed
for ( name in this.tools ) {
- if ( !names[name] ) {
- this.tools[name].destroy();
- this.toolbar.releaseTool( this.tools[name] );
- remove.push( this.tools[name] );
- delete this.tools[name];
+ if ( !names[ name ] ) {
+ this.tools[ name ].destroy();
+ this.toolbar.releaseTool( this.tools[ name ] );
+ remove.push( this.tools[ name ] );
+ delete this.tools[ name ];
}
}
if ( remove.length ) {
this.clearItems();
this.toolbar.getToolFactory().disconnect( this );
for ( name in this.tools ) {
- this.toolbar.releaseTool( this.tools[name] );
- this.tools[name].disconnect( this ).destroy();
- delete this.tools[name];
+ this.toolbar.releaseTool( this.tools[ name ] );
+ this.tools[ name ].disconnect( this ).destroy();
+ delete this.tools[ name ];
}
this.$element.remove();
};
var bodyHeight, oldOverflow,
$scrollable = this.container.$element;
- oldOverflow = $scrollable[0].style.overflow;
- $scrollable[0].style.overflow = 'hidden';
+ oldOverflow = $scrollable[ 0 ].style.overflow;
+ $scrollable[ 0 ].style.overflow = 'hidden';
- // Force… ugh… something to happen
- $scrollable.contents().hide();
- $scrollable.height();
- $scrollable.contents().show();
+ OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
- bodyHeight = Math.round( this.text.$element.outerHeight( true ) );
- $scrollable[0].style.overflow = oldOverflow;
+ bodyHeight = this.text.$element.outerHeight( true );
+ $scrollable[ 0 ].style.overflow = oldOverflow;
return bodyHeight;
};
// Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
// Need to do it after transition completes (250ms), add 50ms just in case.
setTimeout( function () {
- var oldOverflow = $scrollable[0].style.overflow;
- $scrollable[0].style.overflow = 'hidden';
+ var oldOverflow = $scrollable[ 0 ].style.overflow;
+ $scrollable[ 0 ].style.overflow = 'hidden';
- // Force… ugh… something to happen
- $scrollable.contents().hide();
- $scrollable.height();
- $scrollable.contents().show();
+ OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
- $scrollable[0].style.overflow = oldOverflow;
+ $scrollable[ 0 ].style.overflow = oldOverflow;
}, 300 );
return this;
}
if ( others.length ) {
for ( i = 0, len = others.length; i < len; i++ ) {
- other = others[i];
+ other = others[ i ];
this.$actions.append( other.$element );
other.toggleFramed( false );
}
if ( !this.isOpening() ) {
// If the dialog is currently opening, this will be called automatically soon.
// This also calls #fitActions.
- this.manager.updateWindowSize( this );
+ this.updateSize();
}
};
// Detect clipping
this.toggleVerticalActionLayout( false );
for ( i = 0, len = actions.length; i < len; i++ ) {
- action = actions[i];
+ action = actions[ i ];
if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
this.toggleVerticalActionLayout( true );
break;
if ( this.verticalActionLayout !== previous ) {
this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
// We changed the layout, window height might need to be updated.
- this.manager.updateWindowSize( this );
+ this.updateSize();
}
};
}
if ( others.length ) {
for ( i = 0, len = others.length; i < len; i++ ) {
- other = others[i];
+ other = others[ i ];
this.$otherActions.append( other.$element );
other.toggleFramed( true );
}
warning = false;
for ( i = 0, len = errors.length; i < len; i++ ) {
- if ( !errors[i].isRecoverable() ) {
+ if ( !errors[ i ].isRecoverable() ) {
recoverable = false;
}
- if ( errors[i].isWarning() ) {
+ if ( errors[ i ].isWarning() ) {
warning = true;
}
$item = this.$( '<div>' )
.addClass( 'oo-ui-processDialog-error' )
- .append( errors[i].getMessage() );
- items.push( $item[0] );
+ .append( errors[ i ].getMessage() );
+ items.push( $item[ 0 ] );
}
this.$errorItems = this.$( items );
if ( recoverable ) {
}
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
$target = $( e.target ).closest( '.oo-ui-pageLayout' );
for ( name in this.pages ) {
// Check for page match, exclude current page to find only page changes
- if ( this.pages[name].$element[0] === $target[0] && name !== this.currentPageName ) {
+ if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
this.setPage( name );
break;
}
if ( !page && this.outlined ) {
this.selectFirstSelectablePage();
page = this.stackLayout.getCurrentItem();
- if ( !page ) {
- return;
- }
+ }
+ 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();
+ $input[ 0 ].focus();
}
}
};
index = $.inArray( page, pages );
if ( index !== -1 ) {
- next = pages[index + 1];
- prev = pages[index - 1];
+ next = pages[ index + 1 ];
+ prev = pages[ index - 1 ];
// Prefer adjacent pages at the same level
if ( this.outlined ) {
level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
* @return {OO.ui.PageLayout|undefined} Page, if found
*/
OO.ui.BookletLayout.prototype.getPage = function ( name ) {
- return this.pages[name];
+ return this.pages[ name ];
};
/**
*
* @return {string|null} Current page name
*/
-OO.ui.BookletLayout.prototype.getPageName = function () {
+OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
return this.currentPageName;
};
// Remove pages with same names
for ( i = 0, len = pages.length; i < len; i++ ) {
- page = pages[i];
+ page = pages[ i ];
name = page.getName();
if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
// Correct the insertion index
- currentIndex = $.inArray( this.pages[name], stackLayoutPages );
+ currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
if ( currentIndex !== -1 && currentIndex + 1 < index ) {
index--;
}
- remove.push( this.pages[name] );
+ remove.push( this.pages[ name ] );
}
}
if ( remove.length ) {
// Add new pages
for ( i = 0, len = pages.length; i < len; i++ ) {
- page = pages[i];
+ page = pages[ i ];
name = page.getName();
- this.pages[page.getName()] = page;
+ this.pages[ page.getName() ] = page;
if ( this.outlined ) {
item = new OO.ui.OutlineOptionWidget( { $: this.$, data: name } );
page.setOutlineItem( item );
items = [];
for ( i = 0, len = pages.length; i < len; i++ ) {
- page = pages[i];
+ page = pages[ i ];
name = page.getName();
- delete this.pages[name];
+ delete this.pages[ name ];
if ( this.outlined ) {
items.push( this.outlineSelectWidget.getItemFromData( name ) );
page.setOutlineItem( null );
if ( this.outlined ) {
this.outlineSelectWidget.clearItems();
for ( i = 0, len = pages.length; i < len; i++ ) {
- pages[i].setOutlineItem( null );
+ pages[ i ].setOutlineItem( null );
}
}
this.stackLayout.clearItems();
OO.ui.BookletLayout.prototype.setPage = function ( name ) {
var selectedItem,
$focused,
- page = this.pages[name];
+ page = this.pages[ name ];
if ( name !== this.currentPageName ) {
if ( this.outlined ) {
}
}
if ( page ) {
- if ( this.currentPageName && this.pages[this.currentPageName] ) {
- this.pages[this.currentPageName].setActive( false );
+ if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
+ this.pages[ this.currentPageName ].setActive( false );
// Blur anything focused if the next page doesn't have anything focusable - this
// is not needed if the next page has something focusable because once it is focused
// this blur happens automatically
if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
- $focused = this.pages[this.currentPageName].$element.find( ':focus' );
+ $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
if ( $focused.length ) {
- $focused[0].blur();
+ $focused[ 0 ].blur();
}
}
}
* @cfg {string} [help] Explanatory text shown as a '?' icon.
*/
OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
+ var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
+
// Configuration initialization
config = $.extend( { align: 'left' }, config );
// Properties
this.$field = this.$( '<div>' );
+ 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 );
};
/* Methods */
-/**
- * @inheritdoc
- */
-OO.ui.FieldLayout.prototype.getTagName = function () {
- if ( this.fieldWidget instanceof OO.ui.InputWidget ) {
- return 'label';
- } else {
- return 'div';
- }
-};
-
/**
* Handle field disable events.
*
}
// 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
return this;
};
+/**
+ * Layout made of a field, a button, and an optional label.
+ *
+ * @class
+ * @extends OO.ui.FieldLayout
+ *
+ * @constructor
+ * @param {OO.ui.Widget} fieldWidget Field widget
+ * @param {OO.ui.ButtonWidget} buttonWidget Button widget
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
+ * @cfg {string} [help] Explanatory text shown as a '?' icon.
+ */
+OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
+ // Configuration initialization
+ config = $.extend( { align: 'left' }, config );
+
+ // Properties (must be set before parent constructor, which calls #getTagName)
+ this.fieldWidget = fieldWidget;
+ this.buttonWidget = buttonWidget;
+
+ // Parent constructor
+ OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
+
+ // Mixin constructors
+ OO.ui.LabelElement.call( this, config );
+
+ // Properties
+ this.$button = this.$( '<div>' )
+ .addClass( 'oo-ui-actionFieldLayout-button' )
+ .append( this.buttonWidget.$element );
+
+ this.$input = this.$( '<div>' )
+ .addClass( 'oo-ui-actionFieldLayout-input' )
+ .append( this.fieldWidget.$element );
+
+ this.$field
+ .addClass( 'oo-ui-actionFieldLayout' )
+ .append( this.$input, this.$button );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
+
/**
* Layout made of a fieldset and optional legend.
*
OO.ui.LabelElement.call( this, config );
OO.ui.GroupElement.call( this, config );
+ if ( config.help ) {
+ this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ $: this.$,
+ classes: [ 'oo-ui-fieldsetLayout-help' ],
+ framed: false,
+ icon: 'info'
+ } );
+
+ this.popupButtonWidget.getPopup().$body.append(
+ this.$( '<div>' )
+ .text( config.help )
+ .addClass( 'oo-ui-fieldsetLayout-help-content' )
+ );
+ this.$help = this.popupButtonWidget.$element;
+ } else {
+ this.$help = this.$( [] );
+ }
+
// Initialization
this.$element
.addClass( 'oo-ui-fieldsetLayout' )
- .prepend( this.$icon, this.$label, this.$group );
+ .prepend( this.$help, this.$icon, this.$label, this.$group );
if ( $.isArray( config.items ) ) {
this.addItems( config.items );
}
// Initialization
this.$element.addClass( 'oo-ui-gridLayout' );
for ( i = 0, len = panels.length; i < len; i++ ) {
- this.panels.push( panels[i] );
- this.$element.append( panels[i].$element );
+ this.panels.push( panels[ i ] );
+ this.$element.append( panels[ i ].$element );
}
if ( config.widths || config.heights ) {
this.layout( config.widths || [ 1 ], config.heights || [ 1 ] );
// Sum up denominators
for ( x = 0; x < cols; x++ ) {
- xd += widths[x];
+ xd += widths[ x ];
}
for ( y = 0; y < rows; y++ ) {
- yd += heights[y];
+ yd += heights[ y ];
}
// Store factors
this.widths = [];
this.heights = [];
for ( x = 0; x < cols; x++ ) {
- this.widths[x] = widths[x] / xd;
+ this.widths[ x ] = widths[ x ] / xd;
}
for ( y = 0; y < rows; y++ ) {
- this.heights[y] = heights[y] / yd;
+ this.heights[ y ] = heights[ y ] / yd;
}
// Synchronize view
this.update();
rows = this.heights.length;
for ( y = 0; y < rows; y++ ) {
- height = this.heights[y];
+ height = this.heights[ y ];
for ( x = 0; x < cols; x++ ) {
- width = this.widths[x];
- panel = this.panels[i];
+ 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 ) {
return this.panels[ ( x * this.widths.length ) + y ];
};
+/**
+ * Layout with a content and menu area.
+ *
+ * The menu area can be positioned at the top, after, bottom or before. The content area will fill
+ * all remaining space.
+ *
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number|string} [menuSize='18em'] Size of menu in pixels or any CSS unit
+ * @cfg {boolean} [showMenu=true] Show menu
+ * @cfg {string} [position='before'] Position of menu, either `top`, `after`, `bottom` or `before`
+ * @cfg {boolean} [collapse] Collapse the menu out of view
+ */
+OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
+ var positions = this.constructor.static.menuPositions;
+
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.MenuLayout.super.call( this, config );
+
+ // Properties
+ this.showMenu = config.showMenu !== false;
+ this.menuSize = config.menuSize || '18em';
+ this.menuPosition = positions[ config.menuPosition ] || positions.before;
+
+ /**
+ * Menu DOM node
+ *
+ * @property {jQuery}
+ */
+ this.$menu = this.$( '<div>' );
+ /**
+ * Content DOM node
+ *
+ * @property {jQuery}
+ */
+ this.$content = this.$( '<div>' );
+
+ // Events
+ this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
+
+ // Initialization
+ this.toggleMenu( this.showMenu );
+ this.$menu
+ .addClass( 'oo-ui-menuLayout-menu' )
+ .css( this.menuPosition.sizeProperty, this.menuSize );
+ this.$content.addClass( 'oo-ui-menuLayout-content' );
+ this.$element
+ .addClass( 'oo-ui-menuLayout ' + this.menuPosition.className )
+ .append( this.$content, this.$menu );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
+
+/* Static Properties */
+
+OO.ui.MenuLayout.static.menuPositions = {
+ top: {
+ sizeProperty: 'height',
+ positionProperty: 'top',
+ className: 'oo-ui-menuLayout-top'
+ },
+ after: {
+ sizeProperty: 'width',
+ positionProperty: 'right',
+ rtlPositionProperty: 'left',
+ className: 'oo-ui-menuLayout-after'
+ },
+ bottom: {
+ sizeProperty: 'height',
+ positionProperty: 'bottom',
+ className: 'oo-ui-menuLayout-bottom'
+ },
+ before: {
+ sizeProperty: 'width',
+ positionProperty: 'left',
+ rtlPositionProperty: 'right',
+ className: 'oo-ui-menuLayout-before'
+ }
+};
+
+/* Methods */
+
+/**
+ * Handle DOM attachment events
+ */
+OO.ui.MenuLayout.prototype.onElementAttach = function () {
+ // getPositionProperty won't know about directionality until the layout is attached
+ if ( this.showMenu ) {
+ this.$content.css( this.getPositionProperty(), this.menuSize );
+ }
+};
+
+/**
+ * Toggle menu.
+ *
+ * @param {boolean} showMenu Show menu, omit to toggle
+ * @chainable
+ */
+OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
+ showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
+
+ if ( this.showMenu !== showMenu ) {
+ this.showMenu = showMenu;
+ this.updateSizes();
+ }
+
+ return this;
+};
+
+/**
+ * Check if menu is visible
+ *
+ * @return {boolean} Menu is visible
+ */
+OO.ui.MenuLayout.prototype.isMenuVisible = function () {
+ return this.showMenu;
+};
+
+/**
+ * Set menu size.
+ *
+ * @param {number|string} size Size of menu in pixels or any CSS unit
+ * @chainable
+ */
+OO.ui.MenuLayout.prototype.setMenuSize = function ( size ) {
+ this.menuSize = size;
+ this.updateSizes();
+
+ return this;
+};
+
+/**
+ * Update menu and content CSS based on current menu size and visibility
+ */
+OO.ui.MenuLayout.prototype.updateSizes = function () {
+ if ( this.showMenu ) {
+ this.$menu
+ .css( this.menuPosition.sizeProperty, this.menuSize )
+ .css( 'overflow', '' );
+ this.$content.css( this.getPositionProperty(), this.menuSize );
+ } else {
+ this.$menu
+ .css( this.menuPosition.sizeProperty, 0 )
+ .css( 'overflow', 'hidden' );
+ this.$content.css( this.getPositionProperty(), 0 );
+ }
+};
+
+/**
+ * Get menu size.
+ *
+ * @return {number|string} Menu size
+ */
+OO.ui.MenuLayout.prototype.getMenuSize = function () {
+ return this.menuSize;
+};
+
+/**
+ * Set menu position.
+ *
+ * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
+ * @throws {Error} If position value is not supported
+ * @chainable
+ */
+OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
+ var positionProperty, positions = this.constructor.static.menuPositions;
+
+ if ( !positions[ position ] ) {
+ throw new Error( 'Cannot set position; unsupported position value: ' + position );
+ }
+
+ positionProperty = this.getPositionProperty();
+ this.$menu.css( this.menuPosition.sizeProperty, '' );
+ this.$content.css( positionProperty, '' );
+ this.$element.removeClass( this.menuPosition.className );
+
+ this.menuPosition = positions[ position ];
+
+ this.updateSizes();
+ this.$element.addClass( this.menuPosition.className );
+
+ return this;
+};
+
+/**
+ * Get menu position.
+ *
+ * @return {string} Menu position
+ */
+OO.ui.MenuLayout.prototype.getMenuPosition = function () {
+ return this.menuPosition;
+};
+
+/**
+ * Get the menu position property.
+ *
+ * @return {string} Menu position CSS property
+ */
+OO.ui.MenuLayout.prototype.getPositionProperty = function () {
+ if ( this.menuPosition.rtlPositionProperty && this.$element.css( 'direction' ) === 'rtl' ) {
+ return this.menuPosition.rtlPositionProperty;
+ } else {
+ return this.menuPosition.positionProperty;
+ }
+};
+
/**
* Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
*
* @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
* @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 ) {
OO.ui.GroupElement.prototype.addItems.call( this, items, index );
if ( !this.currentItem && items.length ) {
- this.setItem( items[0] );
+ this.setItem( items[ 0 ] );
}
return this;
if ( $.inArray( this.currentItem, items ) !== -1 ) {
if ( this.items.length ) {
- this.setItem( this.items[0] );
+ this.setItem( this.items[ 0 ] );
} else {
this.unsetCurrentItem();
}
if ( item !== this.currentItem ) {
if ( !this.continuous ) {
for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[i].$element.css( 'display', '' );
+ this.items[ i ].$element.css( 'display', '' );
}
}
if ( $.inArray( item, this.items ) !== -1 ) {
*/
OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
// Only deactivate when clicking outside the dropdown element
- if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) {
+ if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
this.setActive( false );
}
};
this.collapsibleTools = [];
for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
- if ( this.tools[ allowCollapse[i] ] !== undefined ) {
- this.collapsibleTools.push( this.tools[ allowCollapse[i] ] );
+ if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
+ this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
}
}
// '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', 'inline-block' );
+ this.getExpandCollapseTool().$element.css( 'display', 'block' );
}
this.updateCollapsibleState();
.setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
- this.collapsibleTools[i].toggle( this.expanded );
+ this.collapsibleTools[ i ].toggle( this.expanded );
}
};
labelTexts = [];
for ( name in this.tools ) {
- if ( this.tools[name].isActive() ) {
- labelTexts.push( this.tools[name].getTitle() );
+ if ( this.tools[ name ].isActive() ) {
+ labelTexts.push( this.tools[ name ].getTitle() );
}
}
// During construction, #setDisabled is called before the OO.ui.GroupElement constructor
if ( this.items ) {
for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[i].updateDisabled();
+ this.items[ i ].updateDisabled();
}
}
*
* @class
* @abstract
+ * @deprecated Use LookupElement instead.
*
* @constructor
* @param {OO.ui.TextInputWidget} input Input widget
this.lookupInput = input;
this.$overlay = config.$overlay || this.$element;
this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
- $: OO.ui.Element.getJQuery( this.$overlay ),
+ $: OO.ui.Element.static.getJQuery( this.$overlay ),
input: this.lookupInput,
$container: config.$container
} );
this.abortLookupRequest();
if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
- deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
+ deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[ value ] ) );
} else {
this.lookupInput.pushPending();
this.lookupQuery = value;
if ( ourRequest === widget.lookupRequest ) {
widget.lookupQuery = null;
widget.lookupRequest = null;
- widget.lookupCache[value] = widget.getLookupCacheItemFromData( data );
- deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[value] ) );
+ widget.lookupCache[ value ] = widget.getLookupCacheItemFromData( data );
+ deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[ value ] ) );
}
} )
.fail( function () {
i = -1;
len = items.length;
while ( ++i < len ) {
- if ( items[i].isMovable() ) {
- firstMovable = items[i];
+ if ( items[ i ].isMovable() ) {
+ firstMovable = items[ i ];
break;
}
}
i = len;
while ( i-- ) {
- if ( items[i].isMovable() ) {
- lastMovable = items[i];
+ if ( items[ i ].isMovable() ) {
+ lastMovable = items[ i ];
break;
}
}
this.emit( 'change', value );
this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
+ this.$element.attr( 'aria-checked', value.toString() );
}
return this;
};
* @mixins OO.ui.LabelElement
* @mixins OO.ui.TitledElement
* @mixins OO.ui.FlaggedElement
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
*/
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.LabelElement.call( this, config );
OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
OO.ui.FlaggedElement.call( this, config );
+ OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
// Properties
this.href = null;
OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TabIndexedElement );
/* Events */
return false;
};
+/**
+ * @inheritdoc
+ */
+OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
+ // Remove the tab-index while the button is down to prevent the button from stealing focus
+ this.$button.removeAttr( 'tabindex' );
+ // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
+ // reliably reapply the tabindex and remove the pressed class
+ this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
+
+ return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
+ // Restore the tab-index after the button is up to restore the button's accessibility
+ this.$button.attr( 'tabindex', this.tabIndex );
+ // Stop listening for mouseup, since we only needed this once
+ this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
+
+ return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e );
+};
+
/**
* Handles keypress events.
*
// Initialization
this.$element
.addClass( 'oo-ui-popupButtonWidget' )
+ .attr( 'aria-haspopup', 'true' )
.append( this.popup.$element );
};
*/
OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
// Skip clicks within the popup
- if ( $.contains( this.popup.$element[0], e.target ) ) {
+ if ( $.contains( this.popup.$element[ 0 ], e.target ) ) {
return;
}
OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
value = !!value;
if ( value !== this.value ) {
+ this.$button.attr( 'aria-pressed', value.toString() );
this.setActive( value );
}
*/
OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
// Skip clicks within the menu
- if ( $.contains( this.menu.$element[0], e.target ) ) {
+ if ( $.contains( this.menu.$element[ 0 ], e.target ) ) {
return;
}
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.FlaggedElement
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
// Parent constructor
OO.ui.InputWidget.super.call( this, config );
- // Mixin constructors
- OO.ui.FlaggedElement.call( this, config );
-
// Properties
this.$input = this.getInputElement( config );
this.value = '';
this.inputFilter = config.inputFilter;
+ // Mixin constructors
+ OO.ui.FlaggedElement.call( this, config );
+ OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
+
// Events
this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement );
+OO.mixinClass( OO.ui.InputWidget, OO.ui.TabIndexedElement );
/* Events */
/**
* Get input element.
*
+ * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
+ * different circumstances. The element must have a `value` property (like form elements).
+ *
* @private
- * @param {Object} [config] Configuration options
+ * @param {Object} config Configuration options
* @return {jQuery} Input element
*/
OO.ui.InputWidget.prototype.getInputElement = function () {
* @return {string} Input value
*/
OO.ui.InputWidget.prototype.getValue = function () {
+ // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
+ // it, and we won't know unless they're kind enough to trigger a 'change' event.
+ var value = this.$input.val();
+ if ( this.value !== value ) {
+ this.setValue( value );
+ }
return this.value;
};
* @param {boolean} isRTL
*/
OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
- if ( isRTL ) {
- this.$input.removeClass( 'oo-ui-ltr' );
- this.$input.addClass( 'oo-ui-rtl' );
- } else {
- this.$input.removeClass( 'oo-ui-rtl' );
- this.$input.addClass( 'oo-ui-ltr' );
- }
+ this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
};
/**
*/
OO.ui.InputWidget.prototype.setValue = function ( 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 cleanUpValue, 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;
};
if ( this.$input.is( ':checkbox,:radio' ) ) {
this.$input.click();
} else if ( this.$input.is( ':input' ) ) {
- this.$input[0].focus();
+ this.$input[ 0 ].focus();
}
}
};
* @chainable
*/
OO.ui.InputWidget.prototype.focus = function () {
- this.$input[0].focus();
+ this.$input[ 0 ].focus();
return this;
};
* @chainable
*/
OO.ui.InputWidget.prototype.blur = function () {
- this.$input[0].blur();
+ this.$input[ 0 ].blur();
return this;
};
/* Methods */
/**
- * Get input element.
- *
+ * @inheritdoc
* @private
- * @param {Object} [config] Configuration options
- * @return {jQuery} Input element
*/
OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
- // Configuration initialization
- config = config || {};
-
var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
-
return this.$( html );
};
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {boolean} [selected=false] Whether the checkbox is initially selected
*/
OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
// Parent constructor
// Initialization
this.$element.addClass( 'oo-ui-checkboxInputWidget' );
+ this.setSelected( config.selected !== undefined ? config.selected : false );
};
/* Setup */
/* Methods */
/**
- * Get input element.
- *
+ * @inheritdoc
* @private
- * @return {jQuery} Input element
*/
OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
return this.$( '<input type="checkbox" />' );
};
/**
- * Get checked state of the checkbox
+ * @inheritdoc
+ */
+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' ) );
+ } );
+ }
+};
+
+/**
+ * Set selection state of this checkbox.
*
- * @return {boolean} If the checkbox is checked
+ * @param {boolean} state Whether the checkbox is selected
+ * @chainable
*/
-OO.ui.CheckboxInputWidget.prototype.getValue = function () {
- return this.value;
+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;
};
/**
- * Set checked state of the checkbox
+ * Check if this checkbox is selected.
*
- * @param {boolean} value New value
+ * @return {boolean} Checkbox is selected
*/
-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.CheckboxInputWidget.prototype.isSelected = function () {
+ // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
+ // it, and we won't know unless they're kind enough to trigger a 'change' event.
+ var selected = this.$input.prop( 'checked' );
+ if ( this.selected !== selected ) {
+ this.setSelected( selected );
+ }
+ return this.selected;
+};
+
+/**
+ * A OO.ui.DropdownWidget synchronized with a `<input type=hidden>` for form submission. Intended to
+ * be used within a OO.ui.FormLayout.
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
+ */
+OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties (must be done before parent constructor which calls #setDisabled)
+ this.dropdownWidget = new OO.ui.DropdownWidget( {
+ $: this.$
+ } );
+
+ // Parent constructor
+ OO.ui.DropdownInputWidget.super.call( this, config );
+
+ // Events
+ this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
+
+ // Initialization
+ this.setOptions( config.options || [] );
+ this.$element
+ .addClass( 'oo-ui-dropdownInputWidget' )
+ .append( this.dropdownWidget.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ * @private
+ */
+OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
+ return this.$( '<input type="hidden">' );
+};
+
+/**
+ * Handles menu select events.
+ *
+ * @param {OO.ui.MenuOptionWidget} item Selected menu item
+ */
+OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
+ this.setValue( item.getData() );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
+ var item = this.dropdownWidget.getMenu().getItemFromData( value );
+ if ( item ) {
+ this.dropdownWidget.getMenu().selectItem( item );
}
+ OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value );
+ return this;
};
/**
* @inheritdoc
*/
-OO.ui.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.DropdownInputWidget.prototype.setDisabled = function ( state ) {
+ this.dropdownWidget.setDisabled( state );
+ OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state );
+ return this;
+};
+
+/**
+ * Set the options available for this input.
+ *
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @chainable
+ */
+OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
+ var value = this.getValue();
+
+ // Rebuild the dropdown menu
+ this.dropdownWidget.getMenu()
+ .clearItems()
+ .addItems( options.map( function ( opt ) {
+ return new OO.ui.MenuOptionWidget( {
+ data: opt.data,
+ label: opt.label !== undefined ? opt.label : opt.data
+ } );
+ } ) );
+
+ // Restore the previous value, or reset to something sensible
+ if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
+ // Previous value is still available, ensure consistency with the dropdown
+ this.setValue( value );
+ } else {
+ // No longer valid, reset
+ if ( options.length ) {
+ this.setValue( options[ 0 ].data );
+ }
}
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.focus = function () {
+ this.dropdownWidget.getMenu().toggle( true );
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.blur = function () {
+ this.dropdownWidget.getMenu().toggle( false );
+ return this;
};
/**
* 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.
*
- * This class doesn't make it possible to learn whether the radio button is selected ("pressed").
- *
* @class
* @extends OO.ui.InputWidget
*
* @constructor
* @param {Object} [config] Configuration options
- * @param {boolean} [config.selected=false] Whether the radio button is initially selected
+ * @cfg {boolean} [selected=false] Whether the radio button is initially selected
*/
OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
// Parent constructor
/* Methods */
/**
- * Get input element.
- *
+ * @inheritdoc
* @private
- * @return {jQuery} Input element
*/
OO.ui.RadioInputWidget.prototype.getInputElement = function () {
return this.$( '<input type="radio" />' );
* @param {Object} [config] Configuration options
* @cfg {string} [type='text'] HTML tag `type` attribute
* @cfg {string} [placeholder] Placeholder text
+ * @cfg {boolean} [autofocus=false] Ask the browser to focus this widget, using the 'autofocus' HTML
+ * attribute
* @cfg {boolean} [readOnly=false] Prevent changes
+ * @cfg {number} [maxLength] Maximum allowed number of characters to input
* @cfg {boolean} [multiline=false] Allow multiple lines of text
* @cfg {boolean} [autosize=false] Automatically resize to fit content
* @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
- * @cfg {RegExp|string} [validate] Regular expression (or symbolic name referencing
+ * @cfg {string} [labelPosition='after'] Label position, 'before' or 'after'
+ * @cfg {RegExp|string} [validate] Regular expression to validate against (or symbolic name referencing
* one, see #static-validationPatterns)
*/
OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
// Configuration initialization
- config = $.extend( { readOnly: false }, config );
+ config = $.extend( {
+ type: 'text',
+ labelPosition: 'after',
+ maxRows: 10
+ }, config );
// Parent constructor
OO.ui.TextInputWidget.super.call( this, config );
OO.ui.IconElement.call( this, config );
OO.ui.IndicatorElement.call( this, config );
OO.ui.PendingElement.call( this, config );
+ OO.ui.LabelElement.call( this, config );
// Properties
this.readOnly = false;
this.multiline = !!config.multiline;
this.autosize = !!config.autosize;
- this.maxRows = config.maxRows !== undefined ? config.maxRows : 10;
+ this.maxRows = config.maxRows;
this.validate = null;
+ this.attached = false;
+
+ // Clone for resizing
+ if ( this.autosize ) {
+ this.$clone = this.$input
+ .clone()
+ .insertAfter( this.$input )
+ .hide();
+ }
this.setValidation( config.validate );
+ this.setPosition( config.labelPosition );
// Events
this.$input.on( {
blur: this.setValidityFlag.bind( this )
} );
this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
+ this.$element.on( 'DOMNodeRemovedFromDocument', this.onElementDetach.bind( this ) );
this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
+ this.on( 'labelChange', this.updatePosition.bind( this ) );
// Initialization
this.$element
.addClass( 'oo-ui-textInputWidget' )
- .append( this.$icon, this.$indicator );
- this.setReadOnly( config.readOnly );
+ .append( this.$icon, this.$indicator, this.$label );
+ this.setReadOnly( !!config.readOnly );
if ( config.placeholder ) {
this.$input.attr( 'placeholder', config.placeholder );
}
- this.$element.attr( 'role', 'textbox' );
+ if ( config.maxLength ) {
+ this.$input.attr( 'maxlength', config.maxLength );
+ }
+ if ( config.autofocus ) {
+ this.$input.attr( 'autofocus', 'autofocus' );
+ }
};
/* Setup */
OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.LabelElement );
/* Static properties */
*/
OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
if ( e.which === 1 ) {
- this.$input[0].focus();
+ this.$input[ 0 ].focus();
this.emit( 'icon' );
return false;
}
*/
OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
if ( e.which === 1 ) {
- this.$input[0].focus();
+ this.$input[ 0 ].focus();
this.emit( 'indicator' );
return false;
}
* @param {jQuery.Event} e Element attach event
*/
OO.ui.TextInputWidget.prototype.onElementAttach = function () {
+ this.attached = true;
+ // If we reattached elsewhere, the valCache is now invalid
+ this.valCache = null;
this.adjustSize();
+ this.positionLabel();
+};
+
+/**
+ * Handle element detach events.
+ *
+ * @param {jQuery.Event} e Element detach event
+ */
+OO.ui.TextInputWidget.prototype.onElementDetach = function () {
+ this.attached = false;
};
/**
* @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 && this.$input.val() !== this.valCache ) {
- $clone = this.$input.clone()
+ if ( this.multiline && this.autosize && this.attached && 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 );
+ .css( 'height', 0 );
+
+ this.$clone[ 0 ].style.display = 'block';
+
this.valCache = this.$input.val();
- scrollHeight = $clone[0].scrollHeight;
+
+ 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.
- *
+ * @inheritdoc
* @private
- * @param {Object} [config] Configuration options
- * @return {jQuery} Input element
*/
OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
- // Configuration initialization
- config = config || {};
-
- var type = config.type || 'text';
-
- return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="' + type + '" />' );
+ return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="' + config.type + '" />' );
};
/**
if ( validate instanceof RegExp ) {
this.validate = validate;
} else {
- this.validate = this.constructor.static.validationPatterns[validate] || /.*/;
+ this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
}
};
return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
};
+/**
+ * Set the position of the inline label.
+ *
+ * @param {string} labelPosition Label position, 'before' or 'after'
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.setPosition = function ( labelPosition ) {
+ this.labelPosition = labelPosition;
+ this.updatePosition();
+ return this;
+};
+
+/**
+ * Update the position of the inline label.
+ *
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.updatePosition = function () {
+ var after = this.labelPosition === 'after';
+
+ this.$element
+ .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', this.label && after )
+ .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', this.label && !after );
+
+ if ( this.label ) {
+ this.positionLabel();
+ }
+
+ return this;
+};
+
+/**
+ * Position the label by setting the correct padding on the input.
+ *
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.positionLabel = function () {
+ // Clear old values
+ this.$input
+ // Clear old values if present
+ .css( {
+ 'padding-right': '',
+ 'padding-left': ''
+ } );
+
+ if ( !this.$label.text() ) {
+ return;
+ }
+
+ var after = this.labelPosition === 'after',
+ rtl = this.$element.css( 'direction' ) === 'rtl',
+ property = after === rtl ? 'padding-left' : 'padding-right';
+
+ this.$input.css( property, this.$label.outerWidth() );
+
+ return this;
+};
+
/**
* Text input with a menu of optional values.
*
) );
this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
{
- $: OO.ui.Element.getJQuery( this.$overlay ),
+ $: OO.ui.Element.static.getJQuery( this.$overlay ),
widget: this,
input: this.input,
disabled: this.isDisabled()
return this;
};
-/**
- * Make the option's highlight flash.
- *
- * While flashing, the visual style of the pressed state is removed if present.
- *
- * @return {jQuery.Promise} Promise resolved when flashing is done
- */
-OO.ui.OptionWidget.prototype.flash = function () {
- var widget = this,
- $element = this.$element,
- deferred = $.Deferred();
-
- if ( !this.isDisabled() && this.constructor.static.pressable ) {
- $element.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
- setTimeout( function () {
- // Restore original classes
- $element
- .toggleClass( 'oo-ui-optionWidget-highlighted', widget.highlighted )
- .toggleClass( 'oo-ui-optionWidget-pressed', widget.pressed );
-
- setTimeout( function () {
- deferred.resolve();
- }, 100 );
-
- }, 100 );
- }
-
- return deferred.promise();
-};
-
/**
* Option widget with an option icon and indicator.
*
* @class
* @extends OO.ui.DecoratedOptionWidget
* @mixins OO.ui.ButtonElement
+ * @mixins OO.ui.TabIndexedElement
*
* @constructor
* @param {Object} [config] Configuration options
// Mixin constructors
OO.ui.ButtonElement.call( this, config );
+ OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
// Initialization
this.$element.addClass( 'oo-ui-buttonOptionWidget' );
OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
+OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.TabIndexedElement );
/* Static Properties */
OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
if (
this.isVisible() &&
- !$.contains( this.$element[0], e.target ) &&
+ !$.contains( this.$element[ 0 ], e.target ) &&
( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
) {
this.toggle( false );
} );
// Compute initial popupOffset based on alignment
- popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[this.align];
+ popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[ this.align ];
// Figure out if this will cause the popup to go beyond the edge of the container
- originOffset = Math.round( this.$element.offset().left );
- containerLeft = Math.round( this.$container.offset().left );
+ originOffset = this.$element.offset().left;
+ containerLeft = this.$container.offset().left;
containerWidth = this.$container.innerWidth();
containerRight = containerLeft + containerWidth;
popupLeft = popupOffset - this.containerPadding;
// Adjust offset to avoid anchor being rendered too close to the edge
// $anchor.width() doesn't work with the pure CSS anchor (returns 0)
// TODO: Find a measurement that works for CSS anchors and image anchors
- anchorWidth = this.$anchor[0].scrollWidth * 2;
+ anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
if ( popupOffset + this.width < anchorWidth ) {
popupOffset = anchorWidth - this.width;
} else if ( -popupOffset < anchorWidth ) {
// Initialization
this.setProgress( config.progress !== undefined ? config.progress : false );
- this.$bar.addClass( 'oo-ui-progressBarWidget-bar');
+ this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
this.$element
.attr( {
role: 'progressbar',
var i, len;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- if ( this.items[i].isSelected() ) {
- return this.items[i];
+ if ( this.items[ i ].isSelected() ) {
+ return this.items[ i ];
}
}
return null;
var i, len;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- if ( this.items[i].isHighlighted() ) {
- return this.items[i];
+ if ( this.items[ i ].isHighlighted() ) {
+ return this.items[ i ];
}
}
return null;
changed = false;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- highlighted = this.items[i] === item;
- if ( this.items[i].isHighlighted() !== highlighted ) {
- this.items[i].setHighlighted( highlighted );
+ highlighted = this.items[ i ] === item;
+ if ( this.items[ i ].isHighlighted() !== highlighted ) {
+ this.items[ i ].setHighlighted( highlighted );
changed = true;
}
}
changed = false;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- selected = this.items[i] === item;
- if ( this.items[i].isSelected() !== selected ) {
- this.items[i].setSelected( selected );
+ selected = this.items[ i ] === item;
+ if ( this.items[ i ].isSelected() !== selected ) {
+ this.items[ i ].setSelected( selected );
changed = true;
}
}
changed = false;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- pressed = this.items[i] === item;
- if ( this.items[i].isPressed() !== pressed ) {
- this.items[i].setPressed( pressed );
+ pressed = this.items[ i ] === item;
+ if ( this.items[ i ].isPressed() !== pressed ) {
+ this.items[ i ].setPressed( pressed );
changed = true;
}
}
/**
* Get an item relative to another one.
*
- * @param {OO.ui.OptionWidget} item Item to start at
- * @param {number} direction Direction to move in, -1 to look backward, 1 to move forward
+ * @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;
};
var i, len, item;
for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
+ item = this.items[ i ];
if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
return item;
}
// Deselect items being removed
for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
+ item = items[ i ];
if ( item.isSelected() ) {
this.selectItem( null );
}
OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
// Properties
- this.flashing = false;
this.visible = false;
this.newItems = null;
this.autoHide = config.autoHide === undefined || !!config.autoHide;
*/
OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
if (
- !OO.ui.contains( this.$element[0], e.target, true ) &&
- ( !this.$widget || !OO.ui.contains( this.$widget[0], e.target, true ) )
+ !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
+ ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
) {
this.toggle( false );
}
/**
* Choose an item.
*
- * This will close the menu when done, unlike selectItem which only changes selection.
+ * This will close the menu, unlike #selectItem which only changes selection.
*
* @param {OO.ui.OptionWidget} item Item to choose
* @chainable
*/
OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
- var widget = this;
-
- // Parent method
OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
-
- if ( item && !this.flashing ) {
- this.flashing = true;
- item.flash().done( function () {
- widget.toggle( false );
- widget.flashing = false;
- } );
- } else {
- this.toggle( false );
- }
-
+ this.toggle( false );
return this;
};
}
for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
+ item = items[ i ];
if ( this.isVisible() ) {
// Defer fitting label until item has been attached
item.fitLabel();
var i, len,
change = visible !== this.isVisible(),
elementDoc = this.getElementDocument(),
- widgetDoc = this.$widget ? this.$widget[0].ownerDocument : null;
+ widgetDoc = this.$widget ? this.$widget[ 0 ].ownerDocument : null;
// Parent method
OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
// Change focus to enable keyboard navigation
if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
this.$previousFocus = this.$( ':focus' );
- this.$input[0].focus();
+ this.$input[ 0 ].focus();
}
if ( this.newItems && this.newItems.length ) {
for ( i = 0, len = this.newItems.length; i < len; i++ ) {
- this.newItems[i].fitLabel();
+ this.newItems[ i ].fitLabel();
}
this.newItems = null;
}
} else {
this.unbindKeyDownListener();
if ( this.isolated && this.$previousFocus ) {
- this.$previousFocus[0].focus();
+ this.$previousFocus[ 0 ].focus();
this.$previousFocus = null;
}
elementDoc.removeEventListener(
*/
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();