/*!
- * OOjs UI v0.1.0-pre (55b861b167)
+ * OOjs UI v0.1.0-pre (f9c217dfa4)
* 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-08-21T16:59:20Z
+ * Date: 2014-09-10T17:25:40Z
*/
( function ( OO ) {
styleText = '@import url(' + styleNode.href + ');';
} else {
// Internal stylesheet; just copy the text
- styleText = styleNode.textContent;
+ // For IE10 we need to fall back to .cssText, BUT that's undefined in
+ // other browsers, so fall back to '' rather than 'undefined'
+ styleText = styleNode.textContent || parentDoc.styleSheets[i].cssText || '';
}
// Create a node with a unique ID that we're going to monitor to see when the CSS
/* Methods */
+/**
+ * Handle mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.Window.prototype.onMouseDown = function ( e ) {
+ // Prevent clicking on the click-block from stealing focus
+ if ( e.target === this.$element[0] ) {
+ return false;
+ }
+};
+
/**
* Check if window has been initialized.
*
this.$foot = this.$( '<div>' );
this.$overlay = this.$( '<div>' );
+ // Events
+ this.$element.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
+
// Initialization
this.$head.addClass( 'oo-ui-window-head' );
this.$body.addClass( 'oo-ui-window-body' );
doc.write(
'<!doctype html>' +
'<html>' +
- '<body class="oo-ui-window-isolated oo-ui-window-content oo-ui-' + this.dir + '"' +
+ '<body class="oo-ui-window-isolated oo-ui-' + this.dir + '"' +
' style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
+ '<div class="oo-ui-window-content"></div>' +
'</body>' +
'</html>'
);
* @abstract
* @class
* @extends OO.ui.Window
- * @mixins OO.ui.LabeledElement
*
* @constructor
* @param {Object} [config] Configuration options
this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this );
this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this );
- // Events
- this.$element.on( 'mousedown', false );
-
// Initialization
this.$element
.addClass( 'oo-ui-windowManager' )
// Ensure handlers get called after preparingToOpen is set
this.preparingToOpen.done( function () {
if ( manager.modal ) {
- manager.$( manager.getElementDocument() ).on( {
- // Prevent scrolling by keys in top-level window
- keydown: manager.onDocumentKeyDownHandler
- } );
- manager.$( manager.getElementWindow() ).on( {
- // Prevent scrolling by wheel in top-level window
- mousewheel: manager.onWindowMouseWheelHandler,
- // Start listening for top-level window dimension changes
- 'orientationchange resize': manager.onWindowResizeHandler
- } );
- // Hide other content from screen readers
- manager.$ariaHidden = $( 'body' )
- .children()
- .not( manager.$element.parentsUntil( 'body' ).last() )
- .attr( 'aria-hidden', '' );
+ manager.toggleGlobalEvents( true );
+ manager.toggleAriaIsolation( true );
}
manager.currentWindow = win;
manager.opening = opening;
*
* @param {OO.ui.Window|string} win Window object or symbolic name of window to close
* @param {Object} [data] Window closing data
- * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-closing}
+ * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
* for more details about the `closing` promise
* @throws {Error} If no window by that name is being managed
* @fires closing
win.teardown( data ).then( function () {
closing.notify( { state: 'teardown' } );
if ( manager.modal ) {
- manager.$( manager.getElementDocument() ).off( {
- // Allow scrolling by keys in top-level window
- keydown: manager.onDocumentKeyDownHandler
- } );
- manager.$( manager.getElementWindow() ).off( {
- // Allow scrolling by wheel in top-level window
- mousewheel: manager.onWindowMouseWheelHandler,
- // Stop listening for top-level window dimension changes
- 'orientationchange resize': manager.onWindowResizeHandler
- } );
- }
- // Restore screen reader visiblity
- if ( manager.$ariaHidden ) {
- manager.$ariaHidden.removeAttr( 'aria-hidden' );
- manager.$ariaHidden = null;
+ manager.toggleGlobalEvents( false );
+ manager.toggleAriaIsolation( false );
}
manager.closing = null;
manager.currentWindow = null;
return this;
};
+/**
+ * Bind or unbind global events for scrolling.
+ *
+ * @param {boolean} [on] Bind global events
+ * @chainable
+ */
+OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
+ on = on === undefined ? !!this.globalEvents : !!on;
+
+ if ( on ) {
+ if ( !this.globalEvents ) {
+ this.$( this.getElementDocument() ).on( {
+ // Prevent scrolling by keys in top-level window
+ keydown: this.onDocumentKeyDownHandler
+ } );
+ this.$( this.getElementWindow() ).on( {
+ // Prevent scrolling by wheel in top-level window
+ mousewheel: this.onWindowMouseWheelHandler,
+ // Start listening for top-level window dimension changes
+ 'orientationchange resize': this.onWindowResizeHandler
+ } );
+ this.globalEvents = true;
+ }
+ } else if ( this.globalEvents ) {
+ // Unbind global events
+ this.$( this.getElementDocument() ).off( {
+ // Allow scrolling by keys in top-level window
+ keydown: this.onDocumentKeyDownHandler
+ } );
+ this.$( this.getElementWindow() ).off( {
+ // Allow scrolling by wheel in top-level window
+ mousewheel: this.onWindowMouseWheelHandler,
+ // Stop listening for top-level window dimension changes
+ 'orientationchange resize': this.onWindowResizeHandler
+ } );
+ this.globalEvents = false;
+ }
+
+ return this;
+};
+
+/**
+ * Toggle screen reader visibility of content other than the window manager.
+ *
+ * @param {boolean} [isolate] Make only the window manager visible to screen readers
+ * @chainable
+ */
+OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
+ isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
+
+ if ( isolate ) {
+ if ( !this.$ariaHidden ) {
+ // Hide everything other than the window manager from screen readers
+ this.$ariaHidden = $( 'body' )
+ .children()
+ .not( this.$element.parentsUntil( 'body' ).last() )
+ .attr( 'aria-hidden', '' );
+ }
+ } else if ( this.$ariaHidden ) {
+ // Restore screen reader visiblity
+ this.$ariaHidden.removeAttr( 'aria-hidden' );
+ this.$ariaHidden = null;
+ }
+
+ return this;
+};
+
+/**
+ * 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.$element.remove();
+};
+
/**
* @abstract
* @class
* @class
*
* @constructor
- * @param {jQuery} $button Button node, assigned to #$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 null to have no tabIndex
* @cfg {string} [accessKey] Button's access key
*/
-OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) {
+OO.ui.ButtonElement = function OoUiButtonElement( config ) {
// Configuration initialization
config = config || {};
// Properties
- this.$button = $button;
- this.tabIndex = null;
+ this.$button = null;
this.framed = null;
+ this.tabIndex = null;
+ this.accessKey = null;
this.active = false;
this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
-
- // Events
- this.$button.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
+ this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this );
// Initialization
- this.$element.addClass( 'oo-ui-buttonedElement' );
- this.$button
- .addClass( 'oo-ui-buttonedElement-button' )
- .attr( 'role', 'button' );
+ this.$element.addClass( 'oo-ui-buttonElement' );
+ this.toggleFramed( config.framed === undefined || config.framed );
this.setTabIndex( config.tabIndex || 0 );
this.setAccessKey( config.accessKey );
- this.toggleFramed( config.framed === undefined || config.framed );
+ this.setButtonElement( config.$button || this.$( '<a>' ) );
};
/* Setup */
-OO.initClass( OO.ui.ButtonedElement );
+OO.initClass( OO.ui.ButtonElement );
/* Static Properties */
* @inheritable
* @property {boolean}
*/
-OO.ui.ButtonedElement.static.cancelButtonMouseDownEvents = true;
+OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
/* Methods */
+/**
+ * Set the button element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $button Element to use as button
+ */
+OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
+ if ( this.$button ) {
+ this.$button
+ .removeClass( 'oo-ui-buttonElement-button' )
+ .removeAttr( 'role accesskey tabindex' )
+ .off( this.onMouseDownHandler );
+ }
+
+ this.$button = $button
+ .addClass( 'oo-ui-buttonElement-button' )
+ .attr( { role: 'button', accesskey: this.accessKey, tabindex: this.tabIndex } )
+ .on( 'mousedown', this.onMouseDownHandler );
+};
+
/**
* Handles mouse down events.
*
* @param {jQuery.Event} e Mouse down event
*/
-OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) {
+OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
if ( this.isDisabled() || e.which !== 1 ) {
return false;
}
- // tabIndex should generally be interacted with via the property, but it's not possible to
- // reliably unset a tabIndex via a property so we use the (lowercase) "tabindex" attribute
- this.tabIndex = this.$button.attr( 'tabindex' );
// 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-buttonedElement-pressed' );
+ 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 );
*
* @param {jQuery.Event} e Mouse up event
*/
-OO.ui.ButtonedElement.prototype.onMouseUp = function ( e ) {
+OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
if ( this.isDisabled() || e.which !== 1 ) {
return false;
}
// Restore the tab-index after the button is up to restore the button's accesssibility
this.$button.attr( 'tabindex', this.tabIndex );
- this.$element.removeClass( 'oo-ui-buttonedElement-pressed' );
+ this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
// Stop listening for mouseup, since we only needed this once
this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
};
* @param {boolean} [framed] Make button framed, omit to toggle
* @chainable
*/
-OO.ui.ButtonedElement.prototype.toggleFramed = function ( framed ) {
+OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) {
framed = framed === undefined ? !this.framed : !!framed;
if ( framed !== this.framed ) {
this.framed = framed;
this.$element
- .toggleClass( 'oo-ui-buttonedElement-frameless', !framed )
- .toggleClass( 'oo-ui-buttonedElement-framed', framed );
+ .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
+ .toggleClass( 'oo-ui-buttonElement-framed', framed );
}
return this;
* @param {number|null} tabIndex Button's tab index, use null to remove
* @chainable
*/
-OO.ui.ButtonedElement.prototype.setTabIndex = function ( tabIndex ) {
- if ( typeof tabIndex === 'number' && tabIndex >= 0 ) {
- this.$button.attr( 'tabindex', tabIndex );
- } else {
- this.$button.removeAttr( 'tabindex' );
+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
+ * Set access key.
*
* @param {string} accessKey Button's access key, use empty string to remove
* @chainable
*/
-OO.ui.ButtonedElement.prototype.setAccessKey = function ( accessKey ) {
- if ( typeof accessKey === 'string' && accessKey.length ) {
- this.$button.attr( 'accesskey', accessKey );
- } else {
- this.$button.removeAttr( 'accesskey' );
+OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
+ accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
+
+ if ( this.accessKey !== accessKey ) {
+ if ( this.$button ) {
+ if ( accessKey !== null ) {
+ this.$button.attr( 'accesskey', accessKey );
+ } else {
+ this.$button.removeAttr( 'accesskey' );
+ }
+ }
+ this.accessKey = accessKey;
}
+
return this;
};
* @param {boolean} [value] Make button active
* @chainable
*/
-OO.ui.ButtonedElement.prototype.setActive = function ( value ) {
- this.$element.toggleClass( 'oo-ui-buttonedElement-active', !!value );
+OO.ui.ButtonElement.prototype.setActive = function ( value ) {
+ this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
return this;
};
/**
- * Element that can be automatically clipped to visible boundaies.
+ * Element containing a sequence of child elements.
*
* @abstract
* @class
*
* @constructor
- * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable
* @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
*/
-OO.ui.ClippableElement = function OoUiClippableElement( $clippable, config ) {
- // Configuration initialization
+OO.ui.GroupElement = function OoUiGroupElement( config ) {
+ // Configuration
config = config || {};
// Properties
- this.$clippable = $clippable;
- this.clipping = false;
- this.clipped = false;
- this.$clippableContainer = null;
- this.$clippableScroller = null;
- this.$clippableWindow = null;
- this.idealWidth = null;
- this.idealHeight = null;
- this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this );
- this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this );
+ this.$group = null;
+ this.items = [];
+ this.aggregateItemEvents = {};
// Initialization
- this.$clippable.addClass( 'oo-ui-clippableElement-clippable' );
+ this.setGroupElement( config.$group || this.$( '<div>' ) );
};
/* Methods */
/**
- * Set clipping.
+ * Set the group element.
*
- * @param {boolean} value Enable clipping
- * @chainable
+ * If an element is already set, items will be moved to the new element.
+ *
+ * @param {jQuery} $group Element to use as group
*/
-OO.ui.ClippableElement.prototype.setClipping = function ( value ) {
- value = !!value;
+OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) {
+ var i, len;
- if ( this.clipping !== value ) {
- this.clipping = value;
- if ( this.clipping ) {
- this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
- // If the clippable container is the body, 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.$clippableContainer;
- this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
- this.$clippableWindow = this.$( this.getElementWindow() )
- .on( 'resize', this.onClippableWindowResizeHandler );
- // Initial clip after visible
- setTimeout( OO.ui.bind( this.clip, this ) );
- } else {
- this.$clippableContainer = null;
- this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
- this.$clippableScroller = null;
- this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
- this.$clippableWindow = null;
- }
+ this.$group = $group;
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ this.$group.append( this.items[i].$element );
}
-
- return this;
};
/**
- * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
+ * Check if there are no items.
*
- * @return {boolean} Element will be clipped to the visible area
+ * @return {boolean} Group is empty
*/
-OO.ui.ClippableElement.prototype.isClipping = function () {
- return this.clipping;
+OO.ui.GroupElement.prototype.isEmpty = function () {
+ return !this.items.length;
};
/**
- * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
+ * Get items.
*
- * @return {boolean} Part of the element is being clipped
+ * @return {OO.ui.Element[]} Items
*/
-OO.ui.ClippableElement.prototype.isClipped = function () {
- return this.clipped;
+OO.ui.GroupElement.prototype.getItems = function () {
+ return this.items.slice( 0 );
};
/**
- * Set the ideal size.
+ * Add an aggregate item event.
*
- * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
- * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
+ * Aggregated events are listened to on each item and then emitted by the group under a new name,
+ * and with an additional leading parameter containing the item that emitted the original event.
+ * Other arguments that were emitted from the original event are passed through.
+ *
+ * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
+ * event, use null value to remove aggregation
+ * @throws {Error} If aggregation already exists
*/
-OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
- this.idealWidth = width;
- this.idealHeight = height;
+OO.ui.GroupElement.prototype.aggregate = function ( events ) {
+ var i, len, item, add, remove, itemEvent, groupEvent;
+
+ for ( itemEvent in events ) {
+ groupEvent = events[itemEvent];
+
+ // Remove existing aggregated event
+ if ( itemEvent in this.aggregateItemEvents ) {
+ // Don't allow duplicate aggregations
+ if ( groupEvent ) {
+ throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
+ }
+ // Remove event aggregation from existing items
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ item = this.items[i];
+ if ( item.connect && item.disconnect ) {
+ remove = {};
+ remove[itemEvent] = [ 'emit', groupEvent, item ];
+ item.disconnect( this, remove );
+ }
+ }
+ // Prevent future items from aggregating event
+ delete this.aggregateItemEvents[itemEvent];
+ }
+
+ // Add new aggregate event
+ if ( groupEvent ) {
+ // Make future items aggregate event
+ this.aggregateItemEvents[itemEvent] = groupEvent;
+ // Add event aggregation to existing items
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ item = this.items[i];
+ if ( item.connect && item.disconnect ) {
+ add = {};
+ add[itemEvent] = [ 'emit', groupEvent, item ];
+ item.connect( this, add );
+ }
+ }
+ }
+ }
};
/**
- * Clip element to visible boundaries and allow scrolling when needed.
+ * Add items.
*
- * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
- * overlapped by, the visible area of the nearest scrollable container.
+ * Adding an existing item (by value) will move it.
*
+ * @param {OO.ui.Element[]} items Item
+ * @param {number} [index] Index to insert items at
* @chainable
*/
-OO.ui.ClippableElement.prototype.clip = function () {
- if ( !this.clipping ) {
- // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
- return this;
- }
+OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
+ var i, len, item, event, events, currentIndex,
+ itemElements = [];
- var buffer = 10,
- cOffset = this.$clippable.offset(),
- $container = this.$clippableContainer.is( 'body' ) ? this.$clippableWindow : this.$clippableContainer,
- ccOffset = $container.offset() || { top: 0, left: 0 },
- ccHeight = $container.innerHeight() - buffer,
- ccWidth = $container.innerWidth() - buffer,
- scrollTop = this.$clippableScroller.scrollTop(),
- scrollLeft = this.$clippableScroller.scrollLeft(),
- desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
- desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
- naturalWidth = this.$clippable.prop( 'scrollWidth' ),
- naturalHeight = this.$clippable.prop( 'scrollHeight' ),
- clipWidth = desiredWidth < naturalWidth,
- clipHeight = desiredHeight < naturalHeight;
+ for ( i = 0, len = items.length; i < len; i++ ) {
+ item = items[i];
- if ( clipWidth ) {
- this.$clippable.css( { overflowX: 'auto', 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', '' );
+ // Check if item exists then remove it first, effectively "moving" it
+ currentIndex = $.inArray( item, this.items );
+ if ( currentIndex >= 0 ) {
+ this.removeItems( [ item ] );
+ // Adjust index to compensate for removal
+ if ( currentIndex < index ) {
+ index--;
+ }
+ }
+ // Add the item
+ if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
+ events = {};
+ for ( event in this.aggregateItemEvents ) {
+ events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
+ }
+ item.connect( this, events );
+ }
+ item.setElementGroup( this );
+ itemElements.push( item.$element.get( 0 ) );
}
- if ( clipHeight ) {
- this.$clippable.css( { overflowY: 'auto', height: desiredHeight } );
+
+ if ( index === undefined || index < 0 || index >= this.items.length ) {
+ this.$group.append( itemElements );
+ this.items.push.apply( this.items, items );
+ } else if ( index === 0 ) {
+ this.$group.prepend( itemElements );
+ this.items.unshift.apply( this.items, items );
} 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.items[index].$element.before( itemElements );
+ this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
}
- this.clipped = clipWidth || clipHeight;
-
return this;
};
/**
- * Element with named flags that can be added, removed, listed and checked.
+ * Remove items.
*
- * A flag, when set, adds a CSS class on the `$element` by combing `oo-ui-flaggableElement-` with
- * the flag name. Flags are primarily useful for styling.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
- */
-OO.ui.FlaggableElement = function OoUiFlaggableElement( config ) {
- // Config initialization
- config = config || {};
-
- // Properties
- this.flags = {};
-
- // Initialization
- this.setFlags( config.flags );
-};
-
-/* Events */
-
-/**
- * @event flag
- * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean
- * added/removed properties
- */
-
-/* Methods */
-
-/**
- * Check if a flag is set.
- *
- * @param {string} flag Name of flag
- * @return {boolean} Has flag
- */
-OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) {
- return flag in this.flags;
-};
-
-/**
- * Get the names of all flags set.
- *
- * @return {string[]} flags Flag names
- */
-OO.ui.FlaggableElement.prototype.getFlags = function () {
- return Object.keys( this.flags );
-};
-
-/**
- * Clear all flags.
- *
- * @chainable
- * @fires flag
- */
-OO.ui.FlaggableElement.prototype.clearFlags = function () {
- var flag,
- changes = {},
- classPrefix = 'oo-ui-flaggableElement-';
-
- for ( flag in this.flags ) {
- changes[flag] = false;
- delete this.flags[flag];
- this.$element.removeClass( classPrefix + flag );
- }
-
- this.emit( 'flag', changes );
-
- return this;
-};
-
-/**
- * Add one or more flags.
- *
- * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
- * keyed by flag name containing boolean set/remove instructions.
- * @chainable
- * @fires flag
- */
-OO.ui.FlaggableElement.prototype.setFlags = function ( flags ) {
- var i, len, flag,
- changes = {},
- classPrefix = 'oo-ui-flaggableElement-';
-
- if ( typeof flags === 'string' ) {
- // Set
- this.flags[flags] = true;
- this.$element.addClass( classPrefix + flags );
- } else if ( $.isArray( flags ) ) {
- for ( i = 0, len = flags.length; i < len; i++ ) {
- flag = flags[i];
- // Set
- changes[flag] = true;
- this.flags[flag] = true;
- this.$element.addClass( classPrefix + flag );
- }
- } else if ( OO.isPlainObject( flags ) ) {
- for ( flag in flags ) {
- if ( flags[flag] ) {
- // Set
- changes[flag] = true;
- this.flags[flag] = true;
- this.$element.addClass( classPrefix + flag );
- } else {
- // Remove
- changes[flag] = false;
- delete this.flags[flag];
- this.$element.removeClass( classPrefix + flag );
- }
- }
- }
-
- this.emit( 'flag', changes );
-
- return this;
-};
-
-/**
- * Element containing a sequence of child elements.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {jQuery} $group Container node, assigned to #$group
- * @param {Object} [config] Configuration options
- */
-OO.ui.GroupElement = function OoUiGroupElement( $group, config ) {
- // Configuration
- config = config || {};
-
- // Properties
- this.$group = $group;
- this.items = [];
- this.aggregateItemEvents = {};
-};
-
-/* Methods */
-
-/**
- * Check if there are no items.
- *
- * @return {boolean} Group is empty
- */
-OO.ui.GroupElement.prototype.isEmpty = function () {
- return !this.items.length;
-};
-
-/**
- * Get items.
- *
- * @return {OO.ui.Element[]} Items
- */
-OO.ui.GroupElement.prototype.getItems = function () {
- return this.items.slice( 0 );
-};
-
-/**
- * Add an aggregate item event.
- *
- * Aggregated events are listened to on each item and then emitted by the group under a new name,
- * and with an additional leading parameter containing the item that emitted the original event.
- * Other arguments that were emitted from the original event are passed through.
- *
- * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
- * event, use null value to remove aggregation
- * @throws {Error} If aggregation already exists
- */
-OO.ui.GroupElement.prototype.aggregate = function ( events ) {
- var i, len, item, add, remove, itemEvent, groupEvent;
-
- for ( itemEvent in events ) {
- groupEvent = events[itemEvent];
-
- // Remove existing aggregated event
- if ( itemEvent in this.aggregateItemEvents ) {
- // Don't allow duplicate aggregations
- if ( groupEvent ) {
- throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
- }
- // Remove event aggregation from existing items
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
- if ( item.connect && item.disconnect ) {
- remove = {};
- remove[itemEvent] = [ 'emit', groupEvent, item ];
- item.disconnect( this, remove );
- }
- }
- // Prevent future items from aggregating event
- delete this.aggregateItemEvents[itemEvent];
- }
-
- // Add new aggregate event
- if ( groupEvent ) {
- // Make future items aggregate event
- this.aggregateItemEvents[itemEvent] = groupEvent;
- // Add event aggregation to existing items
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[i];
- if ( item.connect && item.disconnect ) {
- add = {};
- add[itemEvent] = [ 'emit', groupEvent, item ];
- item.connect( this, add );
- }
- }
- }
- }
-};
-
-/**
- * Add items.
- *
- * @param {OO.ui.Element[]} items Item
- * @param {number} [index] Index to insert items at
- * @chainable
- */
-OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
- var i, len, item, event, events, currentIndex,
- itemElements = [];
-
- for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[i];
-
- // Check if item exists then remove it first, effectively "moving" it
- currentIndex = $.inArray( item, this.items );
- if ( currentIndex >= 0 ) {
- this.removeItems( [ item ] );
- // Adjust index to compensate for removal
- if ( currentIndex < index ) {
- index--;
- }
- }
- // Add the item
- if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
- events = {};
- for ( event in this.aggregateItemEvents ) {
- events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
- }
- item.connect( this, events );
- }
- item.setElementGroup( this );
- itemElements.push( item.$element.get( 0 ) );
- }
-
- if ( index === undefined || index < 0 || index >= this.items.length ) {
- this.$group.append( itemElements );
- this.items.push.apply( this.items, items );
- } else if ( index === 0 ) {
- this.$group.prepend( itemElements );
- this.items.unshift.apply( this.items, items );
- } else {
- this.items[index].$element.before( itemElements );
- this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
- }
-
- return this;
-};
-
-/**
- * Remove items.
- *
- * Items will be detached, not removed, so they can be used later.
+ * Items will be detached, not removed, so they can be used later.
*
* @param {OO.ui.Element[]} items Items to remove
* @chainable
* @class
*
* @constructor
- * @param {jQuery} $icon Icon node, assigned to #$icon
* @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.IconedElement = function OoUiIconedElement( $icon, config ) {
+OO.ui.IconElement = function OoUiIconElement( config ) {
// Config intialization
config = config || {};
// Properties
- this.$icon = $icon;
+ this.$icon = null;
this.icon = null;
+ this.iconTitle = null;
// Initialization
- this.$icon.addClass( 'oo-ui-iconedElement-icon' );
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.IconedElement );
+OO.initClass( OO.ui.IconElement );
/* Static Properties */
* use the 'default' key to specify the icon to be used when there is no icon in the user's
* language
*/
-OO.ui.IconedElement.static.icon = null;
+OO.ui.IconElement.static.icon = null;
+
+/**
+ * Icon title.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null} Icon title text, a function that returns text or null for no
+ * icon title
+ */
+OO.ui.IconElement.static.iconTitle = null;
/* Methods */
/**
- * Set icon.
+ * Set the icon element.
*
- * @param {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
- * @chainable
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $icon Element to use as icon
*/
-OO.ui.IconedElement.prototype.setIcon = function ( icon ) {
- icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
-
- if ( this.icon ) {
- this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
- }
- if ( typeof icon === 'string' ) {
- icon = icon.trim();
- if ( icon.length ) {
- this.$icon.addClass( 'oo-ui-icon-' + icon );
- this.icon = icon;
- }
+OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
+ if ( this.$icon ) {
+ this.$icon
+ .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
+ .removeAttr( 'title' );
+ }
+
+ this.$icon = $icon
+ .addClass( 'oo-ui-iconElement-icon' )
+ .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
+ if ( this.iconTitle !== null ) {
+ this.$icon.attr( 'title', this.iconTitle );
+ }
+};
+
+/**
+ * Set icon.
+ *
+ * @param {Object|string|null} icon Symbolic icon name, or map of icon names keyed by language ID;
+ * use the 'default' key to specify the icon to be used when there is no icon in the user's
+ * language, use null to remove icon
+ * @chainable
+ */
+OO.ui.IconElement.prototype.setIcon = function ( icon ) {
+ icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
+ icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
+
+ if ( this.icon !== icon ) {
+ if ( this.$icon ) {
+ if ( this.icon !== null ) {
+ this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
+ }
+ if ( icon !== null ) {
+ this.$icon.addClass( 'oo-ui-icon-' + icon );
+ }
+ }
+ this.icon = icon;
+ }
+
+ this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
+
+ return this;
+};
+
+/**
+ * Set icon title.
+ *
+ * @param {string|Function|null} icon Icon title text, a function that returns text or null
+ * for no icon title
+ * @chainable
+ */
+OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
+ iconTitle = typeof iconTitle === 'function' ||
+ ( typeof iconTitle === 'string' && iconTitle.length ) ?
+ OO.ui.resolveMsg( iconTitle ) : null;
+
+ if ( this.iconTitle !== iconTitle ) {
+ this.iconTitle = iconTitle;
+ if ( this.$icon ) {
+ if ( this.iconTitle !== null ) {
+ this.$icon.attr( 'title', iconTitle );
+ } else {
+ this.$icon.removeAttr( 'title' );
+ }
+ }
}
- this.$element.toggleClass( 'oo-ui-iconedElement', !!this.icon );
return this;
};
*
* @return {string} Icon
*/
-OO.ui.IconedElement.prototype.getIcon = function () {
+OO.ui.IconElement.prototype.getIcon = function () {
return this.icon;
};
* @class
*
* @constructor
- * @param {jQuery} $indicator Indicator node, assigned to #$indicator
* @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 return text
+ * @cfg {string} [indicatorTitle] Indicator title text or a function that returns text
*/
-OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) {
+OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
// Config intialization
config = config || {};
// Properties
- this.$indicator = $indicator;
+ this.$indicator = null;
this.indicator = null;
- this.indicatorLabel = null;
+ this.indicatorTitle = null;
// Initialization
- this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' );
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.IndicatedElement );
+OO.initClass( OO.ui.IndicatorElement );
/* Static Properties */
* @inheritable
* @property {string|null} Symbolic indicator name or null for no indicator
*/
-OO.ui.IndicatedElement.static.indicator = null;
+OO.ui.IndicatorElement.static.indicator = null;
/**
* Indicator title.
*
* @static
* @inheritable
- * @property {string|Function|null} Indicator title text, a function that return text or null for no
+ * @property {string|Function|null} Indicator title text, a function that returns text or null for no
* indicator title
*/
-OO.ui.IndicatedElement.static.indicatorTitle = null;
+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.$indicatorTitle.attr( 'title', this.indicatorTitle );
+ }
+};
+
/**
* Set indicator.
*
* @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
* @chainable
*/
-OO.ui.IndicatedElement.prototype.setIndicator = function ( indicator ) {
- if ( this.indicator ) {
- this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
- this.indicator = null;
- }
- if ( typeof indicator === 'string' ) {
- indicator = indicator.trim();
- if ( indicator.length ) {
- this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
- this.indicator = indicator;
+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-indicatedElement', !!this.indicator );
+
+ this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
return this;
};
/**
- * Set indicator label.
+ * Set indicator title.
*
- * @param {string|Function|null} indicator Indicator title text, a function that return text or null
- * for no indicator title
+ * @param {string|Function|null} indicator Indicator title text, a function that returns text or
+ * null for no indicator title
* @chainable
*/
-OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
- this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle );
+OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
+ indicatorTitle = typeof indicatorTitle === 'function' ||
+ ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
+ OO.ui.resolveMsg( indicatorTitle ) : null;
- if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) {
- this.$indicator.attr( 'title', indicatorTitle );
- } else {
- this.$indicator.removeAttr( 'title' );
+ 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;
*
* @return {string} title Symbolic name of indicator
*/
-OO.ui.IndicatedElement.prototype.getIndicator = function () {
+OO.ui.IndicatorElement.prototype.getIndicator = function () {
return this.indicator;
};
*
* @return {string} Indicator title text
*/
-OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () {
+OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
return this.indicatorTitle;
};
* @class
*
* @constructor
- * @param {jQuery} $label Label node, assigned to #$label
* @param {Object} [config] Configuration options
- * @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} [$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 ) {
+ // Config intialization
+ config = config || {};
+
+ // Properties
+ this.$label = null;
+ this.label = null;
+ this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
+
+ // Initialization
+ this.setLabel( config.label || this.constructor.static.label );
+ this.setLabelElement( config.$label || this.$( '<span>' ) );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.LabelElement );
+
+/* Static Properties */
+
+/**
+ * Label.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null} Label text; a function that returns nodes or text; or null for
+ * no label
+ */
+OO.ui.LabelElement.static.label = null;
+
+/* Methods */
+
+/**
+ * Set the label element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $label Element to use as label
+ */
+OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) {
+ if ( this.$label ) {
+ this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
+ }
+
+ 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;
+
+ if ( this.label !== label ) {
+ if ( this.$label ) {
+ this.setLabelContent( label );
+ }
+ this.label = label;
+ }
+
+ this.$element.toggleClass( 'oo-ui-labelElement', !!this.label );
+
+ return this;
+};
+
+/**
+ * Get the label.
+ *
+ * @return {jQuery|string|Function|null} label 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();
+ }
+ this.$label.css( 'display', !label ? 'none' : '' );
+};
+
+/**
+ * Element containing an OO.ui.PopupWidget object.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object} [popup] Configuration to pass to popup
+ * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus
+ */
+OO.ui.PopupElement = function OoUiPopupElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.popup = new OO.ui.PopupWidget( $.extend(
+ { autoClose: true },
+ config.popup,
+ { $: this.$, $autoCloseIgnore: this.$element }
+ ) );
+};
+
+/* Methods */
+
+/**
+ * Get popup.
+ *
+ * @return {OO.ui.PopupWidget} Popup widget
+ */
+OO.ui.PopupElement.prototype.getPopup = function () {
+ return this.popup;
+};
+
+/**
+ * Element with named flags that can be added, removed, listed and checked.
+ *
+ * A flag, when set, adds a CSS class on the `$element` by combining `oo-ui-flaggedElement-` with
+ * the flag name. Flags are primarily useful for styling.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
+ * @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element
+ */
+OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
+ // Config initialization
+ config = config || {};
+
+ // Properties
+ this.flags = {};
+ this.$flagged = null;
+
+ // Initialization
+ this.setFlags( config.flags );
+ this.setFlaggedElement( config.$flagged || this.$element );
+};
+
+/* Events */
+
+/**
+ * @event flag
+ * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean
+ * added/removed properties
+ */
+
+/* Methods */
+
+/**
+ * Set the flagged element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $flagged Element to add flags to
+ */
+OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
+ var classNames = Object.keys( this.flags ).map( function ( flag ) {
+ return 'oo-ui-flaggedElement-' + flag;
+ } ).join( ' ' );
+
+ if ( this.$flagged ) {
+ this.$flagged.removeClass( classNames );
+ }
+
+ this.$flagged = $flagged.addClass( classNames );
+};
+
+/**
+ * Check if a flag is set.
+ *
+ * @param {string} flag Name of flag
+ * @return {boolean} Has flag
+ */
+OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) {
+ return flag in this.flags;
+};
+
+/**
+ * Get the names of all flags set.
+ *
+ * @return {string[]} flags Flag names
+ */
+OO.ui.FlaggedElement.prototype.getFlags = function () {
+ return Object.keys( this.flags );
+};
+
+/**
+ * Clear all flags.
+ *
+ * @chainable
+ * @fires flag
+ */
+OO.ui.FlaggedElement.prototype.clearFlags = function () {
+ var flag, className,
+ changes = {},
+ remove = [],
+ classPrefix = 'oo-ui-flaggedElement-';
+
+ for ( flag in this.flags ) {
+ className = classPrefix + flag;
+ changes[flag] = false;
+ delete this.flags[flag];
+ remove.push( className );
+ }
+
+ if ( this.$flagged ) {
+ this.$flagged.removeClass( remove.join( ' ' ) );
+ }
+
+ this.emit( 'flag', changes );
+
+ return this;
+};
+
+/**
+ * Add one or more flags.
+ *
+ * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
+ * keyed by flag name containing boolean set/remove instructions.
+ * @chainable
+ * @fires flag
+ */
+OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) {
+ var i, len, flag, className,
+ changes = {},
+ add = [],
+ remove = [],
+ classPrefix = 'oo-ui-flaggedElement-';
+
+ if ( typeof flags === 'string' ) {
+ className = classPrefix + flags;
+ // Set
+ 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];
+ className = classPrefix + flag;
+ // Set
+ 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] ) {
+ // Set
+ 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];
+ remove.push( className );
+ }
+ }
+ }
+ }
+
+ if ( this.$flagged ) {
+ this.$flagged
+ .addClass( add.join( ' ' ) )
+ .removeClass( remove.join( ' ' ) );
+ }
+
+ this.emit( 'flag', changes );
+
+ return this;
+};
+
+/**
+ * Element with a title.
+ *
+ * Titles are rendered by the browser and are made visible when hovering the element. Titles are
+ * not visible on touch devices.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$titled] Titled node, assigned to #$titled, omit to use #$element
+ * @cfg {string|Function} [title] Title text or a function that returns text
*/
-OO.ui.LabeledElement = function OoUiLabeledElement( $label, config ) {
+OO.ui.TitledElement = function OoUiTitledElement( config ) {
// Config intialization
config = config || {};
// Properties
- this.$label = $label;
- this.label = null;
+ this.$titled = null;
+ this.title = null;
// Initialization
- this.$label.addClass( 'oo-ui-labeledElement-label' );
- this.setLabel( config.label || this.constructor.static.label );
- this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
+ this.setTitle( config.title || this.constructor.static.title );
+ this.setTitledElement( config.$titled || this.$element );
};
/* Setup */
-OO.initClass( OO.ui.LabeledElement );
+OO.initClass( OO.ui.TitledElement );
/* Static Properties */
/**
- * Label.
+ * Title.
*
* @static
* @inheritable
- * @property {string|Function|null} Label text; a function that returns a nodes or text; or null for
- * no label
+ * @property {string|Function} Title text or a function that returns text
*/
-OO.ui.LabeledElement.static.label = null;
+OO.ui.TitledElement.static.title = null;
/* Methods */
/**
- * Set the label.
+ * Set the titled element.
*
- * An empty string will result in the label being hidden. A string containing only whitespace will
- * be converted to a single
+ * If an element is already set, it will be cleaned up before setting up the new element.
*
- * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or
- * text; or null for no label
- * @chainable
+ * @param {jQuery} $titled Element to set title on
*/
-OO.ui.LabeledElement.prototype.setLabel = function ( label ) {
- var empty = false;
-
- this.label = label = OO.ui.resolveMsg( label ) || null;
- if ( typeof label === 'string' && label.length ) {
- 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();
- empty = true;
+OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) {
+ if ( this.$titled ) {
+ this.$titled.removeAttr( 'title' );
}
- this.$element.toggleClass( 'oo-ui-labeledElement', !empty );
- this.$label.css( 'display', empty ? 'none' : '' );
- return this;
+ this.$titled = $titled;
+ if ( this.title ) {
+ this.$titled.attr( 'title', this.title );
+ }
};
/**
- * Get the label.
+ * Set title.
*
- * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
- * text; or null for no label
+ * @param {string|Function|null} title Title text, a function that returns text or null for no title
+ * @chainable
*/
-OO.ui.LabeledElement.prototype.getLabel = function () {
- return this.label;
+OO.ui.TitledElement.prototype.setTitle = function ( title ) {
+ title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
+
+ if ( this.title !== title ) {
+ if ( this.$titled ) {
+ if ( title !== null ) {
+ this.$titled.attr( 'title', title );
+ } else {
+ this.$titled.removeAttr( 'title' );
+ }
+ }
+ this.title = title;
+ }
+
+ return this;
};
/**
- * Fit the label.
+ * Get title.
*
- * @chainable
+ * @return {string} Title string
*/
-OO.ui.LabeledElement.prototype.fitLabel = function () {
- if ( this.$label.autoEllipsis && this.autoFitLabel ) {
- this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
- }
- return this;
+OO.ui.TitledElement.prototype.getTitle = function () {
+ return this.title;
};
/**
- * Element containing an OO.ui.PopupWidget object.
+ * Element that can be automatically clipped to visible boundaries.
+ *
+ * Whenever the element's natural height changes, you have to call
+ * #clip to make sure it's still clipping correctly.
*
* @abstract
* @class
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {Object} [popup] Configuration to pass to popup
- * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus
+ * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
*/
-OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) {
+OO.ui.ClippableElement = function OoUiClippableElement( config ) {
// Configuration initialization
config = config || {};
// Properties
- this.popup = new OO.ui.PopupWidget( $.extend(
- { autoClose: true },
- config.popup,
- { $: this.$, $autoCloseIgnore: this.$element }
- ) );
+ this.$clippable = null;
+ this.clipping = false;
+ this.clippedHorizontally = false;
+ this.clippedVertically = false;
+ this.$clippableContainer = null;
+ this.$clippableScroller = null;
+ this.$clippableWindow = null;
+ this.idealWidth = null;
+ this.idealHeight = null;
+ this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this );
+ this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this );
+
+ // Initialization
+ this.setClippableElement( config.$clippable || this.$element );
};
/* Methods */
/**
- * Get popup.
+ * Set clippable element.
*
- * @return {OO.ui.PopupWidget} Popup widget
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $clippable Element to make clippable
*/
-OO.ui.PopuppableElement.prototype.getPopup = function () {
- return this.popup;
+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 = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
+ this.clip();
};
/**
- * Element with a title.
- *
- * Titles are rendered by the browser and are made visible when hovering the element. Titles are
- * not visible on touch devices.
+ * Toggle clipping.
*
- * @abstract
- * @class
+ * Do not turn clipping on until after the element is attached to the DOM and visible.
*
- * @constructor
- * @param {jQuery} $label Titled node, assigned to #$titled
- * @param {Object} [config] Configuration options
- * @cfg {string|Function} [title] Title text or a function that returns text
+ * @param {boolean} [clipping] Enable clipping, omit to toggle
+ * @chainable
*/
-OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) {
- // Config intialization
- config = config || {};
+OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
+ clipping = clipping === undefined ? !this.clipping : !!clipping;
- // Properties
- this.$titled = $titled;
- this.title = null;
+ if ( this.clipping !== clipping ) {
+ 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
+ // jQuery.scrollTop on the window because of browser inconsistencies
+ this.$clippableScroller = this.$clippableContainer.is( 'body' ) ?
+ this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) :
+ this.$clippableContainer;
+ this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
+ this.$clippableWindow = this.$( this.getElementWindow() )
+ .on( 'resize', this.onClippableWindowResizeHandler );
+ // Initial clip after visible
+ this.clip();
+ } else {
+ this.$clippable.css( { width: '', height: '' } );
+ this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
+ this.$clippable.css( { overflowX: '', overflowY: '' } );
- // Initialization
- this.setTitle( config.title || this.constructor.static.title );
+ this.$clippableContainer = null;
+ this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
+ this.$clippableScroller = null;
+ this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
+ this.$clippableWindow = null;
+ }
+ }
+
+ return this;
};
-/* Setup */
+/**
+ * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
+ *
+ * @return {boolean} Element will be clipped to the visible area
+ */
+OO.ui.ClippableElement.prototype.isClipping = function () {
+ return this.clipping;
+};
-OO.initClass( OO.ui.TitledElement );
+/**
+ * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
+ *
+ * @return {boolean} Part of the element is being clipped
+ */
+OO.ui.ClippableElement.prototype.isClipped = function () {
+ return this.clippedHorizontally || this.clippedVertically;
+};
-/* Static Properties */
+/**
+ * Check if the right of the element is being clipped by the nearest scrollable container.
+ *
+ * @return {boolean} Part of the element is being clipped
+ */
+OO.ui.ClippableElement.prototype.isClippedHorizontally = function () {
+ return this.clippedHorizontally;
+};
/**
- * Title.
+ * Check if the bottom of the element is being clipped by the nearest scrollable container.
*
- * @static
- * @inheritable
- * @property {string|Function} Title text or a function that returns text
+ * @return {boolean} Part of the element is being clipped
*/
-OO.ui.TitledElement.static.title = null;
+OO.ui.ClippableElement.prototype.isClippedVertically = function () {
+ return this.clippedVertically;
+};
-/* Methods */
+/**
+ * Set the ideal size.
+ *
+ * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
+ * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
+ */
+OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
+ this.idealWidth = width;
+ this.idealHeight = height;
+};
/**
- * Set title.
+ * Clip element to visible boundaries and allow scrolling when needed. Call this method when
+ * the element's natural height changes.
+ *
+ * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
+ * overlapped by, the visible area of the nearest scrollable container.
*
- * @param {string|Function|null} title Title text, a function that returns text or null for no title
* @chainable
*/
-OO.ui.TitledElement.prototype.setTitle = function ( title ) {
- this.title = title = OO.ui.resolveMsg( title ) || null;
+OO.ui.ClippableElement.prototype.clip = function () {
+ if ( !this.clipping ) {
+ // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
+ return this;
+ }
+
+ var buffer = 10,
+ cOffset = this.$clippable.offset(),
+ $container = this.$clippableContainer.is( 'body' ) ?
+ this.$clippableWindow : this.$clippableContainer,
+ ccOffset = $container.offset() || { top: 0, left: 0 },
+ ccHeight = $container.innerHeight() - buffer,
+ ccWidth = $container.innerWidth() - buffer,
+ scrollTop = this.$clippableScroller.scrollTop(),
+ scrollLeft = this.$clippableScroller.scrollLeft(),
+ desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
+ desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
+ naturalWidth = this.$clippable.prop( 'scrollWidth' ),
+ naturalHeight = this.$clippable.prop( 'scrollHeight' ),
+ clipWidth = desiredWidth < naturalWidth,
+ clipHeight = desiredHeight < naturalHeight;
- if ( typeof title === 'string' && title.length ) {
- this.$titled.attr( 'title', title );
+ if ( clipWidth ) {
+ this.$clippable.css( { overflowX: 'auto', width: desiredWidth } );
} else {
- this.$titled.removeAttr( 'title' );
+ 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', '' );
+ }
+ if ( clipHeight ) {
+ this.$clippable.css( { overflowY: 'auto', 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', '' );
}
- return this;
-};
+ this.clippedHorizontally = clipWidth;
+ this.clippedVertically = clipHeight;
-/**
- * Get title.
- *
- * @return {string} Title string
- */
-OO.ui.TitledElement.prototype.getTitle = function () {
- return this.title;
+ return this;
};
/**
* @abstract
* @class
* @extends OO.ui.Widget
- * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.IconElement
*
* @constructor
* @param {OO.ui.ToolGroup} toolGroup
OO.ui.Tool.super.call( this, config );
// Mixin constructors
- OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
+ OO.ui.IconElement.call( this, config );
// Properties
this.toolGroup = toolGroup;
/* Setup */
OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
-OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.Tool, OO.ui.IconElement );
/* Events */
// Mixin constructors
OO.EventEmitter.call( this );
- OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+ OO.ui.GroupElement.call( this, config );
// Properties
this.toolFactory = toolFactory;
OO.ui.ToolGroup.super.call( this, config );
// Mixin constructors
- OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+ OO.ui.GroupElement.call( this, config );
// Properties
this.toolbar = toolbar;
*
* @class
* @extends OO.ui.Layout
- * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.LabelElement
*
* Available label alignment modes include:
* - left: Label is before the field and aligned away from it, best for when the user will be
* @cfg {string} [help] Explanatory text shown as a '?' icon.
*/
OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) {
- var popupButtonWidget;
// Config initialization
config = $.extend( { align: 'left' }, config );
OO.ui.FieldLayout.super.call( this, config );
// Mixin constructors
- this.$help = this.$( '<div>' );
- OO.ui.LabeledElement.call( this, this.$( '<label>' ), config );
- if ( config.help ) {
- popupButtonWidget = new OO.ui.PopupButtonWidget( $.extend(
- {
- $: this.$,
- frameless: true,
- icon: 'info',
- title: config.help
- },
- config,
- { label: null }
- ) );
- popupButtonWidget.getPopup().$body.append( this.getElementDocument().createTextNode( config.help ) );
- this.$help = popupButtonWidget.$element;
- }
+ OO.ui.LabelElement.call( this, config );
// Properties
this.$field = this.$( '<div>' );
this.field = field;
this.align = null;
+ if ( config.help ) {
+ this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ $: this.$,
+ framed: false,
+ icon: 'info',
+ title: config.help
+ } );
+
+ this.popupButtonWidget.getPopup().$body.append( this.$( '<span>' ).text( config.help ) );
+ this.$help = this.popupButtonWidget.$element;
+ } else {
+ this.$help = this.$( '<div>' );
+ }
// Events
if ( this.field instanceof OO.ui.InputWidget ) {
/* Setup */
OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
/* Methods */
*
* @class
* @extends OO.ui.Layout
- * @mixins OO.ui.LabeledElement
- * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.LabelElement
+ * @mixins OO.ui.IconElement
* @mixins OO.ui.GroupElement
*
* @constructor
OO.ui.FieldsetLayout.super.call( this, config );
// Mixin constructors
- OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
- OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
- OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+ OO.ui.IconElement.call( this, config );
+ OO.ui.LabelElement.call( this, config );
+ OO.ui.GroupElement.call( this, config );
// Initialization
this.$element
/* Setup */
OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
/* Static Properties */
OO.ui.StackLayout.super.call( this, config );
// Mixin constructors
- OO.ui.GroupElement.call( this, this.$element, config );
+ OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
// Properties
this.currentItem = null;
* @abstract
* @class
* @extends OO.ui.ToolGroup
- * @mixins OO.ui.IconedElement
- * @mixins OO.ui.IndicatedElement
- * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.IndicatorElement
+ * @mixins OO.ui.LabelElement
* @mixins OO.ui.TitledElement
* @mixins OO.ui.ClippableElement
*
OO.ui.PopupToolGroup.super.call( this, toolbar, config );
// Mixin constructors
- OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
- OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
- OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
- OO.ui.TitledElement.call( this, this.$element, config );
- OO.ui.ClippableElement.call( this, this.$group, config );
+ OO.ui.IconElement.call( this, config );
+ OO.ui.IndicatorElement.call( this, config );
+ OO.ui.LabelElement.call( this, config );
+ OO.ui.TitledElement.call( this, config );
+ OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
// Properties
this.active = false;
/* Setup */
OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
-OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatedElement );
-OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatorElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabelElement );
OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
if ( this.active !== value ) {
this.active = value;
if ( value ) {
- this.setClipping( true );
- this.$element.addClass( 'oo-ui-popupToolGroup-active' );
this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
+
+ // Try anchoring the popup to the left first
+ this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
+ this.toggleClipping( true );
+ if ( this.isClippedHorizontally() ) {
+ // Anchoring to the left caused the popup to clip, so anchor it to the right instead
+ this.toggleClipping( false );
+ this.$element
+ .removeClass( 'oo-ui-popupToolGroup-left' )
+ .addClass( 'oo-ui-popupToolGroup-right' );
+ this.toggleClipping( true );
+ }
} else {
- this.setClipping( false );
- this.$element.removeClass( 'oo-ui-popupToolGroup-active' );
this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
+ this.$element.removeClass(
+ 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
+ );
+ this.toggleClipping( false );
}
}
};
* @abstract
* @class
* @extends OO.ui.Tool
- * @mixins OO.ui.PopuppableElement
+ * @mixins OO.ui.PopupElement
*
* @constructor
* @param {OO.ui.Toolbar} toolbar
OO.ui.PopupTool.super.call( this, toolbar, config );
// Mixin constructors
- OO.ui.PopuppableElement.call( this, config );
+ OO.ui.PopupElement.call( this, config );
// Initialization
this.$element
/* Setup */
OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
-OO.mixinClass( OO.ui.PopupTool, OO.ui.PopuppableElement );
+OO.mixinClass( OO.ui.PopupTool, OO.ui.PopupElement );
/* Methods */
* @extends OO.ui.GroupElement
*
* @constructor
- * @param {jQuery} $group Container node, assigned to #$group
* @param {Object} [config] Configuration options
*/
-OO.ui.GroupWidget = function OoUiGroupWidget( $element, config ) {
+OO.ui.GroupWidget = function OoUiGroupWidget( config ) {
// Parent constructor
- OO.ui.GroupWidget.super.call( this, $element, config );
+ OO.ui.GroupWidget.super.call( this, config );
};
/* Setup */
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.GroupElement
- * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.IconElement
*
* @constructor
* @param {OO.ui.OutlineWidget} outline Outline to control
OO.ui.OutlineControlsWidget.super.call( this, config );
// Mixin constructors
- OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
- OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
+ OO.ui.GroupElement.call( this, config );
+ OO.ui.IconElement.call( this, config );
// Properties
this.outline = outline;
OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
-OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement );
/* Events */
OO.ui.ButtonGroupWidget.super.call( this, config );
// Mixin constructors
- OO.ui.GroupElement.call( this, this.$element, config );
+ OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
// Initialization
this.$element.addClass( 'oo-ui-buttonGroupWidget' );
*
* @class
* @extends OO.ui.Widget
- * @mixins OO.ui.ButtonedElement
- * @mixins OO.ui.IconedElement
- * @mixins OO.ui.IndicatedElement
- * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.ButtonElement
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.IndicatorElement
+ * @mixins OO.ui.LabelElement
* @mixins OO.ui.TitledElement
- * @mixins OO.ui.FlaggableElement
+ * @mixins OO.ui.FlaggedElement
*
* @constructor
* @param {Object} [config] Configuration options
OO.ui.ButtonWidget.super.call( this, config );
// Mixin constructors
- OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
- OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
- OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
- OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
- OO.ui.TitledElement.call( this, this.$button, config );
- OO.ui.FlaggableElement.call( this, config );
+ OO.ui.ButtonElement.call( this, config );
+ OO.ui.IconElement.call( this, config );
+ OO.ui.IndicatorElement.call( this, config );
+ OO.ui.LabelElement.call( this, config );
+ OO.ui.TitledElement.call( this, config, $.extend( {}, config, { $titled: this.$button } ) );
+ OO.ui.FlaggedElement.call( this, config );
// Properties
this.href = null;
/* Setup */
OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonedElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatedElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggableElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
/* Events */
*/
OO.ui.ActionWidget.prototype.setIcon = function () {
// Mixin method
- OO.ui.IconedElement.prototype.setIcon.apply( this, arguments );
+ OO.ui.IconElement.prototype.setIcon.apply( this, arguments );
this.propagateResize();
return this;
*/
OO.ui.ActionWidget.prototype.setLabel = function () {
// Mixin method
- OO.ui.LabeledElement.prototype.setLabel.apply( this, arguments );
+ OO.ui.LabelElement.prototype.setLabel.apply( this, arguments );
this.propagateResize();
return this;
*/
OO.ui.ActionWidget.prototype.setFlags = function () {
// Mixin method
- OO.ui.FlaggableElement.prototype.setFlags.apply( this, arguments );
+ OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments );
this.propagateResize();
return this;
*/
OO.ui.ActionWidget.prototype.clearFlags = function () {
// Mixin method
- OO.ui.FlaggableElement.prototype.clearFlags.apply( this, arguments );
+ OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments );
this.propagateResize();
return this;
*
* @class
* @extends OO.ui.ButtonWidget
- * @mixins OO.ui.PopuppableElement
+ * @mixins OO.ui.PopupElement
*
* @constructor
* @param {Object} [config] Configuration options
OO.ui.PopupButtonWidget.super.call( this, config );
// Mixin constructors
- OO.ui.PopuppableElement.call( this, config );
+ OO.ui.PopupElement.call( this, config );
// Initialization
this.$element
/* Setup */
OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
-OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopuppableElement );
+OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement );
/* Methods */
/**
* Icon widget.
*
+ * See OO.ui.IconElement for more information.
+ *
* @class
* @extends OO.ui.Widget
- * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.IconElement
* @mixins OO.ui.TitledElement
*
* @constructor
OO.ui.IconWidget.super.call( this, config );
// Mixin constructors
- OO.ui.IconedElement.call( this, this.$element, config );
- OO.ui.TitledElement.call( this, this.$element, config );
+ OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
+ OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
// Initialization
this.$element.addClass( 'oo-ui-iconWidget' );
/* Setup */
OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
/* Static Properties */
/**
* Indicator widget.
*
- * See OO.ui.IndicatedElement for more information.
+ * See OO.ui.IndicatorElement for more information.
*
* @class
* @extends OO.ui.Widget
- * @mixins OO.ui.IndicatedElement
+ * @mixins OO.ui.IndicatorElement
* @mixins OO.ui.TitledElement
*
* @constructor
OO.ui.IndicatorWidget.super.call( this, config );
// Mixin constructors
- OO.ui.IndicatedElement.call( this, this.$element, config );
- OO.ui.TitledElement.call( this, this.$element, config );
+ OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
+ OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
// Initialization
this.$element.addClass( 'oo-ui-indicatorWidget' );
/* Setup */
OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatedElement );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
/* Static Properties */
*
* @class
* @extends OO.ui.Widget
- * @mixins OO.ui.IconedElement
- * @mixins OO.ui.IndicatedElement
- * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.IndicatorElement
+ * @mixins OO.ui.LabelElement
* @mixins OO.ui.TitledElement
*
* @constructor
OO.ui.InlineMenuWidget.super.call( this, config );
// Mixin constructors
- OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
- OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
- OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
- OO.ui.TitledElement.call( this, this.$label, config );
+ OO.ui.IconElement.call( this, config );
+ OO.ui.IndicatorElement.call( this, config );
+ OO.ui.LabelElement.call( this, config );
+ OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
// Properties
this.menu = new OO.ui.MenuWidget( $.extend( { $: this.$, widget: this }, config.menu ) );
/* Setup */
OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatedElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconElement );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatorElement );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabelElement );
OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement );
/* Methods */
*
* @class
* @extends OO.ui.InputWidget
- * @mixins OO.ui.IconedElement
- * @mixins OO.ui.IndicatedElement
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.IndicatorElement
*
* @constructor
* @param {Object} [config] Configuration options
OO.ui.TextInputWidget.super.call( this, config );
// Mixin constructors
- OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
- OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
+ OO.ui.IconElement.call( this, config );
+ OO.ui.IndicatorElement.call( this, config );
// Properties
this.pending = 0;
/* Setup */
OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
-OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatedElement );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
/* Events */
*
* @class
* @extends OO.ui.Widget
- * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.LabelElement
*
* @constructor
* @param {Object} [config] Configuration options
OO.ui.LabelWidget.super.call( this, config );
// Mixin constructors
- OO.ui.LabeledElement.call( this, this.$element, config );
+ OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
// Properties
this.input = config.input;
/* Setup */
OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement );
/* Static Properties */
-OO.ui.LabelWidget.static.tagName = 'label';
+OO.ui.LabelWidget.static.tagName = 'span';
/* Methods */
*
* @class
* @extends OO.ui.Widget
- * @mixins OO.ui.LabeledElement
- * @mixins OO.ui.FlaggableElement
+ * @mixins OO.ui.LabelElement
+ * @mixins OO.ui.FlaggedElement
*
* @constructor
* @param {Mixed} data Option data
// Mixin constructors
OO.ui.ItemWidget.call( this );
- OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
- OO.ui.FlaggableElement.call( this, config );
+ OO.ui.LabelElement.call( this, config );
+ OO.ui.FlaggedElement.call( this, config );
// Properties
this.data = data;
OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabeledElement );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggableElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement );
/* Static Properties */
-OO.ui.OptionWidget.static.tagName = 'li';
-
OO.ui.OptionWidget.static.selectable = true;
OO.ui.OptionWidget.static.highlightable = true;
*
* @class
* @extends OO.ui.OptionWidget
- * @mixins OO.ui.IconedElement
- * @mixins OO.ui.IndicatedElement
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.IndicatorElement
*
* @constructor
* @param {Mixed} data Option data
OO.ui.DecoratedOptionWidget.super.call( this, data, config );
// Mixin constructors
- OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
- OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
+ OO.ui.IconElement.call( this, config );
+ OO.ui.IndicatorElement.call( this, config );
// Initialization
this.$element
/* Setup */
OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatedElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
/**
* Option widget that looks like a button.
*
* @class
* @extends OO.ui.DecoratedOptionWidget
- * @mixins OO.ui.ButtonedElement
+ * @mixins OO.ui.ButtonElement
*
* @constructor
* @param {Mixed} data Option data
OO.ui.ButtonOptionWidget.super.call( this, data, config );
// Mixin constructors
- OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
+ OO.ui.ButtonElement.call( this, config );
// Initialization
this.$element.addClass( 'oo-ui-buttonOptionWidget' );
/* Setup */
OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
-OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonedElement );
+OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
/* Static Properties */
*
* @class
* @extends OO.ui.Widget
- * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.LabelElement
*
* @constructor
* @param {Object} [config] Configuration options
OO.ui.PopupWidget.super.call( this, config );
// Mixin constructors
- OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
- OO.ui.ClippableElement.call( this, this.$( '<div>' ), config );
+ OO.ui.LabelElement.call( this, config );
+ OO.ui.ClippableElement.call( this, config );
// Properties
this.visible = false;
this.$popup = this.$( '<div>' );
this.$head = this.$( '<div>' );
- this.$body = this.$clippable;
+ this.$body = this.$( '<div>' );
this.$anchor = this.$( '<div>' );
this.$container = config.$container || this.$( 'body' );
this.autoClose = !!config.autoClose;
if ( config.padded ) {
this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
}
+ this.setClippableElement( this.$body );
};
/* Setup */
OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
/* Events */
if ( change ) {
if ( show ) {
- this.setClipping( true );
if ( this.autoClose ) {
this.bindMouseDownListener();
}
this.updateDimensions();
+ this.toggleClipping( true );
} else {
- this.setClipping( false );
+ this.toggleClipping( false );
if ( this.autoClose ) {
this.unbindMouseDownListener();
}
OO.ui.SelectWidget.super.call( this, config );
// Mixin constructors
- OO.ui.GroupWidget.call( this, this.$element, config );
+ OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
// Properties
this.pressed = false;
* @param {OO.ui.OptionWidget[]} items Removed items
*/
-/* Static Properties */
-
-OO.ui.SelectWidget.static.tagName = 'ul';
-
/* Methods */
/**
OO.ui.MenuWidget.super.call( this, config );
// Mixin constructors
- OO.ui.ClippableElement.call( this, this.$group, config );
+ OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
// Properties
this.flashing = false;
};
/**
- * Add items.
- *
- * Adding an existing item (by value) will move it.
- *
- * @param {OO.ui.MenuItemWidget[]} items Items to add
- * @param {number} [index] Index to insert items after
- * @chainable
+ * @inheritdoc
*/
OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
var i, len, item;
for ( i = 0, len = items.length; i < len; i++ ) {
item = items[i];
if ( this.isVisible() ) {
- // Defer fitting label until
+ // Defer fitting label until item has been attached
item.fitLabel();
} else {
this.newItems.push( item );
}
}
+ // Reevaluate clipping
+ this.clip();
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuWidget.prototype.removeItems = function ( items ) {
+ // Parent method
+ OO.ui.MenuWidget.super.prototype.removeItems.call( this, items );
+
+ // Reevaluate clipping
+ this.clip();
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuWidget.prototype.clearItems = function () {
+ // Parent method
+ OO.ui.MenuWidget.super.prototype.clearItems.call( this );
+
+ // Reevaluate clipping
+ this.clip();
+
return this;
};
}
this.newItems = null;
}
- this.setClipping( true );
+ this.toggleClipping( true );
// Auto-hide
if ( this.autoHide ) {
this.getElementDocument().removeEventListener(
'mousedown', this.onDocumentMouseDownHandler, true
);
- this.setClipping( false );
+ this.toggleClipping( false );
}
}
/**
* Menu for a text input widget.
*
- * This menu is specially designed to be positioned beneeth the text input widget. Even if the input
+ * This menu is specially designed to be positioned beneath the text input widget. Even if the input
* is in a different frame, the menu's position is automatically calulated and maintained when the
* menu is toggled or the window is resized.
*