Source: plugins/infinite.js

"use strict";
var Util = require('../util'),
	Base = require('../base');

var transform = Util.prefixStyle("transform");
var transition = Util.prefixStyle("transition");

/**
 * An infinity dom-recycled list plugin for xscroll.
 * @constructor
 * @param {object} cfg
 * @param {string} cfg.transition recomposition cell with a transition
 * @param {string} cfg.infiniteElements dom-selector for reused elements
 * @param {function} cfg.renderHook render function for cell by per col or per row duration scrolling
 * @extends {Base}
 */
var Infinite = function(cfg) {
	Infinite.superclass.constructor.call(this, cfg);
	this.userConfig = Util.mix({
		transition: 'all 0.5s ease'
	}, cfg);
}

Util.extend(Infinite, Base, {
	/**
	 * a pluginId
	 * @memberOf Infinite
	 * @type {string}
	 */
	pluginId: "infinite",
	/**
	 * store the visible elements inside of view.
	 * @memberOf Infinite
	 * @type {object}
	 */
	visibleElements: {},
	/**
	 * store all elements data.
	 * @memberOf Infinite
	 * @type {object}
	 */
	sections: {},
	/**
	 * plugin initializer
	 * @memberOf Infinite
	 * @override Base
	 * @return {Infinite}
	 */
	pluginInitializer: function(xscroll) {
		var self = this;
		self.xscroll = xscroll;
		self.isY = !!(xscroll.userConfig.zoomType == "y");
		self._ = {
			_top:self.isY ? "_top" : "_left",
			_height:self.isY ? "_height" : "_width",
			top:self.isY ? "top" : "left",
			height:self.isY ? "height" : "width",
			width:self.isY ? "width" : "height",
			y:self.isY ? "y" : "x",
			translate:self.isY ? "translateY" : "translateX",
			containerHeight:self.isY ? "containerHeight" : "containerWidth",
			scrollTop:self.isY ? "scrollTop" : "scrollLeft",
		}
		self._initInfinite();
		xscroll.on("afterrender", function() {
			self.render();
			self._bindEvt();
		});
		return self;
	},
	/**
	 * detroy the plugin
	 * @memberOf Infinite
	 * @override Base
	 * @return {Infinite}
	 */
	pluginDestructor: function() {
		var self = this;
		var _ = self._;
		for (var i = 0; i < self.infiniteLength; i++) {
			self.infiniteElements[i].style[_.top] = "auto";
			self.infiniteElements[i].style[transform] = "none";
			self.infiniteElements[i].style.visibility = "hidden";
		}
		self.xscroll && self.xscroll.off("scroll", self._updateByScroll, self);
		self.xscroll && self.xscroll.off("tap panstart pan panend", self._cellEventsHandler, self);
		return self;
	},
	_initInfinite: function() {
		var self = this;
		var xscroll = self.xscroll;
		var _ = self._;
		self.sections = {};
		self.infiniteElements = xscroll.renderTo.querySelectorAll(self.userConfig.infiniteElements);
		self.infiniteLength = self.infiniteElements.length;
		self.infiniteElementsCache = (function() {
			var tmp = []
			for (var i = 0; i < self.infiniteLength; i++) {
				tmp.push({});
				self.infiniteElements[i].style.position = "absolute";
				self.infiniteElements[i].style[_.top] = 0;
				self.infiniteElements[i].style.visibility = "hidden";
				self.infiniteElements[i].style.display = "block";
				Util.addClass(self.infiniteElements[i], "_xs_infinite_elements_");
			}
			return tmp;
		})();
		self.elementsPos = {};
		return self;
	},
	_renderUnRecycledEl: function() {
		var self = this;
		var _ = self._;
		var translateZ = self.userConfig.gpuAcceleration ? " translateZ(0) " : "";
		for (var i in self.__serializedData) {
			var unrecycledEl = self.__serializedData[i];
			if (self.__serializedData[i]['recycled'] === false) {
				var el = unrecycledEl.id && document.getElementById(unrecycledEl.id.replace("#", "")) || document.createElement("div");
				var randomId = Util.guid("xs-row-");
				el.id = unrecycledEl.id || randomId;
				unrecycledEl.id = el.id;
				self.xscroll.content.appendChild(el);
				for (var attrName in unrecycledEl.style) {
					if (attrName != _.height && attrName != "display" && attrName != "position") {
						el.style[attrName] = unrecycledEl.style[attrName];
					}
				}
				el.style[_.top] = 0;
				el.style.position = "absolute";
				el.style.display = "block";
				el.style[_.height] = unrecycledEl[_._height] + "px";
				el.style[transform] = _.translate + "(" + unrecycledEl[_._top] + "px) " + translateZ;
				Util.addClass(el, unrecycledEl.className);
				self.userConfig.renderHook.call(self, el, unrecycledEl);
			}
		}
	},
	/**
	 * render or update the scroll contents
	 * @memberOf Infinite
	 * @return {Infinite}
	 */
	render: function() {
		var self = this;
		var _  = self._;
		var xscroll = self.xscroll;
		var offset = self.isY ? xscroll.getScrollTop() : xscroll.getScrollLeft();
		self.visibleElements = self.getVisibleElements(offset);
		self.__serializedData = self._computeDomPositions();
		xscroll.sticky && xscroll.sticky.render(true); //force render
		xscroll.fixed && xscroll.fixed.render();
		var size = xscroll[_.height];
		var containerSize = self._containerSize;
		if (containerSize < size) {
			containerSize = size;
		}
		xscroll[_.containerHeight] = containerSize;
		xscroll.container.style[_.height] = containerSize + "px";
		xscroll.content.style[_.height] = containerSize + "px";
		self._renderUnRecycledEl();
		self._updateByScroll();
		self._updateByRender(offset);
		self.xscroll.boundryCheck();
		return self;
	},
	_getChangedRows: function(newElementsPos) {
		var self = this;
		var changedRows = {};
		for (var i in self.elementsPos) {
			if (!newElementsPos.hasOwnProperty(i)) {
				changedRows[i] = "delete";
			}
		}
		for (var i in newElementsPos) {
			if (newElementsPos[i].recycled && !self.elementsPos.hasOwnProperty(i)) {
				changedRows[i] = "add";
			}
		}
		self.elementsPos = newElementsPos;
		return changedRows;
	},
	_updateByScroll: function(e) {
		var self = this;
		var xscroll = self.xscroll;
		var _ = self._;
		var _pos = e && e[_.scrollTop];
		var pos = _pos === undefined ? (self.isY ? xscroll.getScrollTop() : xscroll.getScrollLeft()) : _pos;
		var elementsPos = self.getVisibleElements(pos);
		var changedRows = self.changedRows = self._getChangedRows(elementsPos);
		try{
			for (var i in changedRows) {
				if (changedRows[i] == "delete") {
					self._pushEl(i);
				}
				if (changedRows[i] == "add") {
					var elObj = self._popEl(elementsPos[i][self.guid]);
					var index = elObj.index;
					var el = elObj.el;
					if (el) {
						self.infiniteElementsCache[index].guid = elementsPos[i].guid;
						self.__serializedData[elementsPos[i].guid].__infiniteIndex = index;
						self._renderData(el, elementsPos[i]);
						self._renderStyle(el, elementsPos[i]);
					}
				}
			}
		}catch(e){
			console.warn('Not enough infiniteElements setted!');
		}
		return self;
	},
	_updateByRender: function(pos) {
		var self = this;
		var _ = self._;
		var xscroll = self.xscroll;
		var pos = pos === undefined ? (self.isY ? xscroll.getScrollTop() : xscroll.getScrollLeft()) : pos;
		var prevElementsPos = self.visibleElements;
		var newElementsPos = self.getVisibleElements(pos);
		var prevEl, newEl;
		//repaint
		for (var i in newElementsPos) {
			newEl = newElementsPos[i];
			for (var j in prevElementsPos) {
				prevEl = prevElementsPos[j];
				if (prevEl.guid === newEl.guid) {
					if (newEl.style != prevEl.style || newEl[_._top] != prevEl[_._top] || newEl[_._height] != prevEl[_._height]) {
						self._renderStyle(self.infiniteElements[newEl.__infiniteIndex], newEl, true);
					}
					if (JSON.stringify(newEl.data) != JSON.stringify(prevEl.data)) {
						self._renderData(self.infiniteElements[newEl.__infiniteIndex], newEl);
					}
				} else {
					// paint
					if (self.__serializedData[newEl.guid].recycled && self.__serializedData[newEl.guid].__infiniteIndex === undefined) {
						var elObj = self._popEl();
						self.__serializedData[newEl.guid].__infiniteIndex = elObj.index;
						self._renderData(elObj.el, newEl);
						self._renderStyle(elObj.el, newEl);
					}
				}
			}
		}
		self.visibleElements = newElementsPos;
	},
	/**
	 * get all element posInfo such as top,height,template,html
	 * @return {Array}
	 **/
	_computeDomPositions: function() {
		var self = this;
		var _ = self._;
		var pos = 0,
			size = 0,
			sections = self.sections,
			section;
		var data = [];
		var serializedData = {};
		for (var i in sections) {
			for (var j = 0, len = sections[i].length; j < len; j++) {
				section = sections[i][j];
				section.sectionId = i;
				section.index = j;
				data.push(section);
			}
		}

		//f = v/itemSize*1000 < 60 => v = 0.06 * itemSize
		self.userConfig.maxSpeed = 0.06 * 50;
		for (var i = 0, l = data.length; i < l; i++) {
			var item = data[i];
			size = item.style && item.style[_.height] >= 0 && item.style.position != "fixed" ? item.style[_.height] : 0;
			item.guid = item.guid || Util.guid();
			item[_._top] = pos;
			item[_._height] = size;
			item.recycled = item.recycled === false ? false : true;
			pos += size;
			serializedData[item.guid] = item;
		}
		self._containerSize = pos;
		return serializedData;
	},
	/**
	 * get all elements inside of the view.
	 * @memberOf Infinite
	 * @param {number} pos scrollLeft or scrollTop
	 * @return {object} visibleElements
	 */
	getVisibleElements: function(pos) {
		var self = this;
		var xscroll = self.xscroll;
		var _ = self._;
		var pos = pos === undefined ? (self.isY ? xscroll.getScrollTop() : xscroll.getScrollLeft()) : pos;
		var threshold = self.userConfig.threshold >= 0 ? self.userConfig.threshold : xscroll[_.height] / 3;
		var tmp = {},
			item;
		var data = self.__serializedData;
		for (var i in data) {
			item = data[i];
			if (item[_._top] >= pos - threshold && item[_._top] <= pos + xscroll[_.height] + threshold) {
				tmp[item.guid] = item;
			}
		}
		return JSON.parse(JSON.stringify(tmp));
	},
	_popEl: function() {
		var self = this;
		for (var i = 0; i < self.infiniteLength; i++) {
			if (!self.infiniteElementsCache[i]._visible) {
				self.infiniteElementsCache[i]._visible = true;
				return {
					index: i,
					el: self.infiniteElements[i]
				}
			}
		}
	},
	_pushEl: function(guid) {
		var self = this;
		for (var i = 0; i < self.infiniteLength; i++) {
			if (self.infiniteElementsCache[i].guid == guid) {
				self.infiniteElementsCache[i]._visible = false;
				self.infiniteElements[i].style.visibility = "hidden";
				self.infiniteElementsCache[i].guid = null;
			}
		}
	},
	_renderData: function(el, elementObj) {
		var self = this;
		if (!el || !elementObj || elementObj.style.position == "fixed") return;
		self.userConfig.renderHook.call(self, el, elementObj);
	},
	_renderStyle: function(el, elementObj, useTransition) {
		var self = this;
		var _ = self._;
		if (!el) return;
		var translateZ = self.xscroll.userConfig.gpuAcceleration ? " translateZ(0) " : "";
		//update style
		for (var attrName in elementObj.style) {
			if (attrName != _.height && attrName != "display" && attrName != "position") {
				el.style[attrName] = elementObj.style[attrName];
			}
		}
		el.setAttribute("xs-index", elementObj.index);
		el.setAttribute("xs-sectionid", elementObj.sectionId);
		el.setAttribute("xs-guid", elementObj.guid);
		el.style.visibility = "visible";
		el.style[_.height] = elementObj[_._height] + "px";
		el.style[transform] = _.translate + "(" + elementObj[_._top] + "px) " + translateZ;
		el.style[transition] = useTransition ? self.userConfig.transition : "none";
	},
	getCell: function(e) {
		var self = this,
			cell;
		var el = Util.findParentEl(e.target, "._xs_infinite_elements_", self.xscroll.renderTo);
		if(!el){
			el = Util.findParentEl(e.target, ".xs-sticky-handler", self.xscroll.renderTo);
		}
		var guid = el && el.getAttribute("xs-guid");
		if (undefined === guid) return;
		return {
			data:self.__serializedData[guid],
			el:el
		};
	},
	_bindEvt: function() {
		var self = this;
		if (self._isEvtBinded) return;
		self._isEvtBinded = true;
		self.xscroll.renderTo.addEventListener("webkitTransitionEnd", function(e) {
			if (e.target.className.match(/xs-row/)) {
				e.target.style.webkitTransition = "";
			}
		});
		self.xscroll.on("scroll", self._updateByScroll, self);
		self.xscroll.on("tap panstart pan panend", self._cellEventsHandler, self);
		return self;
	},
	_cellEventsHandler: function(e) {
		var self = this;
		var cell = self.getCell(e);
		e.cell = cell.data;
		e.cellEl = cell.el;
		e.cell && self[e.type].call(self, e);
	},
	/**
	 * tap event
	 * @memberOf Infinite
	 * @param {object} e events data include cell object
	 * @event
	 */
	tap: function(e) {
		this.trigger("tap", e);
		return this;
	},
	/**
	 * panstart event
	 * @memberOf Infinite
	 * @param {object} e events data include cell object
	 * @event
	 */
	panstart: function(e) {
		this.trigger("panstart", e);
		return this;
	},
	/**
	 * pan event
	 * @memberOf Infinite
	 * @param {object} e events data include cell object
	 * @event
	 */
	pan: function(e) {
		this.trigger("pan", e);
		return this;
	},
	/**
	 * panend event
	 * @memberOf Infinite
	 * @param {object} e events data include cell object
	 * @event
	 */
	panend: function(e) {
		this.trigger("panend", e);
		return this;
	},
	/**
	 * insert data before a position
	 * @memberOf Infinite
	 * @param {string} sectionId sectionId of the target cell
	 * @param {number} index index of the target cell
	 * @param {object} data data to insert
	 * @return {Infinite}
	 */
	insertBefore: function(sectionId, index, data) {
		var self = this;
		if (sectionId === undefined || index === undefined || data === undefined) return self;
		if (!self.sections[sectionId]) {
			self.sections[sectionId] = [];
		}
		self.sections[sectionId].splice(index, 0, data);
		return self;
	},
	/**
	 * insert data after a position
	 * @memberOf Infinite
	 * @param {string} sectionId sectionId of the target cell
	 * @param {number} index index of the target cell
	 * @param {object} data data to insert
	 * @return {Infinite}
	 */
	insertAfter: function(sectionId, index, data) {
		var self = this;
		if (sectionId === undefined || index === undefined || data === undefined) return self;
		if (!self.sections[sectionId]) {
			self.sections[sectionId] = [];
		}
		self.sections[sectionId].splice(Number(index) + 1, 0, data);
		return self;
	},
	/**
	 * append data after a section
	 * @memberOf Infinite
	 * @param {string} sectionId sectionId for the append cell
	 * @param {object} data data to append
	 * @return {Infinite}
	 */
	append: function(sectionId, data) {
		var self = this;
		if (!self.sections[sectionId]) {
			self.sections[sectionId] = [];
		}
		self.sections[sectionId] = self.sections[sectionId].concat(data);
		return self;
	},
	/**
	 * remove some data by sectionId,from,number
	 * @memberOf Infinite
	 * @param {string} sectionId sectionId for the append cell
	 * @param {number} from removed index from
	 * @param {number} number removed data number
	 * @return {Infinite}
	 */
	remove: function(sectionId, from, number) {
		var self = this;
		var number = number || 1;
		if (undefined === sectionId || !self.sections[sectionId]) return self;
		//remove a section
		if (undefined === from) {
			self.sections[sectionId] = null;
			return self;
		}
		//remove some data in section
		if (self.sections[sectionId] && self.sections[sectionId][from]) {
			self.sections[sectionId].splice(from, number);
			return self;
		}
		return self;
	},
	/**
	 * replace some data by sectionId and index
	 * @memberOf Infinite
	 * @param {string} sectionId sectionId to replace
	 * @param {number} index removed index from
	 * @param {object} data new data to replace
	 * @return {Infinite}
	 */
	replace: function(sectionId, index, data) {
		var self = this;
		if (undefined === sectionId || !self.sections[sectionId]) return self;
		self.sections[sectionId][index] = data;
		return self;
	},
	/**
	 * get data by sectionId and index
	 * @memberOf Infinite
	 * @param {string} sectionId sectionId
	 * @param {number} index index in the section
	 * @return {object} data data
	 */
	get: function(sectionId, index) {
		if (undefined === sectionId) return;
		if (undefined === index) return this.sections[sectionId];
		return this.sections[sectionId][index];
	}
});

if (typeof module == 'object' && module.exports) {
	module.exports = Infinite;
}