@svgrid/grid 2.6.19 → 2.6.21
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/dist/SvGrid.controller.svelte.d.ts +1 -0
- package/dist/SvGrid.controller.svelte.js +64 -2
- package/dist/SvGrid.svelte +2 -1
- package/dist/SvGrid.types.d.ts +150 -0
- package/dist/ai.d.ts +28 -0
- package/dist/ai.js +6 -0
- package/dist/cdn/{GridMenus-B0F9iBrG.js → GridMenus-BfTAKn84.js} +1 -1
- package/dist/cdn/{GridMenus-IHK_l7m6.js → GridMenus-C3bJd7w8.js} +1 -1
- package/dist/cdn/{src-Cd0tearp.js → src-BYq-qyrp.js} +1012 -999
- package/dist/cdn/{src-B1TdiyS8.js → src-DBel9wRZ.js} +1324 -1311
- package/dist/cdn/svgrid.js +1 -1
- package/dist/cdn/svgrid.svelte-external.js +1 -1
- package/dist/cdn/validate-_CDJzgIo.js +75 -0
- package/dist/cell-formatting.d.ts +2 -0
- package/dist/cell-formatting.js +2 -0
- package/dist/chart-export.d.ts +1 -0
- package/dist/chart.d.ts +31 -5
- package/dist/chart.js +9 -3
- package/dist/core.d.ts +197 -0
- package/dist/core.js +72 -0
- package/dist/createTree.svelte.d.ts +3 -0
- package/dist/createTree.svelte.js +1 -0
- package/dist/datetime/date-core.d.ts +2 -0
- package/dist/datetime/date-restrict.d.ts +1 -0
- package/dist/datetime/timezone.d.ts +1 -0
- package/dist/dock-manager-model.d.ts +3 -0
- package/dist/dock-manager-model.js +1 -0
- package/dist/dock-model.d.ts +6 -0
- package/dist/dock-model.js +3 -0
- package/dist/editor-contract.d.ts +1 -0
- package/dist/list-option.d.ts +1 -0
- package/dist/positioning.d.ts +2 -0
- package/dist/scheduler-ical.d.ts +1 -0
- package/dist/scheduler-model.d.ts +1 -0
- package/dist/summaries.js +22 -0
- package/dist/svgrid-wrapper.types.d.ts +5 -0
- package/dist/toast-store.svelte.d.ts +4 -0
- package/dist/validate.d.ts +50 -0
- package/dist/validate.js +187 -0
- package/package.json +4 -1
- package/src/SvGrid.controller.svelte.ts +68 -2
- package/src/SvGrid.svelte +2 -1
- package/src/SvGrid.types.ts +150 -0
- package/src/ai.ts +28 -0
- package/src/cell-formatting.ts +2 -0
- package/src/chart-export.ts +1 -0
- package/src/chart.ts +31 -5
- package/src/core.ts +207 -0
- package/src/createTree.svelte.ts +3 -0
- package/src/datetime/date-core.ts +2 -0
- package/src/datetime/date-restrict.ts +1 -0
- package/src/datetime/timezone.ts +1 -0
- package/src/dock-manager-model.ts +3 -0
- package/src/dock-model.ts +6 -0
- package/src/editor-contract.ts +1 -0
- package/src/list-option.ts +1 -0
- package/src/positioning.ts +2 -0
- package/src/scheduler-ical.ts +1 -0
- package/src/scheduler-model.ts +1 -0
- package/src/summaries.ts +21 -0
- package/src/svgrid-wrapper.types.ts +5 -0
- package/src/svgrid.summaries.test.ts +217 -0
- package/src/toast-store.svelte.ts +4 -0
- package/src/validate.test.ts +207 -0
- package/src/validate.ts +269 -0
package/dist/positioning.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export type Viewport = {
|
|
|
37
37
|
width: number;
|
|
38
38
|
height: number;
|
|
39
39
|
};
|
|
40
|
+
/** Where to put a floating element: its preferred side, offsets, and collision behaviour. */
|
|
40
41
|
export type ComputePositionOptions = {
|
|
41
42
|
/** Preferred placement. Default `'bottom-start'`. */
|
|
42
43
|
placement?: Placement;
|
|
@@ -61,6 +62,7 @@ export type ComputePositionOptions = {
|
|
|
61
62
|
/** Floor for the available main-axis size returned by `size`. Default 0. */
|
|
62
63
|
minMainAxis?: number;
|
|
63
64
|
};
|
|
65
|
+
/** The resolved position, including the side actually used after collision handling. */
|
|
64
66
|
export type ComputePositionResult = {
|
|
65
67
|
/** Floating left, in viewport (fixed-position) coordinates. */
|
|
66
68
|
x: number;
|
package/dist/scheduler-ical.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* LOCATION. No `Date.now()` in the core - `toICS` accepts a stamp.
|
|
7
7
|
*/
|
|
8
8
|
import type { RecurrenceRule } from './recurrence.js';
|
|
9
|
+
/** One VEVENT: the fields SvGrid reads from and writes to an iCalendar feed. */
|
|
9
10
|
export type ICalEvent = {
|
|
10
11
|
uid?: string;
|
|
11
12
|
title: string;
|
|
@@ -171,6 +171,7 @@ export type DayLayout<TData = unknown> = {
|
|
|
171
171
|
events: PositionedEvent<TData>[];
|
|
172
172
|
overflows: OverflowMarker<TData>[];
|
|
173
173
|
};
|
|
174
|
+
/** Tuning for event layout: how overlapping events share horizontal space. */
|
|
174
175
|
export type LayoutOptions = {
|
|
175
176
|
dayStartHour?: number;
|
|
176
177
|
dayEndHour?: number;
|
package/dist/summaries.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// summaries handlers extracted from the controller. Imperative event handlers
|
|
2
|
+
// reading/writing controller state via the `ctx` handle; the reactive core
|
|
3
|
+
// ($state/$derived/$effect) stays in the controller.
|
|
4
|
+
import { applyGroupAggregate, } from "./index.js";
|
|
1
5
|
import "./sv-grid-scrollbar.js";
|
|
2
6
|
import { getCellKey, } from "./SvGrid.helpers.js";
|
|
3
7
|
import { formatSummaryNumeric, } from "./cell-values.js";
|
|
@@ -25,6 +29,24 @@ export function createSummaries(ctx) {
|
|
|
25
29
|
const fieldFn = def.fieldFn;
|
|
26
30
|
const field = def.field;
|
|
27
31
|
const columnId = column.id;
|
|
32
|
+
// A column that declares its own `summary` opts out of the default
|
|
33
|
+
// sum/count below. Only that column pays for the aggregator dispatch, so
|
|
34
|
+
// a grid that declares none keeps the original hot loop exactly as it was.
|
|
35
|
+
const declared = def.summary;
|
|
36
|
+
if (declared !== undefined) {
|
|
37
|
+
if (declared === false) {
|
|
38
|
+
summary[columnId] = "";
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const value = applyGroupAggregate(declared, columnId, rows);
|
|
42
|
+
summary[columnId] =
|
|
43
|
+
typeof value === "number" && Number.isFinite(value)
|
|
44
|
+
? formatSummaryNumeric(column, value)
|
|
45
|
+
: value == null
|
|
46
|
+
? ""
|
|
47
|
+
: String(value);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
28
50
|
let numericSum = 0;
|
|
29
51
|
let numericCount = 0;
|
|
30
52
|
for (let i = 0; i < rowCount; i += 1) {
|
|
@@ -3,6 +3,7 @@ import type { FilterOperator, Props } from './SvGrid.types.js';
|
|
|
3
3
|
import type { GridExportOptions, GridClipboardOptions } from './export-format.js';
|
|
4
4
|
import type { ChartSpec, ChartType } from './chart.js';
|
|
5
5
|
import type { GridPredicateExpr } from './filtering/predicate-expr.js';
|
|
6
|
+
/** The comparisons a column filter can use, as shown in the filter menu. */
|
|
6
7
|
export type SvGridFilterOperator = FilterOperator;
|
|
7
8
|
/**
|
|
8
9
|
* A serializable snapshot of everything that makes up the current "view":
|
|
@@ -80,6 +81,7 @@ export type SvGridTransaction<TData> = {
|
|
|
80
81
|
update?: ReadonlyArray<TData>;
|
|
81
82
|
remove?: ReadonlyArray<TData | string>;
|
|
82
83
|
};
|
|
84
|
+
/** What a batched `applyTransaction` did: how many rows were added, updated and removed. */
|
|
83
85
|
export type SvGridTransactionResult = {
|
|
84
86
|
added: number;
|
|
85
87
|
updated: number;
|
|
@@ -458,6 +460,7 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
458
460
|
/** Force a recompute of the row pipeline + a re-render. */
|
|
459
461
|
refresh(): void;
|
|
460
462
|
};
|
|
463
|
+
/** The props `<SvGrid>` accepts. See the SvGrid reference for the full list with defaults. */
|
|
461
464
|
export type SvGridWrapperProps<TFeatures extends TableFeatures, TData extends RowData> = {
|
|
462
465
|
data: ReadonlyArray<TData>;
|
|
463
466
|
columns: Array<ColumnDef<TFeatures, TData>>;
|
|
@@ -505,6 +508,8 @@ export type SvGridWrapperProps<TFeatures extends TableFeatures, TData extends Ro
|
|
|
505
508
|
enableCellSelection?: boolean;
|
|
506
509
|
enableInlineEditing?: boolean;
|
|
507
510
|
enableRowSummaries?: boolean;
|
|
511
|
+
/** Shortcut alias for `enableRowSummaries`; wins when both are set. */
|
|
512
|
+
summary?: boolean;
|
|
508
513
|
/** Receives the imperative grid API when the component is ready. */
|
|
509
514
|
onApiReady?: (api: SvGridApi<TFeatures, TData>) => void;
|
|
510
515
|
};
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* Mount a single <SvToaster /> once near the app root to render the queue.
|
|
15
15
|
*/
|
|
16
16
|
import type { Snippet } from 'svelte';
|
|
17
|
+
/** A toast's severity, which selects its colour and icon. */
|
|
17
18
|
export type ToastVariant = 'info' | 'success' | 'warning' | 'error';
|
|
18
19
|
/** A button rendered inside a toast (a primary `action` or a secondary `cancel`). */
|
|
19
20
|
export type ToastAction = {
|
|
@@ -23,6 +24,7 @@ export type ToastAction = {
|
|
|
23
24
|
/** Keep the toast open after the click (default: dismiss it). */
|
|
24
25
|
keepOpen?: boolean;
|
|
25
26
|
};
|
|
27
|
+
/** Options for one toast: its variant, how long it stays, and any action button. */
|
|
26
28
|
export type ToastOptions = {
|
|
27
29
|
variant?: ToastVariant;
|
|
28
30
|
/** Auto-dismiss after N ms. `0` = sticky (dismiss manually). Default 4000. */
|
|
@@ -38,6 +40,7 @@ export type ToastOptions = {
|
|
|
38
40
|
/** Render a fully custom toast body instead of the icon + title + message. */
|
|
39
41
|
render?: Snippet<[Toast]>;
|
|
40
42
|
};
|
|
43
|
+
/** A live toast: its options plus the id needed to dismiss it. */
|
|
41
44
|
export type Toast = {
|
|
42
45
|
id: number;
|
|
43
46
|
message: string;
|
|
@@ -58,6 +61,7 @@ export type PromiseMessages<T> = {
|
|
|
58
61
|
error: string | ((error: unknown) => string);
|
|
59
62
|
};
|
|
60
63
|
type VariantOptions = Omit<ToastOptions, 'variant'>;
|
|
64
|
+
/** The callable toast API - `toast(msg)` plus `.success` / `.error` / friends. */
|
|
61
65
|
export type ToastFn = {
|
|
62
66
|
(message: string, options?: ToastOptions): number;
|
|
63
67
|
info: (message: string, options?: VariantOptions) => number;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development-time configuration checks.
|
|
3
|
+
*
|
|
4
|
+
* The grid used to fail silently on the most common mistakes. A misspelled
|
|
5
|
+
* `field` rendered a column of empty cells with nothing on the console; a
|
|
6
|
+
* `pageSize` with no pagination was quietly ignored; a column marked
|
|
7
|
+
* `sortable` with no sorting enabled just did not sort. Each of those costs
|
|
8
|
+
* someone an afternoon, and none of them were detectable from the outside.
|
|
9
|
+
*
|
|
10
|
+
* This module is the check. It is pure and returns messages rather than
|
|
11
|
+
* printing them, so it can be unit-tested; the controller runs it in an effect
|
|
12
|
+
* and prints each message once, in dev builds only.
|
|
13
|
+
*
|
|
14
|
+
* Rules for anything added here:
|
|
15
|
+
* - Never fire on valid config. A false positive teaches people to ignore
|
|
16
|
+
* warnings, which is worse than staying silent.
|
|
17
|
+
* - Say what happened, what it means, and how to fix it - the house style set
|
|
18
|
+
* by the one pre-existing warning in server-data-source.ts.
|
|
19
|
+
* - Stay O(columns), or O(columns x a fixed row sample). This runs on data
|
|
20
|
+
* changes, and must not scale with row count.
|
|
21
|
+
*/
|
|
22
|
+
import type { ColumnDef, RowData, TableFeatures } from './core.js';
|
|
23
|
+
export type ValidateInput<TFeatures extends TableFeatures, TData extends RowData> = {
|
|
24
|
+
data: ReadonlyArray<TData>;
|
|
25
|
+
columns: ReadonlyArray<ColumnDef<TFeatures, TData>>;
|
|
26
|
+
/** The resolved feature set, after the boolean shortcuts have injected theirs. */
|
|
27
|
+
features?: Record<string, unknown>;
|
|
28
|
+
sortable?: boolean;
|
|
29
|
+
pageable?: boolean;
|
|
30
|
+
showPagination?: boolean;
|
|
31
|
+
pageSize?: number;
|
|
32
|
+
groupBy?: ReadonlyArray<string>;
|
|
33
|
+
treeData?: {
|
|
34
|
+
parentField?: string;
|
|
35
|
+
idField?: string;
|
|
36
|
+
column?: string;
|
|
37
|
+
};
|
|
38
|
+
initialColumnPinning?: {
|
|
39
|
+
left?: ReadonlyArray<string>;
|
|
40
|
+
right?: ReadonlyArray<string>;
|
|
41
|
+
};
|
|
42
|
+
columnVirtualization?: boolean;
|
|
43
|
+
externalPagination?: boolean;
|
|
44
|
+
rowCount?: number;
|
|
45
|
+
externalSort?: boolean;
|
|
46
|
+
onSortingChange?: unknown;
|
|
47
|
+
externalFilter?: boolean;
|
|
48
|
+
onFiltersChange?: unknown;
|
|
49
|
+
};
|
|
50
|
+
export declare function validateGridConfig<TFeatures extends TableFeatures, TData extends RowData>(input: ValidateInput<TFeatures, TData>): string[];
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** How many rows to sample when deciding whether a `field` exists. */
|
|
2
|
+
const FIELD_SAMPLE_ROWS = 10;
|
|
3
|
+
const DOCS = 'https://svgrid.com/docs/getting-started/3-data-and-columns/';
|
|
4
|
+
/** Every leaf column, flattened through group columns. */
|
|
5
|
+
function leaves(columns, out = []) {
|
|
6
|
+
for (const col of columns) {
|
|
7
|
+
if (col.columns?.length)
|
|
8
|
+
leaves(col.columns, out);
|
|
9
|
+
else
|
|
10
|
+
out.push(col);
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
export function validateGridConfig(input) {
|
|
15
|
+
const messages = [];
|
|
16
|
+
const cols = leaves(input.columns ?? []);
|
|
17
|
+
// ---- 1. A `field` that does not exist on the data -------------------------
|
|
18
|
+
// The single most expensive silent failure: the column renders, and every
|
|
19
|
+
// cell in it is blank. Sampled across several rows so genuinely sparse data
|
|
20
|
+
// (a key absent from row 0 but present later) does not trip it.
|
|
21
|
+
const sample = (input.data ?? []).slice(0, FIELD_SAMPLE_ROWS);
|
|
22
|
+
if (sample.length) {
|
|
23
|
+
const known = new Set();
|
|
24
|
+
for (const row of sample) {
|
|
25
|
+
if (row && typeof row === 'object')
|
|
26
|
+
for (const k of Object.keys(row))
|
|
27
|
+
known.add(k);
|
|
28
|
+
}
|
|
29
|
+
for (const col of cols) {
|
|
30
|
+
const field = col.field;
|
|
31
|
+
// `fieldFn` computes its value, and an id-only column (actions, buttons)
|
|
32
|
+
// never reads the row - neither needs a matching key.
|
|
33
|
+
if (!field || col.fieldFn)
|
|
34
|
+
continue;
|
|
35
|
+
if (known.has(field))
|
|
36
|
+
continue;
|
|
37
|
+
const guess = nearest(field, [...known]);
|
|
38
|
+
messages.push(`[svgrid] Column field "${field}" does not exist on your row data, so that ` +
|
|
39
|
+
`column renders empty.${guess ? ` Did you mean "${guess}"?` : ''} ` +
|
|
40
|
+
`Available keys: ${[...known].slice(0, 12).join(', ')}. See ${DOCS}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// ---- 2. Duplicate column ids ---------------------------------------------
|
|
44
|
+
// Two columns resolving to the same id makes selection, sorting and column
|
|
45
|
+
// state address the wrong one, with no error anywhere.
|
|
46
|
+
const seen = new Map();
|
|
47
|
+
for (const col of cols) {
|
|
48
|
+
const id = (col.id ?? col.field);
|
|
49
|
+
if (!id)
|
|
50
|
+
continue;
|
|
51
|
+
seen.set(id, (seen.get(id) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
for (const [id, n] of seen) {
|
|
54
|
+
if (n > 1) {
|
|
55
|
+
messages.push(`[svgrid] ${n} columns share the id "${id}". Column state (sorting, ` +
|
|
56
|
+
`filtering, pinning, width) is keyed by id, so they will act as one. ` +
|
|
57
|
+
`Give each an explicit \`id\`. See ${DOCS}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ---- 3. `pageSize` with pagination never asked for ------------------------
|
|
61
|
+
// Only when BOTH pagination props are absent. `pageable={false}` is a
|
|
62
|
+
// deliberate statement - often a bound toggle that flips on later - whereas
|
|
63
|
+
// an unset prop means the author never considered it. Warning on the former
|
|
64
|
+
// fired on our own shortcut-config demo, which is exactly the kind of noise
|
|
65
|
+
// that teaches people to ignore warnings.
|
|
66
|
+
const paginationUnset = input.pageable === undefined && input.showPagination === undefined;
|
|
67
|
+
if (input.pageSize !== undefined && paginationUnset) {
|
|
68
|
+
messages.push('[svgrid] `pageSize` is set but pagination was never turned on, so it has ' +
|
|
69
|
+
'no effect and every row renders. Add `pageable` to switch the pager on.');
|
|
70
|
+
}
|
|
71
|
+
// ---- 4. A column asks to sort, but nothing enables sorting ----------------
|
|
72
|
+
// `sortable` on the grid injects rowSortingFeature, so only warn when neither
|
|
73
|
+
// the shortcut nor an explicitly registered feature is present.
|
|
74
|
+
const sortingEnabled = input.sortable === true || Boolean(input.features?.rowSortingFeature);
|
|
75
|
+
if (!sortingEnabled && cols.some((c) => c.sortable === true)) {
|
|
76
|
+
messages.push('[svgrid] A column sets `sortable: true`, but sorting is not enabled on the ' +
|
|
77
|
+
'grid, so its header does nothing. Add `sortable` to <SvGrid> (it registers ' +
|
|
78
|
+
'the sorting feature for you).');
|
|
79
|
+
}
|
|
80
|
+
// ---- 5. Column ids referenced by other props ------------------------------
|
|
81
|
+
// `groupBy` and `treeData.column` address columns by id. A name that matches
|
|
82
|
+
// nothing is silently ignored, so grouping or the tree expander just never
|
|
83
|
+
// appears and there is nothing to debug against.
|
|
84
|
+
const columnIds = new Set();
|
|
85
|
+
for (const col of cols) {
|
|
86
|
+
const id = (col.id ?? col.field);
|
|
87
|
+
if (id)
|
|
88
|
+
columnIds.add(id);
|
|
89
|
+
}
|
|
90
|
+
for (const id of input.groupBy ?? []) {
|
|
91
|
+
if (columnIds.has(id))
|
|
92
|
+
continue;
|
|
93
|
+
const guess = nearest(id, [...columnIds]);
|
|
94
|
+
messages.push(`[svgrid] \`groupBy\` refers to column "${id}", which does not exist, so ` +
|
|
95
|
+
`that grouping level is ignored.${guess ? ` Did you mean "${guess}"?` : ''} ` +
|
|
96
|
+
`Column ids: ${[...columnIds].slice(0, 12).join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
const treeColumn = input.treeData?.column;
|
|
99
|
+
if (treeColumn && !columnIds.has(treeColumn)) {
|
|
100
|
+
messages.push(`[svgrid] \`treeData.column\` refers to column "${treeColumn}", which does ` +
|
|
101
|
+
`not exist, so the expander falls back to the first visible column.`);
|
|
102
|
+
}
|
|
103
|
+
// ---- 6. treeData pointing at fields the data does not have ----------------
|
|
104
|
+
if (sample.length && input.treeData) {
|
|
105
|
+
const known = new Set();
|
|
106
|
+
for (const row of sample) {
|
|
107
|
+
if (row && typeof row === 'object')
|
|
108
|
+
for (const k of Object.keys(row))
|
|
109
|
+
known.add(k);
|
|
110
|
+
}
|
|
111
|
+
const parentField = input.treeData.parentField;
|
|
112
|
+
if (parentField && !known.has(parentField)) {
|
|
113
|
+
const guess = nearest(parentField, [...known]);
|
|
114
|
+
messages.push(`[svgrid] \`treeData.parentField\` is "${parentField}", which is not on ` +
|
|
115
|
+
`your row data, so every row becomes a root and no hierarchy appears.` +
|
|
116
|
+
`${guess ? ` Did you mean "${guess}"?` : ''}`);
|
|
117
|
+
}
|
|
118
|
+
// `idField` defaults to 'id'; only check what was asked for explicitly.
|
|
119
|
+
const idField = input.treeData.idField;
|
|
120
|
+
if (idField && !known.has(idField)) {
|
|
121
|
+
messages.push(`[svgrid] \`treeData.idField\` is "${idField}", which is not on your row ` +
|
|
122
|
+
`data, so parent lookups cannot match and the tree stays flat.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ---- 7. Pinning that column virtualization will hide ----------------------
|
|
126
|
+
// Documented incompatibility: the virtualizer recycles column DOM nodes, so
|
|
127
|
+
// sticky pinning cannot survive it. `columnVirtualization` defaults to ON,
|
|
128
|
+
// which means the natural way to write this silently does nothing.
|
|
129
|
+
const pinned = (input.initialColumnPinning?.left?.length ?? 0) +
|
|
130
|
+
(input.initialColumnPinning?.right?.length ?? 0);
|
|
131
|
+
if (pinned > 0 && input.columnVirtualization !== false) {
|
|
132
|
+
messages.push('[svgrid] `initialColumnPinning` is set while column virtualization is on ' +
|
|
133
|
+
'(its default), so the pinned columns will not stick - the virtualizer ' +
|
|
134
|
+
'recycles column nodes. Add `columnVirtualization={false}`.');
|
|
135
|
+
}
|
|
136
|
+
// ---- 8. Server-mode contracts left half-wired -----------------------------
|
|
137
|
+
// Each of these makes the grid hand control to the consumer. Miss the other
|
|
138
|
+
// half and the feature looks broken rather than unconfigured.
|
|
139
|
+
if (input.externalPagination === true && input.rowCount === undefined) {
|
|
140
|
+
messages.push('[svgrid] `externalPagination` is on but `rowCount` is not set, so the pager ' +
|
|
141
|
+
'cannot know how many pages exist. Pass the server total as `rowCount`.');
|
|
142
|
+
}
|
|
143
|
+
if (input.externalSort === true && !input.onSortingChange) {
|
|
144
|
+
messages.push('[svgrid] `externalSort` is on but there is no `onSortingChange` handler, so ' +
|
|
145
|
+
'clicking a header changes nothing - the grid stopped sorting and nobody ' +
|
|
146
|
+
'is listening. Add `onSortingChange` and re-fetch in your handler.');
|
|
147
|
+
}
|
|
148
|
+
if (input.externalFilter === true && !input.onFiltersChange) {
|
|
149
|
+
messages.push('[svgrid] `externalFilter` is on but there is no `onFiltersChange` handler, ' +
|
|
150
|
+
'so filtering the grid changes nothing. Add `onFiltersChange` and re-fetch ' +
|
|
151
|
+
'in your handler.');
|
|
152
|
+
}
|
|
153
|
+
return messages;
|
|
154
|
+
}
|
|
155
|
+
/** Closest key by edit distance, for a "did you mean" hint. Undefined if none is close. */
|
|
156
|
+
function nearest(target, candidates) {
|
|
157
|
+
let best;
|
|
158
|
+
let bestScore = Infinity;
|
|
159
|
+
for (const c of candidates) {
|
|
160
|
+
const d = distance(target.toLowerCase(), c.toLowerCase());
|
|
161
|
+
if (d < bestScore) {
|
|
162
|
+
bestScore = d;
|
|
163
|
+
best = c;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Only suggest a genuinely near miss - a third of the length, at most 3 edits.
|
|
167
|
+
const limit = Math.min(3, Math.floor(target.length / 3) + 1);
|
|
168
|
+
return bestScore <= limit ? best : undefined;
|
|
169
|
+
}
|
|
170
|
+
/** Levenshtein distance, iterative single-row. Inputs here are identifier-length. */
|
|
171
|
+
function distance(a, b) {
|
|
172
|
+
if (a === b)
|
|
173
|
+
return 0;
|
|
174
|
+
const prev = new Array(b.length + 1);
|
|
175
|
+
for (let j = 0; j <= b.length; j++)
|
|
176
|
+
prev[j] = j;
|
|
177
|
+
for (let i = 1; i <= a.length; i++) {
|
|
178
|
+
let carry = prev[0];
|
|
179
|
+
prev[0] = i;
|
|
180
|
+
for (let j = 1; j <= b.length; j++) {
|
|
181
|
+
const next = Math.min(prev[j] + 1, prev[j - 1] + 1, carry + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
182
|
+
carry = prev[j];
|
|
183
|
+
prev[j] = next;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return prev[b.length];
|
|
187
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svgrid/grid",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.21",
|
|
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",
|
|
@@ -138,5 +138,8 @@
|
|
|
138
138
|
},
|
|
139
139
|
"peerDependencies": {
|
|
140
140
|
"svelte": "^5.0.0"
|
|
141
|
+
},
|
|
142
|
+
"dependencies": {
|
|
143
|
+
"esm-env": "^1.2.2"
|
|
141
144
|
}
|
|
142
145
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
resolveMaxDomHeight,
|
|
33
33
|
} from "./virtualization/scroll-scaling";
|
|
34
34
|
import { buildPreMeasureItems } from "./virtualization/virtualizer";
|
|
35
|
+
import { DEV } from "esm-env";
|
|
35
36
|
import "./sv-grid-scrollbar";
|
|
36
37
|
import {
|
|
37
38
|
computeColumnStat,
|
|
@@ -61,6 +62,7 @@ import {
|
|
|
61
62
|
import {
|
|
62
63
|
createSummaries,
|
|
63
64
|
} from "./summaries";
|
|
65
|
+
|
|
64
66
|
import {
|
|
65
67
|
createMenus,
|
|
66
68
|
} from "./menus";
|
|
@@ -606,6 +608,17 @@ export function createSvGridController<
|
|
|
606
608
|
(props.selectionMode ?? "both") === "both"),
|
|
607
609
|
);
|
|
608
610
|
|
|
611
|
+
// The aggregate footer row. `summary` is the shortcut alias, so it wins over
|
|
612
|
+
// the fine-grained prop - the same precedence `selectable` uses.
|
|
613
|
+
//
|
|
614
|
+
// It starts OFF. It used to default ON, which meant a plain
|
|
615
|
+
// `<SvGrid {data} {columns} />` grew a totals row nobody asked for, and every
|
|
616
|
+
// caller that did not want one had to say so: 375 call sites in this repo
|
|
617
|
+
// passed `enableRowSummaries={false}` against 6 that opted in.
|
|
618
|
+
const rowSummariesEnabled = $derived(
|
|
619
|
+
props.summary ?? props.enableRowSummaries ?? false,
|
|
620
|
+
);
|
|
621
|
+
|
|
609
622
|
|
|
610
623
|
|
|
611
624
|
// Internal source-of-truth for data and column defs. Seeded from props and
|
|
@@ -2583,7 +2596,7 @@ export function createSvGridController<
|
|
|
2583
2596
|
*/
|
|
2584
2597
|
const eagerSummaries = $derived.by(() => {
|
|
2585
2598
|
if (!summarize) return null;
|
|
2586
|
-
if (!
|
|
2599
|
+
if (!rowSummariesEnabled) return null;
|
|
2587
2600
|
const rows = allRows;
|
|
2588
2601
|
const columns = allColumns;
|
|
2589
2602
|
if (rows.length * columns.length > SUMMARY_DEFER_CELL_LIMIT) return null;
|
|
@@ -2607,7 +2620,7 @@ export function createSvGridController<
|
|
|
2607
2620
|
void editedCellValues;
|
|
2608
2621
|
const rows = allRows;
|
|
2609
2622
|
const columns = allColumns;
|
|
2610
|
-
if (!
|
|
2623
|
+
if (!rowSummariesEnabled) {
|
|
2611
2624
|
deferredSummaries = {};
|
|
2612
2625
|
return;
|
|
2613
2626
|
}
|
|
@@ -3499,6 +3512,7 @@ export function createSvGridController<
|
|
|
3499
3512
|
get showColumnFiltersEffective() { return showColumnFiltersEffective; },
|
|
3500
3513
|
get showInlineColumnFilterEffective() { return showInlineColumnFilterEffective; },
|
|
3501
3514
|
get showRowSelectionEffective() { return showRowSelectionEffective; },
|
|
3515
|
+
get rowSummariesEnabled() { return rowSummariesEnabled; },
|
|
3502
3516
|
get enableCellSelectionEffective() { return enableCellSelectionEffective; },
|
|
3503
3517
|
get flushScheduledScrollSync() { return flushScheduledScrollSync; },
|
|
3504
3518
|
get scheduleScrollSync() { return scheduleScrollSync; },
|
|
@@ -3945,6 +3959,58 @@ export function createSvGridController<
|
|
|
3945
3959
|
const { buildApi } = createGridApi<TFeatures, TData>(ctx);
|
|
3946
3960
|
const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
|
|
3947
3961
|
|
|
3962
|
+
// Dev-time configuration checks. Silent misconfiguration was the grid's
|
|
3963
|
+
// biggest usability gap - a misspelled `field` rendered a column of blank
|
|
3964
|
+
// cells and printed nothing, and an inert `pageSize` was simply ignored.
|
|
3965
|
+
//
|
|
3966
|
+
// `validateGridConfig` is pure (see validate.ts) so it can be unit-tested;
|
|
3967
|
+
// this only decides when to run it, and prints each distinct message once.
|
|
3968
|
+
//
|
|
3969
|
+
// It is loaded with `import()` rather than a static import so the checks live
|
|
3970
|
+
// in a lazy chunk. The base bundle is size-budgeted and already sits at its
|
|
3971
|
+
// limit, so a static import cost ~1 KB gzip that a production app would carry
|
|
3972
|
+
// and never execute.
|
|
3973
|
+
//
|
|
3974
|
+
// `DEV` comes from `esm-env` because the alternatives do not work here:
|
|
3975
|
+
// `process.env.NODE_ENV` is undefined in Vite's browser runtime (the checks
|
|
3976
|
+
// silently never ran), and Vite's own flag makes svelte-package warn that it
|
|
3977
|
+
// only works for Vite consumers. esm-env resolves through export conditions,
|
|
3978
|
+
// so it is correct in dev and folds to `false` in a production build. It is
|
|
3979
|
+
// already a direct dependency of Svelte, so it adds nothing to an install.
|
|
3980
|
+
if (DEV) {
|
|
3981
|
+
const warnedConfig = new Set<string>();
|
|
3982
|
+
$effect(() => {
|
|
3983
|
+
// Read the reactive values synchronously so the effect tracks them; the
|
|
3984
|
+
// module load below is async and would otherwise register no dependencies.
|
|
3985
|
+
const snapshot = {
|
|
3986
|
+
data: internalData,
|
|
3987
|
+
columns: internalColumns,
|
|
3988
|
+
features: resolveEffectiveFeatures() as Record<string, unknown>,
|
|
3989
|
+
sortable: props.sortable,
|
|
3990
|
+
pageable: props.pageable,
|
|
3991
|
+
showPagination: props.showPagination,
|
|
3992
|
+
pageSize: props.pageSize,
|
|
3993
|
+
groupBy: props.groupBy,
|
|
3994
|
+
treeData: props.treeData,
|
|
3995
|
+
initialColumnPinning: props.initialColumnPinning,
|
|
3996
|
+
columnVirtualization: props.columnVirtualization,
|
|
3997
|
+
externalPagination: props.externalPagination,
|
|
3998
|
+
rowCount: props.rowCount,
|
|
3999
|
+
externalSort: props.externalSort,
|
|
4000
|
+
onSortingChange: props.onSortingChange,
|
|
4001
|
+
externalFilter: props.externalFilter,
|
|
4002
|
+
onFiltersChange: props.onFiltersChange,
|
|
4003
|
+
};
|
|
4004
|
+
void import("./validate").then(({ validateGridConfig }) => {
|
|
4005
|
+
for (const message of validateGridConfig(snapshot as never)) {
|
|
4006
|
+
if (warnedConfig.has(message)) continue;
|
|
4007
|
+
warnedConfig.add(message);
|
|
4008
|
+
console.warn(message);
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
|
|
3948
4014
|
// Aligned grids: register in the shared group on mount, and mirror column
|
|
3949
4015
|
// resizes to peers whenever columnWidths changes. Horizontal-scroll mirroring
|
|
3950
4016
|
// is driven from onBodyScroll (via ctx.broadcastAlignedScroll).
|
package/src/SvGrid.svelte
CHANGED
|
@@ -184,6 +184,7 @@
|
|
|
184
184
|
ctrl.showInlineColumnFilterEffective,
|
|
185
185
|
);
|
|
186
186
|
const showRowSelectionEffective = $derived(ctrl.showRowSelectionEffective);
|
|
187
|
+
const rowSummariesEnabled = $derived(ctrl.rowSummariesEnabled);
|
|
187
188
|
const grid = $derived(ctrl.grid);
|
|
188
189
|
const allColumns = $derived(ctrl.allColumns);
|
|
189
190
|
const headerGroups = $derived(ctrl.headerGroups);
|
|
@@ -2519,7 +2520,7 @@
|
|
|
2519
2520
|
{/each}
|
|
2520
2521
|
</tbody>
|
|
2521
2522
|
{/if}
|
|
2522
|
-
{#if
|
|
2523
|
+
{#if rowSummariesEnabled}
|
|
2523
2524
|
<!-- svelte-ignore a11y_no_redundant_roles -->
|
|
2524
2525
|
<tfoot class="sv-grid-foot" role="rowgroup">
|
|
2525
2526
|
<tr
|