Update OOjs to v1.1.10
authorJames D. Forrester <jforrester@wikimedia.org>
Wed, 11 Nov 2015 17:02:06 +0000 (09:02 -0800)
committerJames D. Forrester <jforrester@wikimedia.org>
Wed, 11 Nov 2015 18:04:19 +0000 (10:04 -0800)
Release notes:
 https://git.wikimedia.org/blob/oojs%2Fcore.git/v1.1.10/History.md

Change-Id: Id19682f59690aafc70fa05c6febf32b1206090c2

maintenance/jsduck/categories.json
resources/lib/oojs/oojs.jquery.js

index ef264c3..41b56f6 100644 (file)
                "name": "Upstream",
                "groups": [
                        {
-                               "name": "OOJS",
-                               "classes": ["OO", "OO.*"]
+                               "name": "OOjs",
+                               "classes": [
+                                       "OO",
+                                       "OO.EmitterList",
+                                       "OO.EventEmitter",
+                                       "OO.Factory",
+                                       "OO.Registry",
+                                       "OO.SortedEmitterList"
+                               ]
+                       },
+                       {
+                               "name": "OOUI",
+                               "classes": [
+                                       "OO.ui",
+                                       "OO.ui.*"
+                               ]
                        },
                        {
                                "name": "jQuery",
index 9395ecf..3857f99 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs v1.1.9 optimised for jQuery
+ * OOjs v1.1.10 optimised for jQuery
  * https://www.mediawiki.org/wiki/OOjs
  *
  * Copyright 2011-2015 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2015-08-25T21:35:29Z
+ * Date: 2015-11-11T16:49:11Z
  */
 ( function ( global ) {
 
@@ -90,17 +90,19 @@ oo.initClass = function ( fn ) {
  * @throws {Error} If target already inherits from origin
  */
 oo.inheritClass = function ( targetFn, originFn ) {
+       var targetConstructor;
+
        if ( targetFn.prototype instanceof originFn ) {
                throw new Error( '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;
+       targetFn[ 'super' ] = targetFn.parent = originFn;
 
        targetFn.prototype = createObject( originFn.prototype, {
                // Restore constructor property of targetFn
@@ -154,7 +156,7 @@ oo.mixinClass = function ( targetFn, 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 +165,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 {
@@ -183,8 +185,8 @@ oo.mixinClass = function ( targetFn, originFn ) {
  * 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 +196,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,7 +212,7 @@ 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 ) {
@@ -220,15 +222,15 @@ oo.setProp = function ( obj ) {
                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];
+       prop[ arguments[ arguments.length - 2 ] ] = arguments[ arguments.length - 1 ];
 };
 
 /**
@@ -260,7 +262,7 @@ oo.cloneObject = function ( origin ) {
 
        for ( key in origin ) {
                if ( hasOwn.call( origin, key ) ) {
-                       r[key] = origin[key];
+                       r[ key ] = origin[ key ];
                }
        }
 
@@ -270,7 +272,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 +285,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
+               /*jshint bitwise:false */
+               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 +358,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 +366,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 +425,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,7 +476,7 @@ 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;
 
@@ -472,7 +510,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 +519,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 +553,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 ] );
                }
        }
 
@@ -601,7 +639,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 +670,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 +715,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 +732,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 +751,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 +760,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 +796,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 +820,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,6 +851,480 @@ oo.isPlainObject = $.isPlainObject;
 
 }() );
 
+( 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.
+        *
+        * @private
+        * @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 ], insertionIndex );
+       }
+
+       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 */
 
 /**
@@ -858,11 +1374,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 +1397,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,7 +1417,7 @@ 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 ];
        }
 };
 
@@ -984,7 +1500,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 +1515,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