@humanspeak/svelte-virtual-list 0.2.4 → 0.2.6

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.
@@ -71,7 +71,6 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
71
71
  * @param {HTMLElement[]} itemElements - Array of currently rendered item elements
72
72
  * @param {{ start: number }} visibleRange - Object containing the start index of visible items
73
73
  * @param {Record<number, number>} heightCache - Cache of previously measured item heights
74
- * @param {number} lastMeasuredIndex - Index of the last measured item
75
74
  * @param {number} currentItemHeight - Current average item height being used
76
75
  *
77
76
  * @returns {{
@@ -85,13 +84,12 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
85
84
  * itemElements,
86
85
  * { start: 0 },
87
86
  * {},
88
- * -1,
89
87
  * 40
90
88
  * )
91
89
  */
92
90
  export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
93
91
  start: number;
94
- }, heightCache: Record<number, number>, lastMeasuredIndex: number, currentItemHeight: number) => {
92
+ }, heightCache: Record<number, number>, currentItemHeight: number) => {
95
93
  newHeight: number;
96
94
  newLastMeasuredIndex: number;
97
95
  updatedHeightCache: Record<number, number>;
@@ -120,4 +118,39 @@ export declare const calculateAverageHeight: (itemElements: HTMLElement[], visib
120
118
  * () => console.log('All items processed')
121
119
  * )
122
120
  */
123
- export declare const processChunked: (items: any[], chunkSize: number, onProgress: (processed: number) => void, onComplete: () => void) => Promise<void>;
121
+ export declare const processChunked: (items: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
122
+ chunkSize: number, onProgress: (processed: number) => void, // eslint-disable-line no-unused-vars
123
+ onComplete: () => void) => Promise<void>;
124
+ /**
125
+ * Builds a block sum array for fast offset calculation in large virtual lists.
126
+ * Each entry in the array is the total height up to the end of that block (exclusive).
127
+ *
128
+ * @param {Record<number, number>} heightCache - Map of measured item heights
129
+ * @param {number} calculatedItemHeight - Estimated height for unmeasured items
130
+ * @param {number} totalItems - Total number of items in the list
131
+ * @param {number} blockSize - Number of items per block
132
+ * @returns {number[]} Array of prefix sums at each block boundary
133
+ */
134
+ export declare const buildBlockSums: (heightCache: Record<number, number>, calculatedItemHeight: number, totalItems: number, blockSize?: number) => number[];
135
+ /**
136
+ * Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
137
+ *
138
+ * Uses block memoization for efficient O(b) offset calculation, where b = block size (default 1000).
139
+ * For very large lists, this avoids O(n) iteration for every scroll.
140
+ *
141
+ * - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
142
+ * - For small indices, falls back to the original logic.
143
+ *
144
+ * @param {Record<number, number>} heightCache - Map of measured item heights
145
+ * @param {number} calculatedItemHeight - Estimated height for unmeasured items
146
+ * @param {number} idx - The index to scroll to (exclusive)
147
+ * @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
148
+ * @param {number} [blockSize=1000] - Block size for memoization
149
+ * @returns {number} The total offset in pixels from the top of the list to the start of the item at idx.
150
+ *
151
+ * @example
152
+ * // For best performance with repeated queries:
153
+ * const blockSums = buildBlockSums(heightCache, calculatedItemHeight, items.length);
154
+ * const offset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, 12345, blockSums);
155
+ */
156
+ export declare const getScrollOffsetForIndex: (heightCache: Record<number, number>, calculatedItemHeight: number, idx: number, blockSums?: number[], blockSize?: number) => number;
@@ -108,7 +108,6 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
108
108
  * @param {HTMLElement[]} itemElements - Array of currently rendered item elements
109
109
  * @param {{ start: number }} visibleRange - Object containing the start index of visible items
110
110
  * @param {Record<number, number>} heightCache - Cache of previously measured item heights
111
- * @param {number} lastMeasuredIndex - Index of the last measured item
112
111
  * @param {number} currentItemHeight - Current average item height being used
113
112
  *
114
113
  * @returns {{
@@ -122,16 +121,15 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
122
121
  * itemElements,
123
122
  * { start: 0 },
124
123
  * {},
125
- * -1,
126
124
  * 40
127
125
  * )
128
126
  */
129
- export const calculateAverageHeight = (itemElements, visibleRange, heightCache, lastMeasuredIndex, currentItemHeight) => {
127
+ export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight) => {
130
128
  const validElements = itemElements.filter((el) => el);
131
129
  if (validElements.length === 0) {
132
130
  return {
133
131
  newHeight: currentItemHeight,
134
- newLastMeasuredIndex: lastMeasuredIndex,
132
+ newLastMeasuredIndex: visibleRange.start,
135
133
  updatedHeightCache: heightCache
136
134
  };
137
135
  }
@@ -140,14 +138,23 @@ export const calculateAverageHeight = (itemElements, visibleRange, heightCache,
140
138
  validElements.forEach((el, i) => {
141
139
  const itemIndex = visibleRange.start + i;
142
140
  if (!newHeightCache[itemIndex]) {
143
- newHeightCache[itemIndex] = el.getBoundingClientRect().height;
141
+ try {
142
+ const height = el.getBoundingClientRect().height;
143
+ if (Number.isFinite(height) && height > 0) {
144
+ newHeightCache[itemIndex] = height;
145
+ }
146
+ }
147
+ catch {
148
+ // Skip invalid measurements
149
+ }
144
150
  }
145
151
  });
146
- // Calculate average from cached heights
147
- const heights = Object.values(newHeightCache);
148
- const averageHeight = heights.reduce((sum, h) => sum + h, 0) / heights.length;
152
+ // Calculate average from valid cached heights
153
+ const validHeights = Object.values(newHeightCache).filter((h) => Number.isFinite(h) && h > 0);
149
154
  return {
150
- newHeight: averageHeight > 0 && !isNaN(averageHeight) ? averageHeight : currentItemHeight,
155
+ newHeight: validHeights.length > 0
156
+ ? validHeights.reduce((sum, h) => sum + h, 0) / validHeights.length
157
+ : currentItemHeight,
151
158
  newLastMeasuredIndex: visibleRange.start,
152
159
  updatedHeightCache: newHeightCache
153
160
  };
@@ -195,3 +202,68 @@ onComplete) => {
195
202
  };
196
203
  await processChunk(0);
197
204
  };
205
+ /**
206
+ * Builds a block sum array for fast offset calculation in large virtual lists.
207
+ * Each entry in the array is the total height up to the end of that block (exclusive).
208
+ *
209
+ * @param {Record<number, number>} heightCache - Map of measured item heights
210
+ * @param {number} calculatedItemHeight - Estimated height for unmeasured items
211
+ * @param {number} totalItems - Total number of items in the list
212
+ * @param {number} blockSize - Number of items per block
213
+ * @returns {number[]} Array of prefix sums at each block boundary
214
+ */
215
+ export const buildBlockSums = (heightCache, calculatedItemHeight, totalItems, blockSize = 1000) => {
216
+ const blockSums = [];
217
+ let sum = 0;
218
+ for (let i = 0; i < totalItems; i++) {
219
+ sum += heightCache[i] ?? calculatedItemHeight;
220
+ if ((i + 1) % blockSize === 0) {
221
+ blockSums.push(sum);
222
+ }
223
+ }
224
+ // Push the last partial block if needed
225
+ if (totalItems % blockSize !== 0) {
226
+ blockSums.push(sum);
227
+ }
228
+ return blockSums;
229
+ };
230
+ /**
231
+ * Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
232
+ *
233
+ * Uses block memoization for efficient O(b) offset calculation, where b = block size (default 1000).
234
+ * For very large lists, this avoids O(n) iteration for every scroll.
235
+ *
236
+ * - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
237
+ * - For small indices, falls back to the original logic.
238
+ *
239
+ * @param {Record<number, number>} heightCache - Map of measured item heights
240
+ * @param {number} calculatedItemHeight - Estimated height for unmeasured items
241
+ * @param {number} idx - The index to scroll to (exclusive)
242
+ * @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
243
+ * @param {number} [blockSize=1000] - Block size for memoization
244
+ * @returns {number} The total offset in pixels from the top of the list to the start of the item at idx.
245
+ *
246
+ * @example
247
+ * // For best performance with repeated queries:
248
+ * const blockSums = buildBlockSums(heightCache, calculatedItemHeight, items.length);
249
+ * const offset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, 12345, blockSums);
250
+ */
251
+ export const getScrollOffsetForIndex = (heightCache, calculatedItemHeight, idx, blockSums, blockSize = 1000) => {
252
+ if (idx <= 0)
253
+ return 0;
254
+ if (!blockSums) {
255
+ // Fallback: O(n) for a single query
256
+ let offset = 0;
257
+ for (let i = 0; i < idx; i++) {
258
+ offset += heightCache[i] ?? calculatedItemHeight;
259
+ }
260
+ return offset;
261
+ }
262
+ const blockIdx = Math.floor(idx / blockSize);
263
+ let offset = blockIdx > 0 ? blockSums[blockIdx - 1] : 0;
264
+ const start = blockIdx * blockSize;
265
+ for (let i = start; i < idx; i++) {
266
+ offset += heightCache[i] ?? calculatedItemHeight;
267
+ }
268
+ return offset;
269
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-virtual-list",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "A lightweight, high-performance virtual list component for Svelte 5 that renders large datasets with minimal memory usage. Features include dynamic height support, smooth scrolling, TypeScript support, and efficient DOM recycling. Ideal for infinite scrolling lists, data tables, chat interfaces, and any application requiring the rendering of thousands of items without compromising performance. Zero dependencies and fully customizable.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -43,7 +43,8 @@
43
43
  "files": [
44
44
  "dist",
45
45
  "!dist/**/*.test.*",
46
- "!dist/**/*.spec.*"
46
+ "!dist/**/*.spec.*",
47
+ "!dist/test/**/*"
47
48
  ],
48
49
  "scripts": {
49
50
  "build": "vite build && npm run package",
@@ -65,52 +66,59 @@
65
66
  "test:only": "vitest run",
66
67
  "test:watch": "vitest"
67
68
  },
69
+ "overrides": {
70
+ "@sveltejs/kit": {
71
+ "cookie": "^0.7.0"
72
+ }
73
+ },
68
74
  "dependencies": {
69
75
  "esm-env": "^1.2.2"
70
76
  },
71
77
  "devDependencies": {
72
- "@eslint/js": "^9.18.0",
73
- "@playwright/test": "^1.49.1",
74
- "@sveltejs/adapter-auto": "^4.0.0",
75
- "@sveltejs/kit": "^2.16.1",
76
- "@sveltejs/package": "^2.3.8",
77
- "@sveltejs/vite-plugin-svelte": "^5.0.3",
78
+ "@eslint/compat": "^1.3.1",
79
+ "@eslint/js": "^9.29.0",
80
+ "@faker-js/faker": "^9.8.0",
81
+ "@playwright/test": "^1.53.1",
82
+ "@sveltejs/adapter-auto": "^6.0.1",
83
+ "@sveltejs/kit": "^2.22.2",
84
+ "@sveltejs/package": "^2.3.12",
85
+ "@sveltejs/vite-plugin-svelte": "^5.1.0",
78
86
  "@testing-library/jest-dom": "^6.6.3",
79
- "@testing-library/svelte": "^5.2.6",
80
- "@testing-library/user-event": "^14.6.0",
81
- "@types/node": "^22.10.7",
82
- "@typescript-eslint/eslint-plugin": "^8.21.0",
83
- "@typescript-eslint/parser": "^8.21.0",
84
- "@vitest/coverage-v8": "^3.0.3",
85
- "eslint": "^9.18.0",
86
- "eslint-config-prettier": "^10.0.1",
87
- "eslint-plugin-svelte": "^2.46.1",
88
- "globals": "^15.14.0",
89
- "jsdom": "^26.0.0",
90
- "prettier": "^3.4.2",
87
+ "@testing-library/svelte": "^5.2.8",
88
+ "@testing-library/user-event": "^14.6.1",
89
+ "@types/node": "^24.0.4",
90
+ "@typescript-eslint/eslint-plugin": "^8.35.0",
91
+ "@typescript-eslint/parser": "^8.35.0",
92
+ "@vitest/coverage-v8": "^3.2.4",
93
+ "eslint": "^9.29.0",
94
+ "eslint-config-prettier": "^10.1.5",
95
+ "eslint-plugin-import": "^2.32.0",
96
+ "eslint-plugin-svelte": "^3.10.0",
97
+ "eslint-plugin-unused-imports": "^4.1.4",
98
+ "globals": "^16.2.0",
99
+ "jsdom": "^26.1.0",
100
+ "prettier": "^3.6.1",
91
101
  "prettier-plugin-organize-imports": "^4.1.0",
92
- "prettier-plugin-svelte": "^3.3.3",
93
- "publint": "^0.3.2",
94
- "svelte": "^5.19.1",
95
- "svelte-check": "^4.1.4",
96
- "typescript": "^5.7.3",
97
- "vite": "^6.0.11",
98
- "vitest": "^3.0.3"
102
+ "prettier-plugin-sort-json": "^4.1.1",
103
+ "prettier-plugin-svelte": "^3.4.0",
104
+ "prettier-plugin-tailwindcss": "^0.6.13",
105
+ "publint": "^0.3.12",
106
+ "svelte": "^5.34.8",
107
+ "svelte-check": "^4.2.2",
108
+ "typescript": "^5.8.3",
109
+ "typescript-eslint": "^8.35.0",
110
+ "vite": "^6.3.5",
111
+ "vitest": "^3.2.4"
99
112
  },
100
113
  "peerDependencies": {
101
114
  "svelte": "^5.0.0"
102
115
  },
103
116
  "volta": {
104
- "node": "22.13.0"
117
+ "node": "22.16.0"
105
118
  },
106
119
  "publishConfig": {
107
120
  "access": "public"
108
121
  },
109
- "overrides": {
110
- "@sveltejs/kit": {
111
- "cookie": "^0.7.0"
112
- }
113
- },
114
122
  "tags": [
115
123
  "svelte",
116
124
  "virtual-list",