vlist 1.9.1 → 2.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.github.md +104 -97
- package/README.md +46 -33
- package/dist/constants.d.ts +11 -6
- package/dist/constants.js +83 -0
- package/dist/core/create.d.ts +10 -0
- package/dist/core/create.js +740 -0
- package/dist/core/dom.d.ts +8 -0
- package/dist/core/dom.js +47 -0
- package/dist/core/hooks.d.ts +16 -0
- package/dist/core/hooks.js +67 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +13 -0
- package/dist/core/pipeline.d.ts +51 -0
- package/dist/core/pipeline.js +307 -0
- package/dist/core/pool.d.ts +9 -0
- package/dist/core/pool.js +42 -0
- package/dist/core/scroll.d.ts +32 -0
- package/dist/core/scroll.js +137 -0
- package/dist/core/sizes.d.ts +8 -0
- package/dist/core/sizes.js +6 -0
- package/dist/core/state.d.ts +47 -0
- package/dist/core/state.js +56 -0
- package/dist/core/types.d.ts +187 -0
- package/dist/core/types.js +7 -0
- package/dist/{builder → core}/velocity.d.ts +1 -1
- package/dist/core/velocity.js +33 -0
- package/dist/events/emitter.js +60 -0
- package/dist/events/index.js +6 -0
- package/dist/index.d.ts +28 -19
- package/dist/index.js +28 -1
- package/dist/internals.d.ts +11 -7
- package/dist/internals.js +60 -1
- package/dist/plugins/a11y/index.d.ts +2 -0
- package/dist/plugins/a11y/index.js +1 -0
- package/dist/plugins/a11y/plugin.d.ts +13 -0
- package/dist/plugins/a11y/plugin.js +259 -0
- package/dist/{features → plugins}/async/index.d.ts +1 -1
- package/dist/plugins/async/index.js +12 -0
- package/dist/{features → plugins}/async/manager.d.ts +5 -1
- package/dist/plugins/async/manager.js +568 -0
- package/dist/plugins/async/placeholder.js +154 -0
- package/dist/plugins/async/plugin.d.ts +48 -0
- package/dist/plugins/async/plugin.js +311 -0
- package/dist/plugins/async/sparse.js +540 -0
- package/dist/plugins/autosize/index.d.ts +5 -0
- package/dist/plugins/autosize/index.js +4 -0
- package/dist/plugins/autosize/plugin.d.ts +19 -0
- package/dist/plugins/autosize/plugin.js +185 -0
- package/dist/plugins/grid/index.d.ts +7 -0
- package/dist/plugins/grid/index.js +5 -0
- package/dist/plugins/grid/layout.js +275 -0
- package/dist/plugins/grid/plugin.d.ts +23 -0
- package/dist/plugins/grid/plugin.js +347 -0
- package/dist/plugins/grid/renderer.js +525 -0
- package/dist/plugins/grid/types.js +11 -0
- package/dist/plugins/groups/async-bridge.js +246 -0
- package/dist/{features → plugins}/groups/index.d.ts +1 -1
- package/dist/plugins/groups/index.js +13 -0
- package/dist/plugins/groups/layout.js +294 -0
- package/dist/plugins/groups/plugin.d.ts +22 -0
- package/dist/plugins/groups/plugin.js +571 -0
- package/dist/plugins/groups/sticky.js +255 -0
- package/dist/plugins/groups/types.js +12 -0
- package/dist/plugins/masonry/index.d.ts +8 -0
- package/dist/plugins/masonry/index.js +6 -0
- package/dist/plugins/masonry/layout.js +261 -0
- package/dist/plugins/masonry/plugin.d.ts +32 -0
- package/dist/plugins/masonry/plugin.js +381 -0
- package/dist/plugins/masonry/renderer.js +354 -0
- package/dist/plugins/masonry/types.js +9 -0
- package/dist/plugins/page/index.d.ts +5 -0
- package/dist/plugins/page/index.js +5 -0
- package/dist/plugins/page/plugin.d.ts +21 -0
- package/dist/plugins/page/plugin.js +166 -0
- package/dist/plugins/scale/index.d.ts +5 -0
- package/dist/plugins/scale/index.js +4 -0
- package/dist/plugins/scale/plugin.d.ts +24 -0
- package/dist/plugins/scale/plugin.js +507 -0
- package/dist/plugins/scrollbar/controller.js +574 -0
- package/dist/plugins/scrollbar/index.d.ts +7 -0
- package/dist/plugins/scrollbar/index.js +6 -0
- package/dist/plugins/scrollbar/plugin.d.ts +20 -0
- package/dist/plugins/scrollbar/plugin.js +93 -0
- package/dist/plugins/scrollbar/scrollbar.js +556 -0
- package/dist/plugins/selection/index.d.ts +6 -0
- package/dist/plugins/selection/index.js +7 -0
- package/dist/plugins/selection/plugin.d.ts +16 -0
- package/dist/plugins/selection/plugin.js +601 -0
- package/dist/{features → plugins}/selection/state.d.ts +8 -0
- package/dist/plugins/selection/state.js +332 -0
- package/dist/plugins/snapshots/index.d.ts +5 -0
- package/dist/plugins/snapshots/index.js +5 -0
- package/dist/plugins/snapshots/plugin.d.ts +17 -0
- package/dist/plugins/snapshots/plugin.js +301 -0
- package/dist/plugins/sortable/index.d.ts +6 -0
- package/dist/plugins/sortable/index.js +6 -0
- package/dist/plugins/sortable/plugin.d.ts +34 -0
- package/dist/plugins/sortable/plugin.js +753 -0
- package/dist/plugins/table/header.js +501 -0
- package/dist/{features → plugins}/table/index.d.ts +1 -1
- package/dist/plugins/table/index.js +12 -0
- package/dist/plugins/table/layout.js +211 -0
- package/dist/plugins/table/plugin.d.ts +20 -0
- package/dist/plugins/table/plugin.js +391 -0
- package/dist/plugins/table/renderer.js +625 -0
- package/dist/plugins/table/types.js +12 -0
- package/dist/plugins/transition/index.d.ts +5 -0
- package/dist/plugins/transition/index.js +5 -0
- package/dist/plugins/transition/plugin.d.ts +22 -0
- package/dist/plugins/transition/plugin.js +405 -0
- package/dist/rendering/aria.js +23 -0
- package/dist/rendering/index.js +18 -0
- package/dist/rendering/measured.js +98 -0
- package/dist/rendering/renderer.js +586 -0
- package/dist/rendering/scale.js +267 -0
- package/dist/rendering/scroll.js +71 -0
- package/dist/rendering/sizes.js +193 -0
- package/dist/rendering/sort.js +65 -0
- package/dist/rendering/viewport.js +268 -0
- package/dist/size.json +1 -1
- package/dist/types.js +5 -0
- package/dist/utils/padding.d.ts +2 -4
- package/dist/utils/padding.js +49 -0
- package/dist/utils/stats.js +124 -0
- package/dist/vlist-grid.css +1 -1
- package/dist/vlist-masonry.css +1 -1
- package/dist/vlist-table.css +1 -1
- package/dist/vlist.css +1 -1
- package/package.json +9 -4
- package/dist/builder/a11y.d.ts +0 -16
- package/dist/builder/api.d.ts +0 -21
- package/dist/builder/context.d.ts +0 -36
- package/dist/builder/core.d.ts +0 -16
- package/dist/builder/data.d.ts +0 -71
- package/dist/builder/dom.d.ts +0 -15
- package/dist/builder/index.d.ts +0 -25
- package/dist/builder/materialize.d.ts +0 -166
- package/dist/builder/pool.d.ts +0 -10
- package/dist/builder/range.d.ts +0 -10
- package/dist/builder/scroll.d.ts +0 -24
- package/dist/builder/types.d.ts +0 -512
- package/dist/features/async/feature.d.ts +0 -72
- package/dist/features/autosize/feature.d.ts +0 -34
- package/dist/features/autosize/index.d.ts +0 -2
- package/dist/features/grid/feature.d.ts +0 -48
- package/dist/features/grid/index.d.ts +0 -9
- package/dist/features/groups/feature.d.ts +0 -75
- package/dist/features/masonry/feature.d.ts +0 -45
- package/dist/features/masonry/index.d.ts +0 -9
- package/dist/features/page/feature.d.ts +0 -109
- package/dist/features/page/index.d.ts +0 -9
- package/dist/features/scale/feature.d.ts +0 -42
- package/dist/features/scale/index.d.ts +0 -10
- package/dist/features/scrollbar/feature.d.ts +0 -81
- package/dist/features/scrollbar/index.d.ts +0 -8
- package/dist/features/selection/feature.d.ts +0 -91
- package/dist/features/selection/index.d.ts +0 -7
- package/dist/features/snapshots/feature.d.ts +0 -79
- package/dist/features/snapshots/index.d.ts +0 -9
- package/dist/features/sortable/feature.d.ts +0 -101
- package/dist/features/sortable/index.d.ts +0 -6
- package/dist/features/table/feature.d.ts +0 -67
- package/dist/features/transition/feature.d.ts +0 -30
- package/dist/features/transition/index.d.ts +0 -9
- /package/dist/{features → plugins}/async/placeholder.d.ts +0 -0
- /package/dist/{features → plugins}/async/sparse.d.ts +0 -0
- /package/dist/{features → plugins}/grid/layout.d.ts +0 -0
- /package/dist/{features → plugins}/grid/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/grid/types.d.ts +0 -0
- /package/dist/{features → plugins}/groups/async-bridge.d.ts +0 -0
- /package/dist/{features → plugins}/groups/layout.d.ts +0 -0
- /package/dist/{features → plugins}/groups/sticky.d.ts +0 -0
- /package/dist/{features → plugins}/groups/types.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/layout.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/types.d.ts +0 -0
- /package/dist/{features → plugins}/scrollbar/controller.d.ts +0 -0
- /package/dist/{features → plugins}/scrollbar/scrollbar.d.ts +0 -0
- /package/dist/{features → plugins}/table/header.d.ts +0 -0
- /package/dist/{features → plugins}/table/layout.d.ts +0 -0
- /package/dist/{features → plugins}/table/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/table/types.d.ts +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist - Masonry Renderer
|
|
3
|
+
* Renders items in a masonry/Pinterest-style layout with absolute positioning.
|
|
4
|
+
*
|
|
5
|
+
* Unlike grid renderer (which uses row-based positioning), masonry renderer
|
|
6
|
+
* positions each item using its pre-calculated coordinates from the layout phase.
|
|
7
|
+
*
|
|
8
|
+
* Key differences from grid:
|
|
9
|
+
* - Items positioned using cached x/y coordinates (not row/col calculations)
|
|
10
|
+
* - Each item can have different height/width
|
|
11
|
+
* - No row alignment - items flow into shortest column/row
|
|
12
|
+
* - Visibility determined by checking each item's absolute position
|
|
13
|
+
*
|
|
14
|
+
* Performance:
|
|
15
|
+
* - Element pooling avoids createElement cost
|
|
16
|
+
* - Template re-evaluation skipped when item data + state unchanged
|
|
17
|
+
* - O(1) Set-based visibility diffing (not O(n) .some())
|
|
18
|
+
* - Release grace period prevents boundary thrashing (hover blink, transition replay)
|
|
19
|
+
* - Released elements removed from DOM immediately
|
|
20
|
+
*/
|
|
21
|
+
import { sortRenderedDOM } from "../../rendering/sort";
|
|
22
|
+
const createElementPool = (maxSize = 200) => {
|
|
23
|
+
const pool = [];
|
|
24
|
+
const acquire = () => {
|
|
25
|
+
const element = pool.pop();
|
|
26
|
+
if (element) {
|
|
27
|
+
return element;
|
|
28
|
+
}
|
|
29
|
+
const newElement = document.createElement("div");
|
|
30
|
+
newElement.setAttribute("role", "option");
|
|
31
|
+
return newElement;
|
|
32
|
+
};
|
|
33
|
+
const release = (element) => {
|
|
34
|
+
// Remove from DOM immediately — prevents blank divs in the container
|
|
35
|
+
element.remove();
|
|
36
|
+
if (pool.length < maxSize) {
|
|
37
|
+
element.className = "";
|
|
38
|
+
element.textContent = "";
|
|
39
|
+
element.removeAttribute("style");
|
|
40
|
+
element.removeAttribute("data-index");
|
|
41
|
+
element.removeAttribute("data-id");
|
|
42
|
+
element.removeAttribute("data-lane");
|
|
43
|
+
pool.push(element);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const clear = () => {
|
|
47
|
+
pool.length = 0;
|
|
48
|
+
};
|
|
49
|
+
return { acquire, release, clear };
|
|
50
|
+
};
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Release grace period
|
|
53
|
+
// =============================================================================
|
|
54
|
+
/**
|
|
55
|
+
* Number of render cycles to keep an item alive after it leaves the visible set.
|
|
56
|
+
* Prevents boundary thrashing: items near the overscan edge aren't recycled
|
|
57
|
+
* on small scroll deltas, preserving DOM element hover state and avoiding
|
|
58
|
+
* CSS transition replays.
|
|
59
|
+
*/
|
|
60
|
+
const RELEASE_GRACE = 1;
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// Masonry Renderer Factory
|
|
63
|
+
// =============================================================================
|
|
64
|
+
/**
|
|
65
|
+
* Create a masonry renderer for managing DOM elements with absolute positioning.
|
|
66
|
+
*
|
|
67
|
+
* @param itemsContainer - The DOM element that holds rendered items
|
|
68
|
+
* @param template - Item template function
|
|
69
|
+
* @param classPrefix - CSS class prefix
|
|
70
|
+
* @param isHorizontal - Whether layout is horizontal (scrolls right)
|
|
71
|
+
* @param totalItemsGetter - Optional getter for total item count (for aria-setsize)
|
|
72
|
+
* @param ariaIdPrefix - Optional unique prefix for element IDs (for aria-activedescendant)
|
|
73
|
+
*/
|
|
74
|
+
export const createMasonryRenderer = (itemsContainer, template, classPrefix, isHorizontal = false, totalItemsGetter, ariaIdPrefix, ariaPosInSetGetter, interactive) => {
|
|
75
|
+
const pool = createElementPool();
|
|
76
|
+
const rendered = new Map();
|
|
77
|
+
// ── Reusable visibleSet — cleared and repopulated each frame (no allocation) ──
|
|
78
|
+
const visibleSet = new Set();
|
|
79
|
+
// ── Frame counter for release grace period ──
|
|
80
|
+
let frameCounter = 0;
|
|
81
|
+
// Track aria-setsize to avoid redundant updates
|
|
82
|
+
let lastAriaSetSize = "";
|
|
83
|
+
let lastAriaTotal = -1;
|
|
84
|
+
// Reusable item state to avoid allocation per render
|
|
85
|
+
const reusableItemState = { selected: false, focused: false };
|
|
86
|
+
const getItemState = (isSelected, isFocused) => {
|
|
87
|
+
reusableItemState.selected = isSelected;
|
|
88
|
+
reusableItemState.focused = isFocused;
|
|
89
|
+
return reusableItemState;
|
|
90
|
+
};
|
|
91
|
+
// Pre-computed class names
|
|
92
|
+
const baseClass = `${classPrefix}-item ${classPrefix}-masonry-item`;
|
|
93
|
+
const groupHeaderClass = `${classPrefix}-group-header`;
|
|
94
|
+
const selectedClass = `${classPrefix}-item--selected`;
|
|
95
|
+
const focusedClass = `${classPrefix}-item--focused`;
|
|
96
|
+
/**
|
|
97
|
+
* Apply template result to element
|
|
98
|
+
*/
|
|
99
|
+
const applyTemplate = (element, result) => {
|
|
100
|
+
if (typeof result === "string") {
|
|
101
|
+
element.innerHTML = result;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
element.replaceChildren(result);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Apply state-dependent classes
|
|
109
|
+
*/
|
|
110
|
+
const applyClasses = (element, isSelected, isFocused) => {
|
|
111
|
+
element.classList.toggle(selectedClass, isSelected);
|
|
112
|
+
element.classList.toggle(focusedClass, isFocused);
|
|
113
|
+
};
|
|
114
|
+
const positionElement = (element, placement) => {
|
|
115
|
+
if (isHorizontal) {
|
|
116
|
+
element.style.transform = `translate(${Math.round(placement.y)}px, ${Math.round(placement.x)}px)`;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
element.style.transform = `translate(${Math.round(placement.x)}px, ${Math.round(placement.y)}px)`;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Apply size styles to an element.
|
|
124
|
+
*/
|
|
125
|
+
const applySizeStyles = (element, placement) => {
|
|
126
|
+
if (isHorizontal) {
|
|
127
|
+
element.style.width = `${placement.size}px`;
|
|
128
|
+
element.style.height = `${placement.crossSize}px`;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
element.style.width = `${placement.crossSize}px`;
|
|
132
|
+
element.style.height = `${placement.size}px`;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Render a single masonry item (new element from pool)
|
|
137
|
+
*/
|
|
138
|
+
const renderItem = (itemIndex, item, placement, isSelected, isFocused) => {
|
|
139
|
+
const element = pool.acquire();
|
|
140
|
+
const isGH = item.__groupHeader;
|
|
141
|
+
const state = getItemState(isSelected, isFocused);
|
|
142
|
+
// Group headers get a distinct class and role
|
|
143
|
+
element.className = isGH ? groupHeaderClass : baseClass;
|
|
144
|
+
// Set data attributes
|
|
145
|
+
element.dataset.index = String(itemIndex);
|
|
146
|
+
element.dataset.id = String(item.id);
|
|
147
|
+
element.dataset.lane = String(placement.lane);
|
|
148
|
+
if (isGH) {
|
|
149
|
+
element.setAttribute("role", "presentation");
|
|
150
|
+
element.removeAttribute("aria-selected");
|
|
151
|
+
element.removeAttribute("aria-setsize");
|
|
152
|
+
element.removeAttribute("aria-posinset");
|
|
153
|
+
element.removeAttribute("id");
|
|
154
|
+
}
|
|
155
|
+
else if (interactive !== false) {
|
|
156
|
+
element.setAttribute("role", "option");
|
|
157
|
+
element.ariaSelected = String(isSelected);
|
|
158
|
+
if (ariaIdPrefix) {
|
|
159
|
+
element.id = `${ariaIdPrefix}-item-${itemIndex}`;
|
|
160
|
+
}
|
|
161
|
+
if (totalItemsGetter) {
|
|
162
|
+
const total = totalItemsGetter();
|
|
163
|
+
if (total !== lastAriaTotal) {
|
|
164
|
+
lastAriaTotal = total;
|
|
165
|
+
lastAriaSetSize = String(total);
|
|
166
|
+
}
|
|
167
|
+
element.setAttribute("aria-setsize", lastAriaSetSize);
|
|
168
|
+
const posInSet = ariaPosInSetGetter ? ariaPosInSetGetter(itemIndex) : itemIndex + 1;
|
|
169
|
+
element.setAttribute("aria-posinset", String(posInSet));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
element.setAttribute("role", "listitem");
|
|
174
|
+
element.removeAttribute("aria-selected");
|
|
175
|
+
if (totalItemsGetter) {
|
|
176
|
+
const total = totalItemsGetter();
|
|
177
|
+
if (total !== lastAriaTotal) {
|
|
178
|
+
lastAriaTotal = total;
|
|
179
|
+
lastAriaSetSize = String(total);
|
|
180
|
+
}
|
|
181
|
+
element.setAttribute("aria-setsize", lastAriaSetSize);
|
|
182
|
+
const posInSet = ariaPosInSetGetter ? ariaPosInSetGetter(itemIndex) : itemIndex + 1;
|
|
183
|
+
element.setAttribute("aria-posinset", String(posInSet));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Apply sizing
|
|
187
|
+
applySizeStyles(element, placement);
|
|
188
|
+
// Apply template
|
|
189
|
+
const result = template(item, itemIndex, state);
|
|
190
|
+
applyTemplate(element, result);
|
|
191
|
+
// Apply state classes and position
|
|
192
|
+
applyClasses(element, isSelected, isFocused);
|
|
193
|
+
positionElement(element, placement);
|
|
194
|
+
return {
|
|
195
|
+
element,
|
|
196
|
+
lastItemId: item.id,
|
|
197
|
+
lastSelected: isSelected,
|
|
198
|
+
lastFocused: isFocused,
|
|
199
|
+
lastY: placement.y,
|
|
200
|
+
lastX: placement.x,
|
|
201
|
+
lastSize: placement.size,
|
|
202
|
+
lastCrossSize: placement.crossSize,
|
|
203
|
+
lastSeenFrame: frameCounter,
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Render visible items using pre-calculated placements.
|
|
208
|
+
*
|
|
209
|
+
* Performance characteristics:
|
|
210
|
+
* - Uses Set for O(1) visibility check (not O(n) .some())
|
|
211
|
+
* - Skips template re-evaluation when item id + state unchanged
|
|
212
|
+
* - Only updates position when coordinates changed
|
|
213
|
+
* - Release grace period prevents boundary thrashing
|
|
214
|
+
* - Released elements removed from DOM immediately
|
|
215
|
+
*/
|
|
216
|
+
const render = (getItem, placements, selectedIds, focusedIndex) => {
|
|
217
|
+
frameCounter++;
|
|
218
|
+
// Repopulate reusable visibleSet — O(k) clear + O(k) add, no allocation
|
|
219
|
+
visibleSet.clear();
|
|
220
|
+
for (let i = 0; i < placements.length; i++) {
|
|
221
|
+
visibleSet.add(placements[i].index);
|
|
222
|
+
}
|
|
223
|
+
// Release items no longer visible, with grace period to prevent
|
|
224
|
+
// boundary thrashing (hover blink, CSS transition replay).
|
|
225
|
+
// Items that just left the visible set keep their DOM element for
|
|
226
|
+
// RELEASE_GRACE extra render cycles — if they re-enter, the same
|
|
227
|
+
// element is reused with :hover state intact.
|
|
228
|
+
for (const [index, tracked] of rendered) {
|
|
229
|
+
if (visibleSet.has(index)) {
|
|
230
|
+
tracked.lastSeenFrame = frameCounter;
|
|
231
|
+
}
|
|
232
|
+
else if (frameCounter - tracked.lastSeenFrame > RELEASE_GRACE) {
|
|
233
|
+
pool.release(tracked.element);
|
|
234
|
+
rendered.delete(index);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// DocumentFragment for batched DOM insertion of new elements
|
|
238
|
+
// (matches core renderer and grid renderer patterns)
|
|
239
|
+
let fragment = null;
|
|
240
|
+
// Render new or update existing items
|
|
241
|
+
for (let pi = 0; pi < placements.length; pi++) {
|
|
242
|
+
const placement = placements[pi];
|
|
243
|
+
const itemIndex = placement.index;
|
|
244
|
+
const item = getItem(itemIndex);
|
|
245
|
+
if (!item)
|
|
246
|
+
continue;
|
|
247
|
+
const isSelected = selectedIds.has(item.id);
|
|
248
|
+
const isFocused = itemIndex === focusedIndex;
|
|
249
|
+
const existing = rendered.get(itemIndex);
|
|
250
|
+
if (existing) {
|
|
251
|
+
// ── Fast path: skip work when nothing changed ──
|
|
252
|
+
const idChanged = existing.lastItemId !== item.id;
|
|
253
|
+
const selectedChanged = existing.lastSelected !== isSelected;
|
|
254
|
+
const focusedChanged = existing.lastFocused !== isFocused;
|
|
255
|
+
const posChanged = existing.lastY !== placement.y ||
|
|
256
|
+
existing.lastX !== placement.x;
|
|
257
|
+
const sizeChanged = existing.lastSize !== placement.size ||
|
|
258
|
+
existing.lastCrossSize !== placement.crossSize;
|
|
259
|
+
// Template re-evaluation only when item data actually changed
|
|
260
|
+
// (NOT on selection/focus change — that would destroy loaded images)
|
|
261
|
+
if (idChanged) {
|
|
262
|
+
const state = getItemState(isSelected, isFocused);
|
|
263
|
+
const result = template(item, itemIndex, state);
|
|
264
|
+
applyTemplate(existing.element, result);
|
|
265
|
+
// Update data attributes
|
|
266
|
+
existing.element.dataset.id = String(item.id);
|
|
267
|
+
existing.lastItemId = item.id;
|
|
268
|
+
// Refresh aria-posinset when element is reused for a different item
|
|
269
|
+
const isGH = item.__groupHeader;
|
|
270
|
+
if (!isGH) {
|
|
271
|
+
const posInSet = ariaPosInSetGetter ? ariaPosInSetGetter(itemIndex) : itemIndex + 1;
|
|
272
|
+
existing.element.setAttribute("aria-posinset", String(posInSet));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Class + aria updates only when selection/focus changed
|
|
276
|
+
if (idChanged || selectedChanged || focusedChanged) {
|
|
277
|
+
applyClasses(existing.element, isSelected, isFocused);
|
|
278
|
+
existing.element.ariaSelected = String(isSelected);
|
|
279
|
+
existing.lastSelected = isSelected;
|
|
280
|
+
existing.lastFocused = isFocused;
|
|
281
|
+
}
|
|
282
|
+
// Size update when cross-axis or main-axis size changed (e.g. container resize)
|
|
283
|
+
if (sizeChanged) {
|
|
284
|
+
applySizeStyles(existing.element, placement);
|
|
285
|
+
existing.lastSize = placement.size;
|
|
286
|
+
existing.lastCrossSize = placement.crossSize;
|
|
287
|
+
}
|
|
288
|
+
// Position update only when coordinates changed
|
|
289
|
+
if (posChanged) {
|
|
290
|
+
positionElement(existing.element, placement);
|
|
291
|
+
existing.lastY = placement.y;
|
|
292
|
+
existing.lastX = placement.x;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
// Render new item — collect in fragment for batched insertion
|
|
297
|
+
const tracked = renderItem(itemIndex, item, placement, isSelected, isFocused);
|
|
298
|
+
if (!fragment)
|
|
299
|
+
fragment = document.createDocumentFragment();
|
|
300
|
+
fragment.appendChild(tracked.element);
|
|
301
|
+
rendered.set(itemIndex, tracked);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Single DOM insertion for all new elements — minimizes reflows
|
|
305
|
+
if (fragment)
|
|
306
|
+
itemsContainer.appendChild(fragment);
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Clear all rendered items.
|
|
310
|
+
*/
|
|
311
|
+
const clear = () => {
|
|
312
|
+
for (const tracked of rendered.values()) {
|
|
313
|
+
pool.release(tracked.element);
|
|
314
|
+
}
|
|
315
|
+
rendered.clear();
|
|
316
|
+
itemsContainer.innerHTML = "";
|
|
317
|
+
};
|
|
318
|
+
/**
|
|
319
|
+
* Destroy renderer and cleanup.
|
|
320
|
+
*/
|
|
321
|
+
const destroy = () => {
|
|
322
|
+
clear();
|
|
323
|
+
pool.clear();
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* Reorder DOM children so they follow logical data-index order.
|
|
327
|
+
* Called on scroll idle for accessibility — screen readers traverse
|
|
328
|
+
* DOM order, not visual (transform) order. Since items are
|
|
329
|
+
* position:absolute, this has zero visual impact.
|
|
330
|
+
*/
|
|
331
|
+
const sortDOM = () => {
|
|
332
|
+
sortRenderedDOM(itemsContainer, rendered.keys(), (key) => rendered.get(key)?.element);
|
|
333
|
+
};
|
|
334
|
+
const updateItemClasses = (index, isSelected, isFocused) => {
|
|
335
|
+
const existing = rendered.get(index);
|
|
336
|
+
if (!existing)
|
|
337
|
+
return;
|
|
338
|
+
const selectedChanged = existing.lastSelected !== isSelected;
|
|
339
|
+
const focusedChanged = existing.lastFocused !== isFocused;
|
|
340
|
+
if (selectedChanged || focusedChanged) {
|
|
341
|
+
applyClasses(existing.element, isSelected, isFocused);
|
|
342
|
+
existing.lastSelected = isSelected;
|
|
343
|
+
existing.lastFocused = isFocused;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
return {
|
|
347
|
+
render,
|
|
348
|
+
getElement: (index) => rendered.get(index)?.element,
|
|
349
|
+
updateItemClasses,
|
|
350
|
+
sortDOM,
|
|
351
|
+
clear,
|
|
352
|
+
destroy,
|
|
353
|
+
};
|
|
354
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist - Masonry Types
|
|
3
|
+
* Types for masonry/Pinterest-style layout mode
|
|
4
|
+
*
|
|
5
|
+
* Masonry layout arranges items in columns (vertical) or rows (horizontal)
|
|
6
|
+
* where items flow into the shortest column/row, creating a packed layout
|
|
7
|
+
* with no alignment across the cross-axis.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist v2 — Page (Window Scroll) Plugin
|
|
3
|
+
*
|
|
4
|
+
* Redirects scroll from the viewport to the window, enabling
|
|
5
|
+
* the list to scroll with the page. Useful for infinite feeds,
|
|
6
|
+
* full-page lists, and document-integrated virtual scrolling.
|
|
7
|
+
*
|
|
8
|
+
* Priority: 5 (runs early, before plugins that depend on scroll)
|
|
9
|
+
*/
|
|
10
|
+
import type { VListItem } from "../../types";
|
|
11
|
+
import type { VListPlugin } from "../../core/types";
|
|
12
|
+
export interface PagePluginConfig {
|
|
13
|
+
scrollPadding?: {
|
|
14
|
+
top?: number | (() => number);
|
|
15
|
+
bottom?: number | (() => number);
|
|
16
|
+
left?: number | (() => number);
|
|
17
|
+
right?: number | (() => number);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare function page<T extends VListItem = VListItem>(config?: PagePluginConfig): VListPlugin<T>;
|
|
21
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist v2 — Page (Window Scroll) Plugin
|
|
3
|
+
*
|
|
4
|
+
* Redirects scroll from the viewport to the window, enabling
|
|
5
|
+
* the list to scroll with the page. Useful for infinite feeds,
|
|
6
|
+
* full-page lists, and document-integrated virtual scrolling.
|
|
7
|
+
*
|
|
8
|
+
* Priority: 5 (runs early, before plugins that depend on scroll)
|
|
9
|
+
*/
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Helpers
|
|
12
|
+
// =============================================================================
|
|
13
|
+
const resolvePad = (v) => v == null ? 0 : typeof v === "function" ? v() : v;
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Factory
|
|
16
|
+
// =============================================================================
|
|
17
|
+
export function page(config) {
|
|
18
|
+
const scrollPadding = config?.scrollPadding;
|
|
19
|
+
let cleanupResize;
|
|
20
|
+
let cleanupScroll;
|
|
21
|
+
return {
|
|
22
|
+
name: "page",
|
|
23
|
+
priority: 5,
|
|
24
|
+
setup(ctx) {
|
|
25
|
+
const { dom, sizeCache, config: cfg, emitter } = ctx;
|
|
26
|
+
const hz = cfg.horizontal;
|
|
27
|
+
const win = window;
|
|
28
|
+
const state = ctx.getState();
|
|
29
|
+
// ── 1. Disable default viewport scroll & resize ────────────
|
|
30
|
+
ctx.disableDefaultScroll();
|
|
31
|
+
ctx.disableDefaultResize();
|
|
32
|
+
// ── 2. Modify DOM for window scroll ────────────────────────
|
|
33
|
+
dom.root.style.overflow = "visible";
|
|
34
|
+
dom.root.style.height = "auto";
|
|
35
|
+
if (hz) {
|
|
36
|
+
dom.viewport.style.overflowX = "visible";
|
|
37
|
+
dom.viewport.style.overflowY = "visible";
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
dom.viewport.style.overflow = "visible";
|
|
41
|
+
}
|
|
42
|
+
dom.viewport.classList.remove(`${cfg.classPrefix}-viewport--custom-scrollbar`);
|
|
43
|
+
// ── 3. Override scroll position get/set ─────────────────────
|
|
44
|
+
ctx.setScrollFns(() => {
|
|
45
|
+
const rect = dom.viewport.getBoundingClientRect();
|
|
46
|
+
return Math.max(0, hz ? -rect.left : -rect.top);
|
|
47
|
+
}, (pos) => {
|
|
48
|
+
const rect = dom.viewport.getBoundingClientRect();
|
|
49
|
+
const target = (hz ? rect.left + win.scrollX : rect.top + win.scrollY) + pos;
|
|
50
|
+
if (hz) {
|
|
51
|
+
win.scrollTo({ left: target, top: win.scrollY, behavior: "instant" });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
win.scrollTo({ left: win.scrollX, top: target, behavior: "instant" });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
// ── 4. Set container size from window ──────────────────────
|
|
58
|
+
state.containerSize = hz ? win.innerWidth : win.innerHeight;
|
|
59
|
+
state.crossSize = hz ? win.innerHeight : win.innerWidth;
|
|
60
|
+
// ── 5. Window scroll listener ──────────────────────────────
|
|
61
|
+
let idleTimer = null;
|
|
62
|
+
const onWindowScroll = () => {
|
|
63
|
+
const rect = dom.viewport.getBoundingClientRect();
|
|
64
|
+
const pos = Math.max(0, hz ? -rect.left : -rect.top);
|
|
65
|
+
state.prevScrollPosition = state.scrollPosition;
|
|
66
|
+
state.scrollPosition = pos;
|
|
67
|
+
state.scrollDirection = pos > state.prevScrollPosition ? 1
|
|
68
|
+
: pos < state.prevScrollPosition ? -1 : 0;
|
|
69
|
+
ctx.onScrollFrame();
|
|
70
|
+
if (idleTimer !== null)
|
|
71
|
+
clearTimeout(idleTimer);
|
|
72
|
+
idleTimer = setTimeout(() => {
|
|
73
|
+
idleTimer = null;
|
|
74
|
+
ctx.onScrollIdle();
|
|
75
|
+
}, 150);
|
|
76
|
+
};
|
|
77
|
+
win.addEventListener("scroll", onWindowScroll, { passive: true });
|
|
78
|
+
cleanupScroll = () => win.removeEventListener("scroll", onWindowScroll);
|
|
79
|
+
// ── 6. Window resize listener ──────────────────────────────
|
|
80
|
+
let prevW = win.innerWidth;
|
|
81
|
+
let prevH = win.innerHeight;
|
|
82
|
+
const onWindowResize = () => {
|
|
83
|
+
const w = win.innerWidth;
|
|
84
|
+
const h = win.innerHeight;
|
|
85
|
+
const sizeDelta = hz ? Math.abs(w - prevW) : Math.abs(h - prevH);
|
|
86
|
+
if (sizeDelta < 1)
|
|
87
|
+
return;
|
|
88
|
+
prevW = w;
|
|
89
|
+
prevH = h;
|
|
90
|
+
state.containerSize = hz ? w : h;
|
|
91
|
+
state.crossSize = hz ? h : w;
|
|
92
|
+
ctx.forceRender();
|
|
93
|
+
emitter.emit("resize", { width: w, height: h });
|
|
94
|
+
};
|
|
95
|
+
win.addEventListener("resize", onWindowResize);
|
|
96
|
+
cleanupResize = () => win.removeEventListener("resize", onWindowResize);
|
|
97
|
+
// ── 7. Scroll padding (scrollToIndex adjustments) ──────────
|
|
98
|
+
if (scrollPadding) {
|
|
99
|
+
ctx.setScrollToPosFn((index, sc, containerSize, totalItems, align) => {
|
|
100
|
+
const startPad = resolvePad(hz ? scrollPadding.left : scrollPadding.top);
|
|
101
|
+
const endPad = resolvePad(hz ? scrollPadding.right : scrollPadding.bottom);
|
|
102
|
+
if (totalItems === 0)
|
|
103
|
+
return 0;
|
|
104
|
+
const clamped = Math.max(0, Math.min(index, totalItems - 1));
|
|
105
|
+
const offset = sc.getOffset(clamped);
|
|
106
|
+
const itemSize = sc.getSize(clamped);
|
|
107
|
+
const totalSize = sc.getTotalSize();
|
|
108
|
+
const maxScroll = Math.max(0, totalSize - containerSize + endPad);
|
|
109
|
+
let pos;
|
|
110
|
+
switch (align) {
|
|
111
|
+
case "center":
|
|
112
|
+
pos = offset - startPad - (containerSize - startPad - endPad - itemSize) / 2;
|
|
113
|
+
break;
|
|
114
|
+
case "end":
|
|
115
|
+
pos = offset - containerSize + itemSize + endPad;
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
pos = offset - startPad;
|
|
119
|
+
}
|
|
120
|
+
return Math.max(-startPad, Math.min(pos, maxScroll));
|
|
121
|
+
});
|
|
122
|
+
ctx.registerMethod("_scrollItemIntoView", (index) => {
|
|
123
|
+
const containerSize = hz ? win.innerWidth : win.innerHeight;
|
|
124
|
+
const startPad = resolvePad(hz ? scrollPadding.left : scrollPadding.top);
|
|
125
|
+
const endPad = resolvePad(hz ? scrollPadding.right : scrollPadding.bottom);
|
|
126
|
+
const rect = dom.viewport.getBoundingClientRect();
|
|
127
|
+
const domScroll = hz ? win.scrollX : win.scrollY;
|
|
128
|
+
const listScreenPos = hz ? rect.left : rect.top;
|
|
129
|
+
const listDocPos = listScreenPos + domScroll;
|
|
130
|
+
const itemOffset = sizeCache.getOffset(index);
|
|
131
|
+
const itemSize = sizeCache.getSize(index);
|
|
132
|
+
const itemScreenStart = listScreenPos + itemOffset;
|
|
133
|
+
const safeEnd = containerSize - endPad;
|
|
134
|
+
let newTarget = domScroll;
|
|
135
|
+
if (itemScreenStart < startPad) {
|
|
136
|
+
newTarget = listDocPos + itemOffset - startPad;
|
|
137
|
+
}
|
|
138
|
+
else if (itemScreenStart + itemSize > safeEnd) {
|
|
139
|
+
newTarget = listDocPos + itemOffset + itemSize - safeEnd;
|
|
140
|
+
}
|
|
141
|
+
if (newTarget !== domScroll) {
|
|
142
|
+
if (hz) {
|
|
143
|
+
win.scrollTo({ left: newTarget, top: win.scrollY, behavior: "instant" });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
win.scrollTo({ left: win.scrollX, top: newTarget, behavior: "instant" });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// ── 8. Register cleanup ────────────────────────────────────
|
|
152
|
+
ctx.registerDestroyHandler(() => {
|
|
153
|
+
cleanupScroll?.();
|
|
154
|
+
cleanupResize?.();
|
|
155
|
+
if (idleTimer !== null)
|
|
156
|
+
clearTimeout(idleTimer);
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
destroy() {
|
|
160
|
+
cleanupScroll?.();
|
|
161
|
+
cleanupResize?.();
|
|
162
|
+
cleanupScroll = undefined;
|
|
163
|
+
cleanupResize = undefined;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist v2 — Scale Plugin
|
|
3
|
+
*
|
|
4
|
+
* Enables support for 1M+ item lists by compressing the scroll space
|
|
5
|
+
* when total content size exceeds the browser's ~16.7M pixel limit.
|
|
6
|
+
*
|
|
7
|
+
* Priority 20 — runs after layout plugins (10), before selection (50).
|
|
8
|
+
*
|
|
9
|
+
* When compressed:
|
|
10
|
+
* - Native scroll is disabled (overflow: hidden)
|
|
11
|
+
* - Custom wheel handler with lerp smooth scroll
|
|
12
|
+
* - Custom touch handler with momentum
|
|
13
|
+
* - Items positioned relative to viewport via onCalculate hook
|
|
14
|
+
* - Fallback scrollbar created if no scrollbar plugin present
|
|
15
|
+
*
|
|
16
|
+
* Uses v1's rendering/scale.ts pure math functions directly.
|
|
17
|
+
*/
|
|
18
|
+
import type { VListItem } from "../../types";
|
|
19
|
+
import type { VListPlugin } from "../../core/types";
|
|
20
|
+
export interface ScalePluginConfig {
|
|
21
|
+
force?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function scale<T extends VListItem = VListItem>(config?: ScalePluginConfig): VListPlugin<T>;
|
|
24
|
+
//# sourceMappingURL=plugin.d.ts.map
|