compote-ui 0.79.2 → 0.81.0
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/components/data-table-v9/column-filter-editing.svelte.d.ts +41 -0
- package/dist/components/data-table-v9/column-filter-editing.svelte.js +88 -0
- package/dist/components/data-table-v9/column-filter-kind.d.ts +19 -0
- package/dist/components/data-table-v9/column-filter-kind.js +32 -0
- package/dist/components/data-table-v9/create-table.svelte.js +37 -25
- package/dist/components/data-table-v9/data-table-head.svelte +32 -3
- package/dist/components/data-table-v9/data-table-head.svelte.d.ts +2 -0
- package/dist/components/data-table-v9/data-table-utils.d.ts +7 -0
- package/dist/components/data-table-v9/data-table-utils.js +4 -0
- package/dist/components/data-table-v9/data-table.svelte +4 -1
- package/dist/components/data-table-v9/data-table.svelte.d.ts +2 -0
- package/dist/components/data-table-v9/date-cell.d.ts +13 -0
- package/dist/components/data-table-v9/date-cell.js +38 -0
- package/dist/components/data-table-v9/filter-editors/boolean-filter-editor.svelte +45 -0
- package/dist/components/data-table-v9/filter-editors/boolean-filter-editor.svelte.d.ts +7 -0
- package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte +39 -0
- package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte.d.ts +35 -0
- package/dist/components/data-table-v9/filter-editors/date-range-filter-editor.svelte +52 -0
- package/dist/components/data-table-v9/filter-editors/date-range-filter-editor.svelte.d.ts +28 -0
- package/dist/components/data-table-v9/filter-editors/range-filter-editor.svelte +51 -0
- package/dist/components/data-table-v9/filter-editors/range-filter-editor.svelte.d.ts +30 -0
- package/dist/components/data-table-v9/filter-editors/select-filter-editor.svelte +59 -0
- package/dist/components/data-table-v9/filter-editors/select-filter-editor.svelte.d.ts +28 -0
- package/dist/components/data-table-v9/filter-editors/text-filter-editor.svelte +20 -0
- package/dist/components/data-table-v9/filter-editors/text-filter-editor.svelte.d.ts +8 -0
- package/dist/components/data-table-v9/header/column-header-filter.svelte +66 -0
- package/dist/components/data-table-v9/header/column-header-filter.svelte.d.ts +30 -0
- package/dist/components/data-table-v9/index.d.ts +9 -1
- package/dist/components/data-table-v9/index.js +7 -0
- package/dist/components/data-table-v9/table-view-state.svelte.d.ts +1 -0
- package/dist/components/data-table-v9/table-view-state.svelte.js +3 -0
- package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte +13 -269
- package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte.d.ts +1 -1
- package/dist/components/data-table-v9/types.d.ts +3 -1
- package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte +4 -1
- package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte.d.ts +2 -0
- package/dist/components/data-table-v9/virtual/index.d.ts +2 -2
- package/dist/components/data-table-v9/virtual/index.js +1 -1
- package/dist/components/dialog/alert-dialog.svelte +2 -0
- package/dist/components/dialog/dialog-root.svelte +2 -0
- package/dist/components/drawer/drawer-content.svelte +2 -0
- package/dist/components/popover/popover-content.svelte +29 -3
- package/dist/components/popover/popover-content.svelte.d.ts +2 -1
- package/dist/components/popover/popover-description.svelte +4 -0
- package/dist/components/popover/popover-labels.svelte.d.ts +18 -0
- package/dist/components/popover/popover-labels.svelte.js +35 -0
- package/dist/components/popover/popover-root.svelte +3 -0
- package/dist/components/popover/popover-title.svelte +4 -0
- package/dist/icons/PhFunnelFill.svelte +19 -0
- package/dist/icons/PhFunnelFill.svelte.d.ts +5 -0
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/utils/date-filter.d.ts +31 -0
- package/dist/utils/date-filter.js +58 -0
- package/dist/utils/date.d.ts +19 -1
- package/dist/utils/date.js +48 -1
- package/dist/utils/release-focus-on-exit.d.ts +22 -0
- package/dist/utils/release-focus-on-exit.js +26 -0
- package/package.json +1 -1
- package/skills/component-usage/references/overlays-and-floating-ui.md +7 -2
- package/skills/data-table/SKILL.md +80 -1
- package/skills/data-table/references/column-options.md +25 -0
- package/skills/data-table/references/table-state-and-reactivity.md +13 -8
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** The slice of a TanStack column the filter editors read and write. */
|
|
2
|
+
export type FilterableColumn = {
|
|
3
|
+
id: string;
|
|
4
|
+
getFilterValue(): unknown;
|
|
5
|
+
setFilterValue(value: unknown): void;
|
|
6
|
+
};
|
|
7
|
+
export type ColumnFilterEditingOptions = {
|
|
8
|
+
/** The table the edited columns belong to; read on every access so it may change. */
|
|
9
|
+
table: () => object;
|
|
10
|
+
/**
|
|
11
|
+
* Whether `columnId` still has an editor. An editor torn down by an outside change can
|
|
12
|
+
* still emit — Ark's NumberInput commits the typed number on the blur its unmount
|
|
13
|
+
* triggers — and that must not write back a filter the app just cleared.
|
|
14
|
+
*/
|
|
15
|
+
isActive?: (columnId: string) => boolean;
|
|
16
|
+
};
|
|
17
|
+
/** Drops every pending filter edit on `table` without committing, whichever host made it. */
|
|
18
|
+
export declare function cancelPendingFilterEdits(table: object): void;
|
|
19
|
+
/**
|
|
20
|
+
* One host's access to a table's pending filter edits (see PendingFilterEdits). The edits
|
|
21
|
+
* live with the table, not with the host or its editors, so an edit still commits after its
|
|
22
|
+
* editor unmounts with a closing popover.
|
|
23
|
+
*
|
|
24
|
+
* A control shows its pending edit if there is one, otherwise the column's live filter
|
|
25
|
+
* value — so a filter changed from outside (the app calling setFilterValue /
|
|
26
|
+
* setColumnFilters / resetColumnFilters) never leaves a stale value in the inputs, an edit
|
|
27
|
+
* pending during that change is dropped rather than re-applied, and committing one range
|
|
28
|
+
* bound never restores the other's old value.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ColumnFilterEditing {
|
|
31
|
+
#private;
|
|
32
|
+
constructor(options: ColumnFilterEditingOptions);
|
|
33
|
+
textValue(column: FilterableColumn): string;
|
|
34
|
+
editText(column: FilterableColumn, value: string): void;
|
|
35
|
+
rangeBound(column: FilterableColumn, which: 'min' | 'max'): number | null;
|
|
36
|
+
editRange(column: FilterableColumn, which: 'min' | 'max', value: number | null): void;
|
|
37
|
+
/** Drops the pending edits for one column without committing them. */
|
|
38
|
+
cancel(columnId: string): void;
|
|
39
|
+
/** Drops every pending edit on the table without committing — including other hosts'. */
|
|
40
|
+
cancelAll(): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { DebouncedFilterEdits } from '../../utils/debounced-filter-edit.svelte';
|
|
2
|
+
import { mergeRangeFilter, rangeBounds } from '../../utils/range-filter';
|
|
3
|
+
import { getFilterRevisions } from './filter-revisions.svelte';
|
|
4
|
+
/**
|
|
5
|
+
* The debounced edits pending on one table's filters. Shared by every component that edits
|
|
6
|
+
* them (the toolbar filter, each header filter), so the latest edit to a column wins no
|
|
7
|
+
* matter where it was made, and cancelling reaches edits made from any of them.
|
|
8
|
+
*/
|
|
9
|
+
class PendingFilterEdits {
|
|
10
|
+
text = new DebouncedFilterEdits();
|
|
11
|
+
range = new DebouncedFilterEdits();
|
|
12
|
+
}
|
|
13
|
+
const registry = new WeakMap();
|
|
14
|
+
function pendingEdits(table) {
|
|
15
|
+
let edits = registry.get(table);
|
|
16
|
+
if (!edits) {
|
|
17
|
+
edits = new PendingFilterEdits();
|
|
18
|
+
registry.set(table, edits);
|
|
19
|
+
}
|
|
20
|
+
return edits;
|
|
21
|
+
}
|
|
22
|
+
/** Drops every pending filter edit on `table` without committing, whichever host made it. */
|
|
23
|
+
export function cancelPendingFilterEdits(table) {
|
|
24
|
+
const edits = registry.get(table);
|
|
25
|
+
edits?.text.cancelAll();
|
|
26
|
+
edits?.range.cancelAll();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One host's access to a table's pending filter edits (see PendingFilterEdits). The edits
|
|
30
|
+
* live with the table, not with the host or its editors, so an edit still commits after its
|
|
31
|
+
* editor unmounts with a closing popover.
|
|
32
|
+
*
|
|
33
|
+
* A control shows its pending edit if there is one, otherwise the column's live filter
|
|
34
|
+
* value — so a filter changed from outside (the app calling setFilterValue /
|
|
35
|
+
* setColumnFilters / resetColumnFilters) never leaves a stale value in the inputs, an edit
|
|
36
|
+
* pending during that change is dropped rather than re-applied, and committing one range
|
|
37
|
+
* bound never restores the other's old value.
|
|
38
|
+
*/
|
|
39
|
+
export class ColumnFilterEditing {
|
|
40
|
+
#table;
|
|
41
|
+
#isActive;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.#table = options.table;
|
|
44
|
+
this.#isActive = options.isActive ?? (() => true);
|
|
45
|
+
}
|
|
46
|
+
get #edits() {
|
|
47
|
+
return pendingEdits(this.#table());
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What an edit is based on: the column's filter revision, which only ever goes up, so a
|
|
51
|
+
* filter cleared and set back to its old value still makes the edit stale. Tables not
|
|
52
|
+
* made by compote's createTable have no revisions; fall back to the filter value.
|
|
53
|
+
*/
|
|
54
|
+
#base(column) {
|
|
55
|
+
const revisions = getFilterRevisions(this.#table());
|
|
56
|
+
return revisions ? revisions.get(column.id) : column.getFilterValue();
|
|
57
|
+
}
|
|
58
|
+
textValue(column) {
|
|
59
|
+
const value = column.getFilterValue();
|
|
60
|
+
return (this.#edits.text.get(column.id, this.#base(column)) ??
|
|
61
|
+
(typeof value === 'string' ? value : ''));
|
|
62
|
+
}
|
|
63
|
+
editText(column, value) {
|
|
64
|
+
if (!this.#isActive(column.id))
|
|
65
|
+
return;
|
|
66
|
+
this.#edits.text.edit(column.id, this.#base(column), () => value, () => this.#base(column), (text) => column.setFilterValue(text || undefined));
|
|
67
|
+
}
|
|
68
|
+
rangeBound(column, which) {
|
|
69
|
+
const pending = this.#edits.range.get(column.id, this.#base(column));
|
|
70
|
+
if (pending && which in pending)
|
|
71
|
+
return pending[which] ?? null;
|
|
72
|
+
return rangeBounds(column.getFilterValue())[which === 'min' ? 0 : 1] ?? null;
|
|
73
|
+
}
|
|
74
|
+
editRange(column, which, value) {
|
|
75
|
+
if (!this.#isActive(column.id))
|
|
76
|
+
return;
|
|
77
|
+
this.#edits.range.edit(column.id, this.#base(column), (pending) => ({ ...pending, [which]: value }), () => this.#base(column), (pending) => column.setFilterValue(mergeRangeFilter(column.getFilterValue(), pending)));
|
|
78
|
+
}
|
|
79
|
+
/** Drops the pending edits for one column without committing them. */
|
|
80
|
+
cancel(columnId) {
|
|
81
|
+
this.#edits.text.cancel(columnId);
|
|
82
|
+
this.#edits.range.cancel(columnId);
|
|
83
|
+
}
|
|
84
|
+
/** Drops every pending edit on the table without committing — including other hosts'. */
|
|
85
|
+
cancelAll() {
|
|
86
|
+
cancelPendingFilterEdits(this.#table());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DataTableColumnMeta, DataTableColumnType } from './types';
|
|
2
|
+
/** Which filter editor (and default filterFn) a column type uses. */
|
|
3
|
+
export type ColumnFilterKind = 'text' | 'range' | 'boolean' | 'select' | 'date';
|
|
4
|
+
/**
|
|
5
|
+
* The filter kind for a column type. The single mapping both the filter editors and the
|
|
6
|
+
* default filterFn follow, so a column never shows an editor its filterFn can't evaluate.
|
|
7
|
+
* `action` columns carry no value and get no filter. `time` stays a text filter until it has
|
|
8
|
+
* its own contract (a time-of-day range has to decide how to cross midnight).
|
|
9
|
+
*/
|
|
10
|
+
export declare function filterKindFor(type: DataTableColumnType | undefined): ColumnFilterKind | null;
|
|
11
|
+
/**
|
|
12
|
+
* The filter kind of a table column (from its `type`): the editor `ColumnFilterEditor` shows
|
|
13
|
+
* for it and the filter value shape its default filterFn expects.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getColumnFilterKind(column: {
|
|
16
|
+
columnDef: {
|
|
17
|
+
meta?: DataTableColumnMeta;
|
|
18
|
+
};
|
|
19
|
+
}): ColumnFilterKind | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filter kind for a column type. The single mapping both the filter editors and the
|
|
3
|
+
* default filterFn follow, so a column never shows an editor its filterFn can't evaluate.
|
|
4
|
+
* `action` columns carry no value and get no filter. `time` stays a text filter until it has
|
|
5
|
+
* its own contract (a time-of-day range has to decide how to cross midnight).
|
|
6
|
+
*/
|
|
7
|
+
export function filterKindFor(type) {
|
|
8
|
+
switch (type) {
|
|
9
|
+
case 'number':
|
|
10
|
+
case 'currency':
|
|
11
|
+
case 'percent':
|
|
12
|
+
return 'range';
|
|
13
|
+
case 'boolean':
|
|
14
|
+
return 'boolean';
|
|
15
|
+
case 'select':
|
|
16
|
+
return 'select';
|
|
17
|
+
case 'date':
|
|
18
|
+
case 'date-time':
|
|
19
|
+
return 'date';
|
|
20
|
+
case 'action':
|
|
21
|
+
return null;
|
|
22
|
+
default:
|
|
23
|
+
return 'text';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The filter kind of a table column (from its `type`): the editor `ColumnFilterEditor` shows
|
|
28
|
+
* for it and the filter value shape its default filterFn expects.
|
|
29
|
+
*/
|
|
30
|
+
export function getColumnFilterKind(column) {
|
|
31
|
+
return filterKindFor(column.columnDef.meta?.type);
|
|
32
|
+
}
|
|
@@ -4,6 +4,10 @@ import { onDestroy } from 'svelte';
|
|
|
4
4
|
import { renderComponent, renderSnippet } from '@tanstack/svelte-table';
|
|
5
5
|
import { dataTableFeatures } from './features';
|
|
6
6
|
import { TYPE_NUMBER_FORMAT_DEFAULTS } from './data-table-utils';
|
|
7
|
+
import { filterKindFor } from './column-filter-kind';
|
|
8
|
+
import { dateCellTimeZone, formatDateCell } from './date-cell';
|
|
9
|
+
import { isEmptyDateRange, matchesDateRange, resolveDateRange } from '../../utils/date-filter';
|
|
10
|
+
import { cancelPendingFilterEdits } from './column-filter-editing.svelte';
|
|
7
11
|
import { changedFilterIds, FilterRevisions, registerFilterRevisions } from './filter-revisions.svelte';
|
|
8
12
|
import { formatSelectValue, matchesSelectFilter, selectCellValues } from '../../utils/select-filter';
|
|
9
13
|
const oneOfFilterFn = (row, columnId, filterValue) => {
|
|
@@ -90,6 +94,22 @@ export function createTable(options) {
|
|
|
90
94
|
onDestroy(() => subscription.unsubscribe());
|
|
91
95
|
}
|
|
92
96
|
registerFilterRevisions(table, filterRevisions);
|
|
97
|
+
// Debounced filter edits outlive the editors that made them, but not the table.
|
|
98
|
+
onDestroy(() => cancelPendingFilterEdits(table));
|
|
99
|
+
// A reset drops pending filter edits too — including one on a column with no committed
|
|
100
|
+
// filter, whose revision the reset leaves alone, so the edit would otherwise commit
|
|
101
|
+
// afterwards and bring a filter back. TanStack assigns these as own methods of the table
|
|
102
|
+
// (`table.reset` also writes state directly, bypassing onColumnFiltersChange).
|
|
103
|
+
const resetColumnFilters = table.resetColumnFilters;
|
|
104
|
+
table.resetColumnFilters = (defaultState) => {
|
|
105
|
+
cancelPendingFilterEdits(table);
|
|
106
|
+
resetColumnFilters(defaultState);
|
|
107
|
+
};
|
|
108
|
+
const resetTable = table.reset;
|
|
109
|
+
table.reset = () => {
|
|
110
|
+
cancelPendingFilterEdits(table);
|
|
111
|
+
resetTable();
|
|
112
|
+
};
|
|
93
113
|
return table;
|
|
94
114
|
}
|
|
95
115
|
function createColumnVisibility(columns) {
|
|
@@ -128,7 +148,7 @@ function createColumns(columns, localeCtx) {
|
|
|
128
148
|
};
|
|
129
149
|
}
|
|
130
150
|
const columnId = getColumnId(column);
|
|
131
|
-
const derivedFilterFn = column.filterFn ?? getFilterFnForType(column
|
|
151
|
+
const derivedFilterFn = column.filterFn ?? getFilterFnForType(column);
|
|
132
152
|
const columnDef = {
|
|
133
153
|
id: columnId,
|
|
134
154
|
header: column.header,
|
|
@@ -191,27 +211,26 @@ export function getColumnId(column) {
|
|
|
191
211
|
return column.accessorKey;
|
|
192
212
|
throw new Error('DataTableColumn with accessorFn requires an id.');
|
|
193
213
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
case 'number':
|
|
208
|
-
case 'currency':
|
|
209
|
-
case 'percent':
|
|
214
|
+
/**
|
|
215
|
+
* A calendar-day range filter for a date column, judged in the zone the column is shown in
|
|
216
|
+
* (read on every pass, so it follows a changed local zone like the cells do).
|
|
217
|
+
*/
|
|
218
|
+
function dateRangeFilterFn(formatOptions) {
|
|
219
|
+
const filterFn = (row, columnId, range) => matchesDateRange(row.getValue(columnId), range, dateCellTimeZone(formatOptions));
|
|
220
|
+
filterFn.resolveFilterValue = resolveDateRange;
|
|
221
|
+
filterFn.autoRemove = isEmptyDateRange;
|
|
222
|
+
return filterFn;
|
|
223
|
+
}
|
|
224
|
+
function getFilterFnForType(column) {
|
|
225
|
+
switch (filterKindFor(column.type)) {
|
|
226
|
+
case 'range':
|
|
210
227
|
return 'inNumberRange';
|
|
211
228
|
case 'boolean':
|
|
212
229
|
return 'equals';
|
|
213
230
|
case 'select':
|
|
214
231
|
return oneOfFilterFn;
|
|
232
|
+
case 'date':
|
|
233
|
+
return dateRangeFilterFn(column.formatOptions);
|
|
215
234
|
default:
|
|
216
235
|
return undefined;
|
|
217
236
|
}
|
|
@@ -231,14 +250,7 @@ function applyTypeFormat(column, value, localeCtx) {
|
|
|
231
250
|
}).format(numericValue);
|
|
232
251
|
}
|
|
233
252
|
if (column.type === 'date' || column.type === 'time' || column.type === 'date-time') {
|
|
234
|
-
|
|
235
|
-
if (isNaN(dateValue.getTime()))
|
|
236
|
-
return undefined;
|
|
237
|
-
const locale = column.formatLocale ?? localeCtx().locale;
|
|
238
|
-
return new Intl.DateTimeFormat(locale, {
|
|
239
|
-
...TYPE_DATE_FORMAT_DEFAULTS[column.type],
|
|
240
|
-
...column.formatOptions
|
|
241
|
-
}).format(dateValue);
|
|
253
|
+
return formatDateCell(value, column.type, column.formatLocale ?? localeCtx().locale, column.formatOptions);
|
|
242
254
|
}
|
|
243
255
|
if (column.type === 'boolean')
|
|
244
256
|
return value ? 'Yes' : 'No';
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import type { DataTableInstance } from './data-table-utils';
|
|
7
7
|
import type { DataTableViewState } from './table-view-state.svelte';
|
|
8
8
|
import type { DataTableFeatures } from './features';
|
|
9
|
+
import type { DataTableAlign } from './types';
|
|
10
|
+
import ColumnHeaderFilter from './header/column-header-filter.svelte';
|
|
9
11
|
import {
|
|
10
12
|
alignClass,
|
|
11
13
|
getColumnMeta,
|
|
@@ -29,9 +31,17 @@
|
|
|
29
31
|
view: DataTableViewState<T>;
|
|
30
32
|
isVirtual?: boolean;
|
|
31
33
|
hasGrowColumn?: boolean;
|
|
34
|
+
/** Show a filter funnel in the header of every filterable column. */
|
|
35
|
+
headerFilters?: boolean;
|
|
32
36
|
};
|
|
33
37
|
|
|
34
|
-
let {
|
|
38
|
+
let {
|
|
39
|
+
table,
|
|
40
|
+
view,
|
|
41
|
+
isVirtual = false,
|
|
42
|
+
hasGrowColumn = false,
|
|
43
|
+
headerFilters = false
|
|
44
|
+
}: Props = $props();
|
|
35
45
|
|
|
36
46
|
type Header = HeaderGroup<DataTableFeatures, T>['headers'][number];
|
|
37
47
|
|
|
@@ -46,6 +56,21 @@
|
|
|
46
56
|
return undefined;
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
function showHeaderFilter(header: Header) {
|
|
60
|
+
return headerFilters && header.subHeaders.length === 0 && header.column.getCanFilter();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Padding that keeps an active (always visible) filter funnel off the label, so it never
|
|
65
|
+
* hides the sort indicator. The funnel sits on the inner edge opposite the label.
|
|
66
|
+
*/
|
|
67
|
+
function headerFilterPaddingClass(header: Header, align: DataTableAlign | undefined) {
|
|
68
|
+
if (!showHeaderFilter(header)) return undefined;
|
|
69
|
+
const filtered = view.columnFilters.some((filter) => filter.id === header.column.id);
|
|
70
|
+
if (!filtered) return undefined;
|
|
71
|
+
return align === 'right' ? 'pl-6' : 'pr-6';
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
function headerSortDirection(header: Header) {
|
|
50
75
|
void view.sorting;
|
|
51
76
|
return header.column.getIsSorted();
|
|
@@ -141,10 +166,11 @@
|
|
|
141
166
|
<th
|
|
142
167
|
scope="col"
|
|
143
168
|
class={cn(
|
|
144
|
-
'relative h-9 border-b border-surface-3 bg-surface-2 px-3 py-0 align-middle leading-5 font-medium',
|
|
169
|
+
'group/th relative h-9 border-b border-surface-3 bg-surface-2 px-3 py-0 align-middle leading-5 font-medium',
|
|
145
170
|
isVirtual && 'items-center',
|
|
146
171
|
isVirtual && justifyClass(meta?.align),
|
|
147
|
-
alignClass(meta?.align)
|
|
172
|
+
alignClass(meta?.align),
|
|
173
|
+
headerFilterPaddingClass(header, meta?.align)
|
|
148
174
|
)}
|
|
149
175
|
colspan={header.colSpan}
|
|
150
176
|
aria-sort={header.column.getCanSort() ? getHeaderAriaSort(sortDirection) : undefined}
|
|
@@ -179,6 +205,9 @@
|
|
|
179
205
|
<FlexRender {header} />
|
|
180
206
|
{/if}
|
|
181
207
|
{/if}
|
|
208
|
+
{#if !header.isPlaceholder && showHeaderFilter(header)}
|
|
209
|
+
<ColumnHeaderFilter {table} column={header.column} />
|
|
210
|
+
{/if}
|
|
182
211
|
<!-- A group split by pinning renders as adjacent fragments; skip the
|
|
183
212
|
divider between them so the group reads as one continuous header. -->
|
|
184
213
|
{#if header.column.getCanResize() && !continuesInNextFragment}
|
|
@@ -7,6 +7,8 @@ declare function $$render<T extends RowData>(): {
|
|
|
7
7
|
view: DataTableViewState<T>;
|
|
8
8
|
isVirtual?: boolean;
|
|
9
9
|
hasGrowColumn?: boolean;
|
|
10
|
+
/** Show a filter funnel in the header of every filterable column. */
|
|
11
|
+
headerFilters?: boolean;
|
|
10
12
|
};
|
|
11
13
|
exports: {};
|
|
12
14
|
bindings: "";
|
|
@@ -81,6 +81,13 @@ export declare function getUrlCellValue(value: unknown): string | undefined;
|
|
|
81
81
|
export declare function openUrlCell(value: string): void;
|
|
82
82
|
export declare function getPhoneCellValue(value: unknown): string | undefined;
|
|
83
83
|
export declare function openPhoneCell(value: string): void;
|
|
84
|
+
/** A column's display name: its string header, or its id when the header is rendered. */
|
|
85
|
+
export declare function getColumnLabel(column: {
|
|
86
|
+
id: string;
|
|
87
|
+
columnDef: {
|
|
88
|
+
header?: unknown;
|
|
89
|
+
};
|
|
90
|
+
}): string;
|
|
84
91
|
export declare function getColumnMeta(columnDef: {
|
|
85
92
|
meta?: DataTableColumnMeta;
|
|
86
93
|
}): DataTableColumnMeta | undefined;
|
|
@@ -154,6 +154,10 @@ export function getPhoneCellValue(value) {
|
|
|
154
154
|
export function openPhoneCell(value) {
|
|
155
155
|
window.location.href = `tel:${value}`;
|
|
156
156
|
}
|
|
157
|
+
/** A column's display name: its string header, or its id when the header is rendered. */
|
|
158
|
+
export function getColumnLabel(column) {
|
|
159
|
+
return typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id;
|
|
160
|
+
}
|
|
157
161
|
// `columnDef.meta` is natively typed as DataTableColumnMeta via the `columnMeta`
|
|
158
162
|
// type-only slot in features.ts; this accessor just narrows the columnDef shape.
|
|
159
163
|
export function getColumnMeta(columnDef) {
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
caption?: string;
|
|
23
23
|
emptyMessage?: string;
|
|
24
24
|
class?: ClassValue;
|
|
25
|
+
/** Show a filter funnel in the header of every filterable column. */
|
|
26
|
+
headerFilters?: boolean;
|
|
25
27
|
onRowClick?: (details: { row: T; event: MouseEvent }) => void;
|
|
26
28
|
onRowDoubleClick?: (details: { row: T; event: MouseEvent }) => void;
|
|
27
29
|
};
|
|
@@ -31,6 +33,7 @@
|
|
|
31
33
|
caption,
|
|
32
34
|
emptyMessage = 'No rows found',
|
|
33
35
|
class: className,
|
|
36
|
+
headerFilters = false,
|
|
34
37
|
onRowClick,
|
|
35
38
|
onRowDoubleClick,
|
|
36
39
|
...rest
|
|
@@ -77,7 +80,7 @@
|
|
|
77
80
|
{#if caption}
|
|
78
81
|
<caption class="sr-only">{caption}</caption>
|
|
79
82
|
{/if}
|
|
80
|
-
<DataTableHead {table} {view} hasGrowColumn={view.hasGrowColumn} />
|
|
83
|
+
<DataTableHead {table} {view} hasGrowColumn={view.hasGrowColumn} {headerFilters} />
|
|
81
84
|
<tbody>
|
|
82
85
|
{#each view.rowModel.rows as row (row.id)}
|
|
83
86
|
{@const rowSelected = view.rowSelection[row.id] === true}
|
|
@@ -8,6 +8,8 @@ declare function $$render<T extends RowData>(): {
|
|
|
8
8
|
caption?: string;
|
|
9
9
|
emptyMessage?: string;
|
|
10
10
|
class?: ClassValue;
|
|
11
|
+
/** Show a filter funnel in the header of every filterable column. */
|
|
12
|
+
headerFilters?: boolean;
|
|
11
13
|
onRowClick?: (details: {
|
|
12
14
|
row: T;
|
|
13
15
|
event: MouseEvent;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type DateCellType = 'date' | 'time' | 'date-time';
|
|
2
|
+
export declare const TYPE_DATE_FORMAT_DEFAULTS: Record<DateCellType, Intl.DateTimeFormatOptions>;
|
|
3
|
+
/**
|
|
4
|
+
* The zone a date column is shown — and therefore filtered — in: `formatOptions.timeZone`
|
|
5
|
+
* when the column sets one, otherwise the viewer's local zone.
|
|
6
|
+
*/
|
|
7
|
+
export declare function dateCellTimeZone(formatOptions: Intl.DateTimeFormatOptions | undefined): string;
|
|
8
|
+
/**
|
|
9
|
+
* A date cell value formatted for display, or undefined when it can't be read. Reads the
|
|
10
|
+
* value with `readDateInZone`, exactly as the date range filter does, so a cell always shows
|
|
11
|
+
* the day it is filtered by.
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatDateCell(value: unknown, type: DateCellType, locale: string, formatOptions?: Intl.DateTimeFormatOptions): string | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CalendarDate, getLocalTimeZone } from '@internationalized/date';
|
|
2
|
+
import { readDateInZone } from '../../utils/date';
|
|
3
|
+
export const TYPE_DATE_FORMAT_DEFAULTS = {
|
|
4
|
+
date: { day: '2-digit', month: '2-digit', year: 'numeric' },
|
|
5
|
+
time: { hour: '2-digit', minute: '2-digit' },
|
|
6
|
+
'date-time': {
|
|
7
|
+
day: '2-digit',
|
|
8
|
+
month: '2-digit',
|
|
9
|
+
year: 'numeric',
|
|
10
|
+
hour: '2-digit',
|
|
11
|
+
minute: '2-digit'
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The zone a date column is shown — and therefore filtered — in: `formatOptions.timeZone`
|
|
16
|
+
* when the column sets one, otherwise the viewer's local zone.
|
|
17
|
+
*/
|
|
18
|
+
export function dateCellTimeZone(formatOptions) {
|
|
19
|
+
return formatOptions?.timeZone ?? getLocalTimeZone();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A date cell value formatted for display, or undefined when it can't be read. Reads the
|
|
23
|
+
* value with `readDateInZone`, exactly as the date range filter does, so a cell always shows
|
|
24
|
+
* the day it is filtered by.
|
|
25
|
+
*/
|
|
26
|
+
export function formatDateCell(value, type, locale, formatOptions) {
|
|
27
|
+
const timeZone = dateCellTimeZone(formatOptions);
|
|
28
|
+
const date = readDateInZone(value, timeZone);
|
|
29
|
+
if (!date)
|
|
30
|
+
return undefined;
|
|
31
|
+
const options = { ...TYPE_DATE_FORMAT_DEFAULTS[type], ...formatOptions };
|
|
32
|
+
// A calendar date has no instant: format its own year-month-day as UTC midnight in UTC,
|
|
33
|
+
// so no display zone can move it to another day.
|
|
34
|
+
if (date instanceof CalendarDate) {
|
|
35
|
+
return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }).format(date.toDate('UTC'));
|
|
36
|
+
}
|
|
37
|
+
return new Intl.DateTimeFormat(locale, { ...options, timeZone }).format(date.toDate());
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from 'tailwind-variants';
|
|
3
|
+
import type { FilterableColumn } from '../column-filter-editing.svelte';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
column: FilterableColumn;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
let { column }: Props = $props();
|
|
10
|
+
|
|
11
|
+
const value = $derived(column.getFilterValue() as boolean | undefined);
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div class="flex overflow-hidden rounded border border-border text-xs">
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
onclick={() => column.setFilterValue(undefined)}
|
|
18
|
+
class={cn(
|
|
19
|
+
'flex-1 px-2 py-1',
|
|
20
|
+
value === undefined ? 'bg-surface-3 font-medium text-ink' : 'text-ink-dim hover:bg-surface-2'
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
All
|
|
24
|
+
</button>
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
onclick={() => column.setFilterValue(value === true ? undefined : true)}
|
|
28
|
+
class={cn(
|
|
29
|
+
'flex-1 border-x border-border px-2 py-1',
|
|
30
|
+
value === true ? 'bg-surface-3 font-medium text-ink' : 'text-ink-dim hover:bg-surface-2'
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
Yes
|
|
34
|
+
</button>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onclick={() => column.setFilterValue(value === false ? undefined : false)}
|
|
38
|
+
class={cn(
|
|
39
|
+
'flex-1 px-2 py-1',
|
|
40
|
+
value === false ? 'bg-surface-3 font-medium text-ink' : 'text-ink-dim hover:bg-surface-2'
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
No
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FilterableColumn } from '../column-filter-editing.svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
column: FilterableColumn;
|
|
4
|
+
};
|
|
5
|
+
declare const BooleanFilterEditor: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type BooleanFilterEditor = ReturnType<typeof BooleanFilterEditor>;
|
|
7
|
+
export default BooleanFilterEditor;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends RowData">
|
|
2
|
+
import type { Column, RowData } from '@tanstack/svelte-table';
|
|
3
|
+
import type { DataTableFeatures } from '../features';
|
|
4
|
+
import { getColumnFilterKind } from '../column-filter-kind';
|
|
5
|
+
import { ColumnFilterEditing } from '../column-filter-editing.svelte';
|
|
6
|
+
import TextFilterEditor from './text-filter-editor.svelte';
|
|
7
|
+
import RangeFilterEditor from './range-filter-editor.svelte';
|
|
8
|
+
import BooleanFilterEditor from './boolean-filter-editor.svelte';
|
|
9
|
+
import SelectFilterEditor from './select-filter-editor.svelte';
|
|
10
|
+
import DateRangeFilterEditor from './date-range-filter-editor.svelte';
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
column: Column<DataTableFeatures, T, unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* Pending (debounced) text and range edits. Hosts that can tear an editor down while
|
|
16
|
+
* it still holds an edit pass their own, with `isActive`; otherwise the editor uses
|
|
17
|
+
* one bound to the column's table.
|
|
18
|
+
*/
|
|
19
|
+
editing?: ColumnFilterEditing;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let { column, editing: editingProp }: Props = $props();
|
|
23
|
+
|
|
24
|
+
const ownEditing = new ColumnFilterEditing({ table: () => column.table });
|
|
25
|
+
const editing = $derived(editingProp ?? ownEditing);
|
|
26
|
+
const kind = $derived(getColumnFilterKind(column));
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
{#if kind === 'range'}
|
|
30
|
+
<RangeFilterEditor {column} {editing} />
|
|
31
|
+
{:else if kind === 'boolean'}
|
|
32
|
+
<BooleanFilterEditor {column} />
|
|
33
|
+
{:else if kind === 'select'}
|
|
34
|
+
<SelectFilterEditor {column} />
|
|
35
|
+
{:else if kind === 'date'}
|
|
36
|
+
<DateRangeFilterEditor {column} />
|
|
37
|
+
{:else if kind === 'text'}
|
|
38
|
+
<TextFilterEditor {column} {editing} />
|
|
39
|
+
{/if}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Column, RowData } from '@tanstack/svelte-table';
|
|
2
|
+
import type { DataTableFeatures } from '../features';
|
|
3
|
+
import { ColumnFilterEditing } from '../column-filter-editing.svelte';
|
|
4
|
+
declare function $$render<T extends RowData>(): {
|
|
5
|
+
props: {
|
|
6
|
+
column: Column<DataTableFeatures, T, unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Pending (debounced) text and range edits. Hosts that can tear an editor down while
|
|
9
|
+
* it still holds an edit pass their own, with `isActive`; otherwise the editor uses
|
|
10
|
+
* one bound to the column's table.
|
|
11
|
+
*/
|
|
12
|
+
editing?: ColumnFilterEditing;
|
|
13
|
+
};
|
|
14
|
+
exports: {};
|
|
15
|
+
bindings: "";
|
|
16
|
+
slots: {};
|
|
17
|
+
events: {};
|
|
18
|
+
};
|
|
19
|
+
declare class __sveltets_Render<T extends RowData> {
|
|
20
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
21
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
22
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
23
|
+
bindings(): "";
|
|
24
|
+
exports(): {};
|
|
25
|
+
}
|
|
26
|
+
interface $$IsomorphicComponent {
|
|
27
|
+
new <T extends RowData>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
28
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
29
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
30
|
+
<T extends RowData>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
31
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
32
|
+
}
|
|
33
|
+
declare const ColumnFilterEditor: $$IsomorphicComponent;
|
|
34
|
+
type ColumnFilterEditor<T extends RowData> = InstanceType<typeof ColumnFilterEditor<T>>;
|
|
35
|
+
export default ColumnFilterEditor;
|