text: /*!
   * OOUI v0.43.1
   * https://www.mediawiki.org/wiki/OOUI
   *
   * Copyright 2011–2022 OOUI Team and other contributors.
   * Released under the MIT license
   * http://oojs.mit-license.org
   *
   * Date: 2022-02-10T15:03:45Z
   */
  ( function ( OO ) {
  
  'use strict';
  
  /**
   * DraggableElement is a mixin class used to create elements that can be clicked
   * and dragged by a mouse to a new position within a group. This class must be used
   * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
   * the draggable elements.
   *
   * @abstract
   * @class
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {jQuery} [$handle] The part of the element which can be used for dragging, defaults to
   *  the whole element
   * @cfg {boolean} [draggable=true] The items are draggable. This can change with #toggleDraggable
   *  but the draggable state should be called from the DraggableGroupElement, which updates
   *  the whole group
   */
  OO.ui.mixin.DraggableElement = function OoUiMixinDraggableElement( config ) {
  	config = config || {};
  
  	// Properties
  	this.index = null;
  	this.$handle = config.$handle || this.$element;
  	this.wasHandleUsed = null;
  
  	// Initialize and events
  	this.$element
  		.addClass( 'oo-ui-draggableElement' )
  		.on( {
  			mousedown: this.onDragMouseDown.bind( this ),
  			dragstart: this.onDragStart.bind( this ),
  			dragover: this.onDragOver.bind( this ),
  			dragend: this.onDragEnd.bind( this ),
  			drop: this.onDrop.bind( this )
  		} );
  	this.$handle.addClass( 'oo-ui-draggableElement-handle' );
  	this.toggleDraggable( config.draggable === undefined ? true : !!config.draggable );
  };
  
  OO.initClass( OO.ui.mixin.DraggableElement );
  
  /* Events */
  
  /**
   * @event dragstart
   *
   * A dragstart event is emitted when the user clicks and begins dragging an item.
   * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with
   *  the mouse.
   */
  
  /**
   * @event dragend
   * A dragend event is emitted when the user drags an item and releases the mouse,
   * thus terminating the drag operation.
   */
  
  /**
   * @event drop
   * A drop event is emitted when the user drags an item and then releases the mouse button
   * over a valid target.
   */
  
  /* Static Properties */
  
  /**
   * @inheritdoc OO.ui.mixin.ButtonElement
   */
  OO.ui.mixin.DraggableElement.static.cancelButtonMouseDownEvents = false;
  
  /* Methods */
  
  /**
   * Change the draggable state of this widget.
   * This allows users to temporarily halt the dragging operations.
   *
   * @param {boolean} isDraggable Widget supports draggable operations
   * @fires draggable
   */
  OO.ui.mixin.DraggableElement.prototype.toggleDraggable = function ( isDraggable ) {
  	isDraggable = isDraggable !== undefined ? !!isDraggable : !this.draggable;
  
  	if ( this.draggable !== isDraggable ) {
  		this.draggable = isDraggable;
  
  		this.$handle.toggleClass( 'oo-ui-draggableElement-undraggable', !this.draggable );
  
  		// We make the entire element draggable, not just the handle, so that
  		// the whole element appears to move. wasHandleUsed prevents drags from
  		// starting outside the handle
  		this.$element.prop( 'draggable', this.draggable );
  	}
  };
  
  /**
   * Check the draggable state of this widget.
   *
   * @return {boolean} Widget supports draggable operations
   */
  OO.ui.mixin.DraggableElement.prototype.isDraggable = function () {
  	return this.draggable;
  };
  
  /**
   * Respond to mousedown event.
   *
   * @private
   * @param {jQuery.Event} e Drag event
   */
  OO.ui.mixin.DraggableElement.prototype.onDragMouseDown = function ( e ) {
  	if ( !this.isDraggable() ) {
  		return;
  	}
  
  	this.wasHandleUsed =
  		// Optimization: if the handle is the whole element this is always true
  		this.$handle[ 0 ] === this.$element[ 0 ] ||
  		// Check the mousedown occurred inside the handle
  		OO.ui.contains( this.$handle[ 0 ], e.target, true );
  };
  
  /**
   * Respond to dragstart event.
   *
   * @private
   * @param {jQuery.Event} e Drag event
   * @return {boolean} False if the event is cancelled
   * @fires dragstart
   */
  OO.ui.mixin.DraggableElement.prototype.onDragStart = function ( e ) {
  	var element = this,
  		dataTransfer = e.originalEvent.dataTransfer;
  
  	if ( !this.wasHandleUsed || !this.isDraggable() ) {
  		return false;
  	}
  
  	// Define drop effect
  	dataTransfer.dropEffect = 'none';
  	dataTransfer.effectAllowed = 'move';
  	// Support: Firefox
  	// We must set up a dataTransfer data property or Firefox seems to
  	// ignore the fact the element is draggable.
  	try {
  		dataTransfer.setData( 'application-x/OOUI-draggable', this.getIndex() );
  	} catch ( err ) {
  		// The above is only for Firefox. Move on if it fails.
  	}
  	// Briefly add a 'clone' class to style the browser's native drag image
  	this.$element.addClass( 'oo-ui-draggableElement-clone' );
  	// Add placeholder class after the browser has rendered the clone
  	setTimeout( function () {
  		element.$element
  			.removeClass( 'oo-ui-draggableElement-clone' )
  			.addClass( 'oo-ui-draggableElement-placeholder' );
  	} );
  	// Emit event
  	this.emit( 'dragstart', this );
  	return true;
  };
  
  /**
   * Respond to dragend event.
   *
   * @private
   * @fires dragend
   */
  OO.ui.mixin.DraggableElement.prototype.onDragEnd = function () {
  	this.$element.removeClass( 'oo-ui-draggableElement-placeholder' );
  	this.emit( 'dragend' );
  };
  
  /**
   * Handle drop event.
   *
   * @private
   * @param {jQuery.Event} e Drop event
   * @fires drop
   */
  OO.ui.mixin.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.
   *
   * @param {jQuery.Event} e Drag event
   * @private
   */
  OO.ui.mixin.DraggableElement.prototype.onDragOver = function ( e ) {
  	e.preventDefault();
  };
  
  /**
   * Set item index.
   * Store it in the DOM so we can access from the widget drag event.
   *
   * @private
   * @param {number} index Item index
   */
  OO.ui.mixin.DraggableElement.prototype.setIndex = function ( index ) {
  	if ( this.index !== index ) {
  		this.index = index;
  		this.$element.data( 'index', index );
  	}
  };
  
  /**
   * Get item index.
   *
   * @private
   * @return {number} Item index
   */
  OO.ui.mixin.DraggableElement.prototype.getIndex = function () {
  	return this.index;
  };
  
  /**
   * DraggableGroupElement is a mixin class used to create a group element to
   * contain draggable elements, which are items that can be clicked and dragged by a mouse.
   * The class is used with OO.ui.mixin.DraggableElement.
   *
   * @abstract
   * @class
   * @mixins OO.ui.mixin.GroupElement
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {OO.ui.mixin.DraggableElement[]} items
   * @cfg {string} [orientation='vertical'] Item orientation: 'horizontal' or 'vertical'. The orientation
   *  should match the layout of the items. Items displayed in a single row
   *  or in several rows should use horizontal orientation. The vertical orientation should only be
   *  used when the items are displayed in a single column.
   * @cfg {boolean} [draggable=true] The items are draggable. This can change with #toggleDraggable
   */
  OO.ui.mixin.DraggableGroupElement = function OoUiMixinDraggableGroupElement( config ) {
  	// Configuration initialization
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.mixin.GroupElement.call( this, config );
  
  	// Properties
  	this.orientation = config.orientation || 'vertical';
  	this.dragItem = null;
  	this.itemKeys = {};
  	this.dir = null;
  	this.itemsOrder = null;
  	this.draggable = config.draggable === undefined ? true : !!config.draggable;
  
  	// Events
  	this.aggregate( {
  		dragstart: 'itemDragStart',
  		dragend: 'itemDragEnd',
  		drop: 'itemDrop'
  	} );
  	this.connect( this, {
  		itemDragStart: 'onItemDragStart',
  		itemDrop: 'onItemDropOrDragEnd',
  		itemDragEnd: 'onItemDropOrDragEnd'
  	} );
  
  	// Initialize
  	this.addItems( config.items || [] );
  	this.$element
  		.addClass( 'oo-ui-draggableGroupElement' )
  		.toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' );
  };
  
  /* Setup */
  OO.mixinClass( OO.ui.mixin.DraggableGroupElement, OO.ui.mixin.GroupElement );
  
  /* Events */
  
  /**
   * An item has been dragged to a new position, but not yet dropped.
   *
   * @event drag
   * @param {OO.ui.mixin.DraggableElement} item Dragged item
   * @param {number} [newIndex] New index for the item
   */
  
  /**
   * An item has been dropped at a new position.
   *
   * @event reorder
   * @param {OO.ui.mixin.DraggableElement} item Reordered item
   * @param {number} [newIndex] New index for the item
   */
  
  /**
   * Draggable state of this widget has changed.
   *
   * @event draggable
   * @param {boolean} [draggable] Widget is draggable
   */
  
  /* Methods */
  
  /**
   * Change the draggable state of this widget.
   * This allows users to temporarily halt the dragging operations.
   *
   * @param {boolean} isDraggable Widget supports draggable operations
   * @fires draggable
   */
  OO.ui.mixin.DraggableGroupElement.prototype.toggleDraggable = function ( isDraggable ) {
  	isDraggable = isDraggable !== undefined ? !!isDraggable : !this.draggable;
  
  	if ( this.draggable !== isDraggable ) {
  		this.draggable = isDraggable;
  
  		// Tell the items their draggable state changed
  		this.getItems().forEach( function ( item ) {
  			item.toggleDraggable( this.draggable );
  		}.bind( this ) );
  
  		// Emit event
  		this.emit( 'draggable', this.draggable );
  	}
  };
  
  /**
   * Check the draggable state of this widget
   *
   * @return {boolean} Widget supports draggable operations
   */
  OO.ui.mixin.DraggableGroupElement.prototype.isDraggable = function () {
  	return this.draggable;
  };
  
  /**
   * Respond to item drag start event
   *
   * @private
   * @param {OO.ui.mixin.DraggableElement} item Dragged item
   */
  OO.ui.mixin.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
  	if ( !this.isDraggable() ) {
  		return;
  	}
  	// Make a shallow copy of this.items so we can re-order it during previews
  	// without affecting the original array.
  	this.itemsOrder = this.items.slice();
  	this.updateIndexes();
  	if ( this.orientation === 'horizontal' ) {
  		// Calculate and cache directionality on drag start - it's a little
  		// expensive and it shouldn't change while dragging.
  		this.dir = this.$element.css( 'direction' );
  	}
  	this.setDragItem( item );
  };
  
  /**
   * Update the index properties of the items
   */
  OO.ui.mixin.DraggableGroupElement.prototype.updateIndexes = function () {
  	var i, len;
  
  	// Map the index of each object
  	for ( i = 0, len = this.itemsOrder.length; i < len; i++ ) {
  		this.itemsOrder[ i ].setIndex( i );
  	}
  };
  
  /**
   * Handle drop or dragend event and switch the order of the items accordingly
   *
   * @private
   * @param {OO.ui.mixin.DraggableElement} item Dropped item
   * @return {OO.ui.Element} The element, for chaining
   */
  OO.ui.mixin.DraggableGroupElement.prototype.onItemDropOrDragEnd = function () {
  	var targetIndex, originalIndex,
  		item = this.getDragItem();
  
  	// TODO: Figure out a way to configure a list of legally droppable
  	// elements even if they are not yet in the list
  	if ( item ) {
  		originalIndex = this.items.indexOf( item );
  		// If the item has moved forward, add one to the index to account for the left shift
  		targetIndex = item.getIndex() + ( item.getIndex() > originalIndex ? 1 : 0 );
  		if ( targetIndex !== originalIndex ) {
  			this.reorder( this.getDragItem(), targetIndex );
  			this.emit( 'reorder', this.getDragItem(), targetIndex );
  		}
  		this.updateIndexes();
  	}
  	this.unsetDragItem();
  	// Return false to prevent propogation
  	return false;
  };
  
  /**
   * Respond to dragover event
   *
   * @private
   * @param {jQuery.Event} e Dragover event
   * @fires reorder
   */
  OO.ui.mixin.DraggableGroupElement.prototype.onDragOver = function ( e ) {
  	var overIndex, targetIndex,
  		item = this.getDragItem(),
  		dragItemIndex = item.getIndex();
  
  	// Get the OptionWidget item we are dragging over
  	overIndex = $( e.target ).closest( '.oo-ui-draggableElement' ).data( 'index' );
  
  	if ( overIndex !== undefined && overIndex !== dragItemIndex ) {
  		targetIndex = overIndex + ( overIndex > dragItemIndex ? 1 : 0 );
  
  		if ( targetIndex > 0 ) {
  			this.$group.children().eq( targetIndex - 1 ).after( item.$element );
  		} else {
  			this.$group.prepend( item.$element );
  		}
  		// Move item in itemsOrder array
  		this.itemsOrder.splice( overIndex, 0,
  			this.itemsOrder.splice( dragItemIndex, 1 )[ 0 ]
  		);
  		this.updateIndexes();
  		this.emit( 'drag', item, targetIndex );
  	}
  	// Prevent default
  	e.preventDefault();
  };
  
  /**
   * Reorder the items in the group
   *
   * @param {OO.ui.mixin.DraggableElement} item Reordered item
   * @param {number} newIndex New index
   */
  OO.ui.mixin.DraggableGroupElement.prototype.reorder = function ( item, newIndex ) {
  	this.addItems( [ item ], newIndex );
  };
  
  /**
   * Set a dragged item
   *
   * @param {OO.ui.mixin.DraggableElement} item Dragged item
   */
  OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
  	if ( this.dragItem !== item ) {
  		this.dragItem = item;
  		this.$element.on( 'dragover', this.onDragOver.bind( this ) );
  		this.$element.addClass( 'oo-ui-draggableGroupElement-dragging' );
  	}
  };
  
  /**
   * Unset the current dragged item
   */
  OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
  	if ( this.dragItem ) {
  		this.dragItem = null;
  		this.$element.off( 'dragover' );
  		this.$element.removeClass( 'oo-ui-draggableGroupElement-dragging' );
  	}
  };
  
  /**
   * Get the item that is currently being dragged.
   *
   * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is
   *  being dragged
   */
  OO.ui.mixin.DraggableGroupElement.prototype.getDragItem = function () {
  	return this.dragItem;
  };
  
  /**
   * RequestManager is a mixin that manages the lifecycle of a promise-backed request for a widget,
   * such as the {@link OO.ui.mixin.LookupElement}.
   *
   * @class
   * @abstract
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {boolean} [showPendingRequest=true] Show pending state while request data is being fetched.
   *  Requires widget to have also mixed in {@link OO.ui.mixin.PendingElement}.
   */
  OO.ui.mixin.RequestManager = function OoUiMixinRequestManager( config ) {
  	this.requestCache = {};
  	this.requestQuery = null;
  	this.requestRequest = null;
  	this.showPendingRequest = !!this.pushPending && config.showPendingRequest !== false;
  };
  
  /* Setup */
  
  OO.initClass( OO.ui.mixin.RequestManager );
  
  /**
   * Get request results for the current query.
   *
   * @return {jQuery.Promise} Promise object which will be passed response data as the first argument
   *  of the done event. If the request was aborted to make way for a subsequent request, this
   *  promise may not be rejected, depending on what jQuery feels like doing.
   */
  OO.ui.mixin.RequestManager.prototype.getRequestData = function () {
  	var widget = this,
  		value = this.getRequestQuery(),
  		deferred = $.Deferred(),
  		ourRequest;
  
  	this.abortRequest();
  	if ( Object.prototype.hasOwnProperty.call( this.requestCache, value ) ) {
  		deferred.resolve( this.requestCache[ value ] );
  	} else {
  		if ( this.showPendingRequest ) {
  			this.pushPending();
  		}
  		this.requestQuery = value;
  		ourRequest = this.requestRequest = this.getRequest();
  		ourRequest
  			.always( function () {
  				// We need to pop pending even if this is an old request, otherwise
  				// the widget will remain pending forever.
  				// TODO: this assumes that an aborted request will fail or succeed soon after
  				// being aborted, or at least eventually. It would be nice if we could popPending()
  				// at abort time, but only if we knew that we hadn't already called popPending()
  				// for that request.
  				if ( widget.showPendingRequest ) {
  					widget.popPending();
  				}
  			} )
  			.done( function ( response ) {
  				// If this is an old request (and aborting it somehow caused it to still succeed),
  				// ignore its success completely
  				if ( ourRequest === widget.requestRequest ) {
  					widget.requestQuery = null;
  					widget.requestRequest = null;
  					widget.requestCache[ value ] =
  						widget.getRequestCacheDataFromResponse( response );
  					deferred.resolve( widget.requestCache[ value ] );
  				}
  			} )
  			.fail( function () {
  				// If this is an old request (or a request failing because it's being aborted),
  				// ignore its failure completely
  				if ( ourRequest === widget.requestRequest ) {
  					widget.requestQuery = null;
  					widget.requestRequest = null;
  					deferred.reject();
  				}
  			} );
  	}
  	return deferred.promise();
  };
  
  /**
   * Abort the currently pending request, if any.
   *
   * @private
   */
  OO.ui.mixin.RequestManager.prototype.abortRequest = function () {
  	var oldRequest = this.requestRequest;
  	if ( oldRequest ) {
  		// First unset this.requestRequest to the fail handler will notice
  		// that the request is no longer current
  		this.requestRequest = null;
  		this.requestQuery = null;
  		oldRequest.abort();
  	}
  };
  
  /**
   * Get the query to be made.
   *
   * @protected
   * @method
   * @abstract
   * @return {string} query to be used
   */
  OO.ui.mixin.RequestManager.prototype.getRequestQuery = null;
  
  /**
   * Get a new request object of the current query value.
   *
   * @protected
   * @method
   * @abstract
   * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
   */
  OO.ui.mixin.RequestManager.prototype.getRequest = null;
  
  /**
   * Pre-process data returned by the request from #getRequest.
   *
   * The return value of this function will be cached, and any further queries for the given value
   * will use the cache rather than doing API requests.
   *
   * @protected
   * @method
   * @abstract
   * @param {Mixed} response Response from server
   * @return {Mixed} Cached result data
   */
  OO.ui.mixin.RequestManager.prototype.getRequestCacheDataFromResponse = null;
  
  /**
   * LookupElement is a mixin that creates a {@link OO.ui.MenuSelectWidget menu} of suggested
   * values for a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on
   * the characters the user types into the text input field and, in general, the menu is only
   * displayed when the user types. If a suggested value is chosen from the lookup menu, that value
   * becomes the value of the input field.
   *
   * Note that a new menu of suggested items is displayed when a value is chosen from the
   * lookup menu. If this is not the desired behavior, disable lookup menus with the
   * #setLookupsDisabled method, then set the value, then re-enable lookups.
   *
   * See the [OOUI demos][1] for an example.
   *
   * [1]: https://doc.wikimedia.org/oojs-ui/master/demos/#LookupElement-try-inputting-an-integer
   *
   * @class
   * @abstract
   * @mixins OO.ui.mixin.RequestManager
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning.
   *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
   * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered
   *  beneath the specified element.
   * @cfg {Object} [menu] Configuration options to pass to
   *  {@link OO.ui.MenuSelectWidget menu select widget}
   * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the
   *  text input is empty.
   *  By default, the lookup menu is not generated and displayed until the user begins to type.
   * @cfg {boolean} [highlightFirst=true] Whether the first lookup result should be highlighted
   *  (so, that the user can take it over into the input with simply pressing return) automatically
   *  or not.
   * @cfg {boolean} [showSuggestionsOnFocus=true] Show suggestions when focusing the input. If this
   *  is set to false, suggestions will still be shown on a mousedown triggered focus. This matches
   *  browser autocomplete behavior.
   */
  OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
  	// Configuration initialization
  	config = $.extend( { highlightFirst: true }, config );
  
  	// Mixin constructors
  	OO.ui.mixin.RequestManager.call( this, config );
  
  	// Properties
  	this.$overlay = ( config.$overlay === true ?
  		OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
  	this.lookupMenu = new OO.ui.MenuSelectWidget( $.extend( {
  		widget: this,
  		input: this,
  		$floatableContainer: config.$container || this.$element
  	}, config.menu ) );
  
  	this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
  
  	this.lookupsDisabled = false;
  	this.lookupInputFocused = false;
  	this.lookupHighlightFirstItem = config.highlightFirst;
  	this.showSuggestionsOnFocus = config.showSuggestionsOnFocus !== false;
  
  	// Events
  	this.$input.on( {
  		focus: this.onLookupInputFocus.bind( this ),
  		blur: this.onLookupInputBlur.bind( this ),
  		mousedown: this.onLookupInputMouseDown.bind( this )
  	} );
  	this.connect( this, {
  		change: 'onLookupInputChange'
  	} );
  	this.lookupMenu.connect( this, {
  		toggle: 'onLookupMenuToggle',
  		choose: 'onLookupMenuChoose'
  	} );
  
  	// Initialization
  	this.$input.attr( {
  		role: 'combobox',
  		'aria-owns': this.lookupMenu.getElementId(),
  		'aria-autocomplete': 'list'
  	} );
  	this.$element.addClass( 'oo-ui-lookupElement' );
  	this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
  	this.$overlay.append( this.lookupMenu.$element );
  };
  
  /* Setup */
  
  OO.mixinClass( OO.ui.mixin.LookupElement, OO.ui.mixin.RequestManager );
  
  /* Methods */
  
  /**
   * Handle input focus event.
   *
   * @protected
   * @param {jQuery.Event} e Input focus event
   */
  OO.ui.mixin.LookupElement.prototype.onLookupInputFocus = function () {
  	this.lookupInputFocused = true;
  	if ( this.showSuggestionsOnFocus ) {
  		this.populateLookupMenu();
  	}
  };
  
  /**
   * Handle input blur event.
   *
   * @protected
   * @param {jQuery.Event} e Input blur event
   */
  OO.ui.mixin.LookupElement.prototype.onLookupInputBlur = function () {
  	this.closeLookupMenu();
  	this.lookupInputFocused = false;
  };
  
  /**
   * Handle input mouse down event.
   *
   * @protected
   * @param {jQuery.Event} e Input mouse down event
   */
  OO.ui.mixin.LookupElement.prototype.onLookupInputMouseDown = function () {
  	if (
  		!this.lookupMenu.isVisible() &&
  		(
  			// Open the menu if the input was already focused.
  			// This way we allow the user to open the menu again after closing it with Escape (esc)
  			// by clicking in the input.
  			this.lookupInputFocused ||
  			// If showSuggestionsOnFocus is disabled, still open the menu on mousedown.
  			!this.showSuggestionsOnFocus
  		)
  	) {
  		this.populateLookupMenu();
  	}
  };
  
  /**
   * Handle input change event.
   *
   * @protected
   * @param {string} value New input value
   */
  OO.ui.mixin.LookupElement.prototype.onLookupInputChange = function () {
  	if ( this.lookupInputFocused ) {
  		this.populateLookupMenu();
  	}
  };
  
  /**
   * Handle the lookup menu being shown/hidden.
   *
   * @protected
   * @param {boolean} visible Whether the lookup menu is now visible.
   */
  OO.ui.mixin.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
  	if ( !visible ) {
  		// When the menu is hidden, abort any active request and clear the menu.
  		// This has to be done here in addition to closeLookupMenu(), because
  		// MenuSelectWidget will close itself when the user presses Escape (esc).
  		this.abortLookupRequest();
  		this.lookupMenu.clearItems();
  	}
  };
  
  /**
   * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
   *
   * @protected
   * @param {OO.ui.MenuOptionWidget} item Selected item
   */
  OO.ui.mixin.LookupElement.prototype.onLookupMenuChoose = function ( item ) {
  	this.setValue( item.getData() );
  };
  
  /**
   * Get lookup menu.
   *
   * @private
   * @return {OO.ui.MenuSelectWidget}
   */
  OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
  	return this.lookupMenu;
  };
  
  /**
   * Disable or re-enable lookups.
   *
   * When lookups are disabled, calls to #populateLookupMenu will be ignored.
   *
   * @param {boolean} disabled Disable lookups
   */
  OO.ui.mixin.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
  	this.lookupsDisabled = !!disabled;
  };
  
  /**
   * Open the menu. If there are no entries in the menu, this does nothing.
   *
   * @private
   * @chainable
   * @return {OO.ui.Element} The element, for chaining
   */
  OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
  	if ( !this.lookupMenu.isEmpty() ) {
  		this.lookupMenu.toggle( true );
  	}
  	return this;
  };
  
  /**
   * Close the menu, empty it, and abort any pending request.
   *
   * @private
   * @chainable
   * @return {OO.ui.Element} The element, for chaining
   */
  OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
  	this.lookupMenu.toggle( false );
  	this.abortLookupRequest();
  	this.lookupMenu.clearItems();
  	return this;
  };
  
  /**
   * Request menu items based on the input's current value, and when they arrive,
   * populate the menu with these items and show the menu.
   *
   * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
   *
   * @private
   * @chainable
   * @return {OO.ui.Element} The element, for chaining
   */
  OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
  	var widget = this,
  		value = this.getValue();
  
  	if ( this.lookupsDisabled || this.isReadOnly() ) {
  		return;
  	}
  
  	// If the input is empty, clear the menu, unless suggestions when empty are allowed.
  	if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
  		this.closeLookupMenu();
  	// Skip population if there is already a request pending for the current value
  	} else if ( value !== this.lookupQuery ) {
  		this.getLookupMenuItems()
  			.done( function ( items ) {
  				widget.lookupMenu.clearItems();
  				if ( items.length ) {
  					widget.lookupMenu
  						.addItems( items )
  						.toggle( true );
  					widget.initializeLookupMenuSelection();
  				} else {
  					widget.lookupMenu.toggle( false );
  				}
  			} )
  			.fail( function () {
  				widget.lookupMenu.clearItems();
  				widget.lookupMenu.toggle( false );
  			} );
  	}
  
  	return this;
  };
  
  /**
   * Highlight the first selectable item in the menu, if configured.
   *
   * @private
   * @chainable
   */
  OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
  	if ( this.lookupHighlightFirstItem && !this.lookupMenu.findSelectedItem() ) {
  		this.lookupMenu.highlightItem( this.lookupMenu.findFirstSelectableItem() );
  	}
  };
  
  /**
   * Get lookup menu items for the current query.
   *
   * @private
   * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
   *   the done event. If the request was aborted to make way for a subsequent request, this promise
   *   will not be rejected: it will remain pending forever.
   */
  OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
  	return this.getRequestData().then( function ( data ) {
  		return this.getLookupMenuOptionsFromData( data );
  	}.bind( this ) );
  };
  
  /**
   * Abort the currently pending lookup request, if any.
   *
   * @private
   */
  OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
  	this.abortRequest();
  };
  
  /**
   * Get a new request object of the current lookup query value.
   *
   * @protected
   * @method
   * @abstract
   * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
   */
  OO.ui.mixin.LookupElement.prototype.getLookupRequest = null;
  
  /**
   * Pre-process data returned by the request from #getLookupRequest.
   *
   * The return value of this function will be cached, and any further queries for the given value
   * will use the cache rather than doing API requests.
   *
   * @protected
   * @method
   * @abstract
   * @param {Mixed} response Response from server
   * @return {Mixed} Cached result data
   */
  OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = null;
  
  /**
   * Get a list of menu option widgets from the (possibly cached) data returned by
   * #getLookupCacheDataFromResponse.
   *
   * @protected
   * @method
   * @abstract
   * @param {Mixed} data Cached result data, usually an array
   * @return {OO.ui.MenuOptionWidget[]} Menu items
   */
  OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = null;
  
  /**
   * Set the read-only state of the widget.
   *
   * This will also disable/enable the lookups functionality.
   *
   * @param {boolean} readOnly Make input read-only
   * @chainable
   * @return {OO.ui.Element} The element, for chaining
   */
  OO.ui.mixin.LookupElement.prototype.setReadOnly = function ( readOnly ) {
  	// Parent method
  	// Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
  	OO.ui.TextInputWidget.prototype.setReadOnly.call( this, readOnly );
  
  	// During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor.
  	if ( this.isReadOnly() && this.lookupMenu ) {
  		this.closeLookupMenu();
  	}
  
  	return this;
  };
  
  /**
   * @inheritdoc OO.ui.mixin.RequestManager
   */
  OO.ui.mixin.LookupElement.prototype.getRequestQuery = function () {
  	return this.getValue();
  };
  
  /**
   * @inheritdoc OO.ui.mixin.RequestManager
   */
  OO.ui.mixin.LookupElement.prototype.getRequest = function () {
  	return this.getLookupRequest();
  };
  
  /**
   * @inheritdoc OO.ui.mixin.RequestManager
   */
  OO.ui.mixin.LookupElement.prototype.getRequestCacheDataFromResponse = function ( response ) {
  	return this.getLookupCacheDataFromResponse( response );
  };
  
  /**
   * TabPanelLayouts are used within {@link OO.ui.IndexLayout index layouts} to create tab panels that
   * users can select and display from the index's optional {@link OO.ui.TabSelectWidget tab}
   * navigation. TabPanels are usually not instantiated directly, rather extended to include the
   * required content and functionality.
   *
   * Each tab panel must have a unique symbolic name, which is passed to the constructor. In addition,
   * the tab panel's tab item is customized (with a label) using the #setupTabItem method. See
   * {@link OO.ui.IndexLayout IndexLayout} for an example.
   *
   * @class
   * @extends OO.ui.PanelLayout
   *
   * @constructor
   * @param {string} name Unique symbolic name of tab panel
   * @param {Object} [config] Configuration options
   * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for tab panel's tab
   * @cfg {Object} [tabItemConfig] Additional tab item config
   */
  OO.ui.TabPanelLayout = function OoUiTabPanelLayout( name, config ) {
  	// Allow passing positional parameters inside the config object
  	if ( OO.isPlainObject( name ) && config === undefined ) {
  		config = name;
  		name = config.name;
  	}
  
  	// Configuration initialization
  	config = $.extend( { scrollable: true }, config );
  
  	// Parent constructor
  	OO.ui.TabPanelLayout.super.call( this, config );
  
  	// Properties
  	this.name = name;
  	this.label = config.label;
  	this.tabItemConfig = config.tabItemConfig || {};
  	this.tabItem = null;
  	this.active = false;
  
  	// Initialization
  	this.$element
  		.addClass( 'oo-ui-tabPanelLayout' )
  		.attr( 'role', 'tabpanel' );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.TabPanelLayout, OO.ui.PanelLayout );
  
  /* Events */
  
  /**
   * An 'active' event is emitted when the tab panel becomes active. Tab panels become active when
   * they are shown in a index layout that is configured to display only one tab panel at a time.
   *
   * @event active
   * @param {boolean} active Tab panel is active
   */
  
  /* Methods */
  
  /**
   * Get the symbolic name of the tab panel.
   *
   * @return {string} Symbolic name of tab panel
   */
  OO.ui.TabPanelLayout.prototype.getName = function () {
  	return this.name;
  };
  
  /**
   * Check if tab panel is active.
   *
   * Tab panels become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is
   * configured to display only one tab panel at a time. Additional CSS is applied to the tab panel's
   * tab item to reflect the active state.
   *
   * @return {boolean} Tab panel is active
   */
  OO.ui.TabPanelLayout.prototype.isActive = function () {
  	return this.active;
  };
  
  /**
   * Get tab item.
   *
   * The tab item allows users to access the tab panel from the index's tab
   * navigation. The tab item itself can be customized (with a label, level, etc.) using the
   * #setupTabItem method.
   *
   * @return {OO.ui.TabOptionWidget|null} Tab option widget
   */
  OO.ui.TabPanelLayout.prototype.getTabItem = function () {
  	return this.tabItem;
  };
  
  /**
   * Get config for creating a tab item.
   *
   * @return {Object} Tab option config
   */
  OO.ui.TabPanelLayout.prototype.getTabItemConfig = function () {
  	return this.tabItemConfig;
  };
  
  /**
   * Set or unset the tab item.
   *
   * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
   * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
   * level), use #setupTabItem instead of this method.
   *
   * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
   * @chainable
   * @return {OO.ui.TabPanelLayout} The layout, for chaining
   */
  OO.ui.TabPanelLayout.prototype.setTabItem = function ( tabItem ) {
  	this.tabItem = tabItem || null;
  	if ( tabItem ) {
  		this.setupTabItem();
  	}
  	return this;
  };
  
  /**
   * Set up the tab item.
   *
   * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
   * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
   * the #setTabItem method instead.
   *
   * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
   * @chainable
   * @return {OO.ui.TabPanelLayout} The layout, for chaining
   */
  OO.ui.TabPanelLayout.prototype.setupTabItem = function () {
  	this.$element.attr( 'aria-labelledby', this.tabItem.getElementId() );
  
  	this.tabItem.$element.attr( 'aria-controls', this.getElementId() );
  
  	if ( this.label ) {
  		this.tabItem.setLabel( this.label );
  	}
  	return this;
  };
  
  /**
   * Set the tab panel to its 'active' state.
   *
   * Tab panels become active when they are shown in a index layout that is configured to display only
   * one tab panel at a time. Additional CSS is applied to the tab item to reflect the tab panel's
   * active state. Outside of the index context, setting the active state on a tab panel does nothing.
   *
   * @param {boolean} active Tab panel is active
   * @fires active
   */
  OO.ui.TabPanelLayout.prototype.setActive = function ( active ) {
  	active = !!active;
  
  	if ( active !== this.active ) {
  		this.active = active;
  		this.$element.toggleClass( 'oo-ui-tabPanelLayout-active', this.active );
  		this.emit( 'active', this.active );
  	}
  };
  
  /**
   * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that
   * users can select and display from the booklet's optional
   * {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated
   * directly, rather extended to include the required content and functionality.
   *
   * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the
   * page's outline item is customized (with a label, outline level, etc.) using the
   * #setupOutlineItem method. See {@link OO.ui.BookletLayout BookletLayout} for an example.
   *
   * @class
   * @extends OO.ui.PanelLayout
   *
   * @constructor
   * @param {string} name Unique symbolic name of page
   * @param {Object} [config] Configuration options
   */
  OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
  	// Allow passing positional parameters inside the config object
  	if ( OO.isPlainObject( name ) && config === undefined ) {
  		config = name;
  		name = config.name;
  	}
  
  	// Configuration initialization
  	config = $.extend( { scrollable: true }, config );
  
  	// Parent constructor
  	OO.ui.PageLayout.super.call( this, config );
  
  	// Properties
  	this.name = name;
  	this.outlineItem = null;
  	this.active = false;
  
  	// Initialization
  	this.$element.addClass( 'oo-ui-pageLayout' );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
  
  /* Events */
  
  /**
   * An 'active' event is emitted when the page becomes active. Pages become active when they are
   * shown in a booklet layout that is configured to display only one page at a time.
   *
   * @event active
   * @param {boolean} active Page is active
   */
  
  /* Methods */
  
  /**
   * Get the symbolic name of the page.
   *
   * @return {string} Symbolic name of page
   */
  OO.ui.PageLayout.prototype.getName = function () {
  	return this.name;
  };
  
  /**
   * Check if page is active.
   *
   * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is
   * configured to display only one page at a time. Additional CSS is applied to the page's outline
   * item to reflect the active state.
   *
   * @return {boolean} Page is active
   */
  OO.ui.PageLayout.prototype.isActive = function () {
  	return this.active;
  };
  
  /**
   * Get outline item.
   *
   * The outline item allows users to access the page from the booklet's outline
   * navigation. The outline item itself can be customized (with a label, level, etc.) using the
   * #setupOutlineItem method.
   *
   * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
   */
  OO.ui.PageLayout.prototype.getOutlineItem = function () {
  	return this.outlineItem;
  };
  
  /**
   * Set or unset the outline item.
   *
   * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
   * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label
   * or outline level), use #setupOutlineItem instead of this method.
   *
   * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
   * @chainable
   * @return {OO.ui.PageLayout} The layout, for chaining
   */
  OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
  	this.outlineItem = outlineItem || null;
  	if ( outlineItem ) {
  		this.setupOutlineItem();
  	}
  	return this;
  };
  
  /**
   * Set up the outline item.
   *
   * Override this method to customize the outline item (e.g., to add a label or outline level). To
   * set or unset the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or
   * `null`), use the #setOutlineItem method instead.
   *
   * @protected
   */
  OO.ui.PageLayout.prototype.setupOutlineItem = function () {
  };
  
  /**
   * Set the page to its 'active' state.
   *
   * Pages become active when they are shown in a booklet layout that is configured to display only
   * one page at a time. Additional CSS is applied to the outline item to reflect the page's active
   * state. Outside of the booklet context, setting the active state on a page does nothing.
   *
   * @param {boolean} active Page is active
   * @fires active
   */
  OO.ui.PageLayout.prototype.setActive = function ( active ) {
  	active = !!active;
  
  	if ( active !== this.active ) {
  		this.active = active;
  		this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
  		this.emit( 'active', this.active );
  	}
  };
  
  /**
   * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one
   * panel is displayed at a time, though the stack layout can also be configured to show all
   * contained panels, one after another, by setting the #continuous option to 'true'.
   *
   *     @example
   *     // A stack layout with two panels, configured to be displayed continuously
   *     var myStack = new OO.ui.StackLayout( {
   *         items: [
   *             new OO.ui.PanelLayout( {
   *                 $content: $( '<p>Panel One</p>' ),
   *                 padded: true,
   *                 framed: true
   *             } ),
   *             new OO.ui.PanelLayout( {
   *                 $content: $( '<p>Panel Two</p>' ),
   *                 padded: true,
   *                 framed: true
   *             } )
   *         ],
   *         continuous: true
   *     } );
   *     $( document.body ).append( myStack.$element );
   *
   * @class
   * @extends OO.ui.PanelLayout
   * @mixins OO.ui.mixin.GroupElement
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel
   *  is displayed at a time.
   * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
   */
  OO.ui.StackLayout = function OoUiStackLayout( config ) {
  	// Configuration initialization
  	// Make the layout scrollable in continuous mode, otherwise each
  	// panel is responsible for its own scrolling.
  	config = $.extend( { scrollable: !!( config && config.continuous ) }, config );
  
  	// Parent constructor
  	OO.ui.StackLayout.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.GroupElement.call( this, $.extend( { $group: this.$element }, config ) );
  
  	// Properties
  	this.currentItem = null;
  	this.continuous = !!config.continuous;
  
  	// Initialization
  	this.$element.addClass( 'oo-ui-stackLayout' );
  	if ( this.continuous ) {
  		this.$element.addClass( 'oo-ui-stackLayout-continuous' );
  	}
  	this.addItems( config.items || [] );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
  OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );
  
  /* Events */
  
  /**
   * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
   * {@link #clearItems cleared} or {@link #setItem displayed}.
   *
   * @event set
   * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
   */
  
  /* Methods */
  
  /**
   * Get the current panel.
   *
   * @return {OO.ui.Layout|null}
   */
  OO.ui.StackLayout.prototype.getCurrentItem = function () {
  	return this.currentItem;
  };
  
  /**
   * Unset the current item.
   *
   * @private
   * @param {OO.ui.StackLayout} layout
   * @fires set
   */
  OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
  	var prevItem = this.currentItem;
  	if ( prevItem === null ) {
  		return;
  	}
  
  	this.currentItem = null;
  	this.emit( 'set', null );
  };
  
  /**
   * Add panel layouts to the stack layout.
   *
   * Panels will be added to the end of the stack layout array unless the optional index parameter
   * specifies a different insertion point. Adding a panel that is already in the stack will move it
   * to the end of the array or the point specified by the index.
   *
   * @param {OO.ui.Layout[]} [items] Panels to add
   * @param {number} [index] Index of the insertion point
   * @chainable
   * @return {OO.ui.StackLayout} The layout, for chaining
   */
  OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
  	if ( !items || !items.length ) {
  		return this;
  	}
  
  	// Update the visibility
  	this.updateHiddenState( items, this.currentItem );
  
  	// Mixin method
  	OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
  
  	if ( !this.currentItem ) {
  		this.setItem( items[ 0 ] );
  	}
  
  	return this;
  };
  
  /**
   * Remove the specified panels from the stack layout.
   *
   * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all
   * panels, you may wish to use the #clearItems method instead.
   *
   * @param {OO.ui.Layout[]} itemsToRemove Panels to remove
   * @chainable
   * @return {OO.ui.StackLayout} The layout, for chaining
   * @fires set
   */
  OO.ui.StackLayout.prototype.removeItems = function ( itemsToRemove ) {
  	var isCurrentItemRemoved = itemsToRemove.indexOf( this.currentItem ) !== -1,
  		nextItem;
  
  	if ( isCurrentItemRemoved ) {
  		var i = this.items.indexOf( this.currentItem );
  		do {
  			nextItem = this.items[ ++i ];
  		} while ( nextItem && itemsToRemove.indexOf( nextItem ) !== -1 );
  	}
  
  	// Mixin method
  	OO.ui.mixin.GroupElement.prototype.removeItems.call( this, itemsToRemove );
  
  	if ( isCurrentItemRemoved ) {
  		if ( this.items.length ) {
  			this.setItem( nextItem || this.items[ this.items.length - 1 ] );
  		} else {
  			this.unsetCurrentItem();
  		}
  	}
  
  	return this;
  };
  
  /**
   * Clear all panels from the stack layout.
   *
   * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
   * a subset of panels, use the #removeItems method.
   *
   * @chainable
   * @return {OO.ui.StackLayout} The layout, for chaining
   * @fires set
   */
  OO.ui.StackLayout.prototype.clearItems = function () {
  	this.unsetCurrentItem();
  	OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
  
  	return this;
  };
  
  /**
   * Show the specified panel.
   *
   * If another panel is currently displayed, it will be hidden.
   *
   * @param {OO.ui.Layout} item Panel to show
   * @chainable
   * @return {OO.ui.StackLayout} The layout, for chaining
   * @fires set
   */
  OO.ui.StackLayout.prototype.setItem = function ( item ) {
  	if ( item !== this.currentItem ) {
  		this.updateHiddenState( this.items, item );
  
  		if ( this.items.indexOf( item ) !== -1 ) {
  			this.currentItem = item;
  			this.emit( 'set', item );
  		} else {
  			this.unsetCurrentItem();
  		}
  	}
  
  	return this;
  };
  
  /**
   * Reset the scroll offset of all panels, or the container if continuous
   *
   * @inheritdoc
   */
  OO.ui.StackLayout.prototype.resetScroll = function () {
  	if ( this.continuous ) {
  		// Parent method
  		return OO.ui.StackLayout.super.prototype.resetScroll.call( this );
  	}
  	// Reset each panel
  	this.getItems().forEach( function ( panel ) {
  		// eslint-disable-next-line no-jquery/no-class-state
  		var hidden = panel.$element.hasClass( 'oo-ui-element-hidden' );
  		// Scroll can only be reset when panel is visible
  		panel.$element.removeClass( 'oo-ui-element-hidden' );
  		panel.resetScroll();
  		if ( hidden ) {
  			panel.$element.addClass( 'oo-ui-element-hidden' );
  		}
  	} );
  
  	return this;
  };
  
  /**
   * Update the visibility of all items in case of non-continuous view.
   *
   * Ensure all items are hidden except for the selected one.
   * This method does nothing when the stack is continuous.
   *
   * @private
   * @param {OO.ui.Layout[]} items Item list iterate over
   * @param {OO.ui.Layout} [selectedItem] Selected item to show
   */
  OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
  	var i, len;
  
  	if ( !this.continuous ) {
  		for ( i = 0, len = items.length; i < len; i++ ) {
  			if ( !selectedItem || selectedItem !== items[ i ] ) {
  				items[ i ].toggle( false );
  				items[ i ].$element.attr( 'aria-hidden', 'true' );
  			}
  		}
  		if ( selectedItem ) {
  			selectedItem.toggle( true );
  			selectedItem.$element.removeAttr( 'aria-hidden' );
  		}
  	}
  };
  
  /**
   * MenuLayouts combine a menu and a content {@link OO.ui.PanelLayout panel}. The menu is positioned
   * relative to the content (after, before, top, or bottom) and its size is customized with the
   * #menuSize config. The content area will fill all remaining space.
   *
   *     @example
   *     var menuLayout,
   *         menuPanel = new OO.ui.PanelLayout( {
   *             padded: true,
   *             expanded: true,
   *             scrollable: true
   *         } ),
   *         contentPanel = new OO.ui.PanelLayout( {
   *             padded: true,
   *             expanded: true,
   *             scrollable: true
   *         } ),
   *         select = new OO.ui.SelectWidget( {
   *             items: [
   *                 new OO.ui.OptionWidget( {
   *                     data: 'before',
   *                     label: 'Before'
   *                 } ),
   *                 new OO.ui.OptionWidget( {
   *                     data: 'after',
   *                     label: 'After'
   *                 } ),
   *                 new OO.ui.OptionWidget( {
   *                     data: 'top',
   *                     label: 'Top'
   *                 } ),
   *                 new OO.ui.OptionWidget( {
   *                     data: 'bottom',
   *                     label: 'Bottom'
   *                 } )
   *              ]
   *         } ).on( 'select', function ( item ) {
   *            menuLayout.setMenuPosition( item.getData() );
   *         } );
   *
   *     menuLayout = new OO.ui.MenuLayout( {
   *         position: 'top',
   *         menuPanel: menuPanel,
   *         contentPanel: contentPanel
   *     } );
   *     menuLayout.$menu.append(
   *         menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
   *     );
   *     menuLayout.$content.append(
   *         contentPanel.$element.append(
   *             '<b>Content panel</b>',
   *             '<p>Note that the menu is positioned relative to the content panel: ' +
   *             'top, bottom, after, before.</p>'
   *          )
   *     );
   *     $( document.body ).append( menuLayout.$element );
   *
   * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
   * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
   * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
   * may be omitted.
   *
   *     .oo-ui-menuLayout-menu {
   *         width: 200px;
   *         height: 200px;
   *     }
   *
   *     .oo-ui-menuLayout-content {
   *         top: 200px;
   *         left: 200px;
   *         right: 200px;
   *         bottom: 200px;
   *     }
   *
   * @class
   * @extends OO.ui.Layout
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {OO.ui.PanelLayout} [menuPanel] Menu panel
   * @cfg {OO.ui.PanelLayout} [contentPanel] Content panel
   * @cfg {boolean} [expanded=true] Expand the layout to fill the entire parent element.
   * @cfg {boolean} [showMenu=true] Show menu
   * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
   */
  OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
  	// Configuration initialization
  	config = $.extend( {
  		expanded: true,
  		showMenu: true,
  		menuPosition: 'before'
  	}, config );
  
  	// Parent constructor
  	OO.ui.MenuLayout.super.call( this, config );
  
  	this.menuPanel = null;
  	this.contentPanel = null;
  	this.expanded = !!config.expanded;
  	/**
  	 * Menu DOM node
  	 *
  	 * @property {jQuery}
  	 */
  	this.$menu = $( '<div>' );
  	/**
  	 * Content DOM node
  	 *
  	 * @property {jQuery}
  	 */
  	this.$content = $( '<div>' );
  
  	// Initialization
  	this.$menu.addClass( 'oo-ui-menuLayout-menu' );
  	this.$content.addClass( 'oo-ui-menuLayout-content' );
  	this.$element.addClass( 'oo-ui-menuLayout' );
  	if ( config.expanded ) {
  		this.$element.addClass( 'oo-ui-menuLayout-expanded' );
  	} else {
  		this.$element.addClass( 'oo-ui-menuLayout-static' );
  	}
  	if ( config.menuPanel ) {
  		this.setMenuPanel( config.menuPanel );
  	}
  	if ( config.contentPanel ) {
  		this.setContentPanel( config.contentPanel );
  	}
  	this.setMenuPosition( config.menuPosition );
  	this.toggleMenu( config.showMenu );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
  
  /* Methods */
  
  /**
   * Toggle menu.
   *
   * @param {boolean} showMenu Show menu, omit to toggle
   * @chainable
   * @return {OO.ui.MenuLayout} The layout, for chaining
   */
  OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
  	showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
  
  	if ( this.showMenu !== showMenu ) {
  		this.showMenu = showMenu;
  		this.$element
  			.toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
  			.toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
  		this.$menu.attr( 'aria-hidden', this.showMenu ? 'false' : 'true' );
  	}
  
  	return this;
  };
  
  /**
   * Check if menu is visible
   *
   * @return {boolean} Menu is visible
   */
  OO.ui.MenuLayout.prototype.isMenuVisible = function () {
  	return this.showMenu;
  };
  
  /**
   * Set menu position.
   *
   * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
   * @chainable
   * @return {OO.ui.MenuLayout} The layout, for chaining
   */
  OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
  	if ( [ 'top', 'bottom', 'before', 'after' ].indexOf( position ) === -1 ) {
  		position = 'before';
  	}
  
  	this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
  	this.menuPosition = position;
  	if ( this.menuPosition === 'top' || this.menuPosition === 'before' ) {
  		this.$element.append( this.$menu, this.$content );
  	} else {
  		this.$element.append( this.$content, this.$menu );
  	}
  	this.$element.addClass( 'oo-ui-menuLayout-' + position );
  
  	return this;
  };
  
  /**
   * Get menu position.
   *
   * @return {string} Menu position
   */
  OO.ui.MenuLayout.prototype.getMenuPosition = function () {
  	return this.menuPosition;
  };
  
  /**
   * Set the menu panel.
   *
   * @param {OO.ui.PanelLayout} menuPanel Menu panel
   */
  OO.ui.MenuLayout.prototype.setMenuPanel = function ( menuPanel ) {
  	this.menuPanel = menuPanel;
  	this.$menu.append( this.menuPanel.$element );
  };
  
  /**
   * Set the content panel.
   *
   * @param {OO.ui.PanelLayout} contentPanel Content panel
   */
  OO.ui.MenuLayout.prototype.setContentPanel = function ( contentPanel ) {
  	this.contentPanel = contentPanel;
  	this.$content.append( this.contentPanel.$element );
  };
  
  /**
   * Clear the menu panel.
   */
  OO.ui.MenuLayout.prototype.clearMenuPanel = function () {
  	this.menuPanel = null;
  	this.$menu.empty();
  };
  
  /**
   * Clear the content panel.
   */
  OO.ui.MenuLayout.prototype.clearContentPanel = function () {
  	this.contentPanel = null;
  	this.$content.empty();
  };
  
  /**
   * Reset the scroll offset of all panels and the tab select widget
   *
   * @inheritdoc
   */
  OO.ui.MenuLayout.prototype.resetScroll = function () {
  	if ( this.menuPanel ) {
  		this.menuPanel.resetScroll();
  	}
  	if ( this.contentPanel ) {
  		this.contentPanel.resetScroll();
  	}
  
  	return this;
  };
  
  /**
   * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
   * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
   * through the pages and select which one to display. By default, only one page is
   * displayed at a time and the outline is hidden. When a user navigates to a new page,
   * the booklet layout automatically focuses on the first focusable element, unless the
   * default setting is changed. Optionally, booklets can be configured to show
   * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
   *
   *     @example
   *     // Example of a BookletLayout that contains two PageLayouts.
   *
   *     function PageOneLayout( name, config ) {
   *         PageOneLayout.super.call( this, name, config );
   *         this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on ' +
   *         'the left)</p>' );
   *     }
   *     OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
   *     PageOneLayout.prototype.setupOutlineItem = function () {
   *         this.outlineItem.setLabel( 'Page One' );
   *     };
   *
   *     function PageTwoLayout( name, config ) {
   *         PageTwoLayout.super.call( this, name, config );
   *         this.$element.append( '<p>Second page</p>' );
   *     }
   *     OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
   *     PageTwoLayout.prototype.setupOutlineItem = function () {
   *         this.outlineItem.setLabel( 'Page Two' );
   *     };
   *
   *     var page1 = new PageOneLayout( 'one' ),
   *         page2 = new PageTwoLayout( 'two' );
   *
   *     var booklet = new OO.ui.BookletLayout( {
   *         outlined: true
   *     } );
   *
   *     booklet.addPages( [ page1, page2 ] );
   *     $( document.body ).append( booklet.$element );
   *
   * @class
   * @extends OO.ui.MenuLayout
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {boolean} [continuous=false] Show all pages, one after another
   * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is
   *  displayed. Disabled on mobile.
   * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the
   *  pages of the booklet.
   * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages.
   */
  OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
  	// Configuration initialization
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.BookletLayout.super.call( this, config );
  
  	// Properties
  	this.currentPageName = null;
  	this.pages = {};
  	this.ignoreFocus = false;
  	this.stackLayout = new OO.ui.StackLayout( {
  		continuous: !!config.continuous,
  		expanded: this.expanded
  	} );
  	this.setContentPanel( this.stackLayout );
  	this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
  	this.outlineVisible = false;
  	this.outlined = !!config.outlined;
  	if ( this.outlined ) {
  		this.editable = !!config.editable;
  		this.outlineControlsWidget = null;
  		this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
  		this.outlinePanel = new OO.ui.PanelLayout( {
  			expanded: this.expanded,
  			scrollable: true
  		} );
  		this.setMenuPanel( this.outlinePanel );
  		this.outlineVisible = true;
  		if ( this.editable ) {
  			this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
  				this.outlineSelectWidget
  			);
  		}
  	}
  	this.toggleMenu( this.outlined );
  
  	// Events
  	this.stackLayout.connect( this, {
  		set: 'onStackLayoutSet'
  	} );
  	if ( this.outlined ) {
  		this.outlineSelectWidget.connect( this, {
  			select: 'onOutlineSelectWidgetSelect'
  		} );
  	}
  	if ( this.autoFocus ) {
  		// Event 'focus' does not bubble, but 'focusin' does
  		this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
  	}
  
  	// Initialization
  	this.$element.addClass( 'oo-ui-bookletLayout' );
  	this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
  	if ( this.outlined ) {
  		this.outlinePanel.$element
  			.addClass( 'oo-ui-bookletLayout-outlinePanel' )
  			.append( this.outlineSelectWidget.$element );
  		if ( this.editable ) {
  			this.outlinePanel.$element
  				.addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
  				.append( this.outlineControlsWidget.$element );
  		}
  	}
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
  
  /* Events */
  
  /**
   * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the
   * booklet layout.
   *
   * @event set
   * @param {OO.ui.PageLayout} page Current page
   */
  
  /**
   * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
   *
   * @event add
   * @param {OO.ui.PageLayout[]} page Added pages
   * @param {number} index Index pages were added at
   */
  
  /**
   * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
   * {@link #removePages removed} from the booklet.
   *
   * @event remove
   * @param {OO.ui.PageLayout[]} pages Removed pages
   */
  
  /* Methods */
  
  /**
   * Handle stack layout focus.
   *
   * @private
   * @param {jQuery.Event} e Focusin event
   */
  OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
  	var name, $target;
  
  	// Find the page that an element was focused within
  	$target = $( e.target ).closest( '.oo-ui-pageLayout' );
  	for ( name in this.pages ) {
  		// Check for page match, exclude current page to find only page changes
  		if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
  			this.setPage( name );
  			break;
  		}
  	}
  };
  
  /**
   * Handle stack layout set events.
   *
   * @private
   * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
   */
  OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
  	var promise, layout = this;
  	// If everything is unselected, do nothing
  	if ( !page ) {
  		return;
  	}
  	// For continuous BookletLayouts, scroll the selected page into view first
  	if ( this.stackLayout.continuous && !this.scrolling ) {
  		promise = page.scrollElementIntoView();
  	} else {
  		promise = $.Deferred().resolve();
  	}
  	// Focus the first element on the newly selected panel.
  	// Don't focus if the page was set by scrolling.
  	if ( this.autoFocus && !OO.ui.isMobile() && !this.scrolling ) {
  		promise.done( function () {
  			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.
   *
   * @param {number} [itemIndex] A specific item to focus on
   */
  OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
  	var page,
  		items = this.stackLayout.getItems();
  
  	if ( itemIndex !== undefined && items[ itemIndex ] ) {
  		page = items[ itemIndex ];
  	} else {
  		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 ( !OO.ui.contains( page.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
  		page.focus();
  	}
  };
  
  /**
   * Find the first focusable input in the booklet layout and focus
   * on it.
   */
  OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
  	OO.ui.findFocusable( this.stackLayout.$element ).focus();
  };
  
  /**
   * Handle outline widget select events.
   *
   * @private
   * @param {OO.ui.OptionWidget|null} item Selected item
   */
  OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
  	if ( item ) {
  		this.setPage( item.getData() );
  	}
  };
  
  /**
   * Check if booklet has an outline.
   *
   * @return {boolean} Booklet has an outline
   */
  OO.ui.BookletLayout.prototype.isOutlined = function () {
  	return this.outlined;
  };
  
  /**
   * Check if booklet has editing controls.
   *
   * @return {boolean} Booklet is editable
   */
  OO.ui.BookletLayout.prototype.isEditable = function () {
  	return this.editable;
  };
  
  /**
   * Check if booklet has a visible outline.
   *
   * @return {boolean} Outline is visible
   */
  OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
  	return this.outlined && this.outlineVisible;
  };
  
  /**
   * Hide or show the outline.
   *
   * @param {boolean} [show] Show outline, omit to invert current state
   * @chainable
   * @return {OO.ui.BookletLayout} The layout, for chaining
   */
  OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
  	var booklet = this;
  
  	if ( this.outlined ) {
  		show = show === undefined ? !this.outlineVisible : !!show;
  		this.outlineVisible = show;
  		this.toggleMenu( show );
  		if ( show && this.editable ) {
  			// HACK: Kill dumb scrollbars when the sidebar stops animating, see T161798.
  			// Only necessary when outline controls are present, delay matches transition on
  			// `.oo-ui-menuLayout-menu`.
  			setTimeout( function () {
  				OO.ui.Element.static.reconsiderScrollbars( booklet.outlinePanel.$element[ 0 ] );
  			}, OO.ui.theme.getDialogTransitionDuration() );
  		}
  	}
  
  	return this;
  };
  
  /**
   * Find the page closest to the specified page.
   *
   * @param {OO.ui.PageLayout} page Page to use as a reference point
   * @return {OO.ui.PageLayout|null} Page closest to the specified page
   */
  OO.ui.BookletLayout.prototype.findClosestPage = function ( page ) {
  	var next, prev, level,
  		pages = this.stackLayout.getItems(),
  		index = pages.indexOf( page );
  
  	if ( index !== -1 ) {
  		next = pages[ index + 1 ];
  		prev = pages[ index - 1 ];
  		// Prefer adjacent pages at the same level
  		if ( this.outlined ) {
  			level = this.outlineSelectWidget.findItemFromData( page.getName() ).getLevel();
  			if (
  				prev &&
  				level === this.outlineSelectWidget.findItemFromData( prev.getName() ).getLevel()
  			) {
  				return prev;
  			}
  			if (
  				next &&
  				level === this.outlineSelectWidget.findItemFromData( next.getName() ).getLevel()
  			) {
  				return next;
  			}
  		}
  	}
  	return prev || next || null;
  };
  
  /**
   * Get the outline widget.
   *
   * If the booklet is not outlined, the method will return `null`.
   *
   * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
   */
  OO.ui.BookletLayout.prototype.getOutline = function () {
  	return this.outlineSelectWidget;
  };
  
  /**
   * Get the outline controls widget.
   *
   * If the outline is not editable, the method will return `null`.
   *
   * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
   */
  OO.ui.BookletLayout.prototype.getOutlineControls = function () {
  	return this.outlineControlsWidget;
  };
  
  /**
   * Get a page by its symbolic name.
   *
   * @param {string} name Symbolic name of page
   * @return {OO.ui.PageLayout|undefined} Page, if found
   */
  OO.ui.BookletLayout.prototype.getPage = function ( name ) {
  	return this.pages[ name ];
  };
  
  /**
   * Get the current page.
   *
   * @return {OO.ui.PageLayout|undefined} Current page, if found
   */
  OO.ui.BookletLayout.prototype.getCurrentPage = function () {
  	var name = this.getCurrentPageName();
  	return name ? this.getPage( name ) : undefined;
  };
  
  /**
   * Get the symbolic name of the current page.
   *
   * @return {string|null} Symbolic name of the current page
   */
  OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
  	return this.currentPageName;
  };
  
  /**
   * Add pages to the booklet layout
   *
   * When pages are added with the same names as existing pages, the existing pages will be
   * automatically removed before the new pages are added.
   *
   * @param {OO.ui.PageLayout[]} pages Pages to add
   * @param {number} index Index of the insertion point
   * @fires add
   * @chainable
   * @return {OO.ui.BookletLayout} The layout, for chaining
   */
  OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
  	var i, len, name, page, item, currentIndex,
  		stackLayoutPages = this.stackLayout.getItems(),
  		remove = [],
  		items = [];
  
  	// Remove pages with same names
  	for ( i = 0, len = pages.length; i < len; i++ ) {
  		page = pages[ i ];
  		name = page.getName();
  
  		if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
  			// Correct the insertion index
  			currentIndex = stackLayoutPages.indexOf( this.pages[ name ] );
  			if ( currentIndex !== -1 && currentIndex + 1 < index ) {
  				index--;
  			}
  			remove.push( this.pages[ name ] );
  		}
  	}
  	if ( remove.length ) {
  		this.removePages( remove );
  	}
  
  	// Add new pages
  	for ( i = 0, len = pages.length; i < len; i++ ) {
  		page = pages[ i ];
  		name = page.getName();
  		this.pages[ page.getName() ] = page;
  		if ( this.outlined ) {
  			item = new OO.ui.OutlineOptionWidget( { data: name } );
  			page.setOutlineItem( item );
  			items.push( item );
  		}
  	}
  
  	if ( this.outlined ) {
  		this.outlineSelectWidget.addItems( items, index );
  		// It's impossible to lose a selection here. Selecting something else is business logic.
  	}
  	this.stackLayout.addItems( pages, index );
  	this.emit( 'add', pages, index );
  
  	return this;
  };
  
  /**
   * Remove the specified pages from the booklet layout.
   *
   * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
   *
   * @param {OO.ui.PageLayout[]} pages An array of pages to remove
   * @fires remove
   * @chainable
   * @return {OO.ui.BookletLayout} The layout, for chaining
   */
  OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
  	var i, len, name, page,
  		itemsToRemove = [];
  
  	for ( i = 0, len = pages.length; i < len; i++ ) {
  		page = pages[ i ];
  		name = page.getName();
  		delete this.pages[ name ];
  		if ( this.outlined ) {
  			itemsToRemove.push( this.outlineSelectWidget.findItemFromData( name ) );
  			page.setOutlineItem( null );
  		}
  		// If the current page is removed, clear currentPageName
  		if ( this.currentPageName === name ) {
  			this.currentPageName = null;
  		}
  	}
  	if ( itemsToRemove.length ) {
  		this.outlineSelectWidget.removeItems( itemsToRemove );
  		// We might loose the selection here, but what to select instead is business logic.
  	}
  	this.stackLayout.removeItems( pages );
  	this.emit( 'remove', pages );
  
  	return this;
  };
  
  /**
   * Clear all pages from the booklet layout.
   *
   * To remove only a subset of pages from the booklet, use the #removePages method.
   *
   * @fires remove
   * @chainable
   * @return {OO.ui.BookletLayout} The layout, for chaining
   */
  OO.ui.BookletLayout.prototype.clearPages = function () {
  	var i, len,
  		pages = this.stackLayout.getItems();
  
  	this.pages = {};
  	this.currentPageName = null;
  	if ( this.outlined ) {
  		this.outlineSelectWidget.clearItems();
  		for ( i = 0, len = pages.length; i < len; i++ ) {
  			pages[ i ].setOutlineItem( null );
  		}
  	}
  	this.stackLayout.clearItems();
  
  	this.emit( 'remove', pages );
  
  	return this;
  };
  
  /**
   * Set the current page by symbolic name.
   *
   * @fires set
   * @param {string} name Symbolic name of page
   */
  OO.ui.BookletLayout.prototype.setPage = function ( name ) {
  	var page = this.pages[ name ];
  	if ( !page || name === this.currentPageName ) {
  		return;
  	}
  
  	var previousPage = this.currentPageName ? this.pages[ this.currentPageName ] : null;
  	this.currentPageName = name;
  
  	if ( this.outlined ) {
  		var selectedItem = this.outlineSelectWidget.findSelectedItem();
  		if ( !selectedItem || selectedItem.getData() !== name ) {
  			// Warning! This triggers a "select" event and the .onOutlineSelectWidgetSelect()
  			// handler, which calls .setPage() a second time. Make sure .currentPageName is set to
  			// break this loop.
  			this.outlineSelectWidget.selectItemByData( name );
  		}
  	}
  
  	var $focused;
  	if ( previousPage ) {
  		previousPage.setActive( false );
  		// Blur anything focused if the next page doesn't have anything focusable.
  		// This is not needed if the next page has something focusable (because once it is
  		// focused this blur happens automatically). If the layout is non-continuous, this
  		// check is meaningless because the next page is not visible yet and thus can't
  		// hold focus.
  		if ( this.autoFocus &&
  			!OO.ui.isMobile() &&
  			this.stackLayout.continuous &&
  			OO.ui.findFocusable( page.$element ).length !== 0
  		) {
  			$focused = previousPage.$element.find( ':focus' );
  			if ( $focused.length ) {
  				$focused[ 0 ].blur();
  			}
  		}
  	}
  	page.setActive( true );
  	this.stackLayout.setItem( page );
  	if ( !this.stackLayout.continuous && previousPage ) {
  		// This should not be necessary, since any inputs on the previous page should have
  		// been blurred when it was hidden, but browsers are not very consistent about
  		// this.
  		$focused = previousPage.$element.find( ':focus' );
  		if ( $focused.length ) {
  			$focused[ 0 ].blur();
  		}
  	}
  	this.emit( 'set', page );
  };
  
  /**
   * For outlined-continuous booklets, also reset the outlineSelectWidget to the first item.
   *
   * @inheritdoc
   */
  OO.ui.BookletLayout.prototype.resetScroll = function () {
  	// Parent method
  	OO.ui.BookletLayout.super.prototype.resetScroll.call( this );
  
  	if (
  		this.outlined &&
  		this.stackLayout.continuous &&
  		this.outlineSelectWidget.findFirstSelectableItem()
  	) {
  		this.scrolling = true;
  		this.outlineSelectWidget.selectItem( this.outlineSelectWidget.findFirstSelectableItem() );
  		this.scrolling = false;
  	}
  	return this;
  };
  
  /**
   * Select the first selectable page.
   *
   * @chainable
   * @return {OO.ui.BookletLayout} The layout, for chaining
   */
  OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
  	if ( !this.outlineSelectWidget.findSelectedItem() ) {
  		this.outlineSelectWidget.selectItem( this.outlineSelectWidget.findFirstSelectableItem() );
  	}
  
  	return this;
  };
  
  /**
   * IndexLayouts contain {@link OO.ui.TabPanelLayout tab panel layouts} as well as
   * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the tab panels
   * and select which one to display. By default, only one tab panel is displayed at a time. When a
   * user navigates to a new tab panel, the index layout automatically focuses on the first focusable
   * element, unless the default setting is changed.
   *
   * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
   *
   *     @example
   *     // Example of a IndexLayout that contains two TabPanelLayouts.
   *
   *     function TabPanelOneLayout( name, config ) {
   *         TabPanelOneLayout.super.call( this, name, config );
   *         this.$element.append( '<p>First tab panel</p>' );
   *     }
   *     OO.inheritClass( TabPanelOneLayout, OO.ui.TabPanelLayout );
   *     TabPanelOneLayout.prototype.setupTabItem = function () {
   *         this.tabItem.setLabel( 'Tab panel one' );
   *     };
   *
   *     var tabPanel1 = new TabPanelOneLayout( 'one' ),
   *         tabPanel2 = new OO.ui.TabPanelLayout( 'two', { label: 'Tab panel two' } );
   *
   *     tabPanel2.$element.append( '<p>Second tab panel</p>' );
   *
   *     var index = new OO.ui.IndexLayout();
   *
   *     index.addTabPanels( [ tabPanel1, tabPanel2 ] );
   *     $( document.body ).append( index.$element );
   *
   * @class
   * @extends OO.ui.MenuLayout
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {OO.ui.StackLayout} [contentPanel] Content stack (see MenuLayout)
   * @cfg {boolean} [continuous=false] Show all tab panels, one after another
   * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new tab panel is
   *  displayed. Disabled on mobile.
   * @cfg {boolean} [framed=true] Render the tabs with frames
   */
  OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
  	// Configuration initialization
  	config = $.extend( {}, config, { menuPosition: 'top' } );
  
  	// Parent constructor
  	OO.ui.IndexLayout.super.call( this, config );
  
  	// Properties
  	this.currentTabPanelName = null;
  	// Allow infused widgets to pass existing tabPanels
  	this.tabPanels = config.tabPanels || {};
  
  	this.ignoreFocus = false;
  	this.stackLayout = this.contentPanel || new OO.ui.StackLayout( {
  		continuous: !!config.continuous,
  		expanded: this.expanded
  	} );
  	this.setContentPanel( this.stackLayout );
  	this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
  
  	// Allow infused widgets to pass an existing tabSelectWidget
  	this.tabSelectWidget = config.tabSelectWidget || new OO.ui.TabSelectWidget( {
  		framed: config.framed === undefined || config.framed
  	} );
  	this.tabPanel = this.menuPanel || new OO.ui.PanelLayout( {
  		expanded: this.expanded
  	} );
  	this.setMenuPanel( this.tabPanel );
  
  	this.toggleMenu( true );
  
  	// Events
  	this.stackLayout.connect( this, {
  		set: 'onStackLayoutSet'
  	} );
  	this.tabSelectWidget.connect( this, {
  		select: 'onTabSelectWidgetSelect'
  	} );
  	if ( this.autoFocus ) {
  		// Event 'focus' does not bubble, but 'focusin' does.
  		this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
  	}
  
  	// Initialization
  	this.$element.addClass( 'oo-ui-indexLayout' );
  	this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
  	this.tabPanel.$element
  		.addClass( 'oo-ui-indexLayout-tabPanel' )
  		.append( this.tabSelectWidget.$element );
  
  	this.selectFirstSelectableTabPanel();
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
  
  /* Events */
  
  /**
   * A 'set' event is emitted when a tab panel is {@link #setTabPanel set} to be displayed by the
   * index layout.
   *
   * @event set
   * @param {OO.ui.TabPanelLayout} tabPanel Current tab panel
   */
  
  /**
   * An 'add' event is emitted when tab panels are {@link #addTabPanels added} to the index layout.
   *
   * @event add
   * @param {OO.ui.TabPanelLayout[]} tabPanel Added tab panels
   * @param {number} index Index tab panels were added at
   */
  
  /**
   * A 'remove' event is emitted when tab panels are {@link #clearTabPanels cleared} or
   * {@link #removeTabPanels removed} from the index.
   *
   * @event remove
   * @param {OO.ui.TabPanelLayout[]} tabPanel Removed tab panels
   */
  
  /* Methods */
  
  /**
   * Handle stack layout focus.
   *
   * @private
   * @param {jQuery.Event} e Focusing event
   */
  OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
  	var name, $target;
  
  	// Find the tab panel that an element was focused within
  	$target = $( e.target ).closest( '.oo-ui-tabPanelLayout' );
  	for ( name in this.tabPanels ) {
  		// Check for tab panel match, exclude current tab panel to find only tab panel changes
  		if ( this.tabPanels[ name ].$element[ 0 ] === $target[ 0 ] &&
  				name !== this.currentTabPanelName ) {
  			this.setTabPanel( name );
  			break;
  		}
  	}
  };
  
  /**
   * Handle stack layout set events.
   *
   * @private
   * @param {OO.ui.PanelLayout|null} tabPanel The tab panel that is now the current panel
   */
  OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( tabPanel ) {
  	// If everything is unselected, do nothing
  	if ( !tabPanel ) {
  		return;
  	}
  	// Focus the first element on the newly selected panel
  	if ( this.autoFocus && !OO.ui.isMobile() ) {
  		this.focus();
  	}
  };
  
  /**
   * Focus the first input in the current tab panel.
   *
   * If no tab panel is selected, the first selectable tab panel will be selected.
   * If the focus is already in an element on the current tab panel, nothing will happen.
   *
   * @param {number} [itemIndex] A specific item to focus on
   */
  OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
  	var tabPanel,
  		items = this.stackLayout.getItems();
  
  	if ( itemIndex !== undefined && items[ itemIndex ] ) {
  		tabPanel = items[ itemIndex ];
  	} else {
  		tabPanel = this.stackLayout.getCurrentItem();
  	}
  
  	if ( !tabPanel ) {
  		this.selectFirstSelectableTabPanel();
  		tabPanel = this.stackLayout.getCurrentItem();
  	}
  	if ( !tabPanel ) {
  		return;
  	}
  	// Only change the focus if is not already in the current page
  	if ( !OO.ui.contains(
  		tabPanel.$element[ 0 ],
  		this.getElementDocument().activeElement,
  		true
  	) ) {
  		tabPanel.focus();
  	}
  };
  
  /**
   * Find the first focusable input in the index layout and focus
   * on it.
   */
  OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
  	OO.ui.findFocusable( this.stackLayout.$element ).focus();
  };
  
  /**
   * Handle tab widget select events.
   *
   * @private
   * @param {OO.ui.OptionWidget|null} item Selected item
   */
  OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
  	if ( item ) {
  		this.setTabPanel( item.getData() );
  	}
  };
  
  /**
   * Get the tab panel closest to the specified tab panel.
   *
   * @param {OO.ui.TabPanelLayout} tabPanel Tab panel to use as a reference point
   * @return {OO.ui.TabPanelLayout|null} Tab panel closest to the specified
   */
  OO.ui.IndexLayout.prototype.getClosestTabPanel = function ( tabPanel ) {
  	var next, prev, level,
  		tabPanels = this.stackLayout.getItems(),
  		index = tabPanels.indexOf( tabPanel );
  
  	if ( index !== -1 ) {
  		next = tabPanels[ index + 1 ];
  		prev = tabPanels[ index - 1 ];
  		// Prefer adjacent tab panels at the same level
  		level = this.tabSelectWidget.findItemFromData( tabPanel.getName() ).getLevel();
  		if (
  			prev &&
  			level === this.tabSelectWidget.findItemFromData( prev.getName() ).getLevel()
  		) {
  			return prev;
  		}
  		if (
  			next &&
  			level === this.tabSelectWidget.findItemFromData( next.getName() ).getLevel()
  		) {
  			return next;
  		}
  	}
  	return prev || next || null;
  };
  
  /**
   * Get the tabs widget.
   *
   * @return {OO.ui.TabSelectWidget} Tabs widget
   */
  OO.ui.IndexLayout.prototype.getTabs = function () {
  	return this.tabSelectWidget;
  };
  
  /**
   * Get a tab panel by its symbolic name.
   *
   * @param {string} name Symbolic name of tab panel
   * @return {OO.ui.TabPanelLayout|undefined} Tab panel, if found
   */
  OO.ui.IndexLayout.prototype.getTabPanel = function ( name ) {
  	return this.tabPanels[ name ];
  };
  
  /**
   * Get the current tab panel.
   *
   * @return {OO.ui.TabPanelLayout|undefined} Current tab panel, if found
   */
  OO.ui.IndexLayout.prototype.getCurrentTabPanel = function () {
  	var name = this.getCurrentTabPanelName();
  	return name ? this.getTabPanel( name ) : undefined;
  };
  
  /**
   * Get the symbolic name of the current tab panel.
   *
   * @return {string|null} Symbolic name of the current tab panel
   */
  OO.ui.IndexLayout.prototype.getCurrentTabPanelName = function () {
  	return this.currentTabPanelName;
  };
  
  /**
   * Add tab panels to the index layout.
   *
   * When tab panels are added with the same names as existing tab panels, the existing tab panels
   * will be automatically removed before the new tab panels are added.
   *
   * @param {OO.ui.TabPanelLayout[]} tabPanels Tab panels to add
   * @param {number} index Index of the insertion point
   * @fires add
   * @chainable
   * @return {OO.ui.IndexLayout} The layout, for chaining
   */
  OO.ui.IndexLayout.prototype.addTabPanels = function ( tabPanels, index ) {
  	var i, len, name, tabPanel, tabItem, currentIndex,
  		stackLayoutTabPanels = this.stackLayout.getItems(),
  		remove = [],
  		tabItems = [];
  
  	// Remove tab panels with same names
  	for ( i = 0, len = tabPanels.length; i < len; i++ ) {
  		tabPanel = tabPanels[ i ];
  		name = tabPanel.getName();
  
  		if ( Object.prototype.hasOwnProperty.call( this.tabPanels, name ) ) {
  			// Correct the insertion index
  			currentIndex = stackLayoutTabPanels.indexOf( this.tabPanels[ name ] );
  			if ( currentIndex !== -1 && currentIndex + 1 < index ) {
  				index--;
  			}
  			remove.push( this.tabPanels[ name ] );
  		}
  	}
  	if ( remove.length ) {
  		this.removeTabPanels( remove );
  	}
  
  	// Add new tab panels
  	for ( i = 0, len = tabPanels.length; i < len; i++ ) {
  		tabPanel = tabPanels[ i ];
  		name = tabPanel.getName();
  		this.tabPanels[ name ] = tabPanel;
  		tabItem = new OO.ui.TabOptionWidget(
  			$.extend( { data: name }, tabPanel.getTabItemConfig() )
  		);
  		tabPanel.setTabItem( tabItem );
  		tabItems.push( tabItem );
  	}
  
  	if ( tabItems.length ) {
  		this.tabSelectWidget.addItems( tabItems, index );
  		this.selectFirstSelectableTabPanel();
  	}
  	this.stackLayout.addItems( tabPanels, index );
  	this.emit( 'add', tabPanels, index );
  
  	return this;
  };
  
  /**
   * Remove the specified tab panels from the index layout.
   *
   * To remove all tab panels from the index, you may wish to use the #clearTabPanels method instead.
   *
   * @param {OO.ui.TabPanelLayout[]} tabPanels An array of tab panels to remove
   * @fires remove
   * @chainable
   * @return {OO.ui.IndexLayout} The layout, for chaining
   */
  OO.ui.IndexLayout.prototype.removeTabPanels = function ( tabPanels ) {
  	var i, len, name, tabPanel,
  		items = [];
  
  	for ( i = 0, len = tabPanels.length; i < len; i++ ) {
  		tabPanel = tabPanels[ i ];
  		name = tabPanel.getName();
  		delete this.tabPanels[ name ];
  		items.push( this.tabSelectWidget.findItemFromData( name ) );
  		tabPanel.setTabItem( null );
  	}
  	if ( items.length ) {
  		this.tabSelectWidget.removeItems( items );
  		this.selectFirstSelectableTabPanel();
  	}
  	this.stackLayout.removeItems( tabPanels );
  	this.emit( 'remove', tabPanels );
  
  	return this;
  };
  
  /**
   * Clear all tab panels from the index layout.
   *
   * To remove only a subset of tab panels from the index, use the #removeTabPanels method.
   *
   * @fires remove
   * @chainable
   * @return {OO.ui.IndexLayout} The layout, for chaining
   */
  OO.ui.IndexLayout.prototype.clearTabPanels = function () {
  	var i, len,
  		tabPanels = this.stackLayout.getItems();
  
  	this.tabPanels = {};
  	this.currentTabPanelName = null;
  	this.tabSelectWidget.clearItems();
  	for ( i = 0, len = tabPanels.length; i < len; i++ ) {
  		tabPanels[ i ].setTabItem( null );
  	}
  	this.stackLayout.clearItems();
  
  	this.emit( 'remove', tabPanels );
  
  	return this;
  };
  
  /**
   * Set the current tab panel by symbolic name.
   *
   * @fires set
   * @param {string} name Symbolic name of tab panel
   */
  OO.ui.IndexLayout.prototype.setTabPanel = function ( name ) {
  	var selectedItem,
  		$focused,
  		previousTabPanel,
  		tabPanel;
  
  	if ( name !== this.currentTabPanelName ) {
  		tabPanel = this.getTabPanel( name );
  		previousTabPanel = this.getCurrentTabPanel();
  		selectedItem = this.tabSelectWidget.findSelectedItem();
  		if ( !selectedItem || selectedItem.getData() !== name ) {
  			this.tabSelectWidget.selectItemByData( name );
  		}
  		if ( tabPanel ) {
  			if ( previousTabPanel ) {
  				previousTabPanel.setActive( false );
  				// Blur anything focused if the next tab panel doesn't have anything focusable.
  				// This is not needed if the next tab panel has something focusable (because once
  				// it is focused this blur happens automatically). If the layout is non-continuous,
  				// this check is meaningless because the next tab panel is not visible yet and thus
  				// can't hold focus.
  				if (
  					this.autoFocus &&
  					!OO.ui.isMobile() &&
  					this.stackLayout.continuous &&
  					OO.ui.findFocusable( tabPanel.$element ).length !== 0
  				) {
  					$focused = previousTabPanel.$element.find( ':focus' );
  					if ( $focused.length ) {
  						$focused[ 0 ].blur();
  					}
  				}
  			}
  			this.currentTabPanelName = name;
  			tabPanel.setActive( true );
  			this.stackLayout.setItem( tabPanel );
  			if ( !this.stackLayout.continuous && previousTabPanel ) {
  				// This should not be necessary, since any inputs on the previous tab panel should
  				// have been blurred when it was hidden, but browsers are not very consistent about
  				// this.
  				$focused = previousTabPanel.$element.find( ':focus' );
  				if ( $focused.length ) {
  					$focused[ 0 ].blur();
  				}
  			}
  			this.emit( 'set', tabPanel );
  		}
  	}
  };
  
  /**
   * Select the first selectable tab panel.
   *
   * @chainable
   * @return {OO.ui.IndexLayout} The layout, for chaining
   */
  OO.ui.IndexLayout.prototype.selectFirstSelectableTabPanel = function () {
  	if ( !this.tabSelectWidget.findSelectedItem() ) {
  		this.tabSelectWidget.selectItem( this.tabSelectWidget.findFirstSelectableItem() );
  	}
  
  	return this;
  };
  
  /**
   * ToggleWidget implements basic behavior of widgets with an on/off state.
   * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
   *
   * @abstract
   * @class
   * @extends OO.ui.Widget
   * @mixins OO.ui.mixin.TitledElement
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {boolean} [value=false] The toggle’s initial on/off state.
   *  By default, the toggle is in the 'off' state.
   */
  OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
  	// Configuration initialization
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.ToggleWidget.super.call( this, config );
  
  	// Mixin constructor
  	OO.ui.mixin.TitledElement.call( this, config );
  
  	// Properties
  	this.value = null;
  
  	// Initialization
  	this.$element.addClass( 'oo-ui-toggleWidget' );
  	this.setValue( !!config.value );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
  OO.mixinClass( OO.ui.ToggleWidget, OO.ui.mixin.TitledElement );
  
  /* Events */
  
  /**
   * @event change
   *
   * A change event is emitted when the on/off state of the toggle changes.
   *
   * @param {boolean} value Value representing the new state of the toggle
   */
  
  /* Methods */
  
  /**
   * Get the value representing the toggle’s state.
   *
   * @return {boolean} The on/off state of the toggle
   */
  OO.ui.ToggleWidget.prototype.getValue = function () {
  	return this.value;
  };
  
  /**
   * Set the state of the toggle: `true` for 'on', `false` for 'off'.
   *
   * @param {boolean} value The state of the toggle
   * @fires change
   * @chainable
   * @return {OO.ui.Widget} The widget, for chaining
   */
  OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
  	value = !!value;
  	if ( this.value !== value ) {
  		this.value = value;
  		this.emit( 'change', value );
  		this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
  		this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
  	}
  	return this;
  };
  
  /**
   * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
   * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
   * configured with {@link OO.ui.mixin.IconElement icons},
   * {@link OO.ui.mixin.IndicatorElement indicators},
   * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
   * and {@link OO.ui.mixin.LabelElement labels}. Please see
   * the [OOUI documentation][1] on MediaWiki for more information.
   *
   *     @example
   *     // Toggle buttons in the 'off' and 'on' state.
   *     var toggleButton1 = new OO.ui.ToggleButtonWidget( {
   *             label: 'Toggle Button off'
   *         } ),
   *         toggleButton2 = new OO.ui.ToggleButtonWidget( {
   *             label: 'Toggle Button on',
   *             value: true
   *         } );
   *     // Append the buttons to the DOM.
   *     $( document.body ).append( toggleButton1.$element, toggleButton2.$element );
   *
   * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Toggle_buttons
   *
   * @class
   * @extends OO.ui.ToggleWidget
   * @mixins OO.ui.mixin.ButtonElement
   * @mixins OO.ui.mixin.IconElement
   * @mixins OO.ui.mixin.IndicatorElement
   * @mixins OO.ui.mixin.LabelElement
   * @mixins OO.ui.mixin.FlaggedElement
   * @mixins OO.ui.mixin.TabIndexedElement
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {boolean} [value=false] The toggle button’s initial on/off
   *  state. By default, the button is in the 'off' state.
   */
  OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
  	// Configuration initialization
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.ToggleButtonWidget.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.ButtonElement.call( this, $.extend( {
  		active: this.active
  	}, config ) );
  	OO.ui.mixin.IconElement.call( this, config );
  	OO.ui.mixin.IndicatorElement.call( this, config );
  	OO.ui.mixin.LabelElement.call( this, config );
  	OO.ui.mixin.FlaggedElement.call( this, config );
  	OO.ui.mixin.TabIndexedElement.call( this, $.extend( {
  		$tabIndexed: this.$button
  	}, config ) );
  
  	// Events
  	this.connect( this, {
  		click: 'onAction'
  	} );
  
  	// Initialization
  	this.$button.append( this.$icon, this.$label, this.$indicator );
  	this.$element
  		.addClass( 'oo-ui-toggleButtonWidget' )
  		.append( this.$button );
  	this.setTitledElement( this.$button );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
  OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.ButtonElement );
  OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IconElement );
  OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IndicatorElement );
  OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.LabelElement );
  OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.FlaggedElement );
  OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TabIndexedElement );
  
  /* Static Properties */
  
  /**
   * @static
   * @inheritdoc
   */
  OO.ui.ToggleButtonWidget.static.tagName = 'span';
  
  /* Methods */
  
  /**
   * Handle the button action being triggered.
   *
   * @private
   */
  OO.ui.ToggleButtonWidget.prototype.onAction = function () {
  	this.setValue( !this.value );
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
  	value = !!value;
  	if ( value !== this.value ) {
  		// Might be called from parent constructor before ButtonElement constructor
  		if ( this.$button ) {
  			this.$button.attr( 'aria-pressed', value.toString() );
  		}
  		this.setActive( value );
  	}
  
  	// Parent method
  	OO.ui.ToggleButtonWidget.super.prototype.setValue.call( this, value );
  
  	return this;
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
  	if ( this.$button ) {
  		this.$button.removeAttr( 'aria-pressed' );
  	}
  	OO.ui.mixin.ButtonElement.prototype.setButtonElement.call( this, $button );
  	this.$button.attr( 'aria-pressed', this.value.toString() );
  };
  
  /**
   * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
   * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
   * visually by a slider in the leftmost position.
   *
   *     @example
   *     // Toggle switches in the 'off' and 'on' position.
   *     var toggleSwitch1 = new OO.ui.ToggleSwitchWidget(),
   *         toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
   *             value: true
   *         } );
   *         // Create a FieldsetLayout to layout and label switches.
   *         fieldset = new OO.ui.FieldsetLayout( {
   *             label: 'Toggle switches'
   *         } );
   *     fieldset.addItems( [
   *         new OO.ui.FieldLayout( toggleSwitch1, {
   *             label: 'Off',
   *             align: 'top'
   *         } ),
   *         new OO.ui.FieldLayout( toggleSwitch2, {
   *             label: 'On',
   *             align: 'top'
   *         } )
   *     ] );
   *     $( document.body ).append( fieldset.$element );
   *
   * @class
   * @extends OO.ui.ToggleWidget
   * @mixins OO.ui.mixin.TabIndexedElement
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
   *  By default, the toggle switch is in the 'off' position.
   */
  OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
  	// Parent constructor
  	OO.ui.ToggleSwitchWidget.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.TabIndexedElement.call( this, config );
  
  	// Properties
  	this.dragging = false;
  	this.dragStart = null;
  	this.sliding = false;
  	this.$glow = $( '<span>' );
  	this.$grip = $( '<span>' );
  
  	// Events
  	this.$element.on( {
  		click: this.onClick.bind( this ),
  		keypress: this.onKeyPress.bind( this )
  	} );
  
  	// Initialization
  	this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
  	this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
  	this.$element
  		.addClass( 'oo-ui-toggleSwitchWidget' )
  		.attr( 'role', 'switch' )
  		.append( this.$glow, this.$grip );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
  OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
  
  /* Methods */
  
  /**
   * Handle mouse click events.
   *
   * @private
   * @param {jQuery.Event} e Mouse click event
   * @return {undefined|boolean} False to prevent default if event is handled
   */
  OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
  	if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
  		this.setValue( !this.value );
  	}
  	return false;
  };
  
  /**
   * Handle key press events.
   *
   * @private
   * @param {jQuery.Event} e Key press event
   * @return {undefined|boolean} False to prevent default if event is handled
   */
  OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
  	if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
  		this.setValue( !this.value );
  		return false;
  	}
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.ToggleSwitchWidget.prototype.setValue = function ( value ) {
  	OO.ui.ToggleSwitchWidget.super.prototype.setValue.call( this, value );
  	this.$element.attr( 'aria-checked', this.value.toString() );
  	return this;
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.ToggleSwitchWidget.prototype.simulateLabelClick = function () {
  	if ( !this.isDisabled() ) {
  		this.setValue( !this.value );
  	}
  	this.focus();
  };
  
  /**
   * OutlineControlsWidget is a set of controls for an
   * {@link OO.ui.OutlineSelectWidget outline select widget}.
   * Controls include moving items up and down, removing items, and adding different kinds of items.
   *
   * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
   *
   * @class
   * @extends OO.ui.Widget
   * @mixins OO.ui.mixin.GroupElement
   *
   * @constructor
   * @param {OO.ui.OutlineSelectWidget} outline Outline to control
   * @param {Object} [config] Configuration options
   * @cfg {Object} [abilities] List of abilties
   * @cfg {boolean} [abilities.move=true] Allow moving movable items
   * @cfg {boolean} [abilities.remove=true] Allow removing removable items
   */
  OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
  	// Allow passing positional parameters inside the config object
  	if ( OO.isPlainObject( outline ) && config === undefined ) {
  		config = outline;
  		outline = config.outline;
  	}
  
  	// Configuration initialization
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.OutlineControlsWidget.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.GroupElement.call( this, config );
  
  	// Properties
  	this.outline = outline;
  	this.$movers = $( '<div>' );
  	this.upButton = new OO.ui.ButtonWidget( {
  		framed: false,
  		icon: 'upTriangle',
  		title: OO.ui.msg( 'ooui-outline-control-move-up' )
  	} );
  	this.downButton = new OO.ui.ButtonWidget( {
  		framed: false,
  		icon: 'downTriangle',
  		title: OO.ui.msg( 'ooui-outline-control-move-down' )
  	} );
  	this.removeButton = new OO.ui.ButtonWidget( {
  		framed: false,
  		icon: 'trash',
  		title: OO.ui.msg( 'ooui-outline-control-remove' )
  	} );
  	this.abilities = { move: true, remove: true };
  
  	// Events
  	outline.connect( this, {
  		select: 'onOutlineChange',
  		add: 'onOutlineChange',
  		remove: 'onOutlineChange'
  	} );
  	this.upButton.connect( this, {
  		click: [ 'emit', 'move', -1 ]
  	} );
  	this.downButton.connect( this, {
  		click: [ 'emit', 'move', 1 ]
  	} );
  	this.removeButton.connect( this, {
  		click: [ 'emit', 'remove' ]
  	} );
  
  	// Initialization
  	this.$element.addClass( 'oo-ui-outlineControlsWidget' );
  	this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
  	this.$movers
  		.addClass( 'oo-ui-outlineControlsWidget-movers' )
  		.append( this.upButton.$element, this.downButton.$element, this.removeButton.$element );
  	this.$element.append( this.$icon, this.$group, this.$movers );
  	this.setAbilities( config.abilities || {} );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
  OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.GroupElement );
  
  /* Events */
  
  /**
   * @event move
   * @param {number} places Number of places to move
   */
  
  /**
   * @event remove
   */
  
  /* Methods */
  
  /**
   * Set abilities.
   *
   * @param {Object} abilities List of abilties
   * @param {boolean} [abilities.move] Allow moving movable items
   * @param {boolean} [abilities.remove] Allow removing removable items
   */
  OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
  	var ability;
  
  	for ( ability in this.abilities ) {
  		if ( abilities[ ability ] !== undefined ) {
  			this.abilities[ ability ] = !!abilities[ ability ];
  		}
  	}
  
  	this.onOutlineChange();
  };
  
  /**
   * Handle outline change events.
   *
   * @private
   */
  OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
  	var i, len, firstMovable, lastMovable,
  		items = this.outline.getItems(),
  		selectedItem = this.outline.findSelectedItem(),
  		movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
  		removable = this.abilities.remove && selectedItem && selectedItem.isRemovable();
  
  	if ( movable ) {
  		i = -1;
  		len = items.length;
  		while ( ++i < len ) {
  			if ( items[ i ].isMovable() ) {
  				firstMovable = items[ i ];
  				break;
  			}
  		}
  		i = len;
  		while ( i-- ) {
  			if ( items[ i ].isMovable() ) {
  				lastMovable = items[ i ];
  				break;
  			}
  		}
  	}
  	this.upButton.setDisabled( !movable || selectedItem === firstMovable );
  	this.downButton.setDisabled( !movable || selectedItem === lastMovable );
  	this.removeButton.setDisabled( !removable );
  };
  
  /**
   * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
   *
   * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
   * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
   * for an example.
   *
   * @class
   * @extends OO.ui.DecoratedOptionWidget
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {number} [level=0] Indentation level
   * @cfg {boolean} [movable=false] Allow modification from
   *  {@link OO.ui.OutlineControlsWidget outline controls}.
   * @cfg {boolean} [removable=false]
   */
  OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
  	// Configuration initialization
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.OutlineOptionWidget.super.call( this, config );
  
  	// Properties
  	this.movable = !!config.movable;
  	this.removable = !!config.removable;
  
  	// Initialization
  	this.$element.addClass( 'oo-ui-outlineOptionWidget' );
  	this.setLevel( config.level );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
  
  /* Static Properties */
  
  /**
   * @static
   * @inheritdoc
   */
  OO.ui.OutlineOptionWidget.static.highlightable = true;
  
  /**
   * @static
   * @inheritdoc
   */
  OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
  
  /**
   * @static
   * @inheritable
   * @property {string}
   */
  OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
  
  /**
   * @static
   * @inheritable
   * @property {number}
   */
  OO.ui.OutlineOptionWidget.static.levels = 3;
  
  /* Methods */
  
  /**
   * Check if item is movable.
   *
   * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
   *
   * @return {boolean} Item is movable
   */
  OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
  	return this.movable;
  };
  
  /**
   * Check if item is removable.
   *
   * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
   *
   * @return {boolean} Item is removable
   */
  OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
  	return this.removable;
  };
  
  /**
   * Get indentation level.
   *
   * @return {number} Indentation level
   */
  OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
  	return this.level;
  };
  
  /**
   * Set movability.
   *
   * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
   *
   * @param {boolean} movable Item is movable
   * @chainable
   * @return {OO.ui.Widget} The widget, for chaining
   */
  OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
  	this.movable = !!movable;
  	this.updateThemeClasses();
  	return this;
  };
  
  /**
   * Set removability.
   *
   * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
   *
   * @param {boolean} removable Item is removable
   * @chainable
   * @return {OO.ui.Widget} The widget, for chaining
   */
  OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
  	this.removable = !!removable;
  	this.updateThemeClasses();
  	return this;
  };
  
  /**
   * Set indentation level.
   *
   * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
   * @chainable
   * @return {OO.ui.Widget} The widget, for chaining
   */
  OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
  	if ( this.level === level ) {
  		return this;
  	}
  
  	var levels = this.constructor.static.levels,
  		levelClass = this.constructor.static.levelClass;
  
  	if ( this.level !== undefined ) {
  		this.$element.removeClass( levelClass + this.level );
  	}
  	this.level = level > 0 ? Math.min( level, levels - 1 ) : 0;
  	this.$element.addClass( levelClass + this.level );
  	this.updateThemeClasses();
  
  	return this;
  };
  
  /**
   * OutlineSelectWidget is a structured list that contains
   * {@link OO.ui.OutlineOptionWidget outline options}
   * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls}
   * widget.
   *
   * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
   *
   * @class
   * @extends OO.ui.SelectWidget
   * @mixins OO.ui.mixin.TabIndexedElement
   *
   * @constructor
   * @param {Object} [config] Configuration options
   */
  OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
  	// Parent constructor
  	OO.ui.OutlineSelectWidget.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.TabIndexedElement.call( this, config );
  
  	// Events
  	this.$element.on( {
  		focus: this.bindDocumentKeyDownListener.bind( this ),
  		blur: this.unbindDocumentKeyDownListener.bind( this )
  	} );
  
  	// Initialization
  	this.$element.addClass( 'oo-ui-outlineSelectWidget' );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
  OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
  
  /**
   * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
   * can be selected and configured with data. The class is
   * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
   * [OOUI documentation on MediaWiki] [1] for more information.
   *
   * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_options
   *
   * @class
   * @extends OO.ui.OptionWidget
   * @mixins OO.ui.mixin.ButtonElement
   * @mixins OO.ui.mixin.IconElement
   * @mixins OO.ui.mixin.IndicatorElement
   *
   * @constructor
   * @param {Object} [config] Configuration options
   */
  OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
  	// Configuration initialization
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.ButtonOptionWidget.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.ButtonElement.call( this, config );
  	OO.ui.mixin.IconElement.call( this, config );
  	OO.ui.mixin.IndicatorElement.call( this, config );
  
  	// Initialization
  	this.$element.addClass( 'oo-ui-buttonOptionWidget' );
  	this.$button.append( this.$icon, this.$label, this.$indicator );
  	this.$element.append( this.$button );
  	this.setTitledElement( this.$button );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.OptionWidget );
  OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.ButtonElement );
  OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.IconElement );
  OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.IndicatorElement );
  
  /* Static Properties */
  
  /**
   * Allow button mouse down events to pass through so they can be handled by the parent select widget
   *
   * @static
   * @inheritdoc
   */
  OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
  
  /**
   * @static
   * @inheritdoc
   */
  OO.ui.ButtonOptionWidget.static.highlightable = false;
  
  /* Methods */
  
  /**
   * @inheritdoc
   */
  OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
  	OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
  
  	if ( this.constructor.static.selectable ) {
  		this.setActive( state );
  	}
  
  	return this;
  };
  
  /**
   * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
   * button options and is used together with
   * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
   * highlighting, choosing, and selecting mutually exclusive options. Please see
   * the [OOUI documentation on MediaWiki] [1] for more information.
   *
   *     @example
   *     // A ButtonSelectWidget that contains three ButtonOptionWidgets.
   *     var option1 = new OO.ui.ButtonOptionWidget( {
   *             data: 1,
   *             label: 'Option 1',
   *             title: 'Button option 1'
   *         } ),
   *         option2 = new OO.ui.ButtonOptionWidget( {
   *             data: 2,
   *             label: 'Option 2',
   *             title: 'Button option 2'
   *         } ),
   *         option3 = new OO.ui.ButtonOptionWidget( {
   *             data: 3,
   *             label: 'Option 3',
   *             title: 'Button option 3'
   *         } ),
   *         buttonSelect = new OO.ui.ButtonSelectWidget( {
   *             items: [ option1, option2, option3 ]
   *         } );
   *     $( document.body ).append( buttonSelect.$element );
   *
   * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
   *
   * @class
   * @extends OO.ui.SelectWidget
   * @mixins OO.ui.mixin.TabIndexedElement
   *
   * @constructor
   * @param {Object} [config] Configuration options
   */
  OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
  	// Parent constructor
  	OO.ui.ButtonSelectWidget.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.TabIndexedElement.call( this, config );
  
  	// Events
  	this.$element.on( {
  		focus: this.bindDocumentKeyDownListener.bind( this ),
  		blur: this.unbindDocumentKeyDownListener.bind( this )
  	} );
  
  	// Initialization
  	this.$element.addClass( 'oo-ui-buttonSelectWidget' );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
  OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
  
  /**
   * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
   *
   * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
   * {@link OO.ui.TabPanelLayout tab panel layouts}. See {@link OO.ui.IndexLayout IndexLayout}
   * for an example.
   *
   * @class
   * @extends OO.ui.OptionWidget
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {string} [href] Hyperlink to add to TabOption. Mostly used in OOUI PHP.
   */
  OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
  	// Configuration initialization
  	config = config || {};
  
  	if ( config.href ) {
  		config = $.extend( {
  			$label: $( '<a>' ).attr( 'href', config.href )
  		}, config );
  	}
  
  	// Parent constructor
  	OO.ui.TabOptionWidget.super.call( this, config );
  
  	// Initialization
  	this.$element
  		.addClass( 'oo-ui-tabOptionWidget' )
  		.attr( 'role', 'tab' );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
  
  /* Static Properties */
  
  /**
   * @static
   * @inheritdoc
   */
  OO.ui.TabOptionWidget.static.highlightable = false;
  
  /**
   * @static
   * @inheritdoc
   */
  OO.ui.TabOptionWidget.static.scrollIntoViewOnSelect = true;
  
  /**
   * Center tab horizontally after selecting on mobile
   *
   * @param {Object} [config] Configuration options
   * @return {jQuery.Promise} Promise which resolves when the scroll is complete
   */
  OO.ui.TabOptionWidget.prototype.scrollElementIntoView = function ( config ) {
  	var padding;
  	if ( !OO.ui.isMobile() || !this.getElementGroup() ) {
  		// Parent method
  		return OO.ui.TabOptionWidget.super.prototype.scrollElementIntoView.call( this );
  	} else {
  		padding = Math.max( (
  			this.getElementGroup().$element[ 0 ].clientWidth - this.$element[ 0 ].clientWidth
  		) / 2, 0 );
  		// Parent method
  		return OO.ui.TabOptionWidget.super.prototype.scrollElementIntoView.call( this, $.extend(
  			{
  				padding: {
  					left: padding,
  					right: padding
  				}
  			},
  			config
  		) );
  	}
  };
  
  /**
   * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
   *
   * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
   *
   * @class
   * @extends OO.ui.SelectWidget
   * @mixins OO.ui.mixin.TabIndexedElement
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {boolean} [framed=true] Use framed tabs
   */
  OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
  	// Parent constructor
  	OO.ui.TabSelectWidget.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.TabIndexedElement.call( this, config );
  
  	// Events
  	this.$element.on( {
  		focus: this.bindDocumentKeyDownListener.bind( this ),
  		blur: this.unbindDocumentKeyDownListener.bind( this )
  	} );
  
  	// Initialization
  	this.$element
  		.addClass( 'oo-ui-tabSelectWidget' )
  		.attr( 'role', 'tablist' );
  
  	this.toggleFramed( config.framed === undefined || config.framed );
  
  	if ( OO.ui.isMobile() ) {
  		this.$element.addClass( 'oo-ui-tabSelectWidget-mobile' );
  	}
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
  OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
  
  /* Methods */
  
  /**
   * Check if tabs are framed.
   *
   * @return {boolean} Tabs are framed
   */
  OO.ui.TabSelectWidget.prototype.isFramed = function () {
  	return this.framed;
  };
  
  /**
   * Render the tabs with or without frames.
   *
   * @param {boolean} [framed] Make tabs framed, omit to toggle
   * @chainable
   * @return {OO.ui.Element} The element, for chaining
   */
  OO.ui.TabSelectWidget.prototype.toggleFramed = function ( framed ) {
  	framed = framed === undefined ? !this.framed : !!framed;
  	if ( framed !== this.framed ) {
  		this.framed = framed;
  		this.$element
  			.toggleClass( 'oo-ui-tabSelectWidget-frameless', !framed )
  			.toggleClass( 'oo-ui-tabSelectWidget-framed', framed );
  	}
  
  	return this;
  };
  
  /**
   * ButtonMenuSelectWidgets launch a menu of options created with OO.ui.MenuOptionWidget.
   * The ButtonMenuSelectWidget takes care of opening and displaying the menu so that
   * users can interact with it.
   *
   *     @example
   *     // A ButtonMenuSelectWidget with a menu that contains three options.
   *     var buttonMenu = new OO.ui.ButtonMenuSelectWidget( {
   *         icon: 'menu',
   *         menu: {
   *             items: [
   *                 new OO.ui.MenuOptionWidget( {
   *                     data: 'a',
   *                     label: 'First'
   *                 } ),
   *                 new OO.ui.MenuOptionWidget( {
   *                     data: 'b',
   *                     label: 'Second'
   *                 } ),
   *                 new OO.ui.MenuOptionWidget( {
   *                     data: 'c',
   *                     label: 'Third'
   *                 } )
   *             ]
   *         }
   *     } );
   *
   *     $( document.body ).append( buttonMenu.$element );
   *
   *     // When using the `clearOnSelect` option, listen to the `choose` event
   *     // to avoid getting the null select event.
   *     buttonMenu.getMenu().on( 'choose', function ( menuOption ) {
   *         console.log( menuOption.getData() );
   *     } );
   *
   * @class
   * @extends OO.ui.ButtonWidget
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {boolean} [clearOnSelect=true] Clear selection immediately after making it
   * @cfg {Object} [menu] Configuration options to pass to
   *  {@link OO.ui.MenuSelectWidget menu select widget}.
   * @cfg {jQuery|boolean} [$overlay] Render the menu into a separate layer. This configuration is
   *  useful in cases where the expanded menu is larger than its containing `<div>`. The specified
   *  overlay layer is usually on top of the containing `<div>` and has a larger area. By default,
   *  the menu uses relative positioning. Pass 'true' to use the default overlay.
   *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
   */
  OO.ui.ButtonMenuSelectWidget = function OoUiButtonMenuSelectWidget( config ) {
  	// Configuration initialization
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.ButtonMenuSelectWidget.super.call( this, config );
  
  	this.$overlay = ( config.$overlay === true ?
  		OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
  
  	// Properties
  	this.clearOnSelect = config.clearOnSelect !== false;
  	this.menu = new OO.ui.MenuSelectWidget( $.extend( {
  		widget: this,
  		$floatableContainer: this.$element
  	}, config.menu ) );
  
  	// Events
  	this.connect( this, {
  		click: 'onButtonMenuClick'
  	} );
  	this.getMenu().connect( this, {
  		select: 'onMenuSelect',
  		toggle: 'onMenuToggle'
  	} );
  
  	// Initialization
  	this.$button
  		.attr( {
  			'aria-expanded': 'false',
  			'aria-haspopup': 'true',
  			'aria-owns': this.menu.getElementId()
  		} );
  	this.$element.addClass( 'oo-ui-buttonMenuSelectWidget' );
  	this.$overlay.append( this.menu.$element );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.ButtonMenuSelectWidget, OO.ui.ButtonWidget );
  
  /* Methods */
  
  /**
   * Get the menu.
   *
   * @return {OO.ui.MenuSelectWidget} Menu of widget
   */
  OO.ui.ButtonMenuSelectWidget.prototype.getMenu = function () {
  	return this.menu;
  };
  
  /**
   * Handle menu select events.
   *
   * @private
   * @param {OO.ui.MenuOptionWidget} item Selected menu item
   */
  OO.ui.ButtonMenuSelectWidget.prototype.onMenuSelect = function ( item ) {
  	if ( this.clearOnSelect && item ) {
  		// This will cause an additional 'select' event to fire, so
  		// users should probably listen to the 'choose' event.
  		this.getMenu().selectItem();
  	}
  };
  
  /**
   * Handle menu toggle events.
   *
   * @private
   * @param {boolean} isVisible Open state of the menu
   */
  OO.ui.ButtonMenuSelectWidget.prototype.onMenuToggle = function ( isVisible ) {
  	this.$element.toggleClass( 'oo-ui-buttonElement-pressed', isVisible );
  };
  
  /**
   * Handle mouse click events.
   *
   * @private
   */
  OO.ui.ButtonMenuSelectWidget.prototype.onButtonMenuClick = function () {
  	this.menu.toggle();
  };
  
  /**
   * TagItemWidgets are used within a {@link OO.ui.TagMultiselectWidget
   * TagMultiselectWidget} to display the selected items.
   *
   * @class
   * @extends OO.ui.Widget
   * @mixins OO.ui.mixin.ItemWidget
   * @mixins OO.ui.mixin.LabelElement
   * @mixins OO.ui.mixin.FlaggedElement
   * @mixins OO.ui.mixin.TabIndexedElement
   * @mixins OO.ui.mixin.DraggableElement
   *
   * @constructor
   * @param {Object} [config] Configuration object
   * @cfg {boolean} [valid=true] Item is valid
   * @cfg {boolean} [fixed=false] Item is fixed. This means the item is
   *  always included in the values and cannot be removed.
   */
  OO.ui.TagItemWidget = function OoUiTagItemWidget( config ) {
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.TagItemWidget.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.ItemWidget.call( this );
  	OO.ui.mixin.LabelElement.call( this, config );
  	OO.ui.mixin.FlaggedElement.call( this, config );
  	OO.ui.mixin.TabIndexedElement.call( this, config );
  	OO.ui.mixin.DraggableElement.call( this, config );
  
  	this.valid = config.valid === undefined ? true : !!config.valid;
  	this.fixed = !!config.fixed;
  
  	this.closeButton = new OO.ui.ButtonWidget( {
  		framed: false,
  		icon: 'close',
  		tabIndex: -1,
  		title: OO.ui.msg( 'ooui-item-remove' )
  	} );
  	this.closeButton.setDisabled( this.isDisabled() );
  
  	// Events
  	this.closeButton.connect( this, {
  		click: 'remove'
  	} );
  	this.$element
  		.on( 'click', this.select.bind( this ) )
  		.on( 'keydown', this.onKeyDown.bind( this ) )
  		// Prevent propagation of mousedown; the tag item "lives" in the
  		// clickable area of the TagMultiselectWidget, which listens to
  		// mousedown to open the menu or popup. We want to prevent that
  		// for clicks specifically on the tag itself, so the actions taken
  		// are more deliberate. When the tag is clicked, it will emit the
  		// selection event (similar to how #OO.ui.MultioptionWidget emits 'change')
  		// and can be handled separately.
  		.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
  
  	// Initialization
  	this.$element
  		.addClass( 'oo-ui-tagItemWidget' )
  		.append( this.$label, this.closeButton.$element );
  };
  
  /* Initialization */
  
  OO.inheritClass( OO.ui.TagItemWidget, OO.ui.Widget );
  OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.ItemWidget );
  OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.LabelElement );
  OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.FlaggedElement );
  OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.TabIndexedElement );
  OO.mixinClass( OO.ui.TagItemWidget, OO.ui.mixin.DraggableElement );
  
  /* Events */
  
  /**
   * @event remove
   *
   * A remove action was performed on the item
   */
  
  /**
   * @event navigate
   * @param {string} direction Direction of the movement, forward or backwards
   *
   * A navigate action was performed on the item
   */
  
  /**
   * @event select
   *
   * The tag widget was selected. This can occur when the widget
   * is either clicked or enter was pressed on it.
   */
  
  /**
   * @event valid
   * @param {boolean} isValid Item is valid
   *
   * Item validity has changed
   */
  
  /**
   * @event fixed
   * @param {boolean} isFixed Item is fixed
   *
   * Item fixed state has changed
   */
  
  /* Methods */
  
  /**
   * Set this item as fixed, meaning it cannot be removed
   *
   * @param {boolean} [state] Item is fixed, omit to toggle
   * @fires fixed
   * @return {OO.ui.Widget} The widget, for chaining
   */
  OO.ui.TagItemWidget.prototype.setFixed = function ( state ) {
  	state = state === undefined ? !this.fixed : !!state;
  
  	if ( this.fixed !== state ) {
  		this.fixed = state;
  		if ( this.closeButton ) {
  			this.closeButton.toggle( !this.fixed );
  		}
  
  		if ( !this.fixed && this.elementGroup && !this.elementGroup.isDraggable() ) {
  			// Only enable the state of the item if the
  			// entire group is draggable
  			this.toggleDraggable( !this.fixed );
  		}
  		this.$element.toggleClass( 'oo-ui-tagItemWidget-fixed', this.fixed );
  
  		this.emit( 'fixed', this.isFixed() );
  	}
  	return this;
  };
  
  /**
   * Check whether the item is fixed
   *
   * @return {boolean}
   */
  OO.ui.TagItemWidget.prototype.isFixed = function () {
  	return this.fixed;
  };
  
  /**
   * Handle removal of the item
   *
   * This is mainly for extensibility concerns, so other children
   * of this class can change the behavior if they need to. This
   * is called by both clicking the 'remove' button but also
   * on keypress, which is harder to override if needed.
   *
   * @fires remove
   */
  OO.ui.TagItemWidget.prototype.remove = function () {
  	if ( !this.isDisabled() && !this.isFixed() ) {
  		this.emit( 'remove' );
  	}
  };
  
  /**
   * Handle a keydown event on the widget
   *
   * @fires navigate
   * @fires remove
   * @param {jQuery.Event} e Key down event
   * @return {boolean|undefined} false to stop the operation
   */
  OO.ui.TagItemWidget.prototype.onKeyDown = function ( e ) {
  	var movement;
  
  	if (
  		!this.isDisabled() &&
  		!this.isFixed() &&
  		( e.keyCode === OO.ui.Keys.BACKSPACE || e.keyCode === OO.ui.Keys.DELETE )
  	) {
  		this.remove();
  		return false;
  	} else if ( e.keyCode === OO.ui.Keys.ENTER ) {
  		this.select();
  		return false;
  	} else if (
  		e.keyCode === OO.ui.Keys.LEFT ||
  		e.keyCode === OO.ui.Keys.RIGHT
  	) {
  		if ( OO.ui.Element.static.getDir( this.$element ) === 'rtl' ) {
  			movement = {
  				left: 'forwards',
  				right: 'backwards'
  			};
  		} else {
  			movement = {
  				left: 'backwards',
  				right: 'forwards'
  			};
  		}
  
  		this.emit(
  			'navigate',
  			e.keyCode === OO.ui.Keys.LEFT ?
  				movement.left : movement.right
  		);
  		return false;
  	}
  };
  
  /**
   * Select this item
   *
   * @fires select
   */
  OO.ui.TagItemWidget.prototype.select = function () {
  	if ( !this.isDisabled() ) {
  		this.emit( 'select' );
  	}
  };
  
  /**
   * Set the valid state of this item
   *
   * @param {boolean} [valid] Item is valid, omit to toggle
   * @fires valid
   */
  OO.ui.TagItemWidget.prototype.toggleValid = function ( valid ) {
  	valid = valid === undefined ? !this.valid : !!valid;
  
  	if ( this.valid !== valid ) {
  		this.valid = valid;
  
  		this.setFlags( { invalid: !this.valid } );
  
  		this.emit( 'valid', this.valid );
  	}
  };
  
  /**
   * Check whether the item is valid
   *
   * @return {boolean} Item is valid
   */
  OO.ui.TagItemWidget.prototype.isValid = function () {
  	return this.valid;
  };
  
  /**
   * A basic tag multiselect widget, similar in concept to
   * {@link OO.ui.ComboBoxInputWidget combo box widget} that allows the user to add multiple values
   * that are displayed in a tag area.
   *
   * This widget is a base widget; see {@link OO.ui.MenuTagMultiselectWidget MenuTagMultiselectWidget}
   * and {@link OO.ui.PopupTagMultiselectWidget PopupTagMultiselectWidget} for the implementations
   * that use a menu and a popup respectively.
   *
   *     @example
   *     // A TagMultiselectWidget.
   *     var widget = new OO.ui.TagMultiselectWidget( {
   *         inputPosition: 'outline',
   *         allowedValues: [ 'Option 1', 'Option 2', 'Option 3' ],
   *         selected: [ 'Option 1' ]
   *     } );
   *     $( document.body ).append( widget.$element );
   *
   * @class
   * @extends OO.ui.Widget
   * @mixins OO.ui.mixin.GroupWidget
   * @mixins OO.ui.mixin.DraggableGroupElement
   * @mixins OO.ui.mixin.IndicatorElement
   * @mixins OO.ui.mixin.IconElement
   * @mixins OO.ui.mixin.TabIndexedElement
   * @mixins OO.ui.mixin.FlaggedElement
   * @mixins OO.ui.mixin.TitledElement
   *
   * @constructor
   * @param {Object} config Configuration object
   * @cfg {Object} [input] Configuration options for the input widget
   * @cfg {OO.ui.InputWidget} [inputWidget] An optional input widget. If given, it will
   *  replace the input widget used in the TagMultiselectWidget. If not given,
   *  TagMultiselectWidget creates its own.
   * @cfg {boolean} [inputPosition='inline'] Position of the input. Options are:
   *  - inline: The input is invisible, but exists inside the tag list, so
   *    the user types into the tag groups to add tags.
   *  - outline: The input is underneath the tag area.
   *  - none: No input supplied
   * @cfg {boolean} [allowEditTags=true] Allow editing of the tags by clicking them
   * @cfg {boolean} [allowArbitrary=false] Allow data items to be added even if
   *  not present in the menu.
   * @cfg {Mixed[]} [allowedValues] An array representing the allowed items
   *  by their datas.
   * @cfg {boolean} [allowDuplicates=false] Allow duplicate items to be added
   * @cfg {boolean} [allowDisplayInvalidTags=false] Allow the display of
   *  invalid tags. These tags will display with an invalid state, and
   *  the widget as a whole will have an invalid state if any invalid tags
   *  are present.
   * @cfg {number} [tagLimit] An optional limit on the number of selected options.
   *  If 'tagLimit' is set and is reached, the input is disabled, not allowing any
   *  additions. If 'tagLimit' is unset or is 0, an unlimited number of items can be
   *  added.
   * @cfg {boolean} [allowReordering=true] Allow reordering of the items
   * @cfg {Object[]|string[]} [selected] A set of selected tags. If given,
   *  these will appear in the tag list on initialization, as long as they
   *  pass the validity tests.
   */
  OO.ui.TagMultiselectWidget = function OoUiTagMultiselectWidget( config ) {
  	var inputEvents,
  		rAF = window.requestAnimationFrame || setTimeout,
  		widget = this,
  		$tabFocus = $( '<span>' ).addClass( 'oo-ui-tagMultiselectWidget-focusTrap' );
  
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.TagMultiselectWidget.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.GroupWidget.call( this, config );
  	OO.ui.mixin.IndicatorElement.call( this, config );
  	OO.ui.mixin.IconElement.call( this, config );
  	OO.ui.mixin.TabIndexedElement.call( this, config );
  	OO.ui.mixin.FlaggedElement.call( this, config );
  	OO.ui.mixin.DraggableGroupElement.call( this, config );
  	OO.ui.mixin.TitledElement.call( this, config );
  
  	this.toggleDraggable(
  		config.allowReordering === undefined ?
  			true : !!config.allowReordering
  	);
  
  	this.inputPosition =
  		this.constructor.static.allowedInputPositions.indexOf( config.inputPosition ) > -1 ?
  			config.inputPosition : 'inline';
  	this.allowEditTags = config.allowEditTags === undefined ? true : !!config.allowEditTags;
  	this.allowArbitrary = !!config.allowArbitrary;
  	this.allowDuplicates = !!config.allowDuplicates;
  	this.allowedValues = config.allowedValues || [];
  	this.allowDisplayInvalidTags = config.allowDisplayInvalidTags;
  	this.hasInput = this.inputPosition !== 'none';
  	this.tagLimit = config.tagLimit;
  	this.height = null;
  	this.valid = true;
  
  	this.$content = $( '<div>' ).addClass( 'oo-ui-tagMultiselectWidget-content' );
  	this.$handle = $( '<div>' )
  		.addClass( 'oo-ui-tagMultiselectWidget-handle' )
  		.append(
  			this.$indicator,
  			this.$icon,
  			this.$content
  				.append(
  					this.$group.addClass( 'oo-ui-tagMultiselectWidget-group' )
  				)
  		);
  
  	// Events
  	this.aggregate( {
  		remove: 'itemRemove',
  		navigate: 'itemNavigate',
  		select: 'itemSelect',
  		fixed: 'itemFixed'
  	} );
  	this.connect( this, {
  		itemRemove: 'onTagRemove',
  		itemSelect: 'onTagSelect',
  		itemFixed: 'onTagFixed',
  		itemNavigate: 'onTagNavigate',
  		change: 'onChangeTags'
  	} );
  	this.$handle.on( {
  		mousedown: this.onMouseDown.bind( this )
  	} );
  
  	// Initialize
  	this.$element
  		.addClass( 'oo-ui-tagMultiselectWidget' )
  		.append( this.$handle );
  
  	if ( this.hasInput ) {
  		if ( config.inputWidget ) {
  			this.input = config.inputWidget;
  		} else {
  			this.input = new OO.ui.TextInputWidget( $.extend( {
  				placeholder: config.placeholder,
  				classes: [ 'oo-ui-tagMultiselectWidget-input' ]
  			}, config.input ) );
  		}
  		this.input.setDisabled( this.isDisabled() );
  
  		inputEvents = {
  			focus: this.onInputFocus.bind( this ),
  			blur: this.onInputBlur.bind( this ),
  			'propertychange change click mouseup keydown keyup input cut paste select focus':
  				OO.ui.debounce( this.updateInputSize.bind( this ) ),
  			keydown: this.onInputKeyDown.bind( this ),
  			keypress: this.onInputKeyPress.bind( this )
  		};
  
  		this.input.$input.on( inputEvents );
  		this.inputPlaceholder = this.input.$input.attr( 'placeholder' );
  
  		if ( this.inputPosition === 'outline' ) {
  			// Override max-height for the input widget
  			// in the case the widget is outline so it can
  			// stretch all the way if the widget is wide
  			this.input.$element.css( 'max-width', 'inherit' );
  			this.$element
  				.addClass( 'oo-ui-tagMultiselectWidget-outlined' )
  				.append( this.input.$element );
  		} else {
  			this.$element.addClass( 'oo-ui-tagMultiselectWidget-inlined' );
  			// HACK: When the widget is using 'inline' input, the
  			// behavior needs to only use the $input itself
  			// so we style and size it accordingly (otherwise
  			// the styling and sizing can get very convoluted
  			// when the wrapping divs and other elements)
  			// We are taking advantage of still being able to
  			// call the widget itself for operations like
  			// .getValue() and setDisabled() and .focus() but
  			// having only the $input attached to the DOM
  			this.$group.append( this.input.$input );
  		}
  	} else {
  		this.$content.append( $tabFocus );
  	}
  
  	this.setTabIndexedElement(
  		this.hasInput ?
  			this.input.$input :
  			$tabFocus
  	);
  
  	if ( config.selected ) {
  		this.setValue( config.selected );
  	}
  
  	// HACK: Input size needs to be calculated after everything
  	// else is rendered
  	rAF( function () {
  		if ( widget.hasInput ) {
  			widget.updateInputSize();
  		}
  	} );
  };
  
  /* Initialization */
  
  OO.inheritClass( OO.ui.TagMultiselectWidget, OO.ui.Widget );
  OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.GroupWidget );
  OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.DraggableGroupElement );
  OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.IndicatorElement );
  OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.IconElement );
  OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.TabIndexedElement );
  OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.FlaggedElement );
  OO.mixinClass( OO.ui.TagMultiselectWidget, OO.ui.mixin.TitledElement );
  
  /* Static properties */
  
  /**
   * Allowed input positions.
   * - inline: The input is inside the tag list
   * - outline: The input is under the tag list
   * - none: There is no input
   *
   * @property {Array}
   */
  OO.ui.TagMultiselectWidget.static.allowedInputPositions = [ 'inline', 'outline', 'none' ];
  
  /* Methods */
  
  /**
   * Handle mouse down events.
   *
   * @private
   * @param {jQuery.Event} e Mouse down event
   * @return {boolean} False to prevent defaults
   */
  OO.ui.TagMultiselectWidget.prototype.onMouseDown = function ( e ) {
  	if (
  		!this.isDisabled() &&
  		( !this.hasInput || e.target !== this.input.$input[ 0 ] ) &&
  		e.which === OO.ui.MouseButtons.LEFT
  	) {
  		this.focus();
  		return false;
  	}
  };
  
  /**
   * Handle key press events.
   *
   * @private
   * @param {jQuery.Event} e Key press event
   * @return {boolean} Whether to prevent defaults
   */
  OO.ui.TagMultiselectWidget.prototype.onInputKeyPress = function ( e ) {
  	var stopOrContinue,
  		withMetaKey = e.metaKey || e.ctrlKey;
  
  	if ( !this.isDisabled() ) {
  		if ( e.which === OO.ui.Keys.ENTER ) {
  			stopOrContinue = this.doInputEnter( e, withMetaKey );
  		}
  
  		this.updateInputSize();
  		return stopOrContinue;
  	}
  };
  
  /**
   * Handle key down events.
   *
   * @private
   * @param {jQuery.Event} e Key down event
   * @return {boolean}
   */
  OO.ui.TagMultiselectWidget.prototype.onInputKeyDown = function ( e ) {
  	var movement, direction,
  		widget = this,
  		withMetaKey = e.metaKey || e.ctrlKey;
  
  	function isMovementInsideInput( dir ) {
  		var inputRange = widget.input.getRange(),
  			inputValue = widget.hasInput && widget.input.getValue();
  
  		if ( dir === 'forwards' && inputRange.to > inputValue.length - 1 ) {
  			return false;
  		}
  
  		if ( dir === 'backwards' && inputRange.from <= 0 ) {
  			return false;
  		}
  
  		return true;
  	}
  
  	if ( !this.isDisabled() ) {
  		// 'keypress' event is not triggered for Backspace key
  		if ( e.keyCode === OO.ui.Keys.BACKSPACE ) {
  			return this.doInputBackspace( e, withMetaKey );
  		} else if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
  			return this.doInputEscape( e );
  		} else if (
  			e.keyCode === OO.ui.Keys.LEFT ||
  			e.keyCode === OO.ui.Keys.RIGHT
  		) {
  			if ( OO.ui.Element.static.getDir( this.$element ) === 'rtl' ) {
  				movement = {
  					left: 'forwards',
  					right: 'backwards'
  				};
  			} else {
  				movement = {
  					left: 'backwards',
  					right: 'forwards'
  				};
  			}
  			direction = e.keyCode === OO.ui.Keys.LEFT ?
  				movement.left : movement.right;
  
  			if ( !this.hasInput || !isMovementInsideInput( direction ) ) {
  				return this.doInputArrow( e, direction, withMetaKey );
  			}
  		}
  	}
  };
  
  /**
   * Respond to input focus event
   */
  OO.ui.TagMultiselectWidget.prototype.onInputFocus = function () {
  	this.$element.addClass( 'oo-ui-tagMultiselectWidget-focus' );
  	// Reset validity
  	this.toggleValid( true );
  };
  
  /**
   * Respond to input blur event
   */
  OO.ui.TagMultiselectWidget.prototype.onInputBlur = function () {
  	// Skip of blur was triggered by DOM re-ordering in onChangeTags
  	if ( this.changing ) {
  		return;
  	}
  
  	this.$element.removeClass( 'oo-ui-tagMultiselectWidget-focus' );
  
  	// Set the widget as invalid if there's text in the input
  	this.addTagFromInput();
  	this.toggleValid( this.checkValidity() && ( !this.hasInput || !this.input.getValue() ) );
  };
  
  /**
   * Perform an action after the Enter key on the input
   *
   * @param {jQuery.Event} e Event data
   * @param {boolean} [withMetaKey] Whether this key was pressed with
   * a meta key like Control
   * @return {boolean} Whether to prevent defaults
   */
  OO.ui.TagMultiselectWidget.prototype.doInputEnter = function () {
  	this.addTagFromInput();
  	return false;
  };
  
  /**
   * Perform an action responding to the Backspace key on the input
   *
   * @param {jQuery.Event} e Event data
   * @param {boolean} [withMetaKey] Whether this key was pressed with
   * a meta key like Control
   * @return {boolean} Whether to prevent defaults
   */
  OO.ui.TagMultiselectWidget.prototype.doInputBackspace = function ( e, withMetaKey ) {
  	var items, item;
  
  	if (
  		this.inputPosition === 'inline' &&
  		this.input.getValue() === '' &&
  		!this.isEmpty()
  	) {
  		// Delete the last item
  		items = this.getItems();
  		item = items[ items.length - 1 ];
  
  		if ( !item.isDisabled() && !item.isFixed() ) {
  			this.removeItems( [ item ] );
  			// If Ctrl/Cmd was pressed, delete item entirely.
  			// Otherwise put it into the text field for editing.
  			if ( !withMetaKey ) {
  				this.input.setValue( item.getLabel() );
  			}
  		}
  
  		return false;
  	}
  };
  
  /**
   * Perform an action after the Escape key on the input
   *
   * @param {jQuery.Event} e Event data
   */
  OO.ui.TagMultiselectWidget.prototype.doInputEscape = function () {
  	this.clearInput();
  };
  
  /**
   * Perform an action after the Left/Right arrow key on the input, select the previous
   * item from the input.
   * See #getPreviousItem
   *
   * @param {jQuery.Event} e Event data
   * @param {string} direction Direction of the movement; forwards or backwards
   * @param {boolean} [withMetaKey] Whether this key was pressed with
   *  a meta key like Control
   */
  OO.ui.TagMultiselectWidget.prototype.doInputArrow = function ( e, direction ) {
  	if (
  		this.inputPosition === 'inline' &&
  		!this.isEmpty() &&
  		direction === 'backwards'
  	) {
  		// Get previous item
  		this.getPreviousItem().focus();
  	}
  };
  
  /**
   * Respond to item select event
   *
   * @param {OO.ui.TagItemWidget} item Selected item
   */
  OO.ui.TagMultiselectWidget.prototype.onTagSelect = function ( item ) {
  	if ( this.hasInput && this.allowEditTags && !item.isFixed() ) {
  		if ( this.input.getValue() ) {
  			this.addTagFromInput();
  		}
  		// 1. Get the label of the tag into the input
  		this.input.setValue( item.getLabel() );
  		// 2. Remove the tag
  		this.removeItems( [ item ] );
  		// 3. Focus the input
  		this.focus();
  	}
  };
  
  /**
   * Respond to item fixed state change
   *
   * @param {OO.ui.TagItemWidget} item Selected item
   */
  OO.ui.TagMultiselectWidget.prototype.onTagFixed = function ( item ) {
  	var i,
  		items = this.getItems();
  
  	// Move item to the end of the static items
  	for ( i = 0; i < items.length; i++ ) {
  		if ( items[ i ] !== item && !items[ i ].isFixed() ) {
  			break;
  		}
  	}
  	this.addItems( item, i );
  };
  /**
   * Respond to change event, where items were added, removed, or cleared.
   */
  OO.ui.TagMultiselectWidget.prototype.onChangeTags = function () {
  	var hadFocus,
  		isUnderLimit = this.isUnderLimit();
  
  	this.changing = true;
  
  	// Reset validity
  	this.toggleValid( this.checkValidity() );
  
  	if ( this.hasInput ) {
  		this.updateInputSize();
  		if ( !isUnderLimit ) {
  			// Clear the input
  			this.input.setValue( '' );
  		}
  		if ( this.inputPosition === 'outline' ) {
  			// Show/clear the placeholder and enable/disable the input
  			// based on whether we are/aren't under the specified limit
  			this.input.$input.attr( 'placeholder', isUnderLimit ? this.inputPlaceholder : '' );
  			this.input.setDisabled( !isUnderLimit );
  		} else {
  			hadFocus = document.activeElement === this.input.$input[ 0 ];
  			// Move input to the end of the group
  			this.$group.append( this.input.$input );
  			// Show/hide the input
  			this.input.$input.toggleClass( 'oo-ui-element-hidden', !isUnderLimit );
  			if ( hadFocus && isUnderLimit ) {
  				this.input.focus();
  			}
  		}
  	}
  	this.updateIfHeightChanged();
  
  	this.changing = false;
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.TagMultiselectWidget.prototype.setDisabled = function ( isDisabled ) {
  	// Parent method
  	OO.ui.TagMultiselectWidget.super.prototype.setDisabled.call( this, isDisabled );
  
  	if ( this.hasInput && this.input ) {
  		if ( !isDisabled ) {
  			this.updateInputSize();
  		}
  		this.input.setDisabled( !!isDisabled || !this.isUnderLimit() );
  	}
  
  	if ( this.items ) {
  		this.getItems().forEach( function ( item ) {
  			item.setDisabled( !!isDisabled );
  		} );
  	}
  };
  
  /**
   * Respond to tag remove event
   *
   * @param {OO.ui.TagItemWidget} item Removed tag
   */
  OO.ui.TagMultiselectWidget.prototype.onTagRemove = function ( item ) {
  	this.removeTagByData( item.getData() );
  };
  
  /**
   * Respond to navigate event on the tag
   *
   * @param {OO.ui.TagItemWidget} item Removed tag
   * @param {string} direction Direction of movement; 'forwards' or 'backwards'
   */
  OO.ui.TagMultiselectWidget.prototype.onTagNavigate = function ( item, direction ) {
  	var firstItem = this.getItems()[ 0 ];
  
  	if ( direction === 'forwards' ) {
  		this.getNextItem( item ).focus();
  	} else if ( !this.inputPosition === 'inline' || item !== firstItem ) {
  		// If the widget has an inline input, we want to stop at the starting edge
  		// of the tags
  		this.getPreviousItem( item ).focus();
  	}
  };
  
  /**
   * Get data and label for a new tag from the input value
   *
   * @return {Object} The data and label for a tag
   */
  OO.ui.TagMultiselectWidget.prototype.getTagInfoFromInput = function () {
  	var val = this.input.getValue();
  	return { data: val, label: val };
  };
  
  /**
   * Add tag from input value
   */
  OO.ui.TagMultiselectWidget.prototype.addTagFromInput = function () {
  	var tagInfo = this.getTagInfoFromInput();
  
  	if ( !tagInfo.data ) {
  		return;
  	}
  
  	if ( this.addTag( tagInfo.data, tagInfo.label ) ) {
  		this.clearInput();
  	}
  };
  
  /**
   * Clear the input
   */
  OO.ui.TagMultiselectWidget.prototype.clearInput = function () {
  	this.input.setValue( '' );
  };
  
  /**
   * Check whether the given value is a duplicate of an existing
   * tag already in the list.
   *
   * @param {Mixed} data Requested value
   * @return {boolean} Value is duplicate
   */
  OO.ui.TagMultiselectWidget.prototype.isDuplicateData = function ( data ) {
  	return !!this.findItemFromData( data );
  };
  
  /**
   * Check whether a given value is allowed to be added
   *
   * @param {Mixed} data Requested value
   * @return {boolean} Value is allowed
   */
  OO.ui.TagMultiselectWidget.prototype.isAllowedData = function ( data ) {
  	if (
  		!this.allowDuplicates &&
  		this.isDuplicateData( data )
  	) {
  		return false;
  	}
  
  	if ( this.allowArbitrary ) {
  		return true;
  	}
  
  	// Check with allowed values
  	if (
  		this.getAllowedValues().some( function ( value ) {
  			return data === value;
  		} )
  	) {
  		return true;
  	}
  
  	return false;
  };
  
  /**
   * Get the allowed values list
   *
   * @return {Mixed[]} Allowed data values
   */
  OO.ui.TagMultiselectWidget.prototype.getAllowedValues = function () {
  	return this.allowedValues;
  };
  
  /**
   * Add a value to the allowed values list
   *
   * @param {Mixed} value Allowed data value
   */
  OO.ui.TagMultiselectWidget.prototype.addAllowedValue = function ( value ) {
  	if ( this.allowedValues.indexOf( value ) === -1 ) {
  		this.allowedValues.push( value );
  	}
  };
  
  /**
   * Get the datas of the currently selected items
   *
   * @return {Mixed[]} Datas of currently selected items
   */
  OO.ui.TagMultiselectWidget.prototype.getValue = function () {
  	return this.getItems()
  		.filter( function ( item ) {
  			return item.isValid();
  		} )
  		.map( function ( item ) {
  			return item.getData();
  		} );
  };
  
  /**
   * Set the value of this widget by datas.
   *
   * @param {string|string[]|Object|Object[]} valueObject An object representing the data
   *  and label of the value. If the widget allows arbitrary values,
   *  the items will be added as-is. Otherwise, the data value will
   *  be checked against allowedValues.
   *  This object must contain at least a data key. Example:
   *  { data: 'foo', label: 'Foo item' }
   *  For multiple items, use an array of objects. For example:
   *  [
   *     { data: 'foo', label: 'Foo item' },
   *     { data: 'bar', label: 'Bar item' }
   *  ]
   *  Value can also be added with plaintext array, for example:
   *  [ 'foo', 'bar', 'bla' ] or a single string, like 'foo'
   */
  OO.ui.TagMultiselectWidget.prototype.setValue = function ( valueObject ) {
  	valueObject = Array.isArray( valueObject ) ? valueObject : [ valueObject ];
  
  	this.clearItems();
  	valueObject.forEach( function ( obj ) {
  		if ( typeof obj === 'object' ) {
  			this.addTag( obj.data, obj.label );
  		} else {
  			this.addTag( String( obj ) );
  		}
  	}.bind( this ) );
  };
  
  /**
   * Add tag to the display area.
   *
   * Performs a validation check on the tag to be added.
   *
   * @param {Mixed} data Tag data
   * @param {string} [label=data] Tag label. If no label is provided, the
   *  stringified version of the data will be used instead.
   * @return {boolean} Item was added successfully
   */
  OO.ui.TagMultiselectWidget.prototype.addTag = function ( data, label ) {
  	var newItemWidget,
  		isValid = this.isAllowedData( data );
  
  	if ( this.isUnderLimit() && ( isValid || this.allowDisplayInvalidTags ) ) {
  		newItemWidget = this.createTagItemWidget( data, label );
  		newItemWidget.toggleValid( isValid );
  		this.addItems( [ newItemWidget ] );
  		return true;
  	}
  
  	return false;
  };
  
  /**
   * Check whether the number of current tags is within the limit.
   *
   * @return {boolean} True if current tag count is within the limit or
   *  if 'tagLimit' is not set
   */
  OO.ui.TagMultiselectWidget.prototype.isUnderLimit = function () {
  	return !this.tagLimit ||
  		this.getItemCount() < this.tagLimit;
  };
  
  /**
   * Remove tag by its data property.
   *
   * @param {string|Object} data Tag data
   */
  OO.ui.TagMultiselectWidget.prototype.removeTagByData = function ( data ) {
  	var item = this.findItemFromData( data );
  
  	this.removeItems( [ item ] );
  };
  
  /**
   * Construct a OO.ui.TagItemWidget (or a subclass thereof) from given label and data.
   *
   * @protected
   * @param {Mixed} data Item data
   * @param {string} [label=data] The label text.
   * @return {OO.ui.TagItemWidget}
   */
  OO.ui.TagMultiselectWidget.prototype.createTagItemWidget = function ( data, label ) {
  	return new OO.ui.TagItemWidget( { data: data, label: label || data } );
  };
  
  /**
   * Given an item, returns the item after it. If the item is already the
   * last item, return `this.input`. If no item is passed, returns the
   * very first item.
   *
   * @protected
   * @param {OO.ui.TagItemWidget} [item] Tag item
   * @return {OO.ui.Widget} The next widget available.
   */
  OO.ui.TagMultiselectWidget.prototype.getNextItem = function ( item ) {
  	var itemIndex = this.items.indexOf( item );
  
  	if ( item === undefined || itemIndex === -1 ) {
  		return this.items[ 0 ];
  	}
  
  	if ( itemIndex === this.items.length - 1 ) { // Last item
  		if ( this.hasInput ) {
  			return this.input;
  		} else {
  			// Return first item
  			return this.items[ 0 ];
  		}
  	} else {
  		return this.items[ itemIndex + 1 ];
  	}
  };
  
  /**
   * Given an item, returns the item before it. If the item is already the
   * first item, return `this.input`. If no item is passed, returns the
   * very last item.
   *
   * @protected
   * @param {OO.ui.TagItemWidget} [item] Tag item
   * @return {OO.ui.Widget} The previous widget available.
   */
  OO.ui.TagMultiselectWidget.prototype.getPreviousItem = function ( item ) {
  	var itemIndex = this.items.indexOf( item );
  
  	if ( item === undefined || itemIndex === -1 ) {
  		return this.items[ this.items.length - 1 ];
  	}
  
  	if ( itemIndex === 0 ) {
  		if ( this.hasInput ) {
  			return this.input;
  		} else {
  			// Return the last item
  			return this.items[ this.items.length - 1 ];
  		}
  	} else {
  		return this.items[ itemIndex - 1 ];
  	}
  };
  
  /**
   * Update the dimensions of the text input field to encompass all available area.
   * This is especially relevant for when the input is at the edge of a line
   * and should get smaller. The usual operation (as an inline-block with min-width)
   * does not work in that case, pushing the input downwards to the next line.
   *
   * @private
   */
  OO.ui.TagMultiselectWidget.prototype.updateInputSize = function () {
  	var $lastItem, direction, contentWidth, currentWidth, bestWidth, placeholder;
  
  	if ( this.inputPosition === 'inline' && !this.isDisabled() ) {
  		if ( this.input.$input[ 0 ].scrollWidth === 0 ) {
  			// Input appears to be attached but not visible.
  			// Don't attempt to adjust its size, because our measurements
  			// are going to fail anyway.
  			return;
  		}
  		this.input.$input.css( 'width', '1em' );
  		$lastItem = this.$group.children().last();
  		direction = OO.ui.Element.static.getDir( this.$handle );
  
  		// Get the width of the input with the placeholder text as
  		// the value and save it so that we don't keep recalculating
  		placeholder = this.input.$input.attr( 'placeholder' );
  		if (
  			this.contentWidthWithPlaceholder === undefined &&
  			this.input.getValue() === '' &&
  			placeholder !== undefined
  		) {
  			// Set the value directly to avoid any side effects of setValue
  			this.input.$input.val( placeholder );
  			this.contentWidthWithPlaceholder = this.input.$input[ 0 ].scrollWidth;
  			this.input.$input.val( '' );
  
  		}
  
  		// Always keep the input wide enough for the placeholder text
  		contentWidth = Math.max(
  			this.input.$input[ 0 ].scrollWidth,
  			// undefined arguments in Math.max lead to NaN
  			( this.contentWidthWithPlaceholder === undefined ) ?
  				0 : this.contentWidthWithPlaceholder
  		);
  		currentWidth = this.input.$input.width();
  
  		if ( contentWidth < currentWidth ) {
  			this.updateIfHeightChanged();
  			// All is fine, don't perform expensive calculations
  			return;
  		}
  
  		if ( $lastItem.length === 0 ) {
  			bestWidth = this.$content.innerWidth();
  		} else {
  			bestWidth = direction === 'ltr' ?
  				this.$content.innerWidth() - $lastItem.position().left - $lastItem.outerWidth() :
  				$lastItem.position().left;
  		}
  
  		// Some safety margin because I *really* don't feel like finding out where the
  		// few pixels this is off by are coming from.
  		bestWidth -= 13;
  		if ( contentWidth > bestWidth ) {
  			// This will result in the input getting shifted to the next line
  			bestWidth = this.$content.innerWidth() - 13;
  		}
  		this.input.$input.width( Math.floor( bestWidth ) );
  		this.updateIfHeightChanged();
  	} else {
  		this.updateIfHeightChanged();
  	}
  };
  
  /**
   * Determine if widget height changed, and if so,
   * emit the resize event. This is useful for when there are either
   * menus or popups attached to the bottom of the widget, to allow
   * them to change their positioning in case the widget moved down
   * or up.
   *
   * @private
   */
  OO.ui.TagMultiselectWidget.prototype.updateIfHeightChanged = function () {
  	var height = this.$element.height();
  	if ( height !== this.height ) {
  		this.height = height;
  		this.emit( 'resize' );
  	}
  };
  
  /**
   * Check whether all items in the widget are valid
   *
   * @return {boolean} Widget is valid
   */
  OO.ui.TagMultiselectWidget.prototype.checkValidity = function () {
  	return this.getItems().every( function ( item ) {
  		return item.isValid();
  	} );
  };
  
  /**
   * Set the valid state of this item
   *
   * @param {boolean} [valid] Item is valid, omit to toggle
   * @fires valid
   */
  OO.ui.TagMultiselectWidget.prototype.toggleValid = function ( valid ) {
  	valid = valid === undefined ? !this.valid : !!valid;
  
  	if ( this.valid !== valid ) {
  		this.valid = valid;
  
  		this.setFlags( { invalid: !this.valid } );
  
  		this.emit( 'valid', this.valid );
  	}
  };
  
  /**
   * Get the current valid state of the widget
   *
   * @return {boolean} Widget is valid
   */
  OO.ui.TagMultiselectWidget.prototype.isValid = function () {
  	return this.valid;
  };
  
  /**
   * PopupTagMultiselectWidget is a {@link OO.ui.TagMultiselectWidget OO.ui.TagMultiselectWidget}
   * intended to use a popup. The popup can be configured to have a default input to insert values
   * into the widget.
   *
   *     @example
   *     // A PopupTagMultiselectWidget.
   *     var widget = new OO.ui.PopupTagMultiselectWidget();
   *     $( document.body ).append( widget.$element );
   *
   *     // Example: A PopupTagMultiselectWidget with an external popup.
   *     var popupInput = new OO.ui.TextInputWidget(),
   *         widget = new OO.ui.PopupTagMultiselectWidget( {
   *            popupInput: popupInput,
   *            popup: {
   *               $content: popupInput.$element
   *            }
   *         } );
   *     $( document.body ).append( widget.$element );
   *
   * @class
   * @extends OO.ui.TagMultiselectWidget
   * @mixins OO.ui.mixin.PopupElement
   *
   * @param {Object} config Configuration object
   * @cfg {jQuery} [$overlay] An overlay for the popup.
   *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
   * @cfg {Object} [popup] Configuration options for the popup
   * @cfg {OO.ui.InputWidget} [popupInput] An input widget inside the popup that will be
   *  focused when the popup is opened and will be used as replacement for the
   *  general input in the widget.
   * @deprecated
   */
  OO.ui.PopupTagMultiselectWidget = function OoUiPopupTagMultiselectWidget( config ) {
  	var defaultInput,
  		defaultConfig = { popup: {} };
  
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.PopupTagMultiselectWidget.super.call( this, $.extend( {
  		inputPosition: 'none'
  	}, config ) );
  
  	this.$overlay = ( config.$overlay === true ?
  		OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
  
  	if ( !config.popup ) {
  		// For the default base implementation, we give a popup
  		// with an input widget inside it. For any other use cases
  		// the popup needs to be populated externally and the
  		// event handled to add tags separately and manually
  		defaultInput = new OO.ui.TextInputWidget();
  
  		defaultConfig.popupInput = defaultInput;
  		defaultConfig.popup.$content = defaultInput.$element;
  		defaultConfig.popup.padded = true;
  
  		this.$element.addClass( 'oo-ui-popupTagMultiselectWidget-defaultPopup' );
  	}
  
  	// Add overlay, and add that to the autoCloseIgnore
  	defaultConfig.popup.$overlay = this.$overlay;
  	defaultConfig.popup.$autoCloseIgnore = this.hasInput ?
  		this.input.$element.add( this.$overlay ) : this.$overlay;
  
  	// Allow extending any of the above
  	config = $.extend( defaultConfig, config );
  
  	// Mixin constructors
  	OO.ui.mixin.PopupElement.call( this, config );
  
  	if ( this.hasInput ) {
  		this.input.$input.on( 'focus', this.popup.toggle.bind( this.popup, true ) );
  	}
  
  	// Configuration options
  	this.popupInput = config.popupInput;
  	if ( this.popupInput ) {
  		this.popupInput.connect( this, {
  			enter: 'onPopupInputEnter'
  		} );
  	}
  
  	// Events
  	this.on( 'resize', this.popup.updateDimensions.bind( this.popup ) );
  	this.popup.connect( this, {
  		toggle: 'onPopupToggle'
  	} );
  	this.$tabIndexed.on( 'focus', this.onFocus.bind( this ) );
  
  	// Initialize
  	this.$element
  		.append( this.popup.$element )
  		.addClass( 'oo-ui-popupTagMultiselectWidget' );
  
  	// Deprecation warning
  	OO.ui.warnDeprecation( 'PopupTagMultiselectWidget: Deprecated widget. Use MenuTagMultiselectWidget instead. See T208821.' );
  };
  
  /* Initialization */
  
  OO.inheritClass( OO.ui.PopupTagMultiselectWidget, OO.ui.TagMultiselectWidget );
  OO.mixinClass( OO.ui.PopupTagMultiselectWidget, OO.ui.mixin.PopupElement );
  
  /* Methods */
  
  /**
   * Focus event handler.
   *
   * @private
   */
  OO.ui.PopupTagMultiselectWidget.prototype.onFocus = function () {
  	this.popup.toggle( true );
  };
  
  /**
   * Respond to popup toggle event
   *
   * @param {boolean} isVisible Popup is visible
   */
  OO.ui.PopupTagMultiselectWidget.prototype.onPopupToggle = function ( isVisible ) {
  	if ( isVisible && this.popupInput ) {
  		this.popupInput.focus();
  	}
  };
  
  /**
   * Respond to popup input enter event
   */
  OO.ui.PopupTagMultiselectWidget.prototype.onPopupInputEnter = function () {
  	if ( this.popupInput ) {
  		this.addTagByPopupValue( this.popupInput.getValue() );
  		this.popupInput.setValue( '' );
  	}
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.PopupTagMultiselectWidget.prototype.onTagSelect = function ( item ) {
  	if ( this.popupInput && this.allowEditTags ) {
  		this.popupInput.setValue( item.getData() );
  		this.removeItems( [ item ] );
  
  		this.popup.toggle( true );
  		this.popupInput.focus();
  	} else {
  		// Parent
  		OO.ui.PopupTagMultiselectWidget.super.prototype.onTagSelect.call( this, item );
  	}
  };
  
  /**
   * Add a tag by the popup value.
   * Whatever is responsible for setting the value in the popup should call
   * this method to add a tag, or use the regular methods like #addTag or
   * #setValue directly.
   *
   * @param {string} data The value of item
   * @param {string} [label] The label of the tag. If not given, the data is used.
   */
  OO.ui.PopupTagMultiselectWidget.prototype.addTagByPopupValue = function ( data, label ) {
  	this.addTag( data, label );
  };
  
  /**
   * MenuTagMultiselectWidget is a {@link OO.ui.TagMultiselectWidget OO.ui.TagMultiselectWidget}
   * intended to use a menu of selectable options.
   *
   *     @example
   *     // A basic MenuTagMultiselectWidget.
   *     var widget = new OO.ui.MenuTagMultiselectWidget( {
   *         inputPosition: 'outline',
   *         options: [
   *            { data: 'option1', label: 'Option 1', icon: 'tag' },
   *            { data: 'option2', label: 'Option 2' },
   *            { data: 'option3', label: 'Option 3' },
   *         ],
   *         selected: [ 'option1', 'option2' ]
   *     } );
   *     $( document.body ).append( widget.$element );
   *
   * @class
   * @extends OO.ui.TagMultiselectWidget
   *
   * @constructor
   * @param {Object} [config] Configuration object
   * @cfg {boolean} [clearInputOnChoose=true] Clear the text input value when a menu option is chosen
   * @cfg {Object} [menu] Configuration object for the menu widget
   * @cfg {jQuery} [$overlay] An overlay for the menu.
   *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
   * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
   */
  OO.ui.MenuTagMultiselectWidget = function OoUiMenuTagMultiselectWidget( config ) {
  	var options, selected, configCopy,
  		$autoCloseIgnore = $( [] );
  
  	config = config || {};
  
  	// Ensure that any pre-selected items exist as menu options,
  	// so that they can be added as tags from #setValue
  	options = config.options || [];
  	selected = config.selected || [];
  	options = options.concat(
  		selected.map( function ( option ) {
  			if ( typeof option === 'string' ) {
  				return {
  					data: option,
  					label: option
  				};
  			}
  			return option;
  		} )
  	);
  
  	configCopy = OO.copy( config );
  	configCopy.options = options;
  	configCopy.selected = selected;
  
  	// Parent constructor
  	OO.ui.MenuTagMultiselectWidget.super.call( this, configCopy );
  
  	$autoCloseIgnore = $autoCloseIgnore.add( this.$group );
  	if ( this.hasInput ) {
  		$autoCloseIgnore = $autoCloseIgnore.add( this.input.$element );
  	}
  
  	this.$overlay = ( config.$overlay === true ?
  		OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
  	this.clearInputOnChoose = config.clearInputOnChoose === undefined ||
  		!!config.clearInputOnChoose;
  	this.menu = this.createMenuWidget( $.extend( {
  		widget: this,
  		hideOnChoose: false,
  		input: this.hasInput ? this.input : null,
  		$input: this.hasInput ? this.input.$input : null,
  		filterFromInput: !!this.hasInput,
  		highlightOnFilter: !this.allowArbitrary,
  		multiselect: true,
  		$autoCloseIgnore: $autoCloseIgnore,
  		$floatableContainer: this.hasInput && this.inputPosition === 'outline' ?
  			this.input.$element : this.$element,
  		$overlay: this.$overlay,
  		disabled: this.isDisabled()
  	}, config.menu ) );
  	this.addOptions( options );
  
  	// Events
  	this.menu.connect( this, {
  		choose: 'onMenuChoose',
  		toggle: 'onMenuToggle'
  	} );
  	if ( this.hasInput ) {
  		this.input.connect( this, {
  			change: 'onInputChange'
  		} );
  	}
  	this.connect( this, {
  		resize: 'onResize'
  	} );
  
  	// Initialization
  	this.$overlay.append( this.menu.$element );
  	this.$element.addClass( 'oo-ui-menuTagMultiselectWidget' );
  	// Remove MenuSelectWidget's generic focus owner ARIA attribute
  	// TODO: Should this widget have a `role` that is compatible with this attribute?
  	this.menu.$focusOwner.removeAttr( 'aria-expanded' );
  	// TagMultiselectWidget already does this, but it doesn't work right because this.menu is
  	// not yet set up while the parent constructor runs, and #getAllowedValues rejects everything.
  	if ( selected.length > 0 ) {
  		this.setValue( selected );
  	}
  };
  
  /* Initialization */
  
  OO.inheritClass( OO.ui.MenuTagMultiselectWidget, OO.ui.TagMultiselectWidget );
  
  /* Methods */
  
  /**
   * Respond to resize event
   */
  OO.ui.MenuTagMultiselectWidget.prototype.onResize = function () {
  	// Reposition the menu
  	this.menu.position();
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.MenuTagMultiselectWidget.prototype.onInputFocus = function () {
  	// Parent method
  	OO.ui.MenuTagMultiselectWidget.super.prototype.onInputFocus.call( this );
  
  	this.menu.toggle( true );
  };
  
  /**
   * Respond to input change event
   */
  OO.ui.MenuTagMultiselectWidget.prototype.onInputChange = function () {
  	this.menu.toggle( true );
  };
  
  /**
   * Respond to menu choose event, which is intentional by the user.
   *
   * @param {OO.ui.OptionWidget} menuItem Selected menu items
   * @param {boolean} selected Item is selected
   */
  OO.ui.MenuTagMultiselectWidget.prototype.onMenuChoose = function ( menuItem, selected ) {
  	if ( selected && !this.findItemFromData( menuItem.getData() ) ) {
  		// The menu item is selected, add it to the tags
  		this.addTag( menuItem.getData(), menuItem.getLabel() );
  	} else {
  		// The menu item was unselected, remove the tag
  		this.removeTagByData( menuItem.getData() );
  	}
  
  	if ( this.hasInput && this.clearInputOnChoose ) {
  		this.input.setValue( '' );
  	}
  };
  
  /**
   * Respond to menu toggle event. Reset item highlights on hide.
   *
   * @param {boolean} isVisible The menu is visible
   */
  OO.ui.MenuTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
  	if ( !isVisible ) {
  		this.menu.highlightItem( null );
  		this.menu.scrollToTop();
  	}
  	setTimeout( function () {
  		// Remove MenuSelectWidget's generic focus owner ARIA attribute
  		// TODO: Should this widget have a `role` that is compatible with this attribute?
  		this.menu.$focusOwner.removeAttr( 'aria-expanded' );
  	}.bind( this ) );
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.MenuTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
  	var menuItem = this.menu.findItemFromData( tagItem.getData() );
  	if ( !this.allowArbitrary ) {
  		// Override the base behavior from TagMultiselectWidget; the base behavior
  		// in TagMultiselectWidget is to remove the tag to edit it in the input,
  		// but in our case, we want to utilize the menu selection behavior, and
  		// definitely not remove the item.
  
  		// If there is an input that is used for filtering, erase the value so we don't filter
  		if ( this.hasInput && this.menu.filterFromInput ) {
  			this.input.setValue( '' );
  		}
  
  		this.focus();
  
  		// Highlight the menu item
  		this.menu.highlightItem( menuItem );
  		this.menu.scrollItemIntoView( menuItem );
  
  	} else {
  		// Use the default
  		OO.ui.MenuTagMultiselectWidget.super.prototype.onTagSelect.call( this, tagItem );
  	}
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.MenuTagMultiselectWidget.prototype.removeItems = function ( items ) {
  	var widget = this;
  
  	// Parent
  	OO.ui.MenuTagMultiselectWidget.super.prototype.removeItems.call( this, items );
  
  	items.forEach( function ( tagItem ) {
  		var menuItem = widget.menu.findItemFromData( tagItem.getData() );
  		if ( menuItem ) {
  			// Synchronize the menu selection - unselect the removed tag
  			widget.menu.unselectItem( menuItem );
  		}
  	} );
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.MenuTagMultiselectWidget.prototype.setValue = function ( valueObject ) {
  	valueObject = Array.isArray( valueObject ) ? valueObject : [ valueObject ];
  
  	// We override this method from the parent, to make sure we are adding proper
  	// menu items, and are accounting for cases where we have this widget with
  	// a menu but also 'allowArbitrary'
  	if ( !this.menu ) {
  		return;
  	}
  
  	this.clearItems();
  	valueObject.forEach( function ( obj ) {
  		var data, label, menuItem;
  
  		if ( typeof obj === 'string' ) {
  			data = label = obj;
  		} else {
  			data = obj.data;
  			label = obj.label;
  		}
  
  		// Check if the item is in the menu
  		menuItem = this.menu.getItemFromLabel( label ) || this.menu.findItemFromData( data );
  		if ( menuItem ) {
  			// Menu item found, add the menu item
  			this.addTag( menuItem.getData(), menuItem.getLabel() );
  			// Make sure that item is also selected
  			this.menu.selectItem( menuItem );
  		} else if ( this.allowArbitrary ) {
  			// If the item isn't in the menu, only add it if we
  			// allow for arbitrary values
  			this.addTag( data, label );
  		}
  	}.bind( this ) );
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.MenuTagMultiselectWidget.prototype.setDisabled = function ( isDisabled ) {
  	// Parent method
  	OO.ui.MenuTagMultiselectWidget.super.prototype.setDisabled.call( this, isDisabled );
  
  	if ( this.menu ) {
  		// Protect against calling setDisabled() before the menu was initialized
  		this.menu.setDisabled( isDisabled );
  	}
  };
  
  /**
   * Highlight the first selectable item in the menu, if configured.
   *
   * @private
   * @chainable
   */
  OO.ui.MenuTagMultiselectWidget.prototype.initializeMenuSelection = function () {
  	var highlightedItem;
  	this.menu.highlightItem(
  		this.allowArbitrary ?
  			null :
  			this.menu.findFirstSelectableItem()
  	);
  
  	highlightedItem = this.menu.findHighlightedItem();
  	// Scroll to the highlighted item, if it exists
  	if ( highlightedItem ) {
  		this.menu.scrollItemIntoView( highlightedItem );
  	}
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.MenuTagMultiselectWidget.prototype.getTagInfoFromInput = function () {
  	var val = this.input.getValue(),
  		// Look for a highlighted item first
  		// Then look for the element that fits the data
  		item = this.menu.findHighlightedItem() || this.menu.findItemFromData( val ),
  		data = item ? item.getData() : val,
  		label = item ? item.getLabel() : val;
  
  	return { data: data, label: label };
  };
  
  /**
   * Create the menu for this widget. This is in a separate method so that
   * child classes can override this without polluting the constructor with
   * unnecessary extra objects that will be overidden.
   *
   * @param {Object} menuConfig Configuration options
   * @return {OO.ui.MenuSelectWidget} Menu widget
   */
  OO.ui.MenuTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
  	return new OO.ui.MenuSelectWidget( menuConfig );
  };
  
  /**
   * Add options to the menu, ensuring that they are unique by data.
   *
   * @param {Object[]} menuOptions Object defining options
   */
  OO.ui.MenuTagMultiselectWidget.prototype.addOptions = function ( menuOptions ) {
  	var widget = this,
  		optionsData = [],
  		items = [];
  
  	menuOptions.forEach( function ( obj ) {
  		if ( optionsData.indexOf( obj.data ) === -1 ) {
  			optionsData.push( obj.data );
  			items.push(
  				widget.createMenuOptionWidget( obj.data, obj.label, obj.icon )
  			);
  		}
  	} );
  
  	this.menu.addItems( items );
  };
  
  /**
   * Create a menu option widget.
   *
   * @param {string} data Item data
   * @param {string} [label=data] Item label
   * @param {string} [icon] Symbolic icon name
   * @return {OO.ui.OptionWidget} Option widget
   */
  OO.ui.MenuTagMultiselectWidget.prototype.createMenuOptionWidget = function ( data, label, icon ) {
  	return new OO.ui.MenuOptionWidget( {
  		data: data,
  		label: label || data,
  		icon: icon
  	} );
  };
  
  /**
   * Get the menu
   *
   * @return {OO.ui.MenuSelectWidget} Menu
   */
  OO.ui.MenuTagMultiselectWidget.prototype.getMenu = function () {
  	return this.menu;
  };
  
  /**
   * Get the allowed values list
   *
   * @return {string[]} Allowed data values
   */
  OO.ui.MenuTagMultiselectWidget.prototype.getAllowedValues = function () {
  	var menuDatas = [];
  	if ( this.menu ) {
  		// If the parent constructor is calling us, we're not ready yet, this.menu is not set up.
  		menuDatas = this.menu.getItems().map( function ( menuItem ) {
  			return menuItem.getData();
  		} );
  	}
  	return this.allowedValues.concat( menuDatas );
  };
  
  /**
   * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
   * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link
   * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}.
   * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
   *
   * Although SelectFileWidget inherits from SelectFileInputWidget, it does not
   * behave as an InputWidget, and can't be used in HTML forms.
   *
   *     @example
   *     // A file select widget.
   *     var selectFile = new OO.ui.SelectFileWidget();
   *     $( document.body ).append( selectFile.$element );
   *
   * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets
   *
   * @class
   * @extends OO.ui.SelectFileInputWidget
   * @mixins OO.ui.mixin.PendingElement
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
   * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
   * @cfg {boolean} [buttonOnly=false] Show only the select file button, no info field. Requires
   *  showDropTarget to be false.
   * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be
   *  true.
   * @cfg {number} [thumbnailSizeLimit=20] File size limit in MiB above which to not try and show a
   *  preview (for performance).
   */
  OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
  	var dragHandler, droppable,
  		isSupported = this.constructor.static.isSupported();
  
  	// Configuration initialization
  	config = $.extend( {
  		notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
  		droppable: true,
  		buttonOnly: false,
  		showDropTarget: false,
  		thumbnailSizeLimit: 20
  	}, config );
  
  	if ( !isSupported ) {
  		config.disabled = true;
  	}
  
  	// Parent constructor
  	OO.ui.SelectFileWidget.super.call( this, config );
  
  	// Mixin constructors
  	OO.ui.mixin.PendingElement.call( this );
  
  	if ( !isSupported ) {
  		this.info.setValue( config.notsupported );
  	}
  
  	// Properties
  	droppable = config.droppable && isSupported;
  	this.showDropTarget = droppable && config.showDropTarget;
  	this.thumbnailSizeLimit = config.thumbnailSizeLimit;
  
  	// Initialization
  	if ( this.showDropTarget ) {
  		this.selectButton.setIcon( 'upload' );
  		this.$element
  			.addClass( 'oo-ui-selectFileWidget-dropTarget' )
  			.on( {
  				click: this.onDropTargetClick.bind( this )
  			} )
  			.append(
  				this.info.$element,
  				this.selectButton.$element,
  				$( '<span>' )
  					.addClass( 'oo-ui-selectFileWidget-dropLabel' )
  					.text( OO.ui.msg(
  						this.multiple ?
  							'ooui-selectfile-dragdrop-placeholder-multiple' :
  							'ooui-selectfile-dragdrop-placeholder'
  					) )
  			);
  		if ( !this.multiple ) {
  			this.$thumbnail = $( '<div>' ).addClass( 'oo-ui-selectFileWidget-thumbnail' );
  			this.setPendingElement( this.$thumbnail );
  			this.$element
  				.addClass( 'oo-ui-selectFileWidget-withThumbnail' )
  				.prepend( this.$thumbnail );
  		}
  		this.fieldLayout.$element.remove();
  	} else if ( config.buttonOnly ) {
  		// Copy over any classes that may have been added already.
  		// Ensure no events are bound to this.$element before here.
  		this.selectButton.$element
  			.addClass( this.$element.attr( 'class' ) )
  			.addClass( 'oo-ui-selectFileWidget-buttonOnly' );
  		// Set this.$element to just be the button
  		this.$element = this.selectButton.$element;
  	}
  
  	// Events
  	if ( droppable ) {
  		dragHandler = this.onDragEnterOrOver.bind( this );
  		this.$element.on( {
  			dragenter: dragHandler,
  			dragover: dragHandler,
  			dragleave: this.onDragLeave.bind( this ),
  			drop: this.onDrop.bind( this )
  		} );
  	}
  
  	this.$input
  		.on( 'click', function ( e ) {
  			// Prevents dropTarget to get clicked which calls
  			// a click on this input
  			e.stopPropagation();
  		} );
  
  	this.$element.addClass( 'oo-ui-selectFileWidget' );
  
  	this.updateUI();
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.SelectFileInputWidget );
  OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
  
  /* Static Properties */
  
  /**
   * Check if this widget is supported
   *
   * @static
   * @return {boolean}
   */
  OO.ui.SelectFileWidget.static.isSupported = function () {
  	var $input;
  	if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
  		$input = $( '<input>' ).attr( 'type', 'file' );
  		OO.ui.SelectFileWidget.static.isSupportedCache = $input[ 0 ].files !== undefined;
  	}
  	return OO.ui.SelectFileWidget.static.isSupportedCache;
  };
  
  OO.ui.SelectFileWidget.static.isSupportedCache = null;
  
  /* Events */
  
  /**
   * @event change
   *
   * A change event is emitted when the on/off state of the toggle changes.
   *
   * @param {File|null} value New value
   */
  
  /* Methods */
  
  /**
   * Get the current value of the field
   *
   * For single file widgets returns a File or null.
   * For multiple file widgets returns a list of Files.
   *
   * @return {File|File[]|null}
   */
  OO.ui.SelectFileWidget.prototype.getValue = function () {
  	return this.multiple ? this.currentFiles : this.currentFiles[ 0 ];
  };
  
  /**
   * Set the current value of the field
   *
   * @param {File[]|null} files Files to select
   */
  OO.ui.SelectFileWidget.prototype.setValue = function ( files ) {
  	if ( files && !this.multiple ) {
  		files = files.slice( 0, 1 );
  	}
  
  	function comparableFile( file ) {
  		// Use extend to convert to plain objects so they can be compared.
  		return $.extend( {}, file );
  	}
  
  	if ( !OO.compare(
  		files && files.map( comparableFile ),
  		this.currentFiles && this.currentFiles.map( comparableFile )
  	) ) {
  		this.currentFiles = files || [];
  		this.emit( 'change', this.currentFiles );
  	}
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.SelectFileWidget.prototype.getFilename = function () {
  	return this.currentFiles.map( function ( file ) {
  		return file.name;
  	} ).join( ', ' );
  };
  
  /**
   * Disable InputWidget#onEdit listener, onFileSelected is used instead.
   *
   * @inheritdoc
   */
  OO.ui.SelectFileWidget.prototype.onEdit = function () {};
  
  /**
   * @inheritdoc
   */
  OO.ui.SelectFileWidget.prototype.updateUI = function () {
  	// Too early, or not supported
  	if ( !this.selectButton || !this.constructor.static.isSupported() ) {
  		return;
  	}
  
  	// Parent method
  	OO.ui.SelectFileWidget.super.prototype.updateUI.call( this );
  
  	if ( this.currentFiles.length ) {
  		this.$element.removeClass( 'oo-ui-selectFileInputWidget-empty' );
  
  		if ( this.showDropTarget ) {
  			if ( !this.multiple ) {
  				this.pushPending();
  				this.loadAndGetImageUrl( this.currentFiles[ 0 ] ).done( function ( url ) {
  					this.$thumbnail.css( 'background-image', 'url( ' + url + ' )' );
  				}.bind( this ) ).fail( function () {
  					this.$thumbnail.append(
  						new OO.ui.IconWidget( {
  							icon: 'attachment',
  							classes: [ 'oo-ui-selectFileWidget-noThumbnail-icon' ]
  						} ).$element
  					);
  				}.bind( this ) ).always( function () {
  					this.popPending();
  				}.bind( this ) );
  			}
  			this.$element.off( 'click' );
  		}
  	} else {
  		if ( this.showDropTarget ) {
  			this.$element.off( 'click' );
  			this.$element.on( {
  				click: this.onDropTargetClick.bind( this )
  			} );
  			if ( !this.multiple ) {
  				this.$thumbnail
  					.empty()
  					.css( 'background-image', '' );
  			}
  		}
  		this.$element.addClass( 'oo-ui-selectFileInputWidget-empty' );
  	}
  };
  
  /**
   * If the selected file is an image, get its URL and load it.
   *
   * @param {File} file File
   * @return {jQuery.Promise} Promise resolves with the image URL after it has loaded
   */
  OO.ui.SelectFileWidget.prototype.loadAndGetImageUrl = function ( file ) {
  	var deferred = $.Deferred(),
  		reader = new FileReader();
  
  	if (
  		( OO.getProp( file, 'type' ) || '' ).indexOf( 'image/' ) === 0 &&
  		file.size < this.thumbnailSizeLimit * 1024 * 1024
  	) {
  		reader.onload = function ( event ) {
  			var img = document.createElement( 'img' );
  			img.addEventListener( 'load', function () {
  				if (
  					img.naturalWidth === 0 ||
  					img.naturalHeight === 0 ||
  					img.complete === false
  				) {
  					deferred.reject();
  				} else {
  					deferred.resolve( event.target.result );
  				}
  			} );
  			img.src = event.target.result;
  		};
  		reader.readAsDataURL( file );
  	} else {
  		deferred.reject();
  	}
  
  	return deferred.promise();
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.SelectFileWidget.prototype.onFileSelected = function ( e ) {
  	var files;
  
  	if ( this.inputClearing ) {
  		return;
  	}
  
  	files = this.filterFiles( e.target.files || [] );
  
  	// After a file is selected clear the native widget to avoid confusion
  	this.inputClearing = true;
  	this.$input[ 0 ].value = '';
  	this.inputClearing = false;
  
  	this.setValue( files );
  };
  
  /**
   * Handle drop target click events.
   *
   * @private
   * @param {jQuery.Event} e Key press event
   * @return {undefined|boolean} False to prevent default if event is handled
   */
  OO.ui.SelectFileWidget.prototype.onDropTargetClick = function () {
  	if ( !this.isDisabled() && this.$input ) {
  		this.$input.trigger( 'click' );
  		return false;
  	}
  };
  
  /**
   * Handle drag enter and over events
   *
   * @private
   * @param {jQuery.Event} e Drag event
   * @return {undefined|boolean} False to prevent default if event is handled
   */
  OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
  	var itemsOrFiles,
  		hasDroppableFile = false,
  		dt = e.originalEvent.dataTransfer;
  
  	e.preventDefault();
  	e.stopPropagation();
  
  	if ( this.isDisabled() ) {
  		this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
  		dt.dropEffect = 'none';
  		return false;
  	}
  
  	// DataTransferItem and File both have a type property, but in Chrome files
  	// have no information at this point.
  	itemsOrFiles = dt.items || dt.files;
  	if ( itemsOrFiles && itemsOrFiles.length ) {
  		if ( this.filterFiles( itemsOrFiles ).length ) {
  			hasDroppableFile = true;
  		}
  	// dt.types is Array-like, but not an Array
  	} else if ( Array.prototype.indexOf.call( OO.getProp( dt, 'types' ) || [], 'Files' ) !== -1 ) {
  		// File information is not available at this point for security so just assume
  		// it is acceptable for now.
  		// https://bugzilla.mozilla.org/show_bug.cgi?id=640534
  		hasDroppableFile = true;
  	}
  
  	this.$element.toggleClass( 'oo-ui-selectFileWidget-canDrop', hasDroppableFile );
  	if ( !hasDroppableFile ) {
  		dt.dropEffect = 'none';
  	}
  
  	return false;
  };
  
  /**
   * Handle drag leave events
   *
   * @private
   * @param {jQuery.Event} e Drag event
   */
  OO.ui.SelectFileWidget.prototype.onDragLeave = function () {
  	this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
  };
  
  /**
   * Handle drop events
   *
   * @private
   * @param {jQuery.Event} e Drop event
   * @return {undefined|boolean} False to prevent default if event is handled
   */
  OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
  	var files,
  		dt = e.originalEvent.dataTransfer;
  
  	e.preventDefault();
  	e.stopPropagation();
  	this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
  
  	if ( this.isDisabled() ) {
  		return false;
  	}
  
  	files = this.filterFiles( dt.files || [] );
  	this.setValue( files );
  
  	return false;
  };
  
  /**
   * @inheritdoc
   */
  OO.ui.SelectFileWidget.prototype.setDisabled = function ( disabled ) {
  	disabled = disabled || !this.constructor.static.isSupported();
  
  	// Parent method
  	OO.ui.SelectFileWidget.super.prototype.setDisabled.call( this, disabled );
  };
  
  /**
   * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field},
   * where users can type a search query, and a menu of search results,
   * which is displayed beneath the query field.
   * Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible
   * to the user. Users can choose an item from the menu or type a query into the text field to
   * search for a matching result item.
   * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
   *
   * Each time the query is changed, the search result menu is cleared and repopulated. Please see
   * the [OOUI demos][1] for an example.
   *
   * [1]: https://doc.wikimedia.org/oojs-ui/master/demos/#SearchInputWidget-type-search
   *
   * @class
   * @extends OO.ui.Widget
   *
   * @constructor
   * @param {Object} [config] Configuration options
   * @cfg {string|jQuery} [placeholder] Placeholder text for query input
   * @cfg {string} [value] Initial query value
   * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} for search. Defaults
   *  to a {@link OO.ui.SearchInputWidget search input widget} if not provided.
   */
  OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
  	// Configuration initialization
  	config = config || {};
  
  	// Parent constructor
  	OO.ui.SearchWidget.super.call( this, config );
  
  	// Properties
  	this.query = config.input || new OO.ui.SearchInputWidget( {
  		placeholder: config.placeholder,
  		value: config.value
  	} );
  	this.results = new OO.ui.SelectWidget();
  	this.results.setFocusOwner( this.query.$input );
  	this.$query = $( '<div>' );
  	this.$results = $( '<div>' );
  
  	// Events
  	this.query.connect( this, {
  		change: 'onQueryChange',
  		enter: 'onQueryEnter'
  	} );
  	this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
  
  	// Initialization
  	this.$query
  		.addClass( 'oo-ui-searchWidget-query' )
  		.append( this.query.$element );
  	this.$results
  		.addClass( 'oo-ui-searchWidget-results' )
  		.append( this.results.$element );
  	this.$element
  		.addClass( 'oo-ui-searchWidget' )
  		.append( this.$results, this.$query );
  };
  
  /* Setup */
  
  OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
  
  /* Methods */
  
  /**
   * Handle query key down events.
   *
   * @private
   * @param {jQuery.Event} e Key down event
   */
  OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
  	var highlightedItem, nextItem,
  		dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
  
  	if ( dir ) {
  		highlightedItem = this.results.findHighlightedItem();
  		if ( !highlightedItem ) {
  			highlightedItem = this.results.findSelectedItem();
  		}
  		nextItem = this.results.findRelativeSelectableItem( highlightedItem, dir );
  		// nextItem may be null if there are no results
  		this.results.highlightItem( nextItem );
  		if ( nextItem ) {
  			nextItem.scrollElementIntoView();
  		}
  	}
  };
  
  /**
   * Handle select widget select events.
   *
   * Clears existing results. Subclasses should repopulate items according to new query.
   *
   * @private
   * @param {string} value New value
   */
  OO.ui.SearchWidget.prototype.onQueryChange = function () {
  	// Reset
  	this.results.clearItems();
  };
  
  /**
   * Handle select widget enter key events.
   *
   * Chooses highlighted item.
   *
   * @private
   * @param {string} value New value
   */
  OO.ui.SearchWidget.prototype.onQueryEnter = function () {
  	var highlightedItem = this.results.findHighlightedItem();
  	if ( highlightedItem ) {
  		this.results.chooseItem( highlightedItem );
  	}
  };
  
  /**
   * Get the query input.
   *
   * @return {OO.ui.TextInputWidget} Query input
   */
  OO.ui.SearchWidget.prototype.getQuery = function () {
  	return this.query;
  };
  
  /**
   * Get the search results menu.
   *
   * @return {OO.ui.SelectWidget} Menu of search results
   */
  OO.ui.SearchWidget.prototype.getResults = function () {
  	return this.results;
  };
  
  }( OO ) );
  
  //# sourceMappingURL=oojs-ui-widgets.js.map.json