Merge "Ensure integer compare in Special:WantedCategories"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
index d0d0620..5f75895 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (f1abca8e82)
+ * OOjs UI v0.6.0
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2014 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2014-11-17T23:49:27Z
+ * Date: 2014-12-16T21:00:55Z
  */
 ( function ( OO ) {
 
@@ -101,8 +101,8 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
  *
  * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
  * @param {HTMLElement} contained Node to find
- * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendents
- * @returns {boolean} The node is in the list of target nodes
+ * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
+ * @return {boolean} The node is in the list of target nodes
  */
 OO.ui.contains = function ( containers, contained, matchContainers ) {
        var i;
@@ -314,7 +314,7 @@ OO.ui.PendingElement.prototype.popPending = function () {
  * @param {Object} [config] Configuration options
  */
 OO.ui.ActionSet = function OoUiActionSet( config ) {
-       // Configuration intialization
+       // Configuration initialization
        config = config || {};
 
        // Mixin constructors
@@ -656,7 +656,7 @@ OO.ui.ActionSet.prototype.clear = function () {
 /**
  * Organize actions.
  *
- * This is called whenver organized information is requested. It will only reorganize the actions
+ * This is called whenever organized information is requested. It will only reorganize the actions
  * if something has changed since the last time it ran.
  *
  * @private
@@ -673,7 +673,7 @@ OO.ui.ActionSet.prototype.organize = function () {
                for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
                        action = this.list[i];
                        if ( action.isVisible() ) {
-                               // Populate catgeories
+                               // Populate categories
                                for ( category in this.categories ) {
                                        if ( !this.categorized[category] ) {
                                                this.categorized[category] = {};
@@ -723,13 +723,15 @@ OO.ui.ActionSet.prototype.organize = function () {
  * @cfg {string[]} [classes] CSS class names to add
  * @cfg {string} [text] Text to insert
  * @cfg {jQuery} [$content] Content elements to append (after text)
+ * @cfg {Mixed} [data] Element data
  */
 OO.ui.Element = function OoUiElement( config ) {
        // Configuration initialization
        config = config || {};
 
        // Properties
-       this.$ = config.$ || OO.ui.Element.getJQuery( document );
+       this.$ = config.$ || OO.ui.Element.static.getJQuery( document );
+       this.data = config.data;
        this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
        this.elementGroup = null;
        this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
@@ -775,7 +777,7 @@ OO.ui.Element.static.tagName = 'div';
  *   not in an iframe
  * @return {Function} Bound jQuery function
  */
-OO.ui.Element.getJQuery = function ( context, $iframe ) {
+OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
        function wrapper( selector ) {
                return $( selector, wrapper.context );
        }
@@ -796,7 +798,7 @@ OO.ui.Element.getJQuery = function ( context, $iframe ) {
  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
  * @return {HTMLDocument|null} Document object
  */
-OO.ui.Element.getDocument = function ( obj ) {
+OO.ui.Element.static.getDocument = function ( obj ) {
        // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
        return ( obj[0] && obj[0].ownerDocument ) ||
                // Empty jQuery selections might have a context
@@ -817,7 +819,7 @@ OO.ui.Element.getDocument = function ( obj ) {
  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
  * @return {Window} Window object
  */
-OO.ui.Element.getWindow = function ( obj ) {
+OO.ui.Element.static.getWindow = function ( obj ) {
        var doc = this.getDocument( obj );
        return doc.parentWindow || doc.defaultView;
 };
@@ -829,7 +831,7 @@ OO.ui.Element.getWindow = function ( obj ) {
  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
  * @return {string} Text direction, either 'ltr' or 'rtl'
  */
-OO.ui.Element.getDir = function ( obj ) {
+OO.ui.Element.static.getDir = function ( obj ) {
        var isDoc, isWin;
 
        if ( obj instanceof jQuery ) {
@@ -857,7 +859,7 @@ OO.ui.Element.getDir = function ( obj ) {
  * @param {Object} [offset] Offset to start with, used internally
  * @return {Object} Offset object, containing left and top properties
  */
-OO.ui.Element.getFrameOffset = function ( from, to, offset ) {
+OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
        var i, len, frames, frame, rect;
 
        if ( !to ) {
@@ -902,7 +904,7 @@ OO.ui.Element.getFrameOffset = function ( from, to, offset ) {
  * @param {jQuery} $anchor Element to get $element's position relative to
  * @return {Object} Translated position coordinates, containing top and left properties
  */
-OO.ui.Element.getRelativePosition = function ( $element, $anchor ) {
+OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
        var iframe, iframePos,
                pos = $element.offset(),
                anchorPos = $anchor.offset(),
@@ -932,7 +934,7 @@ OO.ui.Element.getRelativePosition = function ( $element, $anchor ) {
  * @param {HTMLElement} el Element to measure
  * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
  */
-OO.ui.Element.getBorders = function ( el ) {
+OO.ui.Element.static.getBorders = function ( el ) {
        var doc = el.ownerDocument,
                win = doc.parentWindow || doc.defaultView,
                style = win && win.getComputedStyle ?
@@ -959,7 +961,7 @@ OO.ui.Element.getBorders = function ( el ) {
  * @param {HTMLElement|Window} el Element to measure
  * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
  */
-OO.ui.Element.getDimensions = function ( el ) {
+OO.ui.Element.static.getDimensions = function ( el ) {
        var $el, $win,
                doc = el.ownerDocument || el.document,
                win = doc.parentWindow || doc.defaultView;
@@ -997,6 +999,38 @@ OO.ui.Element.getDimensions = function ( el ) {
        }
 };
 
+/**
+ * Get scrollable object parent
+ *
+ * documentElement can't be used to get or set the scrollTop
+ * property on Blink. Changing and testing its value lets us
+ * use 'body' or 'documentElement' based on what is working.
+ *
+ * https://code.google.com/p/chromium/issues/detail?id=303131
+ *
+ * @static
+ * @param {HTMLElement} el Element to find scrollable parent for
+ * @return {HTMLElement} Scrollable parent
+ */
+OO.ui.Element.static.getRootScrollableElement = function ( el ) {
+       var scrollTop, body;
+
+       if ( OO.ui.scrollableElement === undefined ) {
+               body = el.ownerDocument.body;
+               scrollTop = body.scrollTop;
+               body.scrollTop = 1;
+
+               if ( body.scrollTop === 1 ) {
+                       body.scrollTop = scrollTop;
+                       OO.ui.scrollableElement = 'body';
+               } else {
+                       OO.ui.scrollableElement = 'documentElement';
+               }
+       }
+
+       return el.ownerDocument[ OO.ui.scrollableElement ];
+};
+
 /**
  * Get closest scrollable container.
  *
@@ -1008,7 +1042,7 @@ OO.ui.Element.getDimensions = function ( el ) {
  * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
  * @return {HTMLElement} Closest scrollable container
  */
-OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) {
+OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
        var i, val,
                props = [ 'overflow' ],
                $parent = $( el ).parent();
@@ -1018,7 +1052,7 @@ OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) {
        }
 
        while ( $parent.length ) {
-               if ( $parent[0] === el.ownerDocument.body ) {
+               if ( $parent[0] === this.getRootScrollableElement( el ) ) {
                        return $parent[0];
                }
                i = props.length;
@@ -1044,7 +1078,7 @@ OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) {
  *  to scroll in both directions
  * @param {Function} [config.complete] Function to call when scrolling completes
  */
-OO.ui.Element.scrollIntoView = function ( el, config ) {
+OO.ui.Element.static.scrollIntoView = function ( el, config ) {
        // Configuration initialization
        config = config || {};
 
@@ -1057,8 +1091,8 @@ OO.ui.Element.scrollIntoView = function ( el, config ) {
                $win = $( this.getWindow( el ) );
 
        // Compute the distances between the edges of el and the edges of the scroll viewport
-       if ( $sc.is( 'body' ) ) {
-               // If the scrollable container is the <body> this is easy
+       if ( $sc.is( 'html, body' ) ) {
+               // If the scrollable container is the root, this is easy
                rel = {
                        top: eld.rect.top,
                        bottom: $win.innerHeight() - eld.rect.bottom,
@@ -1104,37 +1138,28 @@ OO.ui.Element.scrollIntoView = function ( el, config ) {
        }
 };
 
+/* Methods */
+
 /**
- * Bind a handler for an event on a DOM element.
- *
- * Used to be for working around a jQuery bug (jqbug.com/14180),
- * but obsolete as of jQuery 1.11.0.
+ * Get element data.
  *
- * @static
- * @deprecated Use jQuery#on instead.
- * @param {HTMLElement|jQuery} el DOM element
- * @param {string} event Event to bind
- * @param {Function} callback Callback to call when the event fires
+ * @return {Mixed} Element data
  */
-OO.ui.Element.onDOMEvent = function ( el, event, callback ) {
-       $( el ).on( event, callback );
+OO.ui.Element.prototype.getData = function () {
+       return this.data;
 };
 
 /**
- * Unbind a handler bound with #static-method-onDOMEvent.
+ * Set element data.
  *
- * @deprecated Use jQuery#off instead.
- * @static
- * @param {HTMLElement|jQuery} el DOM element
- * @param {string} event Event to unbind
- * @param {Function} [callback] Callback to unbind
+ * @param {Mixed} Element data
+ * @chainable
  */
-OO.ui.Element.offDOMEvent = function ( el, event, callback ) {
-       $( el ).off( event, callback );
+OO.ui.Element.prototype.setData = function ( data ) {
+       this.data = data;
+       return this;
 };
 
-/* Methods */
-
 /**
  * Check if element supports one or more methods.
  *
@@ -1158,7 +1183,7 @@ OO.ui.Element.prototype.supports = function ( methods ) {
 /**
  * Update the theme-provided classes.
  *
- * @localdoc This is called in element mixins and widget classes anytime state changes.
+ * @localdoc This is called in element mixins and widget classes any time state changes.
  *   Updating is debounced, minimizing overhead of changing multiple attributes and
  *   guaranteeing that theme updates do not occur within an element's constructor
  */
@@ -1202,7 +1227,9 @@ OO.ui.Element.prototype.isElementAttached = function () {
  * @return {HTMLDocument} Document object
  */
 OO.ui.Element.prototype.getElementDocument = function () {
-       return OO.ui.Element.getDocument( this.$element );
+       // Don't use this.$.context because subclasses can rebind this.$
+       // Don't cache this in other ways either because subclasses could can change this.$element
+       return OO.ui.Element.static.getDocument( this.$element );
 };
 
 /**
@@ -1211,14 +1238,14 @@ OO.ui.Element.prototype.getElementDocument = function () {
  * @return {Window} Window object
  */
 OO.ui.Element.prototype.getElementWindow = function () {
-       return OO.ui.Element.getWindow( this.$element );
+       return OO.ui.Element.static.getWindow( this.$element );
 };
 
 /**
  * Get closest scrollable container.
  */
 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
-       return OO.ui.Element.getClosestScrollableContainer( this.$element[0] );
+       return OO.ui.Element.static.getClosestScrollableContainer( this.$element[0] );
 };
 
 /**
@@ -1247,29 +1274,7 @@ OO.ui.Element.prototype.setElementGroup = function ( group ) {
  * @param {Object} [config] Configuration options
  */
 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
-       return OO.ui.Element.scrollIntoView( this.$element[0], config );
-};
-
-/**
- * Bind a handler for an event on this.$element
- *
- * @deprecated Use jQuery#on instead.
- * @param {string} event
- * @param {Function} callback
- */
-OO.ui.Element.prototype.onDOMEvent = function ( event, callback ) {
-       OO.ui.Element.onDOMEvent( this.$element, event, callback );
-};
-
-/**
- * Unbind a handler bound with #offDOMEvent
- *
- * @deprecated Use jQuery#off instead.
- * @param {string} event
- * @param {Function} callback
- */
-OO.ui.Element.prototype.offDOMEvent = function ( event, callback ) {
-       OO.ui.Element.offDOMEvent( this.$element, event, callback );
+       return OO.ui.Element.static.scrollIntoView( this.$element[0], config );
 };
 
 /**
@@ -1451,8 +1456,8 @@ OO.ui.Widget.prototype.updateDisabled = function () {
  *
  * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding
  * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and
- * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchonous
- * processing can complete. Always assume window processes are executed asychronously. See
+ * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchronous
+ * processing can complete. Always assume window processes are executed asynchronously. See
  * OO.ui.Process for more details about how to work with processes. Some events, as well as the
  * #open and #close methods, provide promises which are resolved when the window enters a new state.
  *
@@ -1494,7 +1499,7 @@ OO.ui.Window = function OoUiWindow( config ) {
        this.$frame.addClass( 'oo-ui-window-frame' );
        this.$overlay.addClass( 'oo-ui-window-overlay' );
 
-       // NOTE: Additional intitialization will occur when #setManager is called
+       // NOTE: Additional initialization will occur when #setManager is called
 };
 
 /* Setup */
@@ -1728,17 +1733,51 @@ OO.ui.Window.prototype.getSize = function () {
        return this.size;
 };
 
+/**
+ * Disable transitions on window's frame for the duration of the callback function, then enable them
+ * back.
+ *
+ * @private
+ * @param {Function} callback Function to call while transitions are disabled
+ */
+OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
+       // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
+       // Disable transitions first, otherwise we'll get values from when the window was animating.
+       var oldTransition,
+               styleObj = this.$frame[0].style;
+       oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
+               styleObj.MozTransition || styleObj.WebkitTransition;
+       styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
+               styleObj.MozTransition = styleObj.WebkitTransition = 'none';
+       callback();
+       // Force reflow to make sure the style changes done inside callback really are not transitioned
+       this.$frame.height();
+       styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
+               styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
+};
+
 /**
  * Get the height of the dialog contents.
  *
  * @return {number} Content height
  */
 OO.ui.Window.prototype.getContentHeight = function () {
-       // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements
-       var bodyHeight, oldHeight = this.$frame[0].style.height;
-       this.$frame[0].style.height = '1px';
-       bodyHeight = this.getBodyHeight();
-       this.$frame[0].style.height = oldHeight;
+       var bodyHeight,
+               win = this,
+               bodyStyleObj = this.$body[0].style,
+               frameStyleObj = this.$frame[0].style;
+
+       // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
+       // Disable transitions first, otherwise we'll get values from when the window was animating.
+       this.withoutSizeTransitions( function () {
+               var oldHeight = frameStyleObj.height, oldPosition = bodyStyleObj.position;
+               frameStyleObj.height = '1px';
+               // Force body to resize to new width
+               bodyStyleObj.position = 'relative';
+               bodyHeight = win.getBodyHeight();
+               frameStyleObj.height = oldHeight;
+               bodyStyleObj.position = oldPosition;
+       } );
 
        return Math.round(
                // Add buffer for border
@@ -1840,7 +1879,7 @@ OO.ui.Window.prototype.getTeardownProcess = function () {
 /**
  * Toggle visibility of window.
  *
- * If the window is isolated and hasn't fully loaded yet, the visiblity property will be used
+ * If the window is isolated and hasn't fully loaded yet, the visibility property will be used
  * instead of display.
  *
  * @param {boolean} [show] Make window visible, omit to toggle visibility
@@ -1896,13 +1935,13 @@ OO.ui.Window.prototype.setManager = function ( manager ) {
        } else {
                this.$content = this.$( '<div>' );
                this.$document = $( this.getElementDocument() );
-               this.$content.addClass( 'oo-ui-window-content' );
+               this.$content.addClass( 'oo-ui-window-content' ).attr( 'tabIndex', 0 );
                this.$frame.append( this.$content );
        }
        this.toggle( false );
 
        // Figure out directionality:
-       this.dir = OO.ui.Element.getDir( this.$iframe || this.$content ) || 'ltr';
+       this.dir = OO.ui.Element.static.getDir( this.$iframe || this.$content ) || 'ltr';
 
        return this;
 };
@@ -1934,17 +1973,31 @@ OO.ui.Window.prototype.setSize = function ( size ) {
  * @chainable
  */
 OO.ui.Window.prototype.setDimensions = function ( dim ) {
-       // Apply width before height so height is not based on wrapping content using the wrong width
+       var height,
+               win = this,
+               styleObj = this.$frame[0].style;
+
+       // Calculate the height we need to set using the correct width
+       if ( dim.height === undefined ) {
+               this.withoutSizeTransitions( function () {
+                       var oldWidth = styleObj.width;
+                       win.$frame.css( 'width', dim.width || '' );
+                       height = win.getContentHeight();
+                       styleObj.width = oldWidth;
+               } );
+       } else {
+               height = dim.height;
+       }
+
        this.$frame.css( {
                width: dim.width || '',
                minWidth: dim.minWidth || '',
-               maxWidth: dim.maxWidth || ''
-       } );
-       this.$frame.css( {
-               height: ( dim.height !== undefined ? dim.height : this.getContentHeight() ) || '',
+               maxWidth: dim.maxWidth || '',
+               height: height || '',
                minHeight: dim.minHeight || '',
                maxHeight: dim.maxHeight || ''
        } );
+
        return this;
 };
 
@@ -2013,7 +2066,7 @@ OO.ui.Window.prototype.close = function ( data ) {
 /**
  * Setup window.
  *
- * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
+ * This is called by OO.ui.WindowManager during window opening, and should not be called directly
  * by other systems.
  *
  * @param {Object} [data] Window opening data
@@ -2038,7 +2091,7 @@ OO.ui.Window.prototype.setup = function ( data ) {
 /**
  * Ready window.
  *
- * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
+ * This is called by OO.ui.WindowManager during window opening, and should not be called directly
  * by other systems.
  *
  * @param {Object} [data] Window opening data
@@ -2062,7 +2115,7 @@ OO.ui.Window.prototype.ready = function ( data ) {
 /**
  * Hold window.
  *
- * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
+ * This is called by OO.ui.WindowManager during window closing, and should not be called directly
  * by other systems.
  *
  * @param {Object} [data] Window closing data
@@ -2074,7 +2127,7 @@ OO.ui.Window.prototype.hold = function ( data ) {
 
        this.getHoldProcess( data ).execute().done( function () {
                // Get the focused element within the window's content
-               var $focus = win.$content.find( OO.ui.Element.getDocument( win.$content ).activeElement );
+               var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
 
                // Blur the focused element
                if ( $focus.length ) {
@@ -2093,7 +2146,7 @@ OO.ui.Window.prototype.hold = function ( data ) {
 /**
  * Teardown window.
  *
- * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
+ * This is called by OO.ui.WindowManager during window closing, and should not be called directly
  * by other systems.
  *
  * @param {Object} [data] Window closing data
@@ -2189,7 +2242,7 @@ OO.ui.Window.prototype.load = function () {
        doc.close();
 
        // Properties
-       this.$ = OO.ui.Element.getJQuery( doc, this.$iframe );
+       this.$ = OO.ui.Element.static.getJQuery( doc, this.$iframe );
        this.$content = this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 );
        this.$document = this.$( doc );
 
@@ -2253,6 +2306,7 @@ OO.ui.Dialog = function OoUiDialog( config ) {
        this.actions = new OO.ui.ActionSet();
        this.attachedActions = [];
        this.currentAction = null;
+       this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
 
        // Events
        this.actions.connect( this, {
@@ -2323,7 +2377,8 @@ OO.ui.Dialog.static.escapable = true;
 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
        if ( e.which === OO.ui.Keys.ESCAPE ) {
                this.close();
-               return false;
+               e.preventDefault();
+               e.stopPropagation();
        }
 };
 
@@ -2416,6 +2471,10 @@ OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
                                );
                        }
                        this.actions.add( items );
+
+                       if ( this.constructor.static.escapable ) {
+                               this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
+                       }
                }, this );
 };
 
@@ -2426,6 +2485,10 @@ OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
        // Parent method
        return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
                .first( function () {
+                       if ( this.constructor.static.escapable ) {
+                               this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
+                       }
+
                        this.actions.clear();
                        this.currentAction = null;
                }, this );
@@ -2441,11 +2504,6 @@ OO.ui.Dialog.prototype.initialize = function () {
        // Properties
        this.title = new OO.ui.LabelWidget( { $: this.$ } );
 
-       // Events
-       if ( this.constructor.static.escapable ) {
-               this.$document.on( 'keydown', this.onDocumentKeyDown.bind( this ) );
-       }
-
        // Initialization
        this.$content.addClass( 'oo-ui-dialog-content' );
        this.setPendingElement( this.$head );
@@ -2672,10 +2730,9 @@ OO.ui.WindowManager.prototype.afterWindowResize = function () {
  *
  * @param {jQuery.Event} e Mouse wheel event
  */
-OO.ui.WindowManager.prototype.onWindowMouseWheel = function ( e ) {
-       // Kill all events in the parent window if the child window is isolated,
-       // or if the event didn't come from the child window
-       return !( this.shouldIsolate() || !$.contains( this.getCurrentWindow().$frame[0], e.target ) );
+OO.ui.WindowManager.prototype.onWindowMouseWheel = function () {
+       // Kill all events in the parent window if the child window is isolated
+       return !this.shouldIsolate();
 };
 
 /**
@@ -2693,9 +2750,8 @@ OO.ui.WindowManager.prototype.onDocumentKeyDown = function ( e ) {
                case OO.ui.Keys.UP:
                case OO.ui.Keys.RIGHT:
                case OO.ui.Keys.DOWN:
-                       // Kill all events in the parent window if the child window is isolated,
-                       // or if the event didn't come from the child window
-                       return !( this.shouldIsolate() || !$.contains( this.getCurrentWindow().$frame[0], e.target ) );
+                       // Kill all events in the parent window if the child window is isolated
+                       return !this.shouldIsolate();
        }
 };
 
@@ -3039,7 +3095,7 @@ OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
  * @throws {Error} If windows being removed are not being managed
  */
 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
-       var i, len, win, name,
+       var i, len, win, name, cleanupWindow,
                manager = this,
                promises = [],
                cleanup = function ( name, win ) {
@@ -3053,7 +3109,8 @@ OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
                if ( !win ) {
                        throw new Error( 'Cannot remove window' );
                }
-               promises.push( this.closeWindow( name ).then( cleanup.bind( null, name, win ) ) );
+               cleanupWindow = cleanup.bind( null, name, win );
+               promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
        }
 
        return $.when.apply( $, promises );
@@ -3083,7 +3140,7 @@ OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
                return;
        }
 
-       var viewport = OO.ui.Element.getDimensions( win.getElementWindow() ),
+       var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
                sizes = this.constructor.static.sizes,
                size = win.getSize();
 
@@ -3124,6 +3181,10 @@ OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
                                // Start listening for top-level window dimension changes
                                'orientationchange resize': this.onWindowResizeHandler
                        } );
+                       // Disable window scrolling in isolated windows
+                       if ( !this.shouldIsolate() ) {
+                               $( this.getElementDocument().body ).css( 'overflow', 'hidden' );
+                       }
                        this.globalEvents = true;
                }
        } else if ( this.globalEvents ) {
@@ -3138,6 +3199,9 @@ OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
                        // Stop listening for top-level window dimension changes
                        'orientationchange resize': this.onWindowResizeHandler
                } );
+               if ( !this.shouldIsolate() ) {
+                       $( this.getElementDocument().body ).css( 'overflow', '' );
+               }
                this.globalEvents = false;
        }
 
@@ -3162,7 +3226,7 @@ OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
                                .attr( 'aria-hidden', '' );
                }
        } else if ( this.$ariaHidden ) {
-               // Restore screen reader visiblity
+               // Restore screen reader visibility
                this.$ariaHidden.removeAttr( 'aria-hidden' );
                this.$ariaHidden = null;
        }
@@ -3422,7 +3486,15 @@ OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
 
 /* Methods */
 
-/** */
+/**
+ * Get tools from the factory
+ *
+ * @param {Array} include Included tools
+ * @param {Array} exclude Excluded tools
+ * @param {Array} promote Promoted tools
+ * @param {Array} demote Demoted tools
+ * @return {string[]} List of tools
+ */
 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
        var i, len, included, promoted, demoted,
                auto = [],
@@ -3592,7 +3664,7 @@ OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
 /**
  * Update CSS classes provided by the theme.
  *
- * For elements with theme logic hooks, this should be called anytime there's a state change.
+ * For elements with theme logic hooks, this should be called any time there's a state change.
  *
  * @param {OO.ui.Element} element Element for which to update classes
  * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
@@ -3672,7 +3744,7 @@ OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
                this.$button
                        .removeClass( 'oo-ui-buttonElement-button' )
                        .removeAttr( 'role accesskey tabindex' )
-                       .off( this.onMouseDownHandler );
+                       .off( 'mousedown', this.onMouseDownHandler );
        }
 
        this.$button = $button
@@ -3711,7 +3783,7 @@ OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
        if ( this.isDisabled() || e.which !== 1 ) {
                return false;
        }
-       // Restore the tab-index after the button is up to restore the button's accesssibility
+       // Restore the tab-index after the button is up to restore the button's accessibility
        this.$button.attr( 'tabindex', this.tabIndex );
        this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
        // Stop listening for mouseup, since we only needed this once
@@ -3814,7 +3886,7 @@ OO.ui.ButtonElement.prototype.setActive = function ( value ) {
  * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
  */
 OO.ui.GroupElement = function OoUiGroupElement( config ) {
-       // Configuration intialization
+       // Configuration initialization
        config = config || {};
 
        // Properties
@@ -3862,6 +3934,51 @@ OO.ui.GroupElement.prototype.getItems = function () {
        return this.items.slice( 0 );
 };
 
+/**
+ * Get an item by its data.
+ *
+ * Data is compared by a hash of its value. Only the first item with matching data will be returned.
+ *
+ * @param {Object} data Item data to search for
+ * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
+ */
+OO.ui.GroupElement.prototype.getItemFromData = function ( data ) {
+       var i, len, item,
+               hash = OO.getHash( data );
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               item = this.items[i];
+               if ( hash === OO.getHash( item.getData() ) ) {
+                       return item;
+               }
+       }
+
+       return null;
+};
+
+/**
+ * Get items by their data.
+ *
+ * Data is compared by a hash of its value. All items with matching data will be returned.
+ *
+ * @param {Object} data Item data to search for
+ * @return {OO.ui.Element[]} Items with equivalent data
+ */
+OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) {
+       var i, len, item,
+               hash = OO.getHash( data ),
+               items = [];
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               item = this.items[i];
+               if ( hash === OO.getHash( item.getData() ) ) {
+                       items.push( item );
+               }
+       }
+
+       return items;
+};
+
 /**
  * Add an aggregate item event.
  *
@@ -4032,6 +4149,371 @@ OO.ui.GroupElement.prototype.clearItems = function () {
        return this;
 };
 
+/**
+ * A mixin for an element that can be dragged and dropped.
+ * Use in conjunction with DragGroupWidget
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ */
+OO.ui.DraggableElement = function OoUiDraggableElement() {
+       // Properties
+       this.index = null;
+
+       // Initialize and events
+       this.$element
+               .attr( 'draggable', true )
+               .addClass( 'oo-ui-draggableElement' )
+               .on( {
+                       dragstart: this.onDragStart.bind( this ),
+                       dragover: this.onDragOver.bind( this ),
+                       dragend: this.onDragEnd.bind( this ),
+                       drop: this.onDrop.bind( this )
+               } );
+};
+
+/* Events */
+
+/**
+ * @event dragstart
+ * @param {OO.ui.DraggableElement} item Dragging item
+ */
+
+/**
+ * @event dragend
+ */
+
+/**
+ * @event drop
+ */
+
+/* Methods */
+
+/**
+ * Respond to dragstart event.
+ * @param {jQuery.Event} event jQuery event
+ * @fires dragstart
+ */
+OO.ui.DraggableElement.prototype.onDragStart = function ( e ) {
+       var dataTransfer = e.originalEvent.dataTransfer;
+       // Define drop effect
+       dataTransfer.dropEffect = 'none';
+       dataTransfer.effectAllowed = 'move';
+       // We must set up a dataTransfer data property or Firefox seems to
+       // ignore the fact the element is draggable.
+       try {
+               dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
+       } catch ( err ) {
+               // The above is only for firefox. No need to set a catch clause
+               // if it fails, move on.
+       }
+       // Add dragging class
+       this.$element.addClass( 'oo-ui-draggableElement-dragging' );
+       // Emit event
+       this.emit( 'dragstart', this );
+       return true;
+};
+
+/**
+ * Respond to dragend event.
+ * @fires dragend
+ */
+OO.ui.DraggableElement.prototype.onDragEnd = function () {
+       this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
+       this.emit( 'dragend' );
+};
+
+/**
+ * Handle drop event.
+ * @param {jQuery.Event} event jQuery event
+ * @fires drop
+ */
+OO.ui.DraggableElement.prototype.onDrop = function ( e ) {
+       e.preventDefault();
+       this.emit( 'drop', e );
+};
+
+/**
+ * In order for drag/drop to work, the dragover event must
+ * return false and stop propogation.
+ */
+OO.ui.DraggableElement.prototype.onDragOver = function ( e ) {
+       e.preventDefault();
+};
+
+/**
+ * Set item index.
+ * Store it in the DOM so we can access from the widget drag event
+ * @param {number} Item index
+ */
+OO.ui.DraggableElement.prototype.setIndex = function ( index ) {
+       if ( this.index !== index ) {
+               this.index = index;
+               this.$element.data( 'index', index );
+       }
+};
+
+/**
+ * Get item index
+ * @return {number} Item index
+ */
+OO.ui.DraggableElement.prototype.getIndex = function () {
+       return this.index;
+};
+
+/**
+ * Element containing a sequence of child elements that can be dragged
+ * and dropped.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
+ * @cfg {string} [orientation] Item orientation, 'horizontal' or 'vertical'. Defaults to 'vertical'
+ */
+OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.GroupElement.call( this, config );
+
+       // Properties
+       this.orientation = config.orientation || 'vertical';
+       this.dragItem = null;
+       this.itemDragOver = null;
+       this.itemKeys = {};
+       this.sideInsertion = '';
+
+       // Events
+       this.aggregate( {
+               dragstart: 'itemDragStart',
+               dragend: 'itemDragEnd',
+               drop: 'itemDrop'
+       } );
+       this.connect( this, {
+               itemDragStart: 'onItemDragStart',
+               itemDrop: 'onItemDrop',
+               itemDragEnd: 'onItemDragEnd'
+       } );
+       this.$element.on( {
+               dragover: $.proxy( this.onDragOver, this ),
+               dragleave: $.proxy( this.onDragLeave, this )
+       } );
+
+       // Initialize
+       if ( $.isArray( config.items ) ) {
+               this.addItems( config.items );
+       }
+       this.$placeholder = $( '<div>' )
+               .addClass( 'oo-ui-draggableGroupElement-placeholder' );
+       this.$element
+               .addClass( 'oo-ui-draggableGroupElement' )
+               .append( this.$status )
+               .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
+               .prepend( this.$placeholder );
+};
+
+/* Setup */
+OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
+
+/* Events */
+
+/**
+ * @event reorder
+ * @param {OO.ui.DraggableElement} item Reordered item
+ * @param {number} [newIndex] New index for the item
+ */
+
+/* Methods */
+
+/**
+ * Respond to item drag start event
+ * @param {OO.ui.DraggableElement} item Dragged item
+ */
+OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
+       var i, len;
+
+       // Map the index of each object
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               this.items[i].setIndex( i );
+       }
+
+       if ( this.orientation === 'horizontal' ) {
+               // Set the height of the indicator
+               this.$placeholder.css( {
+                       height: item.$element.outerHeight(),
+                       width: 2
+               } );
+       } else {
+               // Set the width of the indicator
+               this.$placeholder.css( {
+                       height: 2,
+                       width: item.$element.outerWidth()
+               } );
+       }
+       this.setDragItem( item );
+};
+
+/**
+ * Respond to item drag end event
+ */
+OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
+       this.unsetDragItem();
+       return false;
+};
+
+/**
+ * Handle drop event and switch the order of the items accordingly
+ * @param {OO.ui.DraggableElement} item Dropped item
+ * @fires reorder
+ */
+OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
+       var toIndex = item.getIndex();
+       // Check if the dropped item is from the current group
+       // TODO: Figure out a way to configure a list of legally droppable
+       // elements even if they are not yet in the list
+       if ( this.getDragItem() ) {
+               // If the insertion point is 'after', the insertion index
+               // is shifted to the right (or to the left in RTL, hence 'after')
+               if ( this.sideInsertion === 'after' ) {
+                       toIndex++;
+               }
+               // Emit change event
+               this.emit( 'reorder', this.getDragItem(), toIndex );
+       }
+       // Return false to prevent propogation
+       return false;
+};
+
+/**
+ * Handle dragleave event.
+ */
+OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
+       // This means the item was dragged outside the widget
+       this.$placeholder
+               .css( 'left', 0 )
+               .hide();
+};
+
+/**
+ * Respond to dragover event
+ * @param {jQuery.Event} event Event details
+ */
+OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
+       var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
+               itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
+               clientX = e.originalEvent.clientX,
+               clientY = e.originalEvent.clientY;
+
+       // Get the OptionWidget item we are dragging over
+       dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
+       $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
+       if ( $optionWidget[0] ) {
+               itemOffset = $optionWidget.offset();
+               itemBoundingRect = $optionWidget[0].getBoundingClientRect();
+               itemPosition = $optionWidget.position();
+               itemIndex = $optionWidget.data( 'index' );
+       }
+
+       if (
+               itemOffset &&
+               this.isDragging() &&
+               itemIndex !== this.getDragItem().getIndex()
+       ) {
+               if ( this.orientation === 'horizontal' ) {
+                       // Calculate where the mouse is relative to the item width
+                       itemSize = itemBoundingRect.width;
+                       itemMidpoint = itemBoundingRect.left + itemSize / 2;
+                       dragPosition = clientX;
+                       // Which side of the item we hover over will dictate
+                       // where the placeholder will appear, on the left or
+                       // on the right
+                       cssOutput = {
+                               left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
+                               top: itemPosition.top
+                       };
+               } else {
+                       // Calculate where the mouse is relative to the item height
+                       itemSize = itemBoundingRect.height;
+                       itemMidpoint = itemBoundingRect.top + itemSize / 2;
+                       dragPosition = clientY;
+                       // Which side of the item we hover over will dictate
+                       // where the placeholder will appear, on the top or
+                       // on the bottom
+                       cssOutput = {
+                               top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
+                               left: itemPosition.left
+                       };
+               }
+               // Store whether we are before or after an item to rearrange
+               // For horizontal layout, we need to account for RTL, as this is flipped
+               if (  this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
+                       this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
+               } else {
+                       this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
+               }
+               // Add drop indicator between objects
+               if ( this.sideInsertion ) {
+                       this.$placeholder
+                               .css( cssOutput )
+                               .show();
+               } else {
+                       this.$placeholder
+                               .css( {
+                                       left: 0,
+                                       top: 0
+                               } )
+                               .hide();
+               }
+       } else {
+               // This means the item was dragged outside the widget
+               this.$placeholder
+                       .css( 'left', 0 )
+                       .hide();
+       }
+       // Prevent default
+       e.preventDefault();
+};
+
+/**
+ * Set a dragged item
+ * @param {OO.ui.DraggableElement} item Dragged item
+ */
+OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
+       this.dragItem = item;
+};
+
+/**
+ * Unset the current dragged item
+ */
+OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
+       this.dragItem = null;
+       this.itemDragOver = null;
+       this.$placeholder.hide();
+       this.sideInsertion = '';
+};
+
+/**
+ * Get the current dragged item
+ * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
+ */
+OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
+       return this.dragItem;
+};
+
+/**
+ * Check if there's an item being dragged.
+ * @return {Boolean} Item is being dragged
+ */
+OO.ui.DraggableGroupElement.prototype.isDragging = function () {
+       return this.getDragItem() !== null;
+};
+
 /**
  * Element containing an icon.
  *
@@ -4052,7 +4534,7 @@ OO.ui.GroupElement.prototype.clearItems = function () {
  * @cfg {string} [iconTitle] Icon title text or a function that returns text
  */
 OO.ui.IconElement = function OoUiIconElement( config ) {
-       // Configuration intialization
+       // Configuration initialization
        config = config || {};
 
        // Properties
@@ -4190,6 +4672,15 @@ OO.ui.IconElement.prototype.getIcon = function () {
        return this.icon;
 };
 
+/**
+ * Get icon title.
+ *
+ * @return {string} Icon title text
+ */
+OO.ui.IconElement.prototype.getIconTitle = function () {
+       return this.iconTitle;
+};
+
 /**
  * Element containing an indicator.
  *
@@ -4474,7 +4965,6 @@ OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
        } else {
                this.$label.empty();
        }
-       this.$label.css( 'display', !label ? 'none' : '' );
 };
 
 /**
@@ -4522,7 +5012,8 @@ OO.ui.PopupElement.prototype.getPopup = function () {
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {string|string[]} [flags] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
+ * @cfg {string|string[]} [flags] Flags describing importance and functionality, e.g. 'primary',
+ *   'safe', 'progressive', 'destructive' or 'constructive'
  * @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element
  */
 OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
@@ -4846,10 +5337,10 @@ OO.ui.ClippableElement.prototype.toggleClipping = function ( 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
+                       // If the clippable container is the root, we have to listen to scroll events and check
                        // jQuery.scrollTop on the window because of browser inconsistencies
-                       this.$clippableScroller = this.$clippableContainer.is( 'body' ) ?
-                               this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) :
+                       this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
+                               this.$( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
                                this.$clippableContainer;
                        this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
                        this.$clippableWindow = this.$( this.getElementWindow() )
@@ -4940,17 +5431,23 @@ OO.ui.ClippableElement.prototype.clip = function () {
                return this;
        }
 
-       var buffer = 10,
+       var buffer = 7, // Chosen by fair dice roll
                cOffset = this.$clippable.offset(),
-               $container = this.$clippableContainer.is( 'body' ) ?
+               $container = this.$clippableContainer.is( 'html, body' ) ?
                        this.$clippableWindow : this.$clippableContainer,
                ccOffset = $container.offset() || { top: 0, left: 0 },
                ccHeight = $container.innerHeight() - buffer,
                ccWidth = $container.innerWidth() - buffer,
+               cHeight = this.$clippable.outerHeight() + buffer,
+               cWidth = this.$clippable.outerWidth() + buffer,
                scrollTop = this.$clippableScroller.scrollTop(),
                scrollLeft = this.$clippableScroller.scrollLeft(),
-               desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
-               desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
+               desiredWidth = cOffset.left < 0 ?
+                       cWidth + cOffset.left :
+                       ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
+               desiredHeight = cOffset.top < 0 ?
+                       cHeight + cOffset.top :
+                       ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
                naturalWidth = this.$clippable.prop( 'scrollWidth' ),
                naturalHeight = this.$clippable.prop( 'scrollHeight' ),
                clipWidth = desiredWidth < naturalWidth,
@@ -5007,7 +5504,6 @@ OO.ui.Tool = function OoUiTool( toolGroup, config ) {
        this.toolbar = this.toolGroup.getToolbar();
        this.active = false;
        this.$title = this.$( '<span>' );
-       this.$titleText = this.$( '<span>' );
        this.$accel = this.$( '<span>' );
        this.$link = this.$( '<a>' );
        this.title = null;
@@ -5016,14 +5512,18 @@ OO.ui.Tool = function OoUiTool( toolGroup, config ) {
        this.toolbar.connect( this, { updateState: 'onUpdateState' } );
 
        // Initialization
-       this.$titleText.addClass( 'oo-ui-tool-title-text' );
-       this.$accel.addClass( 'oo-ui-tool-accel' );
-       this.$title
-               .addClass( 'oo-ui-tool-title' )
-               .append( this.$titleText, this.$accel );
+       this.$title.addClass( 'oo-ui-tool-title' );
+       this.$accel
+               .addClass( 'oo-ui-tool-accel' )
+               .prop( {
+                       // This may need to be changed if the key names are ever localized,
+                       // but for now they are essentially written in English
+                       dir: 'ltr',
+                       lang: 'en'
+               } );
        this.$link
                .addClass( 'oo-ui-tool-link' )
-               .append( this.$icon, this.$title )
+               .append( this.$icon, this.$title, this.$accel )
                .prop( 'tabIndex', 0 )
                .attr( 'role', 'button' );
        this.$element
@@ -5211,7 +5711,7 @@ OO.ui.Tool.prototype.updateTitle = function () {
                accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
                tooltipParts = [];
 
-       this.$titleText.text( this.title );
+       this.$title.text( this.title );
        this.$accel.text( accel );
 
        if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
@@ -5277,12 +5777,12 @@ OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
 
        // Initialization
        this.$group.addClass( 'oo-ui-toolbar-tools' );
-       this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group );
        if ( config.actions ) {
-               this.$actions.addClass( 'oo-ui-toolbar-actions' );
-               this.$bar.append( this.$actions );
+               this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
        }
-       this.$bar.append( '<div style="clear:both"></div>' );
+       this.$bar
+               .addClass( 'oo-ui-toolbar-bar' )
+               .append( this.$group, '<div style="clear:both"></div>' );
        if ( config.shadow ) {
                this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
        }
@@ -5805,7 +6305,7 @@ OO.ui.MessageDialog.static.verbose = false;
  * Dialog title.
  *
  * A confirmation dialog's title should describe what the progressive action will do. An alert
- * dialog's title should describe what event occured.
+ * dialog's title should describe what event occurred.
  *
  * @static
  * inheritable
@@ -5815,7 +6315,7 @@ OO.ui.MessageDialog.static.title = null;
 
 /**
  * A confirmation dialog's message should describe the consequences of the progressive action. An
- * alert dialog's message should describe why the event occured.
+ * alert dialog's message should describe why the event occurred.
  *
  * @static
  * inheritable
@@ -5830,12 +6330,38 @@ OO.ui.MessageDialog.static.actions = [
 
 /* Methods */
 
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
+       OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
+
+       // Events
+       this.manager.connect( this, {
+               resize: 'onResize'
+       } );
+
+       return this;
+};
+
 /**
  * @inheritdoc
  */
 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
        this.fitActions();
-       return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
+       return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
+};
+
+/**
+ * Handle window resized events.
+ */
+OO.ui.MessageDialog.prototype.onResize = function () {
+       var dialog = this;
+       dialog.fitActions();
+       // Wait for CSS transition to finish and do it again :(
+       setTimeout( function () {
+               dialog.fitActions();
+       }, 300 );
 };
 
 /**
@@ -5902,7 +6428,45 @@ OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
  * @inheritdoc
  */
 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
-       return Math.round( this.text.$element.outerHeight( true ) );
+       var bodyHeight, oldOverflow,
+               $scrollable = this.container.$element;
+
+       oldOverflow = $scrollable[0].style.overflow;
+       $scrollable[0].style.overflow = 'hidden';
+
+       // Force… ugh… something to happen
+       $scrollable.contents().hide();
+       $scrollable.height();
+       $scrollable.contents().show();
+
+       bodyHeight = Math.round( this.text.$element.outerHeight( true ) );
+       $scrollable[0].style.overflow = oldOverflow;
+
+       return bodyHeight;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
+       var $scrollable = this.container.$element;
+       OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
+
+       // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
+       // Need to do it after transition completes (250ms), add 50ms just in case.
+       setTimeout( function () {
+               var oldOverflow = $scrollable[0].style.overflow;
+               $scrollable[0].style.overflow = 'hidden';
+
+               // Force… ugh… something to happen
+               $scrollable.contents().hide();
+               $scrollable.height();
+               $scrollable.contents().show();
+
+               $scrollable[0].style.overflow = oldOverflow;
+       }, 300 );
+
+       return this;
 };
 
 /**
@@ -5961,11 +6525,11 @@ OO.ui.MessageDialog.prototype.attachActions = function () {
                special.primary.toggleFramed( false );
        }
 
-       this.fitActions();
        if ( !this.isOpening() ) {
+               // If the dialog is currently opening, this will be called automatically soon.
+               // This also calls #fitActions.
                this.manager.updateWindowSize( this );
        }
-       this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
 };
 
 /**
@@ -5975,6 +6539,7 @@ OO.ui.MessageDialog.prototype.attachActions = function () {
  */
 OO.ui.MessageDialog.prototype.fitActions = function () {
        var i, len, action,
+               previous = this.verticalActionLayout,
                actions = this.actions.get();
 
        // Detect clipping
@@ -5986,6 +6551,12 @@ OO.ui.MessageDialog.prototype.fitActions = function () {
                        break;
                }
        }
+
+       if ( this.verticalActionLayout !== previous ) {
+               this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
+               // We changed the layout, window height might need to be updated.
+               this.manager.updateWindowSize( this );
+       }
 };
 
 /**
@@ -6161,7 +6732,7 @@ OO.ui.ProcessDialog.prototype.fitLabel = function () {
 };
 
 /**
- * Handle errors that occured durring accept or reject processes.
+ * Handle errors that occurred during accept or reject processes.
  *
  * @param {OO.ui.Error[]} errors Errors to be handled
  */
@@ -6239,7 +6810,7 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
        if ( this.outlined ) {
                this.editable = !!config.editable;
                this.outlineControlsWidget = null;
-               this.outlineWidget = new OO.ui.OutlineWidget( { $: this.$ } );
+               this.outlineSelectWidget = new OO.ui.OutlineSelectWidget( { $: this.$ } );
                this.outlinePanel = new OO.ui.PanelLayout( { $: this.$, scrollable: true } );
                this.gridLayout = new OO.ui.GridLayout(
                        [ this.outlinePanel, this.stackLayout ],
@@ -6248,7 +6819,7 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
                this.outlineVisible = true;
                if ( this.editable ) {
                        this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
-                               this.outlineWidget, { $: this.$ }
+                               this.outlineSelectWidget, { $: this.$ }
                        );
                }
        }
@@ -6256,11 +6827,11 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
        // Events
        this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
        if ( this.outlined ) {
-               this.outlineWidget.connect( this, { select: 'onOutlineWidgetSelect' } );
+               this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
        }
        if ( this.autoFocus ) {
                // Event 'focus' does not bubble, but 'focusin' does
-               this.stackLayout.onDOMEvent( 'focusin', this.onStackLayoutFocus.bind( this ) );
+               this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
        }
 
        // Initialization
@@ -6269,7 +6840,7 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
        if ( this.outlined ) {
                this.outlinePanel.$element
                        .addClass( 'oo-ui-bookletLayout-outlinePanel' )
-                       .append( this.outlineWidget.$element );
+                       .append( this.outlineSelectWidget.$element );
                if ( this.editable ) {
                        this.outlinePanel.$element
                                .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
@@ -6330,28 +6901,46 @@ OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
  * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
  */
 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
-       var $input, layout = this;
+       var layout = this;
        if ( page ) {
                page.scrollElementIntoView( { complete: function () {
                        if ( layout.autoFocus ) {
-                               // Set focus to the first input if nothing on the page is focused yet
-                               if ( !page.$element.find( ':focus' ).length ) {
-                                       $input = page.$element.find( ':input:first' );
-                                       if ( $input.length ) {
-                                               $input[0].focus();
-                                       }
-                               }
+                               layout.focus();
                        }
                } } );
        }
 };
 
+/**
+ * Focus the first input in the current page.
+ *
+ * If no page is selected, the first selectable page will be selected.
+ * If the focus is already in an element on the current page, nothing will happen.
+ */
+OO.ui.BookletLayout.prototype.focus = function () {
+       var $input, page = this.stackLayout.getCurrentItem();
+       if ( !page && this.outlined ) {
+               this.selectFirstSelectablePage();
+               page = this.stackLayout.getCurrentItem();
+               if ( !page ) {
+                       return;
+               }
+       }
+       // Only change the focus if is not already in the current page
+       if ( !page.$element.find( ':focus' ).length ) {
+               $input = page.$element.find( ':input:first' );
+               if ( $input.length ) {
+                       $input[0].focus();
+               }
+       }
+};
+
 /**
  * Handle outline widget select events.
  *
  * @param {OO.ui.OptionWidget|null} item Selected item
  */
-OO.ui.BookletLayout.prototype.onOutlineWidgetSelect = function ( item ) {
+OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
        if ( item ) {
                this.setPage( item.getData() );
        }
@@ -6416,16 +7005,16 @@ OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
                prev = pages[index - 1];
                // Prefer adjacent pages at the same level
                if ( this.outlined ) {
-                       level = this.outlineWidget.getItemFromData( page.getName() ).getLevel();
+                       level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
                        if (
                                prev &&
-                               level === this.outlineWidget.getItemFromData( prev.getName() ).getLevel()
+                               level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
                        ) {
                                return prev;
                        }
                        if (
                                next &&
-                               level === this.outlineWidget.getItemFromData( next.getName() ).getLevel()
+                               level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
                        ) {
                                return next;
                        }
@@ -6437,10 +7026,10 @@ OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
 /**
  * Get the outline widget.
  *
- * @return {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline
+ * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
  */
 OO.ui.BookletLayout.prototype.getOutline = function () {
-       return this.outlineWidget;
+       return this.outlineSelectWidget;
 };
 
 /**
@@ -6467,7 +7056,7 @@ OO.ui.BookletLayout.prototype.getPage = function ( name ) {
  *
  * @return {string|null} Current page name
  */
-OO.ui.BookletLayout.prototype.getPageName = function () {
+OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
        return this.currentPageName;
 };
 
@@ -6512,15 +7101,15 @@ OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
                name = page.getName();
                this.pages[page.getName()] = page;
                if ( this.outlined ) {
-                       item = new OO.ui.OutlineItemWidget( name, page, { $: this.$ } );
+                       item = new OO.ui.OutlineOptionWidget( { $: this.$, data: name } );
                        page.setOutlineItem( item );
                        items.push( item );
                }
        }
 
        if ( this.outlined && items.length ) {
-               this.outlineWidget.addItems( items, index );
-               this.updateOutlineWidget();
+               this.outlineSelectWidget.addItems( items, index );
+               this.selectFirstSelectablePage();
        }
        this.stackLayout.addItems( pages, index );
        this.emit( 'add', pages, index );
@@ -6543,13 +7132,13 @@ OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
                name = page.getName();
                delete this.pages[name];
                if ( this.outlined ) {
-                       items.push( this.outlineWidget.getItemFromData( name ) );
+                       items.push( this.outlineSelectWidget.getItemFromData( name ) );
                        page.setOutlineItem( null );
                }
        }
        if ( this.outlined && items.length ) {
-               this.outlineWidget.removeItems( items );
-               this.updateOutlineWidget();
+               this.outlineSelectWidget.removeItems( items );
+               this.selectFirstSelectablePage();
        }
        this.stackLayout.removeItems( pages );
        this.emit( 'remove', pages );
@@ -6570,7 +7159,7 @@ OO.ui.BookletLayout.prototype.clearPages = function () {
        this.pages = {};
        this.currentPageName = null;
        if ( this.outlined ) {
-               this.outlineWidget.clearItems();
+               this.outlineSelectWidget.clearItems();
                for ( i = 0, len = pages.length; i < len; i++ ) {
                        pages[i].setOutlineItem( null );
                }
@@ -6595,9 +7184,9 @@ OO.ui.BookletLayout.prototype.setPage = function ( name ) {
 
        if ( name !== this.currentPageName ) {
                if ( this.outlined ) {
-                       selectedItem = this.outlineWidget.getSelectedItem();
+                       selectedItem = this.outlineSelectWidget.getSelectedItem();
                        if ( selectedItem && selectedItem.getData() !== name ) {
-                               this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) );
+                               this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) );
                        }
                }
                if ( page ) {
@@ -6622,14 +7211,13 @@ OO.ui.BookletLayout.prototype.setPage = function ( name ) {
 };
 
 /**
- * Call this after adding or removing items from the OutlineWidget.
+ * Select the first selectable page.
  *
  * @chainable
  */
-OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
-       // Auto-select first item when nothing is selected anymore
-       if ( !this.outlineWidget.getSelectedItem() ) {
-               this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() );
+OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
+       if ( !this.outlineSelectWidget.getSelectedItem() ) {
+               this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
        }
 
        return this;
@@ -6659,9 +7247,14 @@ OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
  * @cfg {string} [help] Explanatory text shown as a '?' icon.
  */
 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
+       var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
+
        // Configuration initialization
        config = $.extend( { align: 'left' }, config );
 
+       // Properties (must be set before parent constructor, which calls #getTagName)
+       this.fieldWidget = fieldWidget;
+
        // Parent constructor
        OO.ui.FieldLayout.super.call( this, config );
 
@@ -6670,7 +7263,7 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
 
        // Properties
        this.$field = this.$( '<div>' );
-       this.fieldWidget = fieldWidget;
+       this.$body = this.$( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
        this.align = null;
        if ( config.help ) {
                this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
@@ -6691,17 +7284,21 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
        }
 
        // Events
-       if ( this.fieldWidget instanceof OO.ui.InputWidget ) {
+       if ( hasInputWidget ) {
                this.$label.on( 'click', this.onLabelClick.bind( this ) );
        }
        this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-fieldLayout' );
+       this.$element
+               .addClass( 'oo-ui-fieldLayout' )
+               .append( this.$help, this.$body );
+       this.$body.addClass( 'oo-ui-fieldLayout-body' );
        this.$field
                .addClass( 'oo-ui-fieldLayout-field' )
                .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
                .append( this.fieldWidget.$element );
+
        this.setAlignment( config.align );
 };
 
@@ -6710,10 +7307,6 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
 
-/* Static Properties */
-
-OO.ui.FieldLayout.static.tagName = 'label';
-
 /* Methods */
 
 /**
@@ -6759,9 +7352,9 @@ OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
                }
                // Reorder elements
                if ( value === 'inline' ) {
-                       this.$element.append( this.$field, this.$label, this.$help );
+                       this.$body.append( this.$field, this.$label );
                } else {
-                       this.$element.append( this.$help, this.$label, this.$field );
+                       this.$body.append( this.$label, this.$field );
                }
                // Set classes. The following classes can be used here:
                // * oo-ui-fieldLayout-align-left
@@ -6997,15 +7590,15 @@ OO.ui.GridLayout.prototype.update = function () {
                        width = this.widths[x];
                        panel = this.panels[i];
                        dimensions = {
-                               width: Math.round( width * 100 ) + '%',
-                               height: Math.round( height * 100 ) + '%',
-                               top: Math.round( top * 100 ) + '%'
+                               width: ( width * 100 ) + '%',
+                               height: ( height * 100 ) + '%',
+                               top: ( top * 100 ) + '%'
                        };
                        // If RTL, reverse:
-                       if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) {
-                               dimensions.right = Math.round( left * 100 ) + '%';
+                       if ( OO.ui.Element.static.getDir( this.$.context ) === 'rtl' ) {
+                               dimensions.right = ( left * 100 ) + '%';
                        } else {
-                               dimensions.left = Math.round( left * 100 ) + '%';
+                               dimensions.left = ( left * 100 ) + '%';
                        }
                        // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero
                        if ( width === 0 || height === 0 ) {
@@ -7031,7 +7624,7 @@ OO.ui.GridLayout.prototype.update = function () {
  *
  * @param {number} x Horizontal position
  * @param {number} y Vertical position
- * @return {OO.ui.PanelLayout} The panel at the given postion
+ * @return {OO.ui.PanelLayout} The panel at the given position
  */
 OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
        return this.panels[ ( x * this.widths.length ) + y ];
@@ -7086,7 +7679,6 @@ OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
  * @constructor
  * @param {string} name Unique symbolic name of page
  * @param {Object} [config] Configuration options
- * @param {string} [outlineItem] Outline item widget
  */
 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
        // Configuration initialization
@@ -7097,7 +7689,7 @@ OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
 
        // Properties
        this.name = name;
-       this.outlineItem = config.outlineItem || null;
+       this.outlineItem = null;
        this.active = false;
 
        // Initialization
@@ -7138,7 +7730,7 @@ OO.ui.PageLayout.prototype.isActive = function () {
 /**
  * Get outline item.
  *
- * @return {OO.ui.OutlineItemWidget|null} Outline item widget
+ * @return {OO.ui.OutlineOptionWidget|null} Outline item widget
  */
 OO.ui.PageLayout.prototype.getOutlineItem = function () {
        return this.outlineItem;
@@ -7150,9 +7742,9 @@ OO.ui.PageLayout.prototype.getOutlineItem = function () {
  * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
  *   outline item as desired; this method is called for setting (with an object) and unsetting
  *   (with null) and overriding methods would have to check the value of `outlineItem` to avoid
- *   operating on null instead of an OO.ui.OutlineItemWidget object.
+ *   operating on null instead of an OO.ui.OutlineOptionWidget object.
  *
- * @param {OO.ui.OutlineItemWidget|null} outlineItem Outline item widget, null to clear
+ * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear
  * @chainable
  */
 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
@@ -7168,7 +7760,7 @@ OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
  *
  * @localdoc Subclasses should override this method to adjust the outline item as desired.
  *
- * @param {OO.ui.OutlineItemWidget} outlineItem Outline item widget to setup
+ * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup
  * @chainable
  */
 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
@@ -7201,7 +7793,6 @@ OO.ui.PageLayout.prototype.setActive = function ( active ) {
  * @constructor
  * @param {Object} [config] Configuration options
  * @cfg {boolean} [continuous=false] Show all pages, one after another
- * @cfg {string} [icon=''] Symbolic icon name
  * @cfg {OO.ui.Layout[]} [items] Layouts to add
  */
 OO.ui.StackLayout = function OoUiStackLayout( config ) {
@@ -7581,7 +8172,7 @@ OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
  * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
  */
 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
-       // Configuration intialization
+       // Configuration initialization
        config = config || {};
 
        // Properties (must be set before parent constructor, which calls #populate)
@@ -7640,7 +8231,7 @@ OO.ui.ListToolGroup.prototype.populate = function () {
        // 'display' attribute and restores it, and the tool uses a <span> and can be hidden and re-shown.
        // Is this a jQuery bug? http://jsfiddle.net/gtj4hu3h/
        if ( this.getExpandCollapseTool().$element.css( 'display' ) === 'inline' ) {
-               this.getExpandCollapseTool().$element.css( 'display', 'inline-block' );
+               this.getExpandCollapseTool().$element.css( 'display', 'block' );
        }
 
        this.updateCollapsibleState();
@@ -7858,7 +8449,7 @@ OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
  * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
  *
  * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
- * allows bidrectional communication.
+ * allows bidirectional communication.
  *
  * Use together with OO.ui.GroupWidget to make disabled state inheritable.
  *
@@ -7927,8 +8518,8 @@ OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
        // Properties
        this.lookupInput = input;
        this.$overlay = config.$overlay || this.$element;
-       this.lookupMenu = new OO.ui.TextInputMenuWidget( this, {
-               $: OO.ui.Element.getJQuery( this.$overlay ),
+       this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
+               $: OO.ui.Element.static.getJQuery( this.$overlay ),
                input: this.lookupInput,
                $container: config.$container
        } );
@@ -8009,7 +8600,7 @@ OO.ui.LookupInputWidget.prototype.onLookupMenuToggle = function ( visible ) {
        if ( !visible ) {
                // When the menu is hidden, abort any active request and clear the menu.
                // This has to be done here in addition to closeLookupMenu(), because
-               // MenuWidget will close itself when the user presses Esc.
+               // MenuSelectWidget will close itself when the user presses Esc.
                this.abortLookupRequest();
                this.lookupMenu.clearItems();
        }
@@ -8018,7 +8609,7 @@ OO.ui.LookupInputWidget.prototype.onLookupMenuToggle = function ( visible ) {
 /**
  * Get lookup menu.
  *
- * @return {OO.ui.TextInputMenuWidget}
+ * @return {OO.ui.TextInputMenuSelectWidget}
  */
 OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
        return this.lookupMenu;
@@ -8195,7 +8786,7 @@ OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
  *
  * @abstract
  * @param {Mixed} data Cached result data, usually an array
- * @return {OO.ui.MenuItemWidget[]} Menu items
+ * @return {OO.ui.MenuOptionWidget[]} Menu items
  */
 OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
        // Stub, implemented in subclass
@@ -8215,7 +8806,7 @@ OO.ui.LookupInputWidget.prototype.getLookupCacheItemFromData = function () {
 };
 
 /**
- * Set of controls for an OO.ui.OutlineWidget.
+ * Set of controls for an OO.ui.OutlineSelectWidget.
  *
  * Controls include moving items up and down, removing items, and adding different kinds of items.
  *
@@ -8225,7 +8816,7 @@ OO.ui.LookupInputWidget.prototype.getLookupCacheItemFromData = function () {
  * @mixins OO.ui.IconElement
  *
  * @constructor
- * @param {OO.ui.OutlineWidget} outline Outline to control
+ * @param {OO.ui.OutlineSelectWidget} outline Outline to control
  * @param {Object} [config] Configuration options
  */
 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
@@ -8443,7 +9034,7 @@ OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
  */
 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
        // Configuration initialization
-       config = $.extend( { target: '_blank' }, config );
+       config = config || {};
 
        // Parent constructor
        OO.ui.ButtonWidget.super.call( this, config );
@@ -8854,7 +9445,7 @@ OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
  * Dropdown menus provide a control for accessing a menu and compose a menu within the widget, which
  * can be accessed using the #getMenu method.
  *
- * Use with OO.ui.MenuItemWidget.
+ * Use with OO.ui.MenuOptionWidget.
  *
  * @class
  * @extends OO.ui.Widget
@@ -8881,7 +9472,7 @@ OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
        OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
 
        // Properties
-       this.menu = new OO.ui.MenuWidget( $.extend( { $: this.$, widget: this }, config.menu ) );
+       this.menu = new OO.ui.MenuSelectWidget( $.extend( { $: this.$, widget: this }, config.menu ) );
        this.$handle = this.$( '<span>' );
 
        // Events
@@ -8910,7 +9501,7 @@ OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
 /**
  * Get the menu.
  *
- * @return {OO.ui.MenuWidget} Menu of widget
+ * @return {OO.ui.MenuSelectWidget} Menu of widget
  */
 OO.ui.DropdownWidget.prototype.getMenu = function () {
        return this.menu;
@@ -8919,7 +9510,7 @@ OO.ui.DropdownWidget.prototype.getMenu = function () {
 /**
  * Handles menu select events.
  *
- * @param {OO.ui.MenuItemWidget} item Selected menu item
+ * @param {OO.ui.MenuOptionWidget} item Selected menu item
  */
 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
        var selectedLabel;
@@ -9147,29 +9738,29 @@ OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
  * @chainable
  */
 OO.ui.InputWidget.prototype.setValue = function ( value ) {
-       value = this.sanitizeValue( value );
+       value = this.cleanUpValue( value );
+       // Update the DOM if it has changed. Note that with cleanUpValue, it
+       // is possible for the DOM value to change without this.value changing.
+       if ( this.$input.val() !== value ) {
+               this.$input.val( value );
+       }
        if ( this.value !== value ) {
                this.value = value;
                this.emit( 'change', this.value );
        }
-       // Update the DOM if it has changed. Note that with sanitizeValue, it
-       // is possible for the DOM value to change without this.value changing.
-       if ( this.$input.val() !== this.value ) {
-               this.$input.val( this.value );
-       }
        return this;
 };
 
 /**
- * Sanitize incoming value.
+ * Clean up incoming value.
  *
  * Ensures value is a string, and converts undefined and null to empty string.
  *
  * @private
  * @param {string} value Original value
- * @return {string} Sanitized value
+ * @return {string} Cleaned up value
  */
-OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) {
+OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
        if ( value === undefined || value === null ) {
                return '';
        } else if ( this.inputFilter ) {
@@ -9300,7 +9891,7 @@ OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
  * @return {jQuery} Input element
  */
 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
-       // Configuration intialization
+       // Configuration initialization
        config = config || {};
 
        var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
@@ -9385,6 +9976,7 @@ OO.ui.ButtonInputWidget.prototype.onKeyPress = function ( e ) {
  *
  * @constructor
  * @param {Object} [config] Configuration options
+ * @cfg {boolean} [selected=false] Whether the checkbox is initially selected
  */
 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
        // Parent constructor
@@ -9392,6 +9984,7 @@ OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
 
        // Initialization
        this.$element.addClass( 'oo-ui-checkboxInputWidget' );
+       this.setSelected( config.selected !== undefined ? config.selected : false );
 };
 
 /* Setup */
@@ -9411,39 +10004,107 @@ OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
 };
 
 /**
- * Get checked state of the checkbox
- *
- * @return {boolean} If the checkbox is checked
+ * @inheritdoc
  */
-OO.ui.CheckboxInputWidget.prototype.getValue = function () {
-       return this.value;
+OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
+       var widget = this;
+       if ( !this.isDisabled() ) {
+               // Allow the stack to clear so the value will be updated
+               setTimeout( function () {
+                       widget.setSelected( widget.$input.prop( 'checked' ) );
+               } );
+       }
 };
 
 /**
- * Set checked state of the checkbox
+ * Set selection state of this checkbox.
  *
- * @param {boolean} value New value
+ * @param {boolean} state Whether the checkbox is selected
+ * @chainable
  */
-OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) {
-       value = !!value;
-       if ( this.value !== value ) {
-               this.value = value;
-               this.$input.prop( 'checked', this.value );
-               this.emit( 'change', this.value );
+OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
+       state = !!state;
+       if ( this.selected !== state ) {
+               this.selected = state;
+               this.$input.prop( 'checked', this.selected );
+               this.emit( 'change', this.selected );
        }
+       return this;
+};
+
+/**
+ * Check if this checkbox is selected.
+ *
+ * @return {boolean} Checkbox is selected
+ */
+OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
+       return this.selected;
+};
+
+/**
+ * Radio input widget.
+ *
+ * Radio buttons only make sense as a set, and you probably want to use the OO.ui.RadioSelectWidget
+ * class instead of using this class directly.
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [selected=false] Whether the radio button is initially selected
+ */
+OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
+       // Parent constructor
+       OO.ui.RadioInputWidget.super.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-radioInputWidget' );
+       this.setSelected( config.selected !== undefined ? config.selected : false );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
+
+/* Methods */
+
+/**
+ * Get input element.
+ *
+ * @private
+ * @return {jQuery} Input element
+ */
+OO.ui.RadioInputWidget.prototype.getInputElement = function () {
+       return this.$( '<input type="radio" />' );
 };
 
 /**
  * @inheritdoc
  */
-OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
-       var widget = this;
-       if ( !this.isDisabled() ) {
-               // Allow the stack to clear so the value will be updated
-               setTimeout( function () {
-                       widget.setValue( widget.$input.prop( 'checked' ) );
-               } );
-       }
+OO.ui.RadioInputWidget.prototype.onEdit = function () {
+       // RadioInputWidget doesn't track its state.
+};
+
+/**
+ * Set selection state of this radio button.
+ *
+ * @param {boolean} state Whether the button is selected
+ * @chainable
+ */
+OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
+       // RadioInputWidget doesn't track its state.
+       this.$input.prop( 'checked', state );
+       return this;
+};
+
+/**
+ * Check if this radio button is selected.
+ *
+ * @return {boolean} Radio is selected
+ */
+OO.ui.RadioInputWidget.prototype.isSelected = function () {
+       return this.$input.prop( 'checked' );
 };
 
 /**
@@ -9485,6 +10146,14 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        this.maxRows = config.maxRows !== undefined ? config.maxRows : 10;
        this.validate = null;
 
+       // Clone for resizing
+       if ( this.autosize ) {
+               this.$clone = this.$input
+                       .clone()
+                       .insertAfter( this.$input )
+                       .hide();
+       }
+
        this.setValidation( config.validate );
 
        // Events
@@ -9581,7 +10250,7 @@ OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
  */
 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
        if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
-               this.emit( 'enter' );
+               this.emit( 'enter', e );
        }
 };
 
@@ -9628,7 +10297,7 @@ OO.ui.TextInputWidget.prototype.isReadOnly = function () {
 /**
  * Set the read-only state of the widget.
  *
- * This should probably change the widgets's appearance and prevent it from being used.
+ * This should probably change the widget's appearance and prevent it from being used.
  *
  * @param {boolean} state Make input read-only
  * @chainable
@@ -9647,27 +10316,40 @@ OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
  * @chainable
  */
 OO.ui.TextInputWidget.prototype.adjustSize = function () {
-       var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
+       var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
 
-       if ( this.multiline && this.autosize ) {
-               $clone = this.$input.clone()
+       if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
+               this.$clone
                        .val( this.$input.val() )
+                       .attr( 'rows', '' )
                        // Set inline height property to 0 to measure scroll height
-                       .css( { height: 0 } )
-                       .insertAfter( this.$input );
-               scrollHeight = $clone[0].scrollHeight;
+                       .css( 'height', 0 );
+
+               this.$clone[0].style.display = 'block';
+
+               this.valCache = this.$input.val();
+
+               scrollHeight = this.$clone[0].scrollHeight;
+
                // Remove inline height property to measure natural heights
-               $clone.css( 'height', '' );
-               innerHeight = $clone.innerHeight();
-               outerHeight = $clone.outerHeight();
+               this.$clone.css( 'height', '' );
+               innerHeight = this.$clone.innerHeight();
+               outerHeight = this.$clone.outerHeight();
+
                // Measure max rows height
-               $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' ).val( '' );
-               maxInnerHeight = $clone.innerHeight();
+               this.$clone
+                       .attr( 'rows', this.maxRows )
+                       .css( 'height', 'auto' )
+                       .val( '' );
+               maxInnerHeight = this.$clone.innerHeight();
+
                // Difference between reported innerHeight and scrollHeight with no scrollbars present
                // Equals 1 on Blink-based browsers and 0 everywhere else
-               measurementError = maxInnerHeight - $clone[0].scrollHeight;
-               $clone.remove();
+               measurementError = maxInnerHeight - this.$clone[0].scrollHeight;
                idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
+
+               this.$clone[0].style.display = 'none';
+
                // Only apply inline height when expansion beyond natural height is needed
                if ( idealHeight > innerHeight ) {
                        // Use the difference between the inner and outer height as a buffer
@@ -9781,9 +10463,9 @@ OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
                { $: this.$, indicator: 'down', disabled: this.isDisabled() },
                config.input
        ) );
-       this.menu = new OO.ui.TextInputMenuWidget( this.input, $.extend(
+       this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
                {
-                       $: OO.ui.Element.getJQuery( this.$overlay ),
+                       $: OO.ui.Element.static.getJQuery( this.$overlay ),
                        widget: this,
                        input: this.input,
                        disabled: this.isDisabled()
@@ -9815,6 +10497,14 @@ OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
 
 /* Methods */
 
+/**
+ * Get the combobox's menu.
+ * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
+ */
+OO.ui.ComboBoxWidget.prototype.getMenu = function () {
+       return this.menu;
+};
+
 /**
  * Handle input change events.
  *
@@ -9894,6 +10584,7 @@ OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
  *
  * @constructor
  * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.InputWidget} [input] Input widget this label is for
  */
 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
        // Configuration initialization
@@ -9949,10 +10640,9 @@ OO.ui.LabelWidget.prototype.onClick = function () {
  * @mixins OO.ui.FlaggedElement
  *
  * @constructor
- * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
  */
-OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
+OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
        // Configuration initialization
        config = config || {};
 
@@ -9965,7 +10655,6 @@ OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
        OO.ui.FlaggedElement.call( this, config );
 
        // Properties
-       this.data = data;
        this.selected = false;
        this.highlighted = false;
        this.pressed = false;
@@ -10129,15 +10818,6 @@ OO.ui.OptionWidget.prototype.flash = function () {
        return deferred.promise();
 };
 
-/**
- * Get option data.
- *
- * @return {Mixed} Option data
- */
-OO.ui.OptionWidget.prototype.getData = function () {
-       return this.data;
-};
-
 /**
  * Option widget with an option icon and indicator.
  *
@@ -10149,12 +10829,11 @@ OO.ui.OptionWidget.prototype.getData = function () {
  * @mixins OO.ui.IndicatorElement
  *
  * @constructor
- * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
  */
-OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( data, config ) {
+OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
        // Parent constructor
-       OO.ui.DecoratedOptionWidget.super.call( this, data, config );
+       OO.ui.DecoratedOptionWidget.super.call( this, config );
 
        // Mixin constructors
        OO.ui.IconElement.call( this, config );
@@ -10183,12 +10862,11 @@ OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
  * @mixins OO.ui.ButtonElement
  *
  * @constructor
- * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
  */
-OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
+OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
        // Parent constructor
-       OO.ui.ButtonOptionWidget.super.call( this, data, config );
+       OO.ui.ButtonOptionWidget.super.call( this, config );
 
        // Mixin constructors
        OO.ui.ButtonElement.call( this, config );
@@ -10225,78 +10903,122 @@ OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
 };
 
 /**
- * Item of an OO.ui.MenuWidget.
+ * Option widget that looks like a radio button.
+ *
+ * Use together with OO.ui.RadioSelectWidget.
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
+       // Parent constructor
+       OO.ui.RadioOptionWidget.super.call( this, config );
+
+       // Properties
+       this.radio = new OO.ui.RadioInputWidget( { value: config.data } );
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-radioOptionWidget' )
+               .prepend( this.radio.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
+
+/* Static Properties */
+
+OO.ui.RadioOptionWidget.static.highlightable = false;
+
+OO.ui.RadioOptionWidget.static.pressable = false;
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
+       OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
+
+       this.radio.setSelected( state );
+
+       return this;
+};
+
+/**
+ * Item of an OO.ui.MenuSelectWidget.
  *
  * @class
  * @extends OO.ui.DecoratedOptionWidget
  *
  * @constructor
- * @param {Mixed} data Item data
  * @param {Object} [config] Configuration options
  */
-OO.ui.MenuItemWidget = function OoUiMenuItemWidget( data, config ) {
+OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
        // Configuration initialization
        config = $.extend( { icon: 'check' }, config );
 
        // Parent constructor
-       OO.ui.MenuItemWidget.super.call( this, data, config );
+       OO.ui.MenuOptionWidget.super.call( this, config );
 
        // Initialization
        this.$element
                .attr( 'role', 'menuitem' )
-               .addClass( 'oo-ui-menuItemWidget' );
+               .addClass( 'oo-ui-menuOptionWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.DecoratedOptionWidget );
+OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
 
 /**
- * Section to group one or more items in a OO.ui.MenuWidget.
+ * Section to group one or more items in a OO.ui.MenuSelectWidget.
  *
  * @class
  * @extends OO.ui.DecoratedOptionWidget
  *
  * @constructor
- * @param {Mixed} data Item data
  * @param {Object} [config] Configuration options
  */
-OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) {
+OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
        // Parent constructor
-       OO.ui.MenuSectionItemWidget.super.call( this, data, config );
+       OO.ui.MenuSectionOptionWidget.super.call( this, config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-menuSectionItemWidget' );
+       this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.DecoratedOptionWidget );
+OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
 
 /* Static Properties */
 
-OO.ui.MenuSectionItemWidget.static.selectable = false;
+OO.ui.MenuSectionOptionWidget.static.selectable = false;
 
-OO.ui.MenuSectionItemWidget.static.highlightable = false;
+OO.ui.MenuSectionOptionWidget.static.highlightable = false;
 
 /**
- * Items for an OO.ui.OutlineWidget.
+ * Items for an OO.ui.OutlineSelectWidget.
  *
  * @class
  * @extends OO.ui.DecoratedOptionWidget
  *
  * @constructor
- * @param {Mixed} data Item data
  * @param {Object} [config] Configuration options
  * @cfg {number} [level] Indentation level
  * @cfg {boolean} [movable] Allow modification from outline controls
  */
-OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) {
+OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
        // Configuration initialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.OutlineItemWidget.super.call( this, data, config );
+       OO.ui.OutlineOptionWidget.super.call( this, config );
 
        // Properties
        this.level = 0;
@@ -10304,45 +11026,45 @@ OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) {
        this.removable = !!config.removable;
 
        // Initialization
-       this.$element.addClass( 'oo-ui-outlineItemWidget' );
+       this.$element.addClass( 'oo-ui-outlineOptionWidget' );
        this.setLevel( config.level );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.DecoratedOptionWidget );
+OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
 
 /* Static Properties */
 
-OO.ui.OutlineItemWidget.static.highlightable = false;
+OO.ui.OutlineOptionWidget.static.highlightable = false;
 
-OO.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true;
+OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
 
-OO.ui.OutlineItemWidget.static.levelClass = 'oo-ui-outlineItemWidget-level-';
+OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
 
-OO.ui.OutlineItemWidget.static.levels = 3;
+OO.ui.OutlineOptionWidget.static.levels = 3;
 
 /* Methods */
 
 /**
  * Check if item is movable.
  *
- * Movablilty is used by outline controls.
+ * Movability is used by outline controls.
  *
  * @return {boolean} Item is movable
  */
-OO.ui.OutlineItemWidget.prototype.isMovable = function () {
+OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
        return this.movable;
 };
 
 /**
  * Check if item is removable.
  *
- * Removablilty is used by outline controls.
+ * Removability is used by outline controls.
  *
  * @return {boolean} Item is removable
  */
-OO.ui.OutlineItemWidget.prototype.isRemovable = function () {
+OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
        return this.removable;
 };
 
@@ -10351,19 +11073,19 @@ OO.ui.OutlineItemWidget.prototype.isRemovable = function () {
  *
  * @return {number} Indentation level
  */
-OO.ui.OutlineItemWidget.prototype.getLevel = function () {
+OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
        return this.level;
 };
 
 /**
  * Set movability.
  *
- * Movablilty is used by outline controls.
+ * Movability is used by outline controls.
  *
  * @param {boolean} movable Item is movable
  * @chainable
  */
-OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
+OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
        this.movable = !!movable;
        this.updateThemeClasses();
        return this;
@@ -10372,12 +11094,12 @@ OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
 /**
  * Set removability.
  *
- * Removablilty is used by outline controls.
+ * Removability is used by outline controls.
  *
  * @param {boolean} movable Item is removable
  * @chainable
  */
-OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) {
+OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
        this.removable = !!removable;
        this.updateThemeClasses();
        return this;
@@ -10389,7 +11111,7 @@ OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) {
  * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
  * @chainable
  */
-OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) {
+OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
        var levels = this.constructor.static.levels,
                levelClass = this.constructor.static.levelClass,
                i = levels;
@@ -10777,7 +11499,7 @@ OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
  * @cfg {string} [value] Initial query value
  */
 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
-       // Configuration intialization
+       // Configuration initialization
        config = config || {};
 
        // Parent constructor
@@ -10920,8 +11642,8 @@ OO.ui.SearchWidget.prototype.getResults = function () {
 /**
  * Generic selection of options.
  *
- * Items can contain any rendering, and are uniquely identified by a hash of their data. Any widget
- * that provides options, from which the user must choose one, should be built on this class.
+ * Items can contain any rendering. Any widget that provides options, from which the user must
+ * choose one, should be built on this class.
  *
  * Use together with OO.ui.OptionWidget.
  *
@@ -10946,7 +11668,6 @@ OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        // Properties
        this.pressed = false;
        this.selecting = null;
-       this.hashes = {};
        this.onMouseUpHandler = this.onMouseUp.bind( this );
        this.onMouseMoveHandler = this.onMouseMove.bind( this );
 
@@ -11168,22 +11889,6 @@ OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
        return null;
 };
 
-/**
- * Get an existing item with equivilant data.
- *
- * @param {Object} data Item data to search for
- * @return {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists
- */
-OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
-       var hash = OO.getHash( data );
-
-       if ( Object.prototype.hasOwnProperty.call( this.hashes, hash ) ) {
-               return this.hashes[hash];
-       }
-
-       return null;
-};
-
 /**
  * Toggle pressed state.
  *
@@ -11298,32 +12003,30 @@ OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
 /**
  * Get an item relative to another one.
  *
- * @param {OO.ui.OptionWidget} item Item to start at
- * @param {number} direction Direction to move in, -1 to look backward, 1 to move forward
+ * @param {OO.ui.OptionWidget|null} item Item to start at, null to get relative to list start
+ * @param {number} direction Direction to move in, -1 to move backward, 1 to move forward
  * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
  */
 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
-       var inc = direction > 0 ? 1 : -1,
-               len = this.items.length,
-               index = item instanceof OO.ui.OptionWidget ?
-                       $.inArray( item, this.items ) : ( inc > 0 ? -1 : 0 ),
-               stopAt = Math.max( Math.min( index, len - 1 ), 0 ),
-               i = inc > 0 ?
-                       // Default to 0 instead of -1, if nothing is selected let's start at the beginning
-                       Math.max( index, -1 ) :
-                       // Default to n-1 instead of -1, if nothing is selected let's start at the end
-                       Math.min( index, len );
-
-       while ( len !== 0 ) {
-               i = ( i + inc + len ) % len;
-               item = this.items[i];
+       var currentIndex, nextIndex, i,
+               increase = direction > 0 ? 1 : -1,
+               len = this.items.length;
+
+       if ( item instanceof OO.ui.OptionWidget ) {
+               currentIndex = $.inArray( item, this.items );
+               nextIndex = ( currentIndex + increase + len ) % len;
+       } else {
+               // If no item is selected and moving forward, start at the beginning.
+               // If moving backward, start at the end.
+               nextIndex = direction > 0 ? 0 : len - 1;
+       }
+
+       for ( i = 0; i < len; i++ ) {
+               item = this.items[nextIndex];
                if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
                        return item;
                }
-               // Stop iterating when we've looped all the way around
-               if ( i === stopAt ) {
-                       break;
-               }
+               nextIndex = ( nextIndex + increase + len ) % len;
        }
        return null;
 };
@@ -11331,7 +12034,7 @@ OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direct
 /**
  * Get the next selectable item.
  *
- * @return {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
+ * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
  */
 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
        var i, len, item;
@@ -11349,31 +12052,12 @@ OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
 /**
  * Add items.
  *
- * When items are added with the same values as existing items, the existing items will be
- * automatically removed before the new items are added.
- *
  * @param {OO.ui.OptionWidget[]} items Items to add
  * @param {number} [index] Index to insert items after
  * @fires add
  * @chainable
  */
 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
-       var i, len, item, hash,
-               remove = [];
-
-       for ( i = 0, len = items.length; i < len; i++ ) {
-               item = items[i];
-               hash = OO.getHash( item.getData() );
-               if ( Object.prototype.hasOwnProperty.call( this.hashes, hash ) ) {
-                       // Remove item with same value
-                       remove.push( this.hashes[hash] );
-               }
-               this.hashes[hash] = item;
-       }
-       if ( remove.length ) {
-               this.removeItems( remove );
-       }
-
        // Mixin method
        OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
 
@@ -11393,15 +12077,11 @@ OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
  * @chainable
  */
 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
-       var i, len, item, hash;
+       var i, len, item;
 
+       // Deselect items being removed
        for ( i = 0, len = items.length; i < len; i++ ) {
                item = items[i];
-               hash = OO.getHash( item.getData() );
-               if ( Object.prototype.hasOwnProperty.call( this.hashes, hash ) ) {
-                       // Remove existing item
-                       delete this.hashes[hash];
-               }
                if ( item.isSelected() ) {
                        this.selectItem( null );
                }
@@ -11426,10 +12106,10 @@ OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
 OO.ui.SelectWidget.prototype.clearItems = function () {
        var items = this.items.slice();
 
-       // Clear all items
-       this.hashes = {};
        // Mixin method
        OO.ui.GroupWidget.prototype.clearItems.call( this );
+
+       // Clear selection
        this.selectItem( null );
 
        this.emit( 'remove', items );
@@ -11460,13 +12140,36 @@ OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
 
 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
 
+/**
+ * Select widget containing radio button options.
+ *
+ * Use together with OO.ui.RadioOptionWidget.
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
+       // Parent constructor
+       OO.ui.RadioSelectWidget.super.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-radioSelectWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
+
 /**
  * Overlaid menu of options.
  *
  * Menus are clipped to the visible viewport. They do not provide a control for opening or closing
  * the menu.
  *
- * Use together with OO.ui.MenuItemWidget.
+ * Use together with OO.ui.MenuOptionWidget.
  *
  * @class
  * @extends OO.ui.SelectWidget
@@ -11478,12 +12181,12 @@ OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
  * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
  * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
  */
-OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
+OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
        // Configuration initialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.MenuWidget.super.call( this, config );
+       OO.ui.MenuSelectWidget.super.call( this, config );
 
        // Mixin constructors
        OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
@@ -11504,13 +12207,13 @@ OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
        this.$element
                .hide()
                .attr( 'role', 'menu' )
-               .addClass( 'oo-ui-menuWidget' );
+               .addClass( 'oo-ui-menuSelectWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget );
-OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
+OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement );
 
 /* Methods */
 
@@ -11519,7 +12222,7 @@ OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
  *
  * @param {jQuery.Event} e Key down event
  */
-OO.ui.MenuWidget.prototype.onDocumentMouseDown = function ( e ) {
+OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
        if (
                !OO.ui.contains( this.$element[0], e.target, true ) &&
                ( !this.$widget || !OO.ui.contains( this.$widget[0], e.target, true ) )
@@ -11533,7 +12236,7 @@ OO.ui.MenuWidget.prototype.onDocumentMouseDown = function ( e ) {
  *
  * @param {jQuery.Event} e Key down event
  */
-OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
+OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
        var nextItem,
                handled = false,
                highlightItem = this.getHighlightedItem();
@@ -11580,7 +12283,7 @@ OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
 /**
  * Bind key down listener.
  */
-OO.ui.MenuWidget.prototype.bindKeyDownListener = function () {
+OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
        if ( this.$input ) {
                this.$input.on( 'keydown', this.onKeyDownHandler );
        } else {
@@ -11592,7 +12295,7 @@ OO.ui.MenuWidget.prototype.bindKeyDownListener = function () {
 /**
  * Unbind key down listener.
  */
-OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
+OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
        if ( this.$input ) {
                this.$input.off( 'keydown' );
        } else {
@@ -11608,11 +12311,11 @@ OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
  * @param {OO.ui.OptionWidget} item Item to choose
  * @chainable
  */
-OO.ui.MenuWidget.prototype.chooseItem = function ( item ) {
+OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
        var widget = this;
 
        // Parent method
-       OO.ui.MenuWidget.super.prototype.chooseItem.call( this, item );
+       OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
 
        if ( item && !this.flashing ) {
                this.flashing = true;
@@ -11630,11 +12333,11 @@ OO.ui.MenuWidget.prototype.chooseItem = function ( item ) {
 /**
  * @inheritdoc
  */
-OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
+OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
        var i, len, item;
 
        // Parent method
-       OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index );
+       OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
 
        // Auto-initialize
        if ( !this.newItems ) {
@@ -11660,9 +12363,9 @@ OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
 /**
  * @inheritdoc
  */
-OO.ui.MenuWidget.prototype.removeItems = function ( items ) {
+OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
        // Parent method
-       OO.ui.MenuWidget.super.prototype.removeItems.call( this, items );
+       OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
 
        // Reevaluate clipping
        this.clip();
@@ -11673,9 +12376,9 @@ OO.ui.MenuWidget.prototype.removeItems = function ( items ) {
 /**
  * @inheritdoc
  */
-OO.ui.MenuWidget.prototype.clearItems = function () {
+OO.ui.MenuSelectWidget.prototype.clearItems = function () {
        // Parent method
-       OO.ui.MenuWidget.super.prototype.clearItems.call( this );
+       OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
 
        // Reevaluate clipping
        this.clip();
@@ -11686,7 +12389,7 @@ OO.ui.MenuWidget.prototype.clearItems = function () {
 /**
  * @inheritdoc
  */
-OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
+OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
        visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
 
        var i, len,
@@ -11695,7 +12398,7 @@ OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
                widgetDoc = this.$widget ? this.$widget[0].ownerDocument : null;
 
        // Parent method
-       OO.ui.MenuWidget.super.prototype.toggle.call( this, visible );
+       OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
 
        if ( change ) {
                if ( visible ) {
@@ -11756,19 +12459,19 @@ OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
  * menu is toggled or the window is resized.
  *
  * @class
- * @extends OO.ui.MenuWidget
+ * @extends OO.ui.MenuSelectWidget
  *
  * @constructor
  * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
  * @param {Object} [config] Configuration options
  * @cfg {jQuery} [$container=input.$element] Element to render menu under
  */
-OO.ui.TextInputMenuWidget = function OoUiTextInputMenuWidget( input, config ) {
-       // Configuration intialization
+OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( input, config ) {
+       // Configuration initialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.TextInputMenuWidget.super.call( this, config );
+       OO.ui.TextInputMenuSelectWidget.super.call( this, config );
 
        // Properties
        this.input = input;
@@ -11776,12 +12479,12 @@ OO.ui.TextInputMenuWidget = function OoUiTextInputMenuWidget( input, config ) {
        this.onWindowResizeHandler = this.onWindowResize.bind( this );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-textInputMenuWidget' );
+       this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.TextInputMenuWidget, OO.ui.MenuWidget );
+OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
 
 /* Methods */
 
@@ -11790,14 +12493,14 @@ OO.inheritClass( OO.ui.TextInputMenuWidget, OO.ui.MenuWidget );
  *
  * @param {jQuery.Event} e Window resize event
  */
-OO.ui.TextInputMenuWidget.prototype.onWindowResize = function () {
+OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
        this.position();
 };
 
 /**
  * @inheritdoc
  */
-OO.ui.TextInputMenuWidget.prototype.toggle = function ( visible ) {
+OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
        visible = visible === undefined ? !this.isVisible() : !!visible;
 
        var change = visible !== this.isVisible();
@@ -11810,7 +12513,7 @@ OO.ui.TextInputMenuWidget.prototype.toggle = function ( visible ) {
        }
 
        // Parent method
-       OO.ui.TextInputMenuWidget.super.prototype.toggle.call( this, visible );
+       OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible );
 
        if ( change ) {
                if ( this.isVisible() ) {
@@ -11829,9 +12532,9 @@ OO.ui.TextInputMenuWidget.prototype.toggle = function ( visible ) {
  *
  * @chainable
  */
-OO.ui.TextInputMenuWidget.prototype.position = function () {
+OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
        var $container = this.$container,
-               pos = OO.ui.Element.getRelativePosition( $container, this.$element.offsetParent() );
+               pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
 
        // Position under input
        pos.top += $container.height();
@@ -11848,7 +12551,7 @@ OO.ui.TextInputMenuWidget.prototype.position = function () {
 /**
  * Structured list of items.
  *
- * Use with OO.ui.OutlineItemWidget.
+ * Use with OO.ui.OutlineOptionWidget.
  *
  * @class
  * @extends OO.ui.SelectWidget
@@ -11856,20 +12559,20 @@ OO.ui.TextInputMenuWidget.prototype.position = function () {
  * @constructor
  * @param {Object} [config] Configuration options
  */
-OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) {
+OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
        // Configuration initialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.OutlineWidget.super.call( this, config );
+       OO.ui.OutlineSelectWidget.super.call( this, config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-outlineWidget' );
+       this.$element.addClass( 'oo-ui-outlineSelectWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget );
+OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
 
 /**
  * Switch that slides on and off.