ionic-cordova-gulp-seed
Version:
Ionic & Cordova & Gulp seed with organized code, tests, bower support and some other stuff. Originated from ionic-angular-cordova-seed.
343 lines (314 loc) • 12.3 kB
JavaScript
IonicModule
.factory('$collectionRepeatManager', [
'$rootScope',
'$timeout',
function($rootScope, $timeout) {
/**
* Vocabulary: "primary" and "secondary" size/direction/position mean
* "y" and "x" for vertical scrolling, or "x" and "y" for horizontal scrolling.
*/
function CollectionRepeatManager(options) {
var self = this;
this.dataSource = options.dataSource;
this.element = options.element;
this.scrollView = options.scrollView;
this.isVertical = !!this.scrollView.options.scrollingY;
this.renderedItems = {};
this.dimensions = [];
this.setCurrentIndex(0);
//Override scrollview's render callback
this.scrollView.__$callback = this.scrollView.__callback;
this.scrollView.__callback = angular.bind(this, this.renderScroll);
function getViewportSize() { return self.viewportSize; }
//Set getters and setters to match whether this scrollview is vertical or not
if (this.isVertical) {
this.scrollView.options.getContentHeight = getViewportSize;
this.scrollValue = function() {
return this.scrollView.__scrollTop;
};
this.scrollMaxValue = function() {
return this.scrollView.__maxScrollTop;
};
this.scrollSize = function() {
return this.scrollView.__clientHeight;
};
this.secondaryScrollSize = function() {
return this.scrollView.__clientWidth;
};
this.transformString = function(y, x) {
return 'translate3d('+x+'px,'+y+'px,0)';
};
this.primaryDimension = function(dim) {
return dim.height;
};
this.secondaryDimension = function(dim) {
return dim.width;
};
} else {
this.scrollView.options.getContentWidth = getViewportSize;
this.scrollValue = function() {
return this.scrollView.__scrollLeft;
};
this.scrollMaxValue = function() {
return this.scrollView.__maxScrollLeft;
};
this.scrollSize = function() {
return this.scrollView.__clientWidth;
};
this.secondaryScrollSize = function() {
return this.scrollView.__clientHeight;
};
this.transformString = function(x, y) {
return 'translate3d('+x+'px,'+y+'px,0)';
};
this.primaryDimension = function(dim) {
return dim.width;
};
this.secondaryDimension = function(dim) {
return dim.height;
};
}
}
CollectionRepeatManager.prototype = {
destroy: function() {
this.renderedItems = {};
this.render = angular.noop;
this.calculateDimensions = angular.noop;
this.dimensions = [];
},
/*
* Pre-calculate the position of all items in the data list.
* Do this using the provided width and height (primarySize and secondarySize)
* provided by the dataSource.
*/
calculateDimensions: function() {
/*
* For the sake of explanations below, we're going to pretend we are scrolling
* vertically: Items are laid out with primarySize being height,
* secondarySize being width.
*/
var primaryPos = 0;
var secondaryPos = 0;
var secondaryScrollSize = this.secondaryScrollSize();
var previousItem;
this.dataSource.beforeSiblings && this.dataSource.beforeSiblings.forEach(calculateSize, this);
var beforeSize = primaryPos + (previousItem ? previousItem.primarySize : 0);
primaryPos = secondaryPos = 0;
previousItem = null;
var dimensions = this.dataSource.dimensions.map(calculateSize, this);
var totalSize = primaryPos + (previousItem ? previousItem.primarySize : 0);
return {
beforeSize: beforeSize,
totalSize: totalSize,
dimensions: dimensions
};
function calculateSize(dim) {
//Each dimension is an object {width: Number, height: Number} provided by
//the dataSource
var rect = {
//Get the height out of the dimension object
primarySize: this.primaryDimension(dim),
//Max out the item's width to the width of the scrollview
secondarySize: Math.min(this.secondaryDimension(dim), secondaryScrollSize)
};
//If this isn't the first item
if (previousItem) {
//Move the item's x position over by the width of the previous item
secondaryPos += previousItem.secondarySize;
//If the y position is the same as the previous item and
//the x position is bigger than the scroller's width
if (previousItem.primaryPos === primaryPos &&
secondaryPos + rect.secondarySize > secondaryScrollSize) {
//Then go to the next row, with x position 0
secondaryPos = 0;
primaryPos += previousItem.primarySize;
}
}
rect.primaryPos = primaryPos;
rect.secondaryPos = secondaryPos;
previousItem = rect;
return rect;
}
},
resize: function() {
var result = this.calculateDimensions();
this.dimensions = result.dimensions;
this.viewportSize = result.totalSize;
this.beforeSize = result.beforeSize;
this.setCurrentIndex(0);
this.render(true);
this.dataSource.setup();
},
/*
* setCurrentIndex sets the index in the list that matches the scroller's position.
* Also save the position in the scroller for next and previous items (if they exist)
*/
setCurrentIndex: function(index, height) {
var currentPos = (this.dimensions[index] || {}).primaryPos || 0;
this.currentIndex = index;
this.hasPrevIndex = index > 0;
if (this.hasPrevIndex) {
this.previousPos = Math.max(
currentPos - this.dimensions[index - 1].primarySize,
this.dimensions[index - 1].primaryPos
);
}
this.hasNextIndex = index + 1 < this.dataSource.getLength();
if (this.hasNextIndex) {
this.nextPos = Math.min(
currentPos + this.dimensions[index + 1].primarySize,
this.dimensions[index + 1].primaryPos
);
}
},
/**
* override the scroller's render callback to check if we need to
* re-render our collection
*/
renderScroll: ionic.animationFrameThrottle(function(transformLeft, transformTop, zoom, wasResize) {
if (this.isVertical) {
this.renderIfNeeded(transformTop);
} else {
this.renderIfNeeded(transformLeft);
}
return this.scrollView.__$callback(transformLeft, transformTop, zoom, wasResize);
}),
renderIfNeeded: function(scrollPos) {
if ((this.hasNextIndex && scrollPos >= this.nextPos) ||
(this.hasPrevIndex && scrollPos < this.previousPos)) {
// Math.abs(transformPos - this.lastRenderScrollValue) > 100) {
this.render();
}
},
/*
* getIndexForScrollValue: Given the most recent data index and a new scrollValue,
* find the data index that matches that scrollValue.
*
* Strategy (if we are scrolling down): keep going forward in the dimensions list,
* starting at the given index, until an item with height matching the new scrollValue
* is found.
*
* This is a while loop. In the worst case it will have to go through the whole list
* (eg to scroll from top to bottom). The most common case is to scroll
* down 1-3 items at a time.
*
* While this is not as efficient as it could be, optimizing it gives no noticeable
* benefit. We would have to use a new memory-intensive data structure for dimensions
* to fully optimize it.
*/
getIndexForScrollValue: function(i, scrollValue) {
var rect;
//Scrolling up
if (scrollValue <= this.dimensions[i].primaryPos) {
while ( (rect = this.dimensions[i - 1]) && rect.primaryPos > scrollValue) {
i--;
}
//Scrolling down
} else {
while ( (rect = this.dimensions[i + 1]) && rect.primaryPos < scrollValue) {
i++;
}
}
return i;
},
/*
* render: Figure out the scroll position, the index matching it, and then tell
* the data source to render the correct items into the DOM.
*/
render: function(shouldRedrawAll) {
var self = this;
var i;
var isOutOfBounds = ( this.currentIndex >= this.dataSource.getLength() );
// We want to remove all the items and redraw everything if we're out of bounds
// or a flag is passed in.
if (isOutOfBounds || shouldRedrawAll) {
for (i in this.renderedItems) {
this.removeItem(i);
}
// Just don't render anything if we're out of bounds
if (isOutOfBounds) return;
}
var rect;
var scrollValue = this.scrollValue();
// Scroll size = how many pixels are visible in the scroller at one time
var scrollSize = this.scrollSize();
// We take the current scroll value and add it to the scrollSize to get
// what scrollValue the current visible scroll area ends at.
var scrollSizeEnd = scrollSize + scrollValue;
// Get the new start index for scrolling, based on the current scrollValue and
// the most recent known index
var startIndex = this.getIndexForScrollValue(this.currentIndex, scrollValue);
// If we aren't on the first item, add one row of items before so that when the user is
// scrolling up he sees the previous item
var renderStartIndex = Math.max(startIndex - 1, 0);
// Keep adding items to the 'extra row above' until we get to a new row.
// This is for the case where there are multiple items on one row above
// the current item; we want to keep adding items above until
// a new row is reached.
while (renderStartIndex > 0 &&
(rect = this.dimensions[renderStartIndex]) &&
rect.primaryPos === this.dimensions[startIndex - 1].primaryPos) {
renderStartIndex--;
}
// Keep rendering items, adding them until we are past the end of the visible scroll area
i = renderStartIndex;
while ((rect = this.dimensions[i]) && (rect.primaryPos - rect.primarySize < scrollSizeEnd)) {
doRender(i, rect);
i++;
}
// Render two extra items at the end as a buffer
if (self.dimensions[i]) {
doRender(i, self.dimensions[i]);
i++;
}
if (self.dimensions[i]) {
doRender(i, self.dimensions[i]);
}
var renderEndIndex = i;
// Remove any items that were rendered and aren't visible anymore
for (var renderIndex in this.renderedItems) {
if (renderIndex < renderStartIndex || renderIndex > renderEndIndex) {
this.removeItem(renderIndex);
}
}
this.setCurrentIndex(startIndex);
function doRender(dataIndex, rect) {
if (dataIndex < self.dataSource.dataStartIndex) {
// do nothing
} else {
self.renderItem(dataIndex, rect.primaryPos - self.beforeSize, rect.secondaryPos);
}
}
},
renderItem: function(dataIndex, primaryPos, secondaryPos) {
// Attach an item, and set its transform position to the required value
var item = this.dataSource.attachItemAtIndex(dataIndex);
//console.log(dataIndex, item);
if (item && item.element) {
if (item.primaryPos !== primaryPos || item.secondaryPos !== secondaryPos) {
item.element.css(ionic.CSS.TRANSFORM, this.transformString(
primaryPos, secondaryPos
));
item.primaryPos = primaryPos;
item.secondaryPos = secondaryPos;
}
// Save the item in rendered items
this.renderedItems[dataIndex] = item;
} else {
// If an item at this index doesn't exist anymore, be sure to delete
// it from rendered items
delete this.renderedItems[dataIndex];
}
},
removeItem: function(dataIndex) {
// Detach a given item
var item = this.renderedItems[dataIndex];
if (item) {
item.primaryPos = item.secondaryPos = null;
this.dataSource.detachItem(item);
delete this.renderedItems[dataIndex];
}
}
};
return CollectionRepeatManager;
}]);