Merge "Remove patrol config check in User::isAllowed()"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
index 2565fb5..f280853 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.13.3
+ * OOjs UI v0.15.0
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
- * Copyright 2011–2015 OOjs UI Team and other contributors.
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2015-11-18T01:09:23Z
+ * Date: 2016-01-12T23:06:31Z
  */
 ( function ( OO ) {
 
@@ -44,6 +44,17 @@ OO.ui.Keys = {
        SPACE: 32
 };
 
+/**
+ * Constants for MouseEvent.which
+ *
+ * @property {Object}
+ */
+OO.ui.MouseButtons = {
+       LEFT: 1,
+       MIDDLE: 2,
+       RIGHT: 3
+};
+
 /**
  * @property {Number}
  */
@@ -248,35 +259,27 @@ OO.ui.debounce = function ( func, wait, immediate ) {
 };
 
 /**
- * Proxy for `node.addEventListener( eventName, handler, true )`, if the browser supports it.
- * Otherwise falls back to non-capturing event listeners.
+ * Proxy for `node.addEventListener( eventName, handler, true )`.
  *
  * @param {HTMLElement} node
  * @param {string} eventName
  * @param {Function} handler
+ * @deprecated
  */
 OO.ui.addCaptureEventListener = function ( node, eventName, handler ) {
-       if ( node.addEventListener ) {
-               node.addEventListener( eventName, handler, true );
-       } else {
-               node.attachEvent( 'on' + eventName, handler );
-       }
+       node.addEventListener( eventName, handler, true );
 };
 
 /**
- * Proxy for `node.removeEventListener( eventName, handler, true )`, if the browser supports it.
- * Otherwise falls back to non-capturing event listeners.
+ * Proxy for `node.removeEventListener( eventName, handler, true )`.
  *
  * @param {HTMLElement} node
  * @param {string} eventName
  * @param {Function} handler
+ * @deprecated
  */
 OO.ui.removeCaptureEventListener = function ( node, eventName, handler ) {
-       if ( node.addEventListener ) {
-               node.removeEventListener( eventName, handler, true );
-       } else {
-               node.detachEvent( 'on' + eventName, handler );
-       }
+       node.removeEventListener( eventName, handler, true );
 };
 
 /**
@@ -369,67 +372,150 @@ OO.ui.infuse = function ( idOrNode ) {
                }
                return message;
        };
+} )();
 
-       /**
-        * Package a message and arguments for deferred resolution.
-        *
-        * Use this when you are statically specifying a message and the message may not yet be present.
-        *
-        * @param {string} key Message key
-        * @param {Mixed...} [params] Message parameters
-        * @return {Function} Function that returns the resolved message when executed
-        */
-       OO.ui.deferMsg = function () {
-               var args = arguments;
-               return function () {
-                       return OO.ui.msg.apply( OO.ui, args );
-               };
+/**
+ * Package a message and arguments for deferred resolution.
+ *
+ * Use this when you are statically specifying a message and the message may not yet be present.
+ *
+ * @param {string} key Message key
+ * @param {Mixed...} [params] Message parameters
+ * @return {Function} Function that returns the resolved message when executed
+ */
+OO.ui.deferMsg = function () {
+       var args = arguments;
+       return function () {
+               return OO.ui.msg.apply( OO.ui, args );
        };
+};
 
-       /**
       * Resolve a message.
       *
       * If the message is a function it will be executed, otherwise it will pass through directly.
       *
       * @param {Function|string} msg Deferred message, or message text
       * @return {string} Resolved message
       */
-       OO.ui.resolveMsg = function ( msg ) {
-               if ( $.isFunction( msg ) ) {
-                       return msg();
-               }
-               return msg;
-       };
+/**
+ * Resolve a message.
+ *
+ * If the message is a function it will be executed, otherwise it will pass through directly.
+ *
+ * @param {Function|string} msg Deferred message, or message text
+ * @return {string} Resolved message
+ */
+OO.ui.resolveMsg = function ( msg ) {
+       if ( $.isFunction( msg ) ) {
+               return msg();
+       }
+       return msg;
+};
 
-       /**
       * @param {string} url
       * @return {boolean}
       */
-       OO.ui.isSafeUrl = function ( url ) {
-               var protocol,
-                       // Keep in sync with php/Tag.php
-                       whitelist = [
-                               'bitcoin:', 'ftp:', 'ftps:', 'geo:', 'git:', 'gopher:', 'http:', 'https:', 'irc:', 'ircs:',
-                               'magnet:', 'mailto:', 'mms:', 'news:', 'nntp:', 'redis:', 'sftp:', 'sip:', 'sips:', 'sms:', 'ssh:',
-                               'svn:', 'tel:', 'telnet:', 'urn:', 'worldwind:', 'xmpp:'
-                       ];
-
-               if ( url.indexOf( ':' ) === -1 ) {
-                       // No protocol, safe
-                       return true;
-               }
+/**
+ * @param {string} url
+ * @return {boolean}
+ */
+OO.ui.isSafeUrl = function ( url ) {
+       var protocol,
+               // Keep in sync with php/Tag.php
+               whitelist = [
+                       'bitcoin:', 'ftp:', 'ftps:', 'geo:', 'git:', 'gopher:', 'http:', 'https:', 'irc:', 'ircs:',
+                       'magnet:', 'mailto:', 'mms:', 'news:', 'nntp:', 'redis:', 'sftp:', 'sip:', 'sips:', 'sms:', 'ssh:',
+                       'svn:', 'tel:', 'telnet:', 'urn:', 'worldwind:', 'xmpp:'
+               ];
+
+       if ( url.indexOf( ':' ) === -1 ) {
+               // No protocol, safe
+               return true;
+       }
 
-               protocol = url.split( ':', 1 )[ 0 ] + ':';
-               if ( !protocol.match( /^([A-za-z0-9\+\.\-])+:/ ) ) {
-                       // Not a valid protocol, safe
-                       return true;
-               }
+       protocol = url.split( ':', 1 )[ 0 ] + ':';
+       if ( !protocol.match( /^([A-za-z0-9\+\.\-])+:/ ) ) {
+               // Not a valid protocol, safe
+               return true;
+       }
 
-               // Safe if in the whitelist
-               return whitelist.indexOf( protocol ) !== -1;
-       };
+       // Safe if in the whitelist
+       return whitelist.indexOf( protocol ) !== -1;
+};
 
-} )();
+/**
+ * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
+ * OO.ui.confirm.
+ *
+ * @private
+ * @return {OO.ui.WindowManager}
+ */
+OO.ui.getWindowManager = function () {
+       if ( !OO.ui.windowManager ) {
+               OO.ui.windowManager = new OO.ui.WindowManager();
+               $( 'body' ).append( OO.ui.windowManager.$element );
+               OO.ui.windowManager.addWindows( {
+                       messageDialog: new OO.ui.MessageDialog()
+               } );
+       }
+       return OO.ui.windowManager;
+};
+
+/**
+ * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
+ * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
+ * has only one action button, labelled "OK", clicking it will simply close the dialog.
+ *
+ * A window manager is created automatically when this function is called for the first time.
+ *
+ *     @example
+ *     OO.ui.alert( 'Something happened!' ).done( function () {
+ *         console.log( 'User closed the dialog.' );
+ *     } );
+ *
+ * @param {jQuery|string} text Message text to display
+ * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
+ * @return {jQuery.Promise} Promise resolved when the user closes the dialog
+ */
+OO.ui.alert = function ( text, options ) {
+       return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
+               message: text,
+               verbose: true,
+               actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
+       }, options ) ).then( function ( opened ) {
+               return opened.then( function ( closing ) {
+                       return closing.then( function () {
+                               return $.Deferred().resolve();
+                       } );
+               } );
+       } );
+};
+
+/**
+ * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
+ * the rest of the page will be dimmed out and the user won't be able to interact with it. The
+ * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
+ * (labelled "Cancel").
+ *
+ * A window manager is created automatically when this function is called for the first time.
+ *
+ *     @example
+ *     OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
+ *         if ( confirmed ) {
+ *             console.log( 'User clicked "OK"!' );
+ *         } else {
+ *             console.log( 'User clicked "Cancel" or closed the dialog.' );
+ *         }
+ *     } );
+ *
+ * @param {jQuery|string} text Message text to display
+ * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
+ * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
+ *  confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
+ *  `false`.
+ */
+OO.ui.confirm = function ( text, options ) {
+       return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
+               message: text,
+               verbose: true
+       }, options ) ).then( function ( opened ) {
+               return opened.then( function ( closing ) {
+                       return closing.then( function ( data ) {
+                               return $.Deferred().resolve( !!( data && data.action === 'accept' ) );
+                       } );
+               } );
+       } );
+};
 
 /*!
  * Mixin namespace.
@@ -1404,9 +1490,7 @@ OO.ui.Element.static.getDocument = function ( obj ) {
  */
 OO.ui.Element.static.getWindow = function ( obj ) {
        var doc = this.getDocument( obj );
-       // Support: IE 8
-       // Standard Document.defaultView is IE9+
-       return doc.parentWindow || doc.defaultView;
+       return doc.defaultView;
 };
 
 /**
@@ -1521,14 +1605,8 @@ OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
  */
 OO.ui.Element.static.getBorders = function ( el ) {
        var doc = el.ownerDocument,
-               // Support: IE 8
-               // Standard Document.defaultView is IE9+
-               win = doc.parentWindow || doc.defaultView,
-               style = win && win.getComputedStyle ?
-                       win.getComputedStyle( el, null ) :
-                       // Support: IE 8
-                       // Standard getComputedStyle() is IE9+
-                       el.currentStyle,
+               win = doc.defaultView,
+               style = win.getComputedStyle( el, null ),
                $el = $( el ),
                top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
                left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
@@ -1553,9 +1631,7 @@ OO.ui.Element.static.getBorders = function ( el ) {
 OO.ui.Element.static.getDimensions = function ( el ) {
        var $el, $win,
                doc = el.ownerDocument || el.document,
-               // Support: IE 8
-               // Standard Document.defaultView is IE9+
-               win = doc.parentWindow || doc.defaultView;
+               win = doc.defaultView;
 
        if ( win === el || el === doc.documentElement ) {
                $win = $( win );
@@ -2841,7 +2917,7 @@ OO.ui.Dialog.static.name = '';
  *
  * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
  * that will produce a Label node or string. The title can also be specified with data passed to the
- * constructor (see #getSetupProcess). In this case, the static value will be overriden.
+ * constructor (see #getSetupProcess). In this case, the static value will be overridden.
  *
  * @abstract
  * @static
@@ -2854,7 +2930,7 @@ OO.ui.Dialog.static.title = '';
  * An array of configured {@link OO.ui.ActionWidget action widgets}.
  *
  * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
- * value will be overriden.
+ * value will be overridden.
  *
  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
  *
@@ -2884,7 +2960,7 @@ OO.ui.Dialog.static.escapable = true;
  */
 OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
        if ( e.which === OO.ui.Keys.ESCAPE ) {
-               this.close();
+               this.executeAction( '' );
                e.preventDefault();
                e.stopPropagation();
        }
@@ -4071,10 +4147,10 @@ OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
 /**
  * Get tools from the factory
  *
- * @param {Array} include Included tools
- * @param {Array} exclude Excluded tools
- * @param {Array} promote Promoted tools
- * @param {Array} demote Demoted tools
+ * @param {Array|string} [include] Included tools, see #extract for format
+ * @param {Array|string} [exclude] Excluded tools, see #extract for format
+ * @param {Array|string} [promote] Promoted tools, see #extract for format
+ * @param {Array|string} [demote] Demoted tools, see #extract for format
  * @return {string[]} List of tools
  */
 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
@@ -4102,17 +4178,24 @@ OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, dem
 /**
  * Get a flat list of names from a list of names or groups.
  *
- * Tools can be specified in the following ways:
+ * Normally, `collection` is an array of tool specifications. Tools can be specified in the
+ * following ways:
+ *
+ * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
+ * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
+ *   tool to a group, use OO.ui.Tool.static.group.)
+ *
+ * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
+ * catch-all selector `'*'`.
  *
- * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
- * - All tools in a group: `{ group: 'group-name' }`
- * - All tools: `'*'`
+ * If `used` is passed, tool names that appear as properties in this object will be considered
+ * already assigned, and will not be returned even if specified otherwise. The tool names extracted
+ * by this function call will be added as new properties in the object.
  *
  * @private
- * @param {Array|string} collection List of tools
- * @param {Object} [used] Object with names that should be skipped as properties; extracted
- *  names will be added as properties
- * @return {string[]} List of extracted names
+ * @param {Array|string} collection List of tools, see above
+ * @param {Object} [used] Object containing information about used tools, see above
+ * @return {string[]} List of extracted tool names
  */
 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
        var i, len, item, name, tool,
@@ -4650,13 +4733,13 @@ OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
  * @param {jQuery.Event} e Mouse down event
  */
 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
-       if ( this.isDisabled() || e.which !== 1 ) {
+       if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
                return;
        }
        this.$element.addClass( 'oo-ui-buttonElement-pressed' );
        // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
        // reliably remove the pressed class
-       OO.ui.addCaptureEventListener( this.getElementDocument(), 'mouseup', this.onMouseUpHandler );
+       this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
        // Prevent change of focus unless specifically configured otherwise
        if ( this.constructor.static.cancelButtonMouseDownEvents ) {
                return false;
@@ -4670,12 +4753,12 @@ OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
  * @param {jQuery.Event} e Mouse up event
  */
 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
-       if ( this.isDisabled() || e.which !== 1 ) {
+       if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
                return;
        }
        this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
        // Stop listening for mouseup, since we only needed this once
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup', this.onMouseUpHandler );
+       this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
 };
 
 /**
@@ -4686,7 +4769,7 @@ OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
  * @fires click
  */
 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                if ( this.emit( 'click' ) ) {
                        return false;
                }
@@ -4706,7 +4789,7 @@ OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
        this.$element.addClass( 'oo-ui-buttonElement-pressed' );
        // Run the keyup handler no matter where the key is when the button is let go, so we can
        // reliably remove the pressed class
-       OO.ui.addCaptureEventListener( this.getElementDocument(), 'keyup', this.onKeyUpHandler );
+       this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
 };
 
 /**
@@ -4721,7 +4804,7 @@ OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
        }
        this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
        // Stop listening for keyup, since we only needed this once
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'keyup', this.onKeyUpHandler );
+       this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
 };
 
 /**
@@ -7224,6 +7307,11 @@ OO.ui.mixin.AccessKeyedElement.prototype.getAccessKey = function () {
  * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
  * which creates the tools on demand.
  *
+ * Every Tool subclass must implement two methods:
+ *
+ * - {@link #onUpdateState}
+ * - {@link #onSelect}
+ *
  * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
  * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
  * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
@@ -7396,7 +7484,7 @@ OO.ui.Tool.static.autoAddToGroup = true;
 /**
  * Check if this tool is compatible with given data.
  *
- * This is a stub that can be overriden to provide support for filtering tools based on an
+ * This is a stub that can be overridden to provide support for filtering tools based on an
  * arbitrary piece of information  (e.g., where the cursor is in a document). The implementation
  * must also call this method so that the compatibility check can be performed.
  *
@@ -7412,7 +7500,11 @@ OO.ui.Tool.static.isCompatibleWith = function () {
 /* Methods */
 
 /**
- * Handle the toolbar state being updated.
+ * Handle the toolbar state being updated. This method is called when the
+ * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
+ * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
+ * depending on application state (usually by calling #setDisabled to enable or disable the tool,
+ * or #setActive to mark is as currently in-use or not).
  *
  * This is an abstract method that must be overridden in a concrete subclass.
  *
@@ -7423,7 +7515,8 @@ OO.ui.Tool.static.isCompatibleWith = function () {
 OO.ui.Tool.prototype.onUpdateState = null;
 
 /**
- * Handle the tool being selected.
+ * Handle the tool being selected. This method is called when the user triggers this tool,
+ * usually by clicking on its label/icon.
  *
  * This is an abstract method that must be overridden in a concrete subclass.
  *
@@ -7542,6 +7635,13 @@ OO.ui.Tool.prototype.destroy = function () {
  * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
  * any order, but each can only appear once in the toolbar.
  *
+ * The toolbar can be synchronized with the state of the external "application", like a text
+ * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
+ * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
+ * tool would be disabled while the user is not editing a table). A state change is signalled by
+ * emitting the {@link #event-updateState 'updateState' event}, which calls Tools'
+ * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
+ *
  * The following is an example of a basic toolbar.
  *
  *     @example
@@ -7557,23 +7657,24 @@ OO.ui.Tool.prototype.destroy = function () {
  *     // Define the tools that we're going to place in our toolbar
  *
  *     // Create a class inheriting from OO.ui.Tool
- *     function ImageTool() {
- *         ImageTool.parent.apply( this, arguments );
+ *     function SearchTool() {
+ *         SearchTool.parent.apply( this, arguments );
  *     }
- *     OO.inheritClass( ImageTool, OO.ui.Tool );
+ *     OO.inheritClass( SearchTool, OO.ui.Tool );
  *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
  *     // of 'icon' and 'title' (displayed icon and text).
- *     ImageTool.static.name = 'image';
- *     ImageTool.static.icon = 'image';
- *     ImageTool.static.title = 'Insert image';
+ *     SearchTool.static.name = 'search';
+ *     SearchTool.static.icon = 'search';
+ *     SearchTool.static.title = 'Search...';
  *     // Defines the action that will happen when this tool is selected (clicked).
- *     ImageTool.prototype.onSelect = function () {
- *         $area.text( 'Image tool clicked!' );
+ *     SearchTool.prototype.onSelect = function () {
+ *         $area.text( 'Search tool clicked!' );
  *         // Never display this tool as "active" (selected).
  *         this.setActive( false );
  *     };
+ *     SearchTool.prototype.onUpdateState = function () {};
  *     // Make this tool available in our toolFactory and thus our toolbar
- *     toolFactory.register( ImageTool );
+ *     toolFactory.register( SearchTool );
  *
  *     // Register two more tools, nothing interesting here
  *     function SettingsTool() {
@@ -7587,6 +7688,7 @@ OO.ui.Tool.prototype.destroy = function () {
  *         $area.text( 'Settings tool clicked!' );
  *         this.setActive( false );
  *     };
+ *     SettingsTool.prototype.onUpdateState = function () {};
  *     toolFactory.register( SettingsTool );
  *
  *     // Register two more tools, nothing interesting here
@@ -7601,6 +7703,7 @@ OO.ui.Tool.prototype.destroy = function () {
  *         $area.text( 'More stuff tool clicked!' );
  *         this.setActive( false );
  *     };
+ *     StuffTool.prototype.onUpdateState = function () {};
  *     toolFactory.register( StuffTool );
  *
  *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
@@ -7625,7 +7728,7 @@ OO.ui.Tool.prototype.destroy = function () {
  *         {
  *             // 'bar' tool groups display tools' icons only, side-by-side.
  *             type: 'bar',
- *             include: [ 'image', 'help' ]
+ *             include: [ 'search', 'help' ]
  *         },
  *         {
  *             // 'list' tool groups display both the titles and icons, in a dropdown list.
@@ -7657,9 +7760,10 @@ OO.ui.Tool.prototype.destroy = function () {
  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
  *     // document.
  *     toolbar.initialize();
+ *     toolbar.emit( 'updateState' );
  *
  * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
- * 'updateState' event.
+ * {@link #event-updateState 'updateState' event}.
  *
  *     @example
  *     // Create the toolbar
@@ -7673,28 +7777,24 @@ OO.ui.Tool.prototype.destroy = function () {
  *     // Define the tools that we're going to place in our toolbar
  *
  *     // Create a class inheriting from OO.ui.Tool
- *     function ImageTool() {
- *         ImageTool.parent.apply( this, arguments );
+ *     function SearchTool() {
+ *         SearchTool.parent.apply( this, arguments );
  *     }
- *     OO.inheritClass( ImageTool, OO.ui.Tool );
+ *     OO.inheritClass( SearchTool, OO.ui.Tool );
  *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
  *     // of 'icon' and 'title' (displayed icon and text).
- *     ImageTool.static.name = 'image';
- *     ImageTool.static.icon = 'image';
- *     ImageTool.static.title = 'Insert image';
+ *     SearchTool.static.name = 'search';
+ *     SearchTool.static.icon = 'search';
+ *     SearchTool.static.title = 'Search...';
  *     // Defines the action that will happen when this tool is selected (clicked).
- *     ImageTool.prototype.onSelect = function () {
- *         $area.text( 'Image tool clicked!' );
+ *     SearchTool.prototype.onSelect = function () {
+ *         $area.text( 'Search tool clicked!' );
  *         // Never display this tool as "active" (selected).
  *         this.setActive( false );
  *     };
- *     // The toolbar can be synchronized with the state of some external stuff, like a text
- *     // editor's editing area, highlighting the tools (e.g. a 'bold' tool would be shown as active
- *     // when the text cursor was inside bolded text). Here we simply disable this feature.
- *     ImageTool.prototype.onUpdateState = function () {
- *     };
+ *     SearchTool.prototype.onUpdateState = function () {};
  *     // Make this tool available in our toolFactory and thus our toolbar
- *     toolFactory.register( ImageTool );
+ *     toolFactory.register( SearchTool );
  *
  *     // Register two more tools, nothing interesting here
  *     function SettingsTool() {
@@ -7713,8 +7813,7 @@ OO.ui.Tool.prototype.destroy = function () {
  *         // To update the menu label
  *         this.toolbar.emit( 'updateState' );
  *     };
- *     SettingsTool.prototype.onUpdateState = function () {
- *     };
+ *     SettingsTool.prototype.onUpdateState = function () {};
  *     toolFactory.register( SettingsTool );
  *
  *     // Register two more tools, nothing interesting here
@@ -7734,8 +7833,7 @@ OO.ui.Tool.prototype.destroy = function () {
  *         // To update the menu label
  *         this.toolbar.emit( 'updateState' );
  *     };
- *     StuffTool.prototype.onUpdateState = function () {
- *     };
+ *     StuffTool.prototype.onUpdateState = function () {};
  *     toolFactory.register( StuffTool );
  *
  *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
@@ -7760,7 +7858,7 @@ OO.ui.Tool.prototype.destroy = function () {
  *         {
  *             // 'bar' tool groups display tools' icons only, side-by-side.
  *             type: 'bar',
- *             include: [ 'image', 'help' ]
+ *             include: [ 'search', 'help' ]
  *         },
  *         {
  *             // 'menu' tool groups display both the titles and icons, in a dropdown menu.
@@ -7858,6 +7956,18 @@ OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
 
+/* Events */
+
+/**
+ * @event updateState
+ *
+ * An 'updateState' event must be emitted on the Toolbar (by calling `toolbar.emit( 'updateState' )`)
+ * every time the state of the application using the toolbar changes, and an update to the state of
+ * tools is required.
+ *
+ * @param {Mixed...} data Application-defined parameters
+ */
+
 /* Methods */
 
 /**
@@ -8037,19 +8147,9 @@ OO.ui.Toolbar.prototype.getToolAccelerator = function () {
  * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
  * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
  *
- * Toolgroups can contain individual tools, groups of tools, or all available tools:
- *
- * To include an individual tool (or array of individual tools), specify tools by symbolic name:
- *
- *      include: [ 'tool-name' ] or [ { name: 'tool-name' }]
- *
- * To include a group of tools, specify the group name. (The tool's static ‘group’ config is used to assign the tool to a group.)
- *
- *      include: [ { group: 'group-name' } ]
- *
- *  To include all tools that are not yet assigned to a toolgroup, use the catch-all selector, an asterisk (*):
- *
- *      include: '*'
+ * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
+ * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
+ * The options `exclude`, `promote`, and `demote` support the same formats.
  *
  * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
  * please see the [OOjs UI documentation on MediaWiki][1].
@@ -8064,10 +8164,10 @@ OO.ui.Toolbar.prototype.getToolAccelerator = function () {
  * @constructor
  * @param {OO.ui.Toolbar} toolbar
  * @param {Object} [config] Configuration options
- * @cfg {Array|string} [include=[]] List of tools to include in the toolgroup.
- * @cfg {Array|string} [exclude=[]] List of tools to exclude from the toolgroup.
- * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning of the toolgroup.
- * @cfg {Array|string} [demote=[]] List of tools to demote to the end of the toolgroup.
+ * @cfg {Array|string} [include] List of tools to include in the toolgroup, see above.
+ * @cfg {Array|string} [exclude] List of tools to exclude from the toolgroup, see above.
+ * @cfg {Array|string} [promote] List of tools to promote to the beginning of the toolgroup, see above.
+ * @cfg {Array|string} [demote] List of tools to demote to the end of the toolgroup, see above.
  *  This setting is particularly useful when tools have been added to the toolgroup
  *  en masse (e.g., via the catch-all selector).
  */
@@ -8203,13 +8303,13 @@ OO.ui.ToolGroup.prototype.updateDisabled = function () {
 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
        if (
                !this.isDisabled() &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                this.pressed = this.getTargetTool( e );
                if ( this.pressed ) {
                        this.pressed.setActive( true );
-                       OO.ui.addCaptureEventListener( this.getElementDocument(), 'mouseup', this.onCapturedMouseKeyUpHandler );
-                       OO.ui.addCaptureEventListener( this.getElementDocument(), 'keyup', this.onCapturedMouseKeyUpHandler );
+                       this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
+                       this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
                }
                return false;
        }
@@ -8222,8 +8322,8 @@ OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
  * @param {Event} e Mouse up or key up event
  */
 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup', this.onCapturedMouseKeyUpHandler );
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'keyup', this.onCapturedMouseKeyUpHandler );
+       this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
+       this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
        // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
        // released, but since `this.pressed` will no longer be true, the second call will be ignored.
        this.onMouseKeyUp( e );
@@ -8240,7 +8340,7 @@ OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
 
        if (
                !this.isDisabled() && this.pressed && this.pressed === tool &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                this.pressed.onSelect();
                this.pressed = null;
@@ -8487,6 +8587,7 @@ OO.ui.MessageDialog.static.title = null;
  */
 OO.ui.MessageDialog.static.message = null;
 
+// Note that OO.ui.alert() and OO.ui.confirm() rely on these.
 OO.ui.MessageDialog.static.actions = [
        { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
        { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
@@ -11485,23 +11586,24 @@ OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
  *     // Define the tools that we're going to place in our toolbar
  *
  *     // Create a class inheriting from OO.ui.Tool
- *     function ImageTool() {
- *         ImageTool.parent.apply( this, arguments );
+ *     function SearchTool() {
+ *         SearchTool.parent.apply( this, arguments );
  *     }
- *     OO.inheritClass( ImageTool, OO.ui.Tool );
+ *     OO.inheritClass( SearchTool, OO.ui.Tool );
  *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
  *     // of 'icon' and 'title' (displayed icon and text).
- *     ImageTool.static.name = 'image';
- *     ImageTool.static.icon = 'image';
- *     ImageTool.static.title = 'Insert image';
+ *     SearchTool.static.name = 'search';
+ *     SearchTool.static.icon = 'search';
+ *     SearchTool.static.title = 'Search...';
  *     // Defines the action that will happen when this tool is selected (clicked).
- *     ImageTool.prototype.onSelect = function () {
- *         $area.text( 'Image tool clicked!' );
+ *     SearchTool.prototype.onSelect = function () {
+ *         $area.text( 'Search tool clicked!' );
  *         // Never display this tool as "active" (selected).
  *         this.setActive( false );
  *     };
+ *     SearchTool.prototype.onUpdateState = function () {};
  *     // Make this tool available in our toolFactory and thus our toolbar
- *     toolFactory.register( ImageTool );
+ *     toolFactory.register( SearchTool );
  *
  *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
  *     // little popup window (a PopupWidget).
@@ -11525,7 +11627,7 @@ OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
  *         {
  *             // 'bar' tool groups display tools by icon only
  *             type: 'bar',
- *             include: [ 'image', 'help' ]
+ *             include: [ 'search', 'help' ]
  *         }
  *     ] );
  *
@@ -11706,7 +11808,7 @@ OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
        // Only close toolgroup when a tool was actually selected
        if (
                !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                this.setActive( false );
        }
@@ -11722,7 +11824,7 @@ OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
        if (
                !this.isDisabled() &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                return false;
        }
@@ -11737,7 +11839,7 @@ OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
        if (
                !this.isDisabled() &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                this.setActive( !this.active );
                return false;
@@ -11756,8 +11858,8 @@ OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
        if ( this.active !== value ) {
                this.active = value;
                if ( value ) {
-                       OO.ui.addCaptureEventListener( this.getElementDocument(), 'mouseup', this.onBlurHandler );
-                       OO.ui.addCaptureEventListener( this.getElementDocument(), 'keyup', this.onBlurHandler );
+                       this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
+                       this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
 
                        this.$clippable.css( 'left', '' );
                        // Try anchoring the popup to the left first
@@ -11785,8 +11887,8 @@ OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
                                } );
                        }
                } else {
-                       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup', this.onBlurHandler );
-                       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'keyup', this.onBlurHandler );
+                       this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
+                       this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
                        this.$element.removeClass(
                                'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left  oo-ui-popupToolGroup-right'
                        );
@@ -11827,6 +11929,7 @@ OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
  *     SettingsTool.prototype.onSelect = function () {
  *         this.setActive( false );
  *     };
+ *     SettingsTool.prototype.onUpdateState = function () {};
  *     toolFactory.register( SettingsTool );
  *     // Register two more tools, nothing interesting here
  *     function StuffTool() {
@@ -11834,11 +11937,12 @@ OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
  *     }
  *     OO.inheritClass( StuffTool, OO.ui.Tool );
  *     StuffTool.static.name = 'stuff';
- *     StuffTool.static.icon = 'ellipsis';
+ *     StuffTool.static.icon = 'search';
  *     StuffTool.static.title = 'Change the world';
  *     StuffTool.prototype.onSelect = function () {
  *         this.setActive( false );
  *     };
+ *     StuffTool.prototype.onUpdateState = function () {};
  *     toolFactory.register( StuffTool );
  *     toolbar.setup( [
  *         {
@@ -11846,7 +11950,7 @@ OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
  *             type: 'list',
  *             label: 'ListToolGroup',
  *             indicator: 'down',
- *             icon: 'image',
+ *             icon: 'ellipsis',
  *             title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
  *             header: 'This is the header',
  *             include: [ 'settings', 'stuff' ],
@@ -11981,7 +12085,7 @@ OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
        // Do not close the popup when the user wants to show more/fewer tools
        if (
                $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
-               ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+               ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
        ) {
                // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
                // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
@@ -12012,8 +12116,7 @@ OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
  * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
  *
  * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
- * is set up. Note that all tools must define an {@link OO.ui.Tool#onUpdateState onUpdateState} method if
- * a MenuToolGroup is used.
+ * is set up.
  *
  *     @example
  *     // Example of a MenuToolGroup
@@ -12042,8 +12145,7 @@ OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
  *         // To update the menu label
  *         this.toolbar.emit( 'updateState' );
  *     };
- *     SettingsTool.prototype.onUpdateState = function () {
- *     };
+ *     SettingsTool.prototype.onUpdateState = function () {};
  *     toolFactory.register( SettingsTool );
  *
  *     function StuffTool() {
@@ -12062,8 +12164,7 @@ OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
  *         // To update the menu label
  *         this.toolbar.emit( 'updateState' );
  *     };
- *     StuffTool.prototype.onUpdateState = function () {
- *     };
+ *     StuffTool.prototype.onUpdateState = function () {};
  *     toolFactory.register( StuffTool );
  *
  *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
@@ -13400,6 +13501,7 @@ OO.ui.CapsuleMultiSelectWidget = function OoUiCapsuleMultiSelectWidget( config )
        OO.ui.mixin.IconElement.call( this, config );
 
        // Properties
+       this.$content = $( '<div>' );
        this.allowArbitrary = !!config.allowArbitrary;
        this.$overlay = config.$overlay || this.$element;
        this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
@@ -13431,7 +13533,8 @@ OO.ui.CapsuleMultiSelectWidget = function OoUiCapsuleMultiSelectWidget( config )
                this.$input.on( {
                        focus: this.onInputFocus.bind( this ),
                        blur: this.onInputBlur.bind( this ),
-                       'propertychange change click mouseup keydown keyup input cut paste select': this.onInputChange.bind( this ),
+                       'propertychange change click mouseup keydown keyup input cut paste select focus':
+                               OO.ui.debounce( this.updateInputSize.bind( this ) ),
                        keydown: this.onKeyDown.bind( this ),
                        keypress: this.onKeyPress.bind( this )
                } );
@@ -13442,7 +13545,7 @@ OO.ui.CapsuleMultiSelectWidget = function OoUiCapsuleMultiSelectWidget( config )
                remove: 'onMenuItemsChange'
        } );
        this.$handle.on( {
-               click: this.onClick.bind( this )
+               mousedown: this.onMouseDown.bind( this )
        } );
 
        // Initialization
@@ -13452,21 +13555,23 @@ OO.ui.CapsuleMultiSelectWidget = function OoUiCapsuleMultiSelectWidget( config )
                        role: 'combobox',
                        'aria-autocomplete': 'list'
                } );
-               this.$input.width( '1em' );
+               this.updateInputSize();
        }
        if ( config.data ) {
                this.setItemsFromData( config.data );
        }
+       this.$content.addClass( 'oo-ui-capsuleMultiSelectWidget-content' )
+               .append( this.$group );
        this.$group.addClass( 'oo-ui-capsuleMultiSelectWidget-group' );
        this.$handle.addClass( 'oo-ui-capsuleMultiSelectWidget-handle' )
-               .append( this.$indicator, this.$icon, this.$group );
+               .append( this.$indicator, this.$icon, this.$content );
        this.$element.addClass( 'oo-ui-capsuleMultiSelectWidget' )
                .append( this.$handle );
        if ( this.popup ) {
-               this.$handle.append( $tabFocus );
+               this.$content.append( $tabFocus );
                this.$overlay.append( this.popup.$element );
        } else {
-               this.$handle.append( this.$input );
+               this.$content.append( this.$input );
                this.$overlay.append( this.menu.$element );
        }
        this.onMenuItemsChange();
@@ -13631,6 +13736,7 @@ OO.ui.CapsuleMultiSelectWidget.prototype.addItems = function ( items ) {
        }
        if ( !same ) {
                this.emit( 'change', this.getItemsData() );
+               this.menu.position();
        }
 
        return this;
@@ -13655,6 +13761,7 @@ OO.ui.CapsuleMultiSelectWidget.prototype.removeItems = function ( items ) {
        }
        if ( !same ) {
                this.emit( 'change', this.getItemsData() );
+               this.menu.position();
        }
 
        return this;
@@ -13667,6 +13774,7 @@ OO.ui.CapsuleMultiSelectWidget.prototype.clearItems = function () {
        if ( this.items.length ) {
                OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
                this.emit( 'change', this.getItemsData() );
+               this.menu.position();
        }
        return this;
 };
@@ -13742,15 +13850,17 @@ OO.ui.CapsuleMultiSelectWidget.prototype.onPopupFocusOut = function () {
 };
 
 /**
- * Handle mouse click events.
+ * Handle mouse down events.
  *
  * @private
- * @param {jQuery.Event} e Mouse click event
+ * @param {jQuery.Event} e Mouse down event
  */
-OO.ui.CapsuleMultiSelectWidget.prototype.onClick = function ( e ) {
-       if ( e.which === 1 ) {
+OO.ui.CapsuleMultiSelectWidget.prototype.onMouseDown = function ( e ) {
+       if ( e.which === OO.ui.MouseButtons.LEFT ) {
                this.focus();
                return false;
+       } else {
+               this.updateInputSize();
        }
 };
 
@@ -13784,7 +13894,7 @@ OO.ui.CapsuleMultiSelectWidget.prototype.onKeyPress = function ( e ) {
                        }
 
                        // Make sure the input gets resized.
-                       setTimeout( this.onInputChange.bind( this ), 0 );
+                       setTimeout( this.updateInputSize.bind( this ), 0 );
                }
        }
 };
@@ -13808,14 +13918,42 @@ OO.ui.CapsuleMultiSelectWidget.prototype.onKeyDown = function ( e ) {
 };
 
 /**
- * Handle input change events.
+ * Update the dimensions of the text input field to encompass all available area.
  *
  * @private
  * @param {jQuery.Event} e Event of some sort
  */
-OO.ui.CapsuleMultiSelectWidget.prototype.onInputChange = function () {
+OO.ui.CapsuleMultiSelectWidget.prototype.updateInputSize = function () {
+       var $lastItem, direction, contentWidth, currentWidth, bestWidth;
        if ( !this.isDisabled() ) {
-               this.$input.width( this.$input.val().length + 'em' );
+               this.$input.css( 'width', '1em' );
+               $lastItem = this.$group.children().last();
+               direction = OO.ui.Element.static.getDir( this.$handle );
+               contentWidth = this.$input[ 0 ].scrollWidth;
+               currentWidth = this.$input.width();
+
+               if ( contentWidth < currentWidth ) {
+                       // All is fine, don't perform expensive calculations
+                       return;
+               }
+
+               if ( !$lastItem.length ) {
+                       bestWidth = this.$content.innerWidth();
+               } else {
+                       bestWidth = direction === 'ltr' ?
+                               this.$content.innerWidth() - $lastItem.position().left - $lastItem.outerWidth() :
+                               $lastItem.position().left;
+               }
+               // Some safety margin for sanity, because I *really* don't feel like finding out where the few
+               // pixels this is off by are coming from.
+               bestWidth -= 10;
+               if ( contentWidth > bestWidth ) {
+                       // This will result in the input getting shifted to the next line
+                       bestWidth = this.$content.innerWidth() - 10;
+               }
+               this.$input.width( Math.floor( bestWidth ) );
+
+               this.menu.position();
        }
 };
 
@@ -13849,7 +13987,7 @@ OO.ui.CapsuleMultiSelectWidget.prototype.onMenuItemsChange = function () {
 OO.ui.CapsuleMultiSelectWidget.prototype.clearInput = function () {
        if ( this.$input ) {
                this.$input.val( '' );
-               this.$input.width( '1em' );
+               this.updateInputSize();
        }
        if ( this.popup ) {
                this.popup.toggle( false );
@@ -13902,6 +14040,7 @@ OO.ui.CapsuleMultiSelectWidget.prototype.focus = function () {
                                .first()
                                .focus();
                } else {
+                       this.updateInputSize();
                        this.menu.toggle( true );
                        this.$input.focus();
                }
@@ -14140,7 +14279,7 @@ OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
  * @param {jQuery.Event} e Mouse click event
  */
 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                this.menu.toggle();
        }
        return false;
@@ -14234,7 +14373,7 @@ OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
 
        this.selectButton = new OO.ui.ButtonWidget( {
                classes: [ 'oo-ui-selectFileWidget-selectButton' ],
-               label: 'Select a file',
+               label: OO.ui.msg( 'ooui-selectfile-button-select' ),
                disabled: this.disabled || !this.isSupported
        } );
 
@@ -14382,10 +14521,6 @@ OO.ui.SelectFileWidget.prototype.updateUI = function () {
                        this.setLabel( this.placeholder );
                }
        }
-
-       if ( this.$input ) {
-               this.$input.attr( 'title', this.getLabel() );
-       }
 };
 
 /**
@@ -14406,8 +14541,7 @@ OO.ui.SelectFileWidget.prototype.addInput = function () {
        this.$input = $( '<input type="file">' );
        this.$input.on( 'change', this.onFileSelectedHandler );
        this.$input.attr( {
-               tabindex: -1,
-               title: this.getLabel()
+               tabindex: -1
        } );
        if ( this.accept ) {
                this.$input.attr( 'accept', this.accept.join( ', ' ) );
@@ -15910,7 +16044,7 @@ OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
  * @fires icon
  */
 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
-       if ( e.which === 1 ) {
+       if ( e.which === OO.ui.MouseButtons.LEFT ) {
                this.$input[ 0 ].focus();
                return false;
        }
@@ -15924,7 +16058,7 @@ OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
  * @fires indicator
  */
 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
-       if ( e.which === 1 ) {
+       if ( e.which === OO.ui.MouseButtons.LEFT ) {
                if ( this.type === 'search' ) {
                        // Clear the text field
                        this.setValue( '' );
@@ -16208,7 +16342,7 @@ OO.ui.TextInputWidget.prototype.isAutosizing = function () {
  * @chainable
  */
 OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
-       var textRange, isBackwards, start, end,
+       var isBackwards, start, end,
                input = this.$input[ 0 ];
 
        to = to || from;
@@ -16219,16 +16353,7 @@ OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
 
        this.focus();
 
-       if ( input.setSelectionRange ) {
-               input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
-       } else if ( input.createTextRange ) {
-               // IE 8 and below
-               textRange = input.createTextRange();
-               textRange.collapse( true );
-               textRange.moveStart( 'character', start );
-               textRange.moveEnd( 'character', end - start );
-               textRange.select();
-       }
+       input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
        return this;
 };
 
@@ -16288,6 +16413,47 @@ OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
        return this.selectRange( this.getInputLength() );
 };
 
+/**
+ * Insert new content into the input.
+ *
+ * @param {string} content Content to be inserted
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.insertContent = function ( content ) {
+       var start, end,
+               range = this.getRange(),
+               value = this.getValue();
+
+       start = Math.min( range.from, range.to );
+       end = Math.max( range.from, range.to );
+
+       this.setValue( value.slice( 0, start ) + content + value.slice( end ) );
+       this.selectRange( start + content.length );
+       return this;
+};
+
+/**
+ * Insert new content either side of a selection.
+ *
+ * @param {string} pre Content to be inserted before the selection
+ * @param {string} post Content to be inserted after the selection
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.encapsulateContent = function ( pre, post ) {
+       var start, end,
+               range = this.getRange(),
+               offset = pre.length;
+
+       start = Math.min( range.from, range.to );
+       end = Math.max( range.from, range.to );
+
+       this.selectRange( start ).insertContent( pre );
+       this.selectRange( offset + end ).insertContent( post );
+
+       this.selectRange( offset + start, offset + end );
+       return this;
+};
+
 /**
  * Set the validation pattern.
  *
@@ -16413,14 +16579,6 @@ OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
        return this;
 };
 
-/**
- * Deprecated alias of #setLabelPosition
- *
- * @deprecated Use setLabelPosition instead.
- */
-OO.ui.TextInputWidget.prototype.setPosition =
-       OO.ui.TextInputWidget.prototype.setLabelPosition;
-
 /**
  * Update the position of the inline label.
  *
@@ -16662,7 +16820,7 @@ OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
  * @param {jQuery.Event} e Mouse click event
  */
 OO.ui.ComboBoxInputWidget.prototype.onIndicatorClick = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                this.menu.toggle();
                this.$input[ 0 ].focus();
        }
@@ -17417,7 +17575,7 @@ OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
  *
  * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
  *
- * @param {boolean} movable Item is removable
+ * @param {boolean} removable Item is removable
  * @chainable
  */
 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
@@ -17641,7 +17799,7 @@ OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
  */
 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
        // Capture clicks outside popup
-       OO.ui.addCaptureEventListener( this.getElementWindow(), 'mousedown', this.onMouseDownHandler );
+       this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
 };
 
 /**
@@ -17661,7 +17819,7 @@ OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
  * @private
  */
 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
-       OO.ui.removeCaptureEventListener( this.getElementWindow(), 'mousedown', this.onMouseDownHandler );
+       this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
 };
 
 /**
@@ -17687,7 +17845,7 @@ OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
  * @private
  */
 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
-       OO.ui.addCaptureEventListener( this.getElementWindow(), 'keydown', this.onDocumentKeyDownHandler );
+       this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
 };
 
 /**
@@ -17696,7 +17854,7 @@ OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
  * @private
  */
 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
-       OO.ui.removeCaptureEventListener( this.getElementWindow(), 'keydown', this.onDocumentKeyDownHandler );
+       this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
 };
 
 /**
@@ -18275,22 +18433,14 @@ OO.ui.SelectWidget.static.passAllFilter = function () {
 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
        var item;
 
-       if ( !this.isDisabled() && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                this.togglePressed( true );
                item = this.getTargetItem( e );
                if ( item && item.isSelectable() ) {
                        this.pressItem( item );
                        this.selecting = item;
-                       OO.ui.addCaptureEventListener(
-                               this.getElementDocument(),
-                               'mouseup',
-                               this.onMouseUpHandler
-                       );
-                       OO.ui.addCaptureEventListener(
-                               this.getElementDocument(),
-                               'mousemove',
-                               this.onMouseMoveHandler
-                       );
+                       this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
+                       this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler, true );
                }
        }
        return false;
@@ -18312,16 +18462,14 @@ OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
                        this.selecting = item;
                }
        }
-       if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT && this.selecting ) {
                this.pressItem( null );
                this.chooseItem( this.selecting );
                this.selecting = null;
        }
 
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup',
-               this.onMouseUpHandler );
-       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mousemove',
-               this.onMouseMoveHandler );
+       this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
+       this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler, true );
 
        return false;
 };
@@ -18441,7 +18589,7 @@ OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
  * @protected
  */
 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
-       OO.ui.addCaptureEventListener( this.getElementWindow(), 'keydown', this.onKeyDownHandler );
+       this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
 };
 
 /**
@@ -18450,7 +18598,7 @@ OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
  * @protected
  */
 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
-       OO.ui.removeCaptureEventListener( this.getElementWindow(), 'keydown', this.onKeyDownHandler );
+       this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
 };
 
 /**
@@ -18559,7 +18707,7 @@ OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
  * @protected
  */
 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
-       OO.ui.addCaptureEventListener( this.getElementWindow(), 'keypress', this.onKeyPressHandler );
+       this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
 };
 
 /**
@@ -18571,7 +18719,7 @@ OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
  * @protected
  */
 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
-       OO.ui.removeCaptureEventListener( this.getElementWindow(), 'keypress', this.onKeyPressHandler );
+       this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
        this.clearKeyPressBuffer();
 };
 
@@ -19377,12 +19525,12 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
 
                        // Auto-hide
                        if ( this.autoHide ) {
-                               OO.ui.addCaptureEventListener( this.getElementDocument(), 'mousedown', this.onDocumentMouseDownHandler );
+                               this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
                        }
                } else {
                        this.unbindKeyDownListener();
                        this.unbindKeyPressListener();
-                       OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mousedown', this.onDocumentMouseDownHandler );
+                       this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
                        this.toggleClipping( false );
                }
        }
@@ -19971,7 +20119,7 @@ OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
  * @param {jQuery.Event} e Mouse click event
  */
 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                this.setValue( !this.value );
        }
        return false;