UNPKG

17.5 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5const index = require('./index-a0a08b2a.js');
6const helpers = require('./helpers-d381ec4d.js');
7
8const CELL_TYPE_ITEM = 'item';
9const CELL_TYPE_HEADER = 'header';
10const CELL_TYPE_FOOTER = 'footer';
11const NODE_CHANGE_NONE = 0;
12const NODE_CHANGE_POSITION = 1;
13const NODE_CHANGE_CELL = 2;
14
15const MIN_READS = 2;
16const updateVDom = (dom, heightIndex, cells, range) => {
17 // reset dom
18 for (const node of dom) {
19 node.change = NODE_CHANGE_NONE;
20 node.d = true;
21 }
22 // try to match into exisiting dom
23 const toMutate = [];
24 const end = range.offset + range.length;
25 for (let i = range.offset; i < end; i++) {
26 const cell = cells[i];
27 const node = dom.find(n => n.d && n.cell === cell);
28 if (node) {
29 const top = heightIndex[i];
30 if (top !== node.top) {
31 node.top = top;
32 node.change = NODE_CHANGE_POSITION;
33 }
34 node.d = false;
35 }
36 else {
37 toMutate.push(cell);
38 }
39 }
40 // needs to append
41 const pool = dom.filter(n => n.d);
42 for (const cell of toMutate) {
43 const node = pool.find(n => n.d && n.cell.type === cell.type);
44 const index = cell.i;
45 if (node) {
46 node.d = false;
47 node.change = NODE_CHANGE_CELL;
48 node.cell = cell;
49 node.top = heightIndex[index];
50 }
51 else {
52 dom.push({
53 d: false,
54 cell,
55 visible: true,
56 change: NODE_CHANGE_CELL,
57 top: heightIndex[index],
58 });
59 }
60 }
61 dom
62 .filter(n => n.d && n.top !== -9999)
63 .forEach(n => {
64 n.change = NODE_CHANGE_POSITION;
65 n.top = -9999;
66 });
67};
68const doRender = (el, nodeRender, dom, updateCellHeight) => {
69 const children = Array.from(el.children).filter(n => n.tagName !== 'TEMPLATE');
70 const childrenNu = children.length;
71 let child;
72 for (let i = 0; i < dom.length; i++) {
73 const node = dom[i];
74 const cell = node.cell;
75 // the cell change, the content must be updated
76 if (node.change === NODE_CHANGE_CELL) {
77 if (i < childrenNu) {
78 child = children[i];
79 nodeRender(child, cell, i);
80 }
81 else {
82 const newChild = createNode(el, cell.type);
83 child = nodeRender(newChild, cell, i) || newChild;
84 child.classList.add('virtual-item');
85 el.appendChild(child);
86 }
87 child['$ionCell'] = cell;
88 }
89 else {
90 child = children[i];
91 }
92 // only update position when it changes
93 if (node.change !== NODE_CHANGE_NONE) {
94 child.style.transform = `translate3d(0,${node.top}px,0)`;
95 }
96 // update visibility
97 const visible = cell.visible;
98 if (node.visible !== visible) {
99 if (visible) {
100 child.classList.remove('virtual-loading');
101 }
102 else {
103 child.classList.add('virtual-loading');
104 }
105 node.visible = visible;
106 }
107 // dynamic height
108 if (cell.reads > 0) {
109 updateCellHeight(cell, child);
110 cell.reads--;
111 }
112 }
113};
114const createNode = (el, type) => {
115 const template = getTemplate(el, type);
116 if (template && el.ownerDocument) {
117 return el.ownerDocument.importNode(template.content, true).children[0];
118 }
119 return null;
120};
121const getTemplate = (el, type) => {
122 switch (type) {
123 case CELL_TYPE_ITEM: return el.querySelector('template:not([name])');
124 case CELL_TYPE_HEADER: return el.querySelector('template[name=header]');
125 case CELL_TYPE_FOOTER: return el.querySelector('template[name=footer]');
126 }
127};
128const getViewport = (scrollTop, vierportHeight, margin) => {
129 return {
130 top: Math.max(scrollTop - margin, 0),
131 bottom: scrollTop + vierportHeight + margin
132 };
133};
134const getRange = (heightIndex, viewport, buffer) => {
135 const topPos = viewport.top;
136 const bottomPos = viewport.bottom;
137 // find top index
138 let i = 0;
139 for (; i < heightIndex.length; i++) {
140 if (heightIndex[i] > topPos) {
141 break;
142 }
143 }
144 const offset = Math.max(i - buffer - 1, 0);
145 // find bottom index
146 for (; i < heightIndex.length; i++) {
147 if (heightIndex[i] >= bottomPos) {
148 break;
149 }
150 }
151 const end = Math.min(i + buffer, heightIndex.length);
152 const length = end - offset;
153 return { offset, length };
154};
155const getShouldUpdate = (dirtyIndex, currentRange, range) => {
156 const end = range.offset + range.length;
157 return (dirtyIndex <= end ||
158 currentRange.offset !== range.offset ||
159 currentRange.length !== range.length);
160};
161const findCellIndex = (cells, index) => {
162 const max = cells.length > 0 ? cells[cells.length - 1].index : 0;
163 if (index === 0) {
164 return 0;
165 }
166 else if (index === max + 1) {
167 return cells.length;
168 }
169 else {
170 return cells.findIndex(c => c.index === index);
171 }
172};
173const inplaceUpdate = (dst, src, offset) => {
174 if (offset === 0 && src.length >= dst.length) {
175 return src;
176 }
177 for (let i = 0; i < src.length; i++) {
178 dst[i + offset] = src[i];
179 }
180 return dst;
181};
182const calcCells = (items, itemHeight, headerHeight, footerHeight, headerFn, footerFn, approxHeaderHeight, approxFooterHeight, approxItemHeight, j, offset, len) => {
183 const cells = [];
184 const end = len + offset;
185 for (let i = offset; i < end; i++) {
186 const item = items[i];
187 if (headerFn) {
188 const value = headerFn(item, i, items);
189 if (value != null) {
190 cells.push({
191 i: j++,
192 type: CELL_TYPE_HEADER,
193 value,
194 index: i,
195 height: headerHeight ? headerHeight(value, i) : approxHeaderHeight,
196 reads: headerHeight ? 0 : MIN_READS,
197 visible: !!headerHeight,
198 });
199 }
200 }
201 cells.push({
202 i: j++,
203 type: CELL_TYPE_ITEM,
204 value: item,
205 index: i,
206 height: itemHeight ? itemHeight(item, i) : approxItemHeight,
207 reads: itemHeight ? 0 : MIN_READS,
208 visible: !!itemHeight,
209 });
210 if (footerFn) {
211 const value = footerFn(item, i, items);
212 if (value != null) {
213 cells.push({
214 i: j++,
215 type: CELL_TYPE_FOOTER,
216 value,
217 index: i,
218 height: footerHeight ? footerHeight(value, i) : approxFooterHeight,
219 reads: footerHeight ? 0 : MIN_READS,
220 visible: !!footerHeight,
221 });
222 }
223 }
224 }
225 return cells;
226};
227const calcHeightIndex = (buf, cells, index) => {
228 let acum = buf[index];
229 for (let i = index; i < buf.length; i++) {
230 buf[i] = acum;
231 acum += cells[i].height;
232 }
233 return acum;
234};
235const resizeBuffer = (buf, len) => {
236 if (!buf) {
237 return new Uint32Array(len);
238 }
239 if (buf.length === len) {
240 return buf;
241 }
242 else if (len > buf.length) {
243 const newBuf = new Uint32Array(len);
244 newBuf.set(buf);
245 return newBuf;
246 }
247 else {
248 return buf.subarray(0, len);
249 }
250};
251const positionForIndex = (index, cells, heightIndex) => {
252 const cell = cells.find(c => c.type === CELL_TYPE_ITEM && c.index === index);
253 if (cell) {
254 return heightIndex[cell.i];
255 }
256 return -1;
257};
258
259const virtualScrollCss = "ion-virtual-scroll{display:block;position:relative;width:100%;contain:strict;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}ion-virtual-scroll>.virtual-loading{opacity:0}ion-virtual-scroll>.virtual-item{position:absolute !important;top:0 !important;right:0 !important;left:0 !important;-webkit-transition-duration:0ms;transition-duration:0ms;will-change:transform}";
260
261const VirtualScroll = class {
262 constructor(hostRef) {
263 index.registerInstance(this, hostRef);
264 this.range = { offset: 0, length: 0 };
265 this.viewportHeight = 0;
266 this.cells = [];
267 this.virtualDom = [];
268 this.isEnabled = false;
269 this.viewportOffset = 0;
270 this.currentScrollTop = 0;
271 this.indexDirty = 0;
272 this.lastItemLen = 0;
273 this.totalHeight = 0;
274 /**
275 * It is important to provide this
276 * if virtual item height will be significantly larger than the default
277 * The approximate height of each virtual item template's cell.
278 * This dimension is used to help determine how many cells should
279 * be created when initialized, and to help calculate the height of
280 * the scrollable area. This height value can only use `px` units.
281 * Note that the actual rendered size of each cell comes from the
282 * app's CSS, whereas this approximation is used to help calculate
283 * initial dimensions before the item has been rendered.
284 */
285 this.approxItemHeight = 45;
286 /**
287 * The approximate height of each header template's cell.
288 * This dimension is used to help determine how many cells should
289 * be created when initialized, and to help calculate the height of
290 * the scrollable area. This height value can only use `px` units.
291 * Note that the actual rendered size of each cell comes from the
292 * app's CSS, whereas this approximation is used to help calculate
293 * initial dimensions before the item has been rendered.
294 */
295 this.approxHeaderHeight = 30;
296 /**
297 * The approximate width of each footer template's cell.
298 * This dimension is used to help determine how many cells should
299 * be created when initialized, and to help calculate the height of
300 * the scrollable area. This height value can only use `px` units.
301 * Note that the actual rendered size of each cell comes from the
302 * app's CSS, whereas this approximation is used to help calculate
303 * initial dimensions before the item has been rendered.
304 */
305 this.approxFooterHeight = 30;
306 this.onScroll = () => {
307 this.updateVirtualScroll();
308 };
309 }
310 itemsChanged() {
311 this.calcCells();
312 this.updateVirtualScroll();
313 }
314 componentWillLoad() {
315 console.warn(`[Deprecation Warning]: ion-virtual-scroll has been deprecated and will be removed in Ionic Framework v7.0. See https://ionicframework.com/docs/angular/virtual-scroll for migration steps.`);
316 }
317 async connectedCallback() {
318 const contentEl = this.el.closest('ion-content');
319 if (!contentEl) {
320 console.error('<ion-virtual-scroll> must be used inside an <ion-content>');
321 return;
322 }
323 this.scrollEl = await contentEl.getScrollElement();
324 this.contentEl = contentEl;
325 this.calcCells();
326 this.updateState();
327 }
328 componentDidUpdate() {
329 this.updateState();
330 }
331 disconnectedCallback() {
332 this.scrollEl = undefined;
333 }
334 onResize() {
335 this.calcCells();
336 this.updateVirtualScroll();
337 }
338 /**
339 * Returns the position of the virtual item at the given index.
340 */
341 positionForItem(index) {
342 return Promise.resolve(positionForIndex(index, this.cells, this.getHeightIndex()));
343 }
344 /**
345 * This method marks a subset of items as dirty, so they can be re-rendered. Items should be marked as
346 * dirty any time the content or their style changes.
347 *
348 * The subset of items to be updated can are specifing by an offset and a length.
349 */
350 async checkRange(offset, len = -1) {
351 // TODO: kind of hacky how we do in-place updated of the cells
352 // array. this part needs a complete refactor
353 if (!this.items) {
354 return;
355 }
356 const length = (len === -1)
357 ? this.items.length - offset
358 : len;
359 const cellIndex = findCellIndex(this.cells, offset);
360 const cells = calcCells(this.items, this.itemHeight, this.headerHeight, this.footerHeight, this.headerFn, this.footerFn, this.approxHeaderHeight, this.approxFooterHeight, this.approxItemHeight, cellIndex, offset, length);
361 this.cells = inplaceUpdate(this.cells, cells, cellIndex);
362 this.lastItemLen = this.items.length;
363 this.indexDirty = Math.max(offset - 1, 0);
364 this.scheduleUpdate();
365 }
366 /**
367 * This method marks the tail the items array as dirty, so they can be re-rendered.
368 *
369 * It's equivalent to calling:
370 *
371 * ```js
372 * virtualScroll.checkRange(lastItemLen);
373 * ```
374 */
375 async checkEnd() {
376 if (this.items) {
377 this.checkRange(this.lastItemLen);
378 }
379 }
380 updateVirtualScroll() {
381 // do nothing if virtual-scroll is disabled
382 if (!this.isEnabled || !this.scrollEl) {
383 return;
384 }
385 // unschedule future updates
386 if (this.timerUpdate) {
387 clearTimeout(this.timerUpdate);
388 this.timerUpdate = undefined;
389 }
390 // schedule DOM operations into the stencil queue
391 index.readTask(this.readVS.bind(this));
392 index.writeTask(this.writeVS.bind(this));
393 }
394 readVS() {
395 const { contentEl, scrollEl, el } = this;
396 let topOffset = 0;
397 let node = el;
398 while (node && node !== contentEl) {
399 topOffset += node.offsetTop;
400 node = node.offsetParent;
401 }
402 this.viewportOffset = topOffset;
403 if (scrollEl) {
404 this.viewportHeight = scrollEl.offsetHeight;
405 this.currentScrollTop = scrollEl.scrollTop;
406 }
407 }
408 writeVS() {
409 const dirtyIndex = this.indexDirty;
410 // get visible viewport
411 const scrollTop = this.currentScrollTop - this.viewportOffset;
412 const viewport = getViewport(scrollTop, this.viewportHeight, 100);
413 // compute lazily the height index
414 const heightIndex = this.getHeightIndex();
415 // get array bounds of visible cells base in the viewport
416 const range = getRange(heightIndex, viewport, 2);
417 // fast path, do nothing
418 const shouldUpdate = getShouldUpdate(dirtyIndex, this.range, range);
419 if (!shouldUpdate) {
420 return;
421 }
422 this.range = range;
423 // in place mutation of the virtual DOM
424 updateVDom(this.virtualDom, heightIndex, this.cells, range);
425 // Write DOM
426 // Different code paths taken depending of the render API used
427 if (this.nodeRender) {
428 doRender(this.el, this.nodeRender, this.virtualDom, this.updateCellHeight.bind(this));
429 }
430 else if (this.domRender) {
431 this.domRender(this.virtualDom);
432 }
433 else if (this.renderItem) {
434 index.forceUpdate(this);
435 }
436 }
437 updateCellHeight(cell, node) {
438 const update = () => {
439 if (node['$ionCell'] === cell) {
440 const style = window.getComputedStyle(node);
441 const height = node.offsetHeight + parseFloat(style.getPropertyValue('margin-bottom'));
442 this.setCellHeight(cell, height);
443 }
444 };
445 if (node) {
446 helpers.componentOnReady(node, update);
447 }
448 else {
449 update();
450 }
451 }
452 setCellHeight(cell, height) {
453 const index = cell.i;
454 // the cell might changed since the height update was scheduled
455 if (cell !== this.cells[index]) {
456 return;
457 }
458 if (cell.height !== height || cell.visible !== true) {
459 cell.visible = true;
460 cell.height = height;
461 this.indexDirty = Math.min(this.indexDirty, index);
462 this.scheduleUpdate();
463 }
464 }
465 scheduleUpdate() {
466 clearTimeout(this.timerUpdate);
467 this.timerUpdate = setTimeout(() => this.updateVirtualScroll(), 100);
468 }
469 updateState() {
470 const shouldEnable = !!(this.scrollEl &&
471 this.cells);
472 if (shouldEnable !== this.isEnabled) {
473 this.enableScrollEvents(shouldEnable);
474 if (shouldEnable) {
475 this.updateVirtualScroll();
476 }
477 }
478 }
479 calcCells() {
480 if (!this.items) {
481 return;
482 }
483 this.lastItemLen = this.items.length;
484 this.cells = calcCells(this.items, this.itemHeight, this.headerHeight, this.footerHeight, this.headerFn, this.footerFn, this.approxHeaderHeight, this.approxFooterHeight, this.approxItemHeight, 0, 0, this.lastItemLen);
485 this.indexDirty = 0;
486 }
487 getHeightIndex() {
488 if (this.indexDirty !== Infinity) {
489 this.calcHeightIndex(this.indexDirty);
490 }
491 return this.heightIndex;
492 }
493 calcHeightIndex(index = 0) {
494 // TODO: optimize, we don't need to calculate all the cells
495 this.heightIndex = resizeBuffer(this.heightIndex, this.cells.length);
496 this.totalHeight = calcHeightIndex(this.heightIndex, this.cells, index);
497 this.indexDirty = Infinity;
498 }
499 enableScrollEvents(shouldListen) {
500 if (this.rmEvent) {
501 this.rmEvent();
502 this.rmEvent = undefined;
503 }
504 const scrollEl = this.scrollEl;
505 if (scrollEl) {
506 this.isEnabled = shouldListen;
507 scrollEl.addEventListener('scroll', this.onScroll);
508 this.rmEvent = () => {
509 scrollEl.removeEventListener('scroll', this.onScroll);
510 };
511 }
512 }
513 renderVirtualNode(node) {
514 const { type, value, index } = node.cell;
515 switch (type) {
516 case CELL_TYPE_ITEM: return this.renderItem(value, index);
517 case CELL_TYPE_HEADER: return this.renderHeader(value, index);
518 case CELL_TYPE_FOOTER: return this.renderFooter(value, index);
519 }
520 }
521 render() {
522 return (index.h(index.Host, { style: {
523 height: `${this.totalHeight}px`
524 } }, this.renderItem && (index.h(VirtualProxy, { dom: this.virtualDom }, this.virtualDom.map(node => this.renderVirtualNode(node))))));
525 }
526 get el() { return index.getElement(this); }
527 static get watchers() { return {
528 "itemHeight": ["itemsChanged"],
529 "headerHeight": ["itemsChanged"],
530 "footerHeight": ["itemsChanged"],
531 "items": ["itemsChanged"]
532 }; }
533};
534const VirtualProxy = ({ dom }, children, utils) => {
535 return utils.map(children, (child, i) => {
536 const node = dom[i];
537 const vattrs = child.vattrs || {};
538 let classes = vattrs.class || '';
539 classes += 'virtual-item ';
540 if (!node.visible) {
541 classes += 'virtual-loading';
542 }
543 return Object.assign(Object.assign({}, child), { vattrs: Object.assign(Object.assign({}, vattrs), { class: classes, style: Object.assign(Object.assign({}, vattrs.style), { transform: `translate3d(0,${node.top}px,0)` }) }) });
544 });
545};
546VirtualScroll.style = virtualScrollCss;
547
548exports.ion_virtual_scroll = VirtualScroll;