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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist - Placeholder System
|
|
3
|
+
* Smart placeholder generation for loading states
|
|
4
|
+
*
|
|
5
|
+
* Key features:
|
|
6
|
+
* - Captures per-item field lengths from the first loaded batch
|
|
7
|
+
* - Cycles through real data profiles for natural size variance
|
|
8
|
+
* - Same item template renders both real and placeholder items
|
|
9
|
+
* - Renderer adds CSS class for visual styling (no JS branching needed)
|
|
10
|
+
*/
|
|
11
|
+
import { PLACEHOLDER_FLAG, PLACEHOLDER_ID_PREFIX, MASK_CHARACTER, MAX_SAMPLE_SIZE, } from "../../constants";
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Placeholder Manager
|
|
14
|
+
// =============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Create a placeholder manager that generates realistic placeholder items
|
|
17
|
+
* by capturing per-item field lengths from the first loaded data batch.
|
|
18
|
+
*
|
|
19
|
+
* Placeholders carry the same field names as real items, filled with
|
|
20
|
+
* mask characters sized to match actual data. The renderer detects
|
|
21
|
+
* placeholders via the `_isPlaceholder` flag and applies a CSS class
|
|
22
|
+
* — no template branching required.
|
|
23
|
+
*/
|
|
24
|
+
export const createPlaceholderManager = (config = {}) => {
|
|
25
|
+
const { maskCharacter = MASK_CHARACTER, maxSampleSize = MAX_SAMPLE_SIZE, } = config;
|
|
26
|
+
// State
|
|
27
|
+
let lengthProfiles = [];
|
|
28
|
+
let hasAnalyzed = false;
|
|
29
|
+
// ==========================================================================
|
|
30
|
+
// Structure Analysis
|
|
31
|
+
// ==========================================================================
|
|
32
|
+
/**
|
|
33
|
+
* Capture per-item field lengths from the first loaded batch.
|
|
34
|
+
* Each sampled item produces one LengthProfile that records the
|
|
35
|
+
* string length of every non-internal field. When generating
|
|
36
|
+
* placeholder #N, we cycle through these profiles so that size
|
|
37
|
+
* variance mirrors the real data distribution.
|
|
38
|
+
*/
|
|
39
|
+
const analyzeStructure = (items) => {
|
|
40
|
+
if (hasAnalyzed || items.length === 0)
|
|
41
|
+
return;
|
|
42
|
+
const sampleSize = Math.min(items.length, maxSampleSize);
|
|
43
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
44
|
+
const item = items[i];
|
|
45
|
+
if (!item || typeof item !== "object")
|
|
46
|
+
continue;
|
|
47
|
+
const profile = {};
|
|
48
|
+
let hasFields = false;
|
|
49
|
+
for (const [field, value] of Object.entries(item)) {
|
|
50
|
+
// Skip internal fields and id
|
|
51
|
+
if (field.startsWith("_") || field === "id")
|
|
52
|
+
continue;
|
|
53
|
+
profile[field] = String(value ?? "").length;
|
|
54
|
+
hasFields = true;
|
|
55
|
+
}
|
|
56
|
+
// Only store profiles that have at least one field —
|
|
57
|
+
// id-only items produce empty profiles which aren't useful
|
|
58
|
+
if (hasFields) {
|
|
59
|
+
lengthProfiles.push(profile);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
hasAnalyzed = true;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Check if structure has been analyzed
|
|
66
|
+
*/
|
|
67
|
+
const hasAnalyzedStructure = () => hasAnalyzed;
|
|
68
|
+
// ==========================================================================
|
|
69
|
+
// Placeholder Generation
|
|
70
|
+
// ==========================================================================
|
|
71
|
+
/**
|
|
72
|
+
* Generate a single placeholder item.
|
|
73
|
+
* Uses the length profile at `index % profiles.length` so each
|
|
74
|
+
* placeholder has a unique but realistic field size distribution.
|
|
75
|
+
*/
|
|
76
|
+
const generate = (index) => {
|
|
77
|
+
const placeholder = {
|
|
78
|
+
id: `${PLACEHOLDER_ID_PREFIX}${index}`,
|
|
79
|
+
[PLACEHOLDER_FLAG]: true,
|
|
80
|
+
_index: index,
|
|
81
|
+
};
|
|
82
|
+
// No profiles yet — basic fallback
|
|
83
|
+
if (lengthProfiles.length === 0) {
|
|
84
|
+
placeholder.label = maskCharacter.repeat(12);
|
|
85
|
+
return placeholder;
|
|
86
|
+
}
|
|
87
|
+
// Cycle through captured profiles
|
|
88
|
+
const profile = lengthProfiles[index % lengthProfiles.length];
|
|
89
|
+
for (const [field, length] of Object.entries(profile)) {
|
|
90
|
+
placeholder[field] = maskCharacter.repeat(Math.max(1, length));
|
|
91
|
+
}
|
|
92
|
+
return placeholder;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Generate multiple placeholder items
|
|
96
|
+
*/
|
|
97
|
+
const generateRange = (start, end) => {
|
|
98
|
+
const items = [];
|
|
99
|
+
for (let i = start; i <= end; i++) {
|
|
100
|
+
items.push(generate(i));
|
|
101
|
+
}
|
|
102
|
+
return items;
|
|
103
|
+
};
|
|
104
|
+
// ==========================================================================
|
|
105
|
+
// Lifecycle
|
|
106
|
+
// ==========================================================================
|
|
107
|
+
/**
|
|
108
|
+
* Clear analyzed structure
|
|
109
|
+
*/
|
|
110
|
+
const clear = () => {
|
|
111
|
+
lengthProfiles = [];
|
|
112
|
+
hasAnalyzed = false;
|
|
113
|
+
};
|
|
114
|
+
// ==========================================================================
|
|
115
|
+
// Return Public API
|
|
116
|
+
// ==========================================================================
|
|
117
|
+
return {
|
|
118
|
+
analyzeStructure,
|
|
119
|
+
hasAnalyzedStructure,
|
|
120
|
+
generate,
|
|
121
|
+
generateRange,
|
|
122
|
+
clear,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// Utility Functions
|
|
127
|
+
// =============================================================================
|
|
128
|
+
/**
|
|
129
|
+
* Check if an item is a placeholder
|
|
130
|
+
*/
|
|
131
|
+
export const isPlaceholderItem = (item) => {
|
|
132
|
+
if (!item || typeof item !== "object") {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return item[PLACEHOLDER_FLAG] === true;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Filter out placeholder items from an array
|
|
139
|
+
*/
|
|
140
|
+
export const filterPlaceholders = (items) => {
|
|
141
|
+
return items.filter((item) => !isPlaceholderItem(item));
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Count non-placeholder items in an array
|
|
145
|
+
*/
|
|
146
|
+
export const countRealItems = (items) => {
|
|
147
|
+
let count = 0;
|
|
148
|
+
for (const item of items) {
|
|
149
|
+
if (item !== undefined && !isPlaceholderItem(item)) {
|
|
150
|
+
count++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return count;
|
|
154
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist v2 — Async Plugin
|
|
3
|
+
*
|
|
4
|
+
* Enables async data loading with sparse storage, placeholders, and infinite scroll.
|
|
5
|
+
* Priority 20 — runs before scrollbar and selection, after layout plugins.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Replaces default data source with async adapter-backed storage
|
|
9
|
+
* - Lazy-loads data in chunks on scroll + idle detection
|
|
10
|
+
* - Shows placeholders for unloaded items
|
|
11
|
+
* - Deduplicates concurrent fetch requests
|
|
12
|
+
* - Velocity-aware loading: skips loads during fast scrolling, loads on idle
|
|
13
|
+
* - Infinite scroll: loads next page when scrolling near bottom
|
|
14
|
+
* - Public methods: reload(), loadVisibleRange()
|
|
15
|
+
* - Emits: load:start, load:end, error events
|
|
16
|
+
*/
|
|
17
|
+
import type { VListItem, VListAdapter } from "../../types";
|
|
18
|
+
import type { VListPlugin } from "../../core/types";
|
|
19
|
+
export interface DataPluginConfig<T extends VListItem = VListItem> {
|
|
20
|
+
/** Async data source (required) */
|
|
21
|
+
adapter: VListAdapter<T>;
|
|
22
|
+
/** Total number of items (optional - if not provided, adapter must return it) */
|
|
23
|
+
total?: number;
|
|
24
|
+
/** Whether to automatically load initial data (default: true) */
|
|
25
|
+
autoLoad?: boolean;
|
|
26
|
+
/** Storage configuration */
|
|
27
|
+
storage?: {
|
|
28
|
+
/** Number of items per chunk (default: 100) */
|
|
29
|
+
chunkSize?: number;
|
|
30
|
+
/** Maximum cached items before eviction (default: 10000) */
|
|
31
|
+
maxCachedItems?: number;
|
|
32
|
+
/** Extra items to keep around visible range (default: 500) */
|
|
33
|
+
evictionBuffer?: number;
|
|
34
|
+
};
|
|
35
|
+
/** Loading behavior configuration */
|
|
36
|
+
loading?: {
|
|
37
|
+
/** Velocity threshold above which data loading is cancelled (px/ms) */
|
|
38
|
+
cancelThreshold?: number;
|
|
39
|
+
/** Velocity threshold for preloading (px/ms) */
|
|
40
|
+
preloadThreshold?: number;
|
|
41
|
+
/** Number of items to preload in scroll direction */
|
|
42
|
+
preloadAhead?: number;
|
|
43
|
+
/** Maximum concurrent chunk requests (0 = unlimited, default: 6) */
|
|
44
|
+
maxConcurrent?: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export declare function data<T extends VListItem = VListItem>(config: DataPluginConfig<T>): VListPlugin<T>;
|
|
48
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist v2 — Async Plugin
|
|
3
|
+
*
|
|
4
|
+
* Enables async data loading with sparse storage, placeholders, and infinite scroll.
|
|
5
|
+
* Priority 20 — runs before scrollbar and selection, after layout plugins.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Replaces default data source with async adapter-backed storage
|
|
9
|
+
* - Lazy-loads data in chunks on scroll + idle detection
|
|
10
|
+
* - Shows placeholders for unloaded items
|
|
11
|
+
* - Deduplicates concurrent fetch requests
|
|
12
|
+
* - Velocity-aware loading: skips loads during fast scrolling, loads on idle
|
|
13
|
+
* - Infinite scroll: loads next page when scrolling near bottom
|
|
14
|
+
* - Public methods: reload(), loadVisibleRange()
|
|
15
|
+
* - Emits: load:start, load:end, error events
|
|
16
|
+
*/
|
|
17
|
+
import { createDataManager } from "./manager";
|
|
18
|
+
import { INITIAL_LOAD_SIZE, LOAD_VELOCITY_THRESHOLD, PRELOAD_VELOCITY_THRESHOLD, PRELOAD_AHEAD, MAX_CONCURRENT_LOADS, } from "../../constants";
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Factory
|
|
21
|
+
// =============================================================================
|
|
22
|
+
export function data(config) {
|
|
23
|
+
const { adapter, total, autoLoad = true, storage } = config;
|
|
24
|
+
const cancelThreshold = config.loading?.cancelThreshold ?? LOAD_VELOCITY_THRESHOLD;
|
|
25
|
+
const preloadThreshold = config.loading?.preloadThreshold ?? PRELOAD_VELOCITY_THRESHOLD;
|
|
26
|
+
const preloadAhead = config.loading?.preloadAhead ?? PRELOAD_AHEAD;
|
|
27
|
+
const maxConcurrent = config.loading?.maxConcurrent ?? MAX_CONCURRENT_LOADS;
|
|
28
|
+
let dataManager;
|
|
29
|
+
let engineState;
|
|
30
|
+
let sizeCache;
|
|
31
|
+
let emitter;
|
|
32
|
+
let dom;
|
|
33
|
+
let forceRender;
|
|
34
|
+
let pendingRange = null;
|
|
35
|
+
let decelerationTimer = null;
|
|
36
|
+
let idleTimer = null;
|
|
37
|
+
let currentVelocity = 0;
|
|
38
|
+
let autoLoadCancelled = false;
|
|
39
|
+
// Track last requested chunk range to skip redundant ensure() calls
|
|
40
|
+
let lastFirstChunk = -1;
|
|
41
|
+
let lastLastChunk = -1;
|
|
42
|
+
const chunkSize = storage?.chunkSize ?? INITIAL_LOAD_SIZE;
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Helpers
|
|
45
|
+
// ============================================================================
|
|
46
|
+
const resetDeceleration = () => {
|
|
47
|
+
pendingRange = null;
|
|
48
|
+
if (decelerationTimer !== null) {
|
|
49
|
+
clearTimeout(decelerationTimer);
|
|
50
|
+
decelerationTimer = null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const ensure = (start, end) => {
|
|
54
|
+
return dataManager.ensureRange(start, end);
|
|
55
|
+
};
|
|
56
|
+
const emitLoadStart = (offset, limit = INITIAL_LOAD_SIZE) => {
|
|
57
|
+
emitter.emit("load:start", { offset, limit });
|
|
58
|
+
};
|
|
59
|
+
const onEnsureError = (error) => {
|
|
60
|
+
emitter.emit("error", { error, context: "ensureRange" });
|
|
61
|
+
};
|
|
62
|
+
const loadPendingRange = () => {
|
|
63
|
+
if (!pendingRange) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
pendingRange = null;
|
|
67
|
+
const currentRange = {
|
|
68
|
+
start: engineState.startIndex,
|
|
69
|
+
end: engineState.startIndex + Math.max(0, engineState.visibleCount - 1),
|
|
70
|
+
};
|
|
71
|
+
if (currentRange.end < currentRange.start) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
ensure(currentRange.start, currentRange.end).catch(onEnsureError);
|
|
75
|
+
};
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Plugin Definition
|
|
78
|
+
// ============================================================================
|
|
79
|
+
return {
|
|
80
|
+
name: "data",
|
|
81
|
+
priority: 20,
|
|
82
|
+
setup(ctx) {
|
|
83
|
+
engineState = ctx.getState();
|
|
84
|
+
sizeCache = ctx.sizeCache;
|
|
85
|
+
emitter = ctx.emitter;
|
|
86
|
+
dom = ctx.dom;
|
|
87
|
+
forceRender = ctx.forceRender.bind(ctx);
|
|
88
|
+
// Create data manager — but first wire up virtualTotalFn
|
|
89
|
+
// so scrollToIndex and api.total reflect the async data total
|
|
90
|
+
let dataManagerRef = null;
|
|
91
|
+
ctx.setVirtualTotalFn(() => dataManagerRef?.getTotal() ?? 0);
|
|
92
|
+
dataManager = dataManagerRef = createDataManager({
|
|
93
|
+
adapter,
|
|
94
|
+
...(total !== undefined && { initialTotal: total }),
|
|
95
|
+
pageSize: storage?.chunkSize ?? INITIAL_LOAD_SIZE,
|
|
96
|
+
maxConcurrent,
|
|
97
|
+
...(storage && {
|
|
98
|
+
storage: {
|
|
99
|
+
...(storage.chunkSize !== undefined && { chunkSize: storage.chunkSize }),
|
|
100
|
+
...(storage.maxCachedItems !== undefined && { maxCachedItems: storage.maxCachedItems }),
|
|
101
|
+
...(storage.evictionBuffer !== undefined && { evictionBuffer: storage.evictionBuffer }),
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
onDataChange: () => {
|
|
105
|
+
if (engineState.initialized) {
|
|
106
|
+
const newTotal = dataManager.getTotal();
|
|
107
|
+
engineState.totalItems = newTotal;
|
|
108
|
+
const oldTotal = sizeCache.getTotal();
|
|
109
|
+
if (newTotal !== oldTotal) {
|
|
110
|
+
sizeCache.rebuild(newTotal);
|
|
111
|
+
}
|
|
112
|
+
ctx.updateContentSize(sizeCache.getTotalSize());
|
|
113
|
+
ctx.renderIfNeeded();
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
onItemsLoaded: (loadedItems) => {
|
|
117
|
+
if (engineState.initialized) {
|
|
118
|
+
forceRender();
|
|
119
|
+
emitter.emit("load:end", { items: loadedItems, total: dataManager.getTotal() });
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
// Bridge async data manager to the render pipeline
|
|
124
|
+
ctx.setGetItemFn((index) => dataManager.getItem(index));
|
|
125
|
+
ctx.setRemoveItemFn((id) => {
|
|
126
|
+
const index = dataManager.getIndexById(id);
|
|
127
|
+
if (index < 0)
|
|
128
|
+
return -1;
|
|
129
|
+
const removed = dataManager.removeItem(id);
|
|
130
|
+
if (!removed)
|
|
131
|
+
return -1;
|
|
132
|
+
return index;
|
|
133
|
+
});
|
|
134
|
+
ctx.setInsertItemFn((item, index) => {
|
|
135
|
+
dataManager.insertItem(item, index);
|
|
136
|
+
});
|
|
137
|
+
ctx.setUpdateItemFn((id, updates) => {
|
|
138
|
+
const index = dataManager.getIndexById(id);
|
|
139
|
+
if (index < 0)
|
|
140
|
+
return false;
|
|
141
|
+
return dataManager.updateItem(index, updates);
|
|
142
|
+
});
|
|
143
|
+
// Register public methods
|
|
144
|
+
ctx.registerMethod("reload", async () => {
|
|
145
|
+
pendingRange = null;
|
|
146
|
+
lastFirstChunk = -1;
|
|
147
|
+
lastLastChunk = -1;
|
|
148
|
+
ctx.forceRender();
|
|
149
|
+
await dataManager.reload();
|
|
150
|
+
ctx.scrollTo(0);
|
|
151
|
+
if (autoLoad) {
|
|
152
|
+
emitLoadStart(0);
|
|
153
|
+
await dataManager.loadInitial();
|
|
154
|
+
ctx.forceRender();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
ctx.registerMethod("loadVisibleRange", async () => {
|
|
158
|
+
pendingRange = null;
|
|
159
|
+
ctx.forceRender();
|
|
160
|
+
const total = dataManager.getTotal();
|
|
161
|
+
if (engineState.visibleCount > 0 && engineState.startIndex < total) {
|
|
162
|
+
const end = Math.min(engineState.startIndex + engineState.visibleCount - 1, total - 1);
|
|
163
|
+
emitLoadStart(engineState.startIndex, end - engineState.startIndex + 1);
|
|
164
|
+
await ensure(engineState.startIndex, end);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
ctx.registerMethod("getTotal", () => {
|
|
168
|
+
return dataManager.getTotal();
|
|
169
|
+
});
|
|
170
|
+
ctx.registerMethod("setTotal", (total) => {
|
|
171
|
+
dataManager.setTotal(total);
|
|
172
|
+
});
|
|
173
|
+
ctx.registerMethod("_getTotal", () => dataManager.getTotal());
|
|
174
|
+
ctx.registerMethod("_setTotal", (t) => {
|
|
175
|
+
dataManager.setTotal(t);
|
|
176
|
+
});
|
|
177
|
+
ctx.registerMethod("_cancelAutoLoad", () => {
|
|
178
|
+
autoLoadCancelled = true;
|
|
179
|
+
});
|
|
180
|
+
ctx.registerMethod("_getLoadedItem", (index) => {
|
|
181
|
+
return dataManager.getStorage().get(index);
|
|
182
|
+
});
|
|
183
|
+
ctx.registerMethod("_getLoadedCount", () => dataManager.getCached());
|
|
184
|
+
// ARIA: aria-busy for loading state
|
|
185
|
+
emitter.on("load:start", () => {
|
|
186
|
+
dom.root.setAttribute("aria-busy", "true");
|
|
187
|
+
});
|
|
188
|
+
emitter.on("load:end", () => {
|
|
189
|
+
dom.root.removeAttribute("aria-busy");
|
|
190
|
+
});
|
|
191
|
+
ctx.registerDestroyHandler(() => {
|
|
192
|
+
if (idleTimer !== null) {
|
|
193
|
+
clearTimeout(idleTimer);
|
|
194
|
+
idleTimer = null;
|
|
195
|
+
}
|
|
196
|
+
resetDeceleration();
|
|
197
|
+
});
|
|
198
|
+
// Track velocity for load gating
|
|
199
|
+
emitter.on("velocity:change", ({ velocity }) => {
|
|
200
|
+
currentVelocity = Math.abs(velocity);
|
|
201
|
+
});
|
|
202
|
+
// Network recovery
|
|
203
|
+
const handleOnline = () => {
|
|
204
|
+
if (engineState.destroyed)
|
|
205
|
+
return;
|
|
206
|
+
if (engineState.visibleCount > 0 && engineState.startIndex < dataManager.getTotal()) {
|
|
207
|
+
const end = Math.min(engineState.startIndex + engineState.visibleCount - 1, dataManager.getTotal() - 1);
|
|
208
|
+
ensure(engineState.startIndex, end).catch(onEnsureError);
|
|
209
|
+
}
|
|
210
|
+
loadPendingRange();
|
|
211
|
+
};
|
|
212
|
+
window.addEventListener("online", handleOnline);
|
|
213
|
+
ctx.registerDestroyHandler(() => {
|
|
214
|
+
window.removeEventListener("online", handleOnline);
|
|
215
|
+
});
|
|
216
|
+
// Load initial data (if autoLoad is enabled)
|
|
217
|
+
if (autoLoad) {
|
|
218
|
+
queueMicrotask(() => {
|
|
219
|
+
if (autoLoadCancelled)
|
|
220
|
+
return;
|
|
221
|
+
emitLoadStart(0);
|
|
222
|
+
dataManager.loadInitial().catch((error) => {
|
|
223
|
+
emitter.emit("error", { error, context: "loadInitial" });
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else if (total !== undefined) {
|
|
228
|
+
dataManager.setTotal(total);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
hooks: {
|
|
232
|
+
onAfterScroll() {
|
|
233
|
+
if (engineState.destroyed)
|
|
234
|
+
return;
|
|
235
|
+
const visEnd = engineState.startIndex + Math.max(0, engineState.visibleCount - 1);
|
|
236
|
+
// Fast scrolling (above cancelThreshold): skip loading, defer to idle
|
|
237
|
+
if (currentVelocity > cancelThreshold) {
|
|
238
|
+
if (decelerationTimer !== null) {
|
|
239
|
+
clearTimeout(decelerationTimer);
|
|
240
|
+
decelerationTimer = null;
|
|
241
|
+
}
|
|
242
|
+
pendingRange = { start: engineState.startIndex, end: visEnd };
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Moderate scrolling (between preloadThreshold and cancelThreshold):
|
|
246
|
+
// load visible range immediately, debounce preload-ahead only
|
|
247
|
+
if (currentVelocity > preloadThreshold) {
|
|
248
|
+
const fc = Math.floor(engineState.startIndex / chunkSize);
|
|
249
|
+
const lc = Math.floor(visEnd / chunkSize);
|
|
250
|
+
if (fc !== lastFirstChunk || lc !== lastLastChunk) {
|
|
251
|
+
lastFirstChunk = fc;
|
|
252
|
+
lastLastChunk = lc;
|
|
253
|
+
if (visEnd >= engineState.startIndex) {
|
|
254
|
+
ensure(engineState.startIndex, visEnd).catch(onEnsureError);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
let loadStart = engineState.startIndex;
|
|
258
|
+
let loadEnd = visEnd;
|
|
259
|
+
const dir = engineState.scrollDirection;
|
|
260
|
+
if (dir > 0) {
|
|
261
|
+
loadEnd = Math.min(loadEnd + preloadAhead, dataManager.getTotal() - 1);
|
|
262
|
+
}
|
|
263
|
+
else if (dir < 0) {
|
|
264
|
+
loadStart = Math.max(0, loadStart - preloadAhead);
|
|
265
|
+
}
|
|
266
|
+
pendingRange = { start: loadStart, end: loadEnd };
|
|
267
|
+
if (decelerationTimer !== null) {
|
|
268
|
+
clearTimeout(decelerationTimer);
|
|
269
|
+
}
|
|
270
|
+
decelerationTimer = setTimeout(() => {
|
|
271
|
+
decelerationTimer = null;
|
|
272
|
+
if (engineState.destroyed || !pendingRange)
|
|
273
|
+
return;
|
|
274
|
+
const { start, end } = pendingRange;
|
|
275
|
+
pendingRange = null;
|
|
276
|
+
if (end >= start) {
|
|
277
|
+
ensure(start, end).catch(onEnsureError);
|
|
278
|
+
}
|
|
279
|
+
}, 100);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// Slow scrolling (below preloadThreshold): load visible range immediately
|
|
283
|
+
resetDeceleration();
|
|
284
|
+
const fc = Math.floor(engineState.startIndex / chunkSize);
|
|
285
|
+
const lc = Math.floor(visEnd / chunkSize);
|
|
286
|
+
if (fc !== lastFirstChunk || lc !== lastLastChunk) {
|
|
287
|
+
lastFirstChunk = fc;
|
|
288
|
+
lastLastChunk = lc;
|
|
289
|
+
if (visEnd >= engineState.startIndex) {
|
|
290
|
+
ensure(engineState.startIndex, visEnd).catch(onEnsureError);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
onIdle() {
|
|
295
|
+
if (engineState.destroyed)
|
|
296
|
+
return;
|
|
297
|
+
currentVelocity = 0;
|
|
298
|
+
lastFirstChunk = -1;
|
|
299
|
+
lastLastChunk = -1;
|
|
300
|
+
loadPendingRange();
|
|
301
|
+
resetDeceleration();
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
destroy() {
|
|
305
|
+
if (idleTimer !== null) {
|
|
306
|
+
clearTimeout(idleTimer);
|
|
307
|
+
}
|
|
308
|
+
resetDeceleration();
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|