bo-grid 0.1.0 → 0.7.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/README.md +154 -28
- package/dist/grid/Cell.svelte +173 -11
- package/dist/grid/Cell.svelte.d.ts +22 -1
- package/dist/grid/FilterMenu.svelte +263 -0
- package/dist/grid/FilterMenu.svelte.d.ts +15 -0
- package/dist/grid/Grid.svelte +978 -61
- package/dist/grid/Grid.svelte.d.ts +72 -1
- package/dist/grid/Pager.svelte +59 -0
- package/dist/grid/Pager.svelte.d.ts +9 -0
- package/dist/grid/RowMenu.svelte +66 -0
- package/dist/grid/RowMenu.svelte.d.ts +12 -0
- package/dist/grid/ToolPanel.svelte +117 -0
- package/dist/grid/ToolPanel.svelte.d.ts +15 -0
- package/dist/grid/column.d.ts +33 -5
- package/dist/grid/column.js +8 -6
- package/dist/grid/export.js +1 -1
- package/dist/grid/filtering.d.ts +41 -0
- package/dist/grid/filtering.js +107 -0
- package/dist/grid/grouping.d.ts +2 -0
- package/dist/grid/pin.d.ts +10 -6
- package/dist/grid/pin.js +38 -15
- package/dist/grid/sizing.d.ts +2 -2
- package/dist/grid/sizing.js +4 -3
- package/dist/grid/source.d.ts +3 -0
- package/dist/grid/source.js +5 -0
- package/dist/grid/source.svelte.d.ts +2 -1
- package/dist/grid/source.svelte.js +6 -5
- package/dist/grid/tree.d.ts +12 -0
- package/dist/grid/tree.js +21 -0
- package/dist/index.d.ts +1 -0
- package/package.json +3 -1
|
@@ -2,6 +2,7 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
import type { ColumnDef, GridRow, SortState, CellEditEvent } from './column';
|
|
3
3
|
import { type GridTheme } from './theme';
|
|
4
4
|
import { type AggKind } from './aggregate';
|
|
5
|
+
import { type ColumnFilter } from './filtering';
|
|
5
6
|
import type { RowSource } from './source';
|
|
6
7
|
type $$ComponentProps = {
|
|
7
8
|
rows: GridRow[];
|
|
@@ -21,8 +22,19 @@ type $$ComponentProps = {
|
|
|
21
22
|
/** Called with the selected row ids whenever the row-selection set changes. */
|
|
22
23
|
onRowSelectionChange?: (selectedIds: Array<string | number>) => void;
|
|
23
24
|
/** Column keys to hide (controlled). Build your own column-picker UI and
|
|
24
|
-
drive this prop — the grid stays presentation-only.
|
|
25
|
+
drive this prop — the grid stays presentation-only. Composed (union) with
|
|
26
|
+
columns the user hides at runtime via the column menu. */
|
|
25
27
|
hiddenColumns?: string[];
|
|
28
|
+
/** Called with all currently-hidden column keys whenever the runtime set
|
|
29
|
+
changes (column menu hide/show). */
|
|
30
|
+
onColumnVisibilityChange?: (hidden: string[]) => void;
|
|
31
|
+
/** Enable a per-column header menu (a ⋮ trigger) with sort, hide and (with
|
|
32
|
+
`filterMenu`) filter actions. Default false. */
|
|
33
|
+
columnMenu?: boolean;
|
|
34
|
+
/** Show a "Columns" button that opens a panel to toggle column visibility
|
|
35
|
+
(the place to restore columns hidden via the menu). Lazy-loaded. Default
|
|
36
|
+
false. */
|
|
37
|
+
columnsPanel?: boolean;
|
|
26
38
|
/** Return extra CSS class(es) for a data row (e.g. to colour by value).
|
|
27
39
|
Style them via `:global(.your-class)` since rows live inside the grid. */
|
|
28
40
|
rowClass?: (row: GridRow) => string | undefined;
|
|
@@ -38,6 +50,12 @@ type $$ComponentProps = {
|
|
|
38
50
|
sort?: SortState[];
|
|
39
51
|
/** Called with the new sort order whenever a header is clicked. */
|
|
40
52
|
onSortChange?: (sort: SortState[]) => void;
|
|
53
|
+
/** Controlled column filters (keyed by column key). When set, the grid
|
|
54
|
+
reflects these and reports changes via `onFilterChange` instead of holding
|
|
55
|
+
its own. Omit for uncontrolled filtering. */
|
|
56
|
+
columnFilters?: Record<string, ColumnFilter>;
|
|
57
|
+
/** Called with the full column-filter map whenever a header filter changes. */
|
|
58
|
+
onFilterChange?: (filters: Record<string, ColumnFilter>) => void;
|
|
41
59
|
/** Show a pinned totals row: each column with a `groupAgg` shows that
|
|
42
60
|
aggregate over all (filtered) rows. In-memory mode only. Default false. */
|
|
43
61
|
footer?: boolean;
|
|
@@ -54,6 +72,59 @@ type $$ComponentProps = {
|
|
|
54
72
|
/** Show a per-column filter input row under the header. Rows must match every
|
|
55
73
|
non-empty column filter (AND). In-memory mode only. Default false. */
|
|
56
74
|
filterRow?: boolean;
|
|
75
|
+
/** Enable a per-column header filter menu (lazy-loaded on first open). Each
|
|
76
|
+
filterable column shows a funnel; the menu's control matches the column
|
|
77
|
+
type (text/number/date). Override or disable per column with `col.filter`.
|
|
78
|
+
Works in source mode too (filters are delegated to the `RowSource`); set
|
|
79
|
+
filters need in-memory data. Default false. */
|
|
80
|
+
filterMenu?: boolean;
|
|
81
|
+
/** Show a built-in quick-filter search box above the grid that matches across
|
|
82
|
+
all column values (ANDed with the `filter` prop). In-memory mode only.
|
|
83
|
+
Default false. */
|
|
84
|
+
quickFilter?: boolean;
|
|
85
|
+
/** Show an Excel-style fill handle at the selection's bottom-right corner;
|
|
86
|
+
drag it to copy the selected value(s) across the extended range (editable
|
|
87
|
+
columns only). In-memory mode only. Default false. */
|
|
88
|
+
fillHandle?: boolean;
|
|
89
|
+
/** Message shown when there are no rows. Default 'No matching rows'. */
|
|
90
|
+
emptyMessage?: string;
|
|
91
|
+
/** Show a loading overlay over the grid (for consumer-driven async work in
|
|
92
|
+
in-memory mode; source mode shows skeleton rows automatically). */
|
|
93
|
+
loading?: boolean;
|
|
94
|
+
/** Right-click row menu. Return the items for a row; an empty array shows no
|
|
95
|
+
menu. Each item runs `onSelect` and closes the menu. */
|
|
96
|
+
rowMenu?: (row: GridRow) => Array<{
|
|
97
|
+
label: string;
|
|
98
|
+
onSelect: () => void;
|
|
99
|
+
}>;
|
|
100
|
+
/** Master-detail: render an expandable detail panel under a row. Adds a
|
|
101
|
+
leading expand-toggle column. In-memory mode only (overrides rowHeight). */
|
|
102
|
+
detail?: Snippet<[{
|
|
103
|
+
row: GridRow;
|
|
104
|
+
}]>;
|
|
105
|
+
/** Height (px) of the expanded detail panel. Default 160. */
|
|
106
|
+
detailHeight?: number;
|
|
107
|
+
/** Tree data: return a row's children (undefined/empty = leaf). When set,
|
|
108
|
+
`rows` are the roots; the grid renders an indented, expandable tree.
|
|
109
|
+
In-memory mode; filter/sort/group/paginate are not applied to the tree. */
|
|
110
|
+
getChildren?: (row: GridRow) => GridRow[] | undefined;
|
|
111
|
+
/** Enable drag-to-reorder rows via a handle in the first column. Called with
|
|
112
|
+
the from/to indices (into the visible rows) on drop — reorder your own
|
|
113
|
+
`rows` in here. Flat, unsorted, in-memory lists only. */
|
|
114
|
+
onRowReorder?: (fromIndex: number, toIndex: number) => void;
|
|
115
|
+
/** Rows per page. When > 0 (in-memory mode), shows a pager instead of one
|
|
116
|
+
long scroll; rows still virtualize within a page. Default 0 (off). */
|
|
117
|
+
pageSize?: number;
|
|
118
|
+
/** Controlled current page (0-based). Omit for uncontrolled paging. */
|
|
119
|
+
page?: number;
|
|
120
|
+
/** Called with the new page index when the pager is used. */
|
|
121
|
+
onPageChange?: (page: number) => void;
|
|
122
|
+
/** Called with the new column-key order after a header drag-reorder. */
|
|
123
|
+
onColumnReorder?: (keys: string[]) => void;
|
|
124
|
+
/** Called with a column key + new width after a drag-resize. */
|
|
125
|
+
onColumnResize?: (key: string, width: number) => void;
|
|
126
|
+
/** Accessible name for the grid (`aria-label` on the `role="grid"` root). */
|
|
127
|
+
ariaLabel?: string;
|
|
57
128
|
filter?: string;
|
|
58
129
|
groupBy?: string[];
|
|
59
130
|
aggregations?: AggKind[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Pagination bar for the grid. Presentation-only: the parent owns the page
|
|
3
|
+
// state and reorders via `onGoto`.
|
|
4
|
+
let {
|
|
5
|
+
page,
|
|
6
|
+
pageCount,
|
|
7
|
+
total,
|
|
8
|
+
onGoto,
|
|
9
|
+
}: {
|
|
10
|
+
page: number;
|
|
11
|
+
pageCount: number;
|
|
12
|
+
total: number;
|
|
13
|
+
onGoto: (page: number) => void;
|
|
14
|
+
} = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="pager" role="navigation" aria-label="Pagination">
|
|
18
|
+
<button type="button" class="pg" disabled={page === 0} aria-label="First page" onclick={() => onGoto(0)}>«</button>
|
|
19
|
+
<button type="button" class="pg" disabled={page === 0} onclick={() => onGoto(page - 1)}>‹ Prev</button>
|
|
20
|
+
<span class="pageinfo">Page {page + 1} of {pageCount} · {total.toLocaleString()} rows</span>
|
|
21
|
+
<button type="button" class="pg" disabled={page >= pageCount - 1} onclick={() => onGoto(page + 1)}>Next ›</button>
|
|
22
|
+
<button type="button" class="pg" disabled={page >= pageCount - 1} aria-label="Last page" onclick={() => onGoto(pageCount - 1)}>»</button>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
.pager {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 8px;
|
|
30
|
+
padding: 6px 10px;
|
|
31
|
+
background: var(--bo-header-bg);
|
|
32
|
+
border-top: 0.5px solid var(--bo-border);
|
|
33
|
+
}
|
|
34
|
+
.pg {
|
|
35
|
+
padding: 3px 9px;
|
|
36
|
+
font: inherit;
|
|
37
|
+
font-family: var(--bo-mono);
|
|
38
|
+
font-size: 11px;
|
|
39
|
+
color: var(--bo-text);
|
|
40
|
+
background: transparent;
|
|
41
|
+
border: 0.5px solid var(--bo-border);
|
|
42
|
+
border-radius: 6px;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
}
|
|
45
|
+
.pg:hover:not(:disabled) {
|
|
46
|
+
background: var(--bo-row-hover);
|
|
47
|
+
border-color: var(--bo-sel-border);
|
|
48
|
+
}
|
|
49
|
+
.pg:disabled {
|
|
50
|
+
opacity: 0.4;
|
|
51
|
+
cursor: not-allowed;
|
|
52
|
+
}
|
|
53
|
+
.pageinfo {
|
|
54
|
+
font-family: var(--bo-mono);
|
|
55
|
+
font-size: 11px;
|
|
56
|
+
color: var(--bo-text-dim);
|
|
57
|
+
font-variant-numeric: tabular-nums;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Floating right-click row menu. Presentation-only: the parent owns open/close
|
|
3
|
+
// state and positions it via x/y.
|
|
4
|
+
let {
|
|
5
|
+
x,
|
|
6
|
+
y,
|
|
7
|
+
items,
|
|
8
|
+
onClose,
|
|
9
|
+
}: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
items: Array<{ label: string; onSelect: () => void }>;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
} = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
class="rowmenu"
|
|
19
|
+
role="menu"
|
|
20
|
+
tabindex="-1"
|
|
21
|
+
style="left:{x}px;top:{y}px;"
|
|
22
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
23
|
+
>
|
|
24
|
+
{#each items as item (item.label)}
|
|
25
|
+
<button
|
|
26
|
+
class="rowmenu-item"
|
|
27
|
+
type="button"
|
|
28
|
+
role="menuitem"
|
|
29
|
+
onclick={() => {
|
|
30
|
+
item.onSelect();
|
|
31
|
+
onClose();
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
{item.label}
|
|
35
|
+
</button>
|
|
36
|
+
{/each}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.rowmenu {
|
|
41
|
+
position: fixed;
|
|
42
|
+
z-index: 20;
|
|
43
|
+
min-width: 150px;
|
|
44
|
+
padding: 4px;
|
|
45
|
+
background: var(--bo-header-bg);
|
|
46
|
+
border: 0.5px solid var(--bo-border);
|
|
47
|
+
border-radius: 8px;
|
|
48
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
|
49
|
+
}
|
|
50
|
+
.rowmenu-item {
|
|
51
|
+
display: block;
|
|
52
|
+
width: 100%;
|
|
53
|
+
padding: 6px 10px;
|
|
54
|
+
font: inherit;
|
|
55
|
+
font-size: 12px;
|
|
56
|
+
text-align: left;
|
|
57
|
+
color: var(--bo-text);
|
|
58
|
+
background: transparent;
|
|
59
|
+
border: 0;
|
|
60
|
+
border-radius: 5px;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
.rowmenu-item:hover {
|
|
64
|
+
background: var(--bo-row-hover);
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
items: Array<{
|
|
5
|
+
label: string;
|
|
6
|
+
onSelect: () => void;
|
|
7
|
+
}>;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
};
|
|
10
|
+
declare const RowMenu: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type RowMenu = ReturnType<typeof RowMenu>;
|
|
12
|
+
export default RowMenu;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Floating columns panel: toggle column visibility (and restore hidden ones).
|
|
3
|
+
// Lazy-loaded by Grid; presentation-only — the parent owns the visibility set.
|
|
4
|
+
let {
|
|
5
|
+
columns,
|
|
6
|
+
hidden,
|
|
7
|
+
x,
|
|
8
|
+
y,
|
|
9
|
+
onToggle,
|
|
10
|
+
onShowAll,
|
|
11
|
+
onClose,
|
|
12
|
+
}: {
|
|
13
|
+
columns: Array<{ key: string; header: string }>;
|
|
14
|
+
hidden: string[];
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
onToggle: (key: string) => void;
|
|
18
|
+
onShowAll: () => void;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
let search = $state('');
|
|
23
|
+
const shown = $derived(
|
|
24
|
+
columns.filter((c) => c.header.toLowerCase().includes(search.trim().toLowerCase())),
|
|
25
|
+
);
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<div
|
|
29
|
+
class="bo-toolpanel"
|
|
30
|
+
role="dialog"
|
|
31
|
+
tabindex="-1"
|
|
32
|
+
aria-label="Columns"
|
|
33
|
+
style="left:{x}px;top:{y}px;"
|
|
34
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
35
|
+
onkeydown={(e) => e.key === 'Escape' && onClose()}
|
|
36
|
+
>
|
|
37
|
+
<div class="bo-tp-head">
|
|
38
|
+
<span>Columns</span>
|
|
39
|
+
<button type="button" class="bo-tp-link" onclick={onShowAll}>Show all</button>
|
|
40
|
+
</div>
|
|
41
|
+
<input class="bo-tp-search" type="search" bind:value={search} placeholder="search…" aria-label="Search columns" />
|
|
42
|
+
<div class="bo-tp-list">
|
|
43
|
+
{#each shown as col (col.key)}
|
|
44
|
+
<label class="bo-tp-opt">
|
|
45
|
+
<input type="checkbox" checked={!hidden.includes(col.key)} onchange={() => onToggle(col.key)} />
|
|
46
|
+
<span>{col.header}</span>
|
|
47
|
+
</label>
|
|
48
|
+
{/each}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
.bo-toolpanel {
|
|
54
|
+
position: fixed;
|
|
55
|
+
z-index: 30;
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
gap: 7px;
|
|
59
|
+
width: 200px;
|
|
60
|
+
padding: 10px;
|
|
61
|
+
background: var(--bo-header-bg);
|
|
62
|
+
border: 0.5px solid var(--bo-border);
|
|
63
|
+
border-radius: 8px;
|
|
64
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
color: var(--bo-text);
|
|
67
|
+
}
|
|
68
|
+
.bo-tp-head {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: baseline;
|
|
71
|
+
justify-content: space-between;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
color: var(--bo-text-dim);
|
|
74
|
+
}
|
|
75
|
+
.bo-tp-link {
|
|
76
|
+
padding: 0;
|
|
77
|
+
font: inherit;
|
|
78
|
+
font-size: 11px;
|
|
79
|
+
color: var(--bo-up);
|
|
80
|
+
background: none;
|
|
81
|
+
border: 0;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
}
|
|
84
|
+
.bo-tp-link:hover {
|
|
85
|
+
text-decoration: underline;
|
|
86
|
+
}
|
|
87
|
+
.bo-tp-search {
|
|
88
|
+
width: 100%;
|
|
89
|
+
padding: 5px 7px;
|
|
90
|
+
font: inherit;
|
|
91
|
+
color: var(--bo-text);
|
|
92
|
+
background: var(--bo-bg);
|
|
93
|
+
border: 0.5px solid var(--bo-border);
|
|
94
|
+
border-radius: 5px;
|
|
95
|
+
}
|
|
96
|
+
.bo-tp-list {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
max-height: 220px;
|
|
100
|
+
overflow-y: auto;
|
|
101
|
+
}
|
|
102
|
+
.bo-tp-opt {
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 7px;
|
|
106
|
+
padding: 4px 4px;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
}
|
|
110
|
+
.bo-tp-opt:hover {
|
|
111
|
+
background: var(--bo-row-hover);
|
|
112
|
+
}
|
|
113
|
+
.bo-tp-opt span {
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
columns: Array<{
|
|
3
|
+
key: string;
|
|
4
|
+
header: string;
|
|
5
|
+
}>;
|
|
6
|
+
hidden: string[];
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
onToggle: (key: string) => void;
|
|
10
|
+
onShowAll: () => void;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
};
|
|
13
|
+
declare const ToolPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
14
|
+
type ToolPanel = ReturnType<typeof ToolPanel>;
|
|
15
|
+
export default ToolPanel;
|
package/dist/grid/column.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Candle } from '../types';
|
|
2
2
|
import { type DateStyle } from '../format/format';
|
|
3
3
|
import type { AggKind } from './aggregate';
|
|
4
|
+
import type { FilterKind } from './filtering';
|
|
4
5
|
export type Align = 'left' | 'right';
|
|
5
6
|
interface ColBase {
|
|
6
7
|
/** Field on the row to read for this column's value. */
|
|
@@ -8,27 +9,54 @@ interface ColBase {
|
|
|
8
9
|
header: string;
|
|
9
10
|
/** Fixed width in px. Ignored when `flex` is set. */
|
|
10
11
|
width?: number;
|
|
12
|
+
/** Min/max width (px) enforced while drag-resizing this column. */
|
|
13
|
+
minWidth?: number;
|
|
14
|
+
maxWidth?: number;
|
|
11
15
|
/** flex-grow weight; column stretches to fill remaining space. */
|
|
12
16
|
flex?: number;
|
|
13
17
|
align?: Align;
|
|
18
|
+
/** Extra class(es) for this column's data cells — a static string, or a
|
|
19
|
+
function of the cell value/row for conditional styling. Target via
|
|
20
|
+
`:global(.bo-grid .c.your-class)`. */
|
|
21
|
+
cellClass?: string | ((value: unknown, row: GridRow) => string | undefined);
|
|
22
|
+
/** Extra class(es) for this column's header. Target via `:global`. */
|
|
23
|
+
headerClass?: string;
|
|
14
24
|
/** Amber flash on value change (drives off the row's flashSeq/flashDir). */
|
|
15
25
|
flash?: boolean;
|
|
16
26
|
/** Set false to disable header-click sorting on this column. */
|
|
17
27
|
sortable?: boolean;
|
|
28
|
+
/** Custom ascending comparator for this column's values (e.g. enum priority or
|
|
29
|
+
natural sort). Direction is applied by the grid. In-memory mode only. */
|
|
30
|
+
compare?: (a: unknown, b: unknown) => number;
|
|
18
31
|
/** When grouping is active, show this aggregate of the column on group headers. */
|
|
19
32
|
groupAgg?: AggKind;
|
|
20
|
-
/** Pin this column
|
|
21
|
-
|
|
33
|
+
/** Pin this column so it stays visible during horizontal scroll. `true` /
|
|
34
|
+
`'left'` pins to the left edge, `'right'` to the right. */
|
|
35
|
+
pinned?: boolean | 'left' | 'right';
|
|
22
36
|
/** Allow inline editing (double-click or Enter on the focused cell). */
|
|
23
37
|
editable?: boolean;
|
|
24
38
|
/** Validate an edited value before it commits (inline edit or paste). Return
|
|
25
39
|
false to reject and keep the old value. Receives the coerced value. */
|
|
26
40
|
validate?: (value: string | number, row: GridRow) => boolean;
|
|
41
|
+
/** Editable choices: when set, editing renders a `<select>` of these options
|
|
42
|
+
instead of a text input (enum/status columns). */
|
|
43
|
+
options?: string[];
|
|
44
|
+
/** Set a native `title` tooltip on each cell (the full formatted value) — handy
|
|
45
|
+
when content truncates. */
|
|
46
|
+
tooltip?: boolean;
|
|
47
|
+
/** Custom display formatter, overriding the built-in type formatter. Applies
|
|
48
|
+
to display, tooltip, copy and (formatted) export. `row` is absent for
|
|
49
|
+
aggregate cells. */
|
|
50
|
+
format?: (value: unknown, row?: GridRow) => string;
|
|
27
51
|
/** Set false to disable drag-to-resize on this column (default on). */
|
|
28
52
|
resizable?: boolean;
|
|
29
53
|
/** Parent header label. Consecutive columns sharing a `group` render under a
|
|
30
54
|
spanning header. Best with fixed-width columns. */
|
|
31
55
|
group?: string;
|
|
56
|
+
/** Header filter-menu control for this column (requires `filterMenu` on
|
|
57
|
+
<Grid>). Defaults to the column's type; `'set'` shows a value checklist;
|
|
58
|
+
`false` disables filtering for this column. */
|
|
59
|
+
filter?: false | FilterKind;
|
|
32
60
|
}
|
|
33
61
|
export interface CellEditEvent {
|
|
34
62
|
row: GridRow;
|
|
@@ -78,15 +106,15 @@ export interface GridRow {
|
|
|
78
106
|
flashDir: 'up' | 'down';
|
|
79
107
|
[field: string]: unknown;
|
|
80
108
|
}
|
|
81
|
-
export declare function formatCell(col: ColumnDef, value: unknown): string;
|
|
109
|
+
export declare function formatCell(col: ColumnDef, value: unknown, row?: GridRow): string;
|
|
82
110
|
export declare function isSortable(col: ColumnDef): boolean;
|
|
83
111
|
export declare function isEditable(col: ColumnDef): boolean;
|
|
84
|
-
export declare function compareRows(a: GridRow, b: GridRow, sort: SortState): number;
|
|
112
|
+
export declare function compareRows(a: GridRow, b: GridRow, sort: SortState, col?: ColumnDef): number;
|
|
85
113
|
/**
|
|
86
114
|
* Multi-key comparison: apply each sort in order, returning the first that
|
|
87
115
|
* separates the rows. An empty list leaves rows in their original order.
|
|
88
116
|
*/
|
|
89
|
-
export declare function compareBySorts(a: GridRow, b: GridRow, sorts: readonly SortState[]): number;
|
|
117
|
+
export declare function compareBySorts(a: GridRow, b: GridRow, sorts: readonly SortState[], colOf?: (key: string) => ColumnDef | undefined): number;
|
|
90
118
|
export declare function colStyle(col: ColumnDef): string;
|
|
91
119
|
/** Concrete pixel width for a column (flex columns get a sensible default). */
|
|
92
120
|
export declare function colWidth(col: ColumnDef): number;
|
package/dist/grid/column.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { fmtPrice, fmtPercent, fmtVolume, fmtDate } from '../format/format';
|
|
2
|
-
export function formatCell(col, value) {
|
|
2
|
+
export function formatCell(col, value, row) {
|
|
3
|
+
if (col.format)
|
|
4
|
+
return col.format(value, row);
|
|
3
5
|
const n = typeof value === 'number' ? value : Number(value);
|
|
4
6
|
switch (col.type) {
|
|
5
7
|
case 'price':
|
|
@@ -22,24 +24,24 @@ export function isSortable(col) {
|
|
|
22
24
|
return col.type !== 'sparkline' && col.sortable !== false;
|
|
23
25
|
}
|
|
24
26
|
export function isEditable(col) {
|
|
25
|
-
return !!col.editable && col.type !== 'sparkline' && col.type !== '
|
|
27
|
+
return !!col.editable && col.type !== 'sparkline' && col.type !== 'custom';
|
|
26
28
|
}
|
|
27
29
|
function rawCompare(a, b) {
|
|
28
30
|
if (typeof a === 'number' && typeof b === 'number')
|
|
29
31
|
return a - b;
|
|
30
32
|
return String(a ?? '').localeCompare(String(b ?? ''));
|
|
31
33
|
}
|
|
32
|
-
export function compareRows(a, b, sort) {
|
|
33
|
-
const d = rawCompare(a[sort.key], b[sort.key]);
|
|
34
|
+
export function compareRows(a, b, sort, col) {
|
|
35
|
+
const d = col?.compare ? col.compare(a[sort.key], b[sort.key]) : rawCompare(a[sort.key], b[sort.key]);
|
|
34
36
|
return sort.dir === 'asc' ? d : -d;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* Multi-key comparison: apply each sort in order, returning the first that
|
|
38
40
|
* separates the rows. An empty list leaves rows in their original order.
|
|
39
41
|
*/
|
|
40
|
-
export function compareBySorts(a, b, sorts) {
|
|
42
|
+
export function compareBySorts(a, b, sorts, colOf) {
|
|
41
43
|
for (const sort of sorts) {
|
|
42
|
-
const d = compareRows(a, b, sort);
|
|
44
|
+
const d = compareRows(a, b, sort, colOf?.(sort.key));
|
|
43
45
|
if (d !== 0)
|
|
44
46
|
return d;
|
|
45
47
|
}
|
package/dist/grid/export.js
CHANGED
|
@@ -19,7 +19,7 @@ export function rowsToMatrix(rows, columns, opts = {}) {
|
|
|
19
19
|
if (opts.header !== false)
|
|
20
20
|
matrix.push(cols.map((c) => c.header));
|
|
21
21
|
for (const row of rows) {
|
|
22
|
-
matrix.push(cols.map((c) => (opts.formatted ? formatCell(c, row[c.key]) : rawValue(c, row[c.key]))));
|
|
22
|
+
matrix.push(cols.map((c) => (opts.formatted ? formatCell(c, row[c.key], row) : rawValue(c, row[c.key]))));
|
|
23
23
|
}
|
|
24
24
|
return matrix;
|
|
25
25
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured per-column filter model (v0.3). Pure and dependency-free so it can
|
|
3
|
+
* be unit-tested and reused by both the in-memory `view` and a server source.
|
|
4
|
+
* The header filter-menu UI (lazy-loaded) writes these; `passesFilters` applies
|
|
5
|
+
* them. Filtering is a snapshot operation, not a per-frame one.
|
|
6
|
+
*/
|
|
7
|
+
import type { ColumnDef, GridRow } from './column';
|
|
8
|
+
export type FilterKind = 'text' | 'number' | 'date' | 'set';
|
|
9
|
+
export type TextOp = 'contains' | 'notContains' | 'equals' | 'starts' | 'ends';
|
|
10
|
+
export type NumberOp = 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' | 'between';
|
|
11
|
+
export type DateOp = 'before' | 'after' | 'on' | 'between';
|
|
12
|
+
export type ColumnFilter = {
|
|
13
|
+
kind: 'text';
|
|
14
|
+
op: TextOp;
|
|
15
|
+
q: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'number';
|
|
18
|
+
op: NumberOp;
|
|
19
|
+
a: number;
|
|
20
|
+
b?: number;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'date';
|
|
23
|
+
op: DateOp;
|
|
24
|
+
a: number;
|
|
25
|
+
b?: number;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'set';
|
|
28
|
+
excluded: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Pick the default filter control for a column from its type. */
|
|
31
|
+
export declare function defaultFilterKind(col: ColumnDef): FilterKind;
|
|
32
|
+
/** A fresh, inactive filter of the given kind (the menu's starting state). */
|
|
33
|
+
export declare function emptyFilter(kind: FilterKind): ColumnFilter;
|
|
34
|
+
/** Whether a filter actually constrains anything (else it's a no-op). */
|
|
35
|
+
export declare function isFilterActive(f: ColumnFilter | undefined | null): boolean;
|
|
36
|
+
/** Does one cell value satisfy one filter? An inactive filter passes everything. */
|
|
37
|
+
export declare function matchesFilter(value: unknown, f: ColumnFilter): boolean;
|
|
38
|
+
/** AND across every active per-column filter. */
|
|
39
|
+
export declare function passesFilters(row: GridRow, filters: Record<string, ColumnFilter>): boolean;
|
|
40
|
+
/** Sorted unique string values for a column — the set-filter checklist. */
|
|
41
|
+
export declare function distinctValues(rows: readonly GridRow[], key: string): string[];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isNumeric } from './column';
|
|
2
|
+
const DAY = 86_400_000;
|
|
3
|
+
/** Pick the default filter control for a column from its type. */
|
|
4
|
+
export function defaultFilterKind(col) {
|
|
5
|
+
if (col.type === 'date')
|
|
6
|
+
return 'date';
|
|
7
|
+
if (isNumeric(col))
|
|
8
|
+
return 'number';
|
|
9
|
+
return 'text';
|
|
10
|
+
}
|
|
11
|
+
/** A fresh, inactive filter of the given kind (the menu's starting state). */
|
|
12
|
+
export function emptyFilter(kind) {
|
|
13
|
+
switch (kind) {
|
|
14
|
+
case 'number':
|
|
15
|
+
return { kind: 'number', op: 'eq', a: NaN };
|
|
16
|
+
case 'date':
|
|
17
|
+
return { kind: 'date', op: 'on', a: NaN };
|
|
18
|
+
case 'set':
|
|
19
|
+
return { kind: 'set', excluded: [] };
|
|
20
|
+
default:
|
|
21
|
+
return { kind: 'text', op: 'contains', q: '' };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Whether a filter actually constrains anything (else it's a no-op). */
|
|
25
|
+
export function isFilterActive(f) {
|
|
26
|
+
if (!f)
|
|
27
|
+
return false;
|
|
28
|
+
switch (f.kind) {
|
|
29
|
+
case 'text':
|
|
30
|
+
return f.q.trim().length > 0;
|
|
31
|
+
case 'number':
|
|
32
|
+
case 'date':
|
|
33
|
+
return Number.isFinite(f.a) && (f.op !== 'between' || Number.isFinite(f.b));
|
|
34
|
+
case 'set':
|
|
35
|
+
return f.excluded.length > 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Does one cell value satisfy one filter? An inactive filter passes everything. */
|
|
39
|
+
export function matchesFilter(value, f) {
|
|
40
|
+
if (!isFilterActive(f))
|
|
41
|
+
return true;
|
|
42
|
+
switch (f.kind) {
|
|
43
|
+
case 'text': {
|
|
44
|
+
const hay = String(value ?? '').toLowerCase();
|
|
45
|
+
const q = f.q.trim().toLowerCase();
|
|
46
|
+
if (f.op === 'contains')
|
|
47
|
+
return hay.includes(q);
|
|
48
|
+
if (f.op === 'notContains')
|
|
49
|
+
return !hay.includes(q);
|
|
50
|
+
if (f.op === 'equals')
|
|
51
|
+
return hay === q;
|
|
52
|
+
if (f.op === 'starts')
|
|
53
|
+
return hay.startsWith(q);
|
|
54
|
+
return hay.endsWith(q); // 'ends'
|
|
55
|
+
}
|
|
56
|
+
case 'set':
|
|
57
|
+
return !f.excluded.includes(String(value ?? ''));
|
|
58
|
+
case 'number':
|
|
59
|
+
case 'date': {
|
|
60
|
+
// Empty/blank cells aren't numbers (Number(null)/Number('') coerce to 0),
|
|
61
|
+
// so exclude them explicitly while a number/date filter is active.
|
|
62
|
+
if (value === null || value === undefined || value === '')
|
|
63
|
+
return false;
|
|
64
|
+
const n = Number(value);
|
|
65
|
+
if (!Number.isFinite(n))
|
|
66
|
+
return false; // non-numeric is excluded while active
|
|
67
|
+
if (f.kind === 'date' && f.op === 'on') {
|
|
68
|
+
return Math.floor(n / DAY) === Math.floor(f.a / DAY); // same (UTC) day
|
|
69
|
+
}
|
|
70
|
+
switch (f.op) {
|
|
71
|
+
case 'eq':
|
|
72
|
+
return n === f.a;
|
|
73
|
+
case 'ne':
|
|
74
|
+
return n !== f.a;
|
|
75
|
+
case 'lt':
|
|
76
|
+
case 'before':
|
|
77
|
+
return n < f.a;
|
|
78
|
+
case 'le':
|
|
79
|
+
return n <= f.a;
|
|
80
|
+
case 'gt':
|
|
81
|
+
case 'after':
|
|
82
|
+
return n > f.a;
|
|
83
|
+
case 'ge':
|
|
84
|
+
return n >= f.a;
|
|
85
|
+
case 'between':
|
|
86
|
+
return n >= f.a && n <= (f.b ?? Infinity);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true; // unreachable fallback
|
|
91
|
+
}
|
|
92
|
+
/** AND across every active per-column filter. */
|
|
93
|
+
export function passesFilters(row, filters) {
|
|
94
|
+
for (const key in filters) {
|
|
95
|
+
const f = filters[key];
|
|
96
|
+
if (isFilterActive(f) && !matchesFilter(row[key], f))
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/** Sorted unique string values for a column — the set-filter checklist. */
|
|
102
|
+
export function distinctValues(rows, key) {
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
for (const row of rows)
|
|
105
|
+
seen.add(String(row[key] ?? ''));
|
|
106
|
+
return [...seen].sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
|
107
|
+
}
|