@vaadin/component-base 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.
@@ -3,30 +3,25 @@
3
3
  * Copyright (c) 2021 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
+ import { Constructor } from '@open-wc/dedupe-mixin';
6
7
 
7
8
  /**
8
9
  * A mixin that manages keyboard handling.
9
10
  * The mixin subscribes to the keyboard events while an actual implementation
10
11
  * for the event handlers is left to the client (a component or another mixin).
11
12
  */
12
- declare function KeyboardMixin<T extends new (...args: any[]) => {}>(base: T): T & KeyboardMixinConstructor;
13
+ export declare function KeyboardMixin<T extends Constructor<HTMLElement>>(base: T): T & Constructor<KeyboardMixinClass>;
13
14
 
14
- interface KeyboardMixinConstructor {
15
- new (...args: any[]): KeyboardMixin;
16
- }
17
-
18
- interface KeyboardMixin {
15
+ export declare class KeyboardMixinClass {
19
16
  /**
20
17
  * A handler for the `keydown` event. By default, it does nothing.
21
18
  * Override the method to implement your own behavior.
22
19
  */
23
- _onKeyDown(event: KeyboardEvent): void;
20
+ protected _onKeyDown(event: KeyboardEvent): void;
24
21
 
25
22
  /**
26
23
  * A handler for the `keyup` event. By default, it does nothing.
27
24
  * Override the method to implement your own behavior.
28
25
  */
29
- _onKeyUp(event: KeyboardEvent): void;
26
+ protected _onKeyUp(event: KeyboardEvent): void;
30
27
  }
31
-
32
- export { KeyboardMixinConstructor, KeyboardMixin };
@@ -3,21 +3,16 @@
3
3
  * Copyright (c) 2021 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
+ import { Constructor } from '@open-wc/dedupe-mixin';
6
7
 
7
8
  /**
8
9
  * A mixin to provide content for named slots defined by component.
9
10
  */
10
- declare function SlotMixin<T extends new (...args: any[]) => {}>(base: T): T & SlotMixinConstructor;
11
+ export declare function SlotMixin<T extends Constructor<HTMLElement>>(base: T): T & Constructor<SlotMixinClass>;
11
12
 
12
- interface SlotMixinConstructor {
13
- new (...args: any[]): SlotMixin;
14
- }
15
-
16
- interface SlotMixin {
13
+ export declare class SlotMixinClass {
17
14
  /**
18
15
  * List of named slots to initialize.
19
16
  */
20
- readonly slots: Record<string, () => HTMLElement>;
17
+ protected readonly slots: Record<string, () => HTMLElement>;
21
18
  }
22
-
23
- export { SlotMixinConstructor, SlotMixin };
@@ -3,27 +3,31 @@
3
3
  * Copyright (c) 2021 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { DisabledMixin } from './disabled-mixin.js';
6
+ import { Constructor } from '@open-wc/dedupe-mixin';
7
+ import { DisabledMixinClass } from './disabled-mixin.js';
7
8
 
8
9
  /**
9
- * A mixin to provide the `tabindex` attribute.
10
+ * A mixin to toggle the `tabindex` attribute.
10
11
  *
11
12
  * By default, the attribute is set to 0 that makes the element focusable.
12
13
  *
13
14
  * The attribute is set to -1 whenever the user disables the element
14
15
  * and restored with the last known value once the element is enabled.
15
16
  */
16
- declare function TabindexMixin<T extends new (...args: any[]) => {}>(base: T): T & TabindexMixinConstructor;
17
+ export declare function TabindexMixin<T extends Constructor<HTMLElement>>(
18
+ base: T
19
+ ): T & Constructor<DisabledMixinClass> & Constructor<TabindexMixinClass>;
17
20
 
18
- interface TabindexMixinConstructor {
19
- new (...args: any[]): TabindexMixin;
20
- }
21
-
22
- interface TabindexMixin extends DisabledMixin {
21
+ export declare class TabindexMixinClass {
23
22
  /**
24
23
  * Indicates whether the element can be focused and where it participates in sequential keyboard navigation.
25
24
  */
26
25
  tabindex: number | undefined | null;
27
- }
28
26
 
29
- export { TabindexMixinConstructor, TabindexMixin };
27
+ /**
28
+ * When the user has changed tabindex while the element is disabled,
29
+ * the observer reverts tabindex to -1 and rather saves the new tabindex value to apply it later.
30
+ * The new value will be applied as soon as the element becomes enabled.
31
+ */
32
+ protected _tabindexChanged(tabindex: number | undefined | null): void;
33
+ }
@@ -6,7 +6,7 @@
6
6
  import { DisabledMixin } from './disabled-mixin.js';
7
7
 
8
8
  /**
9
- * A mixin to provide the `tabindex` attribute.
9
+ * A mixin to toggle the `tabindex` attribute.
10
10
  *
11
11
  * By default, the attribute is set to 0 that makes the element focusable.
12
12
  *
@@ -0,0 +1,507 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { animationFrame, timeOut } from './async.js';
7
+ import { isSafari } from './browser-utils.js';
8
+ import { Debouncer, flush } from './debounce.js';
9
+ import { ironList } from './iron-list-core.js';
10
+
11
+ // iron-list can by default handle sizes up to around 100000.
12
+ // When the size is larger than MAX_VIRTUAL_COUNT _vidxOffset is used
13
+ const MAX_VIRTUAL_COUNT = 100000;
14
+ const OFFSET_ADJUST_MIN_THRESHOLD = 1000;
15
+
16
+ export class IronListAdapter {
17
+ constructor({ createElements, updateElement, scrollTarget, scrollContainer, elementsContainer, reorderElements }) {
18
+ this.isAttached = true;
19
+ this._vidxOffset = 0;
20
+ this.createElements = createElements;
21
+ this.updateElement = updateElement;
22
+ this.scrollTarget = scrollTarget;
23
+ this.scrollContainer = scrollContainer;
24
+ this.elementsContainer = elementsContainer || scrollContainer;
25
+ this.reorderElements = reorderElements;
26
+ // Iron-list uses this value to determine how many pages of elements to render
27
+ this._maxPages = 1.3;
28
+
29
+ this.timeouts = {
30
+ SCROLL_REORDER: 500,
31
+ IGNORE_WHEEL: 500
32
+ };
33
+
34
+ this.__resizeObserver = new ResizeObserver(() => this._resizeHandler());
35
+
36
+ if (getComputedStyle(this.scrollTarget).overflow === 'visible') {
37
+ this.scrollTarget.style.overflow = 'auto';
38
+ }
39
+
40
+ if (getComputedStyle(this.scrollContainer).position === 'static') {
41
+ this.scrollContainer.style.position = 'relative';
42
+ }
43
+
44
+ this.__resizeObserver.observe(this.scrollTarget);
45
+ this.scrollTarget.addEventListener('scroll', () => this._scrollHandler());
46
+
47
+ this._scrollLineHeight = this._getScrollLineHeight();
48
+ this.scrollTarget.addEventListener('wheel', (e) => this.__onWheel(e));
49
+
50
+ if (this.reorderElements) {
51
+ // Reordering the physical elements cancels the user's grab of the scroll bar handle on Safari.
52
+ // Need to defer reordering until the user lets go of the scroll bar handle.
53
+ this.scrollTarget.addEventListener('mousedown', () => (this.__mouseDown = true));
54
+ this.scrollTarget.addEventListener('mouseup', () => {
55
+ this.__mouseDown = false;
56
+ if (this.__pendingReorder) {
57
+ this.__reorderElements();
58
+ }
59
+ });
60
+ }
61
+ }
62
+
63
+ _manageFocus() {}
64
+
65
+ _removeFocusedItem() {}
66
+
67
+ get scrollOffset() {
68
+ return 0;
69
+ }
70
+
71
+ get adjustedFirstVisibleIndex() {
72
+ return this.firstVisibleIndex + this._vidxOffset;
73
+ }
74
+
75
+ get adjustedLastVisibleIndex() {
76
+ return this.lastVisibleIndex + this._vidxOffset;
77
+ }
78
+
79
+ scrollToIndex(index) {
80
+ if (typeof index !== 'number' || isNaN(index) || this.size === 0 || !this.scrollTarget.offsetHeight) {
81
+ return;
82
+ }
83
+ index = this._clamp(index, 0, this.size - 1);
84
+
85
+ const visibleElementCount = this.__getVisibleElements().length;
86
+ let targetVirtualIndex = Math.floor((index / this.size) * this._virtualCount);
87
+ if (this._virtualCount - targetVirtualIndex < visibleElementCount) {
88
+ targetVirtualIndex = this._virtualCount - (this.size - index);
89
+ this._vidxOffset = this.size - this._virtualCount;
90
+ } else if (targetVirtualIndex < visibleElementCount) {
91
+ if (index < OFFSET_ADJUST_MIN_THRESHOLD) {
92
+ targetVirtualIndex = index;
93
+ this._vidxOffset = 0;
94
+ } else {
95
+ targetVirtualIndex = OFFSET_ADJUST_MIN_THRESHOLD;
96
+ this._vidxOffset = index - targetVirtualIndex;
97
+ }
98
+ } else {
99
+ this._vidxOffset = index - targetVirtualIndex;
100
+ }
101
+
102
+ this.__skipNextVirtualIndexAdjust = true;
103
+ super.scrollToIndex(targetVirtualIndex);
104
+
105
+ if (this.adjustedFirstVisibleIndex !== index && this._scrollTop < this._maxScrollTop && !this.grid) {
106
+ // Workaround an iron-list issue by manually adjusting the scroll position
107
+ this._scrollTop -= this.__getIndexScrollOffset(index) || 0;
108
+ }
109
+ this._scrollHandler();
110
+ }
111
+
112
+ flush() {
113
+ // The scroll target is hidden.
114
+ if (this.scrollTarget.offsetHeight === 0) {
115
+ return;
116
+ }
117
+
118
+ this._resizeHandler();
119
+ flush();
120
+ this._scrollHandler();
121
+ this.__scrollReorderDebouncer && this.__scrollReorderDebouncer.flush();
122
+ this.__debouncerWheelAnimationFrame && this.__debouncerWheelAnimationFrame.flush();
123
+ }
124
+
125
+ update(startIndex = 0, endIndex = this.size - 1) {
126
+ this.__getVisibleElements().forEach((el) => {
127
+ if (el.__virtualIndex >= startIndex && el.__virtualIndex <= endIndex) {
128
+ this.__updateElement(el, el.__virtualIndex, true);
129
+ }
130
+ });
131
+ }
132
+
133
+ __updateElement(el, index, forceSameIndexUpdates) {
134
+ // Clean up temporary min height
135
+ if (el.style.minHeight) {
136
+ el.style.minHeight = '';
137
+ }
138
+
139
+ if (!this.__preventElementUpdates && (el.__lastUpdatedIndex !== index || forceSameIndexUpdates)) {
140
+ this.updateElement(el, index);
141
+ el.__lastUpdatedIndex = index;
142
+ }
143
+
144
+ if (el.offsetHeight === 0) {
145
+ // If the elements have 0 height after update (for example due to lazy rendering),
146
+ // it results in iron-list requesting to create an unlimited count of elements.
147
+ // Assign a temporary min height to elements that would otherwise end up having
148
+ // no height.
149
+ el.style.minHeight = '200px';
150
+ }
151
+ }
152
+
153
+ __getIndexScrollOffset(index) {
154
+ const element = this.__getVisibleElements().find((el) => el.__virtualIndex === index);
155
+ return element ? this.scrollTarget.getBoundingClientRect().top - element.getBoundingClientRect().top : undefined;
156
+ }
157
+
158
+ set size(size) {
159
+ if (size === this.size) {
160
+ return;
161
+ }
162
+
163
+ // Prevent element update while the scroll position is being restored
164
+ this.__preventElementUpdates = true;
165
+
166
+ // Record the scroll position before changing the size
167
+ let fvi; // first visible index
168
+ let fviOffsetBefore; // scroll offset of the first visible index
169
+ if (size > 0) {
170
+ fvi = this.adjustedFirstVisibleIndex;
171
+ fviOffsetBefore = this.__getIndexScrollOffset(fvi);
172
+ }
173
+
174
+ // Change the size
175
+ this.__size = size;
176
+
177
+ // Flush before invoking items change to avoid
178
+ // creating excess elements on the following flush()
179
+ flush();
180
+
181
+ this._itemsChanged({
182
+ path: 'items'
183
+ });
184
+ flush();
185
+
186
+ // Try to restore the scroll position if the new size is larger than 0
187
+ if (size > 0) {
188
+ fvi = Math.min(fvi, size - 1);
189
+ this.scrollToIndex(fvi);
190
+
191
+ const fviOffsetAfter = this.__getIndexScrollOffset(fvi);
192
+ if (fviOffsetBefore !== undefined && fviOffsetAfter !== undefined) {
193
+ this._scrollTop += fviOffsetBefore - fviOffsetAfter;
194
+ }
195
+ }
196
+
197
+ if (!this.elementsContainer.children.length) {
198
+ requestAnimationFrame(() => this._resizeHandler());
199
+ }
200
+
201
+ this.__preventElementUpdates = false;
202
+ // Schedule and flush a resize handler
203
+ this._resizeHandler();
204
+ flush();
205
+ }
206
+
207
+ get size() {
208
+ return this.__size;
209
+ }
210
+
211
+ /** @private */
212
+ get _scrollTop() {
213
+ return this.scrollTarget.scrollTop;
214
+ }
215
+
216
+ /** @private */
217
+ set _scrollTop(top) {
218
+ this.scrollTarget.scrollTop = top;
219
+ }
220
+
221
+ /** @private */
222
+ get items() {
223
+ return {
224
+ length: Math.min(this.size, MAX_VIRTUAL_COUNT)
225
+ };
226
+ }
227
+
228
+ /** @private */
229
+ get offsetHeight() {
230
+ return this.scrollTarget.offsetHeight;
231
+ }
232
+
233
+ /** @private */
234
+ get $() {
235
+ return {
236
+ items: this.scrollContainer
237
+ };
238
+ }
239
+
240
+ /** @private */
241
+ updateViewportBoundaries() {
242
+ const styles = window.getComputedStyle(this.scrollTarget);
243
+ this._scrollerPaddingTop = this.scrollTarget === this ? 0 : parseInt(styles['padding-top'], 10);
244
+ this._isRTL = Boolean(styles.direction === 'rtl');
245
+ this._viewportWidth = this.elementsContainer.offsetWidth;
246
+ this._viewportHeight = this.scrollTarget.offsetHeight;
247
+ this._scrollPageHeight = this._viewportHeight - this._scrollLineHeight;
248
+ this.grid && this._updateGridMetrics();
249
+ }
250
+
251
+ /** @private */
252
+ setAttribute() {}
253
+
254
+ /** @private */
255
+ _createPool(size) {
256
+ const physicalItems = this.createElements(size);
257
+ const fragment = document.createDocumentFragment();
258
+ physicalItems.forEach((el) => {
259
+ el.style.position = 'absolute';
260
+ fragment.appendChild(el);
261
+ this.__resizeObserver.observe(el);
262
+ });
263
+ this.elementsContainer.appendChild(fragment);
264
+ return physicalItems;
265
+ }
266
+
267
+ /** @private */
268
+ _assignModels(itemSet) {
269
+ this._iterateItems((pidx, vidx) => {
270
+ const el = this._physicalItems[pidx];
271
+ el.hidden = vidx >= this.size;
272
+ if (!el.hidden) {
273
+ el.__virtualIndex = vidx + (this._vidxOffset || 0);
274
+ this.__updateElement(el, el.__virtualIndex);
275
+ } else {
276
+ delete el.__lastUpdatedIndex;
277
+ }
278
+ }, itemSet);
279
+ }
280
+
281
+ /** @private */
282
+ _isClientFull() {
283
+ // Workaround an issue in iron-list that can cause it to freeze on fast scroll
284
+ setTimeout(() => (this.__clientFull = true));
285
+ return this.__clientFull || super._isClientFull();
286
+ }
287
+
288
+ /** @private */
289
+ translate3d(_x, y, _z, el) {
290
+ el.style.transform = `translateY(${y})`;
291
+ }
292
+
293
+ /** @private */
294
+ toggleScrollListener() {}
295
+
296
+ _scrollHandler() {
297
+ this._adjustVirtualIndexOffset(this._scrollTop - (this.__previousScrollTop || 0));
298
+
299
+ super._scrollHandler();
300
+
301
+ if (this.reorderElements) {
302
+ this.__scrollReorderDebouncer = Debouncer.debounce(
303
+ this.__scrollReorderDebouncer,
304
+ timeOut.after(this.timeouts.SCROLL_REORDER),
305
+ () => this.__reorderElements()
306
+ );
307
+ }
308
+
309
+ this.__previousScrollTop = this._scrollTop;
310
+ }
311
+
312
+ /** @private */
313
+ __onWheel(e) {
314
+ if (e.ctrlKey || this._hasScrolledAncestor(e.target, e.deltaX, e.deltaY)) {
315
+ return;
316
+ }
317
+
318
+ let deltaY = e.deltaY;
319
+ if (e.deltaMode === WheelEvent.DOM_DELTA_LINE) {
320
+ // Scrolling by "lines of text" instead of pixels
321
+ deltaY *= this._scrollLineHeight;
322
+ } else if (e.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
323
+ // Scrolling by "pages" instead of pixels
324
+ deltaY *= this._scrollPageHeight;
325
+ }
326
+
327
+ this._deltaYAcc = this._deltaYAcc || 0;
328
+
329
+ if (this._wheelAnimationFrame) {
330
+ // Accumulate wheel delta while a frame is being processed
331
+ this._deltaYAcc += deltaY;
332
+ e.preventDefault();
333
+ return;
334
+ }
335
+
336
+ deltaY += this._deltaYAcc;
337
+ this._deltaYAcc = 0;
338
+
339
+ this._wheelAnimationFrame = true;
340
+ this.__debouncerWheelAnimationFrame = Debouncer.debounce(
341
+ this.__debouncerWheelAnimationFrame,
342
+ animationFrame,
343
+ () => (this._wheelAnimationFrame = false)
344
+ );
345
+
346
+ const momentum = Math.abs(e.deltaX) + Math.abs(deltaY);
347
+
348
+ if (this._canScroll(this.scrollTarget, e.deltaX, deltaY)) {
349
+ e.preventDefault();
350
+ this.scrollTarget.scrollTop += deltaY;
351
+ this.scrollTarget.scrollLeft += e.deltaX;
352
+
353
+ this._hasResidualMomentum = true;
354
+
355
+ this._ignoreNewWheel = true;
356
+ this._debouncerIgnoreNewWheel = Debouncer.debounce(
357
+ this._debouncerIgnoreNewWheel,
358
+ timeOut.after(this.timeouts.IGNORE_WHEEL),
359
+ () => (this._ignoreNewWheel = false)
360
+ );
361
+ } else if ((this._hasResidualMomentum && momentum <= this._previousMomentum) || this._ignoreNewWheel) {
362
+ e.preventDefault();
363
+ } else if (momentum > this._previousMomentum) {
364
+ this._hasResidualMomentum = false;
365
+ }
366
+ this._previousMomentum = momentum;
367
+ }
368
+
369
+ /**
370
+ * Determines if the element has an ancestor that handles the scroll delta prior to this
371
+ *
372
+ * @private
373
+ */
374
+ _hasScrolledAncestor(el, deltaX, deltaY) {
375
+ if (el === this.scrollTarget || el === this.scrollTarget.getRootNode().host) {
376
+ return false;
377
+ } else if (
378
+ this._canScroll(el, deltaX, deltaY) &&
379
+ ['auto', 'scroll'].indexOf(getComputedStyle(el).overflow) !== -1
380
+ ) {
381
+ return true;
382
+ } else if (el !== this && el.parentElement) {
383
+ return this._hasScrolledAncestor(el.parentElement, deltaX, deltaY);
384
+ }
385
+ }
386
+
387
+ _canScroll(el, deltaX, deltaY) {
388
+ return (
389
+ (deltaY > 0 && el.scrollTop < el.scrollHeight - el.offsetHeight) ||
390
+ (deltaY < 0 && el.scrollTop > 0) ||
391
+ (deltaX > 0 && el.scrollLeft < el.scrollWidth - el.offsetWidth) ||
392
+ (deltaX < 0 && el.scrollLeft > 0)
393
+ );
394
+ }
395
+
396
+ /**
397
+ * @returns {Number|undefined} - The browser's default font-size in pixels
398
+ * @private
399
+ */
400
+ _getScrollLineHeight() {
401
+ const el = document.createElement('div');
402
+ el.style.fontSize = 'initial';
403
+ el.style.display = 'none';
404
+ document.body.appendChild(el);
405
+ const fontSize = window.getComputedStyle(el).fontSize;
406
+ document.body.removeChild(el);
407
+ return fontSize ? window.parseInt(fontSize) : undefined;
408
+ }
409
+
410
+ __getVisibleElements() {
411
+ return Array.from(this.elementsContainer.children).filter((element) => !element.hidden);
412
+ }
413
+
414
+ /** @private */
415
+ __reorderElements() {
416
+ if (this.__mouseDown) {
417
+ this.__pendingReorder = true;
418
+ return;
419
+ }
420
+ this.__pendingReorder = false;
421
+
422
+ const adjustedVirtualStart = this._virtualStart + (this._vidxOffset || 0);
423
+
424
+ // Which row to use as a target?
425
+ const visibleElements = this.__getVisibleElements();
426
+
427
+ const elementWithFocus = visibleElements.find(
428
+ (element) =>
429
+ element.contains(this.elementsContainer.getRootNode().activeElement) ||
430
+ element.contains(this.scrollTarget.getRootNode().activeElement)
431
+ );
432
+ const targetElement = elementWithFocus || visibleElements[0];
433
+ if (!targetElement) {
434
+ // All elements are hidden, don't reorder
435
+ return;
436
+ }
437
+
438
+ // Where the target row should be?
439
+ const targetPhysicalIndex = targetElement.__virtualIndex - adjustedVirtualStart;
440
+
441
+ // Reodrer the DOM elements to keep the target row at the target physical index
442
+ const delta = visibleElements.indexOf(targetElement) - targetPhysicalIndex;
443
+ if (delta > 0) {
444
+ for (let i = 0; i < delta; i++) {
445
+ this.elementsContainer.appendChild(visibleElements[i]);
446
+ }
447
+ } else if (delta < 0) {
448
+ for (let i = visibleElements.length + delta; i < visibleElements.length; i++) {
449
+ this.elementsContainer.insertBefore(visibleElements[i], visibleElements[0]);
450
+ }
451
+ }
452
+
453
+ // Due to a rendering bug, reordering the rows can make parts of the scroll target disappear
454
+ // on Safari when using sticky positioning in case the scroll target is inside a flexbox.
455
+ // This issue manifests with grid (the header can disappear if grid is used inside a flexbox)
456
+ if (isSafari) {
457
+ const { transform } = this.scrollTarget.style;
458
+ this.scrollTarget.style.transform = 'translateZ(0)';
459
+ setTimeout(() => (this.scrollTarget.style.transform = transform));
460
+ }
461
+ }
462
+
463
+ /** @private */
464
+ _adjustVirtualIndexOffset(delta) {
465
+ if (this._virtualCount >= this.size) {
466
+ this._vidxOffset = 0;
467
+ } else if (this.__skipNextVirtualIndexAdjust) {
468
+ this.__skipNextVirtualIndexAdjust = false;
469
+ return;
470
+ } else if (Math.abs(delta) > 10000) {
471
+ // Process a large scroll position change
472
+ const scale = this._scrollTop / (this.scrollTarget.scrollHeight - this.scrollTarget.offsetHeight);
473
+ const offset = scale * this.size;
474
+ this._vidxOffset = Math.round(offset - scale * this._virtualCount);
475
+ } else {
476
+ // Make sure user can always swipe/wheel scroll to the start and end
477
+ const oldOffset = this._vidxOffset;
478
+ const threshold = OFFSET_ADJUST_MIN_THRESHOLD;
479
+ const maxShift = 100;
480
+
481
+ // Near start
482
+ if (this._scrollTop === 0) {
483
+ this._vidxOffset = 0;
484
+ if (oldOffset !== this._vidxOffset) {
485
+ super.scrollToIndex(0);
486
+ }
487
+ } else if (this.firstVisibleIndex < threshold && this._vidxOffset > 0) {
488
+ this._vidxOffset -= Math.min(this._vidxOffset, maxShift);
489
+ super.scrollToIndex(this.firstVisibleIndex + (oldOffset - this._vidxOffset));
490
+ }
491
+
492
+ // Near end
493
+ const maxOffset = this.size - this._virtualCount;
494
+ if (this._scrollTop >= this._maxScrollTop && this._maxScrollTop > 0) {
495
+ this._vidxOffset = maxOffset;
496
+ if (oldOffset !== this._vidxOffset) {
497
+ super.scrollToIndex(this._virtualCount - 1);
498
+ }
499
+ } else if (this.firstVisibleIndex > this._virtualCount - threshold && this._vidxOffset < maxOffset) {
500
+ this._vidxOffset += Math.min(maxOffset - this._vidxOffset, maxShift);
501
+ super.scrollToIndex(this.firstVisibleIndex - (this._vidxOffset - oldOffset));
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ Object.setPrototypeOf(IronListAdapter.prototype, ironList);
@@ -0,0 +1,83 @@
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
+ }