Skip to content

Commit

Permalink
virtualScroll: non-full-featured version, to start testing
Browse files Browse the repository at this point in the history
  • Loading branch information
ajoslin committed Apr 23, 2014
1 parent c2fd9bc commit 934158b
Show file tree
Hide file tree
Showing 7 changed files with 577 additions and 45 deletions.
70 changes: 70 additions & 0 deletions js/angular/directive/collectionRepeat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
IonicModule
.directive('collectionRepeat', [
'$collectionRepeatManager',
'$collectionRepeatDataSource',
'$parse',
function($collectionRepeatManager, $collectionRepeatDataSource, $parse) {
return {
priority: 1000,
transclude: 'element',
terminal: true,
$$tlb: true,
require: '^$ionicScroll',
link: function($scope, $element, $attr, scrollCtrl, $transclude) {
var scrollView = scrollCtrl.scrollView;
if (scrollView.options.scrollingX && scrollView.options.scrollingY) {
throw new Error("Cannot create a collection-repeat within a scrollView that is scrollable on both x and y axis. Choose only one.");
}

var isVertical = !!scrollView.options.scrollingY;
if (isVertical && !$attr.collectionItemHeight) {
throw new Error("collection-repeat expected attribute collection-item-height to be a an expression that returns a number.");
} else if (!isVertical && !$attr.collectionItemWidth) {
throw new Error("collection-repeat expected attribute collection-item-width to be a an expression that returns a number.");
}
var heightGetter = $attr.collectionItemHeight ?
$parse($attr.collectionItemHeight) :
function() { return scrollView.__clientHeight; };
var widthGetter = $attr.collectionItemWidth ?
$parse($attr.collectionItemWidth) :
function() { return scrollView.__clientWidth; };

var match = $attr.collectionRepeat.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
if (!match) {
throw new Error("collection-repeat expected expression in form of '_item_ in _collection_[ track by _id_]' but got '" + $attr.collectionRepeat + "'.");
}

var dataSource = new $collectionRepeatDataSource({
scope: $scope,
transcludeFn: $transclude,
transcludeParent: $element.parent(),
keyExpr: match[1],
listExpr: match[2],
trackByExpr: match[3],
heightGetter: heightGetter,
widthGetter: widthGetter
});
var collectionRepeatManager = new $collectionRepeatManager({
dataSource: dataSource,
element: scrollCtrl.$element,
scrollView: scrollCtrl.scrollView,
});

$scope.$watchCollection(dataSource.listExpr, function(value) {
if (value && !angular.isArray(value)) {
throw new Error("collection-repeat expects an array to repeat over, but instead got '" + typeof value + "'.");
}
dataSource.setData(value);
collectionRepeatManager.resize();
});
ionic.on('resize', function() {
collectionRepeatManager.resize();
}, window);

$scope.$on('$destroy', function() {
collectionRepeatManager.destroy();
dataSource.destroy();
});
}
};
}]);
211 changes: 211 additions & 0 deletions js/angular/service/collectionRepeatDataSource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
IonicModule
.factory('$collectionRepeatDataSource', [
'$cacheFactory',
'$parse',
function($cacheFactory, $parse) {
var nextCacheId = 0;
function CollectionRepeatDataSource(options) {
var self = this;
this.scope = options.scope;
this.transcludeFn = options.transcludeFn;
this.transcludeParent = options.transcludeParent;

this.keyExpr = options.keyExpr;
this.listExpr = options.listExpr;
this.trackByExpr = options.trackByExpr;

this.heightGetter = options.heightGetter;
this.widthGetter = options.widthGetter;

if (this.trackByExpr) {
var trackByGetter = $parse(this.trackByExpr);
var hashFnLocals = {$id: hashKey};
this.trackByIdGetter = function(index, value) {
hashFnLocals[self.keyExpr] = value;
hashFnLocals.$index = index;
return trackByGetter(self.scope, hashFnLocals);
};
} else {
this.trackByIdGetter = function(index, value) {
return hashKey(value);
};
}

var cacheKeys = {};
this.itemCache = $cacheFactory(nextCacheId++, {size: 500});

var _put = this.itemCache.put;
this.itemCache.put = function(key, value) {
cacheKeys[key] = true;
return _put(key, value);
};

var _remove = this.itemCache.remove;
this.itemCache.remove = function(key) {
delete cacheKeys[key];
return _remove(key);
};
this.itemCache.keys = function() {
return cacheKeys;
};
}
CollectionRepeatDataSource.prototype = {
destroy: function() {
this.dimensions.length = 0;
for (var key in this.itemCache.keys()) {
var item = this.itemCache.get(key);
item.element.remove();
item.scope.$destroy();
}
this.itemCache.removeAll();
},
calculateDataDimensions: function() {
var totalWidth = 0;
var totalHeight = 0;
var locals = {};

this.dimensions = this.data.map(function(value, index) {
locals[this.keyExpr] = value;
locals.$index = index;
var ret = {
width: this.widthGetter(this.scope, locals),
height: this.heightGetter(this.scope, locals),
totalWidth: totalWidth,
totalHeight: totalHeight
};
totalWidth += ret.width;
totalHeight += ret.height;
return ret;
}, this);
this.totalWidth = totalWidth;
this.totalHeight = totalHeight;
},
compileItem: function(index, value) {
var key = this.trackByIdGetter(index, value);
var cachedItem = this.itemCache.get(key);
if (cachedItem) return cachedItem;

var item = {};
item.scope = this.scope.$new();
item.scope[this.keyExpr] = value;

this.transcludeFn(item.scope, function(clone) {
item.element = clone;
item.element[0].classList.add('scroll-collection-item');
});

return this.itemCache.put(key, item);
},
getItem: function(index) {
var value = this.data[index];
var item = this.compileItem(index, value);

if (item.scope.$index !== index) {
item.scope.$index = item.index = index;
item.scope.$first = (index === 0);
item.scope.$last = (index === (this.getLength() - 1));
item.scope.$middle = !(item.scope.$first || item.scope.$last);
item.scope.$odd = !(item.scope.$even = (index&1) === 0);
}

return item;
},
detachItem: function(item) {
var i, node, parent;
//Don't .remove(), that will destroy element data
for (i = 0; i < item.element.length; i++) {
node = item.element[i];
parent = node.parentNode;
parent && parent.removeChild(node);
}
//Don't .$destroy(), just stop watchers and events firing
disconnectScope(item.scope);
},
attachItem: function(item) {
if (!item.element[0].parentNode) {
this.transcludeParent[0].appendChild(item.element[0]);
}
reconnectScope(item.scope);
},
getLength: function() {
return this.data && this.data.length || 0;
},
setData: function(value) {
this.data = value;
this.calculateDataDimensions();
},
};

return CollectionRepeatDataSource;
}]);

