@humanspeak/svelte-headless-table 6.0.4 → 6.0.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.
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2024-2025 Humanspeak, Inc.
1
+ Copyright (c) 2024-2026 Humanspeak, Inc.
2
2
 
3
3
  Copyright (c) 2022-2024 Bryan Lee
4
4
 
package/dist/bodyRows.js CHANGED
@@ -242,17 +242,12 @@ export const getColumnedBodyRows = (rows, columnIdOrder) => {
242
242
  clonedCell.row = columnedRows[rowIdx];
243
243
  return clonedCell;
244
244
  });
245
- const visibleCells = columnIdOrder
246
- .map((cid) => {
247
- return cells.find((c) => c.id === cid);
248
- })
249
- .filter(nonUndefined);
245
+ const cellById = new Map(cells.map((c) => [c.id, c]));
246
+ const visibleCells = columnIdOrder.map((cid) => cellById.get(cid)).filter(nonUndefined);
250
247
  columnedRows[rowIdx].cells = visibleCells;
251
248
  // Include hidden cells in `cellForId` to allow row transformations on
252
249
  // hidden cells.
253
- cells.forEach((cell) => {
254
- columnedRows[rowIdx].cellForId[cell.id] = cell;
255
- });
250
+ columnedRows[rowIdx].cellForId = Object.fromEntries(cellById);
256
251
  });
257
252
  return columnedRows;
258
253
  };
@@ -162,27 +162,22 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
162
162
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
163
  rows = fn(rows);
164
164
  });
