@svgrid/grid 3.0.6 → 3.0.8

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 (77) hide show
  1. package/README.md +1 -1
  2. package/dist/SvGrid.controller.svelte.d.ts +2 -0
  3. package/dist/SvGrid.controller.svelte.js +46 -9
  4. package/dist/SvGrid.css +16 -5
  5. package/dist/SvGrid.helpers.js +2 -4
  6. package/dist/SvGrid.svelte +4 -1
  7. package/dist/cdn/{GridMenus-C-NtjlrJ.js → GridMenus-B8-Gbfq2.js} +2 -2
  8. package/dist/cdn/{GridMenus-TssHej3q.js → GridMenus-D15bhpX5.js} +2 -2
  9. package/dist/cdn/{SvGridCellEditor-BlyCSp0q.js → SvGridCellEditor-9yz-SbdZ.js} +1 -1
  10. package/dist/cdn/{SvGridCellEditor-ClRRA9lW.js → SvGridCellEditor-D7-ql1Hd.js} +1 -1
  11. package/dist/cdn/{SvGridChart-DsmwTyZF.js → SvGridChart-DMUOFxOe.js} +3 -3
  12. package/dist/cdn/{SvGridChart-B4EV8U6t.js → SvGridChart-N7ik0oWy.js} +3 -3
  13. package/dist/cdn/{SvGridChartBuilder-cHloHobN.js → SvGridChartBuilder-CB_Jnm2k.js} +3 -3
  14. package/dist/cdn/{SvGridChartBuilder-vOl7C0Za.js → SvGridChartBuilder-Ca7gF8ZK.js} +3 -3
  15. package/dist/cdn/{SvGridChartPanel-AcqF-Ll5.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
  16. package/dist/cdn/{SvGridChartPanel-biMtSBPA.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
  17. package/dist/cdn/{SvGridChartView-I3rDHXAq.js → SvGridChartView-B1LFgoPP.js} +2 -2
  18. package/dist/cdn/{SvGridChartView-Dx_GmZuL.js → SvGridChartView-By3n7DGZ.js} +2 -2
  19. package/dist/cdn/{cell-formatting-DVVKOSI9.js → cell-formatting-Mnv84zoE.js} +27 -5
  20. package/dist/cdn/{chart-DEZxwC0i.js → chart-By7h2Guv.js} +1 -1
  21. package/dist/cdn/{chart-panel-messages-D12XcTZJ.js → chart-panel-messages-BC-tOgFY.js} +1 -1
  22. package/dist/cdn/{chart-panel-messages-DguS7gC5.js → chart-panel-messages-DUaXr-sb.js} +1 -1
  23. package/dist/cdn/{chart-summary-CMAXBGY-.js → chart-summary-BDtrNEiA.js} +1 -1
  24. package/dist/cdn/{chart-trend-BkEp61Gj.js → chart-trend-C1aOAPBZ.js} +1 -1
  25. package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
  26. package/dist/cdn/{export-format-YzdwBww2.js → export-format-y1wdyCcv.js} +93 -97
  27. package/dist/cdn/{src-CgoeSM8k.js → src-CleI0g9l.js} +3256 -3198
  28. package/dist/cdn/{src-DfwxB1TH.js → src-Dz625Fhx.js} +3184 -3126
  29. package/dist/cdn/svgrid.js +11 -11
  30. package/dist/cdn/svgrid.svelte-external.js +11 -11
  31. package/dist/cdn/validate-GEs4ZPSW.js +80 -0
  32. package/dist/cell-formatting.d.ts +9 -0
  33. package/dist/cell-formatting.js +34 -0
  34. package/dist/core.js +124 -21
  35. package/dist/export-format.d.ts +8 -0
  36. package/dist/export-format.js +23 -16
  37. package/dist/headless.d.ts +1 -1
  38. package/dist/headless.js +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/keyboard-handlers.js +5 -1
  42. package/dist/selection.js +14 -4
  43. package/dist/{subscribe.js → subscribe.svelte.js} +5 -1
  44. package/dist/validate.js +10 -3
  45. package/dist/virtualization/column-virtualizer.d.ts +1 -0
  46. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +1 -0
  47. package/dist/virtualization/svelte-virtualizer.svelte.js +1 -0
  48. package/dist/virtualization/virtualizer.d.ts +3 -0
  49. package/dist/virtualization/virtualizer.js +14 -1
  50. package/package.json +1 -1
  51. package/src/SvGrid.controller.svelte.ts +45 -13
  52. package/src/SvGrid.css +16 -5
  53. package/src/SvGrid.helpers.ts +2 -4
  54. package/src/SvGrid.svelte +4 -1
  55. package/src/cell-formatting.ts +31 -0
  56. package/src/core.sort.test.ts +86 -0
  57. package/src/core.ts +114 -21
  58. package/src/export-dates.test.ts +114 -0
  59. package/src/export-format.ts +26 -14
  60. package/src/headless.reactivity.svelte.test.ts +89 -0
  61. package/src/headless.ts +1 -1
  62. package/src/index.ts +1 -1
  63. package/src/keyboard-handlers.ts +5 -1
  64. package/src/selection.test.ts +37 -0
  65. package/src/selection.ts +16 -6
  66. package/src/{subscribe.test.ts → subscribe.svelte.test.ts} +1 -1
  67. package/src/{subscribe.ts → subscribe.svelte.ts} +5 -1
  68. package/src/svgrid.behavior.test.ts +29 -0
  69. package/src/svgrid.data-push.svelte.test.ts +94 -0
  70. package/src/svgrid.live-update-paths.svelte.test.ts +4 -2
  71. package/src/validate.test.ts +16 -0
  72. package/src/validate.ts +10 -2
  73. package/src/virtualization/svelte-virtualizer.svelte.ts +1 -0
  74. package/src/virtualization/virtualizer.test.ts +44 -0
  75. package/src/virtualization/virtualizer.ts +13 -1
  76. package/dist/cdn/validate-AZoD8BoR.js +0 -80
  77. /package/dist/{subscribe.d.ts → subscribe.svelte.d.ts} +0 -0
package/dist/core.js CHANGED
@@ -130,14 +130,54 @@ export function tableFeatures(features) {
130
130
  * text; set a column's type or supply your own comparator to override.
131
131
  */
132
132
  export const sortFns = {
133
- auto: (a, b) => String(a).localeCompare(String(b)),
134
- number: (a, b) => Number(a ?? 0) - Number(b ?? 0),
135
- date: (a, b) => {
136
- const aa = new Date(a).getTime();
137
- const bb = new Date(b).getTime();
138
- return aa - bb;
139
- },
133
+ auto: (a, b) => compareAutoValues(a, b),
134
+ number: (a, b) => compareNumericKeys(numberSortKey(a), numberSortKey(b)),
135
+ date: (a, b) => compareNumericKeys(dateSortKey(a), dateSortKey(b)),
140
136
  };
137
+ /** Null, undefined and the empty string: the values every built-in sort puts first. */
138
+ function isBlankSortValue(value) {
139
+ return value == null || value === '';
140
+ }
141
+ /**
142
+ * Sort key for the `number` comparator. Blanks and anything that does not
143
+ * coerce to a number ('n/a', a placeholder) become -Infinity, so they sort
144
+ * together ahead of every number instead of producing NaN. A NaN key made the
145
+ * comparator inconsistent - NaN compared equal to everything while the numbers
146
+ * around it did not - and `Array.prototype.sort` then returned a visibly
147
+ * unsorted column (#104).
148
+ */
149
+ function numberSortKey(value) {
150
+ if (isBlankSortValue(value))
151
+ return -Infinity;
152
+ const n = Number(value);
153
+ return Number.isNaN(n) ? -Infinity : n;
154
+ }
155
+ /** Sort key for the `date` comparator; blanks and invalid dates sort first. */
156
+ function dateSortKey(value) {
157
+ if (isBlankSortValue(value))
158
+ return -Infinity;
159
+ const t = new Date(value).getTime();
160
+ return Number.isNaN(t) ? -Infinity : t;
161
+ }
162
+ /**
163
+ * The `auto` comparator: blanks first, then numbers in numeric order, then
164
+ * everything else as collated text. Ordering by kind before value keeps it
165
+ * transitive on mixed columns - comparing a number to a string as text
166
+ * instead would give 2 < 10, 10 < '1a' and '1a' < 2, a cycle.
167
+ */
168
+ function compareAutoValues(a, b) {
169
+ const aBlank = isBlankSortValue(a);
170
+ const bBlank = isBlankSortValue(b);
171
+ if (aBlank || bBlank)
172
+ return aBlank === bBlank ? 0 : aBlank ? -1 : 1;
173
+ const aNum = typeof a === 'number';
174
+ const bNum = typeof b === 'number';
175
+ if (aNum && bNum)
176
+ return compareNumericKeys(numberSortKey(a), numberSortKey(b));
177
+ if (aNum !== bNum)
178
+ return aNum ? -1 : 1;
179
+ return compareCollatedKeys(String(a), String(b));
180
+ }
141
181
  /**
142
182
  * Built-in match functions, named by {@link ColumnFilter}'s `fn`.
143
183
  * `includesString` is case-insensitive substring; `equals` is strict identity.
@@ -736,18 +776,42 @@ export function createSortedRowModel(localSortFns = sortFns) {
736
776
  let keys = new Array(n);
737
777
  let compare;
738
778
  let keyOf;
779
+ let autoMode;
739
780
  if (comparator === sortFns.number) {
740
- keyOf = (row) => Number(row.getCellValueByColumnId(columnId) ?? 0);
781
+ keyOf = (row) => numberSortKey(row.getCellValueByColumnId(columnId));
741
782
  for (let i = 0; i < n; i++)
742
783
  keys[i] = keyOf(rows[i]);
743
784
  compare = compareNumericKeys;
744
785
  }
745
786
  else if (comparator === sortFns.date) {
746
- keyOf = (row) => new Date(row.getCellValueByColumnId(columnId)).getTime();
787
+ keyOf = (row) => dateSortKey(row.getCellValueByColumnId(columnId));
788
+ for (let i = 0; i < n; i++)
789
+ keys[i] = keyOf(rows[i]);
790
+ compare = compareNumericKeys;
791
+ }
792
+ else if (comparator === sortFns.auto && (autoMode = autoSortMode(rows, columnId)) === 'number') {
793
+ // A column with no declared type whose values are all numbers. Sorting
794
+ // it as text put 10 before 2 (#104).
795
+ keyOf = (row) => {
796
+ const value = row.getCellValueByColumnId(columnId);
797
+ // A text value arriving in a tick changes the mode; only a full sort
798
+ // can place it.
799
+ return typeof value === 'number' || isBlankSortValue(value)
800
+ ? numberSortKey(value)
801
+ : MISSING_KEY;
802
+ };
747
803
  for (let i = 0; i < n; i++)
748
804
  keys[i] = keyOf(rows[i]);
749
805
  compare = compareNumericKeys;
750
806
  }
807
+ else if (autoMode === 'mixed') {
808
+ // Numbers and text in one column. Rare, so no key tricks: the raw
809
+ // values through the pairwise comparator, which orders by kind first.
810
+ keyOf = (row) => row.getCellValueByColumnId(columnId);
811
+ for (let i = 0; i < n; i++)
812
+ keys[i] = keyOf(rows[i]);
813
+ compare = compareAutoValues;
814
+ }
751
815
  else if (comparator === sortFns.auto) {
752
816
  const strings = new Array(n);
753
817
  // Decide whether ranking is worth attempting BEFORE paying for it.
@@ -770,7 +834,7 @@ export function createSortedRowModel(localSortFns = sortFns) {
770
834
  const sample = new Set();
771
835
  let sampled = 0;
772
836
  for (let i = 0; i < n; i += stride) {
773
- sample.add(String(rows[i].getCellValueByColumnId(columnId)));
837
+ sample.add(autoTextKey(rows[i].getCellValueByColumnId(columnId)));
774
838
  sampled++;
775
839
  }
776
840
  // Only attempt ranking when the sample suggests real repetition.
@@ -778,7 +842,7 @@ export function createSortedRowModel(localSortFns = sortFns) {
778
842
  distinct = new Set();
779
843
  }
780
844
  for (let i = 0; i < n; i++) {
781
- const s = String(rows[i].getCellValueByColumnId(columnId));
845
+ const s = autoTextKey(rows[i].getCellValueByColumnId(columnId));
782
846
  strings[i] = s;
783
847
  if (distinct) {
784
848
  distinct.add(s);
@@ -809,12 +873,21 @@ export function createSortedRowModel(localSortFns = sortFns) {
809
873
  compare = compareNumericKeys;
810
874
  // A replacement whose text is not among the ranked values has no
811
875
  // rank; the repair path treats that as "sort everything again".
812
- keyOf = (row) => rankOf.get(String(row.getCellValueByColumnId(columnId))) ?? MISSING_KEY;
876
+ keyOf = (row) => {
877
+ const value = row.getCellValueByColumnId(columnId);
878
+ // A number arriving in a tick makes the column mixed; resort fully.
879
+ if (typeof value === 'number')
880
+ return MISSING_KEY;
881
+ return rankOf.get(autoTextKey(value)) ?? MISSING_KEY;
882
+ };
813
883
  }
814
884
  else {
815
885
  keys = strings;
816
886
  compare = compareCollatedKeys;
817
- keyOf = (row) => String(row.getCellValueByColumnId(columnId));
887
+ keyOf = (row) => {
888
+ const value = row.getCellValueByColumnId(columnId);
889
+ return typeof value === 'number' ? MISSING_KEY : autoTextKey(value);
890
+ };
818
891
  }
819
892
  }
820
893
  else {
@@ -1037,14 +1110,39 @@ export function createSortedRowModel(localSortFns = sortFns) {
1037
1110
  };
1038
1111
  }
1039
1112
  /**
1040
- * Numeric key comparison for the built-in `number` and `date` comparators.
1041
- * Subtraction rather than `<`/`>` on purpose: it reproduces the originals
1042
- * exactly, NaN included. An unparseable date or a non-numeric value yields NaN,
1043
- * and the sort spec turns a NaN comparison result into 0 (SortCompare coerces
1044
- * it), which is the behaviour callers already depend on.
1113
+ * Numeric key comparison for the built-in `number`, `date` and numeric
1114
+ * `auto` comparators. Keys are never NaN (blanks and unparseable values map to
1115
+ * -Infinity), but two -Infinity keys subtract to NaN, so equal keys return 0
1116
+ * explicitly: the multi-clause sort reads any non-zero result, NaN included,
1117
+ * as decided and would skip the next clause.
1045
1118
  */
1046
1119
  function compareNumericKeys(a, b) {
1047
- return a - b;
1120
+ return a === b ? 0 : a - b;
1121
+ }
1122
+ /** Text key for the `auto` comparator: blanks become '' so they collate first. */
1123
+ function autoTextKey(value) {
1124
+ return isBlankSortValue(value) ? '' : String(value);
1125
+ }
1126
+ /**
1127
+ * How the `auto` comparator should treat a column: 'number' when every
1128
+ * non-blank value is a number, 'mixed' when numbers share it with anything
1129
+ * else, 'text' otherwise. One pass, stopping as soon as the answer is known.
1130
+ */
1131
+ function autoSortMode(rows, columnId) {
1132
+ let numbers = false;
1133
+ let other = false;
1134
+ for (let i = 0; i < rows.length; i++) {
1135
+ const value = rows[i].getCellValueByColumnId(columnId);
1136
+ if (isBlankSortValue(value))
1137
+ continue;
1138
+ if (typeof value === 'number')
1139
+ numbers = true;
1140
+ else
1141
+ other = true;
1142
+ if (numbers && other)
1143
+ return 'mixed';
1144
+ }
1145
+ return numbers ? 'number' : 'text';
1048
1146
  }
1049
1147
  /**
1050
1148
  * Text key comparison for the built-in `auto` comparator.
@@ -1321,8 +1419,13 @@ export function createSvGridCore(options) {
1321
1419
  const original = data[index];
1322
1420
  const kept = previous?.[index];
1323
1421
  if (kept && kept.original === original) {
1324
- kept[ROW_VALUES] = null;
1325
- kept[ROW_CELLS] = null;
1422
+ // Read before writing: a row nobody rendered (a million off-screen
1423
+ // placeholders under a row model) has nothing memoised, and a
1424
+ // store per row was a measurable share of each block landing.
1425
+ if (kept[ROW_VALUES] !== null)
1426
+ kept[ROW_VALUES] = null;
1427
+ if (kept[ROW_CELLS] !== null)
1428
+ kept[ROW_CELLS] = null;
1326
1429
  cachedBaseRows[index] = kept;
1327
1430
  continue;
1328
1431
  }
@@ -21,6 +21,14 @@ export { computeColumnStat, contrastText, formatsNeedingStats, resolveCellFormat
21
21
  /** Best-effort coercion of a cell value to a Date. Accepts Date, epoch ms
22
22
  * numbers, and parseable date strings. Returns null when it isn't a date. */
23
23
  export declare function coerceExportDate(value: unknown): Date | null;
24
+ /**
25
+ * A Date as ISO 8601 in LOCAL time with its offset, e.g.
26
+ * `2024-01-15T00:00:00.000+02:00`. Text exports used `toISOString()`, which
27
+ * is UTC: local midnight on the 15th came out as `2024-01-14T22:00:00.000Z`
28
+ * east of UTC, and anything reading the date part got the wrong day (#103).
29
+ * Same instant, but the date part is the one the grid showed.
30
+ */
31
+ export declare function toLocalIsoString(d: Date): string;
24
32
  /**
25
33
  * Format a raw cell value to its on-screen display string using the column's
26
34
  * `format` config. Mirrors the grid's own numeric / currency / percent / date
@@ -1,4 +1,4 @@
1
- import { formatNumericWithConfig, getDateFormatter, resolveDatePattern, } from './cell-formatting.js';
1
+ import { formatNumericWithConfig, getDateFormatter, parseDateValue, resolveDatePattern, } from './cell-formatting.js';
2
2
  // Re-export the pure conditional-formatting engine here too, so the
3
3
  // Svelte-free `@svgrid/grid/format` subpath carries it for @svgrid/enterprise's
4
4
  // export code (which must not pull in SvGrid.svelte).
@@ -6,17 +6,24 @@ export { computeColumnStat, contrastText, formatsNeedingStats, resolveCellFormat
6
6
  /** Best-effort coercion of a cell value to a Date. Accepts Date, epoch ms
7
7
  * numbers, and parseable date strings. Returns null when it isn't a date. */
8
8
  export function coerceExportDate(value) {
9
- if (value instanceof Date)
10
- return Number.isNaN(value.getTime()) ? null : value;
11
- if (typeof value === 'number' && Number.isFinite(value)) {
12
- const d = new Date(value);
13
- return Number.isNaN(d.getTime()) ? null : d;
14
- }
15
- if (typeof value === 'string' && value.trim() !== '') {
16
- const d = new Date(value);
17
- return Number.isNaN(d.getTime()) ? null : d;
18
- }
19
- return null;
9
+ return parseDateValue(value);
10
+ }
11
+ /**
12
+ * A Date as ISO 8601 in LOCAL time with its offset, e.g.
13
+ * `2024-01-15T00:00:00.000+02:00`. Text exports used `toISOString()`, which
14
+ * is UTC: local midnight on the 15th came out as `2024-01-14T22:00:00.000Z`
15
+ * east of UTC, and anything reading the date part got the wrong day (#103).
16
+ * Same instant, but the date part is the one the grid showed.
17
+ */
18
+ export function toLocalIsoString(d) {
19
+ const pad = (n, w = 2) => String(Math.abs(n)).padStart(w, '0');
20
+ const offset = -d.getTimezoneOffset();
21
+ const sign = offset < 0 ? '-' : '+';
22
+ const year = d.getFullYear();
23
+ return ((year < 0 ? '-' + pad(year, 6) : pad(year, 4)) +
24
+ `-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
25
+ `T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}` +
26
+ `${sign}${pad(Math.floor(Math.abs(offset) / 60))}:${pad(Math.abs(offset) % 60)}`);
20
27
  }
21
28
  /**
22
29
  * Format a raw cell value to its on-screen display string using the column's
@@ -190,7 +197,7 @@ function csvCell(value, delimiter) {
190
197
  const s = value == null
191
198
  ? ''
192
199
  : value instanceof Date
193
- ? value.toISOString()
200
+ ? toLocalIsoString(value)
194
201
  : String(value);
195
202
  if (s === '')
196
203
  return s;
@@ -231,7 +238,7 @@ const HTML_ESCAPE = {
231
238
  "'": '&#39;',
232
239
  };
233
240
  function htmlCell(value) {
234
- const s = value == null ? '' : value instanceof Date ? value.toISOString() : String(value);
241
+ const s = value == null ? '' : value instanceof Date ? toLocalIsoString(value) : String(value);
235
242
  return s.replace(/[&<>"']/g, (c) => HTML_ESCAPE[c]);
236
243
  }
237
244
  /**
@@ -311,7 +318,7 @@ export async function serializeJson(rows, fields, opts = {}) {
311
318
  }
312
319
  /** Escape a Markdown table cell: pipes and newlines would break the row. */
313
320
  function mdCell(value) {
314
- const s = value == null ? '' : value instanceof Date ? value.toISOString() : String(value);
321
+ const s = value == null ? '' : value instanceof Date ? toLocalIsoString(value) : String(value);
315
322
  return s.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
316
323
  }
317
324
  /**
@@ -351,7 +358,7 @@ const XML_ESCAPE = {
351
358
  "'": '&apos;',
352
359
  };
353
360
  function xmlText(value) {
354
- const s = value == null ? '' : value instanceof Date ? value.toISOString() : String(value);
361
+ const s = value == null ? '' : value instanceof Date ? toLocalIsoString(value) : String(value);
355
362
  // Stripping control characters is the point: XML 1.0 forbids them outright,
356
363
  // so a stray \x00 from a data source would produce an unopenable file.
357
364
  // eslint-disable-next-line no-control-regex
@@ -26,7 +26,7 @@
26
26
  export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, createSvGridCore, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, type Cell, type CellContext, type CellSpanParams, type ValueParserParams, type EditorContext, type CellData, type ActiveCellState, type CellFormatConfig, type CellFormatter, type Column, type ColumnDef, type ColumnDefTemplate, type ColumnFilter, type ColumnFiltersState, type ExpandedState, type GridColumnDef, type GridColumns, type GroupAggregator, type GroupingState, type Header, type HeaderContext, type HeaderGroup, type PaginationState, type Row, type RowData, type RowModel, type RowModelFactory, type RowSelectionState, type SortingState, type Store, type SvGrid as SvGridInstance, type SvGridOptions, type TableFeatures, type TreeRowModelOptions, type FlattenTreeOptions, type Updater, } from './core.js';
27
27
  export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte.js';
28
28
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
29
- export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
29
+ export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
30
30
  export { createVirtualizer } from './virtualization/virtualizer.js';
31
31
  export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte.js';
32
32
  export { createColumnVirtualizer } from './virtualization/column-virtualizer.js';
package/dist/headless.js CHANGED
@@ -26,7 +26,7 @@
26
26
  export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, createSvGridCore, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, } from './core.js';
27
27
  export { createGrid, createSvGrid } from './createGrid.svelte.js';
28
28
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
29
- export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
29
+ export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
30
30
  // The virtualizers are pure window arithmetic (scroll offset in, visible
31
31
  // range out) with no DOM of their own, and a custom renderer over 50k rows
32
32
  // needs one. They were only on the barrel, so the Headless demos imported
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { coerceExportDate, formatValueForExport, projectGridRows, toExcelNumFmt,
3
3
  export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, type Cell, type CellContext, type CellSpanParams, type ValueParserParams, type EditorContext, type CellData, type ActiveCellState, type CellFormatConfig, type CellFormatter, type Column, type ColumnDef, type ColumnDefTemplate, type GridColumnDef, type GridColumns, type GroupAggregator, type Header, type HeaderContext, type HeaderGroup, type Row, type RowData, type SortingState, type SvGrid as SvGridInstance, type SvGridOptions, type TableFeatures, type TreeRowModelOptions, type FlattenTreeOptions, type Updater, } from './core.js';
4
4
  export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte.js';
5
5
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
6
- export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
6
+ export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
7
7
  export { default as SvGrid } from './SvGrid.svelte';
8
8
  export type { BoardConfig, BoardLane, BoardCardMoveEvent, BoardCardCommitEvent, BoardLayout, BoardSubtask, BoardComment, BoardMenuContext, BoardLaneMenuContext, BoardDrawerConfig } from './SvGrid.types.js';
9
9
  export { registerBoardView, getBoardView, hasBoardView } from './board-view.svelte.js';
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ export { coerceExportDate, formatValueForExport, projectGridRows, toExcelNumFmt,
3
3
  export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, } from './core.js';
4
4
  export { createGrid, createSvGrid } from './createGrid.svelte.js';
5
5
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
6
- export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
6
+ export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
7
7
  export { default as SvGrid } from './SvGrid.svelte';
8
8
  export { registerBoardView, getBoardView, hasBoardView } from './board-view.svelte.js';
9
9
  // Built-in AI helpers (free): natural-language filter, smart-fill, summarize,
@@ -189,7 +189,11 @@ export function createKeyboard(ctx) {
189
189
  }
190
190
  }
191
191
  if (column?.columnDef.editorType === "checkbox") {
192
- ctx.toggleBooleanCell(current.rowIndex, current.colIndex);
192
+ // Same rule as a click: `editable: false` (or an editable callback
193
+ // saying no) keeps the checkbox read-only.
194
+ if (ctx.isCellEditableAt?.(current.rowIndex, current.colIndex) ?? true) {
195
+ ctx.toggleBooleanCell(current.rowIndex, current.colIndex);
196
+ }
193
197
  return;
194
198
  }
195
199
  ctx.onCellDoubleClick(current.rowIndex, current.colIndex);
package/dist/selection.js CHANGED
@@ -622,10 +622,20 @@ export function createSelection(ctx) {
622
622
  columnId: column.id,
623
623
  row: row.original,
624
624
  });
625
- const isCheckboxColumn = column.columnDef.editorType === "checkbox" ||
626
- typeof cellValue === "boolean";
627
- if (isCheckboxColumn) {
628
- ctx.toggleBooleanCell(rowIndex, colIndex);
625
+ // A declared checkbox editor toggles on click unless the cell is not
626
+ // editable. A value that only HAPPENS to be boolean is drawn as a read-only
627
+ // checkbox, so it toggles only when inline editing is on - it used to flip
628
+ // and write the row on any click, in grids with editing switched off. A
629
+ // column with its own `cell` renderer draws (and handles) the value itself.
630
+ const declaredCheckbox = column.columnDef.editorType === "checkbox";
631
+ const inferredCheckbox = !declaredCheckbox &&
632
+ typeof cellValue === "boolean" &&
633
+ column.columnDef.cell == null;
634
+ if (declaredCheckbox || inferredCheckbox) {
635
+ const editable = ctx.isCellEditableAt?.(rowIndex, colIndex) ?? true;
636
+ if (editable && (declaredCheckbox || ctx.editingEnabled)) {
637
+ ctx.toggleBooleanCell(rowIndex, colIndex);
638
+ }
629
639
  setActiveCell(rowIndex, colIndex);
630
640
  setSelection(rowIndex, colIndex);
631
641
  ctx.editingCell = null;
@@ -16,7 +16,11 @@ const shallowCompare = (a, b) => {
16
16
  * comparison. Call from `<script>` so the selector is contextually typed.
17
17
  */
18
18
  export function subscribeGrid(grid, selector) {
19
- let current = selector(grid.store.state);
19
+ // $state, not a plain variable: core.ts keeps a deliberately framework-free
20
+ // store, so this Svelte adapter is where reactivity belongs. Without it a
21
+ // `$derived` reading .current never re-runs and the docs' headless examples
22
+ // render their seeded state forever.
23
+ let current = $state(selector(grid.store.state));
20
24
  grid.store.subscribe(() => {
21
25
  const next = selector(grid.store.state);
22
26
  if (!shallowCompare(current, next))
package/dist/validate.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { rowPlaceholderState } from './server-block-cache.js';
2
2
  /** How many rows to sample when deciding whether a `field` exists. */
3
3
  const FIELD_SAMPLE_ROWS = 10;
4
+ /** How many rows to look through for that sample, placeholders included. */
5
+ const FIELD_SCAN_ROWS = 1000;
4
6
  const DOCS = 'https://svgrid.com/docs/getting-started/3-data-and-columns/';
5
7
  /** Every leaf column, flattened through group columns. */
6
8
  function leaves(columns, out = []) {
@@ -23,10 +25,15 @@ export function validateGridConfig(input) {
23
25
  // no data keys at all and say nothing about the columns.
24
26
  // Stops at the sample size: this runs on every data change, and a filter
25
27
  // over all the rows first was O(n) per tick on a live feed.
28
+ // The scan is bounded too: a row model scrolled to the middle of a million
29
+ // rows, or just purged by a sort, hands over a list that is placeholders
30
+ // up to the loaded block, and walking all of them for ten real rows was
31
+ // O(n) per block landing. Finding none means nothing to judge by.
26
32
  const sample = [];
27
- for (const row of input.data ?? []) {
28
- if (sample.length >= FIELD_SAMPLE_ROWS)
29
- break;
33
+ const data = input.data ?? [];
34
+ const scan = Math.min(data.length, FIELD_SCAN_ROWS);
35
+ for (let i = 0; i < scan && sample.length < FIELD_SAMPLE_ROWS; i += 1) {
36
+ const row = data[i];
30
37
  if (!rowPlaceholderState(row))
31
38
  sample.push(row);
32
39
  }
@@ -10,6 +10,7 @@ export declare function createColumnVirtualizer(input: {
10
10
  setViewportWidth(viewportWidth: number): void;
11
11
  setHorizontalOffset(scrollOffset: number): void;
12
12
  setOptions(next: Partial<import("./types.js").VirtualizerOptions>): void;
13
+ measure(): void;
13
14
  setScrollOffset(scrollOffset: number): void;
14
15
  setViewportHeight(viewportHeight: number): void;
15
16
  scrollToIndex(index: number): void;
@@ -2,6 +2,7 @@ import type { VirtualizerOptions } from './types.js';
2
2
  export declare function createSvelteVirtualizer(options: VirtualizerOptions): {
3
3
  readonly version: number;
4
4
  setOptions: (next: Partial<VirtualizerOptions>) => void;
5
+ measure: () => void;
5
6
  setScrollOffset: (scrollOffset: number) => void;
6
7
  setViewportHeight: (viewportHeight: number) => void;
7
8
  scrollToIndex: (index: number) => void;
@@ -10,6 +10,7 @@ export function createSvelteVirtualizer(options) {
10
10
  return version;
11
11
  },
12
12
  setOptions: virtualizer.setOptions,
13
+ measure: virtualizer.measure,
13
14
  setScrollOffset: virtualizer.setScrollOffset,
14
15
  setViewportHeight: virtualizer.setViewportHeight,
15
16
  scrollToIndex: virtualizer.scrollToIndex,
@@ -16,6 +16,9 @@ type VirtualizerListener = () => void;
16
16
  export declare function buildPreMeasureItems(count: number, estimateSize: number, viewportHeight: number, overscan: number): Array<VirtualItem>;
17
17
  export declare function createVirtualizer(initial: VirtualizerOptions): {
18
18
  setOptions(next: Partial<VirtualizerOptions>): void;
19
+ /** Drop the cached offsets and re-read every size from `estimateSize`.
20
+ * Call after the sizes a stable size function reports have changed. */
21
+ measure(): void;
19
22
  setScrollOffset(scrollOffset: number): void;
20
23
  setViewportHeight(viewportHeight: number): void;
21
24
  scrollToIndex(index: number): void;
@@ -210,9 +210,22 @@ export function createVirtualizer(initial) {
210
210
  return {
211
211
  setOptions(next) {
212
212
  const merged = { ...options, ...next };
213
- if (sameOptions(options, merged))
213
+ // Passing a size function means "sizes may have changed", even when it is
214
+ // the same reference: the documented variable-height pattern is a stable
215
+ // function reading a mutable map, and keying the cache on identity alone
216
+ // left the offsets and total stale after a row grew (#102).
217
+ const remeasure = typeof next.estimateSize === 'function';
218
+ if (!remeasure && sameOptions(options, merged))
214
219
  return;
215
220
  options = merged;
221
+ if (remeasure)
222
+ offsetCache = null;
223
+ recalc();
224
+ },
225
+ /** Drop the cached offsets and re-read every size from `estimateSize`.
226
+ * Call after the sizes a stable size function reports have changed. */
227
+ measure() {
228
+ offsetCache = null;
216
229
  recalc();
217
230
  },
218
231
  setScrollOffset(scrollOffset) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "3.0.6",
3
+ "version": "3.0.8",
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",
@@ -845,7 +845,18 @@ export function createSvGridController<
845
845
  // `data` is optional on the type (a `rowModel` may supply the rows
846
846
  // instead), so every read falls back to empty rather than to undefined.
847
847
  const EMPTY_DATA: ReadonlyArray<TData> = [];
848
- let internalData = $state.raw<ReadonlyArray<TData>>(props.data ?? EMPTY_DATA);
848
+ // Seeded as a copy, for the same reason the sync effect below makes one: the
849
+ // row model tells data apart by array identity, so the grid has to own an
850
+ // array the consumer cannot grow behind its back.
851
+ let internalData = $state.raw<ReadonlyArray<TData>>(
852
+ props.data?.length ? props.data.slice() : EMPTY_DATA,
853
+ );
854
+ // What the last sync saw, so the effect can tell a replacement (drop the
855
+ // cell edits) from a push or splice (keep them) and skip a no-op run.
856
+ // svelte-ignore state_referenced_locally
857
+ let lastData: ReadonlyArray<TData> = props.data ?? EMPTY_DATA;
858
+ // svelte-ignore state_referenced_locally
859
+ let lastDataLength = props.data?.length ?? 0;
849
860
  // Resolve `cellDataType` / `inferColumnTypes` into concrete editorType +
850
861
  // format defaults once, up front, so every downstream reader sees a normal
851
862
  // column. Explicit fields on the ColumnDef always win.
@@ -893,12 +904,27 @@ export function createSvGridController<
893
904
  }
894
905
 
895
906
  $effect(() => {
907
+ const next = props.data ?? EMPTY_DATA;
908
+ // Reading `length` is what subscribes this effect to a `$state` array's own
909
+ // length signal, so `rows.push(...)` and `rows.splice(...)` re-run it.
910
+ // Without this read nothing in the grid tracks the row count at all:
911
+ // `getRowModel` reads `.length` only inside its cache miss, and the miss is
912
+ // keyed on the array reference, which a mutation does not change.
913
+ const length = next.length;
914
+ const replaced = next !== lastData;
915
+ if (!replaced && length === lastDataLength) return;
916
+ lastData = next;
917
+ lastDataLength = length;
918
+ // A fresh array object, because `getRowModel` memoises on identity. The
919
+ // elements are the same objects, so a row that did not move keeps its row
920
+ // object and a field write still reaches its cell through the proxy.
921
+ internalData = length === 0 ? EMPTY_DATA : next.slice();
896
922
  // When the consumer replaces `data` (e.g. a "Reset" button), drop any
897
923
  // accumulated cell-edit overrides - otherwise `getCellDisplayValue`
898
924
  // would keep returning the old edited values from `editedCellValues`
899
- // even though the underlying data has been replaced.
900
- internalData = props.data ?? EMPTY_DATA;
901
- editedCellValues = {};
925
+ // even though the underlying data has been replaced. Growing or shrinking
926
+ // the array is not a reset, so it keeps them.
927
+ if (replaced) editedCellValues = {};
902
928
  });
903
929
  $effect(() => {
904
930
  internalColumns = resolveCols(props.columns);
@@ -1692,15 +1718,21 @@ export function createSvGridController<
1692
1718
 
1693
1719
  let rows = rawRows;
1694
1720
  if (globalFilter.trim()) {
1695
- const needle = normalizeForFilter(globalFilter, (props.filterLocale ?? props.localization?.locale));
1696
- rows = rows.filter((row) =>
1697
- row
1698
- .getAllCells()
1699
- .some((cell) =>
1700
- normalizeForFilter(String(cell.getValue() ?? ""), (props.filterLocale ?? props.localization?.locale))
1701
- .includes(needle),
1702
- ),
1703
- );
1721
+ const locale = props.filterLocale ?? props.localization?.locale;
1722
+ const needle = normalizeForFilter(globalFilter, locale);
1723
+ // Read the values, not the cells. `getAllCells()` built a cell object
1724
+ // with two closures per column on EVERY row and kept them on the row:
1725
+ // ten million objects for one keystroke over a million rows, and a
1726
+ // search that froze the page for seconds. `cell.getValue()` and
1727
+ // `getCellValueByColumnId` read the same per-row values array, so the
1728
+ // answer is the same. The columns are the ones the rows were built with.
1729
+ const columnIds = grid.getAllColumns().map((c) => c.id);
1730
+ rows = rows.filter((row) => {
1731
+ for (const id of columnIds) {
1732
+ if (normalizeForFilter(String(row.getCellValueByColumnId(id) ?? ""), locale).includes(needle)) return true;
1733
+ }
1734
+ return false;
1735
+ });
1704
1736
  }
1705
1737
 
1706
1738
  rows = applyColumnFilters(rows);
package/src/SvGrid.css CHANGED
@@ -1387,9 +1387,6 @@ select.sv-grid-fr-editor {
1387
1387
  .sv-grid-cell-active,
1388
1388
  .sv-grid-cell-editing {
1389
1389
  position: relative;
1390
- /* High enough to sit above pinned-column stacking contexts (z=4) so the
1391
- list/chips popover overlays adjacent cells instead of being clipped. */
1392
- z-index: 20;
1393
1390
  /* `box-shadow: inset` is bound to the cell's own box, so the ring
1394
1391
  clips cleanly with the cell when it's partially scrolled out of
1395
1392
  view (and shows up consistently at the right edge of a virtualized
@@ -1399,10 +1396,24 @@ select.sv-grid-fr-editor {
1399
1396
  inconsistent at scroll boundaries. */
1400
1397
  box-shadow: inset 0 0 0 2px var(--sg-accent, #0b63f3);
1401
1398
  outline: none;
1402
- /* Let list/chips popouts extend past the cell box (default cells have
1403
- overflow: hidden so the chips picker would otherwise be clipped). */
1399
+ }
1400
+ /* Only the EDITING cell escapes its own box and outranks the pinned-column
1401
+ stacking contexts (z=4), so its list/chips popout overlays the neighbours
1402
+ instead of being clipped. The merely-ACTIVE cell used to get the same
1403
+ treatment, and on a grid scrolled sideways that made it paint straight over
1404
+ the sticky selection / row-number column: the checkbox disappeared and the
1405
+ cell's text - unclipped, because overflow was visible - spilled across it.
1406
+ A grid seeds its active cell at (0,0), so this hit the FIRST row of any
1407
+ horizontally scrolled grid with `showRowSelection`, untouched by the user. */
1408
+ .sv-grid-cell-editing {
1409
+ z-index: 20;
1404
1410
  overflow: visible;
1405
1411
  }
1412
+ /* Above ordinary cells so the ring is not overdrawn by a neighbour, below the
1413
+ sticky columns (z=4) so those keep covering it as they should. */
1414
+ .sv-grid-cell-active:not(.sv-grid-cell-editing) {
1415
+ z-index: 3;
1416
+ }
1406
1417
  /* The active cell a grid mounts with is seeded at (0,0), not chosen: no
1407
1418
  ring until a click, an arrow key or the api activates a cell (the same
1408
1419
  gate the fill handle has always had). Once activated it stays through a