@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.
- package/README.md +7 -2
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +4 -3
- package/src/active-mixin.d.ts +23 -8
- package/src/browser-utils.js +35 -0
- package/src/controller-mixin.d.ts +28 -0
- package/src/controller-mixin.js +67 -0
- package/src/dir-mixin.d.ts +5 -12
- package/src/disabled-mixin.d.ts +5 -8
- package/src/element-mixin.d.ts +12 -12
- package/src/element-mixin.js +1 -41
- package/src/focus-mixin.d.ts +7 -10
- package/src/focus-mixin.js +11 -3
- package/src/iron-list-core.js +951 -0
- package/src/keyboard-mixin.d.ts +5 -10
- package/src/slot-mixin.d.ts +4 -9
- package/src/tabindex-mixin.d.ts +14 -10
- package/src/tabindex-mixin.js +1 -1
- package/src/virtualizer-iron-list-adapter.js +507 -0
- package/src/virtualizer.js +83 -0
package/src/keyboard-mixin.d.ts
CHANGED
|
@@ -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
|
|
13
|
+
export declare function KeyboardMixin<T extends Constructor<HTMLElement>>(base: T): T & Constructor<KeyboardMixinClass>;
|
|
13
14
|
|
|
14
|
-
|
|
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 };
|
package/src/slot-mixin.d.ts
CHANGED
|
@@ -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
|
|
11
|
+
export declare function SlotMixin<T extends Constructor<HTMLElement>>(base: T): T & Constructor<SlotMixinClass>;
|
|
11
12
|
|
|
12
|
-
|
|
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 };
|
package/src/tabindex-mixin.d.ts
CHANGED
|
@@ -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 {
|
|
6
|
+
import { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import { DisabledMixinClass } from './disabled-mixin.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* A mixin to
|
|
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
|
|
17
|
+
export declare function TabindexMixin<T extends Constructor<HTMLElement>>(
|
|
18
|
+
base: T
|
|
19
|
+
): T & Constructor<DisabledMixinClass> & Constructor<TabindexMixinClass>;
|
|
17
20
|
|
|
18
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/tabindex-mixin.js
CHANGED
|
@@ -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
|
+
}
|