feat(Change Detection): Implement collection changes
This commit is contained in:
		
							parent
							
								
									7d0a83a24c
								
							
						
					
					
						commit
						1bd304e7ab
					
				
							
								
								
									
										635
									
								
								modules/change_detection/src/collection_changes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										635
									
								
								modules/change_detection/src/collection_changes.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,635 @@ | |||||||
|  | import { | ||||||
|  |   ListWrapper, | ||||||
|  |   MapWrapper | ||||||
|  | } from 'facade/collection'; | ||||||
|  | 
 | ||||||
|  | import { | ||||||
|  |   int, | ||||||
|  |   isBlank, | ||||||
|  |   isPresent, | ||||||
|  |   stringify, | ||||||
|  |   getMapKey, | ||||||
|  |   looseIdentical, | ||||||
|  | } from 'facade/lang'; | ||||||
|  | 
 | ||||||
|  | export class CollectionChanges { | ||||||
|  |   // todo(vicb) Add fields when supported
 | ||||||
|  |   /* | ||||||
|  |   _collection; | ||||||
|  |   int _length; | ||||||
|  |   DuplicateMap _linkedRecords; | ||||||
|  |   DuplicateMap _unlinkedRecords; | ||||||
|  |   CollectionChangeItem<V> _previousItHead; | ||||||
|  |   CollectionChangeItem<V> _itHead, _itTail; | ||||||
|  |   CollectionChangeItem<V> _additionsHead, _additionsTail; | ||||||
|  |   CollectionChangeItem<V> _movesHead, _movesTail; | ||||||
|  |   CollectionChangeItem<V> _removalsHead, _removalsTail; | ||||||
|  |    */ | ||||||
|  |   constructor() { | ||||||
|  |     this._collection = null; | ||||||
|  |     this._length = null; | ||||||
|  |     /// Keeps track of the used records at any point in time (during & across `_check()` calls)
 | ||||||
|  |     this._linkedRecords = null; | ||||||
|  |     /// Keeps track of the removed records at any point in time during `_check()` calls.
 | ||||||
|  |     this._unlinkedRecords = null; | ||||||
|  | 
 | ||||||
|  |     this._previousItHead = null; | ||||||
|  |     this._itHead = null; | ||||||
|  |     this._itTail = null; | ||||||
|  |     this._additionsHead = null; | ||||||
|  |     this._additionsTail = null; | ||||||
|  |     this._movesHead = null; | ||||||
|  |     this._movesTail = null; | ||||||
|  |     this._removalsHead = null; | ||||||
|  |     this._removalsTail = null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get collection() { | ||||||
|  |     return this._collection; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get length():int { | ||||||
|  |     return this._length; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   forEachItem(fn:Function) { | ||||||
|  |     var record:CollectionChangeRecord; | ||||||
|  |     for (record = this._itHead; record !== null; record = record.next) { | ||||||
|  |       fn(record); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   forEachPreviousItem(fn:Function) { | ||||||
|  |     var record:CollectionChangeRecord; | ||||||
|  |     for (record = this._previousItHead; record !== null; record = record._nextPrevious) { | ||||||
|  |       fn(record); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   forEachAddedItem(fn:Function){ | ||||||
|  |     var record:CollectionChangeRecord; | ||||||
|  |     for (record = this._additionsHead; record !== null; record = record._nextAdded) { | ||||||
|  |       fn(record); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   forEachMovedItem(fn:Function) { | ||||||
|  |     var record:CollectionChangeRecord; | ||||||
|  |     for (record = this._movesHead; record !== null; record = record._nextMoved) { | ||||||
|  |       fn(record); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   forEachRemovedItem(fn:Function){ | ||||||
|  |     var record:CollectionChangeRecord; | ||||||
|  |     for (record = this._removalsHead; record !== null; record = record._nextRemoved) { | ||||||
|  |       fn(record); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // todo(vicb): optim for UnmodifiableListView (frozen arrays)
 | ||||||
|  |   check(collection):boolean { | ||||||
|  |     this._reset(); | ||||||
|  | 
 | ||||||
|  |     var record:CollectionChangeRecord = this._itHead; | ||||||
|  |     var mayBeDirty:boolean = false; | ||||||
|  |     var index:int, item; | ||||||
|  | 
 | ||||||
|  |     if (ListWrapper.isList(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); | ||||||
|  |           mayBeDirty = true; | ||||||
|  |         } else if (mayBeDirty) { | ||||||
|  |           // TODO(misko): can we limit this to duplicates only?
 | ||||||
|  |           record = this._verifyReinsertion(record, item, index); | ||||||
|  |         } | ||||||
|  |         record = record._next; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       // todo(vicb) implement iterators
 | ||||||
|  |       throw "NYI"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this._truncate(record); | ||||||
|  |     this._collection = collection; | ||||||
|  |     return this.isDirty; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // CollectionChanges is considered dirty if it has any additions, moves or removals.
 | ||||||
|  |   get isDirty():boolean { | ||||||
|  |     return this._additionsHead !== null || | ||||||
|  |            this._movesHead !== null || | ||||||
|  |            this._removalsHead !== null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Reset the state of the change objects to show no changes. This means set previousKey to | ||||||
|  |    * currentKey, and clear all of the queues (additions, moves, removals). | ||||||
|  |    * Set the previousIndexes of moved and added items to their currentIndexes | ||||||
|  |    * Reset the list of additions, moves and removals | ||||||
|  |    */ | ||||||
|  |   _reset() { | ||||||
|  |     if (this.isDirty) { | ||||||
|  |       var record:CollectionChangeRecord; | ||||||
|  |       var nextRecord:CollectionChangeRecord; | ||||||
|  | 
 | ||||||
|  |       for (record = this._previousItHead = this._itHead; record !== null; record = record._next) { | ||||||
|  |         record._nextPrevious = record._next; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       for (record = this._additionsHead; record !== null; record = record._nextAdded) { | ||||||
|  |         record.previousIndex = record.currentIndex; | ||||||
|  |       } | ||||||
|  |       this._additionsHead = this._additionsTail = null; | ||||||
|  | 
 | ||||||
|  |       for (record = this._movesHead; record !== null; record = nextRecord) { | ||||||
|  |         record.previousIndex = record.currentIndex; | ||||||
|  |         nextRecord = record._nextMoved; | ||||||
|  |       } | ||||||
|  |       this._movesHead = this._movesTail = null; | ||||||
|  |       this._removalsHead = this._removalsTail = null; | ||||||
|  | 
 | ||||||
|  |       // todo(vicb) when assert gets supported
 | ||||||
|  |       // assert(!this.isDirty);
 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * This is the core function which handles differences between collections. | ||||||
|  |    * | ||||||
|  |    * - [record] is the record which we saw at this position last time. If null then it is a new | ||||||
|  |    *   item. | ||||||
|  |    * - [item] is the current item in the collection | ||||||
|  |    * - [index] is the position of the item in the collection | ||||||
|  |    */ | ||||||
|  |   _mismatch(record:CollectionChangeRecord, item, index:int):CollectionChangeRecord { | ||||||
|  |     // The previous record after which we will append the current one.
 | ||||||
|  |     var previousRecord:CollectionChangeRecord; | ||||||
|  | 
 | ||||||
|  |     if (record === null) { | ||||||
|  |       previousRecord = this._itTail; | ||||||
|  |     } else { | ||||||
|  |       previousRecord = record._prev; | ||||||
|  |       // Remove the record from the collection since we know it does not match the item.
 | ||||||
|  |       this._remove(record); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Attempt to see if we have seen the item before.
 | ||||||
|  |     record = this._linkedRecords === null ? null : this._linkedRecords.get(item, 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); | ||||||
|  |       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); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return record; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * This check is only needed if an array contains duplicates. (Short circuit of nothing dirty) | ||||||
|  |    * | ||||||
|  |    * Use case: `[a, a]` => `[b, a, a]` | ||||||
|  |    * | ||||||
|  |    * If we did not have this check then the insertion of `b` would: | ||||||
|  |    *   1) evict first `a` | ||||||
|  |    *   2) insert `b` at `0` index. | ||||||
|  |    *   3) leave `a` at index `1` as is. <-- this is wrong! | ||||||
|  |    *   3) reinsert `a` at index 2. <-- this is wrong! | ||||||
|  |    * | ||||||
|  |    * The correct behavior is: | ||||||
|  |    *   1) evict first `a` | ||||||
|  |    *   2) insert `b` at `0` index. | ||||||
|  |    *   3) reinsert `a` at index 1. | ||||||
|  |    *   3) move `a` at from `1` to `2`. | ||||||
|  |    * | ||||||
|  |    * | ||||||
|  |    * Double check that we have not evicted a duplicate item. We need to check if the item type may | ||||||
|  |    * have already been removed: | ||||||
|  |    * The insertion of b will evict the first 'a'. If we don't reinsert it now it will be reinserted | ||||||
|  |    * at the end. Which will show up as the two 'a's switching position. This is incorrect, since a | ||||||
|  |    * better way to think of it is as insert of 'b' rather then switch 'a' with 'b' and then add 'a' | ||||||
|  |    * at the end. | ||||||
|  |    */ | ||||||
|  |   _verifyReinsertion(record:CollectionChangeRecord, item, index:int):CollectionChangeRecord { | ||||||
|  |     var reinsertRecord:CollectionChangeRecord = this._unlinkedRecords === null ? | ||||||
|  |       null : this._unlinkedRecords.get(item); | ||||||
|  |     if (reinsertRecord !== null) { | ||||||
|  |       record = this._reinsertAfter(reinsertRecord, record._prev, index); | ||||||
|  |     } else if (record.currentIndex != index) { | ||||||
|  |       record.currentIndex = index; | ||||||
|  |       this._addToMoves(record, index); | ||||||
|  |     } | ||||||
|  |     return record; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get rid of any excess [CollectionChangeItem]s from the previous collection | ||||||
|  |    * | ||||||
|  |    * - [record] The first excess [CollectionChangeItem]. | ||||||
|  |    */ | ||||||
|  |   _truncate(record:CollectionChangeRecord) { | ||||||
|  |     // Anything after that needs to be removed;
 | ||||||
|  |     while (record !== null) { | ||||||
|  |       var nextRecord:CollectionChangeRecord = record._next; | ||||||
|  |       this._addToRemovals(this._unlink(record)); | ||||||
|  |       record = nextRecord; | ||||||
|  |     } | ||||||
|  |     if (this._unlinkedRecords !== null) { | ||||||
|  |       this._unlinkedRecords.clear(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (this._additionsTail !== null) { | ||||||
|  |       this._additionsTail._nextAdded = null; | ||||||
|  |     } | ||||||
|  |     if (this._movesTail !== null) { | ||||||
|  |       this._movesTail._nextMoved = null; | ||||||
|  |     } | ||||||
|  |     if (this._itTail !== null) { | ||||||
|  |       this._itTail._next = null; | ||||||
|  |     } | ||||||
|  |     if (this._removalsTail !== null) { | ||||||
|  |       this._removalsTail._nextRemoved = null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _reinsertAfter(record:CollectionChangeRecord, prevRecord:CollectionChangeRecord, | ||||||
|  |                  index:int):CollectionChangeRecord { | ||||||
|  |     if (this._unlinkedRecords !== null) { | ||||||
|  |       this._unlinkedRecords.remove(record); | ||||||
|  |     } | ||||||
|  |     var prev = record._prevRemoved; | ||||||
|  |     var next = record._nextRemoved; | ||||||
|  | 
 | ||||||
|  |     if (prev === null) { | ||||||
|  |       this._removalsHead = next; | ||||||
|  |     } else { | ||||||
|  |       prev._nextRemoved = next; | ||||||
|  |     } | ||||||
|  |     if (next === null) { | ||||||
|  |       this._removalsTail = prev; | ||||||
|  |     } else { | ||||||
|  |       next._prevRemoved = prev; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this._insertAfter(record, prevRecord, index); | ||||||
|  |     this._addToMoves(record, index); | ||||||
|  |     return record; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _moveAfter(record:CollectionChangeRecord, prevRecord:CollectionChangeRecord, | ||||||
|  |              index:int):CollectionChangeRecord { | ||||||
|  |     this._unlink(record); | ||||||
|  |     this._insertAfter(record, prevRecord, index); | ||||||
|  |     this._addToMoves(record, index); | ||||||
|  |     return record; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _addAfter(record:CollectionChangeRecord, prevRecord:CollectionChangeRecord, | ||||||
|  |             index:int):CollectionChangeRecord { | ||||||
|  |     this._insertAfter(record, prevRecord, index); | ||||||
|  | 
 | ||||||
|  |     if (this._additionsTail === null) { | ||||||
|  |       // todo(vicb)
 | ||||||
|  |       //assert(this._additionsHead === null);
 | ||||||
|  |       this._additionsTail = this._additionsHead = record; | ||||||
|  |     } else { | ||||||
|  |       // todo(vicb)
 | ||||||
|  |       //assert(_additionsTail._nextAdded === null);
 | ||||||
|  |       //assert(record._nextAdded === null);
 | ||||||
|  |       this._additionsTail = this._additionsTail._nextAdded = record; | ||||||
|  |     } | ||||||
|  |     return record; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _insertAfter(record:CollectionChangeRecord, prevRecord:CollectionChangeRecord, | ||||||
|  |               index:int):CollectionChangeRecord { | ||||||
|  |     // todo(vicb)
 | ||||||
|  |     //assert(record != prevRecord);
 | ||||||
|  |     //assert(record._next === null);
 | ||||||
|  |     //assert(record._prev === null);
 | ||||||
|  | 
 | ||||||
|  |     var next:CollectionChangeRecord = prevRecord === null ? this._itHead :prevRecord._next; | ||||||
|  |     // todo(vicb)
 | ||||||
|  |     //assert(next != record);
 | ||||||
|  |     //assert(prevRecord != record);
 | ||||||
|  |     record._next = next; | ||||||
|  |     record._prev = prevRecord; | ||||||
|  |     if (next === null) { | ||||||
|  |       this._itTail = record; | ||||||
|  |     } else { | ||||||
|  |       next._prev = record; | ||||||
|  |     } | ||||||
|  |     if (prevRecord === null) { | ||||||
|  |       this._itHead = record; | ||||||
|  |     } else { | ||||||
|  |       prevRecord._next = record; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (this._linkedRecords === null) { | ||||||
|  |       this._linkedRecords = new _DuplicateMap(); | ||||||
|  |     } | ||||||
|  |     this._linkedRecords.put(record); | ||||||
|  | 
 | ||||||
|  |     record.currentIndex = index; | ||||||
|  |     return record; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _remove(record:CollectionChangeRecord):CollectionChangeRecord { | ||||||
|  |     this._addToRemovals(this._unlink(record)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _unlink(record:CollectionChangeRecord):CollectionChangeRecord { | ||||||
|  |     if (this._linkedRecords !== null) { | ||||||
|  |       this._linkedRecords.remove(record); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var prev = record._prev; | ||||||
|  |     var next = record._next; | ||||||
|  | 
 | ||||||
|  |     // todo(vicb)
 | ||||||
|  |     //assert((record._prev = null) === null);
 | ||||||
|  |     //assert((record._next = null) === null);
 | ||||||
|  | 
 | ||||||
|  |     if (prev === null) { | ||||||
|  |       this._itHead = next; | ||||||
|  |     } else { | ||||||
|  |       prev._next = next; | ||||||
|  |     } | ||||||
|  |     if (next === null) { | ||||||
|  |       this._itTail = prev; | ||||||
|  |     } else { | ||||||
|  |       next._prev = prev; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return record; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _addToMoves(record:CollectionChangeRecord, toIndex:int):CollectionChangeRecord { | ||||||
|  |     // todo(vicb)
 | ||||||
|  |     //assert(record._nextMoved === null);
 | ||||||
|  | 
 | ||||||
|  |     if (record.previousIndex === toIndex) { | ||||||
|  |       return record; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (this._movesTail === null) { | ||||||
|  |       // todo(vicb)
 | ||||||
|  |       //assert(_movesHead === null);
 | ||||||
|  |       this._movesTail = this._movesHead = record; | ||||||
|  |     } else { | ||||||
|  |       // todo(vicb)
 | ||||||
|  |       //assert(_movesTail._nextMoved === null);
 | ||||||
|  |       this._movesTail = this._movesTail._nextMoved = record; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return record; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _addToRemovals(record:CollectionChangeRecord):CollectionChangeRecord { | ||||||
|  |     if (this._unlinkedRecords === null) { | ||||||
|  |       this._unlinkedRecords = new _DuplicateMap(); | ||||||
|  |     } | ||||||
|  |     this._unlinkedRecords.put(record); | ||||||
|  |     record.currentIndex = null; | ||||||
|  |     record._nextRemoved = null; | ||||||
|  | 
 | ||||||
|  |     if (this._removalsTail === null) { | ||||||
|  |       // todo(vicb)
 | ||||||
|  |       //assert(_removalsHead === null);
 | ||||||
|  |       this._removalsTail = this._removalsHead = record; | ||||||
|  |       record._prevRemoved = null; | ||||||
|  |     } else { | ||||||
|  |       // todo(vicb)
 | ||||||
|  |       //assert(_removalsTail._nextRemoved === null);
 | ||||||
|  |       //assert(record._nextRemoved === null);
 | ||||||
|  |       record._prevRemoved = this._removalsTail; | ||||||
|  |       this._removalsTail = this._removalsTail._nextRemoved = record; | ||||||
|  |     } | ||||||
|  |     return record; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   toString():string { | ||||||
|  |     var record:CollectionChangeRecord; | ||||||
|  | 
 | ||||||
|  |     var list = []; | ||||||
|  |     for (record = this._itHead; record !== null; record = record._next) { | ||||||
|  |       ListWrapper.push(list, record); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var previous = []; | ||||||
|  |     for (record = this._previousItHead; record !== null; record = record._nextPrevious) { | ||||||
|  |       ListWrapper.push(previous, record); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var additions = []; | ||||||
|  |     for (record = this._additionsHead; record !== null; record = record._nextAdded) { | ||||||
|  |       ListWrapper.push(additions, record); | ||||||
|  |     } | ||||||
|  |     var moves = []; | ||||||
|  |     for (record = this._movesHead; record !== null; record = record._nextMoved) { | ||||||
|  |       ListWrapper.push(moves, record); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var removals = []; | ||||||
|  |     for (record = this._removalsHead; record !== null; record = record._nextRemoved) { | ||||||
|  |       ListWrapper.push(removals, record); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return "collection: " + list.join(', ') + "\n" + | ||||||
|  |            "previous: " + previous.join(', ') + "\n" + | ||||||
|  |            "additions: " + additions.join(', ') + "\n" + | ||||||
|  |            "moves: " + moves.join(', ') + "\n" + | ||||||
|  |            "removals: " + removals.join(', ') + "\n"; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class CollectionChangeRecord  { | ||||||
|  |   // todo(vicb) add fields when supported
 | ||||||
|  |   /* | ||||||
|  |   int currentIndex; | ||||||
|  |   int previousIndex; | ||||||
|  |   V item; | ||||||
|  | 
 | ||||||
|  |   CollectionChangeItem<V> _nextPrevious; | ||||||
|  |   CollectionChangeItem<V> _prev, _next; | ||||||
|  |   CollectionChangeItem<V> _prevDup, _nextDup; | ||||||
|  |   CollectionChangeItem<V> _prevRemoved, _nextRemoved; | ||||||
|  |   CollectionChangeItem<V> _nextAdded; | ||||||
|  |   CollectionChangeItem<V> _nextMoved; | ||||||
|  |   */ | ||||||
|  | 
 | ||||||
|  |   constructor(item) { | ||||||
|  |     this.currentIndex = null; | ||||||
|  |     this.previousIndex = null; | ||||||
|  |     this.item = item; | ||||||
|  | 
 | ||||||
|  |     this._nextPrevious = null; | ||||||
|  |     this._prev = null; | ||||||
|  |     this._next = null; | ||||||
|  |     this._prevDup = null; | ||||||
|  |     this._nextDup = null; | ||||||
|  |     this._prevRemoved = null; | ||||||
|  |     this._nextRemoved = null; | ||||||
|  |     this._nextAdded = null; | ||||||
|  |     this._nextMoved = null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   toString():string { | ||||||
|  |     return this.previousIndex === this.currentIndex ? | ||||||
|  |       stringify(this.item) : | ||||||
|  |       stringify(this.item) + '[' + stringify(this.previousIndex) + '->' + | ||||||
|  |         stringify(this.currentIndex) + ']'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // A linked list of CollectionChangeRecords with the same CollectionChangeRecord.item
 | ||||||
|  | class _DuplicateItemRecordList { | ||||||
|  |   /* | ||||||
|  |   todo(vicb): add fields when supported | ||||||
|  |   CollectionChangeRecord _head, _tail; | ||||||
|  |   */ | ||||||
|  | 
 | ||||||
|  |   constructor() { | ||||||
|  |     this._head = null; | ||||||
|  |     this._tail = null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Append the record to the list of duplicates. | ||||||
|  |    * | ||||||
|  |    * Note: by design all records in the list of duplicates hold the same value in record.item. | ||||||
|  |    */ | ||||||
|  |   add(record:CollectionChangeRecord) { | ||||||
|  |     if (this._head === null) { | ||||||
|  |       this._head = this._tail = record; | ||||||
|  |       record._nextDup = null; | ||||||
|  |       record._prevDup = null; | ||||||
|  |     } else { | ||||||
|  |       // todo(vicb)
 | ||||||
|  |       //assert(record.item ==  _head.item ||
 | ||||||
|  |       //       record.item is num && record.item.isNaN && _head.item is num && _head.item.isNaN);
 | ||||||
|  |       this._tail._nextDup = record; | ||||||
|  |       record._prevDup = this._tail; | ||||||
|  |       record._nextDup = null; | ||||||
|  |       this._tail = record; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Returns a CollectionChangeRecord having CollectionChangeRecord.item == item and
 | ||||||
|  |   // CollectionChangeRecord.currentIndex >= afterIndex
 | ||||||
|  |   get(item, afterIndex:int):CollectionChangeRecord { | ||||||
|  |     var record:CollectionChangeRecord; | ||||||
|  |     for (record = this._head; record !== null; record = record._nextDup) { | ||||||
|  |       if ((afterIndex === null || afterIndex < record.currentIndex) && | ||||||
|  |            looseIdentical(record.item, item)) { | ||||||
|  |           return record; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Remove one [CollectionChangeItem] from the list of duplicates. | ||||||
|  |    * | ||||||
|  |    * Returns whether the list of duplicates is empty. | ||||||
|  |    */ | ||||||
|  |   remove(record:CollectionChangeRecord):boolean { | ||||||
|  |     // todo(vicb)
 | ||||||
|  |     //assert(() {
 | ||||||
|  |     //  // verify that the record being removed is in the list.
 | ||||||
|  |     //  for (CollectionChangeItem cursor = _head; cursor != null; cursor = cursor._nextDup) {
 | ||||||
|  |     //    if (identical(cursor, record)) return true;
 | ||||||
|  |     //  }
 | ||||||
|  |     //  return false;
 | ||||||
|  |     //});
 | ||||||
|  | 
 | ||||||
|  |     var prev:CollectionChangeRecord = record._prevDup; | ||||||
|  |     var next:CollectionChangeRecord = record._nextDup; | ||||||
|  |     if (prev === null) { | ||||||
|  |       this._head = next; | ||||||
|  |     } else { | ||||||
|  |       prev._nextDup = next; | ||||||
|  |     } | ||||||
|  |     if (next === null) { | ||||||
|  |       this._tail = prev; | ||||||
|  |     } else { | ||||||
|  |       next._prevDup = prev; | ||||||
|  |     } | ||||||
|  |     return this._head === null; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class _DuplicateMap { | ||||||
|  |   // todo(vicb): add fields when supported
 | ||||||
|  |   constructor() { | ||||||
|  |     this.map = MapWrapper.create(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   put(record:CollectionChangeRecord) { | ||||||
|  |     // todo(vicb) handle corner cases
 | ||||||
|  |     var key = getMapKey(record.item); | ||||||
|  | 
 | ||||||
|  |     var duplicates = MapWrapper.get(this.map, key); | ||||||
|  |     if (!isPresent(duplicates)) { | ||||||
|  |       duplicates = new _DuplicateItemRecordList(); | ||||||
|  |       MapWrapper.set(this.map, key, duplicates); | ||||||
|  |     } | ||||||
|  |     duplicates.add(record); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Retrieve the `value` using key. Because the CollectionChangeRecord value maybe 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, afterIndex = null):CollectionChangeRecord { | ||||||
|  |     var key = getMapKey(value); | ||||||
|  | 
 | ||||||
|  |     var recordList = MapWrapper.get(this.map, key); | ||||||
|  |     return isBlank(recordList) ? null : recordList.get(value, afterIndex); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Removes an [CollectionChangeItem] from the list of duplicates. | ||||||
|  |    * | ||||||
|  |    * The list of duplicates also is removed from the map if it gets empty. | ||||||
|  |    */ | ||||||
|  |   remove(record:CollectionChangeRecord):CollectionChangeRecord { | ||||||
|  |     var key = getMapKey(record.item); | ||||||
|  |     // todo(vicb)
 | ||||||
|  |     //assert(this.map.containsKey(key));
 | ||||||
|  |     var recordList:_DuplicateItemRecordList = MapWrapper.get(this.map, key); | ||||||
|  |     // Remove the list of duplicates when it gets empty
 | ||||||
|  |     if (recordList.remove(record)) { | ||||||
|  |       MapWrapper.delete(this.map, key); | ||||||
|  |     } | ||||||
|  |     return record; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get isEmpty():boolean { | ||||||
|  |     return MapWrapper.size(this.map) === 0; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   clear() { | ||||||
|  |     MapWrapper.clear(this.map); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   toString():string { | ||||||
|  |     return '_DuplicateMap(' + stringify(this.map) + ')'; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										280
									
								
								modules/change_detection/test/collection_changes_spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								modules/change_detection/test/collection_changes_spec.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,280 @@ | |||||||
|  | import {describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib'; | ||||||
|  | 
 | ||||||
|  | import {CollectionChanges} from 'change_detection/collection_changes'; | ||||||
|  | 
 | ||||||
|  | import {isBlank, NumberWrapper} from 'facade/lang'; | ||||||
|  | 
 | ||||||
|  | import {ListWrapper} from 'facade/collection'; | ||||||
|  | 
 | ||||||
|  | // todo(vicb): UnmodifiableListView / frozen object when implemented
 | ||||||
|  | // todo(vicb): Update the code & tests for object equality
 | ||||||
|  | export function main() { | ||||||
|  |   describe('collection_changes', function() { | ||||||
|  |     describe('CollectionChanges', function() { | ||||||
|  |       var changes; | ||||||
|  |       var l; | ||||||
|  | 
 | ||||||
|  |       beforeEach(() => { | ||||||
|  |         changes = new CollectionChanges(); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       afterEach(() => { | ||||||
|  |         changes = null; | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should detect additions', () => { | ||||||
|  |         l = []; | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: [] | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.push(l, 'a'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['a[null->0]'], | ||||||
|  |           additions: ['a[null->0]'] | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.push(l, 'b'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['a', 'b[null->1]'], | ||||||
|  |           previous: ['a'], | ||||||
|  |           additions: ['b[null->1]'] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should support changing the reference', () => { | ||||||
|  |         l = [0]; | ||||||
|  |         changes.check(l); | ||||||
|  | 
 | ||||||
|  |         l = [1, 0]; | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['1[null->0]', '0[0->1]'], | ||||||
|  |           previous: ['0[0->1]'], | ||||||
|  |           additions: ['1[null->0]'], | ||||||
|  |           moves: ['0[0->1]'] | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         l = [2, 1, 0]; | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['2[null->0]', '1[0->1]', '0[1->2]'], | ||||||
|  |           previous: ['1[0->1]', '0[1->2]'], | ||||||
|  |           additions: ['2[null->0]'], | ||||||
|  |           moves: ['1[0->1]', '0[1->2]'] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should handle swapping element', () => { | ||||||
|  |         l = [1, 2]; | ||||||
|  |         changes.check(l); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.clear(l); | ||||||
|  |         ListWrapper.push(l, 2); | ||||||
|  |         ListWrapper.push(l, 1); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['2[1->0]', '1[0->1]'], | ||||||
|  |           previous: ['1[0->1]', '2[1->0]'], | ||||||
|  |           moves: ['2[1->0]', '1[0->1]'] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should handle swapping element', () => { | ||||||
|  |         l = ['a', 'b', 'c']; | ||||||
|  |         changes.check(l); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.removeAt(l, 1); | ||||||
|  |         ListWrapper.insert(l, 0, 'b'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['b[1->0]', 'a[0->1]', 'c'], | ||||||
|  |           previous: ['a[0->1]', 'b[1->0]', 'c'], | ||||||
|  |           moves: ['b[1->0]', 'a[0->1]'] | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.removeAt(l, 1); | ||||||
|  |         ListWrapper.push(l, 'a'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['b', 'c[2->1]', 'a[1->2]'], | ||||||
|  |           previous: ['b', 'a[1->2]', 'c[2->1]'], | ||||||
|  |           moves: ['c[2->1]', 'a[1->2]'] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should detect changes in list', () => { | ||||||
|  |         l = []; | ||||||
|  |         changes.check(l); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.push(l, 'a'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['a[null->0]'], | ||||||
|  |           additions: ['a[null->0]'] | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.push(l, 'b'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['a', 'b[null->1]'], | ||||||
|  |           previous: ['a'], | ||||||
|  |           additions: ['b[null->1]'] | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.push(l, 'c'); | ||||||
|  |         ListWrapper.push(l, 'd'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['a', 'b', 'c[null->2]', 'd[null->3]'], | ||||||
|  |           previous: ['a', 'b'], | ||||||
|  |           additions: ['c[null->2]', 'd[null->3]'] | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.removeAt(l, 2); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['a', 'b', 'd[3->2]'], | ||||||
|  |           previous: ['a', 'b', 'c[2->null]', 'd[3->2]'], | ||||||
|  |           moves: ['d[3->2]'], | ||||||
|  |           removals: ['c[2->null]'] | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.clear(l); | ||||||
|  |         ListWrapper.push(l, 'd'); | ||||||
|  |         ListWrapper.push(l, 'c'); | ||||||
|  |         ListWrapper.push(l, 'b'); | ||||||
|  |         ListWrapper.push(l, 'a'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['d[2->0]', 'c[null->1]', 'b[1->2]', 'a[0->3]'], | ||||||
|  |           previous: ['a[0->3]', 'b[1->2]', 'd[2->0]'], | ||||||
|  |           additions: ['c[null->1]'], | ||||||
|  |           moves: ['d[2->0]', 'b[1->2]', 'a[0->3]'] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should test string by value rather than by reference (Dart)', () => { | ||||||
|  |         l = ['a', 'boo']; | ||||||
|  |         changes.check(l); | ||||||
|  | 
 | ||||||
|  |         var b = 'b'; | ||||||
|  |         var oo = 'oo'; | ||||||
|  |         ListWrapper.set(l, 1, b + oo); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['a', 'boo'], | ||||||
|  |           previous: ['a', 'boo'] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should ignore [NaN] != [NaN] (JS)', () => { | ||||||
|  |         l = [NumberWrapper.NaN]; | ||||||
|  |         changes.check(l); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: [NumberWrapper.NaN], | ||||||
|  |           previous: [NumberWrapper.NaN] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should detect [NaN] moves', () => { | ||||||
|  |         l = [NumberWrapper.NaN, NumberWrapper.NaN]; | ||||||
|  |         changes.check(l); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.insert(l, 0, 'foo'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |             collection: ['foo[null->0]', 'NaN[0->1]', 'NaN[1->2]'], | ||||||
|  |             previous: ['NaN[0->1]', 'NaN[1->2]'], | ||||||
|  |             additions: ['foo[null->0]'], | ||||||
|  |             moves: ['NaN[0->1]', 'NaN[1->2]']} | ||||||
|  |         )); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should remove and add same item', () => { | ||||||
|  |         l = ['a', 'b', 'c']; | ||||||
|  |         changes.check(l); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.removeAt(l, 1); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['a', 'c[2->1]'], | ||||||
|  |           previous: ['a', 'b[1->null]', 'c[2->1]'], | ||||||
|  |           moves: ['c[2->1]'], | ||||||
|  |           removals: ['b[1->null]'] | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.insert(l, 1, 'b'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['a', 'b[null->1]', 'c[1->2]'], | ||||||
|  |           previous: ['a', 'c[1->2]'], | ||||||
|  |           additions: ['b[null->1]'], | ||||||
|  |           moves: ['c[1->2]'] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should support duplicates', () => { | ||||||
|  |         l = ['a', 'a', 'a', 'b', 'b']; | ||||||
|  |         changes.check(l); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.removeAt(l, 0); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['a', 'a', 'b[3->2]', 'b[4->3]'], | ||||||
|  |           previous: ['a', 'a', 'a[2->null]', 'b[3->2]', 'b[4->3]'], | ||||||
|  |           moves: ['b[3->2]', 'b[4->3]'], | ||||||
|  |           removals: ['a[2->null]'] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should support insertions/moves', () => { | ||||||
|  |         l = ['a', 'a', 'b', 'b']; | ||||||
|  |         changes.check(l); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.insert(l, 0, 'b'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['b[2->0]', 'a[0->1]', 'a[1->2]', 'b', 'b[null->4]'], | ||||||
|  |           previous: ['a[0->1]', 'a[1->2]', 'b[2->0]', 'b'], | ||||||
|  |           additions: ['b[null->4]'], | ||||||
|  |           moves: ['b[2->0]', 'a[0->1]', 'a[1->2]'] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should not report unnecessary moves', () => { | ||||||
|  |         l = ['a', 'b', 'c']; | ||||||
|  |         changes.check(l); | ||||||
|  | 
 | ||||||
|  |         ListWrapper.clear(l); | ||||||
|  |         ListWrapper.push(l, 'b'); | ||||||
|  |         ListWrapper.push(l, 'a'); | ||||||
|  |         ListWrapper.push(l, 'c'); | ||||||
|  |         changes.check(l); | ||||||
|  |         expect(changes.toString()).toEqual(changesAsString({ | ||||||
|  |           collection: ['b[1->0]', 'a[0->1]', 'c'], | ||||||
|  |           previous: ['a[0->1]', 'b[1->0]', 'c'], | ||||||
|  |           moves: ['b[1->0]', 'a[0->1]'] | ||||||
|  |         })); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function changesAsString({collection, previous, additions, moves, removals}) { | ||||||
|  |   if (isBlank(collection)) collection = []; | ||||||
|  |   if (isBlank(previous)) previous = []; | ||||||
|  |   if (isBlank(additions)) additions = []; | ||||||
|  |   if (isBlank(moves)) moves = []; | ||||||
|  |   if (isBlank(removals)) removals = []; | ||||||
|  | 
 | ||||||
|  |   return "collection: " + collection.join(', ') + "\n" + | ||||||
|  |          "previous: " + previous.join(', ') + "\n" + | ||||||
|  |          "additions: " + additions.join(', ') + "\n" + | ||||||
|  |          "moves: " + moves.join(', ') + "\n" + | ||||||
|  |          "removals: " + removals.join(', ') + "\n"; | ||||||
|  | } | ||||||
| @ -12,6 +12,8 @@ class MapWrapper { | |||||||
|     m.forEach((k,v) => fn(v,k)); |     m.forEach((k,v) => fn(v,k)); | ||||||
|   } |   } | ||||||
|   static int size(m) {return m.length;} |   static int size(m) {return m.length;} | ||||||
|  |   static void delete(m, k) { m.remove(k); } | ||||||
|  |   static void clear(m) { m.clear(); } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO: how to export StringMap=Map as a type? | // TODO: how to export StringMap=Map as a type? | ||||||
| @ -47,6 +49,10 @@ class ListWrapper { | |||||||
|   static List reversed(List list) => list.reversed.toList(); |   static List reversed(List list) => list.reversed.toList(); | ||||||
|   static void push(List l, e) { l.add(e); } |   static void push(List l, e) { l.add(e); } | ||||||
|   static List concat(List a, List b) {a.addAll(b); return a;} |   static List concat(List a, List b) {a.addAll(b); return a;} | ||||||
|  |   static bool isList(l) => l is List; | ||||||
|  |   static void insert(List l, int index, value) { l.insert(index, value); } | ||||||
|  |   static void removeAt(List l, int index) { l.removeAt(index); } | ||||||
|  |   static void clear(List l) { l.clear(); } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class SetWrapper { | class SetWrapper { | ||||||
|  | |||||||
| @ -1,3 +1,5 @@ | |||||||
|  | import {int} from 'facade/lang'; | ||||||
|  | 
 | ||||||
| export var List = window.Array; | export var List = window.Array; | ||||||
| export var Map = window.Map; | export var Map = window.Map; | ||||||
| export var Set = window.Set; | export var Set = window.Set; | ||||||
| @ -11,6 +13,8 @@ export class MapWrapper { | |||||||
|     m.forEach(fn); |     m.forEach(fn); | ||||||
|   } |   } | ||||||
|   static size(m) {return m.size;} |   static size(m) {return m.size;} | ||||||
|  |   static delete(m, k) { m.delete(k); } | ||||||
|  |   static clear(m) { m.clear(); } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO: cannot export StringMap as a type as Dart does not support | // TODO: cannot export StringMap as a type as Dart does not support | ||||||
| @ -80,9 +84,21 @@ export class ListWrapper { | |||||||
|     return a.reverse(); |     return a.reverse(); | ||||||
|   } |   } | ||||||
|   static concat(a, b) {return a.concat(b);} |   static concat(a, b) {return a.concat(b);} | ||||||
|  |   static isList(list) { | ||||||
|  |     return Array.isArray(list); | ||||||
|  |   } | ||||||
|  |   static insert(list, index:int, value) { | ||||||
|  |     list.splice(index, 0, value); | ||||||
|  |   } | ||||||
|  |   static removeAt(list, index:int) { | ||||||
|  |     list.splice(index, 1); | ||||||
|  |   } | ||||||
|  |   static clear(list) { | ||||||
|  |     list.splice(0, list.length); | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class SetWrapper { | export class SetWrapper { | ||||||
|   static createFromList(lst:List) { return new Set(lst); } |   static createFromList(lst:List) { return new Set(lst); } | ||||||
|   static has(s:Set, key):boolean { return s.has(key); } |   static has(s:Set, key):boolean { return s.has(key); } | ||||||
| } | } | ||||||
|  | |||||||
| @ -83,6 +83,10 @@ class NumberWrapper { | |||||||
|   static double parseFloat(String text) { |   static double parseFloat(String text) { | ||||||
|     return double.parse(text); |     return double.parse(text); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   static get NaN => double.NAN; | ||||||
|  | 
 | ||||||
|  |   static bool isNaN(num value) => value.isNaN; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class RegExpWrapper { | class RegExpWrapper { | ||||||
| @ -117,4 +121,14 @@ class BaseException extends Error { | |||||||
|   String toString() { |   String toString() { | ||||||
|     return this.message; |     return this.message; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // Dart can have identical(str1, str2) == false while str1 == str2 | ||||||
|  | bool looseIdentical(a, b) => a is String && b is String ? a == b : identical(a, b); | ||||||
|  | 
 | ||||||
|  | // Dart compare map keys by equality and we can have NaN != NaN | ||||||
|  | dynamic getMapKey(value) { | ||||||
|  |   if (value is! num) return value; | ||||||
|  |   return value.isNaN ? _NAN_KEY : value; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | |||||||
| @ -110,6 +110,14 @@ export class NumberWrapper { | |||||||
|   static parseFloat(text:string):number { |   static parseFloat(text:string):number { | ||||||
|     return parseFloat(text); |     return parseFloat(text); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   static isNaN(value) { | ||||||
|  |     return isNaN(value); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static get NaN():number { | ||||||
|  |     return NaN; | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function int() {}; | export function int() {}; | ||||||
| @ -151,4 +159,16 @@ export class BaseException extends Error { | |||||||
|   toString():String { |   toString():String { | ||||||
|     return this.message; |     return this.message; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // JS has NaN !== NaN | ||||||
|  | export function looseIdentical(a, b):boolean { | ||||||
|  |   return a === b || | ||||||
|  |          typeof a === "number" && typeof b === "number" && isNaN(a) && isNaN(b); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JS considers NaN is the same as NaN for map Key (while NaN !== NaN otherwise) | ||||||
|  | // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map | ||||||
|  | export function getMapKey(value) { | ||||||
|  |   return value; | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user