define(function(require, exports, module) { "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; } /** ignored by jsdoc **/ else if (window.XScroll && window.XScroll.Plugins) { return XScroll.Plugins.Infinite = Infinite; } });