Merge "Use IDatabase type hints in /maintenance"
[lhc/web/wiklou.git] / resources / lib / oojs / oojs.jquery.js
index 9395ecf..f93051c 100644 (file)
@@ -1,18 +1,18 @@
 /*!
- * OOjs v1.1.9 optimised for jQuery
+ * OOjs v2.0.0 optimised for jQuery
  * https://www.mediawiki.org/wiki/OOjs
  *
- * Copyright 2011-2015 OOjs Team and other contributors.
+ * Copyright 2011-2017 OOjs Team and other contributors.
  * Released under the MIT license
- * http://oojs.mit-license.org
+ * https://oojs.mit-license.org
  *
- * Date: 2015-08-25T21:35:29Z
+ * Date: 2017-04-05T02:18:04Z
  */
 ( function ( global ) {
 
 'use strict';
 
-/*exported toString */
+/* exported toString */
 var
        /**
         * Namespace for all classes, static methods and static properties.
@@ -22,21 +22,7 @@ var
        oo = {},
        // Optimisation: Local reference to Object.prototype.hasOwnProperty
        hasOwn = oo.hasOwnProperty,
-       toString = oo.toString,
-       // Object.create() is impossible to fully polyfill, so don't require it
-       createObject = Object.create || ( function () {
-               // Reusable constructor function
-               function Empty() {}
-               return function ( prototype, properties ) {
-                       var obj;
-                       Empty.prototype = prototype;
-                       obj = new Empty();
-                       if ( properties && hasOwn.call( properties, 'constructor' ) ) {
-                               obj.constructor = properties.constructor.value;
-                       }
-                       return obj;
-               };
-       } )();
+       toString = oo.toString;
 
 /* Class Methods */
 
@@ -90,19 +76,25 @@ oo.initClass = function ( fn ) {
  * @throws {Error} If target already inherits from origin
  */
 oo.inheritClass = function ( targetFn, originFn ) {
+       var targetConstructor;
+
+       if ( !originFn ) {
+               throw new Error( 'inheritClass: Origin is not a function (actually ' + originFn + ')' );
+       }
        if ( targetFn.prototype instanceof originFn ) {
-               throw new Error( 'Target already inherits from origin' );
+               throw new Error( 'inheritClass: Target already inherits from origin' );
        }
 
-       var targetConstructor = targetFn.prototype.constructor;
+       targetConstructor = targetFn.prototype.constructor;
 
        // Using ['super'] instead of .super because 'super' is not supported
        // by IE 8 and below (bug 63303).
        // Provide .parent as alias for code supporting older browsers which
        // allows people to comply with their style guide.
-       targetFn['super'] = targetFn.parent = originFn;
+       // eslint-disable-next-line dot-notation
+       targetFn[ 'super' ] = targetFn.parent = originFn;
 
-       targetFn.prototype = createObject( originFn.prototype, {
+       targetFn.prototype = Object.create( originFn.prototype, {
                // Restore constructor property of targetFn
                constructor: {
                        value: targetConstructor,
@@ -114,7 +106,7 @@ oo.inheritClass = function ( targetFn, originFn ) {
 
        // Extend static properties - always initialize both sides
        oo.initClass( originFn );
-       targetFn.static = createObject( originFn.static );
+       targetFn.static = Object.create( originFn.static );
 };
 
 /**
@@ -151,10 +143,14 @@ oo.inheritClass = function ( targetFn, originFn ) {
 oo.mixinClass = function ( targetFn, originFn ) {
        var key;
 
+       if ( !originFn ) {
+               throw new Error( 'mixinClass: Origin is not a function (actually ' + originFn + ')' );
+       }
+
        // Copy prototype properties
        for ( key in originFn.prototype ) {
                if ( key !== 'constructor' && hasOwn.call( originFn.prototype, key ) ) {
-                       targetFn.prototype[key] = originFn.prototype[key];
+                       targetFn.prototype[ key ] = originFn.prototype[ key ];
                }
        }
 
@@ -163,7 +159,7 @@ oo.mixinClass = function ( targetFn, originFn ) {
        if ( originFn.static ) {
                for ( key in originFn.static ) {
                        if ( hasOwn.call( originFn.static, key ) ) {
-                               targetFn.static[key] = originFn.static[key];
+                               targetFn.static[ key ] = originFn.static[ key ];
                        }
                }
        } else {
@@ -171,20 +167,33 @@ oo.mixinClass = function ( targetFn, originFn ) {
        }
 };
 
+/**
+ * Test whether one class is a subclass of another, without instantiating it.
+ *
+ * Every class is considered a subclass of Object and of itself.
+ *
+ * @param {Function} testFn The class to be tested
+ * @param {Function} baseFn The base class
+ * @return {boolean} Whether testFn is a subclass of baseFn (or equal to it)
+ */
+oo.isSubclass = function ( testFn, baseFn ) {
+       return testFn === baseFn || testFn.prototype instanceof baseFn;
+};
+
 /* Object Methods */
 
 /**
  * Get a deeply nested property of an object using variadic arguments, protecting against
  * undefined property errors.
  *
- * `quux = oo.getProp( obj, 'foo', 'bar', 'baz' );` is equivalent to `quux = obj.foo.bar.baz;`
+ * `quux = OO.getProp( obj, 'foo', 'bar', 'baz' );` is equivalent to `quux = obj.foo.bar.baz;`
  * except that the former protects against JS errors if one of the intermediate properties
  * is undefined. Instead of throwing an error, this function will return undefined in
  * that case.
  *
  * @param {Object} obj
- * @param {Mixed...} [keys]
- * @return obj[arguments[1]][arguments[2]].... or undefined
+ * @param {...Mixed} [keys]
+ * @return {Object|undefined} obj[arguments[1]][arguments[2]].... or undefined
  */
 oo.getProp = function ( obj ) {
        var i,
@@ -194,7 +203,7 @@ oo.getProp = function ( obj ) {
                        // Trying to access a property of undefined or null causes an error
                        return undefined;
                }
-               retval = retval[arguments[i]];
+               retval = retval[ arguments[ i ] ];
        }
        return retval;
 };
@@ -210,25 +219,53 @@ oo.getProp = function ( obj ) {
  * is not an object, this function will silently abort.
  *
  * @param {Object} obj
- * @param {Mixed...} [keys]
+ * @param {...Mixed} [keys]
  * @param {Mixed} [value]
  */
 oo.setProp = function ( obj ) {
        var i,
                prop = obj;
-       if ( Object( obj ) !== obj ) {
+       if ( Object( obj ) !== obj || arguments.length < 2 ) {
                return;
        }
        for ( i = 1; i < arguments.length - 2; i++ ) {
-               if ( prop[arguments[i]] === undefined ) {
-                       prop[arguments[i]] = {};
+               if ( prop[ arguments[ i ] ] === undefined ) {
+                       prop[ arguments[ i ] ] = {};
                }
-               if ( Object( prop[arguments[i]] ) !== prop[arguments[i]] ) {
+               if ( Object( prop[ arguments[ i ] ] ) !== prop[ arguments[ i ] ] ) {
                        return;
                }
-               prop = prop[arguments[i]];
+               prop = prop[ arguments[ i ] ];
+       }
+       prop[ arguments[ arguments.length - 2 ] ] = arguments[ arguments.length - 1 ];
+};
+
+/**
+ * Delete a deeply nested property of an object using variadic arguments, protecting against
+ * undefined property errors, and deleting resulting empty objects.
+ *
+ * @param {Object} obj
+ * @param {...Mixed} [keys]
+ */
+oo.deleteProp = function ( obj ) {
+       var i,
+               prop = obj,
+               props = [ prop ];
+       if ( Object( obj ) !== obj || arguments.length < 2 ) {
+               return;
+       }
+       for ( i = 1; i < arguments.length - 1; i++ ) {
+               if ( prop[ arguments[ i ] ] === undefined || Object( prop[ arguments[ i ] ] ) !== prop[ arguments[ i ] ] ) {
+                       return;
+               }
+               prop = prop[ arguments[ i ] ];
+               props.push( prop );
+       }
+       delete prop[ arguments[ i ] ];
+       // Walk back through props removing any plain empty objects
+       while ( ( prop = props.pop() ) && oo.isPlainObject( prop ) && !Object.keys( prop ).length ) {
+               delete props[ props.length - 1 ][ arguments[ props.length ] ];
        }
-       prop[arguments[arguments.length - 2]] = arguments[arguments.length - 1];
 };
 
 /**
@@ -256,11 +293,11 @@ oo.setProp = function ( obj ) {
 oo.cloneObject = function ( origin ) {
        var key, r;
 
-       r = createObject( origin.constructor.prototype );
+       r = Object.create( origin.constructor.prototype );
 
        for ( key in origin ) {
                if ( hasOwn.call( origin, key ) ) {
-                       r[key] = origin[key];
+                       r[ key ] = origin[ key ];
                }
        }
 
@@ -270,7 +307,7 @@ oo.cloneObject = function ( origin ) {
 /**
  * Get an array of all property values in an object.
  *
- * @param {Object} Object to get values from
+ * @param {Object} obj Object to get values from
  * @return {Array} List of object values
  */
 oo.getObjectValues = function ( obj ) {
@@ -283,13 +320,49 @@ oo.getObjectValues = function ( obj ) {
        values = [];
        for ( key in obj ) {
                if ( hasOwn.call( obj, key ) ) {
-                       values[values.length] = obj[key];
+                       values[ values.length ] = obj[ key ];
                }
        }
 
        return values;
 };
 
+/**
+ * Use binary search to locate an element in a sorted array.
+ *
+ * searchFunc is given an element from the array. `searchFunc(elem)` must return a number
+ * above 0 if the element we're searching for is to the right of (has a higher index than) elem,
+ * below 0 if it is to the left of elem, or zero if it's equal to elem.
+ *
+ * To search for a specific value with a comparator function (a `function cmp(a,b)` that returns
+ * above 0 if `a > b`, below 0 if `a < b`, and 0 if `a == b`), you can use
+ * `searchFunc = cmp.bind( null, value )`.
+ *
+ * @param {Array} arr Array to search in
+ * @param {Function} searchFunc Search function
+ * @param {boolean} [forInsertion] If not found, return index where val could be inserted
+ * @return {number|null} Index where val was found, or null if not found
+ */
+oo.binarySearch = function ( arr, searchFunc, forInsertion ) {
+       var mid, cmpResult,
+               left = 0,
+               right = arr.length;
+       while ( left < right ) {
+               // Equivalent to Math.floor( ( left + right ) / 2 ) but much faster
+               // eslint-disable-next-line no-bitwise
+               mid = ( left + right ) >> 1;
+               cmpResult = searchFunc( arr[ mid ] );
+               if ( cmpResult < 0 ) {
+                       right = mid;
+               } else if ( cmpResult > 0 ) {
+                       left = mid + 1;
+               } else {
+                       return mid;
+               }
+       }
+       return forInsertion ? right : null;
+};
+
 /**
  * Recursively compare properties between two objects.
  *
@@ -320,7 +393,7 @@ oo.compare = function ( a, b, asymmetrical ) {
        }
 
        for ( k in a ) {
-               if ( !hasOwn.call( a, k ) || a[k] === undefined || a[k] === b[k] ) {
+               if ( !hasOwn.call( a, k ) || a[ k ] === undefined || a[ k ] === b[ k ] ) {
                        // Support es3-shim: Without the hasOwn filter, comparing [] to {} will be false in ES3
                        // because the shimmed "forEach" is enumerable and shows up in Array but not Object.
                        // Also ignore undefined values, because there is no conceptual difference between
@@ -328,8 +401,8 @@ oo.compare = function ( a, b, asymmetrical ) {
                        continue;
                }
 
-               aValue = a[k];
-               bValue = b[k];
+               aValue = a[ k ];
+               bValue = b[ k ];
                aType = typeof aValue;
                bType = typeof bValue;
                if ( aType !== bType ||
@@ -387,7 +460,7 @@ oo.copy = function ( source, leafCallback, nodeCallback ) {
 
        // source is an array or a plain object
        for ( key in source ) {
-               destination[key] = oo.copy( source[key], leafCallback, nodeCallback );
+               destination[ key ] = oo.copy( source[ key ], leafCallback, nodeCallback );
        }
 
        // This is an internal node, so we don't apply the leafCallback.
@@ -438,13 +511,12 @@ oo.getHash.keySortReplacer = function ( key, val ) {
                i = 0;
                len = keys.length;
                for ( ; i < len; i += 1 ) {
-                       normalized[keys[i]] = val[keys[i]];
+                       normalized[ keys[ i ] ] = val[ keys[ i ] ];
                }
                return normalized;
-
-       // Primitive values and arrays get stable hashes
-       // by default. Lets those be stringified as-is.
        } else {
+               // Primitive values and arrays get stable hashes
+               // by default. Lets those be stringified as-is.
                return val;
        }
 };
@@ -472,7 +544,7 @@ oo.unique = function ( arr ) {
  * By building an object (with the values for keys) in parallel with
  * the array, a new item's existence in the union can be computed faster.
  *
- * @param {Array...} arrays Arrays to union
+ * @param {...Array} arrays Arrays to union
  * @return {Array} Union of the arrays
  */
 oo.simpleArrayUnion = function () {
@@ -481,11 +553,11 @@ oo.simpleArrayUnion = function () {
                result = [];
 
        for ( i = 0, ilen = arguments.length; i < ilen; i++ ) {
-               arr = arguments[i];
+               arr = arguments[ i ];
                for ( j = 0, jlen = arr.length; j < jlen; j++ ) {
-                       if ( !obj[ arr[j] ] ) {
-                               obj[ arr[j] ] = true;
-                               result.push( arr[j] );
+                       if ( !obj[ arr[ j ] ] ) {
+                               obj[ arr[ j ] ] = true;
+                               result.push( arr[ j ] );
                        }
                }
        }
@@ -515,13 +587,13 @@ function simpleArrayCombine( a, b, includeB ) {
                result = [];
 
        for ( i = 0, ilen = b.length; i < ilen; i++ ) {
-               bObj[ b[i] ] = true;
+               bObj[ b[ i ] ] = true;
        }
 
        for ( i = 0, ilen = a.length; i < ilen; i++ ) {
-               isInB = !!bObj[ a[i] ];
+               isInB = !!bObj[ a[ i ] ];
                if ( isInB === includeB ) {
-                       result.push( a[i] );
+                       result.push( a[ i ] );
                }
        }
 
@@ -554,11 +626,11 @@ oo.simpleArrayDifference = function ( a, b ) {
        return simpleArrayCombine( a, b, false );
 };
 
-/*global $ */
+/* global $ */
 
 oo.isPlainObject = $.isPlainObject;
 
-/*global hasOwn */
+/* global hasOwn */
 
 ( function () {
 
@@ -601,7 +673,7 @@ oo.isPlainObject = $.isPlainObject;
                        if ( context === undefined || context === null ) {
                                throw new Error( 'Method name "' + method + '" has no context.' );
                        }
-                       if ( typeof context[method] !== 'function' ) {
+                       if ( typeof context[ method ] !== 'function' ) {
                                // Technically the property could be replaced by a function before
                                // call time. But this probably signals a typo.
                                throw new Error( 'Property "' + method + '" is not a function' );
@@ -632,10 +704,10 @@ oo.isPlainObject = $.isPlainObject;
                validateMethod( method, context );
 
                if ( hasOwn.call( this.bindings, event ) ) {
-                       bindings = this.bindings[event];
+                       bindings = this.bindings[ event ];
                } else {
                        // Auto-initialize bindings list
-                       bindings = this.bindings[event] = [];
+                       bindings = this.bindings[ event ] = [];
                }
                // Add binding
                bindings.push( {
@@ -677,13 +749,13 @@ oo.isPlainObject = $.isPlainObject;
 
                if ( arguments.length === 1 ) {
                        // Remove all bindings for event
-                       delete this.bindings[event];
+                       delete this.bindings[ event ];
                        return this;
                }
 
                validateMethod( method, context );
 
-               if ( !hasOwn.call( this.bindings, event ) || !this.bindings[event].length ) {
+               if ( !hasOwn.call( this.bindings, event ) || !this.bindings[ event ].length ) {
                        // No matching bindings
                        return this;
                }
@@ -694,17 +766,17 @@ oo.isPlainObject = $.isPlainObject;
                }
 
                // Remove matching handlers
-               bindings = this.bindings[event];
+               bindings = this.bindings[ event ];
                i = bindings.length;
                while ( i-- ) {
-                       if ( bindings[i].method === method && bindings[i].context === context ) {
+                       if ( bindings[ i ].method === method && bindings[ i ].context === context ) {
                                bindings.splice( i, 1 );
                        }
                }
 
                // Cleanup if now empty
                if ( bindings.length === 0 ) {
-                       delete this.bindings[event];
+                       delete this.bindings[ event ];
                }
                return this;
        };
@@ -713,7 +785,7 @@ oo.isPlainObject = $.isPlainObject;
         * Emit an event.
         *
         * @param {string} event Type of event
-        * @param {Mixed} args First in a list of variadic arguments passed to event handler (optional)
+        * @param {...Mixed} args First in a list of variadic arguments passed to event handler (optional)
         * @return {boolean} Whether the event was handled by at least one listener
         */
        oo.EventEmitter.prototype.emit = function ( event ) {
@@ -722,12 +794,12 @@ oo.isPlainObject = $.isPlainObject;
 
                if ( hasOwn.call( this.bindings, event ) ) {
                        // Slicing ensures that we don't get tripped up by event handlers that add/remove bindings
-                       bindings = this.bindings[event].slice();
+                       bindings = this.bindings[ event ].slice();
                        for ( i = 1, len = arguments.length; i < len; i++ ) {
-                               args.push( arguments[i] );
+                               args.push( arguments[ i ] );
                        }
                        for ( i = 0, len = bindings.length; i < len; i++ ) {
-                               binding = bindings[i];
+                               binding = bindings[ i ];
                                if ( typeof binding.method === 'string' ) {
                                        // Lookup method by name (late binding)
                                        method = binding.context[ binding.method ];
@@ -758,11 +830,11 @@ oo.isPlainObject = $.isPlainObject;
                var method, args, event;
 
                for ( event in methods ) {
-                       method = methods[event];
+                       method = methods[ event ];
                        // Allow providing additional args
                        if ( Array.isArray( method ) ) {
                                args = method.slice( 1 );
-                               method = method[0];
+                               method = method[ 0 ];
                        } else {
                                args = [];
                        }
@@ -782,23 +854,27 @@ oo.isPlainObject = $.isPlainObject;
         * @chainable
         */
        oo.EventEmitter.prototype.disconnect = function ( context, methods ) {
-               var i, event, bindings;
+               var i, event, method, bindings;
 
                if ( methods ) {
                        // Remove specific connections to the context
                        for ( event in methods ) {
-                               this.off( event, methods[event], context );
+                               method = methods[ event ];
+                               if ( Array.isArray( method ) ) {
+                                       method = method[ 0 ];
+                               }
+                               this.off( event, method, context );
                        }
                } else {
                        // Remove all connections to the context
                        for ( event in this.bindings ) {
-                               bindings = this.bindings[event];
+                               bindings = this.bindings[ event ];
                                i = bindings.length;
                                while ( i-- ) {
                                        // bindings[i] may have been removed by the previous step's
                                        // this.off so check it still exists
-                                       if ( bindings[i] && bindings[i].context === context ) {
-                                               this.off( event, bindings[i].method, context );
+                                       if ( bindings[ i ] && bindings[ i ].context === context ) {
+                                               this.off( event, bindings[ i ].method, context );
                                        }
                                }
                        }
@@ -809,7 +885,492 @@ oo.isPlainObject = $.isPlainObject;
 
 }() );
 
-/*global hasOwn */
+( function () {
+
+       /**
+        * Contain and manage a list of OO.EventEmitter items.
+        *
+        * Aggregates and manages their events collectively.
+        *
+        * This mixin must be used in a class that also mixes in OO.EventEmitter.
+        *
+        * @abstract
+        * @class OO.EmitterList
+        * @constructor
+        */
+       oo.EmitterList = function OoEmitterList() {
+               this.items = [];
+               this.aggregateItemEvents = {};
+       };
+
+       /* Events */
+
+       /**
+        * Item has been added
+        *
+        * @event add
+        * @param {OO.EventEmitter} item Added item
+        * @param {number} index Index items were added at
+        */
+
+       /**
+        * Item has been moved to a new index
+        *
+        * @event move
+        * @param {OO.EventEmitter} item Moved item
+        * @param {number} index Index item was moved to
+        * @param {number} oldIndex The original index the item was in
+        */
+
+       /**
+        * Item has been removed
+        *
+        * @event remove
+        * @param {OO.EventEmitter} item Removed item
+        * @param {number} index Index the item was removed from
+        */
+
+       /**
+        * @event clear The list has been cleared of items
+        */
+
+       /* Methods */
+
+       /**
+        * Normalize requested index to fit into the bounds of the given array.
+        *
+        * @private
+        * @static
+        * @param {Array} arr Given array
+        * @param {number|undefined} index Requested index
+        * @return {number} Normalized index
+        */
+       function normalizeArrayIndex( arr, index ) {
+               return ( index === undefined || index < 0 || index >= arr.length ) ?
+                       arr.length :
+                       index;
+       }
+
+       /**
+        * Get all items.
+        *
+        * @return {OO.EventEmitter[]} Items in the list
+        */
+       oo.EmitterList.prototype.getItems = function () {
+               return this.items.slice( 0 );
+       };
+
+       /**
+        * Get the index of a specific item.
+        *
+        * @param {OO.EventEmitter} item Requested item
+        * @return {number} Index of the item
+        */
+       oo.EmitterList.prototype.getItemIndex = function ( item ) {
+               return this.items.indexOf( item );
+       };
+
+       /**
+        * Get number of items.
+        *
+        * @return {number} Number of items in the list
+        */
+       oo.EmitterList.prototype.getItemCount = function () {
+               return this.items.length;
+       };
+
+       /**
+        * Check if a list contains no items.
+        *
+        * @return {boolean} Group is empty
+        */
+       oo.EmitterList.prototype.isEmpty = function () {
+               return !this.items.length;
+       };
+
+       /**
+        * Aggregate the events emitted by the group.
+        *
+        * When events are aggregated, the group will listen to all contained items for the event,
+        * and then emit the event under a new name. The new event will contain an additional leading
+        * parameter containing the item that emitted the original event. Other arguments emitted from
+        * the original event are passed through.
+        *
+        * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
+        *  aggregated  (e.g., â€˜click’) and the value of the new name to use (e.g., â€˜groupClick’).
+        *  A `null` value will remove aggregated events.
+
+        * @throws {Error} If aggregation already exists
+        */
+       oo.EmitterList.prototype.aggregate = function ( events ) {
+               var i, item, add, remove, itemEvent, groupEvent;
+
+               for ( itemEvent in events ) {
+                       groupEvent = events[ itemEvent ];
+
+                       // Remove existing aggregated event
+                       if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
+                               // Don't allow duplicate aggregations
+                               if ( groupEvent ) {
+                                       throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
+                               }
+                               // Remove event aggregation from existing items
+                               for ( i = 0; i < this.items.length; i++ ) {
+                                       item = this.items[ i ];
+                                       if ( item.connect && item.disconnect ) {
+                                               remove = {};
+                                               remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
+                                               item.disconnect( this, remove );
+                                       }
+                               }
+                               // Prevent future items from aggregating event
+                               delete this.aggregateItemEvents[ itemEvent ];
+                       }
+
+                       // Add new aggregate event
+                       if ( groupEvent ) {
+                               // Make future items aggregate event
+                               this.aggregateItemEvents[ itemEvent ] = groupEvent;
+                               // Add event aggregation to existing items
+                               for ( i = 0; i < this.items.length; i++ ) {
+                                       item = this.items[ i ];
+                                       if ( item.connect && item.disconnect ) {
+                                               add = {};
+                                               add[ itemEvent ] = [ 'emit', groupEvent, item ];
+                                               item.connect( this, add );
+                                       }
+                               }
+                       }
+               }
+       };
+
+       /**
+        * Add items to the list.
+        *
+        * @param {OO.EventEmitter|OO.EventEmitter[]} items Item to add or
+        *  an array of items to add
+        * @param {number} [index] Index to add items at. If no index is
+        *  given, or if the index that is given is invalid, the item
+        *  will be added at the end of the list.
+        * @chainable
+        * @fires add
+        * @fires move
+        */
+       oo.EmitterList.prototype.addItems = function ( items, index ) {
+               var i, oldIndex;
+
+               if ( !Array.isArray( items ) ) {
+                       items = [ items ];
+               }
+
+               if ( items.length === 0 ) {
+                       return this;
+               }
+
+               index = normalizeArrayIndex( this.items, index );
+               for ( i = 0; i < items.length; i++ ) {
+                       oldIndex = this.items.indexOf( items[ i ] );
+                       if ( oldIndex !== -1 ) {
+                               // Move item to new index
+                               index = this.moveItem( items[ i ], index );
+                               this.emit( 'move', items[ i ], index, oldIndex );
+                       } else {
+                               // insert item at index
+                               index = this.insertItem( items[ i ], index );
+                               this.emit( 'add', items[ i ], index );
+                       }
+                       index++;
+               }
+
+               return this;
+       };
+
+       /**
+        * Move an item from its current position to a new index.
+        *
+        * The item is expected to exist in the list. If it doesn't,
+        * the method will throw an exception.
+        *
+        * @private
+        * @param {OO.EventEmitter} item Items to add
+        * @param {number} newIndex Index to move the item to
+        * @return {number} The index the item was moved to
+        * @throws {Error} If item is not in the list
+        */
+       oo.EmitterList.prototype.moveItem = function ( item, newIndex ) {
+               var existingIndex = this.items.indexOf( item );
+
+               if ( existingIndex === -1 ) {
+                       throw new Error( 'Item cannot be moved, because it is not in the list.' );
+               }
+
+               newIndex = normalizeArrayIndex( this.items, newIndex );
+
+               // Remove the item from the current index
+               this.items.splice( existingIndex, 1 );
+
+               // Adjust new index after removal
+               newIndex--;
+
+               // Move the item to the new index
+               this.items.splice( newIndex, 0, item );
+
+               return newIndex;
+       };
+
+       /**
+        * Utility method to insert an item into the list, and
+        * connect it to aggregate events.
+        *
+        * Don't call this directly unless you know what you're doing.
+        * Use #addItems instead.
+        *
+        * This method can be extended in child classes to produce
+        * different behavior when an item is inserted. For example,
+        * inserted items may also be attached to the DOM or may
+        * interact with some other nodes in certain ways. Extending
+        * this method is allowed, but if overriden, the aggregation
+        * of events must be preserved, or behavior of emitted events
+        * will be broken.
+        *
+        * If you are extending this method, please make sure the
+        * parent method is called.
+        *
+        * @protected
+        * @param {OO.EventEmitter} item Items to add
+        * @param {number} index Index to add items at
+        * @return {number} The index the item was added at
+        */
+       oo.EmitterList.prototype.insertItem = function ( item, index ) {
+               var events, event;
+
+               // Add the item to event aggregation
+               if ( item.connect && item.disconnect ) {
+                       events = {};
+                       for ( event in this.aggregateItemEvents ) {
+                               events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
+                       }
+                       item.connect( this, events );
+               }
+
+               index = normalizeArrayIndex( this.items, index );
+
+               // Insert into items array
+               this.items.splice( index, 0, item );
+               return index;
+       };
+
+       /**
+        * Remove items.
+        *
+        * @param {OO.EventEmitter[]} items Items to remove
+        * @chainable
+        * @fires remove
+        */
+       oo.EmitterList.prototype.removeItems = function ( items ) {
+               var i, item, index;
+
+               if ( !Array.isArray( items ) ) {
+                       items = [ items ];
+               }
+
+               if ( items.length === 0 ) {
+                       return this;
+               }
+
+               // Remove specific items
+               for ( i = 0; i < items.length; i++ ) {
+                       item = items[ i ];
+                       index = this.items.indexOf( item );
+                       if ( index !== -1 ) {
+                               if ( item.connect && item.disconnect ) {
+                                       // Disconnect all listeners from the item
+                                       item.disconnect( this );
+                               }
+                               this.items.splice( index, 1 );
+                               this.emit( 'remove', item, index );
+                       }
+               }
+
+               return this;
+       };
+
+       /**
+        * Clear all items
+        *
+        * @chainable
+        * @fires clear
+        */
+       oo.EmitterList.prototype.clearItems = function () {
+               var i, item,
+                       cleared = this.items.splice( 0, this.items.length );
+
+               // Disconnect all items
+               for ( i = 0; i < cleared.length; i++ ) {
+                       item = cleared[ i ];
+                       if ( item.connect && item.disconnect ) {
+                               item.disconnect( this );
+                       }
+               }
+
+               this.emit( 'clear' );
+
+               return this;
+       };
+
+}() );
+
+/**
+ * Manage a sorted list of OO.EmitterList objects.
+ *
+ * The sort order is based on a callback that compares two items. The return value of
+ * callback( a, b ) must be less than zero if a < b, greater than zero if a > b, and zero
+ * if a is equal to b. The callback should only return zero if the two objects are
+ * considered equal.
+ *
+ * When an item changes in a way that could affect their sorting behavior, it must
+ * emit the itemSortChange event. This will cause it to be re-sorted automatically.
+ *
+ * This mixin must be used in a class that also mixes in OO.EventEmitter.
+ *
+ * @abstract
+ * @class OO.SortedEmitterList
+ * @mixins OO.EmitterList
+ * @constructor
+ * @param {Function} sortingCallback Callback that compares two items.
+ */
+oo.SortedEmitterList = function OoSortedEmitterList( sortingCallback ) {
+       // Mixin constructors
+       oo.EmitterList.call( this );
+
+       this.sortingCallback = sortingCallback;
+
+       // Listen to sortChange event and make sure
+       // we re-sort the changed item when that happens
+       this.aggregate( {
+               sortChange: 'itemSortChange'
+       } );
+
+       this.connect( this, {
+               itemSortChange: 'onItemSortChange'
+       } );
+};
+
+oo.mixinClass( oo.SortedEmitterList, oo.EmitterList );
+
+/* Events */
+
+/**
+ * An item has changed properties that affect its sort positioning
+ * inside the list.
+ *
+ * @private
+ * @event itemSortChange
+ */
+
+/* Methods */
+
+/**
+ * Handle a case where an item changed a property that relates
+ * to its sorted order
+ *
+ * @param {OO.EventEmitter} item Item in the list
+ */
+oo.SortedEmitterList.prototype.onItemSortChange = function ( item ) {
+       // Remove the item
+       this.removeItems( item );
+       // Re-add the item so it is in the correct place
+       this.addItems( item );
+};
+
+/**
+ * Change the sorting callback for this sorted list.
+ *
+ * The callback receives two items. The return value of callback(a, b) must be less than zero
+ * if a < b, greater than zero if a > b, and zero if a is equal to b.
+ *
+ * @param {Function} sortingCallback Sorting callback
+ */
+oo.SortedEmitterList.prototype.setSortingCallback = function ( sortingCallback ) {
+       var items = this.getItems();
+
+       this.sortingCallback = sortingCallback;
+
+       // Empty the list
+       this.clearItems();
+       // Re-add the items in the new order
+       this.addItems( items );
+};
+
+/**
+ * Add items to the sorted list.
+ *
+ * @chainable
+ * @param {OO.EventEmitter|OO.EventEmitter[]} items Item to add or
+ *  an array of items to add
+ */
+oo.SortedEmitterList.prototype.addItems = function ( items ) {
+       var index, i, insertionIndex;
+
+       if ( !Array.isArray( items ) ) {
+               items = [ items ];
+       }
+
+       if ( items.length === 0 ) {
+               return this;
+       }
+
+       for ( i = 0; i < items.length; i++ ) {
+               // Find insertion index
+               insertionIndex = this.findInsertionIndex( items[ i ] );
+
+               // Check if the item exists using the sorting callback
+               // and remove it first if it exists
+               if (
+                       // First make sure the insertion index is not at the end
+                       // of the list (which means it does not point to any actual
+                       // items)
+                       insertionIndex <= this.items.length &&
+                       // Make sure there actually is an item in this index
+                       this.items[ insertionIndex ] &&
+                       // The callback returns 0 if the items are equal
+                       this.sortingCallback( this.items[ insertionIndex ], items[ i ] ) === 0
+               ) {
+                       // Remove the existing item
+                       this.removeItems( this.items[ insertionIndex ] );
+               }
+
+               // Insert item at the insertion index
+               index = this.insertItem( items[ i ], insertionIndex );
+               this.emit( 'add', items[ i ], index );
+       }
+
+       return this;
+};
+
+/**
+ * Find the index a given item should be inserted at. If the item is already
+ * in the list, this will return the index where the item currently is.
+ *
+ * @param {OO.EventEmitter} item Items to insert
+ * @return {number} The index the item should be inserted at
+ */
+oo.SortedEmitterList.prototype.findInsertionIndex = function ( item ) {
+       var list = this;
+
+       return oo.binarySearch(
+               this.items,
+               // Fake a this.sortingCallback.bind( null, item ) call here
+               // otherwise this doesn't pass tests in phantomJS
+               function ( otherItem ) {
+                       return list.sortingCallback( item, otherItem );
+               },
+               true
+       );
+
+};
+
+/* global hasOwn */
 
 /**
  * @class OO.Registry
@@ -858,11 +1419,11 @@ oo.mixinClass( oo.Registry, oo.EventEmitter );
 oo.Registry.prototype.register = function ( name, data ) {
        var i, len;
        if ( typeof name === 'string' ) {
-               this.registry[name] = data;
+               this.registry[ name ] = data;
                this.emit( 'register', name, data );
        } else if ( Array.isArray( name ) ) {
                for ( i = 0, len = name.length; i < len; i++ ) {
-                       this.register( name[i], data );
+                       this.register( name[ i ], data );
                }
        } else {
                throw new Error( 'Name must be a string or array, cannot be a ' + typeof name );
@@ -881,12 +1442,12 @@ oo.Registry.prototype.unregister = function ( name ) {
        if ( typeof name === 'string' ) {
                data = this.lookup( name );
                if ( data !== undefined ) {
-                       delete this.registry[name];
+                       delete this.registry[ name ];
                        this.emit( 'unregister', name, data );
                }
        } else if ( Array.isArray( name ) ) {
                for ( i = 0, len = name.length; i < len; i++ ) {
-                       this.unregister( name[i] );
+                       this.unregister( name[ i ] );
                }
        } else {
                throw new Error( 'Name must be a string or array, cannot be a ' + typeof name );
@@ -901,12 +1462,10 @@ oo.Registry.prototype.unregister = function ( name ) {
  */
 oo.Registry.prototype.lookup = function ( name ) {
        if ( hasOwn.call( this.registry, name ) ) {
-               return this.registry[name];
+               return this.registry[ name ];
        }
 };
 
-/*global createObject */
-
 /**
  * @class OO.Factory
  * @extends OO.Registry
@@ -915,7 +1474,7 @@ oo.Registry.prototype.lookup = function ( name ) {
  */
 oo.Factory = function OoFactory() {
        // Parent constructor
-       oo.Factory.parent.call( this );
+       oo.Factory.super.call( this );
 };
 
 /* Inheritance */
@@ -952,7 +1511,7 @@ oo.Factory.prototype.register = function ( constructor ) {
        }
 
        // Parent method
-       oo.Factory.parent.prototype.register.call( this, name, constructor );
+       oo.Factory.super.prototype.register.call( this, name, constructor );
 };
 
 /**
@@ -974,7 +1533,7 @@ oo.Factory.prototype.unregister = function ( constructor ) {
        }
 
        // Parent method
-       oo.Factory.parent.prototype.unregister.call( this, name );
+       oo.Factory.super.prototype.unregister.call( this, name );
 };
 
 /**
@@ -984,7 +1543,7 @@ oo.Factory.prototype.unregister = function ( constructor ) {
  * constructor directly, so leaving one out will pass an undefined to the constructor.
  *
  * @param {string} name Object name
- * @param {Mixed...} [args] Arguments to pass to the constructor
+ * @param {...Mixed} [args] Arguments to pass to the constructor
  * @return {Object} The new object
  * @throws {Error} Unknown object name
  */
@@ -999,7 +1558,7 @@ oo.Factory.prototype.create = function ( name ) {
 
        // Convert arguments to array and shift the first argument (name) off
        for ( i = 1; i < arguments.length; i++ ) {
-               args.push( arguments[i] );
+               args.push( arguments[ i ] );
        }
 
        // We can't use the "new" operator with .apply directly because apply needs a
@@ -1007,12 +1566,12 @@ oo.Factory.prototype.create = function ( name ) {
        // the constructor's prototype (which also makes it an "instanceof" the constructor),
        // then invoke the constructor with the object as context, and return it (ignoring
        // the constructor's return value).
-       obj = createObject( constructor.prototype );
+       obj = Object.create( constructor.prototype );
        constructor.apply( obj, args );
        return obj;
 };
 
-/*jshint node:true */
+/* eslint-env node */
 if ( typeof module !== 'undefined' && module.exports ) {
        module.exports = oo;
 } else {