all files / src/ SortedEmitterList.js

100% Statements 30/30
100% Branches 9/9
100% Functions 6/6
100% Lines 29/29
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144                                        16×   16×       16×       16×                                                                       14×   14×     14×   14×                   46×     46× 10×     36×   102×       98×                           98× 98×     32×                   102×       126×          
/**
 * Manage a sorted list of {@link 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 {@link OO.SortedEmitterList#event:itemSortChange itemSortChange} event.
 * This will cause it to be re-sorted automatically.
 *
 * This mixin must be used in a class that also mixes in {@link OO.EventEmitter}.
 *
 * @abstract
 * @class
 * @mixes OO.EmitterList
 * @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 OO.SortedEmitterList#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 ) {
	const 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.
 *
 * @param {OO.EventEmitter|OO.EventEmitter[]} items Item to add or
 *  an array of items to add
 * @return {OO.SortedEmitterList}
 */
OO.SortedEmitterList.prototype.addItems = function ( items ) {
	if ( !Array.isArray( items ) ) {
		items = [ items ];
	}
 
	if ( items.length === 0 ) {
		return this;
	}
 
	for ( let i = 0; i < items.length; i++ ) {
		// Find insertion index
		const 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
		const 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 ) {
	return OO.binarySearch(
		this.items,
		// Fake a this.sortingCallback.bind( null, item ) call here
		// otherwise this doesn't pass tests in phantomJS
		( otherItem ) => this.sortingCallback( item, otherItem ),
		true
	);
 
};