Merge "Show the revision list immediately on "umerge" log action links"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
index a60c879..67eef40 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (51f513f9d3)
+ * 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-20T00:59:55Z
+ * Date: 2014-09-10T17:25:40Z
  */
 ( function ( OO ) {
 
@@ -654,18 +654,19 @@ OO.ui.Element.static.tagName = 'div';
  *
  * @static
  * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
- * @param {OO.ui.Frame} [frame] Frame of the document context
+ * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
+ *   not in an iframe
  * @return {Function} Bound jQuery function
  */
-OO.ui.Element.getJQuery = function ( context, frame ) {
+OO.ui.Element.getJQuery = function ( context, $iframe ) {
        function wrapper( selector ) {
                return $( selector, wrapper.context );
        }
 
        wrapper.context = this.getDocument( context );
 
-       if ( frame ) {
-               wrapper.frame = frame;
+       if ( $iframe ) {
+               wrapper.$iframe = $iframe;
        }
 
        return wrapper;
@@ -1094,255 +1095,6 @@ OO.ui.Element.prototype.offDOMEvent = function ( event, callback ) {
        };
 }() );
 
-/**
- * Embedded iframe with the same styles as its parent.
- *
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.Frame = function OoUiFrame( config ) {
-       // Parent constructor
-       OO.ui.Frame.super.call( this, config );
-
-       // Mixin constructors
-       OO.EventEmitter.call( this );
-
-       // Properties
-       this.loading = null;
-       this.config = config;
-       this.dir = null;
-
-       // Initialize
-       this.$element
-               .addClass( 'oo-ui-frame' )
-               .attr( { frameborder: 0, scrolling: 'no' } );
-
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.Frame, OO.ui.Element );
-OO.mixinClass( OO.ui.Frame, OO.EventEmitter );
-
-/* Static Properties */
-
-/**
- * @static
- * @inheritdoc
- */
-OO.ui.Frame.static.tagName = 'iframe';
-
-/* Events */
-
-/**
- * @event load
- */
-
-/* Static Methods */
-
-/**
- * Transplant the CSS styles from as parent document to a frame's document.
- *
- * This loops over the style sheets in the parent document, and copies their nodes to the
- * frame's document. It then polls the document to see when all styles have loaded, and once they
- * have, resolves the promise.
- *
- * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
- * and resolve the promise anyway. This protects against cases like a display: none; iframe in
- * Firefox, where the styles won't load until the iframe becomes visible.
- *
- * For details of how we arrived at the strategy used in this function, see #load.
- *
- * @static
- * @inheritable
- * @param {HTMLDocument} parentDoc Document to transplant styles from
- * @param {HTMLDocument} frameDoc Document to transplant styles to
- * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
- * @return {jQuery.Promise} Promise resolved when styles have loaded
- */
-OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) {
-       var i, numSheets, styleNode, newNode, timeoutID, pollNodeId, $pendingPollNodes,
-               $pollNodes = $( [] ),
-               // Fake font-family value
-               fontFamily = 'oo-ui-frame-transplantStyles-loaded',
-               deferred = $.Deferred();
-
-       for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
-               styleNode = parentDoc.styleSheets[i].ownerNode;
-               if ( styleNode.disabled ) {
-                       continue;
-               }
-               if ( styleNode.nodeName.toLowerCase() === 'link' ) {
-                       // External stylesheet
-                       // Create a node with a unique ID that we're going to monitor to see when the CSS
-                       // has loaded
-                       pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + i;
-                       $pollNodes = $pollNodes.add( $( '<div>', frameDoc )
-                               .attr( 'id', pollNodeId )
-                               .appendTo( frameDoc.body )
-                       );
-
-                       // Add <style>@import url(...); #pollNodeId { font-family: ... }</style>
-                       // The font-family rule will only take effect once the @import finishes
-                       newNode = frameDoc.createElement( 'style' );
-                       newNode.textContent = '@import url(' + styleNode.href + ');\n' +
-                               '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
-               } else {
-                       // Not an external stylesheet, or no polling required; just copy the node over
-                       // Can't use importNode here because that breaks in IE
-                       newNode = frameDoc.createElement( 'style' );
-                       newNode.textContent = styleNode.textContent;
-               }
-               frameDoc.head.appendChild( newNode );
-       }
-
-       // Poll every 100ms until all external stylesheets have loaded
-       $pendingPollNodes = $pollNodes;
-       timeoutID = setTimeout( function pollExternalStylesheets() {
-               while (
-                       $pendingPollNodes.length > 0 &&
-                       $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
-               ) {
-                       $pendingPollNodes = $pendingPollNodes.slice( 1 );
-               }
-
-               if ( $pendingPollNodes.length === 0 ) {
-                       // We're done!
-                       if ( timeoutID !== null ) {
-                               timeoutID = null;
-                               $pollNodes.remove();
-                               deferred.resolve();
-                       }
-               } else {
-                       timeoutID = setTimeout( pollExternalStylesheets, 100 );
-               }
-       }, 100 );
-       // ...but give up after a while
-       if ( timeout !== 0 ) {
-               setTimeout( function () {
-                       if ( timeoutID ) {
-                               clearTimeout( timeoutID );
-                               timeoutID = null;
-                               $pollNodes.remove();
-                               deferred.reject();
-                       }
-               }, timeout || 5000 );
-       }
-
-       return deferred.promise();
-};
-
-/* Methods */
-
-/**
- * Load the frame contents.
- *
- * Once the iframe's stylesheets are loaded, the `load` event will be emitted and the returned
- * promise will be resolved. Calling while loading will return a promise but not trigger a new
- * loading cycle. Calling after loading is complete will return a promise that's already been
- * resolved.
- *
- * Sounds simple right? Read on...
- *
- * When you create a dynamic iframe using open/write/close, the window.load event for the
- * iframe is triggered when you call close, and there's no further load event to indicate that
- * everything is actually loaded.
- *
- * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
- * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
- * are added to document.styleSheets immediately, and the only way you can determine whether they've
- * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
- * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
- *
- * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>` tags.
- * Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets until
- * the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the `@import`
- * has finished. And because the contents of the `<style>` tag are from the same origin, accessing
- * .cssRules is allowed.
- *
- * However, now that we control the styles we're injecting, we might as well do away with
- * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
- * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
- * and wait for its font-family to change to someValue. Because `@import` is blocking, the font-family
- * rule is not applied until after the `@import` finishes.
- *
- * All this stylesheet injection and polling magic is in #transplantStyles.
- *
- * @return {jQuery.Promise} Promise resolved when loading is complete
- * @fires load
- */
-OO.ui.Frame.prototype.load = function () {
-       var win, doc,
-               frame = this;
-
-       // Return existing promise if already loading or loaded
-       if ( this.loading ) {
-               return this.loading.promise();
-       }
-
-       // Load the frame
-       this.loading = $.Deferred();
-
-       win = this.$element.prop( 'contentWindow' );
-       doc = win.document;
-
-       // Cache directionality
-       this.dir = OO.ui.Element.getDir( this.$element ) || 'ltr';
-
-       // Initialize contents
-       doc.open();
-       // The following classes can be used here:
-       // oo-ui-ltr
-       // oo-ui-rtl
-       doc.write(
-               '<!doctype html>' +
-               '<html>' +
-                       '<body class="oo-ui-frame-content oo-ui-' + this.getDir() + '" dir="' + this.getDir() + '">' +
-                       '</body>' +
-               '</html>'
-       );
-       doc.close();
-
-       // Properties
-       this.$ = OO.ui.Element.getJQuery( doc, this );
-       this.$content = this.$( '.oo-ui-frame-content' ).attr( 'tabIndex', 0 );
-       this.$document = this.$( doc );
-
-       // Initialization
-       this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
-               .always( function () {
-                       frame.emit( 'load' );
-                       frame.loading.resolve();
-               } );
-
-       return this.loading.promise();
-};
-
-/**
- * Set the size of the frame.
- *
- * @param {number} width Frame width in pixels
- * @param {number} height Frame height in pixels
- * @chainable
- */
-OO.ui.Frame.prototype.setSize = function ( width, height ) {
-       this.$element.css( { width: width, height: height } );
-       return this;
-};
-
-/**
- * Get the directionality of the frame
- *
- * @return {string} Directionality, 'ltr' or 'rtl'
- */
-OO.ui.Frame.prototype.getDir = function () {
-       return this.dir;
-};
-
 /**
  * Container for elements.
  *
@@ -1530,15 +1282,12 @@ OO.ui.Widget.prototype.updateDisabled = function () {
  * If the requested size is not recognized, the window manager will choose a sensible fallback.
  *
  * @constructor
- * @param {OO.ui.WindowManager} manager Manager of window
  * @param {Object} [config] Configuration options
  * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to
  *   use #static-size
  * @fires initialize
  */
-OO.ui.Window = function OoUiWindow( manager, config ) {
-       var win = this;
-
+OO.ui.Window = function OoUiWindow( config ) {
        // Configuration initialization
        config = config || {};
 
@@ -1548,46 +1297,25 @@ OO.ui.Window = function OoUiWindow( manager, config ) {
        // Mixin constructors
        OO.EventEmitter.call( this );
 
-       if ( !( manager instanceof OO.ui.WindowManager ) ) {
-               throw new Error( 'Cannot construct window: window must have a manager' );
-       }
-
        // Properties
-       this.manager = manager;
+       this.manager = null;
        this.initialized = false;
        this.visible = false;
        this.opening = null;
        this.closing = null;
        this.opened = null;
        this.timing = null;
+       this.loading = null;
        this.size = config.size || this.constructor.static.size;
-       this.frame = new OO.ui.Frame( { $: this.$ } );
        this.$frame = this.$( '<div>' );
-       this.$ = function () {
-               throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
-       };
 
        // Initialization
        this.$element
                .addClass( 'oo-ui-window' )
-               // Hide the window using visibility: hidden; while the iframe is still loading
-               // Can't use display: none; because that prevents the iframe from loading in Firefox
-               .css( 'visibility', 'hidden' )
                .append( this.$frame );
-       this.$frame
-               .addClass( 'oo-ui-window-frame' )
-               .append( this.frame.$element );
+       this.$frame.addClass( 'oo-ui-window-frame' );
 
-       // Events
-       this.frame.on( 'load', function () {
-               win.initialize();
-               win.initialized = true;
-               // Undo the visibility: hidden; hack and apply display: none;
-               // We can do this safely now that the iframe has initialized
-               // (don't do this from within #initialize because it has to happen
-               // after the all subclasses have been handled as well).
-               win.$element.hide().css( 'visibility', '' );
-       } );
+       // NOTE: Additional intitialization will occur when #setManager is called
 };
 
 /* Setup */
@@ -1615,47 +1343,189 @@ OO.mixinClass( OO.ui.Window, OO.EventEmitter );
  */
 OO.ui.Window.static.size = 'medium';
 
-/* Methods */
+/* Static Methods */
 
 /**
- * Check if window has been initialized.
+ * Transplant the CSS styles from as parent document to a frame's document.
  *
- * @return {boolean} Window has been initialized
- */
-OO.ui.Window.prototype.isInitialized = function () {
-       return this.initialized;
-};
-
-/**
- * Check if window is visible.
+ * This loops over the style sheets in the parent document, and copies their nodes to the
+ * frame's document. It then polls the document to see when all styles have loaded, and once they
+ * have, resolves the promise.
  *
- * @return {boolean} Window is visible
- */
-OO.ui.Window.prototype.isVisible = function () {
-       return this.visible;
-};
-
-/**
- * Check if window is opening.
+ * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
+ * and resolve the promise anyway. This protects against cases like a display: none; iframe in
+ * Firefox, where the styles won't load until the iframe becomes visible.
  *
- * This is a wrapper around OO.ui.WindowManager#isOpening.
+ * For details of how we arrived at the strategy used in this function, see #load.
  *
- * @return {boolean} Window is opening
+ * @static
+ * @inheritable
+ * @param {HTMLDocument} parentDoc Document to transplant styles from
+ * @param {HTMLDocument} frameDoc Document to transplant styles to
+ * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
+ * @return {jQuery.Promise} Promise resolved when styles have loaded
  */
-OO.ui.Window.prototype.isOpening = function () {
-       return this.manager.isOpening( this );
-};
+OO.ui.Window.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) {
+       var i, numSheets, styleNode, styleText, newNode, timeoutID, pollNodeId, $pendingPollNodes,
+               $pollNodes = $( [] ),
+               // Fake font-family value
+               fontFamily = 'oo-ui-frame-transplantStyles-loaded',
+               nextIndex = parentDoc.oouiFrameTransplantStylesNextIndex || 0,
+               deferred = $.Deferred();
 
-/**
- * Check if window is closing.
- *
- * This is a wrapper around OO.ui.WindowManager#isClosing.
- *
- * @return {boolean} Window is closing
- */
-OO.ui.Window.prototype.isClosing = function () {
-       return this.manager.isClosing( this );
-};
+       for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
+               styleNode = parentDoc.styleSheets[i].ownerNode;
+               if ( styleNode.disabled ) {
+                       continue;
+               }
+
+               if ( styleNode.nodeName.toLowerCase() === 'link' ) {
+                       // External stylesheet; use @import
+                       styleText = '@import url(' + styleNode.href + ');';
+               } else {
+                       // Internal stylesheet; just copy the text
+                       // For IE10 we need to fall back to .cssText, BUT that's undefined in
+                       // other browsers, so fall back to '' rather than 'undefined'
+                       styleText = styleNode.textContent || parentDoc.styleSheets[i].cssText || '';
+               }
+
+               // Create a node with a unique ID that we're going to monitor to see when the CSS
+               // has loaded
+               if ( styleNode.oouiFrameTransplantStylesId ) {
+                       // If we're nesting transplantStyles operations and this node already has
+                       // a CSS rule to wait for loading, reuse it
+                       pollNodeId = styleNode.oouiFrameTransplantStylesId;
+               } else {
+                       // Otherwise, create a new ID
+                       pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + nextIndex;
+                       nextIndex++;
+
+                       // Add #pollNodeId { font-family: ... } to the end of the stylesheet / after the @import
+                       // The font-family rule will only take effect once the @import finishes
+                       styleText += '\n' + '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
+               }
+
+               // Create a node with id=pollNodeId
+               $pollNodes = $pollNodes.add( $( '<div>', frameDoc )
+                       .attr( 'id', pollNodeId )
+                       .appendTo( frameDoc.body )
+               );
+
+               // Add our modified CSS as a <style> tag
+               newNode = frameDoc.createElement( 'style' );
+               newNode.textContent = styleText;
+               newNode.oouiFrameTransplantStylesId = pollNodeId;
+               frameDoc.head.appendChild( newNode );
+       }
+       frameDoc.oouiFrameTransplantStylesNextIndex = nextIndex;
+
+       // Poll every 100ms until all external stylesheets have loaded
+       $pendingPollNodes = $pollNodes;
+       timeoutID = setTimeout( function pollExternalStylesheets() {
+               while (
+                       $pendingPollNodes.length > 0 &&
+                       $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
+               ) {
+                       $pendingPollNodes = $pendingPollNodes.slice( 1 );
+               }
+
+               if ( $pendingPollNodes.length === 0 ) {
+                       // We're done!
+                       if ( timeoutID !== null ) {
+                               timeoutID = null;
+                               $pollNodes.remove();
+                               deferred.resolve();
+                       }
+               } else {
+                       timeoutID = setTimeout( pollExternalStylesheets, 100 );
+               }
+       }, 100 );
+       // ...but give up after a while
+       if ( timeout !== 0 ) {
+               setTimeout( function () {
+                       if ( timeoutID ) {
+                               clearTimeout( timeoutID );
+                               timeoutID = null;
+                               $pollNodes.remove();
+                               deferred.reject();
+                       }
+               }, timeout || 5000 );
+       }
+
+       return deferred.promise();
+};
+
+/* Methods */
+
+/**
+ * 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.
+ *
+ * @return {boolean} Window has been initialized
+ */
+OO.ui.Window.prototype.isInitialized = function () {
+       return this.initialized;
+};
+
+/**
+ * Check if window is visible.
+ *
+ * @return {boolean} Window is visible
+ */
+OO.ui.Window.prototype.isVisible = function () {
+       return this.visible;
+};
+
+/**
+ * Check if window is loading.
+ *
+ * @return {boolean} Window is loading
+ */
+OO.ui.Window.prototype.isLoading = function () {
+       return this.loading && this.loading.state() === 'pending';
+};
+
+/**
+ * Check if window is loaded.
+ *
+ * @return {boolean} Window is loaded
+ */
+OO.ui.Window.prototype.isLoaded = function () {
+       return this.loading && this.loading.state() === 'resolved';
+};
+
+/**
+ * Check if window is opening.
+ *
+ * This is a wrapper around OO.ui.WindowManager#isOpening.
+ *
+ * @return {boolean} Window is opening
+ */
+OO.ui.Window.prototype.isOpening = function () {
+       return this.manager.isOpening( this );
+};
+
+/**
+ * Check if window is closing.
+ *
+ * This is a wrapper around OO.ui.WindowManager#isClosing.
+ *
+ * @return {boolean} Window is closing
+ */
+OO.ui.Window.prototype.isClosing = function () {
+       return this.manager.isClosing( this );
+};
 
 /**
  * Check if window is opened.
@@ -1677,15 +1547,6 @@ OO.ui.Window.prototype.getManager = function () {
        return this.manager;
 };
 
-/**
- * Get the window frame.
- *
- * @return {OO.ui.Frame} Frame of window
- */
-OO.ui.Window.prototype.getFrame = function () {
-       return this.frame;
-};
-
 /**
  * Get the window size.
  *
@@ -1703,8 +1564,8 @@ OO.ui.Window.prototype.getSize = function () {
 OO.ui.Window.prototype.getContentHeight = function () {
        return Math.round(
                // Add buffer for border
-               ( ( this.$frame.outerHeight() - this.$frame.innerHeight() ) * 2 ) +
-               // Height of contents
+               ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
+               // Use combined heights of children
                ( this.$head.outerHeight( true ) + this.getBodyHeight() + this.$foot.outerHeight( true ) )
        );
 };
@@ -1718,6 +1579,15 @@ OO.ui.Window.prototype.getBodyHeight = function () {
        return this.$body[0].scrollHeight;
 };
 
+/**
+ * Get the directionality of the frame
+ *
+ * @return {string} Directionality, 'ltr' or 'rtl'
+ */
+OO.ui.Window.prototype.getDir = function () {
+       return this.dir;
+};
+
 /**
  * Get a process for setting up a window for use.
  *
@@ -1786,6 +1656,76 @@ OO.ui.Window.prototype.getTeardownProcess = function () {
        return new OO.ui.Process();
 };
 
+/**
+ * Toggle visibility of window.
+ *
+ * If the window is isolated and hasn't fully loaded yet, the visiblity property will be used
+ * instead of display.
+ *
+ * @param {boolean} [show] Make window visible, omit to toggle visibility
+ * @fires visible
+ * @chainable
+ */
+OO.ui.Window.prototype.toggle = function ( show ) {
+       show = show === undefined ? !this.visible : !!show;
+
+       if ( show !== this.isVisible() ) {
+               this.visible = show;
+
+               if ( this.isolated && !this.isLoaded() ) {
+                       // Hide the window using visibility instead of display until loading is complete
+                       // Can't use display: none; because that prevents the iframe from loading in Firefox
+                       this.$element.css( 'visibility', show ? 'visible' : 'hidden' );
+               } else {
+                       this.$element.toggle( show ).css( 'visibility', '' );
+               }
+               this.emit( 'toggle', show );
+       }
+
+       return this;
+};
+
+/**
+ * Set the window manager.
+ *
+ * This must be called before initialize. Calling it more than once will cause an error.
+ *
+ * @param {OO.ui.WindowManager} manager Manager for this window
+ * @throws {Error} If called more than once
+ * @chainable
+ */
+OO.ui.Window.prototype.setManager = function ( manager ) {
+       if ( this.manager ) {
+               throw new Error( 'Cannot set window manager, window already has a manager' );
+       }
+
+       // Properties
+       this.manager = manager;
+       this.isolated = manager.shouldIsolate();
+
+       // Initialization
+       if ( this.isolated ) {
+               this.$iframe = this.$( '<iframe>' );
+               this.$iframe.attr( { frameborder: 0, scrolling: 'no' } );
+               this.$frame.append( this.$iframe );
+               this.$ = function () {
+                       throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
+               };
+               // WARNING: Do not use this.$ again until #initialize is called
+       } else {
+               this.$content = this.$( '<div>' );
+               this.$document = $( this.getElementDocument() );
+               this.$content.addClass( 'oo-ui-window-content' );
+               this.$frame.append( this.$content );
+       }
+       this.toggle( false );
+
+       // Figure out directionality:
+       this.dir = OO.ui.Element.getDir( this.$iframe || this.$content ) || 'ltr';
+
+       return this;
+};
+
 /**
  * Set the window size.
  *
@@ -1835,24 +1775,29 @@ OO.ui.Window.prototype.setDimensions = function ( dim ) {
  *
  * Once this method is called, this.$ can be used to create elements within the frame.
  *
+ * @throws {Error} If not attached to a manager
  * @chainable
  */
 OO.ui.Window.prototype.initialize = function () {
+       if ( !this.manager ) {
+               throw new Error( 'Cannot initialize window, must be attached to a manager' );
+       }
+
        // Properties
-       this.$ = this.frame.$;
        this.$head = this.$( '<div>' );
        this.$body = this.$( '<div>' );
        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' );
        this.$foot.addClass( 'oo-ui-window-foot' );
        this.$overlay.addClass( 'oo-ui-window-overlay' );
-       this.frame.$content
-               .addClass( 'oo-ui-window-content' )
-               .append( this.$head, this.$body, this.$foot, this.$overlay );
+       this.$content.append( this.$head, this.$body, this.$foot, this.$overlay );
 
        return this;
 };
@@ -1884,18 +1829,6 @@ OO.ui.Window.prototype.close = function ( data ) {
        return this.manager.closeWindow( this, data );
 };
 
-/**
- * Load window.
- *
- * This is called by OO.ui.WindowManager durring window adding, and should not be called directly
- * by other systems.
- *
- * @return {jQuery.Promise} Promise resolved when window is loaded
- */
-OO.ui.Window.prototype.load = function () {
-       return this.frame.load();
-};
-
 /**
  * Setup window.
  *
@@ -1912,10 +1845,9 @@ OO.ui.Window.prototype.setup = function ( data ) {
        this.$element.show();
        this.visible = true;
        this.getSetupProcess( data ).execute().done( function () {
-               win.manager.updateWindowSize( win );
                // Force redraw by asking the browser to measure the elements' widths
                win.$element.addClass( 'oo-ui-window-setup' ).width();
-               win.frame.$content.addClass( 'oo-ui-window-content-setup' ).width();
+               win.$content.addClass( 'oo-ui-window-content-setup' ).width();
                deferred.resolve();
        } );
 
@@ -1935,11 +1867,11 @@ OO.ui.Window.prototype.ready = function ( data ) {
        var win = this,
                deferred = $.Deferred();
 
-       this.frame.$content[0].focus();
+       this.$content.focus();
        this.getReadyProcess( data ).execute().done( function () {
                // Force redraw by asking the browser to measure the elements' widths
                win.$element.addClass( 'oo-ui-window-ready' ).width();
-               win.frame.$content.addClass( 'oo-ui-window-content-ready' ).width();
+               win.$content.addClass( 'oo-ui-window-content-ready' ).width();
                deferred.resolve();
        } );
 
@@ -1960,13 +1892,17 @@ OO.ui.Window.prototype.hold = function ( data ) {
                deferred = $.Deferred();
 
        this.getHoldProcess( data ).execute().done( function () {
-               var $focused = win.frame.$content.find( ':focus' );
-               if ( $focused.length ) {
-                       $focused[0].blur();
+               // Get the focused element within the window's content
+               var $focus = win.$content.find( OO.ui.Element.getDocument( win.$content ).activeElement );
+
+               // Blur the focused element
+               if ( $focus.length ) {
+                       $focus[0].blur();
                }
+
                // Force redraw by asking the browser to measure the elements' widths
                win.$element.removeClass( 'oo-ui-window-ready' ).width();
-               win.frame.$content.removeClass( 'oo-ui-window-content-ready' ).width();
+               win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
                deferred.resolve();
        } );
 
@@ -1989,7 +1925,7 @@ OO.ui.Window.prototype.teardown = function ( data ) {
        this.getTeardownProcess( data ).execute().done( function () {
                // Force redraw by asking the browser to measure the elements' widths
                win.$element.removeClass( 'oo-ui-window-setup' ).width();
-               win.frame.$content.removeClass( 'oo-ui-window-content-setup' ).width();
+               win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
                win.$element.hide();
                win.visible = false;
                deferred.resolve();
@@ -1998,6 +1934,104 @@ OO.ui.Window.prototype.teardown = function ( data ) {
        return deferred.promise();
 };
 
+/**
+ * Load the frame contents.
+ *
+ * Once the iframe's stylesheets are loaded, the `load` event will be emitted and the returned
+ * promise will be resolved. Calling while loading will return a promise but not trigger a new
+ * loading cycle. Calling after loading is complete will return a promise that's already been
+ * resolved.
+ *
+ * Sounds simple right? Read on...
+ *
+ * When you create a dynamic iframe using open/write/close, the window.load event for the
+ * iframe is triggered when you call close, and there's no further load event to indicate that
+ * everything is actually loaded.
+ *
+ * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
+ * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
+ * are added to document.styleSheets immediately, and the only way you can determine whether they've
+ * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
+ * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
+ *
+ * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>`
+ * tags. Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets
+ * until the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the
+ * `@import` has finished. And because the contents of the `<style>` tag are from the same origin,
+ * accessing .cssRules is allowed.
+ *
+ * However, now that we control the styles we're injecting, we might as well do away with
+ * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
+ * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
+ * and wait for its font-family to change to someValue. Because `@import` is blocking, the
+ * font-family rule is not applied until after the `@import` finishes.
+ *
+ * All this stylesheet injection and polling magic is in #transplantStyles.
+ *
+ * @return {jQuery.Promise} Promise resolved when loading is complete
+ * @fires load
+ */
+OO.ui.Window.prototype.load = function () {
+       var sub, doc, loading,
+               win = this;
+
+       // Non-isolated windows are already "loaded"
+       if ( !this.loading && !this.isolated ) {
+               this.loading = $.Deferred().resolve();
+               this.initialize();
+               // Set initialized state after so sub-classes aren't confused by it being set by calling
+               // their parent initialize method
+               this.initialized = true;
+       }
+
+       // Return existing promise if already loading or loaded
+       if ( this.loading ) {
+               return this.loading.promise();
+       }
+
+       // Load the frame
+       loading = this.loading = $.Deferred();
+       sub = this.$iframe.prop( 'contentWindow' );
+       doc = sub.document;
+
+       // Initialize contents
+       doc.open();
+       doc.write(
+               '<!doctype html>' +
+               '<html>' +
+                       '<body class="oo-ui-window-isolated oo-ui-' + this.dir + '"' +
+                               ' style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
+                               '<div class="oo-ui-window-content"></div>' +
+                       '</body>' +
+               '</html>'
+       );
+       doc.close();
+
+       // Properties
+       this.$ = OO.ui.Element.getJQuery( doc, this.$element );
+       this.$content = this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 );
+       this.$document = this.$( doc );
+
+       // Initialization
+       this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
+               .always( function () {
+                       // Initialize isolated windows
+                       win.initialize();
+                       // Set initialized state after so sub-classes aren't confused by it being set by calling
+                       // their parent initialize method
+                       win.initialized = true;
+                       // Undo the visibility: hidden; hack and apply display: none;
+                       // We can do this safely now that the iframe has initialized
+                       // (don't do this from within #initialize because it has to happen
+                       // after the all subclasses have been handled as well).
+                       win.toggle( win.isVisible() );
+
+                       loading.resolve();
+               } );
+
+       return loading.promise();
+};
+
 /**
  * Base class for all dialogs.
  *
@@ -2022,14 +2056,13 @@ OO.ui.Window.prototype.teardown = function ( data ) {
  * @abstract
  * @class
  * @extends OO.ui.Window
- * @mixins OO.ui.LabeledElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
  */
