@svgrid/grid 2.6.3 → 2.6.5
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 +2 -1
- package/dist/SvGrid.controller.svelte.js +145 -3
- package/dist/SvGrid.css +32 -0
- package/dist/SvGrid.svelte +64 -6
- package/dist/SvGrid.types.d.ts +61 -0
- package/dist/a11y/grid-announcements.d.ts +58 -0
- package/dist/a11y/grid-announcements.js +70 -0
- package/dist/advanced-filter.svelte.d.ts +42 -0
- package/dist/advanced-filter.svelte.js +13 -0
- package/dist/build-api.js +44 -2
- package/dist/cdn/{GridMenus-BoVKgAv8.js → GridMenus-CBvjL5u4.js} +14 -14
- package/dist/cdn/{GridMenus-DL1zrDwf.js → GridMenus-DAzp6YXc.js} +14 -14
- package/dist/cdn/{src-M2NfKnjX.js → src-3WKN7L4G.js} +5932 -5743
- package/dist/cdn/{src-CEpw6Ato.js → src-Cl7h5-Ux.js} +3371 -3182
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/filter-operators.d.ts +2 -1
- package/dist/filter-operators.js +5 -6
- package/dist/filtering/excel-filters.d.ts +1 -0
- package/dist/filtering/excel-filters.js +5 -0
- package/dist/filtering/filter-operator-catalogue.d.ts +42 -0
- package/dist/filtering/filter-operator-catalogue.js +42 -0
- package/dist/filtering/predicate-expr.d.ts +78 -0
- package/dist/filtering/predicate-expr.js +1 -0
- package/dist/filtering/row-predicate.d.ts +17 -0
- package/dist/filtering/row-predicate.js +55 -0
- package/dist/grid-messages.d.ts +7 -0
- package/dist/grid-messages.js +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/server-data-source.d.ts +34 -0
- package/dist/server-data-source.js +19 -0
- package/dist/svgrid-wrapper.types.d.ts +37 -5
- package/dist/themes/index.js +12 -4
- package/package.json +2 -1
- package/src/SvGrid.controller.svelte.ts +169 -2
- package/src/SvGrid.css +32 -0
- package/src/SvGrid.svelte +64 -6
- package/src/SvGrid.types.ts +61 -0
- package/src/a11y/grid-announcements.test.ts +78 -0
- package/src/a11y/grid-announcements.ts +94 -0
- package/src/a11y.announce.test.ts +250 -0
- package/src/a11y.axe.test.ts +194 -0
- package/src/advanced-filter.svelte.ts +59 -0
- package/src/build-api.ts +43 -2
- package/src/filter-operators.ts +9 -6
- package/src/filtering/excel-filters.ts +15 -0
- package/src/filtering/filter-operator-catalogue.ts +74 -0
- package/src/filtering/node-entry.test.ts +75 -0
- package/src/filtering/predicate-expr.ts +50 -0
- package/src/filtering/row-predicate.ts +57 -0
- package/src/grid-messages.ts +18 -0
- package/src/index.ts +28 -0
- package/src/server-data-source.test.ts +94 -0
- package/src/server-data-source.ts +56 -0
- package/src/svgrid-wrapper.types.ts +37 -5
- package/src/svgrid.advanced-filter.test.ts +481 -0
- package/src/svgrid.displayed-rows.test.ts +144 -0
- package/src/svgrid.group-props-reactive.svelte.test.ts +151 -0
- package/src/svgrid.group-props.test.ts +280 -0
- package/src/themes/contrast.test.ts +112 -0
- package/src/themes/index.ts +12 -4
- package/themes/catppuccin.css +3 -3
- package/themes/dracula.css +3 -3
- package/themes/nord.css +3 -3
- package/themes/notion.css +4 -4
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply a row predicate across a depth-ordered row list, keeping group banners
|
|
3
|
+
* consistent with what survives beneath them.
|
|
4
|
+
*
|
|
5
|
+
* The row list interleaves group banners with their descendants, banner first.
|
|
6
|
+
* A predicate written by a user is about DATA, so it is only tested against
|
|
7
|
+
* leaf rows; a banner then survives only if something under it did. Testing a
|
|
8
|
+
* banner directly would drop whole groups because its cells are aggregates, not
|
|
9
|
+
* row values.
|
|
10
|
+
*
|
|
11
|
+
* Note the existing column-filter and facet stages do test banners directly.
|
|
12
|
+
* That is a separate pre-existing inconsistency; this helper is written to be
|
|
13
|
+
* reusable if those are ever brought in line, but changing them is deliberately
|
|
14
|
+
* not bundled into the advanced filter.
|
|
15
|
+
*/
|
|
16
|
+
/** Apply `keep` to leaf rows, dropping banners left with no surviving child. */
|
|
17
|
+
export function applyRowPredicate(rows, keep, isGroup) {
|
|
18
|
+
// Fast path: no grouping in play, so every row is a leaf.
|
|
19
|
+
let hasGroup = false;
|
|
20
|
+
for (const row of rows) {
|
|
21
|
+
if (isGroup(row)) {
|
|
22
|
+
hasGroup = true;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!hasGroup)
|
|
27
|
+
return rows.filter(keep);
|
|
28
|
+
// One reverse pass. `survivorsBelow` counts leaves kept since we last closed
|
|
29
|
+
// out a banner, so a banner is kept when anything after it survived.
|
|
30
|
+
const keepFlags = new Array(rows.length).fill(false);
|
|
31
|
+
let survivorsSinceBanner = 0;
|
|
32
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
33
|
+
const row = rows[i];
|
|
34
|
+
if (isGroup(row)) {
|
|
35
|
+
if (survivorsSinceBanner > 0) {
|
|
36
|
+
keepFlags[i] = true;
|
|
37
|
+
// The banner itself counts as surviving content for any banner above
|
|
38
|
+
// it, so nested groups keep their ancestors.
|
|
39
|
+
survivorsSinceBanner = 1;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
keepFlags[i] = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (keep(row)) {
|
|
46
|
+
keepFlags[i] = true;
|
|
47
|
+
survivorsSinceBanner += 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const out = [];
|
|
51
|
+
for (let i = 0; i < rows.length; i += 1)
|
|
52
|
+
if (keepFlags[i])
|
|
53
|
+
out.push(rows[i]);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
package/dist/grid-messages.d.ts
CHANGED
|
@@ -22,6 +22,13 @@ export type GridMessages = {
|
|
|
22
22
|
total: string;
|
|
23
23
|
rowsSuffix: string;
|
|
24
24
|
rowSuffix: string;
|
|
25
|
+
announceFilterResults: string;
|
|
26
|
+
announceNoMatches: string;
|
|
27
|
+
announceFiltersCleared: string;
|
|
28
|
+
announceRowsSelected: string;
|
|
29
|
+
announceSelectionCleared: string;
|
|
30
|
+
advancedFilterActive: string;
|
|
31
|
+
advancedFilterClear: string;
|
|
25
32
|
opContains: string;
|
|
26
33
|
opNotContains: string;
|
|
27
34
|
opEquals: string;
|
package/dist/grid-messages.js
CHANGED
|
@@ -33,6 +33,13 @@ export const defaultGridMessages = {
|
|
|
33
33
|
total: 'Total',
|
|
34
34
|
rowsSuffix: 'rows',
|
|
35
35
|
rowSuffix: 'row',
|
|
36
|
+
announceFilterResults: '{visible} of {total} rows match the current filters',
|
|
37
|
+
announceNoMatches: 'No rows match the current filters',
|
|
38
|
+
announceFiltersCleared: 'Filters cleared, showing all {total} rows',
|
|
39
|
+
announceRowsSelected: '{count} rows selected',
|
|
40
|
+
announceSelectionCleared: 'Selection cleared',
|
|
41
|
+
advancedFilterActive: 'Advanced filter',
|
|
42
|
+
advancedFilterClear: 'Clear advanced filter',
|
|
36
43
|
menuCopy: 'Copy',
|
|
37
44
|
menuCut: 'Cut',
|
|
38
45
|
menuPaste: 'Paste',
|
package/dist/index.d.ts
CHANGED
|
@@ -215,6 +215,10 @@ export type { VirtualItem, VirtualizerOptions, VirtualizerState } from './virtua
|
|
|
215
215
|
export type { SvGridApi, SvGridFilterOperator, SvGridWrapperProps } from './svgrid-wrapper.types';
|
|
216
216
|
export { parseEditorValue, normalizeEditorOptions, type CellEditorType, type CellEditorOption, } from './editors/cell-editors';
|
|
217
217
|
export { applyExcelFilter, compileExcelFilter, normalizeForFilter, splitInTokens, joinInTokens, trailingInToken, type CompiledExcelFilter, type ExcelFilter, type ExcelFilterOperator, type ExcelFilterOptions, } from './filtering/excel-filters';
|
|
218
|
+
export { ALL_FILTER_OPERATORS, SET_OPERATOR_IDS, VALUELESS_OPERATOR_IDS, RANGE_OPERATOR_IDS, isSetOperator, isValuelessOperator, isRangeOperator, type FilterValueType, } from './filtering/filter-operator-catalogue';
|
|
219
|
+
export { applyRowPredicate } from './filtering/row-predicate';
|
|
220
|
+
export type { GridPredicateExpr, GridScalarExpr, GridComparisonOp, GridArithmeticOp, GridAggFn, } from './filtering/predicate-expr';
|
|
221
|
+
export { registerAdvancedFilterEngine, getAdvancedFilterEngine, hasAdvancedFilterEngine, type AdvancedFilterEngine, type AdvancedFilterCompileContext, type CompiledRowPredicate, } from './advanced-filter.svelte';
|
|
218
222
|
export { getGridCellDomId, getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, type GridCellA11yInput, type GridColumnA11yInput, type GridSortDirection, } from './a11y';
|
|
219
223
|
/**
|
|
220
224
|
* Compatibility alias for teams migrating from `createTable`.
|
package/dist/index.js
CHANGED
|
@@ -250,6 +250,11 @@ export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.sve
|
|
|
250
250
|
export { createColumnVirtualizer } from './virtualization/column-virtualizer';
|
|
251
251
|
export { parseEditorValue, normalizeEditorOptions, } from './editors/cell-editors';
|
|
252
252
|
export { applyExcelFilter, compileExcelFilter, normalizeForFilter, splitInTokens, joinInTokens, trailingInToken, } from './filtering/excel-filters';
|
|
253
|
+
export { ALL_FILTER_OPERATORS, SET_OPERATOR_IDS, VALUELESS_OPERATOR_IDS, RANGE_OPERATOR_IDS, isSetOperator, isValuelessOperator, isRangeOperator, } from './filtering/filter-operator-catalogue';
|
|
254
|
+
export { applyRowPredicate } from './filtering/row-predicate';
|
|
255
|
+
// Advanced-filter seam: enterprise registers the compiler, the grid owns the
|
|
256
|
+
// state and the pipeline slot.
|
|
257
|
+
export { registerAdvancedFilterEngine, getAdvancedFilterEngine, hasAdvancedFilterEngine, } from './advanced-filter.svelte';
|
|
253
258
|
export { getGridCellDomId, getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, } from './a11y';
|
|
254
259
|
/**
|
|
255
260
|
* Compatibility alias for teams migrating from `createTable`.
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* non-optimistic for now - the grid reflects a change only after the
|
|
16
16
|
* follow-up re-fetch of the current page lands.
|
|
17
17
|
*/
|
|
18
|
+
import type { GridPredicateExpr } from './filtering/predicate-expr';
|
|
18
19
|
export type ServerSortModel = Array<{
|
|
19
20
|
id: string;
|
|
20
21
|
desc: boolean;
|
|
@@ -33,6 +34,24 @@ export type ServerFilterModel = {
|
|
|
33
34
|
valueTo?: string;
|
|
34
35
|
selectedValues?: string[];
|
|
35
36
|
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Advanced-filter predicate (Pro), as a JSON AST. Expresses what `columns`
|
|
39
|
+
* cannot: OR across columns, nesting, negation, two conditions on one
|
|
40
|
+
* column, cross-column comparison, and aggregates.
|
|
41
|
+
*
|
|
42
|
+
* CONTRACT - all or nothing. A backend that receives this MUST either:
|
|
43
|
+
*
|
|
44
|
+
* (a) translate the WHOLE expression into its query, make `rowCount`
|
|
45
|
+
* reflect it, and set `appliedExpression: true` on the result; or
|
|
46
|
+
* (b) apply none of it and leave `appliedExpression` unset.
|
|
47
|
+
*
|
|
48
|
+
* Partial application is a contract violation, not a degraded mode: it
|
|
49
|
+
* returns a SUPERSET of the requested rows while the UI says the filter is
|
|
50
|
+
* on. That is strictly worse than not filtering, because nothing about the
|
|
51
|
+
* result looks wrong. When the ack is missing the grid says so rather than
|
|
52
|
+
* filtering the loaded page itself - see `ServerState.expressionUnapplied`.
|
|
53
|
+
*/
|
|
54
|
+
expression?: GridPredicateExpr;
|
|
36
55
|
};
|
|
37
56
|
/** A value column to roll up per group. */
|
|
38
57
|
export type ServerAggregation = {
|
|
@@ -137,6 +156,12 @@ export type ServerResult<TData> = {
|
|
|
137
156
|
rows: ReadonlyArray<TData>;
|
|
138
157
|
/** Total row count after filtering (for the pager). */
|
|
139
158
|
rowCount: number;
|
|
159
|
+
/**
|
|
160
|
+
* Set `true` ONLY when `filterModel.expression` was applied in full. Leave it
|
|
161
|
+
* unset if you ignored the expression; the grid then warns rather than
|
|
162
|
+
* pretending the filter ran. See the contract on `ServerFilterModel.expression`.
|
|
163
|
+
*/
|
|
164
|
+
appliedExpression?: boolean;
|
|
140
165
|
};
|
|
141
166
|
export type ServerDataSource<TData> = {
|
|
142
167
|
getRows(request: ServerRequest): Promise<ServerResult<TData>>;
|
|
@@ -162,6 +187,15 @@ export type ServerState<TData> = {
|
|
|
162
187
|
pageCount: number;
|
|
163
188
|
sortModel: ServerSortModel;
|
|
164
189
|
filterModel: ServerFilterModel;
|
|
190
|
+
/**
|
|
191
|
+
* True when an advanced-filter expression was sent but the source did not
|
|
192
|
+
* acknowledge applying it - so `rows` is unfiltered and the UI should say so.
|
|
193
|
+
* Surface this rather than hiding it: the rows look perfectly normal.
|
|
194
|
+
*
|
|
195
|
+
* Optional so existing code that builds a `ServerState` literal keeps
|
|
196
|
+
* compiling; the controller always sets it.
|
|
197
|
+
*/
|
|
198
|
+
expressionUnapplied?: boolean;
|
|
165
199
|
};
|
|
166
200
|
export type ServerController<TData> = {
|
|
167
201
|
/** Re-fetch the current page (e.g. after a mutation). */
|
|
@@ -10,11 +10,15 @@ export function createServerDataSource(source, options) {
|
|
|
10
10
|
pageCount: 1,
|
|
11
11
|
sortModel: [],
|
|
12
12
|
filterModel: {},
|
|
13
|
+
expressionUnapplied: false,
|
|
13
14
|
};
|
|
14
15
|
// Monotonic request id: only the latest fetch is allowed to land, so a slow
|
|
15
16
|
// response for an old sort/filter can't clobber a newer one.
|
|
16
17
|
let requestSeq = 0;
|
|
17
18
|
let disposed = false;
|
|
19
|
+
// Once per controller: a misconfigured backend would otherwise log on every
|
|
20
|
+
// page, scroll and filter change.
|
|
21
|
+
let warnedExpressionUnapplied = false;
|
|
18
22
|
const emit = () => {
|
|
19
23
|
state.pageCount = Math.max(1, Math.ceil(state.total / state.pageSize));
|
|
20
24
|
options.onChange({ ...state });
|
|
@@ -44,6 +48,21 @@ export function createServerDataSource(source, options) {
|
|
|
44
48
|
return; // stale
|
|
45
49
|
state.rows = result.rows;
|
|
46
50
|
state.total = result.rowCount;
|
|
51
|
+
// An expression was sent but the backend did not acknowledge applying it,
|
|
52
|
+
// so these rows are a SUPERSET of what was asked for. Say so instead of
|
|
53
|
+
// filtering the loaded page here: filtering one page would turn
|
|
54
|
+
// "3 of 1,000,000 match" into a confident lie and make paging incoherent,
|
|
55
|
+
// since page 2 would re-filter a different slice.
|
|
56
|
+
state.expressionUnapplied =
|
|
57
|
+
state.filterModel.expression != null && result.appliedExpression !== true;
|
|
58
|
+
if (state.expressionUnapplied && !warnedExpressionUnapplied) {
|
|
59
|
+
warnedExpressionUnapplied = true;
|
|
60
|
+
console.warn('[svgrid] The data source was sent filterModel.expression but did not ' +
|
|
61
|
+
'return `appliedExpression: true`, so the advanced filter is NOT applied ' +
|
|
62
|
+
'and the rows shown are unfiltered. Apply the whole expression and ' +
|
|
63
|
+
'acknowledge it, or clear the advanced filter. ' +
|
|
64
|
+
'See https://svgrid.com/docs/help/server/server-filtering');
|
|
65
|
+
}
|
|
47
66
|
state.loading = false;
|
|
48
67
|
emit();
|
|
49
68
|
}
|
|
@@ -2,6 +2,7 @@ import type { CellFormatConfig, ColumnDef, RowData, SvGridOptions, TableFeatures
|
|
|
2
2
|
import type { FilterOperator, Props } from './SvGrid.types';
|
|
3
3
|
import type { GridExportOptions, GridClipboardOptions } from './export-format';
|
|
4
4
|
import type { ChartSpec, ChartType } from './chart';
|
|
5
|
+
import type { GridPredicateExpr } from './filtering/predicate-expr';
|
|
5
6
|
export type SvGridFilterOperator = FilterOperator;
|
|
6
7
|
/**
|
|
7
8
|
* A serializable snapshot of everything that makes up the current "view":
|
|
@@ -36,6 +37,13 @@ export type SvGridViewState = {
|
|
|
36
37
|
}>;
|
|
37
38
|
/** Facet (Excel-style value checklist) selections, keyed by column id. */
|
|
38
39
|
facetFilters: Record<string, string[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Advanced-filter expression (Pro). OPTIONAL, and omitted entirely when no
|
|
42
|
+
* advanced filter is set - so views saved before this feature existed, and
|
|
43
|
+
* views from grids that never use it, round-trip byte-identical. An explicit
|
|
44
|
+
* `null` clears the filter on `setState`.
|
|
45
|
+
*/
|
|
46
|
+
advancedFilter?: GridPredicateExpr | null;
|
|
39
47
|
/** Built-in chart panel state (present only when `charting` is on): the
|
|
40
48
|
* ACTIVE chart, for back-compat + spread-and-tweak. */
|
|
41
49
|
chart?: {
|
|
@@ -218,10 +226,25 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
218
226
|
*/
|
|
219
227
|
refreshEditorOptions(columnId?: string): void;
|
|
220
228
|
/**
|
|
221
|
-
* Clear every active column filter (menu, filter-row, set-list, and
|
|
222
|
-
* Resets the grid to "no filtering" in a single call.
|
|
229
|
+
* Clear every active column filter (menu, filter-row, set-list, global, and
|
|
230
|
+
* the advanced filter). Resets the grid to "no filtering" in a single call.
|
|
223
231
|
*/
|
|
224
232
|
clearAllFilters(): void;
|
|
233
|
+
/**
|
|
234
|
+
* Set the advanced-filter expression (Pro). `null` clears it. Composed with
|
|
235
|
+
* AND after the global, column and facet filters.
|
|
236
|
+
*
|
|
237
|
+
* Rows are only removed once `@svgrid/enterprise`'s `enableAdvancedFilter()`
|
|
238
|
+
* has registered a compiler. Without it the expression is stored but nothing
|
|
239
|
+
* is filtered - use `isAdvancedFilterActive()` to tell the two apart.
|
|
240
|
+
*/
|
|
241
|
+
setAdvancedFilter(expr: GridPredicateExpr | null): void;
|
|
242
|
+
/** The current advanced-filter expression, or `null`. */
|
|
243
|
+
getAdvancedFilter(): GridPredicateExpr | null;
|
|
244
|
+
/** Clear the advanced filter, leaving other filter surfaces untouched. */
|
|
245
|
+
clearAdvancedFilter(): void;
|
|
246
|
+
/** Whether an expression is set AND an engine is registered to run it. */
|
|
247
|
+
isAdvancedFilterActive(): boolean;
|
|
225
248
|
/**
|
|
226
249
|
* Read the active column-menu filters as a snapshot. Keyed by column id.
|
|
227
250
|
* Returns an empty object when nothing is filtered. `valueTo` is only
|
|
@@ -233,9 +256,15 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
233
256
|
valueTo?: string;
|
|
234
257
|
}>;
|
|
235
258
|
/**
|
|
236
|
-
* Snapshot of the rows the grid is
|
|
237
|
-
*
|
|
238
|
-
*
|
|
259
|
+
* Snapshot of the DATA rows the grid is displaying right now - after
|
|
260
|
+
* filtering, sorting, grouping and pagination. Use this when you need the
|
|
261
|
+
* visible result set (e.g. to export it as CSV).
|
|
262
|
+
*
|
|
263
|
+
* Group banner rows are not included: the return type is `TData`, and a
|
|
264
|
+
* banner is not one of your rows. That matters while grouping is on, because
|
|
265
|
+
* with every group collapsed this returns an EMPTY array even though the
|
|
266
|
+
* grid visibly shows a banner per group. Count banners from `getState()`
|
|
267
|
+
* rather than from the length of this.
|
|
239
268
|
*/
|
|
240
269
|
getDisplayedRows(): ReadonlyArray<TData>;
|
|
241
270
|
/** Snapshot of the current data array (pre-pipeline). */
|
|
@@ -256,6 +285,9 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
256
285
|
format?: CellFormatConfig;
|
|
257
286
|
/** Effective horizontal alignment ('left' | 'center' | 'right'). */
|
|
258
287
|
align?: 'left' | 'center' | 'right';
|
|
288
|
+
/** The column's declared `editorType`, when set. Lets a filter or
|
|
289
|
+
* expression UI offer type-appropriate operators. */
|
|
290
|
+
editorType?: string;
|
|
259
291
|
}>;
|
|
260
292
|
/**
|
|
261
293
|
* Export the grid to a **CSV** file. Free in the community grid; the
|
package/dist/themes/index.js
CHANGED
|
@@ -81,16 +81,24 @@ export const themePresets = [
|
|
|
81
81
|
light: { bg: '#ffffff', fg: '#282a30', muted: '#6b6f76', border: '#e9e9eb', headerBg: '#f7f8f8', headerFg: '#6b6f76', accent: '#5e6ad2', rowAlt: '#fbfbfc', rowHover: '#f4f4f5', selectionBg: '#e8eafd' },
|
|
82
82
|
dark: { bg: '#08090a', fg: '#f7f8f8', muted: '#8a8f98', border: '#23252a', headerBg: '#0f1011', headerFg: '#8a8f98', accent: '#5e6ad2', rowAlt: '#0f1011', rowHover: '#1a1b1e', selectionBg: '#2a2d4a' } },
|
|
83
83
|
{ id: 'notion', name: 'Notion', radius: 4, font: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
|
84
|
-
|
|
84
|
+
// muted + headerFg darkened from #787774, which sat at 4.48:1 on white and
|
|
85
|
+
// 4.14:1 on the header fill - both just under AA. #6b6a67 keeps the warm
|
|
86
|
+
// grey and clears it at 5.41:1 / 5:1.
|
|
87
|
+
light: { bg: '#ffffff', fg: '#37352f', muted: '#6b6a67', border: '#e9e9e7', headerBg: '#f7f6f3', headerFg: '#6b6a67', accent: '#2383e2', rowAlt: '#fbfbfa', rowHover: '#f1f1ef', selectionBg: '#d9eaf7' },
|
|
85
88
|
dark: { bg: '#191919', fg: '#d4d4d4', muted: '#9b9b9b', border: '#2f2f2f', headerBg: '#202020', headerFg: '#9b9b9b', accent: '#529cca', rowAlt: '#1c1c1c', rowHover: '#252525', selectionBg: '#20415c' } },
|
|
86
89
|
{ id: 'nord', name: 'Nord', radius: 4, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
87
90
|
light: { bg: '#eceff4', fg: '#2e3440', muted: '#4c566a', border: '#d8dee9', headerBg: '#e5e9f0', headerFg: '#2e3440', accent: '#5579a5', rowAlt: '#e5e9f0', rowHover: '#dde3ec', selectionBg: '#d8dee9' },
|
|
88
|
-
|
|
91
|
+
// muted lightened from Nord's #4c566a (nord3): on the nord0 background that
|
|
92
|
+
// is 1.69:1, effectively invisible. #97a0b0 keeps the Nord hue at 4.74:1.
|
|
93
|
+
dark: { bg: '#2e3440', fg: '#d8dee9', muted: '#97a0b0', border: '#434c5e', headerBg: '#3b4252', headerFg: '#e5e9f0', accent: '#88c0d0', rowAlt: '#333b48', rowHover: '#3b4252', selectionBg: '#434c5e' } },
|
|
89
94
|
{ id: 'dracula', name: 'Dracula', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
90
95
|
light: { bg: '#fffbeb', fg: '#1f1f1f', muted: '#6c664b', border: '#dedccf', headerBg: '#ece9df', headerFg: '#1f1f1f', accent: '#644ac9', rowAlt: '#fffdf5', rowHover: '#f2efe3', selectionBg: '#cfcfde' },
|
|
91
|
-
|
|
96
|
+
// muted lightened from Dracula's comment colour #6272a4 (3.03:1) to 5.04:1.
|
|
97
|
+
dark: { bg: '#282a36', fg: '#f8f8f2', muted: '#8b98c9', border: '#44475a', headerBg: '#21222c', headerFg: '#f8f8f2', accent: '#bd93f9', rowAlt: '#2d2f3b', rowHover: '#343746', selectionBg: '#44475a' } },
|
|
92
98
|
{ id: 'catppuccin', name: 'Catppuccin', radius: 8, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
93
|
-
|
|
99
|
+
// muted moved from Latte subtext0 (#6c6f85, 4.37:1) to subtext1 (#5c5f77),
|
|
100
|
+
// still an official Catppuccin colour, at 5.53:1.
|
|
101
|
+
light: { bg: '#eff1f5', fg: '#4c4f69', muted: '#5c5f77', border: '#ccd0da', headerBg: '#e6e9ef', headerFg: '#4c4f69', accent: '#8839ef', rowAlt: '#e6e9ef', rowHover: '#dce0e8', selectionBg: '#dce0e8' },
|
|
94
102
|
dark: { bg: '#1e1e2e', fg: '#cdd6f4', muted: '#a6adc8', border: '#313244', headerBg: '#181825', headerFg: '#cdd6f4', accent: '#cba6f7', rowAlt: '#1a1a28', rowHover: '#313244', selectionBg: '#45475a' } },
|
|
95
103
|
];
|
|
96
104
|
/** The default preset (used when nothing is chosen). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svgrid/grid",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.5",
|
|
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",
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
"@sveltejs/package": "^2.5.7",
|
|
112
112
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
113
113
|
"@vitest/coverage-v8": "^4.1.5",
|
|
114
|
+
"axe-core": "^4.13.0",
|
|
114
115
|
"eslint-plugin-svelte": "^3.17.1",
|
|
115
116
|
"jsdom": "^29.1.1",
|
|
116
117
|
"publint": "^0.3.21",
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyGroupAggregate,
|
|
3
|
+
applyRowPredicate,
|
|
3
4
|
compileExcelFilter,
|
|
4
5
|
type CompiledExcelFilter,
|
|
6
|
+
getAdvancedFilterEngine,
|
|
7
|
+
type GridPredicateExpr,
|
|
5
8
|
normalizeForFilter,
|
|
6
9
|
createColumnVirtualizer,
|
|
7
10
|
createCoreRowModel,
|
|
@@ -94,6 +97,11 @@ import {
|
|
|
94
97
|
createClipboard,
|
|
95
98
|
} from "./clipboard";
|
|
96
99
|
import { resolveGridMessages } from "./grid-messages";
|
|
100
|
+
import { announce } from "./a11y/live-region";
|
|
101
|
+
import {
|
|
102
|
+
filterAnnouncement,
|
|
103
|
+
selectionAnnouncement,
|
|
104
|
+
} from "./a11y/grid-announcements";
|
|
97
105
|
import { getPivotEngine, hasPivotEngine } from "./pivot-view.svelte";
|
|
98
106
|
import {
|
|
99
107
|
filterOperatorOptions,
|
|
@@ -324,6 +332,13 @@ export function createSvGridController<
|
|
|
324
332
|
let globalFilter = $state("");
|
|
325
333
|
let scrollContainer: HTMLDivElement | null = $state(null);
|
|
326
334
|
let gridRootEl: HTMLElement | null = $state(null);
|
|
335
|
+
// Advanced filter (Pro). Seeded once from `initialAdvancedFilter` and driven
|
|
336
|
+
// thereafter through the API - deliberately NOT a live prop, so it cannot
|
|
337
|
+
// repeat the `externalFilter` trap of looking controlled while being read
|
|
338
|
+
// exactly once.
|
|
339
|
+
let advancedFilter = $state<GridPredicateExpr | null>(
|
|
340
|
+
props.initialAdvancedFilter ?? null,
|
|
341
|
+
);
|
|
327
342
|
let filterRowValues = $state<Record<string, string>>({});
|
|
328
343
|
let filterMenuValues = $state<
|
|
329
344
|
Record<
|
|
@@ -712,13 +727,15 @@ export function createSvGridController<
|
|
|
712
727
|
},
|
|
713
728
|
state: {
|
|
714
729
|
columnFilters: [],
|
|
715
|
-
|
|
730
|
+
// svelte-ignore state_referenced_locally
|
|
731
|
+
grouping: treeDataConfig ? [] : [...(props.groupBy ?? [])],
|
|
716
732
|
// svelte-ignore state_referenced_locally
|
|
717
733
|
sorting: props.initialSorting ?? [],
|
|
718
734
|
// svelte-ignore state_referenced_locally
|
|
719
735
|
pagination: { pageIndex: 0, pageSize: props.pageSize ?? 10 },
|
|
720
736
|
rowSelection: {},
|
|
721
|
-
|
|
737
|
+
// svelte-ignore state_referenced_locally
|
|
738
|
+
expanded: { ...(props.expanded ?? {}) },
|
|
722
739
|
activeCell: { rowIndex: 0, colIndex: 0, cellId: null },
|
|
723
740
|
},
|
|
724
741
|
});
|
|
@@ -781,6 +798,42 @@ export function createSvGridController<
|
|
|
781
798
|
userColumnOrder = props.columnOrder ? [...props.columnOrder] : [];
|
|
782
799
|
});
|
|
783
800
|
|
|
801
|
+
// `groupBy` prop -> engine grouping state. Same shape as the `columnOrder`
|
|
802
|
+
// re-seed above: we only write when the PROP itself changes, so a group-by
|
|
803
|
+
// set from the column menu or `api.setGroupBy()` is not clobbered on the next
|
|
804
|
+
// unrelated re-render.
|
|
805
|
+
let lastSeededGroupBy = (props.groupBy ?? []).join("|");
|
|
806
|
+
$effect(() => {
|
|
807
|
+
if (treeDataConfig) return;
|
|
808
|
+
const incoming = (props.groupBy ?? []).join("|");
|
|
809
|
+
if (incoming === lastSeededGroupBy) return;
|
|
810
|
+
lastSeededGroupBy = incoming;
|
|
811
|
+
grid.setGrouping(() => [...(props.groupBy ?? [])]);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// `expanded` prop -> engine expansion state, and the store -> the
|
|
815
|
+
// `onExpandedChange` callback. `lastSeededExpanded` is written from BOTH
|
|
816
|
+
// directions so a controlled parent that echoes the callback value straight
|
|
817
|
+
// back into the prop does not trigger a redundant re-seed (and so no
|
|
818
|
+
// prop -> state -> callback -> prop loop can form).
|
|
819
|
+
let lastSeededExpanded = JSON.stringify(props.expanded ?? {});
|
|
820
|
+
$effect(() => {
|
|
821
|
+
const incoming = JSON.stringify(props.expanded ?? {});
|
|
822
|
+
if (incoming === lastSeededExpanded) return;
|
|
823
|
+
lastSeededExpanded = incoming;
|
|
824
|
+
grid.setExpanded(() => ({ ...(props.expanded ?? {}) }));
|
|
825
|
+
});
|
|
826
|
+
$effect(() => {
|
|
827
|
+
let previous = grid.getState().expanded ?? {};
|
|
828
|
+
return grid.store.subscribe(() => {
|
|
829
|
+
const next = grid.getState().expanded ?? {};
|
|
830
|
+
if (next === previous) return;
|
|
831
|
+
previous = next;
|
|
832
|
+
lastSeededExpanded = JSON.stringify(next);
|
|
833
|
+
props.onExpandedChange?.({ ...next });
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
784
837
|
// Declared here rather than beside the other pagination/state derivations:
|
|
785
838
|
// the auto-group column pipeline below reads it, and a `const` used above its
|
|
786
839
|
// declaration is a type error even though `$derived` is lazy enough at runtime.
|
|
@@ -1345,6 +1398,32 @@ export function createSvGridController<
|
|
|
1345
1398
|
);
|
|
1346
1399
|
}
|
|
1347
1400
|
|
|
1401
|
+
// --- Advanced filter (Pro) ---------------------------------------------
|
|
1402
|
+
// Runs LAST for two reasons: it evaluates over the smallest row set, and
|
|
1403
|
+
// its aggregates (`SUM(amount) > 1000`) fold over what the user is
|
|
1404
|
+
// currently looking at rather than the raw dataset. Composed with AND
|
|
1405
|
+
// against the three stages above.
|
|
1406
|
+
//
|
|
1407
|
+
// Fails OPEN throughout. No engine registered (enterprise absent), an
|
|
1408
|
+
// expression the engine cannot compile, or a throw from inside it all
|
|
1409
|
+
// leave `rows` untouched. A half-applied filter that quietly shows the
|
|
1410
|
+
// wrong rows is worse than one that visibly did nothing.
|
|
1411
|
+
if (advancedFilter) {
|
|
1412
|
+
const engine = getAdvancedFilterEngine();
|
|
1413
|
+
if (engine) {
|
|
1414
|
+
try {
|
|
1415
|
+
const predicate = engine(advancedFilter, {
|
|
1416
|
+
getValue: getRowColumnValue,
|
|
1417
|
+
locale: props.filterLocale ?? props.localization?.locale,
|
|
1418
|
+
rows,
|
|
1419
|
+
});
|
|
1420
|
+
if (predicate) rows = applyRowPredicate(rows, predicate, isGroupRow);
|
|
1421
|
+
} catch {
|
|
1422
|
+
// Malformed expression: leave the rows alone.
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1348
1427
|
return rows;
|
|
1349
1428
|
});
|
|
1350
1429
|
|
|
@@ -1639,6 +1718,92 @@ export function createSvGridController<
|
|
|
1639
1718
|
// map every consumer (SvGrid.svelte, GridFooter, menus, filter labels) reads.
|
|
1640
1719
|
const gridMessages = $derived(resolveGridMessages(props.localization?.text));
|
|
1641
1720
|
|
|
1721
|
+
// ---- Screen-reader status announcements (WCAG 4.1.3) ----
|
|
1722
|
+
// Only genuine status messages belong here: information no focus change
|
|
1723
|
+
// reveals. `a11y/grid-announcements.ts` documents what is deliberately left
|
|
1724
|
+
// unsaid, and why saying it would make the grid talk over itself.
|
|
1725
|
+
|
|
1726
|
+
/** Data rows before any local filtering - the denominator in "12 of 250". */
|
|
1727
|
+
const unfilteredRowTotal = $derived.by(() => {
|
|
1728
|
+
dataStateVersion;
|
|
1729
|
+
void internalData;
|
|
1730
|
+
return grid.getRowModel().rows.length;
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
/** Whether any of the five filter surfaces is currently narrowing the rows. */
|
|
1734
|
+
const anyFilterActive = $derived(
|
|
1735
|
+
globalFilter !== "" ||
|
|
1736
|
+
advancedFilter !== null ||
|
|
1737
|
+
Object.keys(filterRowValues).length > 0 ||
|
|
1738
|
+
Object.keys(filterMenuValues).length > 0 ||
|
|
1739
|
+
Object.keys(valueFilters).length > 0,
|
|
1740
|
+
);
|
|
1741
|
+
|
|
1742
|
+
let filtersWereActive = false;
|
|
1743
|
+
let filterAnnounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1744
|
+
|
|
1745
|
+
$effect(() => {
|
|
1746
|
+
// In external-filter mode the server decides what matched, so the local
|
|
1747
|
+
// counts describe only the page in hand. Announcing them would misreport
|
|
1748
|
+
// the result set, which is worse than staying silent.
|
|
1749
|
+
if (externalFilterEnabled) return;
|
|
1750
|
+
|
|
1751
|
+
const visible = paginatedRowTotal;
|
|
1752
|
+
const total = unfilteredRowTotal;
|
|
1753
|
+
const active = anyFilterActive;
|
|
1754
|
+
const messages = gridMessages;
|
|
1755
|
+
|
|
1756
|
+
untrack(() => {
|
|
1757
|
+
const message = filterAnnouncement(
|
|
1758
|
+
visible,
|
|
1759
|
+
total,
|
|
1760
|
+
active,
|
|
1761
|
+
filtersWereActive,
|
|
1762
|
+
messages,
|
|
1763
|
+
);
|
|
1764
|
+
filtersWereActive = active;
|
|
1765
|
+
if (!message) return;
|
|
1766
|
+
// Typing in the global filter re-runs this per keystroke. A polite region
|
|
1767
|
+
// queues rather than replaces, so without a debounce the user hears the
|
|
1768
|
+
// count for every prefix they typed before the one they care about.
|
|
1769
|
+
clearTimeout(filterAnnounceTimer);
|
|
1770
|
+
filterAnnounceTimer = setTimeout(() => announce(message), 400);
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
return () => clearTimeout(filterAnnounceTimer);
|
|
1774
|
+
});
|
|
1775
|
+
|
|
1776
|
+
// Forward advanced-filter changes to the consumer. Same dedupe-and-skip-first
|
|
1777
|
+
// pattern as the selection forwarder: the panel that authors the expression
|
|
1778
|
+
// lives outside the grid, so a change the grid makes itself (the toolbar's
|
|
1779
|
+
// clear, clearAllFilters) is invisible to it otherwise.
|
|
1780
|
+
let lastAdvancedFilterSerialized = JSON.stringify(
|
|
1781
|
+
untrack(() => advancedFilter) ?? null,
|
|
1782
|
+
);
|
|
1783
|
+
$effect(() => {
|
|
1784
|
+
const serialized = JSON.stringify(advancedFilter ?? null);
|
|
1785
|
+
if (serialized === untrack(() => lastAdvancedFilterSerialized)) return;
|
|
1786
|
+
untrack(() => {
|
|
1787
|
+
lastAdvancedFilterSerialized = serialized;
|
|
1788
|
+
props.onAdvancedFilterChange?.(advancedFilter ?? null);
|
|
1789
|
+
});
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
let lastAnnouncedSelectionCount = 0;
|
|
1793
|
+
$effect(() => {
|
|
1794
|
+
const count = Object.values(rowSelectionState).filter(Boolean).length;
|
|
1795
|
+
const messages = gridMessages;
|
|
1796
|
+
untrack(() => {
|
|
1797
|
+
const message = selectionAnnouncement(
|
|
1798
|
+
lastAnnouncedSelectionCount,
|
|
1799
|
+
count,
|
|
1800
|
+
messages,
|
|
1801
|
+
);
|
|
1802
|
+
lastAnnouncedSelectionCount = count;
|
|
1803
|
+
if (message) announce(message);
|
|
1804
|
+
});
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1642
1807
|
// ---- In-grid pivot mode ----
|
|
1643
1808
|
// The pivot ENGINE ships in @svgrid/enterprise (registered via the pivot-view
|
|
1644
1809
|
// seam). When `pivot` is set and the engine is present, the grid runs it over
|
|
@@ -3089,6 +3254,8 @@ export function createSvGridController<
|
|
|
3089
3254
|
set filterRowValues(v) { filterRowValues = v as never; },
|
|
3090
3255
|
get filterMenuValues() { return filterMenuValues; },
|
|
3091
3256
|
set filterMenuValues(v) { filterMenuValues = v as never; },
|
|
3257
|
+
get advancedFilter() { return advancedFilter; },
|
|
3258
|
+
set advancedFilter(v) { advancedFilter = v as never; },
|
|
3092
3259
|
get verticalScrollbarEl() { return verticalScrollbarEl; },
|
|
3093
3260
|
set verticalScrollbarEl(v) { verticalScrollbarEl = v as never; },
|
|
3094
3261
|
get horizontalScrollbarEl() { return horizontalScrollbarEl; },
|
package/src/SvGrid.css
CHANGED
|
@@ -122,6 +122,38 @@
|
|
|
122
122
|
border-color: var(--sg-accent, #2563eb);
|
|
123
123
|
color: #fff;
|
|
124
124
|
}
|
|
125
|
+
/* Advanced-filter indicator. Reads as state rather than as another action, so
|
|
126
|
+
it uses the accent fill the toolbar buttons reserve for `is-active` instead
|
|
127
|
+
of the neutral button shell. */
|
|
128
|
+
.sv-grid-advf-chip {
|
|
129
|
+
display: inline-flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 6px;
|
|
132
|
+
padding: 5px 6px 5px 11px;
|
|
133
|
+
border-radius: 999px;
|
|
134
|
+
border: 1px solid color-mix(in srgb, var(--sg-accent, #2563eb) 35%, transparent);
|
|
135
|
+
background: color-mix(in srgb, var(--sg-accent, #2563eb) 12%, transparent);
|
|
136
|
+
color: var(--sg-accent, #2563eb);
|
|
137
|
+
font-size: 12.5px;
|
|
138
|
+
font-weight: 600;
|
|
139
|
+
}
|
|
140
|
+
.sv-grid-advf-clear {
|
|
141
|
+
display: inline-flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
width: 18px;
|
|
145
|
+
height: 18px;
|
|
146
|
+
border: 0;
|
|
147
|
+
border-radius: 999px;
|
|
148
|
+
background: transparent;
|
|
149
|
+
color: inherit;
|
|
150
|
+
font-size: 11px;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
transition: background-color 120ms ease;
|
|
153
|
+
}
|
|
154
|
+
.sv-grid-advf-clear:hover {
|
|
155
|
+
background: color-mix(in srgb, var(--sg-accent, #2563eb) 25%, transparent);
|
|
156
|
+
}
|
|
125
157
|
.sv-grid-tool-panel {
|
|
126
158
|
position: absolute;
|
|
127
159
|
top: 0;
|