;(function(define){define(function(require,exports,module){ /** * Dependencies */ var component = require('gaia-component'); var FastList = require('fast-list'); var scheduler = FastList.scheduler; var poplar = require('poplar'); require('gaia-sub-header'); /** * Pointer abstractions */ var isTouch = 'ontouchstart' in window; var touchcancel = isTouch ? 'touchcancel' : 'mousecancel'; var touchstart = isTouch ? 'touchstart' : 'mousedown'; var touchmove = isTouch ? 'touchmove' : 'mousemove'; var touchend = isTouch ? 'touchend' : 'mouseup'; /** * Mini Logger * * @type {Function} */ var debug = 0 ? (...args) => console.log('[GaiaFastList]', ...args) : () => {}; /** * Cache to persist content. * * We open ASAP as we need the cached * HTML content for the first-paint. * As it's async it can be done in * the background. * * @type {Promise} */ var cachesOpen = caches.open('gfl'); /** * Used to hide private properties behind. * * @type {Symbol} */ var keys = { internal: Symbol(), first: Symbol(), img: Symbol() }; /** * Public prototype. * * @type {Object} */ var GaiaFastListProto = { extensible: false, /** * Default store up to 5mb of images. * * @type {Number} */ imageCacheSize: 5e6, /** * Default store up to 500 images * before entries start getting * discarded. * * @type {Number} */ imageCacheLength: 500, /** * Called when the component is first created. * * @private */ created() { debug('create'); this.setupShadowRoot(); this.caching = this.getAttribute('caching'); this.offset = this.getAttribute('offset'); this.picker = this.getAttribute('picker'); this.bottom = this.getAttribute('bottom'); this.top = this.getAttribute('top'); this[keys.internal] = new Internal(this); }, /** * Used to define configuration passed * to internal FastList instantiation. * * NOTE: Must be called before `model` * if defined. * * @param {Object} props * @public */ configure(props) { debug('configure'); this[keys.internal].configure(props); }, /** * Set a data model for the list to render. * * The returned Promise will resolve * when rendering has *fully* completed. * * The `.rendered` Promise can be used * to determine when the list is *visually* * complete. * * @param {Array} value * @return {Promise} * @public */ setModel(value) { return this[keys.internal].setModel(value); }, /** * Should be called by the user when * they have finished incrementally * updating there model. * * This acts as an internal hook used * for updating caches. * * This only really concerns users * who are using caching and fetching * their model in chunks. * * @public */ cache() { debug('cache'); if (!this.caching) return; this[keys.internal].cachedHeight = null; return this[keys.internal].updateCache(); }, /** * Clear cached height and html. * * @public */ clearCache() { debug('clear cache'); this[keys.internal].clearCache(); }, /** * Smooth scrolls to the given position. * * @param {Number} y * @return {Promise} */ scrollTo(y) { return this[keys.internal].scrollTo(y); }, /** * Permanently destroy the component. * * @public */ destroy() { this[keys.internal].destroy(); }, /** * Public attributes/properties configuration * used by gaia-component.js. * * @type {Object} */ attrs: { rendered: { get() { return this[keys.internal].rendered.promise; } }, top: { get() { return this._top; }, set(value) { debug('set top', value); if (value == null) return; value = Number(value); if (value === this._top) return; this.setAttribute('top', value); this._top = value; } }, bottom: { get() { return this._bottom; }, set(value) { debug('set bottom', value); if (value == null) return; value = Number(value); if (value === this._bottom) return; this.setAttribute('bottom', value); this._bottom = value; } }, caching: { get() { return this._caching; }, set(value) { value = value || value === ''; if (value === this._caching) return; if (value) this.setAttribute('caching', ''); else this.removeAttribute('caching'); this._caching = value; } }, offset: { get() { return this._offset || 0; }, set(value) { if (value == this._offset) return; if (value) this.setAttribute('offset', value); else this.removeAttribute('offset'); this._offset = Number(value); } }, scrollTop: { get() { return this[keys.internal].getScrollTop(); }, set(value) { this[keys.internal].setScrollTop(value); } }, minScrollHeight: { get() { return this[keys.internal].list.style.minHeight; }, set(value) { this[keys.internal].list.style.minHeight = value; } }, picker: { get() { return this._picker; }, set(value) { value = value || value === ''; if (value === this._picker) return; if (value) this.setAttr('picker', ''); else this.removeAttr('picker'); this._picker = value; } } }, /*jshint ignore:start*/ template: `
X
search
`, /*jshint ignore:end*/ // test hooks FastList: FastList }; /** * Private internals. * @param {GaiaFastList} el */ function Internal(el) { var shadow = el.shadowRoot; this.el = el; this.renderedCache = this.renderCache(); this.listCreated = new Deferred(); this.rendered = new Deferred(); this.images = { list: [], hash: {}, bytes: 0 }; this.els = { list: shadow.querySelector('ul'), picker: shadow.querySelector('.picker'), overlay: shadow.querySelector('.overlay'), overlayIcon: shadow.querySelector('.overlay > .icon'), overlayText: shadow.querySelector('.overlay > .text'), container: shadow.querySelector('.fast-list'), listContent: shadow.querySelector('.fast-list content'), pickerItems: [] }; // define property names for FastList this.container = this.els.container; this.list = this.els.list; this.itemContainer = el; this.configureTemplates(); this.setupPicker(); // don't memory leak ObjectURLs addEventListener('pagehide', () => this.emptyImageCache()); debug('initialized'); } Internal.prototype = { headerHeight: 40, itemHeight: 60, /** * Setting the model for the first * time creates a new FastList. Setting * it subsequent times rerenders * the FastList with the new data. * * @param {Array} model * @private */ setModel(model) { debug('set model'); if (!model) return Promise.reject(new Error('model undefined')); this.sections = this.sectionize(model); this.model = model; return !this.fastList ? this.createList() : this.reloadData(); }, /** * Creates the FastList. * * We add the fast-gradient last as it's * expensive to paint (50-80ms) and only * required for scrolling, not first paint. * * @return {Promise} * @private */ createList() { return this.renderedCache .then(() => { debug('create list'); this.fastList = new this.el.FastList(this); return this.fastList.rendered; }) .then(() => { this.rendered.resolve(); this.els.list.style.transform = ''; this.removeCachedRender(); return this.fastList.complete; }) .then(() => { this.updateFastGradient(); this.listCreated.resolve(); }); }, /** * Reloading the list will completely * re-render the list. * * We must empty the image cache as data * may have changed or item indexed * may not longer map to new model. * * We must update the fast-gradient * as scrollHeight determines whether * fast-gradient needs to be painted. * * @private */ reloadData() { return this.listCreated.promise .then(() => { debug('reload data'); this.emptyImageCache(); return this.fastList.reloadData(); }) .then(() => this.updateFastGradient()); }, /** * Sorts items into sections using * the user provided getSectionName() * * @param {Array} items * @return {Object} */ sectionize(items) { debug('sectionize'); var sectioned = !!this.getSectionName; var count = 0; var result = {}; for (var i = 0, l = items.length; i < l; i++) { var item = items[i]; var section = sectioned && this.getSectionName(item); // When a section is not defined, flag // first list items and skip logic if (!section) { if (i === 0) item[keys.first] = true; else if (item[keys.first]) delete item[keys.first]; continue; } // When there is no section yet // we can assume that this item // is the first item in the section. if (!result[section]) { result[section] = []; item[keys.first] = true; // Make sure that any previously // assigned flags are removed as // order of items can be changed. } else if (item[keys.first]) { delete item[keys.first]; } result[section].push(item); count++; } this.hasSections = !!count; return this.hasSections && result; }, /** * Mixin properties/methods into the * 'data-source' object passed to FastList. * * @param {Object} props */ configure(props) { Object.assign(this, props); }, /** * Defines the templates to be used * for the sections and items. * * Users can provide templates by * placing