@svgrid/grid 3.0.4 → 4.0.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 +3 -3
- package/dist/SvGrid.controller.svelte.d.ts +12 -2
- package/dist/SvGrid.controller.svelte.js +191 -8
- package/dist/SvGrid.css +73 -0
- package/dist/SvGrid.svelte +82 -3
- package/dist/SvGrid.types.d.ts +105 -3
- package/dist/build-api.js +11 -6
- package/dist/cdn/{GridMenus-7kbpnnBW.js → GridMenus-CLI93jke.js} +26 -26
- package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-CwooL3QA.js} +26 -26
- package/dist/cdn/server-block-cache-CGoWz-87.js +237 -0
- package/dist/cdn/{src-BKhZ6eXd.js → src-B6HXdfHY.js} +4099 -4140
- package/dist/cdn/{src-V1uu8iE9.js → src-C6Iowdvs.js} +4052 -4093
- package/dist/cdn/svgrid.js +27 -26
- package/dist/cdn/svgrid.svelte-external.js +27 -26
- package/dist/cdn/validate-CchXzyrX.js +76 -0
- package/dist/core.js +36 -13
- package/dist/editing.js +10 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +2 -0
- package/dist/grid-messages.js +2 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +2 -3
- package/dist/row-model.d.ts +133 -0
- package/dist/row-model.js +16 -0
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +30 -2
- package/dist/server-block-cache.d.ts +214 -0
- package/dist/server-block-cache.js +531 -0
- package/dist/server-data-source.d.ts +240 -6
- package/dist/server-data-source.js +220 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +8 -3
- package/package.json +6 -1
- package/src/SvGrid.controller.svelte.ts +190 -8
- package/src/SvGrid.css +73 -0
- package/src/SvGrid.svelte +82 -3
- package/src/SvGrid.types.ts +105 -3
- package/src/build-api.ts +11 -4
- package/src/core.rowmodel-cache.test.ts +51 -0
- package/src/core.ts +33 -12
- package/src/editing.ts +8 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.ts +5 -0
- package/src/icon-seam.test.ts +2 -4
- package/src/index.ts +19 -10
- package/src/row-model.ts +146 -0
- package/src/selection.test.ts +3 -0
- package/src/selection.ts +32 -2
- package/src/server-block-cache.test.ts +645 -0
- package/src/server-block-cache.ts +677 -0
- package/src/server-data-source.infinite.test.ts +343 -0
- package/src/server-data-source.ts +450 -27
- package/src/server.ts +47 -0
- package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
- package/src/validate.test.ts +10 -0
- package/src/validate.ts +10 -3
- package/dist/SvGroupCell.svelte +0 -141
- package/dist/SvGroupCell.svelte.d.ts +0 -49
- package/dist/SvRowGroupPanel.svelte +0 -186
- package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
- package/dist/cdn/validate-_CDJzgIo.js +0 -75
- package/dist/server-group-model.d.ts +0 -98
- package/dist/server-group-model.js +0 -263
- package/src/SvGroupCell.svelte +0 -141
- package/src/SvRowGroupPanel.svelte +0 -186
- package/src/server-group-model.test.ts +0 -294
- package/src/server-group-model.ts +0 -370
package/src/validate.test.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { describe, expect, it } from 'vitest'
|
|
7
7
|
import { validateGridConfig } from './validate'
|
|
8
|
+
import { createRowPlaceholder } from './server-block-cache'
|
|
8
9
|
|
|
9
10
|
type Row = { name: string; amount: number }
|
|
10
11
|
const data: Row[] = [
|
|
@@ -48,6 +49,15 @@ describe('unknown field', () => {
|
|
|
48
49
|
expect(run({ columns: [{ id: 'actions', header: '' }] })).toEqual([])
|
|
49
50
|
})
|
|
50
51
|
|
|
52
|
+
it('looks past placeholder rows, which a row model shows before its data lands', () => {
|
|
53
|
+
const loading = [createRowPlaceholder('loading', { index: 0 }), createRowPlaceholder('loading', { index: 1 })]
|
|
54
|
+
// Only placeholders: nothing to judge the columns by, so no warning.
|
|
55
|
+
expect(run({ data: loading })).toEqual([])
|
|
56
|
+
// Placeholders ahead of real rows: the real rows are the sample.
|
|
57
|
+
expect(run({ data: [...loading, ...data] })).toEqual([])
|
|
58
|
+
expect(run({ data: [...loading, ...data], columns: [{ field: 'naem' }] })).toHaveLength(1)
|
|
59
|
+
})
|
|
60
|
+
|
|
51
61
|
it('looks through group columns', () => {
|
|
52
62
|
const msgs = run({ columns: [{ header: 'Group', columns: [{ field: 'nope' }] }] })
|
|
53
63
|
expect(msgs[0]).toContain('"nope" does not exist')
|
package/src/validate.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* changes, and must not scale with row count.
|
|
21
21
|
*/
|
|
22
22
|
import type { ColumnDef, RowData, TableFeatures } from './core'
|
|
23
|
+
import { rowPlaceholderState } from './server-block-cache'
|
|
23
24
|
|
|
24
25
|
/** How many rows to sample when deciding whether a `field` exists. */
|
|
25
26
|
const FIELD_SAMPLE_ROWS = 10
|
|
@@ -37,6 +38,8 @@ export type ValidateInput<TFeatures extends TableFeatures, TData extends RowData
|
|
|
37
38
|
pageSize?: number
|
|
38
39
|
groupBy?: ReadonlyArray<string>
|
|
39
40
|
treeData?: { parentField?: string; idField?: string; column?: string }
|
|
41
|
+
/** Server-side grouping: group rows carry their key and aggregates, not the leaf fields. */
|
|
42
|
+
serverGroup?: unknown
|
|
40
43
|
initialColumnPinning?: { left?: ReadonlyArray<string>; right?: ReadonlyArray<string> }
|
|
41
44
|
columnVirtualization?: boolean
|
|
42
45
|
externalPagination?: boolean
|
|
@@ -69,9 +72,13 @@ export function validateGridConfig<
|
|
|
69
72
|
// ---- 1. A `field` that does not exist on the data -------------------------
|
|
70
73
|
// The single most expensive silent failure: the column renders, and every
|
|
71
74
|
// cell in it is blank. Sampled across several rows so genuinely sparse data
|
|
72
|
-
// (a key absent from row 0 but present later) does not trip it.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
// (a key absent from row 0 but present later) does not trip it. A row
|
|
76
|
+
// model shows placeholder rows before its first block lands; those carry
|
|
77
|
+
// no data keys at all and say nothing about the columns.
|
|
78
|
+
const sample = (input.data ?? []).filter((row) => !rowPlaceholderState(row)).slice(0, FIELD_SAMPLE_ROWS)
|
|
79
|
+
// Under server-side grouping the first rows are group rows, which carry
|
|
80
|
+
// a key and aggregates and none of the leaf fields; nothing to judge by.
|
|
81
|
+
if (sample.length && !input.serverGroup) {
|
|
75
82
|
const known = new Set<string>()
|
|
76
83
|
for (const row of sample) {
|
|
77
84
|
if (row && typeof row === 'object') for (const k of Object.keys(row)) known.add(k)
|
package/dist/SvGroupCell.svelte
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Built-in group cell for the server-side group/tree model. Drop it into the
|
|
3
|
-
grouping column and it draws the expander + indentation for group rows (and an
|
|
4
|
-
indented leaf value) - no hand-written cell recipe. Pair with `serverGroupRows`
|
|
5
|
-
to feed `<SvGrid data>`, and wire `onToggle` to the controller's `toggleGroup`.
|
|
6
|
-
|
|
7
|
-
cell: (ctx) => renderComponent(SvGroupCell, {
|
|
8
|
-
row: ctx.row.original, onToggle: ctl.toggleGroup, leafField: 'name',
|
|
9
|
-
})
|
|
10
|
-
-->
|
|
11
|
-
<script lang="ts" generics="TData">
|
|
12
|
-
import type { ServerDisplayRow } from './server-data-source'
|
|
13
|
-
import type { GridIcons } from './grid-icons'
|
|
14
|
-
|
|
15
|
-
type GroupGridRow = { __group?: ServerDisplayRow<TData> } & Record<string, unknown>
|
|
16
|
-
type Props = {
|
|
17
|
-
/** The grid row from `serverGroupRows` (carries the `__group` marker). */
|
|
18
|
-
row: GroupGridRow
|
|
19
|
-
/**
|
|
20
|
-
* Expand / collapse a group, or load more children. Receives the grid row;
|
|
21
|
-
* wire to `serverGroupNav(ctl).onToggle` (which drives clicks and keyboard).
|
|
22
|
-
*/
|
|
23
|
-
onToggle: (row: GroupGridRow) => void
|
|
24
|
-
/** Field to show for leaf rows in the group column (blank if omitted). */
|
|
25
|
-
leafField?: string
|
|
26
|
-
/** Pixels of indentation per level. Default 18. */
|
|
27
|
-
indent?: number
|
|
28
|
-
/**
|
|
29
|
-
* Icon overrides, the same map `<SvGrid icons>` takes. Only
|
|
30
|
-
* `chevron-right` is drawn here. You mount this component yourself, so it
|
|
31
|
-
* cannot inherit the grid's icons - pass the same object to both to keep
|
|
32
|
-
* the expander consistent with the rest of the chrome.
|
|
33
|
-
*/
|
|
34
|
-
icons?: GridIcons
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let { row, onToggle, leafField, indent = 18, icons }: Props = $props()
|
|
38
|
-
const chevron = $derived(icons?.['chevron-right'])
|
|
39
|
-
const meta = $derived(row.__group)
|
|
40
|
-
</script>
|
|
41
|
-
|
|
42
|
-
{#if meta && meta.kind === 'group'}
|
|
43
|
-
<button
|
|
44
|
-
type="button"
|
|
45
|
-
class="sv-group-cell"
|
|
46
|
-
style={`padding-inline-start: ${meta.level * indent}px`}
|
|
47
|
-
aria-expanded={meta.expanded}
|
|
48
|
-
aria-label={meta.expanded ? 'Collapse group' : 'Expand group'}
|
|
49
|
-
onclick={() => onToggle(row)}
|
|
50
|
-
>
|
|
51
|
-
<!-- Same chevron the grid's own expanders and SvTree draw, so every
|
|
52
|
-
expander in the library looks identical. -->
|
|
53
|
-
<span class="sv-group-chev" class:open={meta.expanded} aria-hidden="true">
|
|
54
|
-
{#if chevron}
|
|
55
|
-
{@render chevron()}
|
|
56
|
-
{:else}
|
|
57
|
-
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
|
|
58
|
-
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
|
|
59
|
-
{/if}
|
|
60
|
-
</span>
|
|
61
|
-
<span class="sv-group-key">{meta.key}</span>
|
|
62
|
-
{#if meta.loading}<span class="sv-group-load" aria-hidden="true">…</span>{/if}
|
|
63
|
-
</button>
|
|
64
|
-
{:else if meta && meta.kind === 'more'}
|
|
65
|
-
<button
|
|
66
|
-
type="button"
|
|
67
|
-
class="sv-group-more"
|
|
68
|
-
style={`padding-inline-start: ${meta.level * indent}px`}
|
|
69
|
-
disabled={meta.loading}
|
|
70
|
-
onclick={() => onToggle(row)}
|
|
71
|
-
>
|
|
72
|
-
{meta.loading ? 'Loading...' : `Load ${meta.remaining} more`}
|
|
73
|
-
</button>
|
|
74
|
-
{:else if meta && meta.kind === 'footer'}
|
|
75
|
-
<span class="sv-group-footer" style={`padding-inline-start: ${meta.level * indent}px`}>Total</span>
|
|
76
|
-
{:else if meta && meta.kind === 'skeleton'}
|
|
77
|
-
<span class="sv-group-skeleton" style={`margin-inline-start: ${meta.level * indent}px`} aria-hidden="true"></span>
|
|
78
|
-
{:else if meta}
|
|
79
|
-
<span class="sv-group-leaf" style={`padding-inline-start: ${(meta.level + 1) * indent}px`}>
|
|
80
|
-
{leafField ? (row[leafField] ?? '') : ''}
|
|
81
|
-
</span>
|
|
82
|
-
{/if}
|
|
83
|
-
|
|
84
|
-
<style>
|
|
85
|
-
.sv-group-cell {
|
|
86
|
-
display: inline-flex;
|
|
87
|
-
align-items: center;
|
|
88
|
-
gap: 6px;
|
|
89
|
-
background: none;
|
|
90
|
-
border: 0;
|
|
91
|
-
padding: 0;
|
|
92
|
-
font: inherit;
|
|
93
|
-
color: inherit;
|
|
94
|
-
cursor: pointer;
|
|
95
|
-
}
|
|
96
|
-
.sv-group-chev {
|
|
97
|
-
display: inline-flex;
|
|
98
|
-
align-items: center;
|
|
99
|
-
justify-content: center;
|
|
100
|
-
width: 12px;
|
|
101
|
-
height: 12px;
|
|
102
|
-
transition: transform 0.15s;
|
|
103
|
-
color: var(--sg-muted, #64748b);
|
|
104
|
-
}
|
|
105
|
-
/* An `icons` override renders inside this wrapper, and this style block is
|
|
106
|
-
scoped, so its markup needs :global() to be reached. Sizing it here means
|
|
107
|
-
a custom chevron lands in the same box as the built-in and rotates with
|
|
108
|
-
the wrapper rather than needing to know the rule exists. */
|
|
109
|
-
.sv-group-chev :global(svg),
|
|
110
|
-
.sv-group-chev :global(img) {
|
|
111
|
-
width: 100%;
|
|
112
|
-
height: 100%;
|
|
113
|
-
display: block;
|
|
114
|
-
}
|
|
115
|
-
.sv-group-chev.open { transform: rotate(90deg); }
|
|
116
|
-
.sv-group-key { font-weight: 600; }
|
|
117
|
-
.sv-group-load { color: var(--sg-muted, #64748b); font-size: 12px; }
|
|
118
|
-
.sv-group-leaf { color: var(--sg-muted, #475569); }
|
|
119
|
-
.sv-group-more {
|
|
120
|
-
background: none;
|
|
121
|
-
border: 0;
|
|
122
|
-
padding: 0;
|
|
123
|
-
font: inherit;
|
|
124
|
-
font-size: 13px;
|
|
125
|
-
color: var(--sg-accent, #2563eb);
|
|
126
|
-
cursor: pointer;
|
|
127
|
-
}
|
|
128
|
-
.sv-group-more:disabled { color: var(--sg-muted, #64748b); cursor: default; }
|
|
129
|
-
.sv-group-footer { font-weight: 600; color: var(--sg-muted, #64748b); font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em; }
|
|
130
|
-
.sv-group-skeleton {
|
|
131
|
-
display: inline-block;
|
|
132
|
-
width: 60%;
|
|
133
|
-
height: 10px;
|
|
134
|
-
border-radius: 5px;
|
|
135
|
-
background: linear-gradient(90deg, var(--sg-border, #e2e8f0) 25%, var(--sg-header-bg, #f1f5f9) 50%, var(--sg-border, #e2e8f0) 75%);
|
|
136
|
-
background-size: 200% 100%;
|
|
137
|
-
animation: sv-group-shimmer 1.2s infinite;
|
|
138
|
-
}
|
|
139
|
-
@keyframes sv-group-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
|
140
|
-
@media (prefers-reduced-motion: reduce) { .sv-group-skeleton { animation: none; } }
|
|
141
|
-
</style>
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { ServerDisplayRow } from './server-data-source.js';
|
|
2
|
-
import type { GridIcons } from './grid-icons.js';
|
|
3
|
-
declare function $$render<TData>(): {
|
|
4
|
-
props: {
|
|
5
|
-
/** The grid row from `serverGroupRows` (carries the `__group` marker). */
|
|
6
|
-
row: {
|
|
7
|
-
__group?: ServerDisplayRow<TData>;
|
|
8
|
-
} & Record<string, unknown>;
|
|
9
|
-
/**
|
|
10
|
-
* Expand / collapse a group, or load more children. Receives the grid row;
|
|
11
|
-
* wire to `serverGroupNav(ctl).onToggle` (which drives clicks and keyboard).
|
|
12
|
-
*/
|
|
13
|
-
onToggle: (row: {
|
|
14
|
-
__group?: ServerDisplayRow<TData>;
|
|
15
|
-
} & Record<string, unknown>) => void;
|
|
16
|
-
/** Field to show for leaf rows in the group column (blank if omitted). */
|
|
17
|
-
leafField?: string;
|
|
18
|
-
/** Pixels of indentation per level. Default 18. */
|
|
19
|
-
indent?: number;
|
|
20
|
-
/**
|
|
21
|
-
* Icon overrides, the same map `<SvGrid icons>` takes. Only
|
|
22
|
-
* `chevron-right` is drawn here. You mount this component yourself, so it
|
|
23
|
-
* cannot inherit the grid's icons - pass the same object to both to keep
|
|
24
|
-
* the expander consistent with the rest of the chrome.
|
|
25
|
-
*/
|
|
26
|
-
icons?: GridIcons;
|
|
27
|
-
};
|
|
28
|
-
exports: {};
|
|
29
|
-
bindings: "";
|
|
30
|
-
slots: {};
|
|
31
|
-
events: {};
|
|
32
|
-
};
|
|
33
|
-
declare class __sveltets_Render<TData> {
|
|
34
|
-
props(): ReturnType<typeof $$render<TData>>['props'];
|
|
35
|
-
events(): ReturnType<typeof $$render<TData>>['events'];
|
|
36
|
-
slots(): ReturnType<typeof $$render<TData>>['slots'];
|
|
37
|
-
bindings(): "";
|
|
38
|
-
exports(): {};
|
|
39
|
-
}
|
|
40
|
-
interface $$IsomorphicComponent {
|
|
41
|
-
new <TData>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TData>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TData>['props']>, ReturnType<__sveltets_Render<TData>['events']>, ReturnType<__sveltets_Render<TData>['slots']>> & {
|
|
42
|
-
$$bindings?: ReturnType<__sveltets_Render<TData>['bindings']>;
|
|
43
|
-
} & ReturnType<__sveltets_Render<TData>['exports']>;
|
|
44
|
-
<TData>(internal: unknown, props: ReturnType<__sveltets_Render<TData>['props']> & {}): ReturnType<__sveltets_Render<TData>['exports']>;
|
|
45
|
-
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
46
|
-
}
|
|
47
|
-
declare const SvGroupCell: $$IsomorphicComponent;
|
|
48
|
-
type SvGroupCell<TData> = InstanceType<typeof SvGroupCell<TData>>;
|
|
49
|
-
export default SvGroupCell;
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
A row-group panel (like the "drag columns to group" bar in enterprise grids).
|
|
3
|
-
Shows the current group-by columns as chips you can remove or drag to reorder,
|
|
4
|
-
plus a "+ Group by" menu to add one. It is also a drop target: a column drag
|
|
5
|
-
that carries `text/sv-column` (its column id) adds that column. Wire `onChange`
|
|
6
|
-
to a server group model's `setGroupBy`, or to client grouping.
|
|
7
|
-
|
|
8
|
-
<SvRowGroupPanel
|
|
9
|
-
columns={[{ id: 'region', label: 'Region' }, { id: 'country', label: 'Country' }]}
|
|
10
|
-
groupBy={view.groupBy}
|
|
11
|
-
onChange={(g) => ctl.setGroupBy(g)}
|
|
12
|
-
/>
|
|
13
|
-
-->
|
|
14
|
-
<script lang="ts">
|
|
15
|
-
import { GRID_ICON_GLYPHS, type GridIconName, type GridIcons } from './grid-icons'
|
|
16
|
-
|
|
17
|
-
type Col = { id: string; label: string }
|
|
18
|
-
type Props = {
|
|
19
|
-
/** Columns that can be grouped on. */
|
|
20
|
-
columns: Col[]
|
|
21
|
-
/** Current group-by column ids, outer to inner. */
|
|
22
|
-
groupBy: string[]
|
|
23
|
-
/** Called with the new group-by order. */
|
|
24
|
-
onChange: (groupBy: string[]) => void
|
|
25
|
-
/** Text shown when nothing is grouped. */
|
|
26
|
-
placeholder?: string
|
|
27
|
-
/**
|
|
28
|
-
* Icon overrides, the same map `<SvGrid icons>` takes. This panel draws
|
|
29
|
-
* `drag-handle`, `remove` and `breadcrumb-separator`. You mount it
|
|
30
|
-
* yourself, so pass the same object you gave the grid to keep them
|
|
31
|
-
* consistent.
|
|
32
|
-
*/
|
|
33
|
-
icons?: GridIcons
|
|
34
|
-
}
|
|
35
|
-
let { columns, groupBy, onChange, placeholder = 'Drag a column here to group by it', icons }: Props = $props()
|
|
36
|
-
|
|
37
|
-
const labelOf = (id: string) => columns.find((c) => c.id === id)?.label ?? id
|
|
38
|
-
const available = $derived(columns.filter((c) => !groupBy.includes(c.id)))
|
|
39
|
-
|
|
40
|
-
let dragIndex = $state<number | null>(null)
|
|
41
|
-
|
|
42
|
-
function remove(id: string) {
|
|
43
|
-
onChange(groupBy.filter((g) => g !== id))
|
|
44
|
-
}
|
|
45
|
-
function add(id: string) {
|
|
46
|
-
if (id && !groupBy.includes(id)) onChange([...groupBy, id])
|
|
47
|
-
}
|
|
48
|
-
function reorder(to: number) {
|
|
49
|
-
if (dragIndex === null || dragIndex === to) return
|
|
50
|
-
const next = [...groupBy]
|
|
51
|
-
const [moved] = next.splice(dragIndex, 1)
|
|
52
|
-
next.splice(to, 0, moved!)
|
|
53
|
-
dragIndex = null
|
|
54
|
-
onChange(next)
|
|
55
|
-
}
|
|
56
|
-
function onPanelDrop(e: DragEvent) {
|
|
57
|
-
const id = e.dataTransfer?.getData('text/sv-column')
|
|
58
|
-
if (id) add(id)
|
|
59
|
-
}
|
|
60
|
-
/** Keyboard equivalent of dragging a chip: moves the chip at `i` one slot earlier/later,
|
|
61
|
-
* reusing `reorder()` (via `dragIndex`) so there is a single source of truth for the move. */
|
|
62
|
-
function moveByKeyboard(i: number, delta: number) {
|
|
63
|
-
const to = i + delta
|
|
64
|
-
if (to < 0 || to >= groupBy.length) return
|
|
65
|
-
dragIndex = i
|
|
66
|
-
reorder(to)
|
|
67
|
-
}
|
|
68
|
-
function onChipKeydown(e: KeyboardEvent, i: number) {
|
|
69
|
-
if (!e.altKey) return
|
|
70
|
-
if (e.key === 'ArrowLeft') {
|
|
71
|
-
e.preventDefault()
|
|
72
|
-
moveByKeyboard(i, -1)
|
|
73
|
-
} else if (e.key === 'ArrowRight') {
|
|
74
|
-
e.preventDefault()
|
|
75
|
-
moveByKeyboard(i, 1)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
</script>
|
|
79
|
-
|
|
80
|
-
<!-- Same override-or-default resolution SvGrid uses, kept local because this
|
|
81
|
-
component is mounted by the consumer and never sees the grid's snippet. -->
|
|
82
|
-
{#snippet ic(name: GridIconName)}
|
|
83
|
-
{@const override = icons?.[name]}
|
|
84
|
-
{#if override}{@render override()}{:else}{GRID_ICON_GLYPHS[name]}{/if}
|
|
85
|
-
{/snippet}
|
|
86
|
-
|
|
87
|
-
<div
|
|
88
|
-
class="sv-rowgroup-panel"
|
|
89
|
-
role="group"
|
|
90
|
-
aria-label="Row groups"
|
|
91
|
-
ondragover={(e) => e.preventDefault()}
|
|
92
|
-
ondrop={onPanelDrop}
|
|
93
|
-
>
|
|
94
|
-
<span class="sv-rgp-label" aria-hidden="true">Group by:</span>
|
|
95
|
-
{#if groupBy.length === 0}
|
|
96
|
-
<span class="sv-rgp-empty">{placeholder}</span>
|
|
97
|
-
{:else}
|
|
98
|
-
{#each groupBy as id, i (id)}
|
|
99
|
-
{#if i > 0}<span class="sv-rgp-sep" aria-hidden="true">{@render ic('breadcrumb-separator')}</span>{/if}
|
|
100
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
101
|
-
<span
|
|
102
|
-
class="sv-rgp-chip"
|
|
103
|
-
class:sv-rgp-chip-drag={dragIndex === i}
|
|
104
|
-
role="group"
|
|
105
|
-
tabindex="0"
|
|
106
|
-
aria-label={`Grouped by ${labelOf(id)}, position ${i + 1} of ${groupBy.length}. Press Alt+Arrow keys to reorder.`}
|
|
107
|
-
draggable="true"
|
|
108
|
-
ondragstart={() => (dragIndex = i)}
|
|
109
|
-
ondragend={() => (dragIndex = null)}
|
|
110
|
-
ondragover={(e) => e.preventDefault()}
|
|
111
|
-
ondrop={() => reorder(i)}
|
|
112
|
-
onkeydown={(e) => onChipKeydown(e, i)}
|
|
113
|
-
>
|
|
114
|
-
<span class="sv-rgp-grip" aria-hidden="true">{@render ic('drag-handle')}</span>
|
|
115
|
-
{labelOf(id)}
|
|
116
|
-
<button type="button" class="sv-rgp-x" onclick={() => remove(id)} aria-label={`Stop grouping by ${labelOf(id)}`}>{@render ic('remove')}</button>
|
|
117
|
-
</span>
|
|
118
|
-
{/each}
|
|
119
|
-
{/if}
|
|
120
|
-
{#if available.length}
|
|
121
|
-
<select
|
|
122
|
-
class="sv-rgp-add"
|
|
123
|
-
aria-label="Add a group column"
|
|
124
|
-
onchange={(e) => { add(e.currentTarget.value); e.currentTarget.value = '' }}
|
|
125
|
-
>
|
|
126
|
-
<option value="">+ Group by</option>
|
|
127
|
-
{#each available as c (c.id)}<option value={c.id}>{c.label}</option>{/each}
|
|
128
|
-
</select>
|
|
129
|
-
{/if}
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<style>
|
|
133
|
-
.sv-rowgroup-panel {
|
|
134
|
-
display: flex;
|
|
135
|
-
align-items: center;
|
|
136
|
-
flex-wrap: wrap;
|
|
137
|
-
gap: 6px;
|
|
138
|
-
padding: 8px 10px;
|
|
139
|
-
border: 1px dashed var(--sg-border, #cbd5e1);
|
|
140
|
-
border-radius: 10px;
|
|
141
|
-
background: var(--sg-header-bg, #f8fafc);
|
|
142
|
-
font-size: 13px;
|
|
143
|
-
}
|
|
144
|
-
.sv-rgp-label { color: var(--sg-muted, #64748b); font-weight: 600; }
|
|
145
|
-
.sv-rgp-empty { color: var(--sg-muted, #94a3b8); font-style: italic; }
|
|
146
|
-
.sv-rgp-sep { color: var(--sg-muted, #94a3b8); }
|
|
147
|
-
.sv-rgp-chip {
|
|
148
|
-
display: inline-flex;
|
|
149
|
-
align-items: center;
|
|
150
|
-
gap: 6px;
|
|
151
|
-
padding: 3px 6px 3px 8px;
|
|
152
|
-
border-radius: 999px;
|
|
153
|
-
background: var(--sg-bg, #fff);
|
|
154
|
-
border: 1px solid var(--sg-border, #e2e8f0);
|
|
155
|
-
cursor: grab;
|
|
156
|
-
user-select: none;
|
|
157
|
-
font-weight: 600;
|
|
158
|
-
}
|
|
159
|
-
.sv-rgp-chip-drag { opacity: 0.5; }
|
|
160
|
-
.sv-rgp-grip { color: var(--sg-muted, #94a3b8); cursor: grab; }
|
|
161
|
-
.sv-rgp-x {
|
|
162
|
-
display: inline-flex;
|
|
163
|
-
align-items: center;
|
|
164
|
-
justify-content: center;
|
|
165
|
-
width: 16px;
|
|
166
|
-
height: 16px;
|
|
167
|
-
border: 0;
|
|
168
|
-
border-radius: 50%;
|
|
169
|
-
background: none;
|
|
170
|
-
color: var(--sg-muted, #64748b);
|
|
171
|
-
cursor: pointer;
|
|
172
|
-
font-size: 14px;
|
|
173
|
-
line-height: 1;
|
|
174
|
-
}
|
|
175
|
-
.sv-rgp-x:hover { background: var(--sg-header-bg, #f1f5f9); color: var(--sg-fg, #0f172a); }
|
|
176
|
-
.sv-rgp-add {
|
|
177
|
-
font: inherit;
|
|
178
|
-
font-size: 13px;
|
|
179
|
-
padding: 4px 8px;
|
|
180
|
-
border-radius: 8px;
|
|
181
|
-
border: 1px solid var(--sg-border, #e2e8f0);
|
|
182
|
-
background: var(--sg-bg, #fff);
|
|
183
|
-
color: var(--sg-accent, #2563eb);
|
|
184
|
-
cursor: pointer;
|
|
185
|
-
}
|
|
186
|
-
</style>
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { type GridIcons } from './grid-icons.js';
|
|
2
|
-
type Col = {
|
|
3
|
-
id: string;
|
|
4
|
-
label: string;
|
|
5
|
-
};
|
|
6
|
-
type Props = {
|
|
7
|
-
/** Columns that can be grouped on. */
|
|
8
|
-
columns: Col[];
|
|
9
|
-
/** Current group-by column ids, outer to inner. */
|
|
10
|
-
groupBy: string[];
|
|
11
|
-
/** Called with the new group-by order. */
|
|
12
|
-
onChange: (groupBy: string[]) => void;
|
|
13
|
-
/** Text shown when nothing is grouped. */
|
|
14
|
-
placeholder?: string;
|
|
15
|
-
/**
|
|
16
|
-
* Icon overrides, the same map `<SvGrid icons>` takes. This panel draws
|
|
17
|
-
* `drag-handle`, `remove` and `breadcrumb-separator`. You mount it
|
|
18
|
-
* yourself, so pass the same object you gave the grid to keep them
|
|
19
|
-
* consistent.
|
|
20
|
-
*/
|
|
21
|
-
icons?: GridIcons;
|
|
22
|
-
};
|
|
23
|
-
declare const SvRowGroupPanel: import("svelte").Component<Props, {}, "">;
|
|
24
|
-
type SvRowGroupPanel = ReturnType<typeof SvRowGroupPanel>;
|
|
25
|
-
export default SvRowGroupPanel;
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
//#region src/validate.ts
|
|
2
|
-
var e = "https://svgrid.com/docs/getting-started/3-data-and-columns/";
|
|
3
|
-
function t(e, n = []) {
|
|
4
|
-
for (let r of e) r.columns?.length ? t(r.columns, n) : n.push(r);
|
|
5
|
-
return n;
|
|
6
|
-
}
|
|
7
|
-
function n(n) {
|
|
8
|
-
let i = [], a = t(n.columns ?? []), o = (n.data ?? []).slice(0, 10);
|
|
9
|
-
if (o.length) {
|
|
10
|
-
let t = /* @__PURE__ */ new Set();
|
|
11
|
-
for (let e of o) if (e && typeof e == "object") for (let n of Object.keys(e)) t.add(n);
|
|
12
|
-
for (let n of a) {
|
|
13
|
-
let a = n.field;
|
|
14
|
-
if (!a || n.fieldFn || t.has(a)) continue;
|
|
15
|
-
let o = r(a, [...t]);
|
|
16
|
-
i.push(`[svgrid] Column field "${a}" does not exist on your row data, so that column renders empty.${o ? ` Did you mean "${o}"?` : ""} Available keys: ${[...t].slice(0, 12).join(", ")}. See ${e}`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
let s = /* @__PURE__ */ new Map();
|
|
20
|
-
for (let e of a) {
|
|
21
|
-
let t = e.id ?? e.field;
|
|
22
|
-
t && s.set(t, (s.get(t) ?? 0) + 1);
|
|
23
|
-
}
|
|
24
|
-
for (let [t, n] of s) n > 1 && i.push(`[svgrid] ${n} columns share the id "${t}". Column state (sorting, filtering, pinning, width) is keyed by id, so they will act as one. Give each an explicit \`id\`. See ${e}`);
|
|
25
|
-
let c = n.pageable === void 0 && n.showPagination === void 0;
|
|
26
|
-
n.pageSize !== void 0 && c && i.push("[svgrid] `pageSize` is set but pagination was never turned on, so it has no effect and every row renders. Add `pageable` to switch the pager on."), !(n.sortable === !0 || n.features?.rowSortingFeature) && a.some((e) => e.sortable === !0) && i.push("[svgrid] A column sets `sortable: true`, but sorting is not enabled on the grid, so its header does nothing. Add `sortable` to <SvGrid> (it registers the sorting feature for you).");
|
|
27
|
-
let l = /* @__PURE__ */ new Set();
|
|
28
|
-
for (let e of a) {
|
|
29
|
-
let t = e.id ?? e.field;
|
|
30
|
-
t && l.add(t);
|
|
31
|
-
}
|
|
32
|
-
for (let e of n.groupBy ?? []) {
|
|
33
|
-
if (l.has(e)) continue;
|
|
34
|
-
let t = r(e, [...l]);
|
|
35
|
-
i.push(`[svgrid] \`groupBy\` refers to column "${e}", which does not exist, so that grouping level is ignored.${t ? ` Did you mean "${t}"?` : ""} Column ids: ${[...l].slice(0, 12).join(", ")}`);
|
|
36
|
-
}
|
|
37
|
-
let u = n.treeData?.column;
|
|
38
|
-
if (u && !l.has(u) && i.push(`[svgrid] \`treeData.column\` refers to column "${u}", which does not exist, so the expander falls back to the first visible column.`), o.length && n.treeData) {
|
|
39
|
-
let e = /* @__PURE__ */ new Set();
|
|
40
|
-
for (let t of o) if (t && typeof t == "object") for (let n of Object.keys(t)) e.add(n);
|
|
41
|
-
let t = n.treeData.parentField;
|
|
42
|
-
if (t && !e.has(t)) {
|
|
43
|
-
let n = r(t, [...e]);
|
|
44
|
-
i.push(`[svgrid] \`treeData.parentField\` is "${t}", which is not on your row data, so every row becomes a root and no hierarchy appears.${n ? ` Did you mean "${n}"?` : ""}`);
|
|
45
|
-
}
|
|
46
|
-
let a = n.treeData.idField;
|
|
47
|
-
a && !e.has(a) && i.push(`[svgrid] \`treeData.idField\` is "${a}", which is not on your row data, so parent lookups cannot match and the tree stays flat.`);
|
|
48
|
-
}
|
|
49
|
-
return (n.initialColumnPinning?.left?.length ?? 0) + (n.initialColumnPinning?.right?.length ?? 0) > 0 && n.columnVirtualization !== !1 && i.push("[svgrid] `initialColumnPinning` is set while column virtualization is on (its default), so the pinned columns will not stick - the virtualizer recycles column nodes. Add `columnVirtualization={false}`."), n.externalPagination === !0 && n.rowCount === void 0 && i.push("[svgrid] `externalPagination` is on but `rowCount` is not set, so the pager cannot know how many pages exist. Pass the server total as `rowCount`."), n.externalSort === !0 && !n.onSortingChange && i.push("[svgrid] `externalSort` is on but there is no `onSortingChange` handler, so clicking a header changes nothing - the grid stopped sorting and nobody is listening. Add `onSortingChange` and re-fetch in your handler."), n.externalFilter === !0 && !n.onFiltersChange && i.push("[svgrid] `externalFilter` is on but there is no `onFiltersChange` handler, so filtering the grid changes nothing. Add `onFiltersChange` and re-fetch in your handler."), i;
|
|
50
|
-
}
|
|
51
|
-
function r(e, t) {
|
|
52
|
-
let n, r = Infinity;
|
|
53
|
-
for (let a of t) {
|
|
54
|
-
let t = i(e.toLowerCase(), a.toLowerCase());
|
|
55
|
-
t < r && (r = t, n = a);
|
|
56
|
-
}
|
|
57
|
-
let a = Math.min(3, Math.floor(e.length / 3) + 1);
|
|
58
|
-
return r <= a ? n : void 0;
|
|
59
|
-
}
|
|
60
|
-
function i(e, t) {
|
|
61
|
-
if (e === t) return 0;
|
|
62
|
-
let n = Array(t.length + 1);
|
|
63
|
-
for (let e = 0; e <= t.length; e++) n[e] = e;
|
|
64
|
-
for (let r = 1; r <= e.length; r++) {
|
|
65
|
-
let i = n[0];
|
|
66
|
-
n[0] = r;
|
|
67
|
-
for (let a = 1; a <= t.length; a++) {
|
|
68
|
-
let o = Math.min(n[a] + 1, n[a - 1] + 1, i + (e[r - 1] === t[a - 1] ? 0 : 1));
|
|
69
|
-
i = n[a], n[a] = o;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return n[t.length];
|
|
73
|
-
}
|
|
74
|
-
//#endregion
|
|
75
|
-
export { n as validateGridConfig };
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server-side GROUP row model (SSRM grouping) - the first-class
|
|
3
|
-
* companion to `createServerDataSource`. Grouping, aggregation, and lazy
|
|
4
|
-
* expansion all flow through the SAME `ServerDataSource.getRows` contract:
|
|
5
|
-
*
|
|
6
|
-
* - The request carries `groupBy` (the columns grouped on) and `groupKeys`
|
|
7
|
-
* (the path of the group being expanded). While `groupKeys.length <
|
|
8
|
-
* groupBy.length` the server returns GROUP rows (one per distinct key at
|
|
9
|
-
* that level, carrying the key + aggregate values); at the bottom level it
|
|
10
|
-
* returns the leaf rows under that path.
|
|
11
|
-
* - Expanding a group is just another `getRows` with `groupKeys` set to that
|
|
12
|
-
* group's path. This controller owns the group tree: expand / collapse,
|
|
13
|
-
* per-node lazy fetch + cache, race-safety, and flattening to a display
|
|
14
|
-
* list the grid renders (group rows with an expander, leaves underneath).
|
|
15
|
-
*
|
|
16
|
-
* Headless and framework-agnostic on purpose: render from `state.displayRows`
|
|
17
|
-
* and call `toggleGroup` from the group cell's expander.
|
|
18
|
-
*/
|
|
19
|
-
import type { ServerAggregation, ServerDataSource, ServerDisplayRow, ServerFilterModel, ServerGroupRow, ServerSortModel } from './server-data-source.js';
|
|
20
|
-
export type ServerGroupState<TData> = {
|
|
21
|
-
/** The flattened tree: top-level groups, with expanded groups' children spliced in. */
|
|
22
|
-
displayRows: ServerDisplayRow<TData>[];
|
|
23
|
-
groupBy: string[];
|
|
24
|
-
/** True during the initial top-level fetch (before the root has ever loaded). */
|
|
25
|
-
loading: boolean;
|
|
26
|
-
/** The rejection from the last failed top-level fetch, else null. */
|
|
27
|
-
error: unknown;
|
|
28
|
-
sortModel: ServerSortModel;
|
|
29
|
-
filterModel: ServerFilterModel;
|
|
30
|
-
/** Ids (JSON of the path) of the currently expanded groups. */
|
|
31
|
-
expandedGroups: string[];
|
|
32
|
-
};
|
|
33
|
-
export type ServerGroupControllerOptions<TData> = {
|
|
34
|
-
/** Columns to group on, outer to inner. Empty in `treeData` mode. */
|
|
35
|
-
groupBy?: string[];
|
|
36
|
-
/** Value columns to roll up per group. */
|
|
37
|
-
aggregations?: ServerAggregation[];
|
|
38
|
-
/** Max children fetched per group in one request. Default 200. */
|
|
39
|
-
pageSize?: number;
|
|
40
|
-
/**
|
|
41
|
-
* Self-referential tree mode: instead of grouping flat rows by columns, each
|
|
42
|
-
* row is a node whose children are fetched on expand. Requires `getRowId`
|
|
43
|
-
* (the node id, which builds the `groupKeys` path) and `hasChildren` (whether
|
|
44
|
-
* a node is expandable). `getRows` receives `groupKeys` = the path to the
|
|
45
|
-
* node being expanded, and returns that node's direct children.
|
|
46
|
-
*/
|
|
47
|
-
treeData?: boolean;
|
|
48
|
-
/** Node id, used to build the `groupKeys` path. Required in `treeData` mode. */
|
|
49
|
-
getRowId?: (row: TData) => string;
|
|
50
|
-
/** Whether a node can be expanded. Required in `treeData` mode. */
|
|
51
|
-
hasChildren?: (row: TData) => boolean;
|
|
52
|
-
/** Emit a subtotal "footer" row after each expanded group's children. */
|
|
53
|
-
groupFooters?: boolean;
|
|
54
|
-
/** Placeholder rows shown while a group's first block is loading. Default 3. */
|
|
55
|
-
skeletonRows?: number;
|
|
56
|
-
onChange: (state: ServerGroupState<TData>) => void;
|
|
57
|
-
};
|
|
58
|
-
export type ServerGroupController<TData> = {
|
|
59
|
-
/** (Re-)fetch the tree from the top. Call once to load the first level. */
|
|
60
|
-
refresh(): void;
|
|
61
|
-
setSort(sortModel: ServerSortModel): void;
|
|
62
|
-
setFilter(filterModel: ServerFilterModel): void;
|
|
63
|
-
/** Change the grouping columns. Collapses everything and reloads the top level. */
|
|
64
|
-
setGroupBy(groupBy: string[]): void;
|
|
65
|
-
/** Expand or collapse a group row (from its expander). */
|
|
66
|
-
toggleGroup(row: ServerGroupRow<TData>): void;
|
|
67
|
-
expandGroup(path: string[]): void;
|
|
68
|
-
collapseGroup(path: string[]): void;
|
|
69
|
-
/** Fetch the next block of a group's children (intra-group paging). */
|
|
70
|
-
loadMoreChildren(path: string[]): void;
|
|
71
|
-
isExpanded(path: string[]): boolean;
|
|
72
|
-
getState(): ServerGroupState<TData>;
|
|
73
|
-
/** Stop accepting in-flight responses. Call on unmount. */
|
|
74
|
-
dispose(): void;
|
|
75
|
-
};
|
|
76
|
-
/**
|
|
77
|
-
* Map a `ServerGroupState.displayRows` to grid rows: each row's data spread at
|
|
78
|
-
* the top level (so normal `field` columns show group subtotals on group rows
|
|
79
|
-
* and cell values on leaves) plus a `__group` marker the built-in `SvGroupCell`
|
|
80
|
-
* reads to draw the expander + indentation. Feed the result to `<SvGrid data>`.
|
|
81
|
-
*/
|
|
82
|
-
export type ServerGroupGridRow<TData> = TData & {
|
|
83
|
-
__group: ServerDisplayRow<TData>;
|
|
84
|
-
};
|
|
85
|
-
export declare function serverGroupRows<TData>(state: ServerGroupState<TData> | undefined | null): ServerGroupGridRow<TData>[];
|
|
86
|
-
/**
|
|
87
|
-
* Build the `serverGroup` prop for `<SvGrid>` from a controller, so the grid's
|
|
88
|
-
* built-in tree keyboard (ArrowRight / ArrowLeft expand / collapse) and treegrid
|
|
89
|
-
* a11y work with no app-level key handling. Reads the `__group` marker that
|
|
90
|
-
* `serverGroupRows` attaches. Usage: `<SvGrid ... serverGroup={serverGroupNav(ctl)} />`.
|
|
91
|
-
*/
|
|
92
|
-
export declare function serverGroupNav<TData>(ctl: ServerGroupController<TData>): {
|
|
93
|
-
isGroup: (row: ServerGroupGridRow<TData>) => boolean;
|
|
94
|
-
level: (row: ServerGroupGridRow<TData>) => number;
|
|
95
|
-
expanded: (row: ServerGroupGridRow<TData>) => boolean;
|
|
96
|
-
onToggle: (row: ServerGroupGridRow<TData>) => void;
|
|
97
|
-
};
|
|
98
|
-
export declare function createServerGroupModel<TData>(source: ServerDataSource<TData>, options: ServerGroupControllerOptions<TData>): ServerGroupController<TData>;
|