vlist 2.0.0 → 2.0.1

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.
Files changed (77) hide show
  1. package/dist/index.js +1 -28
  2. package/dist/internals.js +1 -60
  3. package/package.json +1 -1
  4. package/dist/constants.js +0 -83
  5. package/dist/core/create.js +0 -740
  6. package/dist/core/dom.js +0 -47
  7. package/dist/core/hooks.js +0 -67
  8. package/dist/core/index.js +0 -13
  9. package/dist/core/pipeline.js +0 -307
  10. package/dist/core/pool.js +0 -42
  11. package/dist/core/scroll.js +0 -137
  12. package/dist/core/sizes.js +0 -6
  13. package/dist/core/state.js +0 -56
  14. package/dist/core/types.js +0 -7
  15. package/dist/core/velocity.js +0 -33
  16. package/dist/events/emitter.js +0 -60
  17. package/dist/events/index.js +0 -6
  18. package/dist/plugins/a11y/index.js +0 -1
  19. package/dist/plugins/a11y/plugin.js +0 -259
  20. package/dist/plugins/async/index.js +0 -12
  21. package/dist/plugins/async/manager.js +0 -568
  22. package/dist/plugins/async/placeholder.js +0 -154
  23. package/dist/plugins/async/plugin.js +0 -311
  24. package/dist/plugins/async/sparse.js +0 -540
  25. package/dist/plugins/autosize/index.js +0 -4
  26. package/dist/plugins/autosize/plugin.js +0 -185
  27. package/dist/plugins/grid/index.js +0 -5
  28. package/dist/plugins/grid/layout.js +0 -275
  29. package/dist/plugins/grid/plugin.js +0 -347
  30. package/dist/plugins/grid/renderer.js +0 -525
  31. package/dist/plugins/grid/types.js +0 -11
  32. package/dist/plugins/groups/async-bridge.js +0 -246
  33. package/dist/plugins/groups/index.js +0 -13
  34. package/dist/plugins/groups/layout.js +0 -294
  35. package/dist/plugins/groups/plugin.js +0 -571
  36. package/dist/plugins/groups/sticky.js +0 -255
  37. package/dist/plugins/groups/types.js +0 -12
  38. package/dist/plugins/masonry/index.js +0 -6
  39. package/dist/plugins/masonry/layout.js +0 -261
  40. package/dist/plugins/masonry/plugin.js +0 -381
  41. package/dist/plugins/masonry/renderer.js +0 -354
  42. package/dist/plugins/masonry/types.js +0 -9
  43. package/dist/plugins/page/index.js +0 -5
  44. package/dist/plugins/page/plugin.js +0 -166
  45. package/dist/plugins/scale/index.js +0 -4
  46. package/dist/plugins/scale/plugin.js +0 -507
  47. package/dist/plugins/scrollbar/controller.js +0 -574
  48. package/dist/plugins/scrollbar/index.js +0 -6
  49. package/dist/plugins/scrollbar/plugin.js +0 -93
  50. package/dist/plugins/scrollbar/scrollbar.js +0 -556
  51. package/dist/plugins/selection/index.js +0 -7
  52. package/dist/plugins/selection/plugin.js +0 -601
  53. package/dist/plugins/selection/state.js +0 -332
  54. package/dist/plugins/snapshots/index.js +0 -5
  55. package/dist/plugins/snapshots/plugin.js +0 -301
  56. package/dist/plugins/sortable/index.js +0 -6
  57. package/dist/plugins/sortable/plugin.js +0 -753
  58. package/dist/plugins/table/header.js +0 -501
  59. package/dist/plugins/table/index.js +0 -12
  60. package/dist/plugins/table/layout.js +0 -211
  61. package/dist/plugins/table/plugin.js +0 -391
  62. package/dist/plugins/table/renderer.js +0 -625
  63. package/dist/plugins/table/types.js +0 -12
  64. package/dist/plugins/transition/index.js +0 -5
  65. package/dist/plugins/transition/plugin.js +0 -405
  66. package/dist/rendering/aria.js +0 -23
  67. package/dist/rendering/index.js +0 -18
  68. package/dist/rendering/measured.js +0 -98
  69. package/dist/rendering/renderer.js +0 -586
  70. package/dist/rendering/scale.js +0 -267
  71. package/dist/rendering/scroll.js +0 -71
  72. package/dist/rendering/sizes.js +0 -193
  73. package/dist/rendering/sort.js +0 -65
  74. package/dist/rendering/viewport.js +0 -268
  75. package/dist/types.js +0 -5
  76. package/dist/utils/padding.js +0 -49
  77. package/dist/utils/stats.js +0 -124
