framework7
Version:
Full featured mobile HTML framework for building iOS & Android apps
583 lines (561 loc) • 18.2 kB
JavaScript
import { getDocument } from 'ssr-window';
import $ from '../../shared/dom7.js';
import { extend, deleteProps } from '../../shared/utils.js';
import Framework7Class from '../../shared/class.js';
import { getDevice } from '../../shared/get-device.js';
class VirtualList extends Framework7Class {
constructor(app, params) {
if (params === void 0) {
params = {};
}
super(params, [app]);
const vl = this;
const device = getDevice();
const document = getDocument();
let defaultHeight;
if (app.theme === 'md') {
defaultHeight = 48;
} else if (app.theme === 'ios') {
defaultHeight = 44;
}
const defaults = {
cols: 1,
height: defaultHeight,
cache: true,
dynamicHeightBufferSize: 1,
showFilteredItemsOnly: false,
renderExternal: undefined,
setListHeight: true,
searchByItem: undefined,
searchAll: undefined,
ul: null,
createUl: true,
scrollableParentEl: undefined,
renderItem(item) {
return `
<li>
<div class="item-content">
<div class="item-inner">
<div class="item-title">${item}</div>
</div>
</div>
</li>
`.trim();
},
on: {}
};
// Extend defaults with modules params
vl.useModulesParams(defaults);
vl.params = extend(defaults, params);
if (vl.params.height === undefined || !vl.params.height) {
vl.params.height = defaultHeight;
}
vl.$el = $(params.el);
vl.el = vl.$el[0];
if (vl.$el.length === 0) return undefined;
vl.$el[0].f7VirtualList = vl;
vl.items = vl.params.items;
if (vl.params.showFilteredItemsOnly) {
vl.filteredItems = [];
}
if (vl.params.renderItem) {
vl.renderItem = vl.params.renderItem;
}
vl.$pageContentEl = vl.$el.parents('.page-content');
vl.pageContentEl = vl.$pageContentEl[0];
vl.$scrollableParentEl = vl.params.scrollableParentEl ? $(vl.params.scrollableParentEl).eq(0) : vl.$pageContentEl;
if (!vl.$scrollableParentEl.length && vl.$pageContentEl.length) {
vl.$scrollableParentEl = vl.$pageContentEl;
}
vl.scrollableParentEl = vl.$scrollableParentEl[0];
// Bad scroll
if (typeof vl.params.updatableScroll !== 'undefined') {
vl.updatableScroll = vl.params.updatableScroll;
} else {
vl.updatableScroll = true;
if (device.ios && device.osVersion.split('.')[0] < 8) {
vl.updatableScroll = false;
}
}
// Append <ul>
const ul = vl.params.ul;
vl.$ul = ul ? $(vl.params.ul) : vl.$el.children('ul');
if (vl.$ul.length === 0 && vl.params.createUl) {
vl.$el.append('<ul></ul>');
vl.$ul = vl.$el.children('ul');
}
vl.ul = vl.$ul[0];
let $itemsWrapEl;
if (!vl.ul && !vl.params.createUl) $itemsWrapEl = vl.$el;else $itemsWrapEl = vl.$ul;
extend(vl, {
$itemsWrapEl,
itemsWrapEl: $itemsWrapEl[0],
// DOM cached items
domCache: {},
// Temporary DOM Element
tempDomElement: document.createElement('ul'),
// Last repain position
lastRepaintY: null,
// Fragment
fragment: document.createDocumentFragment(),
// Props
pageHeight: undefined,
rowsPerScreen: undefined,
rowsBefore: undefined,
rowsAfter: undefined,
rowsToRender: undefined,
maxBufferHeight: 0,
listHeight: undefined,
dynamicHeight: typeof vl.params.height === 'function',
autoHeight: vl.params.height === 'auto'
});
// Install Modules
vl.useModules();
// Attach events
const handleScrollBound = vl.handleScroll.bind(vl);
const handleResizeBound = vl.handleResize.bind(vl);
let $pageEl;
let $tabEl;
let $panelEl;
let $popupEl;
vl.attachEvents = function attachEvents() {
$pageEl = vl.$el.parents('.page').eq(0);
$tabEl = vl.$el.parents('.tab').filter(tabEl => {
return $(tabEl).parent('.tabs').parent('.tabs-animated-wrap, swiper-container.tabs').length === 0;
}).eq(0);
$panelEl = vl.$el.parents('.panel').eq(0);
$popupEl = vl.$el.parents('.popup').eq(0);
vl.$scrollableParentEl.on('scroll', handleScrollBound);
if ($pageEl.length) $pageEl.on('page:reinit', handleResizeBound);
if ($tabEl.length) $tabEl.on('tab:show', handleResizeBound);
if ($panelEl.length) $panelEl.on('panel:open', handleResizeBound);
if ($popupEl.length) $popupEl.on('popup:open', handleResizeBound);
app.on('resize', handleResizeBound);
};
vl.detachEvents = function attachEvents() {
vl.$scrollableParentEl.off('scroll', handleScrollBound);
if ($pageEl.length) $pageEl.off('page:reinit', handleResizeBound);
if ($tabEl.length) $tabEl.off('tab:show', handleResizeBound);
if ($panelEl.length) $panelEl.off('panel:open', handleResizeBound);
if ($popupEl.length) $popupEl.off('popup:open', handleResizeBound);
app.off('resize', handleResizeBound);
};
// Init
vl.init();
return vl;
}
setListSize(autoHeightRerender) {
const vl = this;
const items = vl.filteredItems || vl.items;
if (!autoHeightRerender) {
vl.pageHeight = vl.$scrollableParentEl[0].offsetHeight;
}
if (vl.dynamicHeight) {
vl.listHeight = 0;
vl.heights = [];
for (let i = 0; i < items.length; i += 1) {
const itemHeight = vl.params.height(items[i]);
vl.listHeight += itemHeight;
vl.heights.push(itemHeight);
}
} else if (vl.autoHeight) {
vl.listHeight = 0;
if (!vl.heights) vl.heights = [];
if (!vl.heightsCalculated) vl.heightsCalculated = [];
const renderedItems = {};
vl.$itemsWrapEl.find(`[data-virtual-list-index]`).forEach(el => {
renderedItems[parseInt(el.getAttribute('data-virtual-list-index'), 10)] = el;
});
for (let i = 0; i < items.length; i += 1) {
const itemIndex = vl.items.indexOf(items[i]);
const renderedItem = renderedItems[itemIndex];
if (renderedItem) {
if (!vl.heightsCalculated.includes(itemIndex)) {
vl.heights[itemIndex] = renderedItem.offsetHeight;
vl.heightsCalculated.push(itemIndex);
}
}
if (typeof vl.heights[i] === 'undefined') {
vl.heights[itemIndex] = 40;
}
vl.listHeight += vl.heights[itemIndex];
}
} else {
vl.listHeight = Math.ceil(items.length / vl.params.cols) * vl.params.height;
vl.rowsPerScreen = Math.ceil(vl.pageHeight / vl.params.height);
vl.rowsBefore = vl.params.rowsBefore || vl.rowsPerScreen * 2;
vl.rowsAfter = vl.params.rowsAfter || vl.rowsPerScreen;
vl.rowsToRender = vl.rowsPerScreen + vl.rowsBefore + vl.rowsAfter;
vl.maxBufferHeight = vl.rowsBefore / 2 * vl.params.height;
}
if (vl.updatableScroll || vl.params.setListHeight) {
vl.$itemsWrapEl.css({
height: `${vl.listHeight}px`
});
}
}
render(force, forceScrollTop) {
const vl = this;
if (force) vl.lastRepaintY = null;
let scrollTop = -(vl.$el[0].getBoundingClientRect().top - vl.$scrollableParentEl[0].getBoundingClientRect().top);
if (typeof forceScrollTop !== 'undefined') scrollTop = forceScrollTop;
if (vl.lastRepaintY === null || Math.abs(scrollTop - vl.lastRepaintY) > vl.maxBufferHeight || !vl.updatableScroll && vl.$scrollableParentEl[0].scrollTop + vl.pageHeight >= vl.$scrollableParentEl[0].scrollHeight) {
vl.lastRepaintY = scrollTop;
} else {
return;
}
const items = vl.filteredItems || vl.items;
let fromIndex;
let toIndex;
let heightBeforeFirstItem = 0;
let heightBeforeLastItem = 0;
if (vl.dynamicHeight || vl.autoHeight) {
let itemTop = 0;
let itemHeight;
vl.maxBufferHeight = vl.pageHeight;
for (let j = 0; j < vl.heights.length; j += 1) {
itemHeight = vl.heights[j];
if (typeof fromIndex === 'undefined') {
if (itemTop + itemHeight >= scrollTop - vl.pageHeight * 2 * vl.params.dynamicHeightBufferSize) fromIndex = j;else heightBeforeFirstItem += itemHeight;
}
if (typeof toIndex === 'undefined') {
if (itemTop + itemHeight >= scrollTop + vl.pageHeight * 2 * vl.params.dynamicHeightBufferSize || j === vl.heights.length - 1) toIndex = j + 1;
heightBeforeLastItem += itemHeight;
}
itemTop += itemHeight;
}
toIndex = Math.min(toIndex, items.length);
} else {
fromIndex = (parseInt(scrollTop / vl.params.height, 10) - vl.rowsBefore) * vl.params.cols;
if (fromIndex < 0) {
fromIndex = 0;
}
toIndex = Math.min(fromIndex + vl.rowsToRender * vl.params.cols, items.length);
}
let topPosition;
const renderExternalItems = [];
vl.reachEnd = false;
let i;
for (i = fromIndex; i < toIndex; i += 1) {
let itemEl;
// Define real item index
const index = vl.items.indexOf(items[i]);
if (i === fromIndex) vl.currentFromIndex = index;
if (i === toIndex - 1) vl.currentToIndex = index;
if (vl.filteredItems) {
if (vl.items[index] === vl.filteredItems[vl.filteredItems.length - 1]) vl.reachEnd = true;
} else if (index === vl.items.length - 1) vl.reachEnd = true;
// Find items
if (vl.params.renderExternal) {
renderExternalItems.push(items[i]);
} else if (vl.domCache[index]) {
itemEl = vl.domCache[index];
itemEl.f7VirtualListIndex = index;
} else {
if (vl.renderItem) {
vl.tempDomElement.innerHTML = vl.renderItem(items[i], index).trim();
} else {
vl.tempDomElement.innerHTML = items[i].toString().trim();
}
itemEl = vl.tempDomElement.childNodes[0];
if (vl.params.cache) vl.domCache[index] = itemEl;
itemEl.f7VirtualListIndex = index;
}
// Set item top position
if (i === fromIndex) {
if (vl.dynamicHeight || vl.autoHeight) {
topPosition = heightBeforeFirstItem;
} else {
topPosition = i * vl.params.height / vl.params.cols;
}
}
if (!vl.params.renderExternal) {
itemEl.style.top = `${topPosition}px`;
// Before item insert
vl.emit('local::itemBeforeInsert vlItemBeforeInsert', vl, itemEl, items[i]);
// Append item to fragment
vl.fragment.appendChild(itemEl);
}
}
// Update list height with not updatable scroll
if (!vl.updatableScroll) {
if (vl.dynamicHeight || vl.autoHeight) {
vl.itemsWrapEl.style.height = `${heightBeforeLastItem}px`;
} else {
vl.itemsWrapEl.style.height = `${i * vl.params.height / vl.params.cols}px`;
}
}
// Update list html
if (vl.params.renderExternal) {
if (items && items.length === 0) {
vl.reachEnd = true;
}
} else {
vl.emit('local::beforeClear vlBeforeClear', vl, vl.fragment);
vl.itemsWrapEl.innerHTML = '';
vl.emit('local::itemsBeforeInsert vlItemsBeforeInsert', vl, vl.fragment);
if (items && items.length === 0) {
vl.reachEnd = true;
if (vl.params.emptyTemplate) vl.itemsWrapEl.innerHTML = vl.params.emptyTemplate;
} else {
vl.itemsWrapEl.appendChild(vl.fragment);
}
vl.emit('local::itemsAfterInsert vlItemsAfterInsert', vl, vl.fragment);
}
if (typeof forceScrollTop !== 'undefined' && force) {
vl.$scrollableParentEl.scrollTop(forceScrollTop, 0);
}
if (vl.params.renderExternal) {
vl.params.renderExternal(vl, {
fromIndex,
toIndex,
listHeight: vl.listHeight,
topPosition,
items: renderExternalItems
});
}
if (vl.autoHeight) {
requestAnimationFrame(() => {
vl.setListSize(true);
});
}
}
// Filter
filterItems(indexes, resetScrollTop) {
if (resetScrollTop === void 0) {
resetScrollTop = true;
}
const vl = this;
vl.filteredItems = [];
for (let i = 0; i < indexes.length; i += 1) {
vl.filteredItems.push(vl.items[indexes[i]]);
}
if (resetScrollTop) {
vl.$scrollableParentEl[0].scrollTop = 0;
}
vl.update();
}
resetFilter() {
const vl = this;
if (vl.params.showFilteredItemsOnly) {
vl.filteredItems = [];
} else {
vl.filteredItems = null;
delete vl.filteredItems;
}
vl.update();
}
scrollToItem(index) {
const vl = this;
if (index > vl.items.length) return false;
let itemTop = 0;
if (vl.dynamicHeight || vl.autoHeight) {
for (let i = 0; i < index; i += 1) {
itemTop += vl.heights[i];
}
} else {
itemTop = index * vl.params.height;
}
const listTop = vl.$el[0].offsetTop;
vl.render(true, listTop + itemTop - parseInt(vl.$scrollableParentEl.css('padding-top'), 10));
return true;
}
handleScroll() {
const vl = this;
vl.render();
}
// Handle resize event
isVisible() {
const vl = this;
return !!(vl.el.offsetWidth || vl.el.offsetHeight || vl.el.getClientRects().length);
}
handleResize() {
const vl = this;
if (vl.isVisible()) {
vl.heightsCalculated = [];
vl.setListSize();
vl.render(true);
}
}
// Append
appendItems(items) {
const vl = this;
for (let i = 0; i < items.length; i += 1) {
vl.items.push(items[i]);
}
vl.update();
}
appendItem(item) {
const vl = this;
vl.appendItems([item]);
}
// Replace
replaceAllItems(items) {
const vl = this;
vl.items = items;
delete vl.filteredItems;
vl.domCache = {};
vl.update();
}
replaceItem(index, item) {
const vl = this;
vl.items[index] = item;
if (vl.params.cache) delete vl.domCache[index];
vl.update();
}
// Prepend
prependItems(items) {
const vl = this;
for (let i = items.length - 1; i >= 0; i -= 1) {
vl.items.unshift(items[i]);
}
if (vl.params.cache) {
const newCache = {};
Object.keys(vl.domCache).forEach(cached => {
newCache[parseInt(cached, 10) + items.length] = vl.domCache[cached];
});
vl.domCache = newCache;
}
vl.update();
}
prependItem(item) {
const vl = this;
vl.prependItems([item]);
}
// Move
moveItem(from, to) {
const vl = this;
const fromIndex = from;
let toIndex = to;
if (fromIndex === toIndex) return;
// remove item from array
const item = vl.items.splice(fromIndex, 1)[0];
if (toIndex >= vl.items.length) {
// Add item to the end
vl.items.push(item);
toIndex = vl.items.length - 1;
} else {
// Add item to new index
vl.items.splice(toIndex, 0, item);
}
// Update cache
if (vl.params.cache) {
const newCache = {};
Object.keys(vl.domCache).forEach(cached => {
const cachedIndex = parseInt(cached, 10);
const leftIndex = fromIndex < toIndex ? fromIndex : toIndex;
const rightIndex = fromIndex < toIndex ? toIndex : fromIndex;
const indexShift = fromIndex < toIndex ? -1 : 1;
if (cachedIndex < leftIndex || cachedIndex > rightIndex) newCache[cachedIndex] = vl.domCache[cachedIndex];
if (cachedIndex === leftIndex) newCache[rightIndex] = vl.domCache[cachedIndex];
if (cachedIndex > leftIndex && cachedIndex <= rightIndex) newCache[cachedIndex + indexShift] = vl.domCache[cachedIndex];
});
vl.domCache = newCache;
}
vl.update();
}
// Insert before
insertItemBefore(index, item) {
const vl = this;
if (index === 0) {
vl.prependItem(item);
return;
}
if (index >= vl.items.length) {
vl.appendItem(item);
return;
}
vl.items.splice(index, 0, item);
// Update cache
if (vl.params.cache) {
const newCache = {};
Object.keys(vl.domCache).forEach(cached => {
const cachedIndex = parseInt(cached, 10);
if (cachedIndex >= index) {
newCache[cachedIndex + 1] = vl.domCache[cachedIndex];
}
});
vl.domCache = newCache;
}
vl.update();
}
// Delete
deleteItems(indexes) {
const vl = this;
let prevIndex;
let indexShift = 0;
for (let i = 0; i < indexes.length; i += 1) {
let index = indexes[i];
if (typeof prevIndex !== 'undefined') {
if (index > prevIndex) {
indexShift = -i;
}
}
index += indexShift;
prevIndex = indexes[i];
// Delete item
const deletedItem = vl.items.splice(index, 1)[0];
// Delete from filtered
if (vl.filteredItems && vl.filteredItems.indexOf(deletedItem) >= 0) {
vl.filteredItems.splice(vl.filteredItems.indexOf(deletedItem), 1);
}
// Update cache
if (vl.params.cache) {
const newCache = {};
Object.keys(vl.domCache).forEach(cached => {
const cachedIndex = parseInt(cached, 10);
if (cachedIndex === index) {
delete vl.domCache[index];
} else if (parseInt(cached, 10) > index) {
newCache[cachedIndex - 1] = vl.domCache[cached];
} else {
newCache[cachedIndex] = vl.domCache[cached];
}
});
vl.domCache = newCache;
}
}
vl.update();
}
deleteAllItems() {
const vl = this;
vl.items = [];
delete vl.filteredItems;
if (vl.params.cache) vl.domCache = {};
vl.update();
}
deleteItem(index) {
const vl = this;
vl.deleteItems([index]);
}
// Clear cache
clearCache() {
const vl = this;
vl.domCache = {};
}
// Update Virtual List
update(deleteCache) {
const vl = this;
if (deleteCache && vl.params.cache) {
vl.domCache = {};
}
vl.heightsCalculated = [];
vl.setListSize();
vl.render(true);
}
init() {
const vl = this;
vl.attachEvents();
vl.setListSize();
vl.render();
}
destroy() {
let vl = this;
vl.detachEvents();
vl.$el[0].f7VirtualList = null;
delete vl.$el[0].f7VirtualList;
deleteProps(vl);
vl = null;
}
}
export default VirtualList;