165
+ const pluginEntries = Object.entries(pluginInstances);
165
166
  const injectedRows = derived(rows, ($rows) => {
166
167
  derivationCalls.injectedRows++;
167
- // Inject state.
168
168
  $rows.forEach((row) => {
169
169
  row.injectState(tableState);
170
- row.cells.forEach((cell) => {
171
- cell.injectState(tableState);
172
- });
173
- });
174
- // Apply plugin component hooks.
175
- Object.entries(pluginInstances).forEach(([pluginName, pluginInstance]) => {
176
- $rows.forEach((row) => {
177
- if (pluginInstance.hooks?.['tbody.tr'] !== undefined) {
178
- row.applyHook(pluginName, pluginInstance.hooks['tbody.tr'](row));
170
+ row.cells.forEach((cell) => cell.injectState(tableState));
171
+ for (const [pluginName, pluginInstance] of pluginEntries) {
172
+ const trHook = pluginInstance.hooks?.['tbody.tr'];
173
+ if (trHook !== undefined) {
174
+ row.applyHook(pluginName, trHook(row));
179
175
  }
180
- row.cells.forEach((cell) => {
181
- if (pluginInstance.hooks?.['tbody.tr.td'] !== undefined) {
182
- cell.applyHook(pluginName, pluginInstance.hooks['tbody.tr.td'](cell));
183
- }
184
- });
185
- });
176
+ const tdHook = pluginInstance.hooks?.['tbody.tr.td'];
177
+ if (tdHook !== undefined) {
178
+ row.cells.forEach((cell) => cell.applyHook(pluginName, tdHook(cell)));
179
+ }
180
+ }
186
181
  });
187
182
  _rows.set($rows);
188
183
  return $rows;
@@ -196,53 +191,29 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
196
191
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
197
192
  pageRows = fn(pageRows);
198
193
  });
194
+ // Page rows are a subset of the same object references already processed
195
+ // by injectedRows — no need to re-inject state or re-apply hooks.
199
196
  const injectedPageRows = derived(pageRows, ($pageRows) => {
200
197
  derivationCalls.injectedPageRows++;
201
- // Inject state.
202
- $pageRows.forEach((row) => {
203
- row.injectState(tableState);
204
- row.cells.forEach((cell) => {
205
- cell.injectState(tableState);
206
- });
207
- });
208
- // Apply plugin component hooks.
209
- Object.entries(pluginInstances).forEach(([pluginName, pluginInstance]) => {
210
- $pageRows.forEach((row) => {
211
- if (pluginInstance.hooks?.['tbody.tr'] !== undefined) {
212
- row.applyHook(pluginName, pluginInstance.hooks['tbody.tr'](row));
213
- }
214
- row.cells.forEach((cell) => {
215
- if (pluginInstance.hooks?.['tbody.tr.td'] !== undefined) {
216
- cell.applyHook(pluginName, pluginInstance.hooks['tbody.tr.td'](cell));
217
- }
218
- });
219
- });
220
- });
221
198
  _pageRows.set($pageRows);
222
199
  return $pageRows;
223
200
  });
224
201
  const headerRows = derived(injectedColumns, ($injectedColumns) => {
225
202
  derivationCalls.headerRows++;
226
203
  const $headerRows = getHeaderRows(columns, $injectedColumns.map((c) => c.id));
227
- // Inject state.
228
204
  $headerRows.forEach((row) => {
229
205
  row.injectState(tableState);
230
- row.cells.forEach((cell) => {
231
- cell.injectState(tableState);
232
- });
233
- });
234
- // Apply plugin component hooks.
235
- Object.entries(pluginInstances).forEach(([pluginName, pluginInstance]) => {
236
- $headerRows.forEach((row) => {
237
- if (pluginInstance.hooks?.['thead.tr'] !== undefined) {
238
- row.applyHook(pluginName, pluginInstance.hooks['thead.tr'](row));
206
+ row.cells.forEach((cell) => cell.injectState(tableState));
207
+ for (const [pluginName, pluginInstance] of pluginEntries) {
208
+ const trHook = pluginInstance.hooks?.['thead.tr'];
209
+ if (trHook !== undefined) {
210
+ row.applyHook(pluginName, trHook(row));
239
211
  }
240
- row.cells.forEach((cell) => {
241
- if (pluginInstance.hooks?.['thead.tr.th'] !== undefined) {
242
- cell.applyHook(pluginName, pluginInstance.hooks['thead.tr.th'](cell));
243
- }
244
- });
245
- });
212
+ const thHook = pluginInstance.hooks?.['thead.tr.th'];
213
+ if (thHook !== undefined) {
214
+ row.cells.forEach((cell) => cell.applyHook(pluginName, thHook(cell)));
215
+ }
216
+ }
246
217
  });
247
218
  _headerRows.set($headerRows);
248
219
  return $headerRows;
@@ -25,15 +25,20 @@ export const addColumnOrder = ({ initialColumnIdOrder = [], hideUnspecifiedColum
25
25
  const pluginState = { columnIdOrder };
26
26
  const deriveFlatColumns = (flatColumns) => {
27
27
  return derived([flatColumns, columnIdOrder], ([$flatColumns, $columnIdOrder]) => {
28
- const _flatColumns = [...$flatColumns];
28
+ const colById = new Map($flatColumns.map((c) => [c.id, c]));
29
29
  const orderedFlatColumns = [];
30
30
  $columnIdOrder.forEach((id) => {
31
- const colIdx = _flatColumns.findIndex((c) => c.id === id);
32
- orderedFlatColumns.push(..._flatColumns.splice(colIdx, 1));
31
+ const col = colById.get(id);
32
+ if (col !== undefined) {
33
+ orderedFlatColumns.push(col);
34
+ colById.delete(id);
35
+ }
33
36
  });
34
37
  if (!hideUnspecifiedColumns) {
35
- // Push the remaining unspecified columns.
36
- orderedFlatColumns.push(..._flatColumns);
38
+ // Remaining entries preserve original $flatColumns order.
39
+ for (const col of colById.values()) {
40
+ orderedFlatColumns.push(col);
41
+ }
37
42
  }
38
43
  return orderedFlatColumns;
39
44
  });
@@ -59,8 +59,12 @@ export const getGroupedRows = (rows, groupByIds, columnOptions, { repeatCellIds,
59
59
  if (typeof groupOnValue === 'function' || typeof groupOnValue === 'object') {
60
60
  console.warn(`Missing \`getGroupOn\` column option to aggregate column "${groupById}" with object values`);
61
61
  }
62
- const subRows = subRowsForGroupOnValue.get(groupOnValue) ?? [];
63
- subRowsForGroupOnValue.set(groupOnValue, [...subRows, row]);
62
+ let subRows = subRowsForGroupOnValue.get(groupOnValue);
63
+ if (subRows === undefined) {
64
+ subRows = [];
65
+ subRowsForGroupOnValue.set(groupOnValue, subRows);
66
+ }
67
+ subRows.push(row);
64
68
  }
65
69
  const groupedRows = [];
66
70
  let groupRowIdx = 0;
@@ -59,10 +59,18 @@ export const addVirtualScroll = ({ onLoadMore, hasMore: hasMoreConfig, loadMoreT
59
59
  let scrollContainer = null;
60
60
  // Cache for row lookup (set by derivePageRows)
61
61
  let allRowsCache = [];
62
- // Visible range calculation
63
- const visibleRange = derived([rowIds, scrollTop, viewportHeight], ([$rowIds, $scrollTop, $viewportHeight]) => {
64
- return heightManager.getVisibleRange($rowIds, $scrollTop, $viewportHeight, bufferSize);
65
- });
62
+ // Visible range calculation.
63
+ // Return the same object reference when the range hasn't changed to avoid
64
+ // unnecessary downstream store updates (spacer heights, rendered rows).
65
+ let currentRange = { start: 0, end: 0 };
66
+ const visibleRange = derived([rowIds, scrollTop, viewportHeight], ([$rowIds, $scrollTop, $viewportHeight], set) => {
67
+ const range = heightManager.getVisibleRange($rowIds, $scrollTop, $viewportHeight, bufferSize);
68
+ if (range.start === currentRange.start && range.end === currentRange.end) {
69
+ return;
70
+ }
71
+ currentRange = range;
72
+ set(range);
73
+ }, currentRange);
66
74
  // Total height of all rows
67
75
  const totalHeight = derived(rowIds, ($rowIds) => {
68
76
  return heightManager.getTotalHeight($rowIds);
@@ -110,8 +118,7 @@ export const addVirtualScroll = ({ onLoadMore, hasMore: hasMoreConfig, loadMoreT
110
118
  */
111
119
  const handleScroll = (event) => {
112
120
  const target = event.target;
113
- const newScrollTop = target.scrollTop;
114
- scrollTop.set(newScrollTop);
121
+ scrollTop.set(target.scrollTop);
115
122
  checkLoadMore();
116
123
  };
117
124
  /**
@@ -119,6 +126,11 @@ export const addVirtualScroll = ({ onLoadMore, hasMore: hasMoreConfig, loadMoreT
119
126
  */
120
127
  const virtualScroll = (node) => {
121
128
  scrollContainer = node;
129
+ // Disable overflow-anchor to prevent the browser from adjusting
130
+ // scrollTop when spacer heights change. Without this, a feedback
131
+ // loop occurs: spacer change → browser adjusts scrollTop → scroll
132
+ // event → new visible range → spacer change → cascades to bottom.
133
+ node.style.overflowAnchor = 'none';
122
134
  // Set initial viewport height
123
135
  const initialHeight = node.clientHeight;
124
136
  viewportHeight.set(initialHeight);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-headless-table",
3
- "version": "6.0.4",
3
+ "version": "6.0.6",
4
4
  "description": "A powerful, headless table library for Svelte that provides complete control over table UI while handling complex data operations like sorting, filtering, pagination, grouping, and row expansion. Build custom, accessible data tables with zero styling opinions.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -58,47 +58,46 @@
58
58
  "!dist/**/*.spec.*"
59
59
  ],
60
60
  "dependencies": {
61
- "@humanspeak/memory-cache": "^1.0.4",
61
+ "@humanspeak/memory-cache": "^1.0.5",
62
62
  "@humanspeak/svelte-keyed": "^5.0.1",
63
63
  "@humanspeak/svelte-render": "^5.1.1",
64
64
  "@humanspeak/svelte-subscribe": "^5.0.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@eslint/compat": "^2.0.2",
68
- "@eslint/js": "^9.39.2",
69
- "@faker-js/faker": "^10.2.0",
70
- "@playwright/test": "^1.58.1",
71
- "@sveltejs/adapter-auto": "^7.0.0",
72
- "@sveltejs/kit": "^2.50.2",
68
+ "@eslint/js": "^10.0.1",
69
+ "@faker-js/faker": "^10.3.0",
70
+ "@playwright/test": "^1.58.2",
71
+ "@sveltejs/adapter-auto": "^7.0.1",
72
+ "@sveltejs/kit": "^2.53.4",
73
73
  "@sveltejs/package": "^2.5.7",
74
74
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
75
75
  "@testing-library/jest-dom": "^6.9.1",
76
76
  "@testing-library/svelte": "^5.3.1",
77
77
  "@types/eslint": "9.6.1",
78
- "@types/node": "^25.2.0",
79
- "@typescript-eslint/eslint-plugin": "^8.54.0",
80
- "@typescript-eslint/parser": "^8.54.0",
78
+ "@types/node": "^25.3.3",
79
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
80
+ "@typescript-eslint/parser": "^8.56.1",
81
81
  "@vitest/coverage-v8": "^4.0.18",
82
- "concurrently": "^9.2.1",
83
- "eslint": "^9.39.2",
82
+ "eslint": "^10.0.2",
84
83
  "eslint-config-prettier": "10.1.8",
85
84
  "eslint-plugin-import": "2.32.0",
86
- "eslint-plugin-svelte": "3.14.0",
87
- "eslint-plugin-unused-imports": "4.3.0",
88
- "globals": "^17.3.0",
85
+ "eslint-plugin-svelte": "3.15.0",
86
+ "eslint-plugin-unused-imports": "4.4.1",
87
+ "globals": "^17.4.0",
89
88
  "husky": "^9.1.7",
90
89
  "prettier": "^3.8.1",
91
90
  "prettier-plugin-organize-imports": "^4.3.0",
92
91
  "prettier-plugin-sort-json": "^4.2.0",
93
- "prettier-plugin-svelte": "^3.4.1",
92
+ "prettier-plugin-svelte": "^3.5.1",
94
93
  "prettier-plugin-tailwindcss": "^0.7.2",
95
- "publint": "^0.3.17",
96
- "svelte": "^5.49.1",
97
- "svelte-check": "^4.3.6",
94
+ "publint": "^0.3.18",
95
+ "svelte": "^5.53.7",
96
+ "svelte-check": "^4.4.4",
98
97
  "tslib": "^2.8.1",
99
- "type-fest": "^5.4.3",
98
+ "type-fest": "^5.4.4",
100
99
  "typescript": "^5.9.3",
101
- "typescript-eslint": "^8.54.0",
100
+ "typescript-eslint": "^8.56.1",
102
101
  "vite": "^7.3.1",
103
102
  "vitest": "^4.0.18"
104
103
  },
@@ -106,7 +105,7 @@
106
105
  "svelte": "^5.30.0"
107
106
  },
108
107
  "volta": {
109
- "node": "24.12.0"
108
+ "node": "24.14.0"
110
109
  },
111
110
  "scripts": {
112
111
  "build": "vite build && npm run package",
@@ -114,7 +113,7 @@
114
113
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
115
114
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
116
115
  "dev": "vite dev",
117
- "dev:all": "concurrently -k -n pkg,docs,sitemap -c green,cyan,magenta \"pnpm -w -r --filter @humanspeak/svelte-headless-table run dev:pkg\" \"pnpm --filter docs run dev\" \"pnpm --filter docs run sitemap:watch\"",
116
+ "dev:all": "mprocs",
118
117
  "dev:pkg": "svelte-kit sync && svelte-package --watch",
119
118
  "format": "prettier --write .",
120
119
  "lint": "prettier --check . && eslint .",