@spaethtech/svelte-ui 0.17.1-dev.80.6b8fb45 → 0.17.1-dev.82.2748ef4
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/.claude/skills/svelte-ui/SKILL.md +6 -1
- package/dist/components/DataTable.svelte +8 -12
- package/dist/data/query/eval.d.ts +16 -0
- package/dist/data/query/eval.js +156 -0
- package/dist/data/query/index.d.ts +1 -0
- package/dist/data/query/index.js +1 -0
- package/dist/data/table/index.d.ts +1 -0
- package/dist/data/table/index.js +1 -0
- package/dist/data/table/local.d.ts +21 -0
- package/dist/data/table/local.js +63 -0
- package/docs/components.md +3 -1
- package/docs/usage.md +16 -9
- package/package.json +1 -1
|
@@ -184,7 +184,12 @@ examples):
|
|
|
184
184
|
For a start–end range use **`DatePicker range`** (`bind:value` = `{ start, end }`) — it composes
|
|
185
185
|
`Calendar mode="range"`.
|
|
186
186
|
- **Data:** `DataTable` `Query` — driven by the headless layer at **`@spaethtech/svelte-ui/data`**
|
|
187
|
-
(query-language parser/AST
|
|
187
|
+
(query-language parser/AST via `parseQuery`, `createGrid`, `DataGrid<T>`, `DataSet`). For a
|
|
188
|
+
**client-held array**, use **`localFetch(rows, columns, opts?)`** as the grid's `fetch` — it parses
|
|
189
|
+
the query, filters via **`matchesRow(row, ast)`** (the in-memory AST evaluator: cmp/and/or/not/
|
|
190
|
+
match/in/range/search, honouring column `id`/`searchable`/`sortKey`), then sorts + paginates. Only a
|
|
191
|
+
remote/DB backend needs to compile the AST itself; do NOT hand-roll a "strip `$` + substring" filter.
|
|
192
|
+
· **`Pagination`** (standalone
|
|
188
193
|
pager styled like the DataTable footer: tinted band, per-page `Select`, range total, and a
|
|
189
194
|
`First·Prev·[page]/N·Next·Last` stepper whose indicator is an editable `NumberInput` quick-jump;
|
|
190
195
|
`bind:page`/`bind:perPage`, `total`, `perPageOptions`, `showEdges`/`showTotal`, `borderless`;
|
|
@@ -103,10 +103,11 @@
|
|
|
103
103
|
/** Font size for the WHOLE table. Applied as `font-size` on the
|
|
104
104
|
* outer container; every descendant inherits via standard CSS
|
|
105
105
|
* cascade — headers, cells, footer text all follow. */
|
|
106
|
-
// DataTable's own density
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
106
|
+
// DataTable's own density (font size + row height + cell padding) is a pure function of `size` — it
|
|
107
|
+
// NEVER changes with the viewport. It keys off the base tier of a responsive `size` (via
|
|
108
|
+
// resolveScalar) because its font-size/height are single inline-style CSS vars that can't carry
|
|
109
|
+
// per-breakpoint variants; the responsive `size` is still passed through to child controls
|
|
110
|
+
// (Button/Select/Checkbox), which DO resize at every breakpoint.
|
|
110
111
|
const sSize = $derived(resolveScalar(size, "md"));
|
|
111
112
|
const textVar = $derived(
|
|
112
113
|
sSize === "lg" ? "var(--ui-text-lg)" : sSize === "md" ? "var(--ui-text)" : "var(--ui-text-sm)",
|
|
@@ -121,15 +122,10 @@
|
|
|
121
122
|
? "var(--ui-height)"
|
|
122
123
|
: "var(--ui-height-sm)",
|
|
123
124
|
);
|
|
124
|
-
/** Cell padding
|
|
125
|
-
*
|
|
126
|
-
* chunkier on desktop but stay dense on phones. */
|
|
125
|
+
/** Cell padding — a pure function of `size` (no viewport step), so a table's density is exactly its
|
|
126
|
+
* `size` at every width. The values pair with `heightVar` (sm 24 / md 32 / lg 40px). */
|
|
127
127
|
const cellPad = $derived(
|
|
128
|
-
sSize === "lg"
|
|
129
|
-
? "py-3 px-4 md:py-4 md:px-5"
|
|
130
|
-
: sSize === "md"
|
|
131
|
-
? "py-2 px-3 md:py-3 md:px-4"
|
|
132
|
-
: "py-1 px-2 md:py-2 md:px-3",
|
|
128
|
+
sSize === "lg" ? "py-3 px-4" : sSize === "md" ? "py-2 px-3" : "py-1 px-2",
|
|
133
129
|
);
|
|
134
130
|
/** Inter-column gap. Tracks the same size axis so lead/cell/tail
|
|
135
131
|
* spacing stays proportional to cell padding. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type QueryNode } from "./ast.js";
|
|
2
|
+
export interface RowEvalOptions {
|
|
3
|
+
/** Resolve a row's value for a column id. Default: `row[colId]`. */
|
|
4
|
+
getValue?: (row: unknown, colId: string) => unknown;
|
|
5
|
+
/** Column ids scanned by a bare-text `search` node (the columns marked `searchable`). */
|
|
6
|
+
searchColumns?: string[];
|
|
7
|
+
/** Instant used to resolve `now()` values. Default: `new Date()` at call time. */
|
|
8
|
+
now?: Date;
|
|
9
|
+
/** Case-insensitive string comparison + search. Default: `true`. */
|
|
10
|
+
caseInsensitive?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Test a single `row` against a parsed query `ast`. `null`/`undefined` ast matches every row (an empty
|
|
14
|
+
* query is "no filter"). See {@link RowEvalOptions} for value access, searchable columns, and `now()`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function matchesRow(row: unknown, ast: QueryNode | null | undefined, opts?: RowEvalOptions): boolean;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// In-memory evaluator for the query AST — tests a plain row object against a parsed `QueryNode`.
|
|
2
|
+
// Framework-agnostic (no Svelte deps), the client-side counterpart to a server `toConditions`:
|
|
3
|
+
// where a DB backend compiles the AST to SQL, an in-memory grid filters an array with `matchesRow`.
|
|
4
|
+
// Handles all node kinds — and/or/not · cmp · match · in · range · search.
|
|
5
|
+
import { isNow, resolveNow, } from "./ast.js";
|
|
6
|
+
const getProp = (row, col) => row == null ? undefined : row[col];
|
|
7
|
+
const toStr = (v) => v == null ? "" : v instanceof Date ? v.toISOString() : String(v);
|
|
8
|
+
const isNumericLike = (v) => typeof v === "number"
|
|
9
|
+
? Number.isFinite(v)
|
|
10
|
+
: typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v));
|
|
11
|
+
const toBool = (v) => v === true || v === 1 || v === "1" || (typeof v === "string" && v.toLowerCase() === "true");
|
|
12
|
+
const toDateMs = (v) => v instanceof Date ? v.getTime() : new Date(v).getTime();
|
|
13
|
+
/** Resolve a query `Value` (possibly a `now()` marker) to a concrete comparand. */
|
|
14
|
+
const resolveVal = (v, now) => (isNow(v) ? resolveNow(v, now) : v);
|
|
15
|
+
/** Total order for a cell vs a comparand under an optional cast. Returns -1/0/1, or `NaN` if either
|
|
16
|
+
* side can't be coerced to the compared type (an incomparable pair never satisfies an ordering op). */
|
|
17
|
+
function order(cell, val, cast, _now) {
|
|
18
|
+
if (cast === "date" || val instanceof Date) {
|
|
19
|
+
const a = toDateMs(cell);
|
|
20
|
+
const b = val instanceof Date ? val.getTime() : toDateMs(val);
|
|
21
|
+
if (Number.isNaN(a) || Number.isNaN(b))
|
|
22
|
+
return NaN;
|
|
23
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
24
|
+
}
|
|
25
|
+
if (cast === "bool")
|
|
26
|
+
return (toBool(cell) ? 1 : 0) - (toBool(val) ? 1 : 0);
|
|
27
|
+
if (cast === "number" || (cast == null && isNumericLike(cell) && isNumericLike(val))) {
|
|
28
|
+
const a = Number(cell);
|
|
29
|
+
const b = Number(val);
|
|
30
|
+
if (Number.isNaN(a) || Number.isNaN(b))
|
|
31
|
+
return NaN;
|
|
32
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
33
|
+
}
|
|
34
|
+
const cmp = toStr(cell).localeCompare(toStr(val));
|
|
35
|
+
return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
|
|
36
|
+
}
|
|
37
|
+
/** Equality with the same coercion rules as `order`, but string-equality honours `caseInsensitive`. */
|
|
38
|
+
function eq(cell, val, cast, ci) {
|
|
39
|
+
if (cast === "date" || val instanceof Date)
|
|
40
|
+
return toDateMs(cell) === (val instanceof Date ? val.getTime() : toDateMs(val));
|
|
41
|
+
if (cast === "bool")
|
|
42
|
+
return toBool(cell) === toBool(val);
|
|
43
|
+
if (cast === "number" || (cast == null && isNumericLike(cell) && isNumericLike(val)))
|
|
44
|
+
return Number(cell) === Number(val);
|
|
45
|
+
const a = toStr(cell);
|
|
46
|
+
const b = toStr(val);
|
|
47
|
+
return ci ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
48
|
+
}
|
|
49
|
+
function safeRegExp(source, flags) {
|
|
50
|
+
try {
|
|
51
|
+
return new RegExp(source, flags);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Test a single `row` against a parsed query `ast`. `null`/`undefined` ast matches every row (an empty
|
|
59
|
+
* query is "no filter"). See {@link RowEvalOptions} for value access, searchable columns, and `now()`.
|
|
60
|
+
*/
|
|
61
|
+
export function matchesRow(row, ast, opts = {}) {
|
|
62
|
+
if (!ast)
|
|
63
|
+
return true;
|
|
64
|
+
const get = opts.getValue ?? getProp;
|
|
65
|
+
const ci = opts.caseInsensitive ?? true;
|
|
66
|
+
const now = opts.now ?? new Date();
|
|
67
|
+
const searchCols = opts.searchColumns ?? [];
|
|
68
|
+
const strOp = (cell, val, op) => {
|
|
69
|
+
let s = toStr(cell);
|
|
70
|
+
let t = toStr(val);
|
|
71
|
+
if (ci) {
|
|
72
|
+
s = s.toLowerCase();
|
|
73
|
+
t = t.toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
return op === "contains"
|
|
76
|
+
? s.includes(t)
|
|
77
|
+
: op === "ncontains"
|
|
78
|
+
? !s.includes(t)
|
|
79
|
+
: op === "starts"
|
|
80
|
+
? s.startsWith(t)
|
|
81
|
+
: s.endsWith(t); // "ends"
|
|
82
|
+
};
|
|
83
|
+
const inInterval = (x, interval, cast) => {
|
|
84
|
+
const { lower, upper } = interval;
|
|
85
|
+
if (lower) {
|
|
86
|
+
const o = order(x, resolveVal(lower.value, now), cast, now);
|
|
87
|
+
if (Number.isNaN(o) || (lower.inclusive ? o < 0 : o <= 0))
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (upper) {
|
|
91
|
+
const o = order(x, resolveVal(upper.value, now), cast, now);
|
|
92
|
+
if (Number.isNaN(o) || (upper.inclusive ? o > 0 : o >= 0))
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
const ev = (node) => {
|
|
98
|
+
switch (node.kind) {
|
|
99
|
+
case "and":
|
|
100
|
+
return node.children.every(ev);
|
|
101
|
+
case "or":
|
|
102
|
+
return node.children.some(ev);
|
|
103
|
+
case "not":
|
|
104
|
+
return !ev(node.child);
|
|
105
|
+
case "cmp": {
|
|
106
|
+
const cell = get(row, node.col);
|
|
107
|
+
const op = node.op;
|
|
108
|
+
if (op === "contains" || op === "ncontains" || op === "starts" || op === "ends")
|
|
109
|
+
return strOp(cell, node.value, op);
|
|
110
|
+
const val = resolveVal(node.value, now);
|
|
111
|
+
if (op === "eq")
|
|
112
|
+
return eq(cell, val, node.cast, ci);
|
|
113
|
+
if (op === "ne")
|
|
114
|
+
return !eq(cell, val, node.cast, ci);
|
|
115
|
+
const o = order(cell, val, node.cast, now);
|
|
116
|
+
if (Number.isNaN(o))
|
|
117
|
+
return false;
|
|
118
|
+
return op === "gt" ? o > 0 : op === "gte" ? o >= 0 : op === "lt" ? o < 0 : o <= 0;
|
|
119
|
+
}
|
|
120
|
+
case "match": {
|
|
121
|
+
const re = safeRegExp(node.source, node.flags);
|
|
122
|
+
if (!re)
|
|
123
|
+
return false;
|
|
124
|
+
const hit = re.test(toStr(get(row, node.col)));
|
|
125
|
+
return node.negated ? !hit : hit;
|
|
126
|
+
}
|
|
127
|
+
case "in": {
|
|
128
|
+
const cell = get(row, node.col);
|
|
129
|
+
const members = node.values.map((v) => resolveVal(v, now));
|
|
130
|
+
if (Array.isArray(cell)) {
|
|
131
|
+
const inCell = (m) => cell.some((c) => eq(c, m, node.cast, ci));
|
|
132
|
+
return node.mode === "all" ? members.every(inCell) : members.some(inCell);
|
|
133
|
+
}
|
|
134
|
+
const hits = members.map((m) => eq(cell, m, node.cast, ci));
|
|
135
|
+
return node.mode === "all" ? hits.every(Boolean) : hits.some(Boolean);
|
|
136
|
+
}
|
|
137
|
+
case "range": {
|
|
138
|
+
const cell = get(row, node.col);
|
|
139
|
+
const test = (x) => inInterval(x, node.interval, node.cast);
|
|
140
|
+
if (Array.isArray(cell))
|
|
141
|
+
return node.mode === "all" ? cell.every(test) : cell.some(test);
|
|
142
|
+
return test(cell);
|
|
143
|
+
}
|
|
144
|
+
case "search": {
|
|
145
|
+
const needle = ci ? node.text.toLowerCase() : node.text;
|
|
146
|
+
if (!needle)
|
|
147
|
+
return true;
|
|
148
|
+
return searchCols.some((c) => {
|
|
149
|
+
const s = toStr(get(row, c));
|
|
150
|
+
return (ci ? s.toLowerCase() : s).includes(needle);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
return ev(ast);
|
|
156
|
+
}
|
package/dist/data/query/index.js
CHANGED
package/dist/data/table/index.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Column, FetchFn } from "./types.js";
|
|
2
|
+
export interface LocalFetchOptions<T> {
|
|
3
|
+
/** Resolve a row's value for a column id (filter + sort + search). Default: `row[colId]`. */
|
|
4
|
+
getValue?: (row: T, colId: string) => unknown;
|
|
5
|
+
/** Case-insensitive filtering/search. Default: `true`. */
|
|
6
|
+
caseInsensitive?: boolean;
|
|
7
|
+
/** Instant for resolving `now()` in the query. Called per fetch. Default: `new Date()`. */
|
|
8
|
+
now?: () => Date;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build a `FetchFn` that runs entirely in memory over `source`.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const grid = createGrid({ columns, getRowId: (r) => r.id, fetch: localFetch(() => rows, columns) });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Pass a getter (`() => rows`) rather than a bare array if the data is reactive (`$state`) and should
|
|
18
|
+
* re-filter when it changes. Filtering honours the columns' `id` (`$token`) and `searchable` flags; a
|
|
19
|
+
* bare-text search scans the `searchable` columns (or every column if none are marked).
|
|
20
|
+
*/
|
|
21
|
+
export declare function localFetch<T>(source: T[] | (() => T[]), columns: Column<T>[], opts?: LocalFetchOptions<T>): FetchFn<T>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// In-memory `FetchFn` for `createGrid`/`DataTable` — the client-side counterpart to a remote
|
|
2
|
+
// `query()`. Parses the grid's query string, filters the array with `matchesRow`, then sorts and
|
|
3
|
+
// paginates. Drop the whole "compile the AST to your storage" step for client-held data.
|
|
4
|
+
import { parseQuery } from "../query/index.js";
|
|
5
|
+
import { matchesRow } from "../query/eval.js";
|
|
6
|
+
const naturalCompare = (a, b) => {
|
|
7
|
+
if (a == null && b == null)
|
|
8
|
+
return 0;
|
|
9
|
+
if (a == null)
|
|
10
|
+
return 1; // nullish sorts last (asc)
|
|
11
|
+
if (b == null)
|
|
12
|
+
return -1;
|
|
13
|
+
const an = typeof a === "number" || (typeof a === "string" && a.trim() !== "" && !Number.isNaN(Number(a)));
|
|
14
|
+
const bn = typeof b === "number" || (typeof b === "string" && b.trim() !== "" && !Number.isNaN(Number(b)));
|
|
15
|
+
if (an && bn)
|
|
16
|
+
return Number(a) - Number(b);
|
|
17
|
+
return String(a).localeCompare(String(b));
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Build a `FetchFn` that runs entirely in memory over `source`.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const grid = createGrid({ columns, getRowId: (r) => r.id, fetch: localFetch(() => rows, columns) });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Pass a getter (`() => rows`) rather than a bare array if the data is reactive (`$state`) and should
|
|
27
|
+
* re-filter when it changes. Filtering honours the columns' `id` (`$token`) and `searchable` flags; a
|
|
28
|
+
* bare-text search scans the `searchable` columns (or every column if none are marked).
|
|
29
|
+
*/
|
|
30
|
+
export function localFetch(source, columns, opts = {}) {
|
|
31
|
+
const getRows = typeof source === "function" ? source : () => source;
|
|
32
|
+
const marked = columns.filter((c) => c.searchable).map((c) => c.id);
|
|
33
|
+
const searchColumns = marked.length ? marked : columns.map((c) => c.id);
|
|
34
|
+
const sortKeyOf = (id) => columns.find((c) => c.id === id)?.sortKey ?? id;
|
|
35
|
+
const value = (row, col) => opts.getValue ? opts.getValue(row, col) : row[col];
|
|
36
|
+
return (state) => {
|
|
37
|
+
const all = getRows();
|
|
38
|
+
let ast = null;
|
|
39
|
+
try {
|
|
40
|
+
ast = parseQuery(state.q);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
ast = null; // invalid query → show everything (the Query input surfaces the error separately)
|
|
44
|
+
}
|
|
45
|
+
const filtered = ast
|
|
46
|
+
? all.filter((r) => matchesRow(r, ast, {
|
|
47
|
+
getValue: (row, col) => value(row, col),
|
|
48
|
+
searchColumns,
|
|
49
|
+
caseInsensitive: opts.caseInsensitive,
|
|
50
|
+
now: opts.now?.(),
|
|
51
|
+
}))
|
|
52
|
+
: all.slice();
|
|
53
|
+
if (state.sort) {
|
|
54
|
+
const key = sortKeyOf(state.sort.col);
|
|
55
|
+
const dir = state.sort.dir === "desc" ? -1 : 1;
|
|
56
|
+
filtered.sort((a, b) => naturalCompare(value(a, key), value(b, key)) * dir);
|
|
57
|
+
}
|
|
58
|
+
const total = filtered.length;
|
|
59
|
+
const start = (state.page - 1) * state.perPage;
|
|
60
|
+
const rows = filtered.slice(start, start + state.perPage);
|
|
61
|
+
return { current: { rows, total }, loading: false, error: undefined };
|
|
62
|
+
};
|
|
63
|
+
}
|
package/docs/components.md
CHANGED
|
@@ -367,7 +367,9 @@ under the active tab. Fully demoed at `/tabstrip`.
|
|
|
367
367
|
## Data Components
|
|
368
368
|
|
|
369
369
|
These pair with the headless **`@spaethtech/svelte-ui/data`** layer (query-language parser/AST, `createGrid`,
|
|
370
|
-
`DataSet`).
|
|
370
|
+
`DataSet`). For a **client-held array**, `fetch: localFetch(rows, columns)` gives full query support in
|
|
371
|
+
memory (`matchesRow` evaluates the AST — filter + sort + page); only a DB backend compiles the query
|
|
372
|
+
itself. See `docs/usage.md`.
|
|
371
373
|
|
|
372
374
|
### DataTable
|
|
373
375
|
|
package/docs/usage.md
CHANGED
|
@@ -1103,24 +1103,31 @@ bar with `Query` over a `DataSet`.
|
|
|
1103
1103
|
import { createGrid } from "@spaethtech/svelte-ui/data";
|
|
1104
1104
|
import IconPencil from "~icons/mdi/pencil";
|
|
1105
1105
|
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
});
|
|
1106
|
+
const columns = [
|
|
1107
|
+
{ id: "name", label: "Name", searchable: true, cell: nameCell },
|
|
1108
|
+
{ id: "email", label: "Email", searchable: true, cell: emailCell },
|
|
1109
|
+
{ id: "status", label: "Status", filter: { kind: "enum", options: ["active", "invited"] }, cell: statusCell },
|
|
1110
|
+
];
|
|
1111
|
+
|
|
1112
|
+
// REMOTE (DB): compile request.q to your storage.
|
|
1113
|
+
// const grid = createGrid({ columns, fetch: (request) => myQuery(request) });
|
|
1114
|
+
|
|
1115
|
+
// CLIENT array: `localFetch` parses the query, filters (matchesRow), sorts + paginates for you —
|
|
1116
|
+
// full query support ($status == active, $name ~= /re/, ranges, bare search…). No hand-rolled filter.
|
|
1117
|
+
const grid = createGrid({ columns, getRowId: (r) => r.id, fetch: localFetch(() => rows, columns) });
|
|
1114
1118
|
</script>
|
|
1115
1119
|
|
|
1116
1120
|
<DataTable
|
|
1117
1121
|
{grid}
|
|
1122
|
+
{columns}
|
|
1118
1123
|
selectable
|
|
1119
1124
|
rowActions={(row) => [{ label: "Edit", icon: IconPencil, onclick: () => edit(row) }]}
|
|
1120
1125
|
/>
|
|
1121
1126
|
```
|
|
1122
1127
|
|
|
1123
|
-
|
|
1128
|
+
`import { createGrid, localFetch, matchesRow } from "@spaethtech/svelte-ui/data";` — `localFetch` is the
|
|
1129
|
+
client-side counterpart to a remote `query()`; `matchesRow(row, ast)` is the underlying AST evaluator if
|
|
1130
|
+
you need to filter an array yourself. See the live demo for the full DataTable + Query walkthrough.
|
|
1124
1131
|
|
|
1125
1132
|
## Theme Integration
|
|
1126
1133
|
|