-OO.ui.Dialog = function OoUiDialog( manager, config ) {
+OO.ui.Dialog = function OoUiDialog( config ) {
        // Parent constructor
-       OO.ui.Dialog.super.call( this, manager, config );
+       OO.ui.Dialog.super.call( this, config );
 
        // Properties
        this.actions = new OO.ui.ActionSet();
@@ -2102,7 +2135,7 @@ OO.ui.Dialog.static.escapable = true;
  *
  * @param {jQuery.Event} e Key down event
  */
-OO.ui.Dialog.prototype.onFrameDocumentKeyDown = function ( e ) {
+OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
        if ( e.which === OO.ui.Keys.ESCAPE ) {
                this.close();
                return false;
@@ -2234,11 +2267,11 @@ OO.ui.Dialog.prototype.initialize = function () {
 
        // Events
        if ( this.constructor.static.escapable ) {
-               this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) );
+               this.$document.on( 'keydown', OO.ui.bind( this.onDocumentKeyDown, this ) );
        }
 
        // Initialization
-       this.frame.$content.addClass( 'oo-ui-dialog-content' );
+       this.$content.addClass( 'oo-ui-dialog-content' );
 };
 
 /**
@@ -2283,7 +2316,7 @@ OO.ui.Dialog.prototype.executeAction = function ( action ) {
  */
 OO.ui.Dialog.prototype.pushPending = function () {
        if ( this.pending === 0 ) {
-               this.frame.$content.addClass( 'oo-ui-actionDialog-content-pending' );
+               this.$content.addClass( 'oo-ui-actionDialog-content-pending' );
                this.$head.addClass( 'oo-ui-texture-pending' );
        }
        this.pending++;
@@ -2300,7 +2333,7 @@ OO.ui.Dialog.prototype.pushPending = function () {
  */
 OO.ui.Dialog.prototype.popPending = function () {
        if ( this.pending === 1 ) {
-               this.frame.$content.removeClass( 'oo-ui-actionDialog-content-pending' );
+               this.$content.removeClass( 'oo-ui-actionDialog-content-pending' );
                this.$head.removeClass( 'oo-ui-texture-pending' );
        }
        this.pending = Math.max( 0, this.pending - 1 );
@@ -2348,6 +2381,7 @@ OO.ui.Dialog.prototype.popPending = function () {
  *
  * @constructor
  * @param {Object} [config] Configuration options
+ * @cfg {boolean} [isolate] Configure managed windows to isolate their content using inline frames
  * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
  * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
  */
@@ -2364,10 +2398,13 @@ OO.ui.WindowManager = function OoUiWindowManager( config ) {
        // Properties
        this.factory = config.factory;
        this.modal = config.modal === undefined || !!config.modal;
+       this.isolate = !!config.isolate;
        this.windows = {};
        this.opening = null;
        this.opened = null;
        this.closing = null;
+       this.preparingToOpen = null;
+       this.preparingToClose = null;
        this.size = null;
        this.currentWindow = null;
        this.$ariaHidden = null;
@@ -2378,9 +2415,6 @@ OO.ui.WindowManager = function OoUiWindowManager( config ) {
        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' )
@@ -2537,6 +2571,17 @@ OO.ui.WindowManager.prototype.isOpened = function ( win ) {
        return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
 };
 
+/**
+ * Check if window contents should be isolated.
+ *
+ * Window content isolation is done using inline frames.
+ *
+ * @return {boolean} Window contents should be isolated
+ */
+OO.ui.WindowManager.prototype.shouldIsolate = function () {
+       return this.isolate;
+};
+
 /**
  * Check if a window is being managed.
  *
@@ -2605,8 +2650,7 @@ OO.ui.WindowManager.prototype.getTeardownDelay = function () {
  * If window is not yet instantiated, it will be instantiated and added automatically.
  *
  * @param {string} name Symbolic window name
- * @return {jQuery.Promise} Promise resolved when window is ready to be accessed; when resolved the
- *   first argument is an OO.ui.Window; when rejected the first argument is an OO.ui.Error
+ * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
  * @throws {Error} If the symbolic name is unrecognized by the factory
  * @throws {Error} If the symbolic name unrecognized as a managed window
  */
@@ -2622,10 +2666,8 @@ OO.ui.WindowManager.prototype.getWindow = function ( name ) {
                                ) );
                        } else {
                                win = this.factory.create( name, this, { $: this.$ } );
-                               this.addWindows( [ win ] ).then(
-                                       OO.ui.bind( deferred.resolve, deferred, win ),
-                                       deferred.reject
-                               );
+                               this.addWindows( [ win ] );
+                               deferred.resolve( win );
                        }
                } else {
                        deferred.reject( new OO.ui.Error(
@@ -2674,49 +2716,45 @@ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
                opening.reject( new OO.ui.Error(
                        'Cannot open window: window is not attached to manager'
                ) );
+       } else if ( this.preparingToOpen || this.opening || this.opened ) {
+               opening.reject( new OO.ui.Error(
+                       'Cannot open window: another window is opening or open'
+               ) );
        }
 
        // Window opening
        if ( opening.state() !== 'rejected' ) {
-               // Begin loading the window if it's not loaded already - may take noticable time and we want
-               // too do this in paralell with any preparatory actions
-               preparing.push( win.load() );
-
-               if ( this.opening || this.opened ) {
-                       // If a window is currently opening or opened, close it first
-                       preparing.push( this.closeWindow( this.currentWindow ) );
-               } else if ( this.closing ) {
+               // Begin loading the window if it's not loading or loaded already - may take noticable time
+               // and we want to do this in paralell with any other preparatory actions
+               if ( !win.isLoading() && !win.isLoaded() ) {
+                       // Finish initializing the window (must be done after manager is attached to DOM)
+                       win.setManager( this );
+                       preparing.push( win.load() );
+               }
+
+               if ( this.closing ) {
                        // If a window is currently closing, wait for it to complete
                        preparing.push( this.closing );
                }
 
-               $.when.apply( $, preparing ).done( function () {
+               this.preparingToOpen = $.when.apply( $, preparing );
+               // 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;
+                       manager.preparingToOpen = null;
                        manager.emit( 'opening', win, opening, data );
-                       manager.updateWindowSize( win );
                        setTimeout( function () {
                                win.setup( data ).then( function () {
+                                       manager.updateWindowSize( win );
                                        manager.opening.notify( { state: 'setup' } );
                                        setTimeout( function () {
                                                win.ready( data ).then( function () {
+                                                       manager.updateWindowSize( win );
                                                        manager.opening.notify( { state: 'ready' } );
                                                        manager.opening = null;
                                                        manager.opened = $.Deferred();
@@ -2728,7 +2766,7 @@ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
                } );
        }
 
-       return opening;
+       return opening.promise();
 };
 
 /**
@@ -2736,7 +2774,7 @@ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
  *
  * @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
@@ -2763,7 +2801,7 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
                closing.reject( new OO.ui.Error(
                        'Cannot close window: window already closed with different data'
                ) );
-       } else if ( this.closing ) {
+       } else if ( this.preparingToClose || this.closing ) {
                closing.reject( new OO.ui.Error(
                        'Cannot close window: window already closing with different data'
                ) );
@@ -2776,9 +2814,11 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
                        preparing.push( this.opening );
                }
 
-               // Close the window
-               $.when.apply( $, preparing ).done( function () {
+               this.preparingToClose = $.when.apply( $, preparing );
+               // Ensure handlers get called after preparingToClose is set
+               this.preparingToClose.done( function () {
                        manager.closing = closing;
+                       manager.preparingToClose = null;
                        manager.emit( 'closing', win, closing, data );
                        manager.opened = null;
                        opened.resolve( closing.promise(), data );
@@ -2789,21 +2829,8 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
                                                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;
@@ -2815,23 +2842,18 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
                } );
        }
 
-       return closing;
+       return closing.promise();
 };
 
 /**
  * Add windows.
  *
- * If the window manager is attached to the DOM then windows will be automatically loaded as they
- * are added.
- *
  * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
- * @return {jQuery.Promise} Promise resolved when all windows are added
  * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
  *   a statically configured symbolic name
  */
 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
-       var i, len, win, name, list,
-               promises = [];
+       var i, len, win, name, list;
 
        if ( $.isArray( windows ) ) {
                // Convert to map of windows by looking up symbolic names from static configuration
@@ -2852,13 +2874,7 @@ OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
                win = list[name];
                this.windows[name] = win;
                this.$element.append( win.$element );
-
-               if ( this.isElementAttached() ) {
-                       promises.push( win.load() );
-               }
        }
-
-       return $.when.apply( $, promises );
 };
 
 /**
@@ -2933,6 +2949,84 @@ OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
        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
@@ -3307,39 +3401,36 @@ OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
  * @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 */
 
@@ -3350,26 +3441,43 @@ OO.initClass( OO.ui.ButtonedElement );
  * @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' );
-       this.$button
-               // Remove the tab-index while the button is down to prevent the button from stealing focus
-               .removeAttr( 'tabindex' )
-               .addClass( 'oo-ui-buttonedElement-pressed' );
+       // Remove the tab-index while the button is down to prevent the button from stealing focus
+       this.$button.removeAttr( 'tabindex' );
+       this.$element.addClass( 'oo-ui-buttonElement-pressed' );
        // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
        // reliably reapply the tabindex and remove the pressed class
        this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
@@ -3384,14 +3492,13 @@ OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) {
  *
  * @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;
        }
-       this.$button
-               // Restore the tab-index after the button is up to restore the button's accesssibility
-               .attr( 'tabindex', this.tabIndex )
-               .removeClass( 'oo-ui-buttonedElement-pressed' );
+       // 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-buttonElement-pressed' );
        // Stop listening for mouseup, since we only needed this once
        this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
 };
@@ -3402,13 +3509,13 @@ OO.ui.ButtonedElement.prototype.onMouseUp = function ( e ) {
  * @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;
@@ -3420,27 +3527,43 @@ OO.ui.ButtonedElement.prototype.toggleFramed = function ( framed ) {
  * @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;
 };
 
@@ -3450,420 +3573,184 @@ OO.ui.ButtonedElement.prototype.setAccessKey = function ( accessKey ) {
  * @param {boolean} [value] Make button active
  * @chainable
  */
-OO.ui.ButtonedElement.prototype.setActive = function ( value ) {
-       this.$button.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.
- *
- * 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.
+ * Remove items.
  *
- * @abstract
- * @class
+ * Items will be detached, not removed, so they can be used later.
  *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
+ * @param {OO.ui.Element[]} items Items to remove
+ * @chainable
  */
-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 */
-
-/**
- * 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.
- *
- * @param {OO.ui.Element[]} items Items to remove
- * @chainable
- */
-OO.ui.GroupElement.prototype.removeItems = function ( items ) {
-       var i, len, item, index, remove, itemEvent;
+OO.ui.GroupElement.prototype.removeItems = function ( items ) {
+       var i, len, item, index, remove, itemEvent;
 
        // Remove specific items
        for ( i = 0, len = items.length; i < len; i++ ) {
@@ -3932,28 +3819,31 @@ OO.ui.GroupElement.prototype.clearItems = function () {
  * @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 */
 
@@ -3974,32 +3864,93 @@ OO.initClass( OO.ui.IconedElement );
  *  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 the icon element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $icon Element to use as icon
+ */
+OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
+       if ( this.$icon ) {
+               this.$icon
+                       .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
+                       .removeAttr( 'title' );
+       }
+
+       this.$icon = $icon
+               .addClass( 'oo-ui-iconElement-icon' )
+               .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
+       if ( this.iconTitle !== null ) {
+               this.$icon.attr( 'title', this.iconTitle );
+       }
+};
+
 /**
  * Set icon.
  *
- * @param {Object|string} icon Symbolic icon name, or map of icon names keyed by language ID;
+ * @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
+ *  language, use null to remove icon
  * @chainable
  */
-OO.ui.IconedElement.prototype.setIcon = function ( icon ) {
+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 ) {
-               this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
+       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;
        }
-       if ( typeof icon === 'string' ) {
-               icon = icon.trim();
-               if ( icon.length ) {
-                       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;
 };
@@ -4009,7 +3960,7 @@ OO.ui.IconedElement.prototype.setIcon = function ( icon ) {
  *
  * @return {string} Icon
  */
-OO.ui.IconedElement.prototype.getIcon = function () {
+OO.ui.IconElement.prototype.getIcon = function () {
        return this.icon;
 };
 
@@ -4025,29 +3976,30 @@ OO.ui.IconedElement.prototype.getIcon = function () {
  * @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 */
 
@@ -4058,57 +4010,89 @@ OO.initClass( OO.ui.IndicatedElement );
  * @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 indicator.
+ * Set the indicator element.
  *
- * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
- * @chainable
+ * 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.IndicatedElement.prototype.setIndicator = function ( indicator ) {
-       if ( this.indicator ) {
-               this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
-               this.indicator = null;
+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 );
        }
-       if ( typeof indicator === 'string' ) {
-               indicator = indicator.trim();
-               if ( indicator.length ) {
-                       this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
-                       this.indicator = indicator;
+};
+
+/**
+ * Set indicator.
+ *
+ * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
+ * @chainable
+ */
+OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) {
+       indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
+
+       if ( this.indicator !== indicator ) {
+               if ( this.$indicator ) {
+                       if ( this.indicator !== null ) {
+                               this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
+                       }
+                       if ( indicator !== null ) {
+                               this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
+                       }
                }
+               this.indicator = indicator;
        }
-       this.$element.toggleClass( 'oo-ui-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;
@@ -4119,7 +4103,7 @@ OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle )
  *
  * @return {string} title Symbolic name of indicator
  */
-OO.ui.IndicatedElement.prototype.getIndicator = function () {
+OO.ui.IndicatorElement.prototype.getIndicator = function () {
        return this.indicator;
 };
 
@@ -4128,7 +4112,7 @@ OO.ui.IndicatedElement.prototype.getIndicator = function () {
  *
  * @return {string} Indicator title text
  */
-OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () {
+OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
        return this.indicatorTitle;
 };
 
@@ -4139,28 +4123,28 @@ OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () {
  * @class
  *
  * @constructor
- * @param {jQuery} $label Label node, assigned to #$label
  * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$label] Label node, assigned to #$label, omit to use a generated `<span>`
  * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
  * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
  */
-OO.ui.LabeledElement = function OoUiLabeledElement( $label, config ) {
+OO.ui.LabelElement = function OoUiLabelElement( config ) {
        // Config intialization
        config = config || {};
 
        // Properties
-       this.$label = $label;
+       this.$label = null;
        this.label = null;
+       this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
 
        // Initialization
-       this.$label.addClass( 'oo-ui-labeledElement-label' );
        this.setLabel( config.label || this.constructor.static.label );
-       this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
+       this.setLabelElement( config.$label || this.$( '<span>' ) );
 };
 
 /* Setup */
 
-OO.initClass( OO.ui.LabeledElement );
+OO.initClass( OO.ui.LabelElement );
 
 /* Static Properties */
 
@@ -4169,42 +4153,51 @@ OO.initClass( OO.ui.LabeledElement );
  *
  * @static
  * @inheritable
- * @property {string|Function|null} Label text; a function that returns nodes or text; or null for
+ * @property {string|Function|null} Label text; a function that returns nodes or text; or null for
  *  no label
  */
-OO.ui.LabeledElement.static.label = null;
+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 &nbsp;
  *
- * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or
+ * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
  *  text; or null for no label
  * @chainable
  */
-OO.ui.LabeledElement.prototype.setLabel = function ( label ) {
-       var empty = false;
+OO.ui.LabelElement.prototype.setLabel = function ( label ) {
+       label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
+       label = ( typeof label === 'string' && label.length ) || label instanceof jQuery ? label : null;
 
-       this.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( '&nbsp;' );
-               } else {
-                       this.$label.text( label );
+       if ( this.label !== label ) {
+               if ( this.$label ) {
+                       this.setLabelContent( label );
                }
-       } else if ( label instanceof jQuery ) {
-               this.$label.empty().append( label );
-       } else {
-               this.$label.empty();
-               empty = true;
+               this.label = label;
        }
-       this.$element.toggleClass( 'oo-ui-labeledElement', !empty );
-       this.$label.css( 'display', empty ? 'none' : '' );
+
+       this.$element.toggleClass( 'oo-ui-labelElement', !!this.label );
 
        return this;
 };
@@ -4215,7 +4208,7 @@ OO.ui.LabeledElement.prototype.setLabel = function ( label ) {
  * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
  *  text; or null for no label
  */
-OO.ui.LabeledElement.prototype.getLabel = function () {
+OO.ui.LabelElement.prototype.getLabel = function () {
        return this.label;
 };
 
@@ -4224,13 +4217,39 @@ OO.ui.LabeledElement.prototype.getLabel = function () {
  *
  * @chainable
  */
-OO.ui.LabeledElement.prototype.fitLabel = function () {
-       if ( this.$label.autoEllipsis && this.autoFitLabel ) {
+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( '&nbsp;' );
+               } 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.
  *
@@ -4242,7 +4261,7 @@ OO.ui.LabeledElement.prototype.fitLabel = function () {
  * @cfg {Object} [popup] Configuration to pass to popup
  * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus
  */
-OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) {
+OO.ui.PopupElement = function OoUiPopupElement( config ) {
        // Configuration initialization
        config = config || {};
 
@@ -4254,85 +4273,472 @@ OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) {
        ) );
 };
 
-/* Methods */
-
+/* 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.TitledElement = function OoUiTitledElement( config ) {
+       // Config intialization
+       config = config || {};
+
+       // Properties
+       this.$titled = null;
+       this.title = null;
+
+       // Initialization
+       this.setTitle( config.title || this.constructor.static.title );
+       this.setTitledElement( config.$titled || this.$element );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.TitledElement );
+
+/* Static Properties */
+
+/**
+ * Title.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function} Title text or a function that returns text
+ */
+OO.ui.TitledElement.static.title = null;
+
+/* Methods */
+
+/**
+ * Set the titled element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $titled Element to set title on
+ */
+OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) {
+       if ( this.$titled ) {
+               this.$titled.removeAttr( 'title' );
+       }
+
+       this.$titled = $titled;
+       if ( this.title ) {
+               this.$titled.attr( 'title', this.title );
+       }
+};
+
+/**
+ * Set title.
+ *
+ * @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 ) {
+       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;
+};
+
+/**
+ * Get title.
+ *
+ * @return {string} Title string
+ */
+OO.ui.TitledElement.prototype.getTitle = function () {
+       return this.title;
+};
+
+/**
+ * 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 {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
+ */
+OO.ui.ClippableElement = function OoUiClippableElement( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Properties
+       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 */
+
+/**
+ * Set clippable element.
+ *
+ * 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.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();
+};
+
+/**
+ * Toggle clipping.
+ *
+ * Do not turn clipping on until after the element is attached to the DOM and visible.
+ *
+ * @param {boolean} [clipping] Enable clipping, omit to toggle
+ * @chainable
+ */
+OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
+       clipping = clipping === undefined ? !this.clipping : !!clipping;
+
+       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: '' } );
+
+                       this.$clippableContainer = null;
+                       this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
+                       this.$clippableScroller = null;
+                       this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
+                       this.$clippableWindow = null;
+               }
+       }
+
+       return this;
+};
+
+/**
+ * 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;
+};
+
 /**
- * Get popup.
+ * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
  *
- * @return {OO.ui.PopupWidget} Popup widget
+ * @return {boolean} Part of the element is being clipped
  */
-OO.ui.PopuppableElement.prototype.getPopup = function () {
-       return this.popup;
+OO.ui.ClippableElement.prototype.isClipped = function () {
+       return this.clippedHorizontally || this.clippedVertically;
 };
 
 /**
- * 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
+ * Check if the right of the element is being clipped by the nearest scrollable container.
  *
- * @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
+ * @return {boolean} Part of the element is being clipped
  */
-OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) {
-       // Config intialization
-       config = config || {};
-
-       // Properties
-       this.$titled = $titled;
-       this.title = null;
-
-       // Initialization
-       this.setTitle( config.title || this.constructor.static.title );
+OO.ui.ClippableElement.prototype.isClippedHorizontally = function () {
+       return this.clippedHorizontally;
 };
 
-/* Setup */
-
-OO.initClass( OO.ui.TitledElement );
-
-/* Static Properties */
-
 /**
- * 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;
 };
 
 /**
@@ -4341,7 +4747,7 @@ OO.ui.TitledElement.prototype.getTitle = function () {
  * @abstract
  * @class
  * @extends OO.ui.Widget
- * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.IconElement
  *
  * @constructor
  * @param {OO.ui.ToolGroup} toolGroup
@@ -4356,7 +4762,7 @@ OO.ui.Tool = function OoUiTool( toolGroup, config ) {
        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;
@@ -4389,7 +4795,7 @@ OO.ui.Tool = function OoUiTool( toolGroup, config ) {
 /* 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 */
 
@@ -4613,7 +5019,7 @@ OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
 
        // Mixin constructors
        OO.EventEmitter.call( this );
-       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+       OO.ui.GroupElement.call( this, config );
 
        // Properties
        this.toolFactory = toolFactory;
@@ -4828,7 +5234,7 @@ OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
        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;
@@ -5132,9 +5538,9 @@ OO.ui.ToolGroup.prototype.destroy = function () {
  * @constructor
  * @param {Object} [config] Configuration options
  */
-OO.ui.MessageDialog = function OoUiMessageDialog( manager, config ) {
+OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
        // Parent constructor
-       OO.ui.MessageDialog.super.call( this, manager, config );
+       OO.ui.MessageDialog.super.call( this, config );
 
        // Properties
        this.verticalActionLayout = null;
@@ -5280,7 +5686,7 @@ OO.ui.MessageDialog.prototype.initialize = function () {
 
        // Initialization
        this.title.$element.addClass( 'oo-ui-messageDialog-title' );
-       this.frame.$content.addClass( 'oo-ui-messageDialog-content' );
+       this.$content.addClass( 'oo-ui-messageDialog-content' );
        this.container.$element.append( this.text.$element );
        this.text.$element.append( this.title.$element, this.message.$element );
        this.$body.append( this.container.$element );
@@ -5368,9 +5774,9 @@ OO.ui.MessageDialog.prototype.fitActions = function () {
  * @constructor
  * @param {Object} [config] Configuration options
  */
-OO.ui.ProcessDialog = function OoUiProcessDialog( manager, config ) {
+OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
        // Parent constructor
-       OO.ui.ProcessDialog.super.call( this, manager, config );
+       OO.ui.ProcessDialog.super.call( this, config );
 
        // Initialization
        this.$element.addClass( 'oo-ui-processDialog' );
@@ -5453,7 +5859,7 @@ OO.ui.ProcessDialog.prototype.initialize = function () {
        this.$errors
                .addClass( 'oo-ui-processDialog-errors' )
                .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
-       this.frame.$content
+       this.$content
                .addClass( 'oo-ui-processDialog-content' )
                .append( this.$errors );
        this.$navigation
@@ -5988,7 +6394,7 @@ OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
  *
  * @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
@@ -6007,7 +6413,6 @@ OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
  * @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 );
 
@@ -6015,27 +6420,25 @@ OO.ui.FieldLayout = function OoUiFieldLayout( field, 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 ) {
@@ -6055,7 +6458,7 @@ OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) {
 /* 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 */
 
@@ -6128,8 +6531,8 @@ OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
  *
  * @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
@@ -6145,9 +6548,9 @@ OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
        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
@@ -6161,8 +6564,8 @@ OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
 /* 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 */
@@ -6555,7 +6958,7 @@ OO.ui.StackLayout = function OoUiStackLayout( config ) {
        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;
@@ -6741,9 +7144,9 @@ OO.ui.BarToolGroup.static.name = 'bar';
  * @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
  *
@@ -6760,11 +7163,11 @@ OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
        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;
@@ -6800,9 +7203,9 @@ OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
 /* 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 );
 
@@ -6879,13 +7282,25 @@ OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
        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 );
                }
        }
 };
@@ -6979,7 +7394,7 @@ OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
  * @abstract
  * @class
  * @extends OO.ui.Tool
- * @mixins OO.ui.PopuppableElement
+ * @mixins OO.ui.PopupElement
  *
  * @constructor
  * @param {OO.ui.Toolbar} toolbar
@@ -6990,7 +7405,7 @@ OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
        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
@@ -7001,7 +7416,7 @@ OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
 /* 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 */
 
@@ -7037,12 +7452,11 @@ OO.ui.PopupTool.prototype.onUpdateState = function () {
  * @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 */
@@ -7368,7 +7782,7 @@ OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
  * @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
@@ -7382,8 +7796,8 @@ OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, confi
        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;
@@ -7430,7 +7844,7 @@ OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, confi
 
 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 */
 
@@ -7553,7 +7967,7 @@ OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
        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' );
@@ -7572,12 +7986,12 @@ OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
  *
  * @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
@@ -7592,12 +8006,12 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
        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;
@@ -7622,12 +8036,12 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
 /* 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 */
 
@@ -7824,7 +8238,7 @@ OO.ui.ActionWidget.prototype.propagateResize = function () {
  */
 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;
@@ -7835,7 +8249,7 @@ OO.ui.ActionWidget.prototype.setIcon = function () {
  */
 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;
@@ -7846,7 +8260,7 @@ OO.ui.ActionWidget.prototype.setLabel = function () {
  */
 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;
@@ -7857,7 +8271,7 @@ OO.ui.ActionWidget.prototype.setFlags = function () {
  */
 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;
@@ -7882,7 +8296,7 @@ OO.ui.ActionWidget.prototype.toggle = function () {
  *
  * @class
  * @extends OO.ui.ButtonWidget
- * @mixins OO.ui.PopuppableElement
+ * @mixins OO.ui.PopupElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -7892,7 +8306,7 @@ OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
        OO.ui.PopupButtonWidget.super.call( this, config );
 
        // Mixin constructors
-       OO.ui.PopuppableElement.call( this, config );
+       OO.ui.PopupElement.call( this, config );
 
        // Initialization
        this.$element
@@ -7903,7 +8317,7 @@ OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
 /* 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 */
 
@@ -7988,9 +8402,11 @@ OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
 /**
  * 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
@@ -8004,8 +8420,8 @@ OO.ui.IconWidget = function OoUiIconWidget( config ) {
        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' );
@@ -8014,7 +8430,7 @@ OO.ui.IconWidget = function OoUiIconWidget( config ) {
 /* 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 */
@@ -8024,11 +8440,11 @@ OO.ui.IconWidget.static.tagName = 'span';
 /**
  * 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
@@ -8042,8 +8458,8 @@ OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
        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' );
@@ -8052,7 +8468,7 @@ OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
 /* 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 */
@@ -8069,9 +8485,9 @@ OO.ui.IndicatorWidget.static.tagName = 'span';
  *
  * @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
@@ -8086,10 +8502,10 @@ OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
        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 ) );
@@ -8111,9 +8527,9 @@ OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
 /* 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 */
@@ -8450,45 +8866,43 @@ OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
  *
  * @class
  * @extends OO.ui.InputWidget
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.IndicatorElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
  * @cfg {string} [placeholder] Placeholder text
- * @cfg {string} [icon] Symbolic name of icon
  * @cfg {boolean} [multiline=false] Allow multiple lines of text
  * @cfg {boolean} [autosize=false] Automatically resize to fit content
  * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
  */
 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
-       var widget = this;
-       config = $.extend( { maxRows: 10 }, config );
+       // Configuration initialization
+       config = config || {};
 
        // Parent constructor
        OO.ui.TextInputWidget.super.call( this, config );
 
+       // Mixin constructors
+       OO.ui.IconElement.call( this, config );
+       OO.ui.IndicatorElement.call( this, config );
+
        // Properties
        this.pending = 0;
        this.multiline = !!config.multiline;
        this.autosize = !!config.autosize;
-       this.maxRows = config.maxRows;
+       this.maxRows = config.maxRows !== undefined ? config.maxRows : 10;
 
        // Events
        this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) );
        this.$element.on( 'DOMNodeInsertedIntoDocument', OO.ui.bind( this.onElementAttach, this ) );
+       this.$icon.on( 'mousedown', OO.ui.bind( this.onIconMouseDown, this ) );
+       this.$indicator.on( 'mousedown', OO.ui.bind( this.onIndicatorMouseDown, this ) );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-textInputWidget' );
-       if ( config.icon ) {
-               this.$element.addClass( 'oo-ui-textInputWidget-decorated' );
-               this.$element.append(
-                       this.$( '<span>' )
-                               .addClass( 'oo-ui-textInputWidget-icon oo-ui-icon-' + config.icon )
-                               .mousedown( function () {
-                                       widget.$input[0].focus();
-                                       return false;
-                               } )
-               );
-       }
+       this.$element
+               .addClass( 'oo-ui-textInputWidget' )
+               .append( this.$icon, this.$indicator );
        if ( config.placeholder ) {
                this.$input.attr( 'placeholder', config.placeholder );
        }
@@ -8498,6 +8912,8 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
 /* Setup */
 
 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
 
 /* Events */
 
@@ -8509,8 +8925,48 @@ OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
  * @event enter
  */
 
+/**
+ * User clicks the icon.
+ *
+ * @event icon
+ */
+
+/**
+ * User clicks the indicator.
+ *
+ * @event indicator
+ */
+
 /* Methods */
 
+/**
+ * Handle icon mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
+ * @fires icon
+ */
+OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
+       if ( e.which === 1 ) {
+               this.$input[0].focus();
+               this.emit( 'icon' );
+               return false;
+       }
+};
+
+/**
+ * Handle indicator mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
+ * @fires indicator
+ */
+OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
+       if ( e.which === 1 ) {
+               this.$input[0].focus();
+               this.emit( 'indicator' );
+               return false;
+       }
+};
+
 /**
  * Handle key press events.
  *
@@ -8671,12 +9127,134 @@ OO.ui.TextInputWidget.prototype.select = function () {
        return this;
 };
 
+/**
+ * Text input with a menu of optional values.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object} [menu] Configuration options to pass to menu widget
+ * @cfg {Object} [input] Configuration options to pass to input widget
+ */
+OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.ComboBoxWidget.super.call( this, config );
+
+       // Properties
+       this.input = new OO.ui.TextInputWidget( $.extend(
+               { $: this.$, indicator: 'down', disabled: this.isDisabled() },
+               config.input
+       ) );
+       this.menu = new OO.ui.MenuWidget( $.extend(
+               { $: this.$, widget: this, input: this.input, disabled: this.isDisabled() },
+               config.menu
+       ) );
+
+       // Events
+       this.input.connect( this, {
+               change: 'onInputChange',
+               indicator: 'onInputIndicator',
+               enter: 'onInputEnter'
+       } );
+       this.menu.connect( this, {
+               choose: 'onMenuChoose',
+               add: 'onMenuItemsChange',
+               remove: 'onMenuItemsChange'
+       } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-comboBoxWidget' ).append(
+               this.input.$element,
+               this.menu.$element
+       );
+       this.onMenuItemsChange();
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
+
+/* Methods */
+
+/**
+ * Handle input change events.
+ *
+ * @param {string} value New value
+ */
+OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
+       var match = this.menu.getItemFromData( value );
+
+       this.menu.selectItem( match );
+
+       if ( !this.isDisabled() ) {
+               this.menu.toggle( true );
+       }
+};
+
+/**
+ * Handle input indicator events.
+ */
+OO.ui.ComboBoxWidget.prototype.onInputIndicator = function () {
+       if ( !this.isDisabled() ) {
+               this.menu.toggle();
+       }
+};
+
+/**
+ * Handle input enter events.
+ */
+OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
+       if ( !this.isDisabled() ) {
+               this.menu.toggle( false );
+       }
+};
+
+/**
+ * Handle menu choose events.
+ *
+ * @param {OO.ui.OptionWidget} item Chosen item
+ */
+OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
+       if ( item ) {
+               this.input.setValue( item.getData() );
+       }
+};
+
+/**
+ * Handle menu item change events.
+ */
+OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
+       this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
+       // Parent method
+       OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled );
+
+       if ( this.input ) {
+               this.input.setDisabled( this.isDisabled() );
+       }
+       if ( this.menu ) {
+               this.menu.setDisabled( this.isDisabled() );
+       }
+
+       return this;
+};
+
 /**
  * Label widget.
  *
  * @class
  * @extends OO.ui.Widget
- * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.LabelElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -8689,7 +9267,7 @@ OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
        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;
@@ -8706,11 +9284,11 @@ OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
 /* 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 */
 
@@ -8729,8 +9307,8 @@ OO.ui.LabelWidget.prototype.onClick = function () {
  *
  * @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
@@ -8746,8 +9324,8 @@ OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
 
        // 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;
@@ -8771,13 +9349,11 @@ OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
 
 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;
@@ -8933,8 +9509,8 @@ OO.ui.OptionWidget.prototype.getData = function () {
  *
  * @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
@@ -8945,8 +9521,8 @@ OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( data, config )
        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
@@ -8958,8 +9534,8 @@ OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( data, config )
 /* 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.
@@ -8968,7 +9544,7 @@ OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatedElement );
  *
  * @class
  * @extends OO.ui.DecoratedOptionWidget
- * @mixins OO.ui.ButtonedElement
+ * @mixins OO.ui.ButtonElement
  *
  * @constructor
  * @param {Mixed} data Option data
@@ -8979,7 +9555,7 @@ OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
        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' );
@@ -8990,7 +9566,7 @@ OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
 /* 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 */
 
@@ -9197,7 +9773,7 @@ OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) {
  *
  * @class
  * @extends OO.ui.Widget
- * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.LabelElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -9220,14 +9796,14 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
        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;
@@ -9267,12 +9843,13 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
        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 */
@@ -9366,13 +9943,13 @@ OO.ui.PopupWidget.prototype.toggle = function ( show ) {
 
        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();
                        }
@@ -9642,7 +10219,7 @@ OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        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;
@@ -9706,10 +10283,6 @@ OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
  * @param {OO.ui.OptionWidget[]} items Removed items
  */
 
-/* Static Properties */
-
-OO.ui.SelectWidget.static.tagName = 'ul';
-
 /* Methods */
 
 /**
@@ -10191,7 +10764,7 @@ OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
        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;
@@ -10330,13 +10903,7 @@ OO.ui.MenuWidget.prototype.chooseItem = function ( item ) {
 };
 
 /**
- * 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;
@@ -10352,13 +10919,42 @@ OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
        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;
 };
 
@@ -10366,7 +10962,7 @@ OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
  * @inheritdoc
  */
 OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
-       visible = !!visible && !!this.items.length;
+       visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
 
        var i, len,
                change = visible !== this.isVisible();
@@ -10389,7 +10985,7 @@ OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
                                }
                                this.newItems = null;
                        }
-                       this.setClipping( true );
+                       this.toggleClipping( true );
 
                        // Auto-hide
                        if ( this.autoHide ) {
@@ -10406,7 +11002,7 @@ OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
                        this.getElementDocument().removeEventListener(
                                'mousedown', this.onDocumentMouseDownHandler, true
                        );
-                       this.setClipping( false );
+                       this.toggleClipping( false );
                }
        }
 
@@ -10416,7 +11012,7 @@ OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
 /**
  * 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.
  *
@@ -10492,9 +11088,9 @@ OO.ui.TextInputMenuWidget.prototype.position = function () {
        dimensions.top += $container.height();
 
        // Compensate for frame position if in a differnt frame
-       if ( this.input.$.frame && this.input.$.context !== this.$element[0].ownerDocument ) {
+       if ( this.input.$.$iframe && this.input.$.context !== this.$element[0].ownerDocument ) {
                frameOffset = OO.ui.Element.getRelativePosition(
-                       this.input.$.frame.$element, this.$element.offsetParent()
+                       this.input.$.$iframe, this.$element.offsetParent()
                );
                dimensions.left += frameOffset.left;
                dimensions.top += frameOffset.top;