|
|
|
@ -1,11 +1,6 @@
|
|
|
|
|
import {CONST} from 'angular2/src/facade/lang';
|
|
|
|
|
import {BaseException} from 'angular2/src/facade/exceptions';
|
|
|
|
|
import {
|
|
|
|
|
isListLikeIterable,
|
|
|
|
|
iterateListLike,
|
|
|
|
|
ListWrapper,
|
|
|
|
|
MapWrapper
|
|
|
|
|
} from 'angular2/src/facade/collection';
|
|
|
|
|
import {isListLikeIterable, iterateListLike, ListWrapper} from 'angular2/src/facade/collection';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
isBlank,
|
|
|
|
@ -17,17 +12,21 @@ import {
|
|
|
|
|
} from 'angular2/src/facade/lang';
|
|
|
|
|
|
|
|
|
|
import {ChangeDetectorRef} from '../change_detector_ref';
|
|
|
|
|
import {IterableDiffer, IterableDifferFactory} from '../differs/iterable_differs';
|
|
|
|
|
import {IterableDiffer, IterableDifferFactory, TrackByFn} from '../differs/iterable_differs';
|
|
|
|
|
|
|
|
|
|
@CONST()
|
|
|
|
|
export class DefaultIterableDifferFactory implements IterableDifferFactory {
|
|
|
|
|
supports(obj: Object): boolean { return isListLikeIterable(obj); }
|
|
|
|
|
create(cdRef: ChangeDetectorRef): DefaultIterableDiffer { return new DefaultIterableDiffer(); }
|
|
|
|
|
create(cdRef: ChangeDetectorRef, trackByFn?: TrackByFn): DefaultIterableDiffer {
|
|
|
|
|
return new DefaultIterableDiffer(trackByFn);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var trackByIdentity = (index: number, item: any) => item;
|
|
|
|
|
|
|
|
|
|
export class DefaultIterableDiffer implements IterableDiffer {
|
|
|
|
|
private _collection = null;
|
|
|
|
|
private _length: number = null;
|
|
|
|
|
private _collection = null;
|
|
|
|
|
// Keeps track of the used records at any point in time (during & across `_check()` calls)
|
|
|
|
|
private _linkedRecords: _DuplicateMap = null;
|
|
|
|
|
// Keeps track of the removed records at any point in time during `_check()` calls.
|
|
|
|
@ -42,6 +41,10 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
|
|
|
|
private _removalsHead: CollectionChangeRecord = null;
|
|
|
|
|
private _removalsTail: CollectionChangeRecord = null;
|
|
|
|
|
|
|
|
|
|
constructor(private _trackByFn?: TrackByFn) {
|
|
|
|
|
this._trackByFn = isPresent(this._trackByFn) ? this._trackByFn : trackByIdentity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get collection() { return this._collection; }
|
|
|
|
|
|
|
|
|
|
get length(): number { return this._length; }
|
|
|
|
@ -104,31 +107,37 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
|
|
|
|
var mayBeDirty: boolean = false;
|
|
|
|
|
var index: number;
|
|
|
|
|
var item;
|
|
|
|
|
|
|
|
|
|
var itemTrackBy;
|
|
|
|
|
if (isArray(collection)) {
|
|
|
|
|
var list = collection;
|
|
|
|
|
this._length = collection.length;
|
|
|
|
|
|
|
|
|
|
for (index = 0; index < this._length; index++) {
|
|
|
|
|
item = list[index];
|
|
|
|
|
if (record === null || !looseIdentical(record.item, item)) {
|
|
|
|
|
record = this._mismatch(record, item, index);
|
|
|
|
|
itemTrackBy = this._trackByFn(index, item);
|
|
|
|
|
if (record === null || !looseIdentical(record.trackById, itemTrackBy)) {
|
|
|
|
|
record = this._mismatch(record, item, itemTrackBy, index);
|
|
|
|
|
mayBeDirty = true;
|
|
|
|
|
} else if (mayBeDirty) {
|
|
|
|
|
// TODO(misko): can we limit this to duplicates only?
|
|
|
|
|
record = this._verifyReinsertion(record, item, index);
|
|
|
|
|
} else {
|
|
|
|
|
if (mayBeDirty) {
|
|
|
|
|
// TODO(misko): can we limit this to duplicates only?
|
|
|
|
|
record = this._verifyReinsertion(record, item, itemTrackBy, index);
|
|
|
|
|
}
|
|
|
|
|
record.item = item;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
record = record._next;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
index = 0;
|
|
|
|
|
iterateListLike(collection, (item) => {
|
|
|
|
|
if (record === null || !looseIdentical(record.item, item)) {
|
|
|
|
|
record = this._mismatch(record, item, index);
|
|
|
|
|
itemTrackBy = this._trackByFn(index, item);
|
|
|
|
|
if (record === null || !looseIdentical(record.trackById, itemTrackBy)) {
|
|
|
|
|
record = this._mismatch(record, item, itemTrackBy, index);
|
|
|
|
|
mayBeDirty = true;
|
|
|
|
|
} else if (mayBeDirty) {
|
|
|
|
|
// TODO(misko): can we limit this to duplicates only?
|
|
|
|
|
record = this._verifyReinsertion(record, item, index);
|
|
|
|
|
record = this._verifyReinsertion(record, item, itemTrackBy, index);
|
|
|
|
|
}
|
|
|
|
|
record = record._next;
|
|
|
|
|
index++;
|
|
|
|
@ -190,7 +199,8 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
|
|
|
|
*
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
|
|
|
|
_mismatch(record: CollectionChangeRecord, item, index: number): CollectionChangeRecord {
|
|
|
|
|
_mismatch(record: CollectionChangeRecord, item: any, itemTrackBy: any,
|
|
|
|
|
index: number): CollectionChangeRecord {
|
|
|
|
|
// The previous record after which we will append the current one.
|
|
|
|
|
var previousRecord: CollectionChangeRecord;
|
|
|
|
|
|
|
|
|
@ -203,19 +213,20 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attempt to see if we have seen the item before.
|
|
|
|
|
record = this._linkedRecords === null ? null : this._linkedRecords.get(item, index);
|
|
|
|
|
record = this._linkedRecords === null ? null : this._linkedRecords.get(itemTrackBy, index);
|
|
|
|
|
if (record !== null) {
|
|
|
|
|
// We have seen this before, we need to move it forward in the collection.
|
|
|
|
|
this._moveAfter(record, previousRecord, index);
|
|
|
|
|
} else {
|
|
|
|
|
// Never seen it, check evicted list.
|
|
|
|
|
record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(item);
|
|
|
|
|
record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy);
|
|
|
|
|
if (record !== null) {
|
|
|
|
|
// It is an item which we have evicted earlier: reinsert it back into the list.
|
|
|
|
|
this._reinsertAfter(record, previousRecord, index);
|
|
|
|
|
} else {
|
|
|
|
|
// It is a new item: add it.
|
|
|
|
|
record = this._addAfter(new CollectionChangeRecord(item), previousRecord, index);
|
|
|
|
|
record =
|
|
|
|
|
this._addAfter(new CollectionChangeRecord(item, itemTrackBy), previousRecord, index);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return record;
|
|
|
|
@ -248,15 +259,17 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
|
|
|
|
*
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
|
|
|
|
_verifyReinsertion(record: CollectionChangeRecord, item, index: number): CollectionChangeRecord {
|
|
|
|
|
_verifyReinsertion(record: CollectionChangeRecord, item: any, itemTrackBy: any,
|
|
|
|
|
index: number): CollectionChangeRecord {
|
|
|
|
|
var reinsertRecord: CollectionChangeRecord =
|
|
|
|
|
this._unlinkedRecords === null ? null : this._unlinkedRecords.get(item);
|
|
|
|
|
this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy);
|
|
|
|
|
if (reinsertRecord !== null) {
|
|
|
|
|
record = this._reinsertAfter(reinsertRecord, record._prev, index);
|
|
|
|
|
} else if (record.currentIndex != index) {
|
|
|
|
|
record.currentIndex = index;
|
|
|
|
|
this._addToMoves(record, index);
|
|
|
|
|
}
|
|
|
|
|
record.item = item;
|
|
|
|
|
return record;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -457,31 +470,20 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toString(): string {
|
|
|
|
|
var record: CollectionChangeRecord;
|
|
|
|
|
|
|
|
|
|
var list = [];
|
|
|
|
|
for (record = this._itHead; record !== null; record = record._next) {
|
|
|
|
|
list.push(record);
|
|
|
|
|
}
|
|
|
|
|
this.forEachItem((record) => list.push(record));
|
|
|
|
|
|
|
|
|
|
var previous = [];
|
|
|
|
|
for (record = this._previousItHead; record !== null; record = record._nextPrevious) {
|
|
|
|
|
previous.push(record);
|
|
|
|
|
}
|
|
|
|
|
this.forEachPreviousItem((record) => previous.push(record));
|
|
|
|
|
|
|
|
|
|
var additions = [];
|
|
|
|
|
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
|
|
|
|
|
additions.push(record);
|
|
|
|
|
}
|
|
|
|
|
this.forEachAddedItem((record) => additions.push(record));
|
|
|
|
|
|
|
|
|
|
var moves = [];
|
|
|
|
|
for (record = this._movesHead; record !== null; record = record._nextMoved) {
|
|
|
|
|
moves.push(record);
|
|
|
|
|
}
|
|
|
|
|
this.forEachMovedItem((record) => moves.push(record));
|
|
|
|
|
|
|
|
|
|
var removals = [];
|
|
|
|
|
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
|
|
|
|
|
removals.push(record);
|
|
|
|
|
}
|
|
|
|
|
this.forEachRemovedItem((record) => removals.push(record));
|
|
|
|
|
|
|
|
|
|
return "collection: " + list.join(', ') + "\n" + "previous: " + previous.join(', ') + "\n" +
|
|
|
|
|
"additions: " + additions.join(', ') + "\n" + "moves: " + moves.join(', ') + "\n" +
|
|
|
|
@ -512,7 +514,7 @@ export class CollectionChangeRecord {
|
|
|
|
|
/** @internal */
|
|
|
|
|
_nextMoved: CollectionChangeRecord = null;
|
|
|
|
|
|
|
|
|
|
constructor(public item: any) {}
|
|
|
|
|
constructor(public item: any, public trackById: any) {}
|
|
|
|
|
|
|
|
|
|
toString(): string {
|
|
|
|
|
return this.previousIndex === this.currentIndex ?
|
|
|
|
@ -550,13 +552,13 @@ class _DuplicateItemRecordList {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns a CollectionChangeRecord having CollectionChangeRecord.item == item and
|
|
|
|
|
// Returns a CollectionChangeRecord having CollectionChangeRecord.trackById == trackById and
|
|
|
|
|
// CollectionChangeRecord.currentIndex >= afterIndex
|
|
|
|
|
get(item: any, afterIndex: number): CollectionChangeRecord {
|
|
|
|
|
get(trackById: any, afterIndex: number): CollectionChangeRecord {
|
|
|
|
|
var record: CollectionChangeRecord;
|
|
|
|
|
for (record = this._head; record !== null; record = record._nextDup) {
|
|
|
|
|
if ((afterIndex === null || afterIndex < record.currentIndex) &&
|
|
|
|
|
looseIdentical(record.item, item)) {
|
|
|
|
|
looseIdentical(record.trackById, trackById)) {
|
|
|
|
|
return record;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -599,7 +601,7 @@ class _DuplicateMap {
|
|
|
|
|
|
|
|
|
|
put(record: CollectionChangeRecord) {
|
|
|
|
|
// todo(vicb) handle corner cases
|
|
|
|
|
var key = getMapKey(record.item);
|
|
|
|
|
var key = getMapKey(record.trackById);
|
|
|
|
|
|
|
|
|
|
var duplicates = this.map.get(key);
|
|
|
|
|
if (!isPresent(duplicates)) {
|
|
|
|
@ -610,17 +612,17 @@ class _DuplicateMap {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieve the `value` using key. Because the CollectionChangeRecord value maybe one which we
|
|
|
|
|
* Retrieve the `value` using key. Because the CollectionChangeRecord value may be one which we
|
|
|
|
|
* have already iterated over, we use the afterIndex to pretend it is not there.
|
|
|
|
|
*
|
|
|
|
|
* Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a` then asking if we
|
|
|
|
|
* have any more `a`s needs to return the last `a` not the first or second.
|
|
|
|
|
*/
|
|
|
|
|
get(value: any, afterIndex: number = null): CollectionChangeRecord {
|
|
|
|
|
var key = getMapKey(value);
|
|
|
|
|
get(trackById: any, afterIndex: number = null): CollectionChangeRecord {
|
|
|
|
|
var key = getMapKey(trackById);
|
|
|
|
|
|
|
|
|
|
var recordList = this.map.get(key);
|
|
|
|
|
return isBlank(recordList) ? null : recordList.get(value, afterIndex);
|
|
|
|
|
return isBlank(recordList) ? null : recordList.get(trackById, afterIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -629,7 +631,7 @@ class _DuplicateMap {
|
|
|
|
|
* The list of duplicates also is removed from the map if it gets empty.
|
|
|
|
|
*/
|
|
|
|
|
remove(record: CollectionChangeRecord): CollectionChangeRecord {
|
|
|
|
|
var key = getMapKey(record.item);
|
|
|
|
|
var key = getMapKey(record.trackById);
|
|
|
|
|
// todo(vicb)
|
|
|
|
|
// assert(this.map.containsKey(key));
|
|
|
|
|
var recordList: _DuplicateItemRecordList = this.map.get(key);
|
|
|
|
|