/**
* Computes a hash of an 'obj'.
* Hash of a:
* string is string
* number is number as string
* object is either result of calling $$hashKey function on the object or uniquely generated id,
* that is also assigned to the $$hashKey property of the object.
*
* @param obj
* @returns {string} hash string such that the same input will have the same hash string.
* The resulting string key is in 'type:hashKey' format.
*/
function hashKey(obj) {
var objType = typeof obj,
key;

if (objType == 'object' && obj !== null) {
if (typeof (key = obj.$$hashKey) == 'function') {
// must invoke on object to keep the right this
key = obj.$$hashKey();
} else if (key === undefined) {
key = obj.$$hashKey = ionic.Utils.nextUid();
}
} else {
key = obj;
}

return objType + ':' + key;
}

function disconnectScope(scope) {
if (scope.$root === scope) {
return; // we can't disconnect the root node;
}
var parent = scope.$parent;
scope.$$disconnected = true;
// See Scope.$destroy
if (parent.$$childHead === scope) {
parent.$$childHead = scope.$$nextSibling;
}
if (parent.$$childTail === scope) {
parent.$$childTail = scope.$$prevSibling;
}
if (scope.$$prevSibling) {
scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
}
if (scope.$$nextSibling) {
scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
}
scope.$$nextSibling = scope.$$prevSibling = null;
}

function reconnectScope(scope) {
if (scope.$root === scope) {
return; // we can't disconnect the root node;
}
if (!scope.$$disconnected) {
return;
}
var parent = scope.$parent;
scope.$$disconnected = false;
// See Scope.$new for this logic...
scope.$$prevSibling = parent.$$childTail;
if (parent.$$childHead) {
parent.$$childTail.$$nextSibling = scope;
parent.$$childTail = scope;
} else {
parent.$$childHead = parent.$$childTail = scope;
}
}
Loading

0 comments on commit 934158b

Please sign in to comment.