/*!
- * OOjs UI v0.1.0-pre (23565e7519)
+ * OOjs UI v0.6.0
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2014 OOjs Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2014-11-21T22:18:20Z
+ * Date: 2014-12-16T21:00:55Z
*/
( function ( OO ) {
* @cfg {string[]} [classes] CSS class names to add
* @cfg {string} [text] Text to insert
* @cfg {jQuery} [$content] Content elements to append (after text)
+ * @cfg {Mixed} [data] Element data
*/
OO.ui.Element = function OoUiElement( config ) {
// Configuration initialization
config = config || {};
// Properties
- this.$ = config.$ || OO.ui.Element.getJQuery( document );
+ this.$ = config.$ || OO.ui.Element.static.getJQuery( document );
+ this.data = config.data;
this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
this.elementGroup = null;
this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
* not in an iframe
* @return {Function} Bound jQuery function
*/
-OO.ui.Element.getJQuery = function ( context, $iframe ) {
+OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
function wrapper( selector ) {
return $( selector, wrapper.context );
}
* @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
* @return {HTMLDocument|null} Document object
*/
-OO.ui.Element.getDocument = function ( obj ) {
+OO.ui.Element.static.getDocument = function ( obj ) {
// jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
return ( obj[0] && obj[0].ownerDocument ) ||
// Empty jQuery selections might have a context
* @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
* @return {Window} Window object
*/
-OO.ui.Element.getWindow = function ( obj ) {
+OO.ui.Element.static.getWindow = function ( obj ) {
var doc = this.getDocument( obj );
return doc.parentWindow || doc.defaultView;
};
* @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 ) {
* @param {Object} [offset] Offset to start with, used internally
* @return {Object} Offset object, containing left and top properties
*/
-OO.ui.Element.getFrameOffset = function ( from, to, offset ) {
+OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
var i, len, frames, frame, rect;
if ( !to ) {
* @param {jQuery} $anchor Element to get $element's position relative to
* @return {Object} Translated position coordinates, containing top and left properties
*/
-OO.ui.Element.getRelativePosition = function ( $element, $anchor ) {
+OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
var iframe, iframePos,
pos = $element.offset(),
anchorPos = $anchor.offset(),
* @param {HTMLElement} el Element to measure
* @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
*/
-OO.ui.Element.getBorders = function ( el ) {
+OO.ui.Element.static.getBorders = function ( el ) {
var doc = el.ownerDocument,
win = doc.parentWindow || doc.defaultView,
style = win && win.getComputedStyle ?
* @param {HTMLElement|Window} el Element to measure
* @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
*/
-OO.ui.Element.getDimensions = function ( el ) {
+OO.ui.Element.static.getDimensions = function ( el ) {
var $el, $win,
doc = el.ownerDocument || el.document,
win = doc.parentWindow || doc.defaultView;
}
};
+/**
+ * Get scrollable object parent
+ *
+ * documentElement can't be used to get or set the scrollTop
+ * property on Blink. Changing and testing its value lets us
+ * use 'body' or 'documentElement' based on what is working.
+ *
+ * https://code.google.com/p/chromium/issues/detail?id=303131
+ *
+ * @static
+ * @param {HTMLElement} el Element to find scrollable parent for
+ * @return {HTMLElement} Scrollable parent
+ */
+OO.ui.Element.static.getRootScrollableElement = function ( el ) {
+ var scrollTop, body;
+
+ if ( OO.ui.scrollableElement === undefined ) {
+ body = el.ownerDocument.body;
+ scrollTop = body.scrollTop;
+ body.scrollTop = 1;
+
+ if ( body.scrollTop === 1 ) {
+ body.scrollTop = scrollTop;
+ OO.ui.scrollableElement = 'body';
+ } else {
+ OO.ui.scrollableElement = 'documentElement';
+ }
+ }
+
+ return el.ownerDocument[ OO.ui.scrollableElement ];
+};
+
/**
* Get closest scrollable container.
*
* @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
* @return {HTMLElement} Closest scrollable container
*/
-OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) {
+OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
var i, val,
props = [ 'overflow' ],
$parent = $( el ).parent();
}
while ( $parent.length ) {
- if ( $parent[0] === el.ownerDocument.body ) {
+ if ( $parent[0] === this.getRootScrollableElement( el ) ) {
return $parent[0];
}
i = props.length;
* to scroll in both directions
* @param {Function} [config.complete] Function to call when scrolling completes
*/
-OO.ui.Element.scrollIntoView = function ( el, config ) {
+OO.ui.Element.static.scrollIntoView = function ( el, config ) {
// Configuration initialization
config = config || {};
$win = $( this.getWindow( el ) );
// Compute the distances between the edges of el and the edges of the scroll viewport
- if ( $sc.is( 'body' ) ) {
- // If the scrollable container is the <body> this is easy
+ if ( $sc.is( 'html, body' ) ) {
+ // If the scrollable container is the root, this is easy
rel = {
top: eld.rect.top,
bottom: $win.innerHeight() - eld.rect.bottom,
}
};
+/* Methods */
+
/**
- * Bind a handler for an event on a DOM element.
+ * Get element data.
*
- * Used to be for working around a jQuery bug (jqbug.com/14180),
- * but obsolete as of jQuery 1.11.0.
- *
- * @static
- * @deprecated Use jQuery#on instead.
- * @param {HTMLElement|jQuery} el DOM element
- * @param {string} event Event to bind
- * @param {Function} callback Callback to call when the event fires
+ * @return {Mixed} Element data
*/
-OO.ui.Element.onDOMEvent = function ( el, event, callback ) {
- $( el ).on( event, callback );
+OO.ui.Element.prototype.getData = function () {
+ return this.data;
};
/**
- * Unbind a handler bound with #static-method-onDOMEvent.
+ * Set element data.
*
- * @deprecated Use jQuery#off instead.
- * @static
- * @param {HTMLElement|jQuery} el DOM element
- * @param {string} event Event to unbind
- * @param {Function} [callback] Callback to unbind
+ * @param {Mixed} Element data
+ * @chainable
*/
-OO.ui.Element.offDOMEvent = function ( el, event, callback ) {
- $( el ).off( event, callback );
+OO.ui.Element.prototype.setData = function ( data ) {
+ this.data = data;
+ return this;
};
-/* Methods */
-
/**
* Check if element supports one or more methods.
*
* @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 );
};
/**
return this.size;
};
+/**
+ * Disable transitions on window's frame for the duration of the callback function, then enable them
+ * back.
+ *
+ * @private
+ * @param {Function} callback Function to call while transitions are disabled
+ */
+OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
+ // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
+ // Disable transitions first, otherwise we'll get values from when the window was animating.
+ var oldTransition,
+ styleObj = this.$frame[0].style;
+ oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
+ styleObj.MozTransition || styleObj.WebkitTransition;
+ styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
+ styleObj.MozTransition = styleObj.WebkitTransition = 'none';
+ callback();
+ // Force reflow to make sure the style changes done inside callback really are not transitioned
+ this.$frame.height();
+ styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
+ styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
+};
+
/**
* Get the height of the dialog contents.
*
* @return {number} Content height
*/
OO.ui.Window.prototype.getContentHeight = function () {
- // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements
- var bodyHeight, oldHeight = this.$frame[0].style.height;
- this.$frame[0].style.height = '1px';
- bodyHeight = this.getBodyHeight();
- this.$frame[0].style.height = oldHeight;
+ var bodyHeight,
+ win = this,
+ bodyStyleObj = this.$body[0].style,
+ frameStyleObj = this.$frame[0].style;
+
+ // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
+ // Disable transitions first, otherwise we'll get values from when the window was animating.
+ this.withoutSizeTransitions( function () {
+ var oldHeight = frameStyleObj.height, oldPosition = bodyStyleObj.position;
+ frameStyleObj.height = '1px';
+ // Force body to resize to new width
+ bodyStyleObj.position = 'relative';
+ bodyHeight = win.getBodyHeight();
+ frameStyleObj.height = oldHeight;
+ bodyStyleObj.position = oldPosition;
+ } );
return Math.round(
// Add buffer for border
} else {
this.$content = this.$( '<div>' );
this.$document = $( this.getElementDocument() );
- this.$content.addClass( 'oo-ui-window-content' );
+ this.$content.addClass( 'oo-ui-window-content' ).attr( 'tabIndex', 0 );
this.$frame.append( this.$content );
}
this.toggle( false );
// Figure out directionality:
- this.dir = OO.ui.Element.getDir( this.$iframe || this.$content ) || 'ltr';
+ this.dir = OO.ui.Element.static.getDir( this.$iframe || this.$content ) || 'ltr';
return this;
};
* @chainable
*/
OO.ui.Window.prototype.setDimensions = function ( dim ) {
- // Apply width before height so height is not based on wrapping content using the wrong width
+ var height,
+ win = this,
+ styleObj = this.$frame[0].style;
+
+ // Calculate the height we need to set using the correct width
+ if ( dim.height === undefined ) {
+ this.withoutSizeTransitions( function () {
+ var oldWidth = styleObj.width;
+ win.$frame.css( 'width', dim.width || '' );
+ height = win.getContentHeight();
+ styleObj.width = oldWidth;
+ } );
+ } else {
+ height = dim.height;
+ }
+
this.$frame.css( {
width: dim.width || '',
minWidth: dim.minWidth || '',
- maxWidth: dim.maxWidth || ''
- } );
- this.$frame.css( {
- height: ( dim.height !== undefined ? dim.height : this.getContentHeight() ) || '',
+ maxWidth: dim.maxWidth || '',
+ height: height || '',
minHeight: dim.minHeight || '',
maxHeight: dim.maxHeight || ''
} );
+
return this;
};
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 ) {
doc.close();
// Properties
- this.$ = OO.ui.Element.getJQuery( doc, this.$iframe );
+ this.$ = OO.ui.Element.static.getJQuery( doc, this.$iframe );
this.$content = this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 );
this.$document = this.$( doc );
this.actions = new OO.ui.ActionSet();
this.attachedActions = [];
this.currentAction = null;
+ this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
// Events
this.actions.connect( this, {
OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
if ( e.which === OO.ui.Keys.ESCAPE ) {
this.close();
- return false;
+ e.preventDefault();
+ e.stopPropagation();
}
};
);
}
this.actions.add( items );
+
+ if ( this.constructor.static.escapable ) {
+ this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
+ }
}, this );
};
// Parent method
return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
.first( function () {
+ if ( this.constructor.static.escapable ) {
+ this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
+ }
+
this.actions.clear();
this.currentAction = null;
}, this );
// Properties
this.title = new OO.ui.LabelWidget( { $: this.$ } );
- // Events
- if ( this.constructor.static.escapable ) {
- this.$document.on( 'keydown', this.onDocumentKeyDown.bind( this ) );
- }
-
// Initialization
this.$content.addClass( 'oo-ui-dialog-content' );
this.setPendingElement( this.$head );
*
* @param {jQuery.Event} e Mouse wheel event
*/
-OO.ui.WindowManager.prototype.onWindowMouseWheel = function ( e ) {
- // Kill all events in the parent window if the child window is isolated,
- // or if the event didn't come from the child window
- return !( this.shouldIsolate() || !$.contains( this.getCurrentWindow().$frame[0], e.target ) );
+OO.ui.WindowManager.prototype.onWindowMouseWheel = function () {
+ // Kill all events in the parent window if the child window is isolated
+ return !this.shouldIsolate();
};
/**
case OO.ui.Keys.UP:
case OO.ui.Keys.RIGHT:
case OO.ui.Keys.DOWN:
- // Kill all events in the parent window if the child window is isolated,
- // or if the event didn't come from the child window
- return !( this.shouldIsolate() || !$.contains( this.getCurrentWindow().$frame[0], e.target ) );
+ // Kill all events in the parent window if the child window is isolated
+ return !this.shouldIsolate();
}
};
* @throws {Error} If windows being removed are not being managed
*/
OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
- var i, len, win, name,
+ var i, len, win, name, cleanupWindow,
manager = this,
promises = [],
cleanup = function ( name, win ) {
if ( !win ) {
throw new Error( 'Cannot remove window' );
}
- promises.push( this.closeWindow( name ).then( cleanup.bind( null, name, win ) ) );
+ cleanupWindow = cleanup.bind( null, name, win );
+ promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
}
return $.when.apply( $, promises );
return;
}
- var viewport = OO.ui.Element.getDimensions( win.getElementWindow() ),
+ var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
sizes = this.constructor.static.sizes,
size = win.getSize();
// Start listening for top-level window dimension changes
'orientationchange resize': this.onWindowResizeHandler
} );
+ // Disable window scrolling in isolated windows
+ if ( !this.shouldIsolate() ) {
+ $( this.getElementDocument().body ).css( 'overflow', 'hidden' );
+ }
this.globalEvents = true;
}
} else if ( this.globalEvents ) {
// Stop listening for top-level window dimension changes
'orientationchange resize': this.onWindowResizeHandler
} );
+ if ( !this.shouldIsolate() ) {
+ $( this.getElementDocument().body ).css( 'overflow', '' );
+ }
this.globalEvents = false;
}
/* Methods */
-/** */
+/**
+ * Get tools from the factory
+ *
+ * @param {Array} include Included tools
+ * @param {Array} exclude Excluded tools
+ * @param {Array} promote Promoted tools
+ * @param {Array} demote Demoted tools
+ * @return {string[]} List of tools
+ */
OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
var i, len, included, promoted, demoted,
auto = [],
this.$button
.removeClass( 'oo-ui-buttonElement-button' )
.removeAttr( 'role accesskey tabindex' )
- .off( this.onMouseDownHandler );
+ .off( 'mousedown', this.onMouseDownHandler );
}
this.$button = $button
return this.items.slice( 0 );
};
+/**
+ * Get an item by its data.
+ *
+ * Data is compared by a hash of its value. Only the first item with matching data will be returned.
+ *
+ * @param {Object} data Item data to search for
+ * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
+ */
+OO.ui.GroupElement.prototype.getItemFromData = function ( data ) {
+ var i, len, item,
+ hash = OO.getHash( data );
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ item = this.items[i];
+ if ( hash === OO.getHash( item.getData() ) ) {
+ return item;
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Get items by their data.
+ *
+ * Data is compared by a hash of its value. All items with matching data will be returned.
+ *
+ * @param {Object} data Item data to search for
+ * @return {OO.ui.Element[]} Items with equivalent data
+ */
+OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) {
+ var i, len, item,
+ hash = OO.getHash( data ),
+ items = [];
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ item = this.items[i];
+ if ( hash === OO.getHash( item.getData() ) ) {
+ items.push( item );
+ }
+ }
+
+ return items;
+};
+
/**
* Add an aggregate item event.
*
return this;
};
+/**
+ * A mixin for an element that can be dragged and dropped.
+ * Use in conjunction with DragGroupWidget
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ */
+OO.ui.DraggableElement = function OoUiDraggableElement() {
+ // Properties
+ this.index = null;
+
+ // 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 )
+ } );
+};
+
+/* Events */
+
+/**
+ * @event dragstart
+ * @param {OO.ui.DraggableElement} item Dragging item
+ */
+
+/**
+ * @event dragend
+ */
+
+/**
+ * @event drop
+ */
+
+/* Methods */
+
+/**
+ * Respond to dragstart event.
+ * @param {jQuery.Event} event jQuery event
+ * @fires dragstart
+ */
+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;
+};
+
+/**
+ * Respond to dragend event.
+ * @fires dragend
+ */
+OO.ui.DraggableElement.prototype.onDragEnd = function () {
+ this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
+ this.emit( 'dragend' );
+};
+
+/**
+ * Handle drop event.
+ * @param {jQuery.Event} event jQuery event
+ * @fires drop
+ */
+OO.ui.DraggableElement.prototype.onDrop = function ( e ) {
+ e.preventDefault();
+ this.emit( 'drop', e );
+};
+
+/**
+ * In order for drag/drop to work, the dragover event must
+ * return false and stop propogation.
+ */
+OO.ui.DraggableElement.prototype.onDragOver = function ( e ) {
+ e.preventDefault();
+};
+
+/**
+ * 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 );
+ }
+};
+
+/**
+ * Get item index
+ * @return {number} Item index
+ */
+OO.ui.DraggableElement.prototype.getIndex = function () {
+ return this.index;
+};
+
+/**
+ * Element containing a sequence of child elements that can be dragged
+ * and dropped.
+ *
+ * @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.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.GroupElement.call( this, config );
+
+ // Properties
+ this.orientation = config.orientation || 'vertical';
+ this.dragItem = null;
+ this.itemDragOver = null;
+ this.itemKeys = {};
+ this.sideInsertion = '';
+
+ // Events
+ this.aggregate( {
+ dragstart: 'itemDragStart',
+ dragend: 'itemDragEnd',
+ drop: 'itemDrop'
+ } );
+ this.connect( this, {
+ itemDragStart: 'onItemDragStart',
+ itemDrop: 'onItemDrop',
+ itemDragEnd: 'onItemDragEnd'
+ } );
+ this.$element.on( {
+ dragover: $.proxy( this.onDragOver, this ),
+ dragleave: $.proxy( this.onDragLeave, this )
+ } );
+
+ // Initialize
+ if ( $.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+ this.$placeholder = $( '<div>' )
+ .addClass( 'oo-ui-draggableGroupElement-placeholder' );
+ this.$element
+ .addClass( 'oo-ui-draggableGroupElement' )
+ .append( this.$status )
+ .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
+ .prepend( this.$placeholder );
+};
+
+/* Setup */
+OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
+
+/* Events */
+
+/**
+ * @event reorder
+ * @param {OO.ui.DraggableElement} item Reordered item
+ * @param {number} [newIndex] New index for the item
+ */
+
+/* Methods */
+
+/**
+ * Respond to item drag start event
+ * @param {OO.ui.DraggableElement} item Dragged item
+ */
+OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
+ var i, len;
+
+ // Map the index of each object
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ this.items[i].setIndex( i );
+ }
+
+ if ( this.orientation === 'horizontal' ) {
+ // Set the height of the indicator
+ this.$placeholder.css( {
+ height: item.$element.outerHeight(),
+ width: 2
+ } );
+ } else {
+ // Set the width of the indicator
+ this.$placeholder.css( {
+ height: 2,
+ width: item.$element.outerWidth()
+ } );
+ }
+ this.setDragItem( item );
+};
+
+/**
+ * Respond to item drag end event
+ */
+OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
+ this.unsetDragItem();
+ return false;
+};
+
+/**
+ * Handle drop event and switch the order of the items accordingly
+ * @param {OO.ui.DraggableElement} item Dropped item
+ * @fires reorder
+ */
+OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
+ var toIndex = item.getIndex();
+ // Check if the dropped item is from the current group
+ // TODO: Figure out a way to configure a list of legally droppable
+ // elements even if they are not yet in the list
+ if ( this.getDragItem() ) {
+ // If the insertion point is 'after', the insertion index
+ // is shifted to the right (or to the left in RTL, hence 'after')
+ if ( this.sideInsertion === 'after' ) {
+ toIndex++;
+ }
+ // Emit change event
+ this.emit( 'reorder', this.getDragItem(), toIndex );
+ }
+ // Return false to prevent propogation
+ return false;
+};
+
+/**
+ * Handle dragleave event.
+ */
+OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
+ // This means the item was dragged outside the widget
+ this.$placeholder
+ .css( 'left', 0 )
+ .hide();
+};
+
+/**
+ * Respond to dragover event
+ * @param {jQuery.Event} event Event details
+ */
+OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
+ var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
+ itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
+ clientX = e.originalEvent.clientX,
+ clientY = e.originalEvent.clientY;
+
+ // Get the OptionWidget item we are dragging over
+ dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
+ $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
+ if ( $optionWidget[0] ) {
+ itemOffset = $optionWidget.offset();
+ itemBoundingRect = $optionWidget[0].getBoundingClientRect();
+ itemPosition = $optionWidget.position();
+ itemIndex = $optionWidget.data( 'index' );
+ }
+
+ if (
+ itemOffset &&
+ this.isDragging() &&
+ itemIndex !== this.getDragItem().getIndex()
+ ) {
+ if ( this.orientation === 'horizontal' ) {
+ // Calculate where the mouse is relative to the item width
+ itemSize = itemBoundingRect.width;
+ itemMidpoint = itemBoundingRect.left + itemSize / 2;
+ dragPosition = clientX;
+ // Which side of the item we hover over will dictate
+ // where the placeholder will appear, on the left or
+ // on the right
+ cssOutput = {
+ left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
+ top: itemPosition.top
+ };
+ } else {
+ // Calculate where the mouse is relative to the item height
+ itemSize = itemBoundingRect.height;
+ itemMidpoint = itemBoundingRect.top + itemSize / 2;
+ dragPosition = clientY;
+ // Which side of the item we hover over will dictate
+ // where the placeholder will appear, on the top or
+ // on the bottom
+ cssOutput = {
+ top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
+ left: itemPosition.left
+ };
+ }
+ // Store whether we are before or after an item to rearrange
+ // For horizontal layout, we need to account for RTL, as this is flipped
+ if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
+ this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
+ } else {
+ this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
+ }
+ // Add drop indicator between objects
+ if ( this.sideInsertion ) {
+ this.$placeholder
+ .css( cssOutput )
+ .show();
+ } else {
+ this.$placeholder
+ .css( {
+ left: 0,
+ top: 0
+ } )
+ .hide();
+ }
+ } else {
+ // This means the item was dragged outside the widget
+ this.$placeholder
+ .css( 'left', 0 )
+ .hide();
+ }
+ // Prevent default
+ e.preventDefault();
+};
+
+/**
+ * Set a dragged item
+ * @param {OO.ui.DraggableElement} item Dragged item
+ */
+OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
+ this.dragItem = item;
+};
+
+/**
+ * Unset the current dragged item
+ */
+OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
+ this.dragItem = null;
+ this.itemDragOver = null;
+ this.$placeholder.hide();
+ this.sideInsertion = '';
+};
+
+/**
+ * Get the current dragged item
+ * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
+ */
+OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
+ return this.dragItem;
+};
+
+/**
+ * Check if there's an item being dragged.
+ * @return {Boolean} Item is being dragged
+ */
+OO.ui.DraggableGroupElement.prototype.isDragging = function () {
+ return this.getDragItem() !== null;
+};
+
/**
* Element containing an icon.
*
return this.icon;
};
+/**
+ * Get icon title.
+ *
+ * @return {string} Icon title text
+ */
+OO.ui.IconElement.prototype.getIconTitle = function () {
+ return this.iconTitle;
+};
+
/**
* Element containing an indicator.
*
} else {
this.$label.empty();
}
- this.$label.css( 'display', !label ? 'none' : '' );
};
/**
*
* @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 ) {
this.clipping = clipping;
if ( clipping ) {
this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
- // If the clippable container is the body, we have to listen to scroll events and check
+ // If the clippable container is the root, we have to listen to scroll events and check
// jQuery.scrollTop on the window because of browser inconsistencies
- this.$clippableScroller = this.$clippableContainer.is( 'body' ) ?
- this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) :
+ this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
+ this.$( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
this.$clippableContainer;
this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
this.$clippableWindow = this.$( this.getElementWindow() )
return this;
}
- var buffer = 10,
+ var buffer = 7, // Chosen by fair dice roll
cOffset = this.$clippable.offset(),
- $container = this.$clippableContainer.is( 'body' ) ?
+ $container = this.$clippableContainer.is( 'html, body' ) ?
this.$clippableWindow : this.$clippableContainer,
ccOffset = $container.offset() || { top: 0, left: 0 },
ccHeight = $container.innerHeight() - buffer,
ccWidth = $container.innerWidth() - buffer,
+ cHeight = this.$clippable.outerHeight() + buffer,
+ cWidth = this.$clippable.outerWidth() + buffer,
scrollTop = this.$clippableScroller.scrollTop(),
scrollLeft = this.$clippableScroller.scrollLeft(),
- desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
- desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
+ desiredWidth = cOffset.left < 0 ?
+ cWidth + cOffset.left :
+ ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
+ desiredHeight = cOffset.top < 0 ?
+ cHeight + cOffset.top :
+ ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
naturalWidth = this.$clippable.prop( 'scrollWidth' ),
naturalHeight = this.$clippable.prop( 'scrollHeight' ),
clipWidth = desiredWidth < naturalWidth,
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 ) {
/* Methods */
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
+ OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
+
+ // Events
+ this.manager.connect( this, {
+ resize: 'onResize'
+ } );
+
+ return this;
+};
+
/**
* @inheritdoc
*/
OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
this.fitActions();
- return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
+ return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
+};
+
+/**
+ * Handle window resized events.
+ */
+OO.ui.MessageDialog.prototype.onResize = function () {
+ var dialog = this;
+ dialog.fitActions();
+ // Wait for CSS transition to finish and do it again :(
+ setTimeout( function () {
+ dialog.fitActions();
+ }, 300 );
};
/**
* @inheritdoc
*/
OO.ui.MessageDialog.prototype.getBodyHeight = function () {
- return Math.round( this.text.$element.outerHeight( true ) );
+ var bodyHeight, oldOverflow,
+ $scrollable = this.container.$element;
+
+ oldOverflow = $scrollable[0].style.overflow;
+ $scrollable[0].style.overflow = 'hidden';
+
+ // Force… ugh… something to happen
+ $scrollable.contents().hide();
+ $scrollable.height();
+ $scrollable.contents().show();
+
+ bodyHeight = Math.round( this.text.$element.outerHeight( true ) );
+ $scrollable[0].style.overflow = oldOverflow;
+
+ return bodyHeight;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
+ var $scrollable = this.container.$element;
+ OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
+
+ // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
+ // Need to do it after transition completes (250ms), add 50ms just in case.
+ setTimeout( function () {
+ var oldOverflow = $scrollable[0].style.overflow;
+ $scrollable[0].style.overflow = 'hidden';
+
+ // Force… ugh… something to happen
+ $scrollable.contents().hide();
+ $scrollable.height();
+ $scrollable.contents().show();
+
+ $scrollable[0].style.overflow = oldOverflow;
+ }, 300 );
+
+ return this;
};
/**
special.primary.toggleFramed( false );
}
- this.fitActions();
if ( !this.isOpening() ) {
+ // If the dialog is currently opening, this will be called automatically soon.
+ // This also calls #fitActions.
this.manager.updateWindowSize( this );
}
- this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
};
/**
*/
OO.ui.MessageDialog.prototype.fitActions = function () {
var i, len, action,
+ previous = this.verticalActionLayout,
actions = this.actions.get();
// Detect clipping
break;
}
}
+
+ if ( this.verticalActionLayout !== previous ) {
+ this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
+ // We changed the layout, window height might need to be updated.
+ this.manager.updateWindowSize( this );
+ }
};
/**
}
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
* @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
*/
OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
- var $input, layout = this;
+ var layout = this;
if ( page ) {
page.scrollElementIntoView( { complete: function () {
if ( layout.autoFocus ) {
- // Set focus to the first input if nothing on the page is focused yet
- if ( !page.$element.find( ':focus' ).length ) {
- $input = page.$element.find( ':input:first' );
- if ( $input.length ) {
- $input[0].focus();
- }
- }
+ layout.focus();
}
} } );
}
};
+/**
+ * Focus the first input in the current page.
+ *
+ * If no page is selected, the first selectable page will be selected.
+ * If the focus is already in an element on the current page, nothing will happen.
+ */
+OO.ui.BookletLayout.prototype.focus = function () {
+ var $input, page = this.stackLayout.getCurrentItem();
+ if ( !page && this.outlined ) {
+ this.selectFirstSelectablePage();
+ page = this.stackLayout.getCurrentItem();
+ if ( !page ) {
+ return;
+ }
+ }
+ // Only change the focus if is not already in the current page
+ if ( !page.$element.find( ':focus' ).length ) {
+ $input = page.$element.find( ':input:first' );
+ if ( $input.length ) {
+ $input[0].focus();
+ }
+ }
+};
+
/**
* Handle outline widget select events.
*
*
* @return {string|null} Current page name
*/
-OO.ui.BookletLayout.prototype.getPageName = function () {
+OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
return this.currentPageName;
};
name = page.getName();
this.pages[page.getName()] = page;
if ( this.outlined ) {
- item = new OO.ui.OutlineOptionWidget( name, page, { $: this.$ } );
+ item = new OO.ui.OutlineOptionWidget( { $: this.$, data: name } );
page.setOutlineItem( item );
items.push( item );
}
if ( this.outlined && items.length ) {
this.outlineSelectWidget.addItems( items, index );
- this.updateOutlineSelectWidget();
+ this.selectFirstSelectablePage();
}
this.stackLayout.addItems( pages, index );
this.emit( 'add', pages, index );
}
if ( this.outlined && items.length ) {
this.outlineSelectWidget.removeItems( items );
- this.updateOutlineSelectWidget();
+ this.selectFirstSelectablePage();
}
this.stackLayout.removeItems( pages );
this.emit( 'remove', pages );
};
/**
- * Call this after adding or removing items from the OutlineSelectWidget.
+ * Select the first selectable page.
*
* @chainable
*/
-OO.ui.BookletLayout.prototype.updateOutlineSelectWidget = function () {
- // Auto-select first item when nothing is selected anymore
+OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
if ( !this.outlineSelectWidget.getSelectedItem() ) {
this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
}
* @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
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 ) {
* @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 ) {
// '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();
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
} );
*/
OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
// Configuration initialization
- config = $.extend( { target: '_blank' }, config );
+ config = config || {};
// Parent constructor
OO.ui.ButtonWidget.super.call( this, config );
* @chainable
*/
OO.ui.InputWidget.prototype.setValue = function ( value ) {
- value = this.sanitizeValue( value );
+ value = this.cleanUpValue( value );
+ // Update the DOM if it has changed. Note that with cleanUpValue, it
+ // is possible for the DOM value to change without this.value changing.
+ if ( this.$input.val() !== value ) {
+ this.$input.val( value );
+ }
if ( this.value !== value ) {
this.value = value;
this.emit( 'change', this.value );
}
- // Update the DOM if it has changed. Note that with sanitizeValue, it
- // is possible for the DOM value to change without this.value changing.
- if ( this.$input.val() !== this.value ) {
- this.$input.val( this.value );
- }
return this;
};
/**
- * Sanitize incoming value.
+ * Clean up incoming value.
*
* Ensures value is a string, and converts undefined and null to empty string.
*
* @private
* @param {string} value Original value
- * @return {string} Sanitized value
+ * @return {string} Cleaned up value
*/
-OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) {
+OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
if ( value === undefined || value === null ) {
return '';
} else if ( this.inputFilter ) {
*
* @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 */
};
/**
- * Get checked state of the checkbox
- *
- * @return {boolean} If the checkbox is checked
+ * @inheritdoc
*/
-OO.ui.CheckboxInputWidget.prototype.getValue = function () {
- return this.value;
+OO.ui.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 checked state of the checkbox
+ * Set selection state of this checkbox.
*
- * @param {boolean} value New value
+ * @param {boolean} state Whether the checkbox is selected
+ * @chainable
*/
-OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) {
- value = !!value;
- if ( this.value !== value ) {
- this.value = value;
- this.$input.prop( 'checked', this.value );
- this.emit( 'change', this.value );
+OO.ui.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;
+};
+
+/**
+ * Check if this checkbox is selected.
+ *
+ * @return {boolean} Checkbox is selected
+ */
+OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
+ return this.selected;
+};
+
+/**
+ * Radio input widget.
+ *
+ * Radio buttons only make sense as a set, and you probably want to use the OO.ui.RadioSelectWidget
+ * class instead of using this class directly.
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [selected=false] Whether the radio button is initially selected
+ */
+OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
+ // Parent constructor
+ OO.ui.RadioInputWidget.super.call( this, config );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-radioInputWidget' );
+ this.setSelected( config.selected !== undefined ? config.selected : false );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
+
+/* Methods */
+
+/**
+ * Get input element.
+ *
+ * @private
+ * @return {jQuery} Input element
+ */
+OO.ui.RadioInputWidget.prototype.getInputElement = function () {
+ return this.$( '<input type="radio" />' );
};
/**
* @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.RadioInputWidget.prototype.onEdit = function () {
+ // RadioInputWidget doesn't track its state.
+};
+
+/**
+ * Set selection state of this radio button.
+ *
+ * @param {boolean} state Whether the button is selected
+ * @chainable
+ */
+OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
+ // RadioInputWidget doesn't track its state.
+ this.$input.prop( 'checked', state );
+ return this;
+};
+
+/**
+ * Check if this radio button is selected.
+ *
+ * @return {boolean} Radio is selected
+ */
+OO.ui.RadioInputWidget.prototype.isSelected = function () {
+ return this.$input.prop( 'checked' );
};
/**
this.maxRows = config.maxRows !== undefined ? config.maxRows : 10;
this.validate = null;
+ // Clone for resizing
+ if ( this.autosize ) {
+ this.$clone = this.$input
+ .clone()
+ .insertAfter( this.$input )
+ .hide();
+ }
+
this.setValidation( config.validate );
// Events
*/
OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
- this.emit( 'enter' );
+ this.emit( 'enter', e );
}
};
* @chainable
*/
OO.ui.TextInputWidget.prototype.adjustSize = function () {
- var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
+ var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
- if ( this.multiline && this.autosize ) {
- $clone = this.$input.clone()
+ if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
+ this.$clone
.val( this.$input.val() )
+ .attr( 'rows', '' )
// Set inline height property to 0 to measure scroll height
- .css( { height: 0 } )
- .insertAfter( this.$input );
- scrollHeight = $clone[0].scrollHeight;
+ .css( 'height', 0 );
+
+ this.$clone[0].style.display = 'block';
+
+ this.valCache = this.$input.val();
+
+ scrollHeight = this.$clone[0].scrollHeight;
+
// Remove inline height property to measure natural heights
- $clone.css( 'height', '' );
- innerHeight = $clone.innerHeight();
- outerHeight = $clone.outerHeight();
+ this.$clone.css( 'height', '' );
+ innerHeight = this.$clone.innerHeight();
+ outerHeight = this.$clone.outerHeight();
+
// Measure max rows height
- $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' ).val( '' );
- maxInnerHeight = $clone.innerHeight();
+ this.$clone
+ .attr( 'rows', this.maxRows )
+ .css( 'height', 'auto' )
+ .val( '' );
+ maxInnerHeight = this.$clone.innerHeight();
+
// Difference between reported innerHeight and scrollHeight with no scrollbars present
// Equals 1 on Blink-based browsers and 0 everywhere else
- measurementError = maxInnerHeight - $clone[0].scrollHeight;
- $clone.remove();
+ measurementError = maxInnerHeight - this.$clone[0].scrollHeight;
idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
+
+ this.$clone[0].style.display = 'none';
+
// Only apply inline height when expansion beyond natural height is needed
if ( idealHeight > innerHeight ) {
// Use the difference between the inner and outer height as a buffer
) );
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()
/* Methods */
+/**
+ * Get the combobox's menu.
+ * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
+ */
+OO.ui.ComboBoxWidget.prototype.getMenu = function () {
+ return this.menu;
+};
+
/**
* Handle input change events.
*
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {OO.ui.InputWidget} [input] Input widget this label is for
*/
OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
// Configuration initialization
* @mixins OO.ui.FlaggedElement
*
* @constructor
- * @param {Mixed} data Option data
* @param {Object} [config] Configuration options
*/
-OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
+OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
// Configuration initialization
config = config || {};
OO.ui.FlaggedElement.call( this, config );
// Properties
- this.data = data;
this.selected = false;
this.highlighted = false;
this.pressed = false;
return deferred.promise();
};
-/**
- * Get option data.
- *
- * @return {Mixed} Option data
- */
-OO.ui.OptionWidget.prototype.getData = function () {
- return this.data;
-};
-
/**
* Option widget with an option icon and indicator.
*
* @mixins OO.ui.IndicatorElement
*
* @constructor
- * @param {Mixed} data Option data
* @param {Object} [config] Configuration options
*/
-OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( data, config ) {
+OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
// Parent constructor
- OO.ui.DecoratedOptionWidget.super.call( this, data, config );
+ OO.ui.DecoratedOptionWidget.super.call( this, config );
// Mixin constructors
OO.ui.IconElement.call( this, config );
* @mixins OO.ui.ButtonElement
*
* @constructor
- * @param {Mixed} data Option data
* @param {Object} [config] Configuration options
*/
-OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
+OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
// Parent constructor
- OO.ui.ButtonOptionWidget.super.call( this, data, config );
+ OO.ui.ButtonOptionWidget.super.call( this, config );
// Mixin constructors
OO.ui.ButtonElement.call( this, config );
return this;
};
+/**
+ * Option widget that looks like a radio button.
+ *
+ * Use together with OO.ui.RadioSelectWidget.
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
+ // Parent constructor
+ OO.ui.RadioOptionWidget.super.call( this, config );
+
+ // Properties
+ this.radio = new OO.ui.RadioInputWidget( { value: config.data } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-radioOptionWidget' )
+ .prepend( this.radio.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
+
+/* Static Properties */
+
+OO.ui.RadioOptionWidget.static.highlightable = false;
+
+OO.ui.RadioOptionWidget.static.pressable = false;
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
+ OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
+
+ this.radio.setSelected( state );
+
+ return this;
+};
+
/**
* Item of an OO.ui.MenuSelectWidget.
*
* @extends OO.ui.DecoratedOptionWidget
*
* @constructor
- * @param {Mixed} data Item data
* @param {Object} [config] Configuration options
*/
-OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( data, config ) {
+OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
// Configuration initialization
config = $.extend( { icon: 'check' }, config );
// Parent constructor
- OO.ui.MenuOptionWidget.super.call( this, data, config );
+ OO.ui.MenuOptionWidget.super.call( this, config );
// Initialization
this.$element
* @extends OO.ui.DecoratedOptionWidget
*
* @constructor
- * @param {Mixed} data Item data
* @param {Object} [config] Configuration options
*/
-OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( data, config ) {
+OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
// Parent constructor
- OO.ui.MenuSectionOptionWidget.super.call( this, data, config );
+ OO.ui.MenuSectionOptionWidget.super.call( this, config );
// Initialization
this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
* @extends OO.ui.DecoratedOptionWidget
*
* @constructor
- * @param {Mixed} data Item data
* @param {Object} [config] Configuration options
* @cfg {number} [level] Indentation level
* @cfg {boolean} [movable] Allow modification from outline controls
*/
-OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( data, config ) {
+OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
// Configuration initialization
config = config || {};
// Parent constructor
- OO.ui.OutlineOptionWidget.super.call( this, data, config );
+ OO.ui.OutlineOptionWidget.super.call( this, config );
// Properties
this.level = 0;
/**
* Generic selection of options.
*
- * Items can contain any rendering, and are uniquely identified by a hash of their data. Any widget
- * that provides options, from which the user must choose one, should be built on this class.
+ * Items can contain any rendering. Any widget that provides options, from which the user must
+ * choose one, should be built on this class.
*
* Use together with OO.ui.OptionWidget.
*
// Properties
this.pressed = false;
this.selecting = null;
- this.hashes = {};
this.onMouseUpHandler = this.onMouseUp.bind( this );
this.onMouseMoveHandler = this.onMouseMove.bind( this );
return null;
};
-/**
- * Get an existing item with equivalent data.
- *
- * @param {Object} data Item data to search for
- * @return {OO.ui.OptionWidget|null} Item with equivalent value, `null` if none exists
- */
-OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
- var hash = OO.getHash( data );
-
- if ( Object.prototype.hasOwnProperty.call( this.hashes, hash ) ) {
- return this.hashes[hash];
- }
-
- return null;
-};
-
/**
* Toggle pressed state.
*
/**
* Get an item relative to another one.
*
- * @param {OO.ui.OptionWidget} item Item to start at
- * @param {number} direction Direction to move in, -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;
};
/**
* Add items.
*
- * When items are added with the same values as existing items, the existing items will be
- * automatically removed before the new items are added.
- *
* @param {OO.ui.OptionWidget[]} items Items to add
* @param {number} [index] Index to insert items after
* @fires add
* @chainable
*/
OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
- var i, len, item, hash,
- remove = [];
-
- for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
- hash = OO.getHash( item.getData() );
- if ( Object.prototype.hasOwnProperty.call( this.hashes, hash ) ) {
- // Remove item with same value
- remove.push( this.hashes[hash] );
- }
- this.hashes[hash] = item;
- }
- if ( remove.length ) {
- this.removeItems( remove );
- }
-
// Mixin method
OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
* @chainable
*/
OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
- var i, len, item, hash;
+ var i, len, item;
+ // Deselect items being removed
for ( i = 0, len = items.length; i < len; i++ ) {
item = items[i];
- hash = OO.getHash( item.getData() );
- if ( Object.prototype.hasOwnProperty.call( this.hashes, hash ) ) {
- // Remove existing item
- delete this.hashes[hash];
- }
if ( item.isSelected() ) {
this.selectItem( null );
}
OO.ui.SelectWidget.prototype.clearItems = function () {
var items = this.items.slice();
- // Clear all items
- this.hashes = {};
// Mixin method
OO.ui.GroupWidget.prototype.clearItems.call( this );
+
+ // Clear selection
this.selectItem( null );
this.emit( 'remove', items );
OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
+/**
+ * Select widget containing radio button options.
+ *
+ * Use together with OO.ui.RadioOptionWidget.
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
+ // Parent constructor
+ OO.ui.RadioSelectWidget.super.call( this, config );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-radioSelectWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
+
/**
* Overlaid menu of options.
*
*/
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();