vlist 2.0.0 → 2.0.2
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 +2 -2
- package/README.md +2 -2
- package/dist/core/dom.d.ts +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/pipeline.d.ts +2 -2
- package/dist/core/scroll.d.ts +1 -1
- package/dist/core/types.d.ts +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -28
- package/dist/internals.js +1 -60
- package/dist/plugins/scrollbar/controller.d.ts +3 -3
- package/dist/plugins/scrollbar/scrollbar.d.ts +2 -2
- package/dist/rendering/renderer.d.ts +2 -2
- package/dist/rendering/viewport.d.ts +1 -1
- package/dist/size.json +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/dist/constants.js +0 -83
- package/dist/core/create.js +0 -740
- package/dist/core/dom.js +0 -47
- package/dist/core/hooks.js +0 -67
- package/dist/core/index.js +0 -13
- package/dist/core/pipeline.js +0 -307
- package/dist/core/pool.js +0 -42
- package/dist/core/scroll.js +0 -137
- package/dist/core/sizes.js +0 -6
- package/dist/core/state.js +0 -56
- package/dist/core/types.js +0 -7
- package/dist/core/velocity.js +0 -33
- package/dist/events/emitter.js +0 -60
- package/dist/events/index.js +0 -6
- package/dist/plugins/a11y/index.js +0 -1
- package/dist/plugins/a11y/plugin.js +0 -259
- package/dist/plugins/async/index.js +0 -12
- package/dist/plugins/async/manager.js +0 -568
- package/dist/plugins/async/placeholder.js +0 -154
- package/dist/plugins/async/plugin.js +0 -311
- package/dist/plugins/async/sparse.js +0 -540
- package/dist/plugins/autosize/index.js +0 -4
- package/dist/plugins/autosize/plugin.js +0 -185
- package/dist/plugins/grid/index.js +0 -5
- package/dist/plugins/grid/layout.js +0 -275
- package/dist/plugins/grid/plugin.js +0 -347
- package/dist/plugins/grid/renderer.js +0 -525
- package/dist/plugins/grid/types.js +0 -11
- package/dist/plugins/groups/async-bridge.js +0 -246
- package/dist/plugins/groups/index.js +0 -13
- package/dist/plugins/groups/layout.js +0 -294
- package/dist/plugins/groups/plugin.js +0 -571
- package/dist/plugins/groups/sticky.js +0 -255
- package/dist/plugins/groups/types.js +0 -12
- package/dist/plugins/masonry/index.js +0 -6
- package/dist/plugins/masonry/layout.js +0 -261
- package/dist/plugins/masonry/plugin.js +0 -381
- package/dist/plugins/masonry/renderer.js +0 -354
- package/dist/plugins/masonry/types.js +0 -9
- package/dist/plugins/page/index.js +0 -5
- package/dist/plugins/page/plugin.js +0 -166
- package/dist/plugins/scale/index.js +0 -4
- package/dist/plugins/scale/plugin.js +0 -507
- package/dist/plugins/scrollbar/controller.js +0 -574
- package/dist/plugins/scrollbar/index.js +0 -6
- package/dist/plugins/scrollbar/plugin.js +0 -93
- package/dist/plugins/scrollbar/scrollbar.js +0 -556
- package/dist/plugins/selection/index.js +0 -7
- package/dist/plugins/selection/plugin.js +0 -601
- package/dist/plugins/selection/state.js +0 -332
- package/dist/plugins/snapshots/index.js +0 -5
- package/dist/plugins/snapshots/plugin.js +0 -301
- package/dist/plugins/sortable/index.js +0 -6
- package/dist/plugins/sortable/plugin.js +0 -753
- package/dist/plugins/table/header.js +0 -501
- package/dist/plugins/table/index.js +0 -12
- package/dist/plugins/table/layout.js +0 -211
- package/dist/plugins/table/plugin.js +0 -391
- package/dist/plugins/table/renderer.js +0 -625
- package/dist/plugins/table/types.js +0 -12
- package/dist/plugins/transition/index.js +0 -5
- package/dist/plugins/transition/plugin.js +0 -405
- package/dist/rendering/aria.js +0 -23
- package/dist/rendering/index.js +0 -18
- package/dist/rendering/measured.js +0 -98
- package/dist/rendering/renderer.js +0 -586
- package/dist/rendering/scale.js +0 -267
- package/dist/rendering/scroll.js +0 -71
- package/dist/rendering/sizes.js +0 -193
- package/dist/rendering/sort.js +0 -65
- package/dist/rendering/viewport.js +0 -268
- package/dist/types.js +0 -5
- package/dist/utils/padding.js +0 -49
- 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
|
-
}
|
package/dist/core/hooks.js
DELETED
|
@@ -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
|
-
}
|
package/dist/core/index.js
DELETED
|
@@ -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";
|
package/dist/core/pipeline.js
DELETED
|
@@ -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
|
-
}
|
package/dist/core/scroll.js
DELETED
|
@@ -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
|
-
}
|
package/dist/core/sizes.js
DELETED
package/dist/core/state.js
DELETED
|
@@ -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
|
-
}
|