@svgrid/grid 3.0.7 → 3.0.9

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 (91) hide show
  1. package/README.md +1 -1
  2. package/dist/GridMenus.svelte +3 -2
  3. package/dist/SvGrid.controller.svelte.d.ts +60 -8
  4. package/dist/SvGrid.controller.svelte.js +490 -114
  5. package/dist/SvGrid.css +186 -3
  6. package/dist/SvGrid.svelte +285 -164
  7. package/dist/SvGrid.types.d.ts +24 -7
  8. package/dist/cdn/{GridMenus-CcwU8I4t.js → GridMenus-B1sipj6y.js} +165 -165
  9. package/dist/cdn/{GridMenus-BgUS-W3o.js → GridMenus-DlAzfHLk.js} +201 -201
  10. package/dist/cdn/server-block-cache-Dj-KPqEQ.js +1289 -0
  11. package/dist/cdn/{src-kE9B98r6.js → src-BYsa66I0.js} +9747 -10178
  12. package/dist/cdn/{src-25JmJ_7g.js → src-pA4RroLV.js} +10709 -11140
  13. package/dist/cdn/svgrid.js +27 -27
  14. package/dist/cdn/svgrid.svelte-external.js +27 -27
  15. package/dist/cdn/validate-XuMe4_KR.js +80 -0
  16. package/dist/cell-render.js +25 -3
  17. package/dist/columns.js +16 -1
  18. package/dist/conditional-formatting.js +9 -2
  19. package/dist/core.d.ts +93 -0
  20. package/dist/core.js +313 -47
  21. package/dist/filtering/excel-filters.js +41 -6
  22. package/dist/headless.d.ts +1 -1
  23. package/dist/headless.js +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +2 -1
  26. package/dist/keyboard-handlers.js +5 -1
  27. package/dist/selection.d.ts +2 -2
  28. package/dist/selection.js +46 -12
  29. package/dist/server-block-cache.d.ts +7 -32
  30. package/dist/server-block-cache.js +21 -12
  31. package/dist/server-data-source.js +3 -1
  32. package/dist/server.d.ts +1 -0
  33. package/dist/server.js +1 -0
  34. package/dist/spreadsheet.js +7 -0
  35. package/dist/{subscribe.js → subscribe.svelte.js} +5 -1
  36. package/dist/validate.js +13 -14
  37. package/dist/virtualization/column-virtualizer.d.ts +4 -0
  38. package/dist/virtualization/column-virtualizer.js +2 -0
  39. package/dist/virtualization/types.d.ts +20 -0
  40. package/dist/virtualization/virtualizer.js +90 -24
  41. package/dist/windowed-brand.d.ts +22 -0
  42. package/dist/windowed-brand.js +60 -0
  43. package/dist/windowed-data.d.ts +6 -0
  44. package/dist/windowed-data.js +34 -0
  45. package/dist/windowed-row-model.d.ts +1 -0
  46. package/dist/windowed-row-model.js +97 -0
  47. package/package.json +1 -1
  48. package/src/GridMenus.svelte +3 -2
  49. package/src/SvGrid.controller.svelte.ts +483 -122
  50. package/src/SvGrid.css +186 -3
  51. package/src/SvGrid.svelte +285 -164
  52. package/src/SvGrid.types.ts +24 -7
  53. package/src/cell-render.ts +24 -2
  54. package/src/columns.ts +16 -1
  55. package/src/conditional-formatting.ts +6 -4
  56. package/src/core.sort.test.ts +61 -0
  57. package/src/core.ts +309 -49
  58. package/src/counting-cell.test.svelte +11 -0
  59. package/src/filtering/excel-filters.ts +39 -6
  60. package/src/headless.reactivity.svelte.test.ts +89 -0
  61. package/src/headless.ts +1 -1
  62. package/src/index.ts +7 -1
  63. package/src/keyboard-handlers.ts +5 -1
  64. package/src/new-features.test.ts +24 -0
  65. package/src/selection.test.ts +51 -0
  66. package/src/selection.ts +47 -14
  67. package/src/server-block-cache.ts +27 -10
  68. package/src/server-data-source.ts +3 -1
  69. package/src/server.ts +6 -0
  70. package/src/spreadsheet.ts +6 -0
  71. package/src/{subscribe.test.ts → subscribe.svelte.test.ts} +1 -1
  72. package/src/{subscribe.ts → subscribe.svelte.ts} +5 -1
  73. package/src/svgrid.behavior.test.ts +58 -0
  74. package/src/svgrid.column-cell-recycling.test.ts +120 -0
  75. package/src/svgrid.interaction.test.ts +4 -1
  76. package/src/svgrid.new-features.wrapper.test.ts +5 -4
  77. package/src/svgrid.pinned-column-virtualization.test.ts +162 -0
  78. package/src/svgrid.row-model-prop.svelte.test.ts +15 -3
  79. package/src/validate.test.ts +20 -4
  80. package/src/validate.ts +13 -16
  81. package/src/virtualization/column-virtualizer.ts +6 -0
  82. package/src/virtualization/types.ts +20 -0
  83. package/src/virtualization/virtualizer.test.ts +152 -0
  84. package/src/virtualization/virtualizer.ts +93 -27
  85. package/src/windowed-brand.ts +67 -0
  86. package/src/windowed-data.test.ts +105 -0
  87. package/src/windowed-data.ts +36 -0
  88. package/src/windowed-row-model.ts +113 -0
  89. package/dist/cdn/server-block-cache-DIqb3VZn.js +0 -241
  90. package/dist/cdn/validate-AZoD8BoR.js +0 -80
  91. /package/dist/{subscribe.d.ts → subscribe.svelte.d.ts} +0 -0
