mediawiki-extensions-Echo/modules/viewmodel/mw.echo.dm.SortedList.js
Moriel Schottlender 02530f19e1 Implement SortedList in Echo notifications
This is especially important for combined notifications and
notification lists from different sources; the model list should
be sorted to reflect items by timestamp and unread status.

Note: The dm.List and dm.SortedList now mirror the structures
OO.EmitterList and OO.SortedEmitterList that are awaiting to be
added for oojs in Ib94e4e4a49 and  I3fd569691549 respectively.
Once those are available, the dm.List and dm.SortedList can
be removed, and the model can mixin OO.SortedEmitterList instead.

Change-Id: I97e1ecbe5dccc478be527a94f037500f78f74b14
2015-11-06 11:01:10 -08:00

165 lines
4.6 KiB
JavaScript

( function ( mw, oo ) {
/**
* @class mw.echo.dm.SortedList
* Contains and a sorted mw.echo.dm.List
*
* @constructor
*/
mw.echo.dm.SortedList = function OoSortedEmitterList() {
// Mixin constructors
mw.echo.dm.List.call( this );
this.sortingCallback = function ( a, b ) {
return a.getTimestamp() - b.getTimestamp();
};
// 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( mw.echo.dm.SortedList, mw.echo.dm.List );
/**
* Handle a case where an item changed a property that relates
* to its sorted order
*
* @param {mw.echo.dm.NotificationItem} item Item in the list
*/
mw.echo.dm.SortedList.prototype.onItemSortChange = function ( item ) {
// Remove the item
this.removeItems( item );
// Re-add the item so it is in the correct place
this.addItems( item );
};
/**
* Set the sorting callback for this sorted list.
*
* @param {Function} sortingCallback Sorting callback
*/
mw.echo.dm.SortedList.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.
*
* @param {OO.EventEmitter|OO.EventEmitter[]} items Item to add or
* an array of items to add
*/
mw.echo.dm.SortedList.prototype.addItems = function ( items ) {
var index, i, insertionIndex;
if ( !Array.isArray( items ) ) {
items = [ items ];
}
if ( items.length === 0 ) {
return this;
}
// Call parent mixin
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;
};
/**
* Normalize requested index to fit into the array.
* In the case of a sorted list, the index
*
* @param {OO.EventEmitter} item Items to insert
* @return {number} The index the item should be inserted into
*/
mw.echo.dm.SortedList.prototype.findInsertionIndex = function ( item ) {
var list = this;
return this.binarySearchIndex(
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
);
};
/**
* 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
*/
mw.echo.dm.SortedList.prototype.binarySearchIndex = function ( arr, searchFunc, forInsertion ) {
// TODO: Replace this with OO.binarySearch
// See https://gerrit.wikimedia.org/r/#/c/246813/
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;
};
}( mediaWiki, OO ) );