@vaadin/virtual-list 22.0.0-alpha9 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,487 +0,0 @@
1
- import { timeOut, animationFrame } from '@vaadin/component-base/src/async.js';
2
- import { Debouncer, flush } from '@vaadin/component-base/src/debounce.js';
3
- import { ironList } from './iron-list.js';
4
-
5
- // iron-list can by default handle sizes up to around 100000.
6
- // When the size is larger than MAX_VIRTUAL_COUNT _vidxOffset is used
7
- const MAX_VIRTUAL_COUNT = 100000;
8
- const OFFSET_ADJUST_MIN_THRESHOLD = 1000;
9
-
10
- export class IronListAdapter {
11
- constructor({ createElements, updateElement, scrollTarget, scrollContainer, elementsContainer, reorderElements }) {
12
- this.isAttached = true;
13
- this._vidxOffset = 0;
14
- this.createElements = createElements;
15
- this.updateElement = updateElement;
16
- this.scrollTarget = scrollTarget;
17
- this.scrollContainer = scrollContainer;
18
- this.elementsContainer = elementsContainer || scrollContainer;
19
- this.reorderElements = reorderElements;
20
-
21
- this.__safari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
22
-
23
- this.timeouts = {
24
- SCROLL_REORDER: 500,
25
- IGNORE_WHEEL: 500
26
- };
27
-
28
- this.__resizeObserver = new ResizeObserver(() => this._resizeHandler());
29
-
30
- if (getComputedStyle(this.scrollTarget).overflow === 'visible') {
31
- this.scrollTarget.style.overflow = 'auto';
32
- }
33
-
34
- if (getComputedStyle(this.scrollContainer).position === 'static') {
35
- this.scrollContainer.style.position = 'relative';
36
- }
37
-
38
- this.__resizeObserver.observe(this.scrollTarget);
39
- this.scrollTarget.addEventListener('scroll', () => this._scrollHandler());
40
-
41
- this._scrollLineHeight = this._getScrollLineHeight();
42
- this.scrollTarget.addEventListener('wheel', (e) => this.__onWheel(e));
43
-
44
- if (this.reorderElements) {
45
- // Reordering the physical elements cancels the user's grab of the scroll bar handle on Safari.
46
- // Need to defer reordering until the user lets go of the scroll bar handle.
47
- this.scrollTarget.addEventListener('mousedown', () => (this.__mouseDown = true));
48
- this.scrollTarget.addEventListener('mouseup', () => {
49
- this.__mouseDown = false;
50
- if (this.__pendingReorder) {
51
- this.__reorderElements();
52
- }
53
- });
54
- }
55
- }
56
-
57
- _manageFocus() {}
58
-
59
- _removeFocusedItem() {}
60
-
61
- get scrollOffset() {
62
- return 0;
63
- }
64
-
65
- get adjustedFirstVisibleIndex() {
66
- return this.firstVisibleIndex + this._vidxOffset;
67
- }
68
-
69
- get adjustedLastVisibleIndex() {
70
- return this.lastVisibleIndex + this._vidxOffset;
71
- }
72
-
73
- scrollToIndex(index) {
74
- if (typeof index !== 'number' || isNaN(index) || this.size === 0 || !this.scrollTarget.offsetHeight) {
75
- return;
76
- }
77
- index = this._clamp(index, 0, this.size - 1);
78
-
79
- const visibleElementCount = this.__getVisibleElements().length;
80
- let targetVirtualIndex = Math.floor((index / this.size) * this._virtualCount);
81
- if (this._virtualCount - targetVirtualIndex < visibleElementCount) {
82
- targetVirtualIndex = this._virtualCount - (this.size - index);
83
- this._vidxOffset = this.size - this._virtualCount;
84
- } else if (targetVirtualIndex < visibleElementCount) {
85
- if (index < OFFSET_ADJUST_MIN_THRESHOLD) {
86
- targetVirtualIndex = index;
87
- this._vidxOffset = 0;
88
- } else {
89
- targetVirtualIndex = OFFSET_ADJUST_MIN_THRESHOLD;
90
- this._vidxOffset = index - targetVirtualIndex;
91
- }
92
- } else {
93
- this._vidxOffset = index - targetVirtualIndex;
94
- }
95
-
96
- this.__skipNextVirtualIndexAdjust = true;
97
- super.scrollToIndex(targetVirtualIndex);
98
- if (this.firstVisibleIndex !== index - this._vidxOffset && this._scrollTop < this._maxScrollTop && !this.grid) {
99
- // Second invocation to scrollToIndex may be needed to workaround an issue in iron-list
100
- super.scrollToIndex(targetVirtualIndex);
101
- }
102
- this._scrollHandler();
103
- }
104
-
105
- flush() {
106
- // The scroll target is hidden.
107
- if (this.scrollTarget.offsetHeight === 0) {
108
- return;
109
- }
110
-
111
- this._resizeHandler();
112
- flush();
113
- this._scrollHandler();
114
- this.__scrollReorderDebouncer && this.__scrollReorderDebouncer.flush();
115
- this.__debouncerWheelAnimationFrame && this.__debouncerWheelAnimationFrame.flush();
116
- }
117
-
118
- update(startIndex = 0, endIndex = this.size - 1) {
119
- this.__getVisibleElements().forEach((el) => {
120
- if (el.__virtualIndex >= startIndex && el.__virtualIndex <= endIndex) {
121
- this.__updateElement(el, el.__virtualIndex);
122
- }
123
- });
124
- }
125
-
126
- __updateElement(el, index) {
127
- // Clean up temporary min height
128
- if (el.style.minHeight) {
129
- el.style.minHeight = '';
130
- }
131
-
132
- this.updateElement(el, index);
133
-
134
- if (el.offsetHeight === 0) {
135
- // If the elements have 0 height after update (for example due to lazy rendering),
136
- // it results in iron-list requesting to create an unlimited count of elements.
137
- // Assign a temporary min height to elements that would otherwise end up having
138
- // no height.
139
- el.style.minHeight = '200px';
140
- }
141
- }
142
-
143
- __getIndexScrollOffset(index) {
144
- const element = this.__getVisibleElements().find((el) => el.__virtualIndex === index);
145
- return element ? this.scrollTarget.getBoundingClientRect().top - element.getBoundingClientRect().top : undefined;
146
- }
147
-
148
- set size(size) {
149
- if (size === this.size) {
150
- return;
151
- }
152
-
153
- // Record the scroll position before changing the size
154
- let fvi; // first visible index
155
- let fviOffsetBefore; // scroll offset of the first visible index
156
- if (size > 0) {
157
- fvi = this.firstVisibleIndex + this._vidxOffset;
158
- fviOffsetBefore = this.__getIndexScrollOffset(fvi);
159
- }
160
-
161
- // Change the size
162
- this.__size = size;
163
-
164
- // Flush before invoking items change to avoid
165
- // creating excess elements on the following flush()
166
- flush();
167
-
168
- this._itemsChanged({
169
- path: 'items'
170
- });
171
- flush();
172
-
173
- // Try to restore the scroll position if the new size is larger than 0
174
- if (size > 0) {
175
- fvi = Math.min(fvi, size - 1);
176
- this.scrollToIndex(fvi);
177
-
178
- const fviOffsetAfter = this.__getIndexScrollOffset(fvi);
179
- if (fviOffsetBefore !== undefined && fviOffsetAfter !== undefined) {
180
- this._scrollTop += fviOffsetBefore - fviOffsetAfter;
181
- }
182
- }
183
-
184
- if (!this.elementsContainer.children.length) {
185
- requestAnimationFrame(() => this._resizeHandler());
186
- }
187
- }
188
-
189
- get size() {
190
- return this.__size;
191
- }
192
-
193
- /** @private */
194
- get _scrollTop() {
195
- return this.scrollTarget.scrollTop;
196
- }
197
-
198
- /** @private */
199
- set _scrollTop(top) {
200
- this.scrollTarget.scrollTop = top;
201
- }
202
-
203
- /** @private */
204
- get items() {
205
- return {
206
- length: Math.min(this.size, MAX_VIRTUAL_COUNT)
207
- };
208
- }
209
-
210
- /** @private */
211
- get offsetHeight() {
212
- return this.scrollTarget.offsetHeight;
213
- }
214
-
215
- /** @private */
216
- get $() {
217
- return {
218
- items: this.scrollContainer
219
- };
220
- }
221
-
222
- /** @private */
223
- updateViewportBoundaries() {
224
- const styles = window.getComputedStyle(this.scrollTarget);
225
- this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(styles['padding-top'], 10);
226
- this._isRTL = Boolean(styles.direction === 'rtl');
227
- this._viewportWidth = this.elementsContainer.offsetWidth;
228
- this._viewportHeight = this.scrollTarget.offsetHeight;
229
- this._scrollPageHeight = this._viewportHeight - this._scrollLineHeight;
230
- this.grid && this._updateGridMetrics();
231
- }
232
-
233
- /** @private */
234
- setAttribute() {}
235
-
236
- /** @private */
237
- _createPool(size) {
238
- const physicalItems = this.createElements(size);
239
- const fragment = document.createDocumentFragment();
240
- physicalItems.forEach((el) => {
241
- el.style.position = 'absolute';
242
- fragment.appendChild(el);
243
- this.__resizeObserver.observe(el);
244
- });
245
- this.elementsContainer.appendChild(fragment);
246
- return physicalItems;
247
- }
248
-
249
- /** @private */
250
- _assignModels(itemSet) {
251
- this._iterateItems((pidx, vidx) => {
252
- const el = this._physicalItems[pidx];
253
- el.hidden = vidx >= this.size;
254
- if (!el.hidden) {
255
- el.__virtualIndex = vidx + (this._vidxOffset || 0);
256
- this.__updateElement(el, el.__virtualIndex);
257
- }
258
- }, itemSet);
259
- }
260
-
261
- /** @private */
262
- _isClientFull() {
263
- // Workaround an issue in iron-list that can cause it to freeze on fast scroll
264
- setTimeout(() => (this.__clientFull = true));
265
- return this.__clientFull || super._isClientFull();
266
- }
267
-
268
- /** @private */
269
- translate3d(_x, y, _z, el) {
270
- el.style.transform = `translateY(${y})`;
271
- }
272
-
273
- /** @private */
274
- toggleScrollListener() {}
275
-
276
- _scrollHandler() {
277
- this._adjustVirtualIndexOffset(this._scrollTop - (this.__previousScrollTop || 0));
278
-
279
- super._scrollHandler();
280
-
281
- if (this.reorderElements) {
282
- this.__scrollReorderDebouncer = Debouncer.debounce(
283
- this.__scrollReorderDebouncer,
284
- timeOut.after(this.timeouts.SCROLL_REORDER),
285
- () => this.__reorderElements()
286
- );
287
- }
288
-
289
- this.__previousScrollTop = this._scrollTop;
290
- }
291
-
292
- /** @private */
293
- __onWheel(e) {
294
- if (e.ctrlKey || this._hasScrolledAncestor(e.target, e.deltaX, e.deltaY)) {
295
- return;
296
- }
297
-
298
- let deltaY = e.deltaY;
299
- if (e.deltaMode === WheelEvent.DOM_DELTA_LINE) {
300
- // Scrolling by "lines of text" instead of pixels
301
- deltaY *= this._scrollLineHeight;
302
- } else if (e.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
303
- // Scrolling by "pages" instead of pixels
304
- deltaY *= this._scrollPageHeight;
305
- }
306
-
307
- this._deltaYAcc = this._deltaYAcc || 0;
308
-
309
- if (this._wheelAnimationFrame) {
310
- // Accumulate wheel delta while a frame is being processed
311
- this._deltaYAcc += deltaY;
312
- e.preventDefault();
313
- return;
314
- }
315
-
316
- deltaY += this._deltaYAcc;
317
- this._deltaYAcc = 0;
318
-
319
- this._wheelAnimationFrame = true;
320
- this.__debouncerWheelAnimationFrame = Debouncer.debounce(
321
- this.__debouncerWheelAnimationFrame,
322
- animationFrame,
323
- () => (this._wheelAnimationFrame = false)
324
- );
325
-
326
- const momentum = Math.abs(e.deltaX) + Math.abs(deltaY);
327
-
328
- if (this._canScroll(this.scrollTarget, e.deltaX, deltaY)) {
329
- e.preventDefault();
330
- this.scrollTarget.scrollTop += deltaY;
331
- this.scrollTarget.scrollLeft += e.deltaX;
332
-
333
- this._hasResidualMomentum = true;
334
-
335
- this._ignoreNewWheel = true;
336
- this._debouncerIgnoreNewWheel = Debouncer.debounce(
337
- this._debouncerIgnoreNewWheel,
338
- timeOut.after(this.timeouts.IGNORE_WHEEL),
339
- () => (this._ignoreNewWheel = false)
340
- );
341
- } else if ((this._hasResidualMomentum && momentum <= this._previousMomentum) || this._ignoreNewWheel) {
342
- e.preventDefault();
343
- } else if (momentum > this._previousMomentum) {
344
- this._hasResidualMomentum = false;
345
- }
346
- this._previousMomentum = momentum;
347
- }
348
-
349
- /**
350
- * Determines if the element has an ancestor that handles the scroll delta prior to this
351
- *
352
- * @private
353
- */
354
- _hasScrolledAncestor(el, deltaX, deltaY) {
355
- if (el === this.scrollTarget || el === this.scrollTarget.getRootNode().host) {
356
- return false;
357
- } else if (
358
- this._canScroll(el, deltaX, deltaY) &&
359
- ['auto', 'scroll'].indexOf(getComputedStyle(el).overflow) !== -1
360
- ) {
361
- return true;
362
- } else if (el !== this && el.parentElement) {
363
- return this._hasScrolledAncestor(el.parentElement, deltaX, deltaY);
364
- }
365
- }
366
-
367
- _canScroll(el, deltaX, deltaY) {
368
- return (
369
- (deltaY > 0 && el.scrollTop < el.scrollHeight - el.offsetHeight) ||
370
- (deltaY < 0 && el.scrollTop > 0) ||
371
- (deltaX > 0 && el.scrollLeft < el.scrollWidth - el.offsetWidth) ||
372
- (deltaX < 0 && el.scrollLeft > 0)
373
- );
374
- }
375
-
376
- /**
377
- * @returns {Number|undefined} - The browser's default font-size in pixels
378
- * @private
379
- */
380
- _getScrollLineHeight() {
381
- const el = document.createElement('div');
382
- el.style.fontSize = 'initial';
383
- el.style.display = 'none';
384
- document.body.appendChild(el);
385
- const fontSize = window.getComputedStyle(el).fontSize;
386
- document.body.removeChild(el);
387
- return fontSize ? window.parseInt(fontSize) : undefined;
388
- }
389
-
390
- __getVisibleElements() {
391
- return Array.from(this.elementsContainer.children).filter((element) => !element.hidden);
392
- }
393
-
394
- /** @private */
395
- __reorderElements() {
396
- if (this.__mouseDown) {
397
- this.__pendingReorder = true;
398
- return;
399
- }
400
- this.__pendingReorder = false;
401
-
402
- const adjustedVirtualStart = this._virtualStart + (this._vidxOffset || 0);
403
-
404
- // Which row to use as a target?
405
- const visibleElements = this.__getVisibleElements();
406
-
407
- const elementWithFocus = visibleElements.find(
408
- (element) =>
409
- element.contains(this.elementsContainer.getRootNode().activeElement) ||
410
- element.contains(this.scrollTarget.getRootNode().activeElement)
411
- );
412
- const targetElement = elementWithFocus || visibleElements[0];
413
- if (!targetElement) {
414
- // All elements are hidden, don't reorder
415
- return;
416
- }
417
-
418
- // Where the target row should be?
419
- const targetPhysicalIndex = targetElement.__virtualIndex - adjustedVirtualStart;
420
-
421
- // Reodrer the DOM elements to keep the target row at the target physical index
422
- const delta = visibleElements.indexOf(targetElement) - targetPhysicalIndex;
423
- if (delta > 0) {
424
- for (let i = 0; i < delta; i++) {
425
- this.elementsContainer.appendChild(visibleElements[i]);
426
- }
427
- } else if (delta < 0) {
428
- for (let i = visibleElements.length + delta; i < visibleElements.length; i++) {
429
- this.elementsContainer.insertBefore(visibleElements[i], visibleElements[0]);
430
- }
431
- }
432
-
433
- // Due to a rendering bug, reordering the rows can make parts of the scroll target disappear
434
- // on Safari when using sticky positioning in case the scroll target is inside a flexbox.
435
- // This issue manifests with grid (the header can disappear if grid is used inside a flexbox)
436
- if (this.__safari) {
437
- const { transform } = this.scrollTarget.style;
438
- this.scrollTarget.style.transform = 'translateZ(0)';
439
- setTimeout(() => (this.scrollTarget.style.transform = transform));
440
- }
441
- }
442
-
443
- /** @private */
444
- _adjustVirtualIndexOffset(delta) {
445
- if (this._virtualCount >= this.size) {
446
- this._vidxOffset = 0;
447
- } else if (this.__skipNextVirtualIndexAdjust) {
448
- this.__skipNextVirtualIndexAdjust = false;
449
- return;
450
- } else if (Math.abs(delta) > 10000) {
451
- // Process a large scroll position change
452
- const scale = this._scrollTop / (this.scrollTarget.scrollHeight - this.scrollTarget.offsetHeight);
453
- const offset = scale * this.size;
454
- this._vidxOffset = Math.round(offset - scale * this._virtualCount);
455
- } else {
456
- // Make sure user can always swipe/wheel scroll to the start and end
457
- const oldOffset = this._vidxOffset;
458
- const threshold = OFFSET_ADJUST_MIN_THRESHOLD;
459
- const maxShift = 100;
460
-
461
- // Near start
462
- if (this._scrollTop === 0) {
463
- this._vidxOffset = 0;
464
- if (oldOffset !== this._vidxOffset) {
465
- super.scrollToIndex(0);
466
- }
467
- } else if (this.firstVisibleIndex < threshold && this._vidxOffset > 0) {
468
- this._vidxOffset -= Math.min(this._vidxOffset, maxShift);
469
- super.scrollToIndex(this.firstVisibleIndex + (oldOffset - this._vidxOffset));
470
- }
471
-
472
- // Near end
473
- const maxOffset = this.size - this._virtualCount;
474
- if (this._scrollTop >= this._maxScrollTop && this._maxScrollTop > 0) {
475
- this._vidxOffset = maxOffset;
476
- if (oldOffset !== this._vidxOffset) {
477
- super.scrollToIndex(this._virtualCount - 1);
478
- }
479
- } else if (this.firstVisibleIndex > this._virtualCount - threshold && this._vidxOffset < maxOffset) {
480
- this._vidxOffset += Math.min(maxOffset - this._vidxOffset, maxShift);
481
- super.scrollToIndex(this.firstVisibleIndex - (this._vidxOffset - oldOffset));
482
- }
483
- }
484
- }
485
- }
486
-
487
- Object.setPrototypeOf(IronListAdapter.prototype, ironList);
@@ -1,83 +0,0 @@
1
- import { IronListAdapter } from './virtualizer-iron-list-adapter.js';
2
-
3
- export class Virtualizer {
4
- /**
5
- * @typedef {Object} VirtualizerConfig
6
- * @property {Function} createElements Function that returns the given number of new elements
7
- * @property {Function} updateElement Function that updates the element at a specific index
8
- * @property {HTMLElement} scrollTarget Reference to the scrolling element
9
- * @property {HTMLElement} scrollContainer Reference to a wrapper for the item elements (or a slot) inside the scrollTarget
10
- * @property {HTMLElement | undefined} elementsContainer Reference to the container in which the item elements are placed, defaults to scrollContainer
11
- * @property {boolean | undefined} reorderElements Determines whether the physical item elements should be kept in order in the DOM
12
- * @param {VirtualizerConfig} config Configuration for the virtualizer
13
- */
14
- constructor(config) {
15
- this.__adapter = new IronListAdapter(config);
16
- }
17
-
18
- /**
19
- * The size of the virtualizer
20
- * @param {number} size The size of the virtualizer
21
- */
22
- set size(size) {
23
- this.__adapter.size = size;
24
- }
25
-
26
- /**
27
- * The size of the virtualizer
28
- * @return {number | undefined} The size of the virtualizer
29
- */
30
- get size() {
31
- return this.__adapter.size;
32
- }
33
-
34
- /**
35
- * Scroll to a specific index in the virtual list
36
- *
37
- * @method scrollToIndex
38
- * @param {number} index The index of the item
39
- */
40
- scrollToIndex(index) {
41
- this.__adapter.scrollToIndex(index);
42
- }
43
-
44
- /**
45
- * Requests the virtualizer to re-render the item elements on an index range, if currently in the DOM
46
- *
47
- * @method update
48
- * @param {number | undefined} startIndex The start index of the range
49
- * @param {number | undefined} endIndex The end index of the range
50
- */
51
- update(startIndex = 0, endIndex = this.size - 1) {
52
- this.__adapter.update(startIndex, endIndex);
53
- }
54
-
55
- /**
56
- * Flushes active asynchronous tasks so that the component and the DOM end up in a stable state
57
- *
58
- * @method update
59
- * @param {number | undefined} startIndex The start index of the range
60
- * @param {number | undefined} endIndex The end index of the range
61
- */
62
- flush() {
63
- this.__adapter.flush();
64
- }
65
-
66
- /**
67
- * Gets the index of the first visible item in the viewport.
68
- *
69
- * @return {number}
70
- */
71
- get firstVisibleIndex() {
72
- return this.__adapter.adjustedFirstVisibleIndex;
73
- }
74
-
75
- /**
76
- * Gets the index of the last visible item in the viewport.
77
- *
78
- * @return {number}
79
- */
80
- get lastVisibleIndex() {
81
- return this.__adapter.adjustedLastVisibleIndex;
82
- }
83
- }