@@ -5,6 +5,8 @@ function sameOptions(a, b) {
5
5
  return (a.count === b.count &&
6
6
  a.estimateSize === b.estimateSize &&
7
7
  (a.overscan ?? 6) === (b.overscan ?? 6) &&
8
+ a.overscanBehind === b.overscanBehind &&
9
+ a.overscanMin === b.overscanMin &&
8
10
  a.viewportHeight === b.viewportHeight &&
9
11
  (a.scrollOffset ?? 0) === (b.scrollOffset ?? 0));
10
12
  }
@@ -107,11 +109,12 @@ function buildVariableItems(startIndex, endIndex, offsets) {
107
109
  }
108
110
  return items;
109
111
  }
110
- function createState(options,
112
+ /** The items under the viewport at the current scroll offset, before any
113
+ * overscan. */
114
+ function visibleRange(options,
111
115
  /** Pre-built cumulative offsets for the variable-size path. */
112
116
  offsets) {
113
117
  const count = Math.max(options.count, 0);
114
- const overscan = Math.max(options.overscan ?? 6, 0);
115
118
  const viewportHeight = Math.max(options.viewportHeight, 0);
116
119
  if (typeof options.estimateSize === 'function' && offsets) {
117
120
  const totalSize = offsets[count] ?? 0;
@@ -140,29 +143,47 @@ offsets) {
140
143
  hi = mid;
141
144
  }
142
145
  const visibleEnd = Math.max(lo - 1, visibleStart);
143
- const startIndex = count === 0 ? 0 : clamp(visibleStart - overscan, 0, count - 1);
144
- const endIndex = count === 0 ? -1 : clamp(visibleEnd + overscan, 0, count - 1);
145
- return {
146
- items: endIndex >= startIndex ? buildVariableItems(startIndex, endIndex, offsets) : [],
147
- totalSize,
148
- startIndex,
149
- endIndex,
150
- scrollOffset,
151
- viewportHeight,
152
- };
146
+ return { count, viewportHeight, totalSize, scrollOffset, visibleStart, visibleEnd, uniformSize: 0 };
153
147
  }
154
148
  // Uniform-size fast path (original behavior).
155
- const estimateSize = Math.max(typeof options.estimateSize === 'number' ? options.estimateSize : 1, 1);
156
- const totalSize = count * estimateSize;
149
+ const uniformSize = Math.max(typeof options.estimateSize === 'number' ? options.estimateSize : 1, 1);
150
+ const totalSize = count * uniformSize;
157
151
  const maxOffset = Math.max(totalSize - viewportHeight, 0);
158
152
  const scrollOffset = clamp(options.scrollOffset ?? 0, 0, maxOffset);
159
- const visibleStart = Math.floor(scrollOffset / estimateSize);
160
- const visibleCount = Math.ceil(viewportHeight / estimateSize);
153
+ const visibleStart = Math.floor(scrollOffset / uniformSize);
154
+ const visibleCount = Math.ceil(viewportHeight / uniformSize);
161
155
  const visibleEnd = Math.min(visibleStart + visibleCount, Math.max(count - 1, 0));
162
- const startIndex = count === 0 ? 0 : clamp(visibleStart - overscan, 0, count - 1);
163
- const endIndex = count === 0 ? -1 : clamp(visibleEnd + overscan, 0, count - 1);
156
+ return { count, viewportHeight, totalSize, scrollOffset, visibleStart, visibleEnd, uniformSize };
157
+ }
158
+ function createState(options,
159
+ /** Pre-built cumulative offsets for the variable-size path. */
160
+ offsets,
161
+ /** The scroll moved on from the window already rendered. With
162
+ * `overscanMin`, only then does the full `overscan` go ahead; a window
163
+ * built at rest or after a jump carries `overscanMin`, so a thumb drag
164
+ * that jumps every frame renders no more than it needs. */
165
+ scrolling = false) {
166
+ const full = Math.max(options.overscan ?? 6, 0);
167
+ const overscan = options.overscanMin === undefined || scrolling ? full : clamp(options.overscanMin, 0, full);
168
+ // The overscan goes ahead of the scroll and `overscanBehind` behind it:
169
+ // items behind the movement were just scrolled past, and a horizontal
170
+ // scroll that kept three columns on each side drew ~40% more cells than it
171
+ // needed to (350 vs 275 in the wide benchmark).
172
+ const behind = Math.min(Math.max(options.overscanBehind ?? full, 0), overscan);
173
+ const direction = options.scrollDirection ?? 0;
174
+ const overscanBefore = direction > 0 ? behind : overscan;
175
+ const overscanAfter = direction < 0 ? behind : overscan;
176
+ const { count, viewportHeight, totalSize, scrollOffset, visibleStart, visibleEnd, uniformSize } = visibleRange(options, offsets);
177
+ const startIndex = count === 0 ? 0 : clamp(visibleStart - overscanBefore, 0, count - 1);
178
+ const endIndex = count === 0 ? -1 : clamp(visibleEnd + overscanAfter, 0, count - 1);
179
+ let items = [];
180
+ if (endIndex >= startIndex) {
181
+ items = uniformSize > 0
182
+ ? buildUniformItems(startIndex, endIndex, uniformSize)
183
+ : buildVariableItems(startIndex, endIndex, offsets);
184
+ }
164
185
  return {
165
- items: endIndex >= startIndex ? buildUniformItems(startIndex, endIndex, estimateSize) : [],
186
+ items,
166
187
  totalSize,
167
188
  startIndex,
168
189
  endIndex,
@@ -200,13 +221,54 @@ export function createVirtualizer(initial) {
200
221
  function emit() {
201
222
  listeners.forEach((listener) => listener());
202
223
  }
203
- function recalc() {
204
- const next = createState(options, getOffsets());
224
+ function recalc(scrolling = false) {
225
+ const next = createState(options, getOffsets(), scrolling);
205
226
  if (sameState(state, next))
206
227
  return;
228
+ // Hand back the previous item object for a row whose index, offset, size
229
+ // and slot did not change. The grid's row {#each} is keyed by slot, so a
230
+ // row that stays in the window keeps its <tr>, but a new item object
231
+ // still invalidated everything that row and its cells derive from it:
232
+ // every scroll frame re-checked ~300 cells to render the two that came
233
+ // into view, and Svelte's dependency marking was most of the frame's
234
+ // script time (measured 4.8 of 8.3 ms on a 100k-row grid).
235
+ if (state.items.length > 0 && next.items.length > 0) {
236
+ const first = state.items[0].index;
237
+ const items = next.items;
238
+ for (let i = 0; i < items.length; i += 1) {
239
+ const item = items[i];
240
+ const prev = state.items[item.index - first];
241
+ if (prev && prev.index === item.index && prev.start === item.start && prev.size === item.size && prev.key === item.key) {
242
+ items[i] = prev;
243
+ }
244
+ }
245
+ }
207
246
  state = next;
208
247
  emit();
209
248
  }
249
+ /**
250
+ * What a scroll does to the window already rendered, with `overscanMin`:
251
+ * 'hold' while that window still covers the visible items plus
252
+ * `overscanMin` ahead of the scroll, 'move' once the scroll runs past that
253
+ * (the window moves on with the full overscan ahead), and 'jump' when the
254
+ * visible items are nowhere in it. Without `overscanMin` every scroll moves
255
+ * the window.
256
+ */
257
+ function scrollStep() {
258
+ const min = options.overscanMin;
259
+ if (min === undefined || state.items.length === 0)
260
+ return 'move';
261
+ const ahead = clamp(min, 0, Math.max(options.overscan ?? 6, 0));
262
+ const { count, viewportHeight, totalSize, visibleStart, visibleEnd } = visibleRange(options, getOffsets());
263
+ if (count === 0 || viewportHeight !== state.viewportHeight || totalSize !== state.totalSize)
264
+ return 'jump';
265
+ if (visibleStart > state.endIndex || visibleEnd < state.startIndex)
266
+ return 'jump';
267
+ const direction = options.scrollDirection ?? 0;
268
+ const needStart = Math.max(visibleStart - (direction < 0 ? ahead : 0), 0);
269
+ const needEnd = Math.min(visibleEnd + (direction > 0 ? ahead : 0), count - 1);
270
+ return state.startIndex <= needStart && state.endIndex >= needEnd ? 'hold' : 'move';
271
+ }
210
272
  return {
211
273
  setOptions(next) {
212
274
  const merged = { ...options, ...next };
@@ -229,10 +291,14 @@ export function createVirtualizer(initial) {
229
291
  recalc();
230
292
  },
231
293
  setScrollOffset(scrollOffset) {
232
- if ((options.scrollOffset ?? 0) === scrollOffset)
294
+ const previous = options.scrollOffset ?? 0;
295
+ if (previous === scrollOffset)
233
296
  return;
234
- options = { ...options, scrollOffset };
235
- recalc();
297
+ options = { ...options, scrollOffset, scrollDirection: scrollOffset > previous ? 1 : -1 };
298
+ const step = scrollStep();
299
+ if (step === 'hold')
300
+ return;
301
+ recalc(step === 'move');
236
302
  },
237
303
  setViewportHeight(viewportHeight) {
238
304
  if (options.viewportHeight === viewportHeight)
@@ -0,0 +1,22 @@
1
+ /**
2
+ * What marks an array as windowed data, and how to tell (windowed-data.ts has
3
+ * the full story). Kept apart from createWindowedData so code that only asks
4
+ * "is this windowed?" - the controller, the core's per-update paths - does not
5
+ * pull in the windowed row model that createWindowedData installs.
6
+ */
7
+ export type WindowedSource<T> = {
8
+ /** The number of rows, loaded or not. */
9
+ readonly length: number;
10
+ /** The entry at `index`: a loaded row, a placeholder, or undefined. */
11
+ at(index: number): T | undefined;
12
+ };
13
+ /**
14
+ * An array of `length` entries read through `at(i)`: a real `Array` (a Proxy
15
+ * over an empty one) carrying the windowed mark. Each call returns a new
16
+ * array identity, which is how a source tells the grid its rows changed.
17
+ */
18
+ export declare function makeWindowedArray<T>(length: number, at: (index: number) => T | undefined): T[];
19
+ /** The source behind windowed data, or null for an ordinary array. */
20
+ export declare function windowedSourceOf<T>(data: unknown): WindowedSource<T> | null;
21
+ /** True for windowed data. */
22
+ export declare function isWindowedData(data: unknown): boolean;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * What marks an array as windowed data, and how to tell (windowed-data.ts has
3
+ * the full story). Kept apart from createWindowedData so code that only asks
4
+ * "is this windowed?" - the controller, the core's per-update paths - does not
5
+ * pull in the windowed row model that createWindowedData installs.
6
+ */
7
+ const WINDOWED = Symbol.for('svgrid.windowedData');
8
+ const isIndexKey = (key) => typeof key === 'string' && key.length > 0 && key.length < 16 && /^(0|[1-9]\d*)$/.test(key);
9
+ /**
10
+ * An array of `length` entries read through `at(i)`: a real `Array` (a Proxy
11
+ * over an empty one) carrying the windowed mark. Each call returns a new
12
+ * array identity, which is how a source tells the grid its rows changed.
13
+ */
14
+ export function makeWindowedArray(length, at) {
15
+ const source = { length, at };
16
+ const target = [];
17
+ return new Proxy(target, {
18
+ get(t, key, receiver) {
19
+ if (key === WINDOWED)
20
+ return source;
21
+ if (key === 'length')
22
+ return length;
23
+ if (isIndexKey(key)) {
24
+ const i = Number(key);
25
+ return i < length ? at(i) : undefined;
26
+ }
27
+ return Reflect.get(t, key, receiver);
28
+ },
29
+ has(t, key) {
30
+ if (key === WINDOWED)
31
+ return true;
32
+ if (isIndexKey(key))
33
+ return Number(key) < length;
34
+ return Reflect.has(t, key);
35
+ },
36
+ set() {
37
+ // Read-only: a write would land in the empty target and never be seen.
38
+ return false;
39
+ },
40
+ getOwnPropertyDescriptor(t, key) {
41
+ if (isIndexKey(key) && Number(key) < length) {
42
+ return { value: at(Number(key)), writable: false, enumerable: true, configurable: true };
43
+ }
44
+ if (key === 'length')
45
+ return { value: length, writable: true, enumerable: false, configurable: false };
46
+ return Reflect.getOwnPropertyDescriptor(t, key);
47
+ },
48
+ });
49
+ }
50
+ /** The source behind windowed data, or null for an ordinary array. */
51
+ export function windowedSourceOf(data) {
52
+ if (data === null || typeof data !== 'object')
53
+ return null;
54
+ const source = data[WINDOWED];
55
+ return source ?? null;
56
+ }
57
+ /** True for windowed data. */
58
+ export function isWindowedData(data) {
59
+ return windowedSourceOf(data) !== null;
60
+ }
@@ -0,0 +1,6 @@
1
+ export { windowedSourceOf, isWindowedData, type WindowedSource } from './windowed-brand.js';
2
+ /**
3
+ * An array of `length` entries read through `at(i)`. Each call returns a new
4
+ * array identity, which is how a source tells the grid its rows changed.
5
+ */
6
+ export declare function createWindowedData<T>(length: number, at: (index: number) => T | undefined): T[];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Windowed data: an array the grid can index without anyone having built it.
3
+ *
4
+ * A server row model knows how many rows there are and holds a few blocks of
5
+ * them. Handing the grid a dense array of `rowCount` entries (placeholders
6
+ * for everything not loaded) made every block landing cost O(rowCount): the
7
+ * array itself, a copy in the controller, and a row object per entry in the
8
+ * core. At 10M rows that was ~1 GB of heap and ~1 s per block; at 100M the
9
+ * grid never painted.
10
+ *
11
+ * `createWindowedData(length, at)` returns a real `Array` (a Proxy over one)
12
+ * whose `length` is the row count and whose entries are read through `at(i)`
13
+ * on demand. The grid recognises it (`windowedSourceOf`) and builds row
14
+ * objects only for the indices it actually reads - the rows on screen, plus
15
+ * whatever a user action touches. Iterating one still works (it is an array),
16
+ * but costs O(length), so the grid's per-update paths check for it first.
17
+ *
18
+ * Windowed data is final: the source has already sorted, filtered and grouped
19
+ * it, so the grid's own sort / filter / group stages do not run over it.
20
+ *
21
+ * The row model that reads it (windowed-row-model.ts) is installed by the
22
+ * first call here, so a grid never handed windowed data does not bundle it.
23
+ */
24
+ import { makeWindowedArray } from './windowed-brand.js';
25
+ import { enableWindowedRowModel } from './windowed-row-model.js';
26
+ export { windowedSourceOf, isWindowedData } from './windowed-brand.js';
27
+ /**
28
+ * An array of `length` entries read through `at(i)`. Each call returns a new
29
+ * array identity, which is how a source tells the grid its rows changed.
30
+ */
31
+ export function createWindowedData(length, at) {
32
+ enableWindowedRowModel();
33
+ return makeWindowedArray(length, at);
34
+ }
@@ -0,0 +1 @@
1
+ export declare function enableWindowedRowModel(): void;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The row model for windowed data (see windowed-data.ts).
3
+ *
4
+ * No build loop and no pipeline stages: `rows` is itself windowed. A row
5
+ * object is made the first time an index is read and kept (up to
6
+ * WINDOWED_ROW_CAP) while its data object is the same one, so scrolling back
7
+ * does not rebuild what is still loaded, and scrolling a 100M-row source end
8
+ * to end keeps a few thousand rows, not 100M.
9
+ *
10
+ * Installed into the core by the first createWindowedData() call rather than
11
+ * imported by core.ts, so a grid that is never handed windowed data does not
12
+ * bundle any of this.
13
+ */
14
+ import { BASE_ROW_METHODS, ROW_CELLS, ROW_CTX, ROW_FILTER_GEN, ROW_FILTER_POS, ROW_SORT_DROPPED, ROW_SORT_KEY, ROW_VALUES, registerWindowedRowModel, } from './core.js';
15
+ import { makeWindowedArray, windowedSourceOf } from './windowed-brand.js';
16
+ /** The windowed-data version a row's memoised values belong to. */
17
+ const ROW_WINDOW_GEN = Symbol('svgrid.row.windowGen');
18
+ const WINDOWED_ROW_CAP = 20000;
19
+ let installed = false;
20
+ export function enableWindowedRowModel() {
21
+ if (installed)
22
+ return;
23
+ installed = true;
24
+ registerWindowedRowModel((ctx) => {
25
+ // Per grid: the model for the current input, and the row objects built
26
+ // so far, by index.
27
+ let current = null;
28
+ const rows = new Map();
29
+ let generation = 0;
30
+ return (data, columns) => {
31
+ const source = windowedSourceOf(data);
32
+ if (!source)
33
+ return null;
34
+ if (current && current.input === data && current.columns === columns)
35
+ return current.model;
36
+ if (!current || current.columns !== columns)
37
+ rows.clear();
38
+ const rowCtx = ctx.rowCtxFor(columns);
39
+ const getRowId = ctx.getRowId();
40
+ const gen = ++generation;
41
+ const m = BASE_ROW_METHODS;
42
+ const rowAt = (index) => {
43
+ if (index < 0 || index >= source.length)
44
+ return undefined;
45
+ const original = source.at(index);
46
+ if (original === undefined)
47
+ return undefined;
48
+ const kept = rows.get(index);
49
+ if (kept && kept.original === original) {
50
+ // A new data version may follow an in-place edit of the object, as
51
+ // in the dense path: drop the memoised values once per version.
52
+ if (kept[ROW_WINDOW_GEN] !== gen) {
53
+ kept[ROW_VALUES] = null;
54
+ kept[ROW_CELLS] = null;
55
+ kept[ROW_WINDOW_GEN] = gen;
56
+ }
57
+ return kept;
58
+ }
59
+ const row = {
60
+ id: getRowId ? getRowId(original, index) : String(index),
61
+ index,
62
+ original,
63
+ depth: 0,
64
+ [ROW_CTX]: rowCtx,
65
+ [ROW_VALUES]: null,
66
+ [ROW_CELLS]: null,
67
+ [ROW_FILTER_GEN]: 0,
68
+ [ROW_FILTER_POS]: -1,
69
+ [ROW_SORT_DROPPED]: 0,
70
+ [ROW_SORT_KEY]: undefined,
71
+ [ROW_WINDOW_GEN]: gen,
72
+ getCanExpand: m.getCanExpand,
73
+ getIsExpanded: m.getIsExpanded,
74
+ toggleExpanded: m.toggleExpanded,
75
+ getIsSelected: m.getIsSelected,
76
+ toggleSelected: m.toggleSelected,
77
+ getAllCells: m.getAllCells,
78
+ getCellValueByColumnId: m.getCellValueByColumnId,
79
+ };
80
+ rows.set(index, row);
81
+ if (rows.size > WINDOWED_ROW_CAP) {
82
+ // Oldest first (Map keeps insertion order); a quarter at a time.
83
+ let drop = WINDOWED_ROW_CAP / 4;
84
+ for (const key of rows.keys()) {
85
+ if (drop-- <= 0)
86
+ break;
87
+ rows.delete(key);
88
+ }
89
+ }
90
+ return row;
91
+ };
92
+ const model = { rows: makeWindowedArray(source.length, rowAt) };
93
+ current = { input: data, columns, model };
94
+ return model;
95
+ };
96
+ });
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "3.0.7",
3
+ "version": "3.0.9",
4
4
  "description": "Svelte 5-native data grid and data table. Headless-first engine with virtual scrolling for 100k+ rows, Excel-style filtering, inline editing, grouping, tree data, pivot, and server-side data. Drop-in <SvGrid> component. TypeScript, SSR-ready.",
5
5
  "author": "jQWidgets <boikom@jqwidgets.com>",
6
6
  "license": "MIT",
@@ -49,7 +49,6 @@
49
49
  const unpinColumn = $derived(ctrl.unpinColumn);
50
50
  const sortDirectionByColumn = $derived(ctrl.sortDirectionByColumn);
51
51
  const groupingColumns = $derived(ctrl.groupingColumns);
52
- const columnVirtualizationEnabled = $derived(ctrl.columnVirtualizationEnabled);
53
52
  const updateFilterOperator = $derived(ctrl.updateFilterOperator);
54
53
  const updateFilterMenuValue = $derived(ctrl.updateFilterMenuValue);
55
54
  const updateFilterMenuValueTo = $derived(ctrl.updateFilterMenuValueTo);
@@ -644,7 +643,9 @@
644
643
  <span class="sv-grid-header-icon">{@render icon("x")}</span> Remove sort
645
644
  </button>
646
645
  {/if}
647
- {#if !columnVirtualizationEnabled}
646
+ <!-- Pinning works with column virtualization on: the pinned columns render
647
+ as their own runs either side of the virtual window. -->
648
+ {#if menuColumnId}
648
649
  {@const menuPinSide = isColumnPinned(menuColumnId)}
649
650
  <div class="sv-grid-menu-sep"></div>
650
651
  <button