@svgrid/grid 3.0.4 → 3.0.6
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 +32 -11
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +33 -5
- package/dist/SvGrid.controller.svelte.js +397 -58
- package/dist/SvGrid.css +200 -2
- package/dist/SvGrid.svelte +350 -40
- package/dist/SvGrid.types.d.ts +433 -7
- package/dist/SvGridDropdown.svelte +20 -0
- package/dist/SvGridDropdown.svelte.d.ts +12 -0
- package/dist/SvModal.svelte +22 -0
- package/dist/SvModal.svelte.d.ts +7 -0
- package/dist/a11y/dismissable.d.ts +10 -0
- package/dist/a11y/dismissable.js +24 -2
- package/dist/build-api.js +92 -37
- package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-C-NtjlrJ.js} +155 -146
- package/dist/cdn/GridMenus-TssHej3q.js +644 -0
- package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
- package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
- package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
- package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
- package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/server-block-cache-DIqb3VZn.js +241 -0
- package/dist/cdn/{src-V1uu8iE9.js → src-CgoeSM8k.js} +5339 -4753
- package/dist/cdn/{src-BKhZ6eXd.js → src-DfwxB1TH.js} +9817 -9231
- package/dist/cdn/svgrid.js +28 -27
- package/dist/cdn/svgrid.svelte-external.js +28 -27
- package/dist/cdn/validate-AZoD8BoR.js +80 -0
- package/dist/cell-formatting.js +18 -1
- package/dist/cell-render.js +25 -6
- package/dist/clipboard.d.ts +2 -2
- package/dist/clipboard.js +72 -23
- package/dist/columns.js +7 -2
- package/dist/conditional-formatting.d.ts +2 -0
- package/dist/conditional-formatting.js +2 -0
- package/dist/core.d.ts +22 -6
- package/dist/core.js +401 -38
- package/dist/editing.js +34 -0
- package/dist/gantt-view.svelte.d.ts +24 -0
- package/dist/gantt-view.svelte.js +13 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +10 -0
- package/dist/grid-messages.js +9 -0
- package/dist/headless.d.ts +5 -0
- package/dist/headless.js +12 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +34 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/keyboard-handlers.js +24 -1
- package/dist/keyboard.js +4 -0
- package/dist/row-drag-touch.d.ts +1 -1
- package/dist/row-drag.js +35 -1
- package/dist/row-model.d.ts +139 -0
- package/dist/row-model.js +16 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +31 -3
- package/dist/server-block-cache.d.ts +219 -0
- package/dist/server-block-cache.js +539 -0
- package/dist/server-data-source.d.ts +274 -8
- package/dist/server-data-source.js +229 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +29 -3
- package/package.json +11 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +397 -61
- package/src/SvGrid.css +200 -2
- package/src/SvGrid.svelte +350 -40
- package/src/SvGrid.types.ts +453 -7
- package/src/SvGridDropdown.svelte +20 -0
- package/src/SvModal.svelte +22 -0
- package/src/SvModal.test.ts +30 -1
- package/src/a11y/dismissable.test.ts +10 -1
- package/src/a11y/dismissable.ts +22 -2
- package/src/build-api.ts +96 -31
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +18 -1
- package/src/cell-render.test.ts +24 -2
- package/src/cell-render.ts +24 -8
- package/src/clipboard.test.ts +107 -0
- package/src/clipboard.ts +62 -23
- package/src/columns.test.ts +4 -4
- package/src/columns.ts +7 -2
- package/src/conditional-formatting.test.ts +8 -0
- package/src/conditional-formatting.ts +3 -0
- package/src/core.rowmodel-cache.test.ts +51 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +407 -38
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +33 -0
- package/src/gantt-stub.test.svelte +38 -0
- package/src/gantt-view.svelte.ts +35 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +22 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/icon-seam.test.ts +2 -4
- package/src/index.ts +38 -10
- package/src/keyboard-handlers.coverage.test.ts +65 -0
- package/src/keyboard-handlers.ts +26 -1
- package/src/keyboard.ts +3 -0
- package/src/row-drag-touch.ts +2 -2
- package/src/row-drag.test.ts +51 -0
- package/src/row-drag.ts +35 -2
- package/src/row-model.ts +152 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.test.ts +3 -0
- package/src/selection.ts +33 -3
- package/src/server-block-cache.test.ts +672 -0
- package/src/server-block-cache.ts +690 -0
- package/src/server-data-source.infinite.test.ts +405 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +496 -29
- package/src/server.ts +49 -0
- package/src/sparkline.test.ts +7 -0
- package/src/sparkline.ts +6 -4
- package/src/svgrid-wrapper.types.ts +32 -0
- package/src/svgrid.api.test.ts +21 -0
- package/src/svgrid.charting.test.ts +12 -4
- package/src/svgrid.context-menu.test.ts +12 -0
- package/src/svgrid.detail-rows.svelte.test.ts +187 -0
- package/src/svgrid.filter-menu-scroll.test.ts +9 -0
- package/src/svgrid.gantt-seam.test.ts +199 -0
- package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +305 -0
- package/src/svgrid.row-pinning.test.ts +15 -1
- package/src/svgrid.selection-bar-seam.test.ts +1 -1
- package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
- package/src/svgrid.upsell-license.test.ts +8 -5
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +34 -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/GridMenus-7kbpnnBW.js +0 -635
- 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/editing.ts
CHANGED
|
@@ -47,6 +47,14 @@ export function createEditing<
|
|
|
47
47
|
function isCellEditable(column: Column<TData>, row?: Row<TData>): boolean {
|
|
48
48
|
const editable = column.columnDef.editable;
|
|
49
49
|
if (editable === false) return false;
|
|
50
|
+
if (row) {
|
|
51
|
+
// A server-side group row is a key with aggregates under the leaf
|
|
52
|
+
// columns, and a placeholder row has no data yet: neither takes an
|
|
53
|
+
// edit, whatever the column says.
|
|
54
|
+
const serverGroup = ctx.props.serverGroup;
|
|
55
|
+
if (serverGroup && row.original !== undefined && serverGroup.isGroup(row.original)) return false;
|
|
56
|
+
if (ctx.placeholderStateOf?.(row)) return false;
|
|
57
|
+
}
|
|
50
58
|
if (typeof editable !== "function") return true;
|
|
51
59
|
if (!row) return true;
|
|
52
60
|
const cellCtx: CellContext<TData> = {
|
|
@@ -150,6 +158,24 @@ export function createEditing<
|
|
|
150
158
|
runHistoryGroup(ctx, () => commitEditingCell());
|
|
151
159
|
}
|
|
152
160
|
|
|
161
|
+
/**
|
|
162
|
+
* True when a column with `rejectInvalid` refuses `value`: its `validate`
|
|
163
|
+
* hook returns `false` or a message. Without the flag nothing is refused.
|
|
164
|
+
*/
|
|
165
|
+
function editRejected(column: any, value: unknown, rowData: unknown): boolean {
|
|
166
|
+
const def = column?.columnDef as
|
|
167
|
+
| { rejectInvalid?: boolean; validate?: (p: unknown) => string | boolean | null | undefined }
|
|
168
|
+
| undefined;
|
|
169
|
+
if (!def?.rejectInvalid || typeof def.validate !== "function") return false;
|
|
170
|
+
const verdict = def.validate({
|
|
171
|
+
value,
|
|
172
|
+
row: rowData,
|
|
173
|
+
rowIndex: ctx.internalData.indexOf(rowData as TData),
|
|
174
|
+
column,
|
|
175
|
+
});
|
|
176
|
+
return verdict === false || (typeof verdict === "string" && verdict.length > 0);
|
|
177
|
+
}
|
|
178
|
+
|
|
153
179
|
function commitEditingCell() {
|
|
154
180
|
if (!ctx.editingCell) return;
|
|
155
181
|
const editing = ctx.editingCell;
|
|
@@ -192,6 +218,12 @@ export function createEditing<
|
|
|
192
218
|
columnId: ctx.editingCell.columnId,
|
|
193
219
|
});
|
|
194
220
|
}
|
|
221
|
+
// A refused edit closes the editor and leaves the row, the overlay,
|
|
222
|
+
// the history and the consumer untouched.
|
|
223
|
+
if (editRejected(column, finalValue, rowData)) {
|
|
224
|
+
ctx.editingCell = null;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
195
227
|
rowData[column.columnDef.field] = finalValue;
|
|
196
228
|
}
|
|
197
229
|
const key = getCellKey(ctx.editingCell.rowId, ctx.editingCell.columnId);
|
|
@@ -770,6 +802,7 @@ export function createEditing<
|
|
|
770
802
|
columnId: column.id,
|
|
771
803
|
})
|
|
772
804
|
: parsed;
|
|
805
|
+
if (editRejected(column, finalValue, row.original)) continue;
|
|
773
806
|
if (oldValue !== finalValue) {
|
|
774
807
|
(row.original as Record<string, unknown>)[field] = finalValue;
|
|
775
808
|
const key = getCellKey(fr.rowId, column.id);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A stand-in for @svgrid/enterprise's SvGridGantt, for `svgrid.gantt-seam.test.ts`.
|
|
4
|
+
*
|
|
5
|
+
* The seam's contract is which props the grid hands the renderer, so this
|
|
6
|
+
* writes each of them into the DOM where a test can read them back. It is
|
|
7
|
+
* named `.test.svelte` so `tools/strip-dist-tests.mjs` keeps it out of the
|
|
8
|
+
* published tarball, and the grid's vitest only collects `*.test.ts`, so it
|
|
9
|
+
* is never mistaken for a suite of its own.
|
|
10
|
+
*/
|
|
11
|
+
import type { ColumnDef, GanttConfig, RowData, TableFeatures } from './index'
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
data = [],
|
|
15
|
+
columns = [],
|
|
16
|
+
gantt,
|
|
17
|
+
getRowId,
|
|
18
|
+
}: {
|
|
19
|
+
data: ReadonlyArray<RowData>
|
|
20
|
+
columns: Array<ColumnDef<TableFeatures, any>>
|
|
21
|
+
gantt: GanttConfig<TableFeatures, any>
|
|
22
|
+
getRowId?: (row: any, index: number) => string
|
|
23
|
+
} = $props()
|
|
24
|
+
|
|
25
|
+
const ids = $derived(
|
|
26
|
+
data.map((r, i) => (getRowId ? getRowId(r, i) : String(i))).join(','),
|
|
27
|
+
)
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
class="gantt-stub"
|
|
32
|
+
data-ids={ids}
|
|
33
|
+
data-rows={data.length}
|
|
34
|
+
data-cols={columns.length}
|
|
35
|
+
data-start={gantt?.startField ?? ''}
|
|
36
|
+
data-parent={gantt?.parentField ?? ''}
|
|
37
|
+
data-has-get-row-id={typeof getRowId === 'function'}
|
|
38
|
+
></div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gantt-view registry - the injection seam for the *Gantt view of the grid*.
|
|
3
|
+
* Setting the `gantt` prop on `<SvGrid>` switches the table for a task table
|
|
4
|
+
* beside a time chart (bars by start / end, a work-breakdown tree, dependency
|
|
5
|
+
* arrows), but the renderer itself ships in `@svgrid/enterprise` (a paid
|
|
6
|
+
* feature). This tiny reactive holder lets enterprise register that renderer at
|
|
7
|
+
* import time; the grid looks it up and mounts it, or shows an upsell
|
|
8
|
+
* placeholder when it is absent. Mirrors the `scheduler-view` / `board-view`
|
|
9
|
+
* pattern.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // in @svgrid/enterprise, at module load:
|
|
13
|
+
* import { registerGanttView } from '@svgrid/grid'
|
|
14
|
+
* import SvGridGantt from './gantt/SvGridGantt.svelte'
|
|
15
|
+
* registerGanttView(SvGridGantt)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { Component } from 'svelte'
|
|
19
|
+
|
|
20
|
+
let renderer = $state<Component<any> | null>(null)
|
|
21
|
+
|
|
22
|
+
/** Register the component that renders `gantt` mode. Enterprise calls this. */
|
|
23
|
+
export function registerGanttView(component: Component<any>): void {
|
|
24
|
+
renderer = component
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The registered Gantt renderer, or null when enterprise is not installed. */
|
|
28
|
+
export function getGanttView(): Component<any> | null {
|
|
29
|
+
return renderer
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Whether a Gantt renderer has been registered. */
|
|
33
|
+
export function hasGanttView(): boolean {
|
|
34
|
+
return renderer != null
|
|
35
|
+
}
|
package/src/grid-icons.ts
CHANGED
|
@@ -97,8 +97,8 @@ export type GridIcons = Partial<Record<GridIconName, Snippet>>
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* Icons whose built-in form is a character rather than an SVG path. Kept as
|
|
100
|
-
* data so `<SvGrid>`, its footer and the
|
|
101
|
-
* the same defaults instead of three copies drifting apart.
|
|
100
|
+
* data so `<SvGrid>`, its footer and the row-group panel in @svgrid/enterprise
|
|
101
|
+
* render the same defaults instead of three copies drifting apart.
|
|
102
102
|
*
|
|
103
103
|
* Each is the exact character that shipped inline before, so a grid that sets
|
|
104
104
|
* no `icons` renders byte-for-byte what it always did.
|
|
@@ -8,6 +8,13 @@ describe('resolveGridMessages', () => {
|
|
|
8
8
|
expect(resolveGridMessages({})).toEqual(defaultGridMessages)
|
|
9
9
|
})
|
|
10
10
|
|
|
11
|
+
it('names the client group expander, so a localized grid has no English left in it', () => {
|
|
12
|
+
const m = resolveGridMessages({ expandGroup: 'Gruppe aufklappen', rowsSuffix: 'Zeilen' })
|
|
13
|
+
expect(m.expandGroup).toBe('Gruppe aufklappen')
|
|
14
|
+
expect(m.collapseGroup).toBe('Collapse group')
|
|
15
|
+
expect(m.rowsSuffix).toBe('Zeilen')
|
|
16
|
+
})
|
|
17
|
+
|
|
11
18
|
it('replaces only the provided keys, keeping English for the rest', () => {
|
|
12
19
|
const m = resolveGridMessages({ noRows: 'Aucune ligne', pageSize: 'Taille :' })
|
|
13
20
|
expect(m.noRows).toBe('Aucune ligne')
|
package/src/grid-messages.ts
CHANGED
|
@@ -106,9 +106,22 @@ export type GridChromeMessages = {
|
|
|
106
106
|
bulkEditCancel: string
|
|
107
107
|
bulkEditNoFields: string
|
|
108
108
|
chartRange: string
|
|
109
|
+
// Rows whose data has not arrived (server-side / infinite loading)
|
|
110
|
+
rowLoadFailed: string
|
|
111
|
+
rowLoadRetry: string
|
|
112
|
+
// The expander on a client-side group row
|
|
113
|
+
expandGroup: string
|
|
114
|
+
collapseGroup: string
|
|
115
|
+
// The chevron of the detail-toggle column (`showDetailToggle`)
|
|
116
|
+
openDetail: string
|
|
117
|
+
closeDetail: string
|
|
109
118
|
// Upsell notes (enterprise views not installed)
|
|
110
119
|
pivotUpsellTitle: string
|
|
111
120
|
pivotUpsellBody: string
|
|
121
|
+
ganttUpsellTitle: string
|
|
122
|
+
ganttUpsellBody: string
|
|
123
|
+
/** Placeholder in the Gantt view's search box. */
|
|
124
|
+
ganttSearchPlaceholder: string
|
|
112
125
|
/** Shared licensing line under every Enterprise upsell note. */
|
|
113
126
|
upsellLicense: string
|
|
114
127
|
upsellLicenseLink: string
|
|
@@ -185,8 +198,17 @@ export const defaultGridMessages: GridChromeMessages = {
|
|
|
185
198
|
opBefore: 'Before',
|
|
186
199
|
opAfter: 'After',
|
|
187
200
|
chartRange: 'Chart selected range',
|
|
201
|
+
rowLoadFailed: 'Could not load these rows.',
|
|
202
|
+
rowLoadRetry: 'Retry',
|
|
203
|
+
expandGroup: 'Expand group',
|
|
204
|
+
collapseGroup: 'Collapse group',
|
|
205
|
+
openDetail: 'Open details',
|
|
206
|
+
closeDetail: 'Close details',
|
|
188
207
|
pivotUpsellTitle: 'Pivot mode',
|
|
189
208
|
pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
|
|
209
|
+
ganttUpsellTitle: 'Gantt view',
|
|
210
|
+
ganttUpsellBody: 'The Gantt view is an Enterprise feature. Install @svgrid/enterprise and call enableGanttView() to render it.',
|
|
211
|
+
ganttSearchPlaceholder: 'Search tasks...',
|
|
190
212
|
upsellLicense:
|
|
191
213
|
'A license key is required for production use. Without one the feature still works, but the grid renders an unlicensed watermark.',
|
|
192
214
|
upsellLicenseLink: 'View licensing',
|
package/src/headless.ts
CHANGED
|
@@ -85,3 +85,27 @@ export {
|
|
|
85
85
|
export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte'
|
|
86
86
|
export { createGridState, createSvGridState } from './createGridState.svelte'
|
|
87
87
|
export { subscribeGrid, subscribeSvGrid } from './subscribe'
|
|
88
|
+
|
|
89
|
+
// The virtualizers are pure window arithmetic (scroll offset in, visible
|
|
90
|
+
// range out) with no DOM of their own, and a custom renderer over 50k rows
|
|
91
|
+
// needs one. They were only on the barrel, so the Headless demos imported
|
|
92
|
+
// the barrel to reach them and paid for the whole thing.
|
|
93
|
+
export { createVirtualizer } from './virtualization/virtualizer'
|
|
94
|
+
export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte'
|
|
95
|
+
export { createColumnVirtualizer } from './virtualization/column-virtualizer'
|
|
96
|
+
export type { VirtualItem, VirtualizerOptions, VirtualizerState } from './virtualization/types'
|
|
97
|
+
|
|
98
|
+
// The WAI-ARIA attribute factories: plain objects to spread onto your own
|
|
99
|
+
// markup (`role`, `aria-sort`, `aria-rowindex`, ...). No DOM is touched here
|
|
100
|
+
// either; a custom renderer that wants keyboard navigation reads
|
|
101
|
+
// `activeCell` from the store and spreads these.
|
|
102
|
+
export {
|
|
103
|
+
getGridCellDomId,
|
|
104
|
+
getGridCellA11yProps,
|
|
105
|
+
getGridHeaderA11yProps,
|
|
106
|
+
getGridRootA11yProps,
|
|
107
|
+
getGridRowA11yProps,
|
|
108
|
+
type GridCellA11yInput,
|
|
109
|
+
type GridColumnA11yInput,
|
|
110
|
+
type GridSortDirection,
|
|
111
|
+
} from './a11y'
|
package/src/history.test.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
pushHistory,
|
|
6
6
|
runHistoryGroup,
|
|
7
7
|
nextGroupId,
|
|
8
|
+
peekUndoHistory,
|
|
9
|
+
peekRedoHistory,
|
|
8
10
|
type HistoryStep,
|
|
9
11
|
} from './history'
|
|
10
12
|
|
|
@@ -162,7 +164,7 @@ describe('runHistoryGroup', () => {
|
|
|
162
164
|
// This is why the group id is ambient rather than stamped afterwards: the
|
|
163
165
|
// cap drops entries from the front, so any index captured before the call
|
|
164
166
|
// would point at the wrong steps by the end of it.
|
|
165
|
-
const { ctx } = makeCtx([step(0)],
|
|
167
|
+
const { ctx } = makeCtx([step(0)], 1)
|
|
166
168
|
runHistoryGroup(ctx, () => {
|
|
167
169
|
pushHistory(ctx, [step(1)])
|
|
168
170
|
pushHistory(ctx, [step(2)])
|
|
@@ -172,6 +174,21 @@ describe('runHistoryGroup', () => {
|
|
|
172
174
|
expect(new Set(ctx.history.map((s) => s.groupId)).size).toBe(1)
|
|
173
175
|
})
|
|
174
176
|
|
|
177
|
+
it('caps by actions, so a group larger than the buffer stays whole', () => {
|
|
178
|
+
// A sort of 5,000 rows is one Ctrl+Z of 20,000 steps; a cap that
|
|
179
|
+
// counted steps cut it to its tail and undid a fifth of it.
|
|
180
|
+
const { ctx } = makeCtx([step(0)], 2)
|
|
181
|
+
runHistoryGroup(ctx, () => { for (let i = 1; i <= 5; i += 1) pushHistory(ctx, [step(i)]) })
|
|
182
|
+
expect(ctx.history.map((s) => s.rowId)).toEqual(['r0', 'r1', 'r2', 'r3', 'r4', 'r5'])
|
|
183
|
+
pushHistory(ctx, [step(9)])
|
|
184
|
+
// Three actions now; the oldest one goes and the group survives whole.
|
|
185
|
+
expect(ctx.history.map((s) => s.rowId)).toEqual(['r1', 'r2', 'r3', 'r4', 'r5', 'r9'])
|
|
186
|
+
expect(ctx.historyPtr).toBe(5)
|
|
187
|
+
undoHistory(ctx)
|
|
188
|
+
undoHistory(ctx)
|
|
189
|
+
expect(ctx.historyPtr).toBe(-1)
|
|
190
|
+
})
|
|
191
|
+
|
|
175
192
|
it('nests into the outer group so one action is one undo', () => {
|
|
176
193
|
const { ctx } = makeCtx()
|
|
177
194
|
runHistoryGroup(ctx, () => {
|
|
@@ -194,3 +211,34 @@ describe('runHistoryGroup', () => {
|
|
|
194
211
|
expect(runHistoryGroup(ctx, () => 42)).toBe(42)
|
|
195
212
|
})
|
|
196
213
|
})
|
|
214
|
+
|
|
215
|
+
describe('tags', () => {
|
|
216
|
+
it('stamps the ambient tag on every step recorded while it is set, grouped or not', () => {
|
|
217
|
+
const { ctx } = makeCtx()
|
|
218
|
+
const sheet = { name: 'Orders' }
|
|
219
|
+
pushHistory(ctx, [step(1)])
|
|
220
|
+
;(ctx as { historyTag?: unknown }).historyTag = sheet
|
|
221
|
+
pushHistory(ctx, [step(2)])
|
|
222
|
+
runHistoryGroup(ctx, () => { pushHistory(ctx, [step(3)]); pushHistory(ctx, [step(4)]) })
|
|
223
|
+
;(ctx as { historyTag?: unknown }).historyTag = undefined
|
|
224
|
+
pushHistory(ctx, [step(5)])
|
|
225
|
+
expect(ctx.history.map((s) => s.tag)).toEqual([undefined, sheet, sheet, sheet, undefined])
|
|
226
|
+
expect(ctx.history[2]!.groupId).toBe(ctx.history[3]!.groupId)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('peeks at the step the next undo or redo would take', () => {
|
|
230
|
+
const { ctx } = makeCtx()
|
|
231
|
+
;(ctx as { historyTag?: unknown }).historyTag = 'Summary'
|
|
232
|
+
pushHistory(ctx, [step(1)])
|
|
233
|
+
;(ctx as { historyTag?: unknown }).historyTag = 'Orders'
|
|
234
|
+
runHistoryGroup(ctx, () => { pushHistory(ctx, [step(2)]); pushHistory(ctx, [step(3)]) })
|
|
235
|
+
expect(peekUndoHistory(ctx)).toEqual({ tag: 'Orders' })
|
|
236
|
+
expect(peekRedoHistory(ctx)).toBeNull()
|
|
237
|
+
undoHistory(ctx)
|
|
238
|
+
expect(peekUndoHistory(ctx)).toEqual({ tag: 'Summary' })
|
|
239
|
+
expect(peekRedoHistory(ctx)).toEqual({ tag: 'Orders' })
|
|
240
|
+
undoHistory(ctx)
|
|
241
|
+
expect(peekUndoHistory(ctx)).toBeNull()
|
|
242
|
+
expect(peekRedoHistory(ctx)).toEqual({ tag: 'Summary' })
|
|
243
|
+
})
|
|
244
|
+
})
|
package/src/history.ts
CHANGED
|
@@ -36,6 +36,15 @@ export type HistoryStep = {
|
|
|
36
36
|
* functions do the work. Never serialised, since a closure cannot be.
|
|
37
37
|
*/
|
|
38
38
|
custom?: { undo: () => void; redo: () => void }
|
|
39
|
+
/**
|
|
40
|
+
* The consumer's own mark on the step, copied from `api.setHistoryTag` at
|
|
41
|
+
* the moment it is recorded. The grid never reads it: it is there for a
|
|
42
|
+
* consumer that shows one of several data sets through the grid and has
|
|
43
|
+
* to know which one a step belongs to before it lets Ctrl+Z apply it (a
|
|
44
|
+
* workbook with several sheets, say). Kept as given, so an object tag
|
|
45
|
+
* compares by identity. Not serialised.
|
|
46
|
+
*/
|
|
47
|
+
tag?: unknown
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
/** Ctx surface these helpers touch. The controller satisfies it structurally. */
|
|
@@ -48,6 +57,8 @@ type HistoryCtx = {
|
|
|
48
57
|
* inside it joins that group, so a command does not have to thread a group
|
|
49
58
|
* id down through the writers it calls. */
|
|
50
59
|
historyGroupId: string | undefined
|
|
60
|
+
/** Stamped on every step recorded while it is set; see `HistoryStep.tag`. */
|
|
61
|
+
historyTag?: unknown
|
|
51
62
|
applyHistoryStep(step: HistoryStep, direction: 'undo' | 'redo'): void
|
|
52
63
|
}
|
|
53
64
|
|
|
@@ -71,6 +82,37 @@ function groupEnd(history: ReadonlyArray<HistoryStep>, start: number): number {
|
|
|
71
82
|
return i
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The last `limit` ACTIONS of a history, whole groups kept. The cap used
|
|
87
|
+
* to count steps, and a sort of 5,000 rows is 20,000 steps in one group:
|
|
88
|
+
* it cut the group to its last 200 cells, so Ctrl+Z put a fifth of a
|
|
89
|
+
* column back and left the rest sorted. An action is one press of Ctrl+Z,
|
|
90
|
+
* so that is what the buffer counts; the steps inside are the cost of it.
|
|
91
|
+
*/
|
|
92
|
+
function lastActions(history: HistoryStep[], limit: number): HistoryStep[] {
|
|
93
|
+
let i = history.length
|
|
94
|
+
let actions = 0
|
|
95
|
+
while (i > 0 && actions < limit) {
|
|
96
|
+
i = groupStart(history, i - 1)
|
|
97
|
+
actions += 1
|
|
98
|
+
}
|
|
99
|
+
return i === 0 ? history : history.slice(i)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** What the next undo would apply: the tag of the step Ctrl+Z takes next
|
|
103
|
+
* (a group carries one tag, so its last step's is the group's). Null when
|
|
104
|
+
* there is nothing to undo. */
|
|
105
|
+
export function peekUndoHistory(ctx: HistoryCtx): { tag: unknown } | null {
|
|
106
|
+
const step = ctx.historyPtr >= 0 ? ctx.history[ctx.historyPtr] : undefined
|
|
107
|
+
return step ? { tag: step.tag } : null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The redo counterpart of `peekUndoHistory`. */
|
|
111
|
+
export function peekRedoHistory(ctx: HistoryCtx): { tag: unknown } | null {
|
|
112
|
+
const step = ctx.historyPtr < ctx.history.length - 1 ? ctx.history[ctx.historyPtr + 1] : undefined
|
|
113
|
+
return step ? { tag: step.tag } : null
|
|
114
|
+
}
|
|
115
|
+
|
|
74
116
|
/** Undo one action. Returns false when there is nothing left to undo. */
|
|
75
117
|
export function undoHistory(ctx: HistoryCtx): boolean {
|
|
76
118
|
if (ctx.historyPtr < 0) return false
|
|
@@ -123,9 +165,13 @@ export function pushHistory(
|
|
|
123
165
|
// Outside one, a group of one is just a step. Tagging it would make undo do
|
|
124
166
|
// the same work through a slower path and would show up in serialized state.
|
|
125
167
|
const id = ctx.historyGroupId ?? (groupId && steps.length > 1 ? groupId : undefined)
|
|
168
|
+
const tag = ctx.historyTag
|
|
126
169
|
let next = ctx.history.slice(0, ctx.historyPtr + 1)
|
|
127
|
-
for (const step of steps)
|
|
128
|
-
|
|
170
|
+
for (const step of steps) {
|
|
171
|
+
const stamped = tag === undefined ? step : { ...step, tag }
|
|
172
|
+
next.push(id ? { ...stamped, groupId: id } : stamped)
|
|
173
|
+
}
|
|
174
|
+
next = lastActions(next, ctx.UNDO_LIMIT)
|
|
129
175
|
ctx.history = next
|
|
130
176
|
ctx.historyPtr = ctx.history.length - 1
|
|
131
177
|
ctx.historyVersion += 1
|
package/src/icon-seam.test.ts
CHANGED
|
@@ -20,18 +20,16 @@ const SRC = join(process.cwd(), 'src')
|
|
|
20
20
|
const read = (f: string) => readFileSync(join(SRC, f), 'utf8')
|
|
21
21
|
|
|
22
22
|
/** Files that render grid chrome and therefore must go through the seam. */
|
|
23
|
-
const CHROME = ['SvGrid.svelte', 'GridMenus.svelte', 'GridFooter.svelte'
|
|
23
|
+
const CHROME = ['SvGrid.svelte', 'GridMenus.svelte', 'GridFooter.svelte']
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Drop the regions allowed to contain raw icon markup: the `icon` snippet's own
|
|
27
|
-
* body (which IS the built-in set)
|
|
28
|
-
* every `<script>` block.
|
|
27
|
+
* body (which IS the built-in set) and every `<script>` block.
|
|
29
28
|
*/
|
|
30
29
|
function stripAllowedRegions(src: string): string {
|
|
31
30
|
let out = src.replace(/<script[\s\S]*?<\/script>/g, '')
|
|
32
31
|
// The built-in catalogue: from the snippet header to its closing tag.
|
|
33
32
|
out = out.replace(/\{#snippet icon\(name: GridIconName\)\}[\s\S]*?\n\{\/snippet\}/g, '')
|
|
34
|
-
out = out.replace(/\{#snippet ic\(name: GridIconName\)\}[\s\S]*?\{\/snippet\}/g, '')
|
|
35
33
|
return out
|
|
36
34
|
}
|
|
37
35
|
|
package/src/index.ts
CHANGED
|
@@ -121,6 +121,23 @@ export type {
|
|
|
121
121
|
SchedulerOccurrenceChangeEvent,
|
|
122
122
|
} from './SvGrid.types'
|
|
123
123
|
export { registerSchedulerView, getSchedulerView, hasSchedulerView } from './scheduler-view.svelte'
|
|
124
|
+
// Gantt view. Same split as the scheduler: the `gantt` prop and its config
|
|
125
|
+
// types live in the free grid, the RENDERER ships in @svgrid/enterprise and
|
|
126
|
+
// registers itself through this seam (see gantt-view.svelte.ts). Unlike the
|
|
127
|
+
// scheduler, the Gantt's layout model lives in enterprise too - only the
|
|
128
|
+
// renderer reads it.
|
|
129
|
+
export type {
|
|
130
|
+
GanttConfig,
|
|
131
|
+
GanttDrawerConfig,
|
|
132
|
+
GanttTaskMoveEvent,
|
|
133
|
+
GanttTaskResizeEvent,
|
|
134
|
+
GanttProgressChangeEvent,
|
|
135
|
+
GanttTaskCommitEvent,
|
|
136
|
+
GanttDependency,
|
|
137
|
+
GanttDependencyType,
|
|
138
|
+
GanttZoom,
|
|
139
|
+
} from './SvGrid.types'
|
|
140
|
+
export { registerGanttView, getGanttView, hasGanttView } from './gantt-view.svelte'
|
|
124
141
|
// Bulk-action bar over the row selection. Prop + types free, renderer Pro.
|
|
125
142
|
export { registerSelectionBarView, getSelectionBarView, hasSelectionBarView } from './selection-bar-view.svelte'
|
|
126
143
|
// Chart view. The `chart` prop + its config live in the free grid AND so does the
|
|
@@ -754,31 +771,42 @@ export {
|
|
|
754
771
|
export {
|
|
755
772
|
createServerDataSource,
|
|
756
773
|
type ServerDataSource,
|
|
774
|
+
type ServerSelectionRule,
|
|
757
775
|
type ServerRequest,
|
|
758
776
|
type ServerResult,
|
|
759
777
|
type ServerController,
|
|
778
|
+
type ServerControllerOptions,
|
|
760
779
|
type ServerState,
|
|
761
780
|
type ServerSortModel,
|
|
762
781
|
type ServerFilterModel,
|
|
782
|
+
type ServerAggFn,
|
|
763
783
|
type ServerAggregation,
|
|
764
784
|
type ServerGroupRow,
|
|
765
785
|
type ServerLeafRow,
|
|
766
786
|
type ServerMoreRow,
|
|
767
787
|
type ServerFooterRow,
|
|
768
788
|
type ServerSkeletonRow,
|
|
789
|
+
type ServerGrandTotalRow,
|
|
790
|
+
type ServerPlaceholderRow,
|
|
769
791
|
type ServerDisplayRow,
|
|
792
|
+
type ServerDetailRow,
|
|
770
793
|
} from './server-data-source'
|
|
771
794
|
export {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
type
|
|
776
|
-
type
|
|
777
|
-
type
|
|
778
|
-
type
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
export {
|
|
795
|
+
createBlockCache,
|
|
796
|
+
createRowPlaceholder,
|
|
797
|
+
rowPlaceholderState,
|
|
798
|
+
type BlockCache,
|
|
799
|
+
type BlockCacheOptions,
|
|
800
|
+
type BlockCacheState,
|
|
801
|
+
type BlockFetchResult,
|
|
802
|
+
type BlockState,
|
|
803
|
+
} from './server-block-cache'
|
|
804
|
+
export {
|
|
805
|
+
toServerFilterColumns,
|
|
806
|
+
type GridRowModel,
|
|
807
|
+
type GridFilterState,
|
|
808
|
+
type GridRowModelSort,
|
|
809
|
+
} from './row-model'
|
|
782
810
|
export {
|
|
783
811
|
createNamedViews,
|
|
784
812
|
memoryViews,
|
|
@@ -576,6 +576,17 @@ describe('onWindowKeydown', () => {
|
|
|
576
576
|
expect(ctx.closeMenus).toHaveBeenCalled()
|
|
577
577
|
})
|
|
578
578
|
|
|
579
|
+
it('Escape closes the funnel filter popover, the value suggestions and the column chooser too', () => {
|
|
580
|
+
// The filter popover focuses its condition input, so Escape from there
|
|
581
|
+
// used to go unanswered while the column menu closed as expected.
|
|
582
|
+
for (const open of [{ filterMenuFor: 'col1' }, { inSuggestFor: 'col1' }, { chooseColumnsPos: { x: 1, y: 1 } }]) {
|
|
583
|
+
const ctx = makeCtx(open)
|
|
584
|
+
const { onWindowKeydown } = createKeyboard(ctx)
|
|
585
|
+
onWindowKeydown(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
586
|
+
expect(ctx.closeMenus).toHaveBeenCalled()
|
|
587
|
+
}
|
|
588
|
+
})
|
|
589
|
+
|
|
579
590
|
it('does nothing when no menu is open', () => {
|
|
580
591
|
const ctx = makeCtx()
|
|
581
592
|
const { onWindowKeydown } = createKeyboard(ctx)
|
|
@@ -678,3 +689,57 @@ describe('onHeaderSortClick', () => {
|
|
|
678
689
|
expect(ctx._state.sorting).toEqual([{ id: 'name', desc: false }])
|
|
679
690
|
})
|
|
680
691
|
})
|
|
692
|
+
|
|
693
|
+
describe('onGridKeyDown / Ctrl+Enter on a server-side tree', () => {
|
|
694
|
+
const tree = () => {
|
|
695
|
+
const toggle = vi.fn()
|
|
696
|
+
const toggleDetail = vi.fn()
|
|
697
|
+
const rows = [
|
|
698
|
+
{ original: { id: 'g', kind: 'group' } },
|
|
699
|
+
{ original: { id: 'a', kind: 'leaf' } },
|
|
700
|
+
]
|
|
701
|
+
const serverGroup = {
|
|
702
|
+
isGroup: (r: { kind: string }) => r.kind === 'group',
|
|
703
|
+
level: () => 0,
|
|
704
|
+
onToggle: toggle,
|
|
705
|
+
toggleDetail,
|
|
706
|
+
}
|
|
707
|
+
return { toggle, toggleDetail, rows, serverGroup }
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
it('opens or closes the group under the active cell', () => {
|
|
711
|
+
const t = tree()
|
|
712
|
+
const ctx = makeCtx({ allRows: t.rows, props: { rowHeight: 30, serverGroup: t.serverGroup, renderDetailRow: () => {} }, state: { activeCell: { rowIndex: 0, colIndex: 0, cellId: null } } })
|
|
713
|
+
createKeyboard(ctx).onGridKeyDown(rootKeyEvent({ key: 'Enter', ctrlKey: true }))
|
|
714
|
+
expect(t.toggle).toHaveBeenCalledWith(t.rows[0]!.original)
|
|
715
|
+
expect(t.toggleDetail).not.toHaveBeenCalled()
|
|
716
|
+
expect(ctx.onCellDoubleClick).not.toHaveBeenCalled()
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
it('opens or closes the detail panel under a leaf, when the grid draws one', () => {
|
|
720
|
+
const t = tree()
|
|
721
|
+
const ctx = makeCtx({ allRows: t.rows, props: { rowHeight: 30, serverGroup: t.serverGroup, renderDetailRow: () => {} }, state: { activeCell: { rowIndex: 1, colIndex: 0, cellId: null } } })
|
|
722
|
+
createKeyboard(ctx).onGridKeyDown(rootKeyEvent({ key: 'Enter', ctrlKey: true }))
|
|
723
|
+
expect(t.toggleDetail).toHaveBeenCalledWith(t.rows[1]!.original)
|
|
724
|
+
expect(t.toggle).not.toHaveBeenCalled()
|
|
725
|
+
// Without a renderer there is no panel to open: Enter falls through to editing.
|
|
726
|
+
const plain = makeCtx({ allRows: t.rows, props: { rowHeight: 30, serverGroup: t.serverGroup }, state: { activeCell: { rowIndex: 1, colIndex: 0, cellId: null } } })
|
|
727
|
+
createKeyboard(plain).onGridKeyDown(rootKeyEvent({ key: 'Enter', ctrlKey: true }))
|
|
728
|
+
expect(t.toggleDetail).toHaveBeenCalledTimes(1)
|
|
729
|
+
expect(plain.onCellDoubleClick).toHaveBeenCalled()
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
it('on a client master-detail calls onDetailToggle with the row, never on the detail row itself', () => {
|
|
733
|
+
const onDetailToggle = vi.fn()
|
|
734
|
+
const rows = [{ original: { id: 'a', kind: 'data' } }, { original: { id: 'a-detail', kind: 'detail' } }]
|
|
735
|
+
const props = { rowHeight: 30, renderDetailRow: () => {}, onDetailToggle, isDetailRow: (r: { kind: string }) => r.kind === 'detail' }
|
|
736
|
+
const ctx = makeCtx({ allRows: rows, props, state: { activeCell: { rowIndex: 0, colIndex: 0, cellId: null } } })
|
|
737
|
+
createKeyboard(ctx).onGridKeyDown(rootKeyEvent({ key: 'Enter', ctrlKey: true }))
|
|
738
|
+
expect(onDetailToggle).toHaveBeenCalledWith(rows[0]!.original, 0)
|
|
739
|
+
expect(ctx.onCellDoubleClick).not.toHaveBeenCalled()
|
|
740
|
+
const onDetail = makeCtx({ allRows: rows, props, state: { activeCell: { rowIndex: 1, colIndex: 0, cellId: null } } })
|
|
741
|
+
createKeyboard(onDetail).onGridKeyDown(rootKeyEvent({ key: 'Enter', ctrlKey: true }))
|
|
742
|
+
expect(onDetailToggle).toHaveBeenCalledTimes(1)
|
|
743
|
+
expect(onDetail.onCellDoubleClick).toHaveBeenCalled()
|
|
744
|
+
})
|
|
745
|
+
})
|
package/src/keyboard-handlers.ts
CHANGED
|
@@ -193,6 +193,28 @@ export function createKeyboard<
|
|
|
193
193
|
row.toggleExpanded?.();
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
|
+
// The same key on a server-side tree: a group opens or closes, a leaf
|
|
197
|
+
// opens or closes its detail panel when the grid draws one.
|
|
198
|
+
const sg = ctx.props.serverGroup;
|
|
199
|
+
if (event.ctrlKey && sg && row) {
|
|
200
|
+
const data = row.original;
|
|
201
|
+
if (sg.isGroup(data)) {
|
|
202
|
+
sg.onToggle(data);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (sg.toggleDetail && ctx.props.renderDetailRow && (sg.hasDetail?.(data) ?? true)) {
|
|
206
|
+
sg.toggleDetail(data);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// ...and on a client-side master-detail: the app's own toggle.
|
|
211
|
+
if (event.ctrlKey && row && ctx.props.onDetailToggle && ctx.props.renderDetailRow) {
|
|
212
|
+
const data = row.original;
|
|
213
|
+
if (!ctx.props.isDetailRow?.(data, current.rowIndex) && (ctx.props.hasDetail?.(data) ?? true)) {
|
|
214
|
+
ctx.props.onDetailToggle(data, current.rowIndex);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
196
218
|
if (column?.columnDef.editorType === "checkbox") {
|
|
197
219
|
ctx.toggleBooleanCell(current.rowIndex, current.colIndex);
|
|
198
220
|
return;
|
|
@@ -287,7 +309,10 @@ export function createKeyboard<
|
|
|
287
309
|
}
|
|
288
310
|
|
|
289
311
|
function onWindowKeydown(event: KeyboardEvent) {
|
|
290
|
-
if (
|
|
312
|
+
if (
|
|
313
|
+
event.key === "Escape" &&
|
|
314
|
+
(ctx.columnMenuFor || ctx.operatorMenuFor || ctx.filterMenuFor || ctx.inSuggestFor || ctx.chooseColumnsPos)
|
|
315
|
+
) {
|
|
291
316
|
ctx.closeMenus();
|
|
292
317
|
}
|
|
293
318
|
}
|
package/src/keyboard.ts
CHANGED
|
@@ -50,6 +50,9 @@ export function getKeyboardIntent(event: KeyboardEvent): GridKeyboardIntent {
|
|
|
50
50
|
// Tab → move right (Shift+Tab → left), wraps at row boundaries
|
|
51
51
|
// Delete → clear contents of the selected cells
|
|
52
52
|
// F2 / Space → start editing the active cell
|
|
53
|
+
// Ctrl+Enter acts on the row rather than moving: a group opens or closes,
|
|
54
|
+
// a row with a detail panel shows or hides it (see the activate branch).
|
|
55
|
+
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) return 'activate'
|
|
53
56
|
if (event.key === 'Enter') return event.shiftKey ? 'moveUp' : 'moveDown'
|
|
54
57
|
if (event.key === 'Tab') return event.shiftKey ? 'tabPrev' : 'tabNext'
|
|
55
58
|
if (event.key === 'F2') return 'activate'
|
package/src/row-drag-touch.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type TouchDragDeps = {
|
|
|
23
23
|
ctx: any;
|
|
24
24
|
managed: () => boolean;
|
|
25
25
|
originalAt: (rowIndex: number) => unknown;
|
|
26
|
-
commitDropOnRow: (rowIndex: number, side: "before" | "after") => void;
|
|
26
|
+
commitDropOnRow: (rowIndex: number, side: "before" | "after" | "into") => void;
|
|
27
27
|
clearIndicators: () => void;
|
|
28
28
|
cancelPendingLeave: () => void;
|
|
29
29
|
/** Publish the dragged row on the shared cross-grid bus. */
|
|
@@ -135,7 +135,7 @@ export function createTouchDrag(deps: TouchDragDeps): TouchDragHandle {
|
|
|
135
135
|
if (commit && ctx.rowDropIndex != null) {
|
|
136
136
|
deps.commitDropOnRow(
|
|
137
137
|
ctx.rowDropIndex as number,
|
|
138
|
-
(ctx.rowDropSide ?? "before") as "before" | "after",
|
|
138
|
+
(ctx.rowDropSide ?? "before") as "before" | "after" | "into",
|
|
139
139
|
);
|
|
140
140
|
} else {
|
|
141
141
|
deps.clearIndicators();
|