From 30651d2c48a5fc664eec6546b363e6b4c1e66e9d Mon Sep 17 00:00:00 2001 From: "James D. Forrester" Date: Wed, 11 Nov 2015 09:02:06 -0800 Subject: [PATCH] Update OOjs to v1.1.10 Release notes: https://git.wikimedia.org/blob/oojs%2Fcore.git/v1.1.10/History.md Change-Id: Id19682f59690aafc70fa05c6febf32b1206090c2 --- maintenance/jsduck/categories.json | 18 +- resources/lib/oojs/oojs.jquery.js | 630 ++++++++++++++++++++++++++--- 2 files changed, 589 insertions(+), 59 deletions(-) diff --git a/maintenance/jsduck/categories.json b/maintenance/jsduck/categories.json index ef264c3296..41b56f6ca5 100644 --- a/maintenance/jsduck/categories.json +++ b/maintenance/jsduck/categories.json @@ -106,8 +106,22 @@ "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", diff --git a/resources/lib/oojs/oojs.jquery.js b/resources/lib/oojs/oojs.jquery.js index 9395ecfe1f..3857f99f51 100644 --- a/resources/lib/oojs/oojs.jquery.js +++ b/resources/lib/oojs/oojs.jquery.js @@ -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.} 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 -- 2.20.1