package/dist/core/dom.js DELETED
@@ -1,47 +0,0 @@
1
- /**
2
- * vlist v2 — DOM Structure
3
- * Container resolution and DOM scaffold creation.
4
- */
5
- // =============================================================================
6
- // Container Resolution
7
- // =============================================================================
8
- export function resolveContainer(container) {
9
- if (typeof container === "string") {
10
- const el = document.querySelector(container);
11
- if (!el)
12
- throw new Error(`[vlist] Container not found: ${container}`);
13
- return el;
14
- }
15
- return container;
16
- }
17
- // =============================================================================
18
- // DOM Structure Factory
19
- // =============================================================================
20
- export function createDOMStructure(container, classPrefix, horizontal, interactive, ariaLabel) {
21
- const rootCls = horizontal ? `${classPrefix} ${classPrefix}--horizontal` : classPrefix;
22
- const vpStyle = horizontal
23
- ? "overflow-x:auto;overflow-y:hidden;height:100%;width:100%"
24
- : "overflow:auto;height:100%;width:100%";
25
- const cStyle = horizontal ? "position:relative;height:100%" : "position:relative;width:100%";
26
- let cAttrs = ` role="${interactive ? "listbox" : "list"}"`;
27
- if (interactive)
28
- cAttrs += ' tabindex="0"';
29
- if (ariaLabel)
30
- cAttrs += ` aria-label="${ariaLabel.replace(/"/g, """)}"`;
31
- if (horizontal)
32
- cAttrs += ' aria-orientation="horizontal"';
33
- container.insertAdjacentHTML("beforeend", `<div class="${rootCls}"><div class="${classPrefix}-viewport" style="${vpStyle}" tabindex="-1"><div class="${classPrefix}-content" style="${cStyle}"${cAttrs}></div></div></div>`);
34
- const root = container.lastElementChild;
35
- const viewport = root.firstElementChild;
36
- const content = viewport.firstElementChild;
37
- const liveRegion = document.createElement("div");
38
- liveRegion.className = `${classPrefix}-live`;
39
- liveRegion.style.cssText =
40
- "position:absolute;width:1px;height:1px;padding:0;margin:-1px;" +
41
- "overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0";
42
- liveRegion.setAttribute("aria-live", "polite");
43
- liveRegion.setAttribute("aria-atomic", "true");
44
- liveRegion.setAttribute("role", "status");
45
- root.appendChild(liveRegion);
46
- return { root, viewport, content, liveRegion };
47
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * vlist v2 — Build-Time Compiled Hooks
3
- *
4
- * Hooks are collected during createVList() and frozen into plain arrays.
5
- * On the hot path they are iterated with a for-loop — no closures,
6
- * no dynamic dispatch, no allocation.
7
- */
8
- // =============================================================================
9
- // Compiler — collects hooks from plugins, returns frozen arrays
10
- // =============================================================================
11
- const EMPTY = [];
12
- const EMPTY_HOOKS = {
13
- calculate: EMPTY, commit: EMPTY, afterScroll: EMPTY, idle: EMPTY, resize: EMPTY,
14
- };
15
- export function compileHooks(plugins) {
16
- if (plugins.length === 0)
17
- return EMPTY_HOOKS;
18
- const calculate = [];
19
- const commit = [];
20
- const afterScroll = [];
21
- const idle = [];
22
- const resize = [];
23
- for (let i = 0; i < plugins.length; i++) {
24
- const h = plugins[i].hooks;
25
- if (h === undefined)
26
- continue;
27
- if (h.onCalculate !== undefined)
28
- calculate.push(h.onCalculate);
29
- if (h.onCommit !== undefined)
30
- commit.push(h.onCommit);
31
- if (h.onAfterScroll !== undefined)
32
- afterScroll.push(h.onAfterScroll);
33
- if (h.onIdle !== undefined)
34
- idle.push(h.onIdle);
35
- if (h.onResize !== undefined)
36
- resize.push(h.onResize);
37
- }
38
- return { calculate, commit, afterScroll, idle, resize };
39
- }
40
- // =============================================================================
41
- // Runners — zero allocation, linear iteration
42
- // =============================================================================
43
- export function runCalculateHooks(hooks, state) {
44
- for (let i = 0; i < hooks.length; i++) {
45
- hooks[i](state);
46
- }
47
- }
48
- export function runCommitHooks(hooks, state) {
49
- for (let i = 0; i < hooks.length; i++) {
50
- hooks[i](state);
51
- }
52
- }
53
- export function runAfterScrollHooks(hooks, scrollPosition, direction) {
54
- for (let i = 0; i < hooks.length; i++) {
55
- hooks[i](scrollPosition, direction);
56
- }
57
- }
58
- export function runIdleHooks(hooks) {
59
- for (let i = 0; i < hooks.length; i++) {
60
- hooks[i]();
61
- }
62
- }
63
- export function runResizeHooks(hooks, width, height) {
64
- for (let i = 0; i < hooks.length; i++) {
65
- hooks[i](width, height);
66
- }
67
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * vlist v2 — Core Public API
3
- */
4
- // Factory
5
- export { createVList } from "./create";
6
- // Engine
7
- export { createEngineState } from "./state";
8
- export { phase1Calculate, phase2Commit, render, createRenderConfig } from "./pipeline";
9
- export { compileHooks, runCalculateHooks, runCommitHooks, runAfterScrollHooks, runIdleHooks, runResizeHooks } from "./hooks";
10
- export { createPool } from "./pool";
11
- export { createSizeCache, countVisibleItems, countItemsFittingFromBottom, getOffsetForVirtualIndex } from "./sizes";
12
- export { createDOMStructure, resolveContainer } from "./dom";
13
- export { createScrollHandler } from "./scroll";
@@ -1,307 +0,0 @@
1
- /**
2
- * vlist v2 — 2-Phase Pipeline
3
- *
4
- * Phase 1: Calculate & Reconcile — zero allocation hot path.
5
- * Reads scroll position + size cache, writes into EngineState TypedArrays.
6
- *
7
- * Phase 2: Commit — reads EngineState buffers, updates DOM via pool.
8
- * Sub-phases: acquire → identity bind → position → release.
9
- *
10
- * Both phases are synchronous. No intermediate objects are allocated.
11
- */
12
- import { runCalculateHooks, runCommitHooks } from "./hooks";
13
- import { PLACEHOLDER_ID_PREFIX } from "../constants";
14
- export function createRenderConfig(classPrefix, horizontal, interactive, startPadding, crossPadStart, crossPadEnd, oddClass, gap, emitter) {
15
- const hasCrossPad = crossPadStart !== 0 || crossPadEnd !== 0;
16
- return {
17
- prefix: classPrefix,
18
- selectedClass: `${classPrefix}-item--selected`,
19
- focusedClass: `${classPrefix}-item--focused`,
20
- placeholderClass: `${classPrefix}-item--placeholder`,
21
- replacedClass: `${classPrefix}-item--replaced`,
22
- translateProp: horizontal ? "translateX(" : "translateY(",
23
- itemRole: interactive ? "option" : "listitem",
24
- interactive,
25
- horizontal,
26
- startPadding,
27
- gap,
28
- hasCrossPad,
29
- crossStartProp: hasCrossPad ? (horizontal ? "top" : "left") : "",
30
- crossEndProp: hasCrossPad ? (horizontal ? "bottom" : "right") : "",
31
- crossStartVal: hasCrossPad ? crossPadStart + "px" : "",
32
- crossEndVal: hasCrossPad ? crossPadEnd + "px" : "",
33
- oddClass,
34
- emitter: emitter ?? null,
35
- };
36
- }
37
- // =============================================================================
38
- // Phase 1 — Calculate & Reconcile
39
- // =============================================================================
40
- /**
41
- * Calculate visible range and fill EngineState buffers.
42
- * Zero allocation — all writes go into pre-allocated TypedArrays.
43
- *
44
- * Guards:
45
- * - Zero container size early exit
46
- * - Empty range sentinel: visibleCount = 0
47
- * - Overscan application
48
- * - Render count safety cap
49
- */
50
- export function phase1Calculate(state, sizeCache, overscan, hooks, startPadding) {
51
- if (state.containerSize <= 0 || state.totalItems === 0) {
52
- state.clear();
53
- return true;
54
- }
55
- const scrollPos = state.scrollPosition;
56
- const containerSize = state.containerSize;
57
- const totalItems = state.totalItems;
58
- const sp = startPadding ?? 0;
59
- state.totalSize = sizeCache.getTotalSize();
60
- // Visible range — items are visually shifted by startPadding in the
61
- // transform, so subtract it from the range start lookup to avoid
62
- // missing items at the top of the viewport.
63
- let visStart = sizeCache.indexAtOffset(sp > 0 ? Math.max(0, scrollPos - sp) : scrollPos);
64
- let visEnd = sizeCache.indexAtOffset(scrollPos + containerSize);
65
- if (visEnd < totalItems - 1)
66
- visEnd++;
67
- visStart = Math.max(0, visStart);
68
- visEnd = Math.min(totalItems - 1, Math.max(0, visEnd));
69
- // Overscan
70
- const renderStart = Math.max(0, visStart - overscan);
71
- const renderEnd = Math.min(totalItems - 1, visEnd + overscan);
72
- // Safety cap
73
- const maxRender = Math.ceil(containerSize / 1) + overscan * 2 + 10;
74
- const count = renderEnd - renderStart + 1;
75
- const safeCap = Math.min(count, state.capacity, maxRender);
76
- // Range-unchanged fast path
77
- if (renderStart === state.prevRangeStart && renderEnd === state.prevRangeEnd && !state.renderPending) {
78
- return false;
79
- }
80
- // Fill TypedArray buffers
81
- state.visibleCount = safeCap;
82
- state.startIndex = renderStart;
83
- for (let i = 0; i < safeCap; i++) {
84
- const idx = renderStart + i;
85
- state.visibleIndices[i] = idx;
86
- state.visibleOffsets[i] = sizeCache.getOffset(idx);
87
- state.visibleSizes[i] = sizeCache.getSize(idx);
88
- }
89
- runCalculateHooks(hooks.calculate, state);
90
- state.prevRangeStart = renderStart;
91
- state.prevRangeEnd = renderEnd;
92
- state.renderPending = false;
93
- return true;
94
- }
95
- // =============================================================================
96
- // Phase 2 — Commit (DOM Update)
97
- // =============================================================================
98
- /** Reusable ItemState singleton — never allocated per frame */
99
- const itemState = { selected: false, focused: false };
100
- /** Linear scan for idx in visibleIndices[0..count). Handles arbitrary order. Zero allocation. */
101
- function isInVisible(indices, count, idx) {
102
- for (let i = 0; i < count; i++) {
103
- if (indices[i] === idx)
104
- return true;
105
- }
106
- return false;
107
- }
108
- /** Module-scope release state — avoids per-frame closure in rendered.forEach */
109
- let _relIndices;
110
- let _relCount;
111
- let _relPool;
112
- let _relRendered;
113
- function releaseIfNotVisible(element, idx) {
114
- if (!isInVisible(_relIndices, _relCount, idx)) {
115
- element.remove();
116
- _relPool.release(element);
117
- _relRendered.delete(idx);
118
- }
119
- }
120
- export function phase2Commit(state, pool, contentElement, template, getItems, rendered, rc, hooks, getItemFn, itemStateFn) {
121
- const items = getItemFn ? null : getItems();
122
- const count = state.visibleCount;
123
- const newIndices = state.visibleIndices;
124
- const ariaTotal = rc.interactive ? String(state.totalItems) : "";
125
- const totalChanged = rc.interactive && state.totalItems !== state.prevAriaTotal;
126
- let fragment = null;
127
- for (let i = 0; i < count; i++) {
128
- const dataIndex = newIndices[i];
129
- const offset = state.visibleOffsets[i];
130
- const size = state.visibleSizes[i];
131
- const item = getItemFn ? getItemFn(dataIndex) : items[dataIndex];
132
- if (itemStateFn) {
133
- itemStateFn(dataIndex, itemState);
134
- }
135
- else {
136
- itemState.selected = false;
137
- itemState.focused = false;
138
- }
139
- let element = rendered.get(dataIndex);
140
- const el = element;
141
- if (element === undefined) {
142
- const acquired = pool.acquire();
143
- if (item !== undefined) {
144
- let result;
145
- try {
146
- result = template(item, dataIndex, itemState);
147
- }
148
- catch (err) {
149
- if (rc.emitter) {
150
- rc.emitter.emit("error", {
151
- error: err instanceof Error ? err : new Error(String(err)),
152
- context: `template:render:${dataIndex}`,
153
- });
154
- }
155
- pool.release(acquired);
156
- continue;
157
- }
158
- if (typeof result === "string") {
159
- acquired.innerHTML = result;
160
- }
161
- else {
162
- acquired.innerHTML = "";
163
- acquired.appendChild(result);
164
- }
165
- }
166
- acquired.setAttribute("role", rc.itemRole);
167
- acquired.setAttribute("data-index", String(dataIndex));
168
- if (rc.interactive) {
169
- acquired.id = rc.prefix + "-item-" + dataIndex;
170
- acquired.setAttribute("aria-posinset", String(dataIndex + 1));
171
- acquired.setAttribute("aria-setsize", ariaTotal);
172
- }
173
- if (item !== undefined) {
174
- const itemId = String(item.id);
175
- acquired.setAttribute("data-id", itemId);
176
- if (itemId.startsWith(PLACEHOLDER_ID_PREFIX)) {
177
- acquired.classList.add(rc.placeholderClass);
178
- }
179
- }
180
- if (rc.hasCrossPad) {
181
- acquired.style.setProperty(rc.crossStartProp, rc.crossStartVal);
182
- acquired.style.setProperty(rc.crossEndProp, rc.crossEndVal);
183
- }
184
- if (itemStateFn) {
185
- acquired.classList.toggle(rc.selectedClass, itemState.selected);
186
- acquired.classList.toggle(rc.focusedClass, itemState.focused);
187
- if (itemState.selected)
188
- acquired.setAttribute("aria-selected", "true");
189
- else
190
- acquired.removeAttribute("aria-selected");
191
- acquired._lastSelected = itemState.selected;
192
- acquired._lastFocused = itemState.focused;
193
- }
194
- if (rc.oddClass)
195
- acquired.classList.toggle(rc.oddClass, (dataIndex & 1) === 1);
196
- const transformOffset = offset + rc.startPadding;
197
- acquired.style.transform = rc.translateProp + transformOffset + "px)";
198
- acquired._lastOffset = transformOffset;
199
- const sizeVal = size - rc.gap;
200
- if (rc.horizontal) {
201
- acquired.style.width = sizeVal + "px";
202
- }
203
- else {
204
- acquired.style.height = sizeVal + "px";
205
- }
206
- acquired._lastSize = sizeVal;
207
- acquired._lastItem = item;
208
- rendered.set(dataIndex, acquired);
209
- if (fragment === null)
210
- fragment = document.createDocumentFragment();
211
- fragment.appendChild(acquired);
212
- }
213
- else {
214
- if (totalChanged) {
215
- element.setAttribute("aria-setsize", ariaTotal);
216
- }
217
- if (item !== undefined && el._lastItem !== item) {
218
- const oldId = element.getAttribute("data-id");
219
- const newId = String(item.id);
220
- let result;
221
- try {
222
- result = template(item, dataIndex, itemState);
223
- }
224
- catch (err) {
225
- if (rc.emitter) {
226
- rc.emitter.emit("error", {
227
- error: err instanceof Error ? err : new Error(String(err)),
228
- context: `template:render:${dataIndex}`,
229
- });
230
- }
231
- continue;
232
- }
233
- if (typeof result === "string") {
234
- element.innerHTML = result;
235
- }
236
- else {
237
- element.innerHTML = "";
238
- element.appendChild(result);
239
- }
240
- element.setAttribute("data-id", newId);
241
- el._lastItem = item;
242
- if (oldId !== newId) {
243
- const wasPlaceholder = oldId !== null && oldId.startsWith(PLACEHOLDER_ID_PREFIX);
244
- const isPlaceholder = newId.startsWith(PLACEHOLDER_ID_PREFIX);
245
- if (wasPlaceholder !== isPlaceholder) {
246
- element.classList.toggle(rc.placeholderClass, isPlaceholder);
247
- }
248
- if (wasPlaceholder && !isPlaceholder) {
249
- element.classList.add(rc.replacedClass);
250
- setTimeout(() => { element.classList.remove(rc.replacedClass); }, 300);
251
- }
252
- }
253
- }
254
- if (itemStateFn) {
255
- if (el._lastSelected !== itemState.selected) {
256
- element.classList.toggle(rc.selectedClass, itemState.selected);
257
- if (itemState.selected)
258
- element.setAttribute("aria-selected", "true");
259
- else
260
- element.removeAttribute("aria-selected");
261
- el._lastSelected = itemState.selected;
262
- }
263
- if (el._lastFocused !== itemState.focused) {
264
- element.classList.toggle(rc.focusedClass, itemState.focused);
265
- el._lastFocused = itemState.focused;
266
- }
267
- }
268
- const transformOffset = offset + rc.startPadding;
269
- if (el._lastOffset !== transformOffset) {
270
- element.style.transform = rc.translateProp + transformOffset + "px)";
271
- el._lastOffset = transformOffset;
272
- }
273
- const sizeVal = size - rc.gap;
274
- if (el._lastSize !== sizeVal) {
275
- if (rc.horizontal) {
276
- element.style.width = sizeVal + "px";
277
- }
278
- else {
279
- element.style.height = sizeVal + "px";
280
- }
281
- el._lastSize = sizeVal;
282
- }
283
- }
284
- }
285
- // Flush all new elements in one DOM operation
286
- if (fragment !== null)
287
- contentElement.appendChild(fragment);
288
- // Release nodes no longer visible (after acquire so new elements are in the
289
- // DOM before stale ones are removed — no single-frame gaps).
290
- _relIndices = newIndices;
291
- _relCount = count;
292
- _relPool = pool;
293
- _relRendered = rendered;
294
- rendered.forEach(releaseIfNotVisible);
295
- if (rc.interactive)
296
- state.prevAriaTotal = state.totalItems;
297
- runCommitHooks(hooks.commit, state);
298
- }
299
- // =============================================================================
300
- // Full Render Cycle
301
- // =============================================================================
302
- export function render(state, sizeCache, overscan, pool, contentElement, template, getItems, rendered, rc, hooks, getItemFn, itemStateFn) {
303
- const changed = phase1Calculate(state, sizeCache, overscan, hooks, rc.startPadding);
304
- if (changed) {
305
- phase2Commit(state, pool, contentElement, template, getItems, rendered, rc, hooks, getItemFn, itemStateFn);
306
- }
307
- }
package/dist/core/pool.js DELETED
@@ -1,42 +0,0 @@
1
- /**
2
- * vlist v2 — Element Pool
3
- *
4
- * acquire() = pop or create, release() = reset + push.
5
- * Max pool size: 100.
6
- */
7
- const MAX_POOL_SIZE = 100;
8
- export function createPool(classPrefix) {
9
- const pool = [];
10
- const itemClass = `${classPrefix}-item`;
11
- const tpl = document.createElement("div");
12
- tpl.className = itemClass;
13
- return {
14
- acquire() {
15
- if (pool.length > 0) {
16
- return pool.pop();
17
- }
18
- return tpl.cloneNode(false);
19
- },
20
- release(element) {
21
- element.className = itemClass;
22
- element.removeAttribute("style");
23
- element.removeAttribute("id");
24
- element.removeAttribute("role");
25
- element.removeAttribute("aria-selected");
26
- element.removeAttribute("aria-posinset");
27
- element.removeAttribute("aria-setsize");
28
- element.removeAttribute("data-index");
29
- element.removeAttribute("data-id");
30
- element.innerHTML = "";
31
- if (pool.length < MAX_POOL_SIZE) {
32
- pool.push(element);
33
- }
34
- },
35
- get size() {
36
- return pool.length;
37
- },
38
- clear() {
39
- pool.length = 0;
40
- },
41
- };
42
- }
@@ -1,137 +0,0 @@
1
- /**
2
- * vlist v2 — Scroll Handling
3
- *
4
- * Wheel interception for synchronous rendering, scroll idle detection,
5
- * and smooth scroll animation.
6
- */
7
- import { SCROLL_IDLE_TIMEOUT, WHEEL_SENSITIVITY, SCROLL_EASING } from "../constants";
8
- export function createScrollHandler(config) {
9
- const { state, viewport, horizontal, wheelEnabled, onFrame, onIdle } = config;
10
- const idleTimeout = config.idleTimeout || SCROLL_IDLE_TIMEOUT;
11
- const target = config.scrollTarget ?? viewport;
12
- let idleTimer = null;
13
- let animationId = null;
14
- // ── Scroll event (passive, for native/touch scrolling) ──────────
15
- function onScrollEvent() {
16
- const pos = horizontal ? viewport.scrollLeft : viewport.scrollTop;
17
- if (Math.abs(pos - state.scrollPosition) < 0.5)
18
- return;
19
- state.prevScrollPosition = state.scrollPosition;
20
- state.scrollPosition = pos;
21
- state.scrollDirection = pos > state.prevScrollPosition ? 1 : pos < state.prevScrollPosition ? -1 : 0;
22
- onFrame();
23
- scheduleIdle();
24
- }
25
- // ── Wheel event (non-passive, synchronous rendering) ────────────
26
- function onWheelEvent(event) {
27
- if (state.isCompressed)
28
- return;
29
- let next;
30
- if (horizontal) {
31
- if (Math.abs(event.deltaX) > Math.abs(event.deltaY))
32
- return;
33
- const current = viewport.scrollLeft;
34
- const max = viewport.scrollWidth - viewport.clientWidth;
35
- next = Math.max(0, Math.min(current + event.deltaY * WHEEL_SENSITIVITY, max));
36
- if (Math.abs(next - current) < 1)
37
- return;
38
- event.preventDefault();
39
- viewport.scrollLeft = next;
40
- }
41
- else {
42
- const crossOverflow = viewport.scrollWidth > viewport.clientWidth;
43
- if (crossOverflow && Math.abs(event.deltaX) > Math.abs(event.deltaY))
44
- return;
45
- if (crossOverflow && event.deltaX !== 0) {
46
- event.preventDefault();
47
- viewport.scrollLeft += event.deltaX;
48
- }
49
- const current = viewport.scrollTop;
50
- const max = viewport.scrollHeight - viewport.clientHeight;
51
- next = Math.max(0, Math.min(current + event.deltaY * WHEEL_SENSITIVITY, max));
52
- if (Math.abs(next - current) < 1)
53
- return;
54
- event.preventDefault();
55
- viewport.scrollTop = next;
56
- }
57
- state.prevScrollPosition = state.scrollPosition;
58
- state.scrollPosition = next;
59
- state.scrollDirection = next > state.prevScrollPosition ? 1 : -1;
60
- onFrame();
61
- scheduleIdle();
62
- }
63
- // ── Idle detection ──────────────────────────────────────────────
64
- function scheduleIdle() {
65
- if (idleTimer !== null)
66
- clearTimeout(idleTimer);
67
- idleTimer = setTimeout(() => {
68
- idleTimer = null;
69
- state.scrollDirection = 0;
70
- onIdle();
71
- }, idleTimeout);
72
- }
73
- // ── Smooth scroll animation ─────────────────────────────────────
74
- function cancelScroll() {
75
- if (animationId !== null) {
76
- cancelAnimationFrame(animationId);
77
- animationId = null;
78
- }
79
- }
80
- function smoothScrollTo(target, duration, setFn, easing = SCROLL_EASING) {
81
- cancelScroll();
82
- const from = state.scrollPosition;
83
- if (Math.abs(target - from) < 1) {
84
- if (setFn)
85
- setFn(target);
86
- else if (horizontal)
87
- viewport.scrollLeft = target;
88
- else
89
- viewport.scrollTop = target;
90
- return;
91
- }
92
- const start = performance.now();
93
- function tick(now) {
94
- const elapsed = now - start;
95
- const t = Math.min(elapsed / duration, 1);
96
- const pos = from + (target - from) * easing(t);
97
- if (setFn)
98
- setFn(pos);
99
- else if (horizontal)
100
- viewport.scrollLeft = pos;
101
- else
102
- viewport.scrollTop = pos;
103
- if (!setFn)
104
- state.scrollPosition = pos;
105
- onFrame();
106
- if (t < 1) {
107
- animationId = requestAnimationFrame(tick);
108
- }
109
- else {
110
- animationId = null;
111
- }
112
- }
113
- animationId = requestAnimationFrame(tick);
114
- }
115
- // ── Public interface ────────────────────────────────────────────
116
- return {
117
- attach() {
118
- target.addEventListener("scroll", onScrollEvent, { passive: true });
119
- if (wheelEnabled) {
120
- target.addEventListener("wheel", onWheelEvent, { passive: false });
121
- }
122
- },
123
- detach() {
124
- target.removeEventListener("scroll", onScrollEvent);
125
- if (wheelEnabled) {
126
- target.removeEventListener("wheel", onWheelEvent);
127
- }
128
- cancelScroll();
129
- if (idleTimer !== null) {
130
- clearTimeout(idleTimer);
131
- idleTimer = null;
132
- }
133
- },
134
- cancelScroll,
135
- smoothScrollTo,
136
- };
137
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * vlist v2 — Size Cache
3
- *
4
- * Float64Array prefix sums with O(1) offset lookups and O(log n) binary search.
5
- */
6
- export { createSizeCache, countVisibleItems, countItemsFittingFromBottom, getOffsetForVirtualIndex, } from "../rendering/sizes";
@@ -1,56 +0,0 @@
1
- /**
2
- * vlist v2 — EngineState
3
- *
4
- * Persistent singleton instantiated once during createVList().
5
- * All hot-path state lives in pre-allocated TypedArrays.
6
- * Phase 1 mutates in place. Phase 2 reads directly. Zero allocation.
7
- */
8
- import { OVERSCAN } from "../constants";
9
- export function createEngineState(initialCapacity) {
10
- const state = {
11
- visibleIndices: new Int32Array(initialCapacity),
12
- visibleOffsets: new Float64Array(initialCapacity),
13
- visibleSizes: new Float64Array(initialCapacity),
14
- visibleCount: 0,
15
- startIndex: 0,
16
- totalSize: 0,
17
- capacity: initialCapacity,
18
- scrollPosition: 0,
19
- prevScrollPosition: 0,
20
- scrollDirection: 0,
21
- containerSize: 0,
22
- crossSize: 0,
23
- totalItems: 0,
24
- prevRangeStart: 0,
25
- prevRangeEnd: -1,
26
- renderPending: false,
27
- initialized: false,
28
- destroyed: false,
29
- isCompressed: false,
30
- compressionRatio: 1,
31
- prevAriaTotal: -1,
32
- resizeCapacity(containerSize, minItemSize, overscan = OVERSCAN) {
33
- if (minItemSize <= 0 || containerSize <= 0)
34
- return;
35
- const needed = Math.ceil(containerSize / minItemSize) + overscan * 2;
36
- if (needed <= state.capacity)
37
- return;
38
- const newCapacity = needed + 8;
39
- const newIndices = new Int32Array(newCapacity);
40
- const newOffsets = new Float64Array(newCapacity);
41
- const newSizes = new Float64Array(newCapacity);
42
- newIndices.set(state.visibleIndices);
43
- newOffsets.set(state.visibleOffsets);
44
- newSizes.set(state.visibleSizes);
45
- state.visibleIndices = newIndices;
46
- state.visibleOffsets = newOffsets;
47
- state.visibleSizes = newSizes;
48
- state.capacity = newCapacity;
49
- },
50
- clear() {
51
- state.visibleCount = 0;
52
- state.startIndex = 0;
53
- },
54
- };
55
- return state;
56
- }