@svgrid/grid 1.0.2 → 1.1.1
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/LICENSE +1 -1
- package/README.md +137 -39
- package/dist/GridFooter.svelte +164 -0
- package/dist/GridFooter.svelte.d.ts +29 -0
- package/dist/GridMenus.svelte +648 -0
- package/dist/GridMenus.svelte.d.ts +31 -0
- package/dist/SvGrid.controller.svelte.d.ts +472 -0
- package/dist/SvGrid.controller.svelte.js +1917 -0
- package/dist/SvGrid.css +2012 -0
- package/dist/SvGrid.helpers.d.ts +35 -0
- package/dist/SvGrid.helpers.js +160 -0
- package/dist/SvGrid.helpers.test.d.ts +1 -0
- package/dist/SvGrid.helpers.test.js +298 -0
- package/dist/SvGrid.svelte +2346 -8742
- package/dist/SvGrid.svelte.d.ts +4 -357
- package/dist/SvGrid.types.d.ts +523 -0
- package/dist/SvGrid.types.js +1 -0
- package/dist/SvGridChart.svelte +1060 -23
- package/dist/SvGridChart.svelte.d.ts +17 -0
- package/dist/aligned-grids.d.ts +6 -0
- package/dist/aligned-grids.js +84 -0
- package/dist/aligned-grids.test.d.ts +1 -0
- package/dist/aligned-grids.test.js +75 -0
- package/dist/build-api.coverage.test.d.ts +20 -0
- package/dist/build-api.coverage.test.js +505 -0
- package/dist/build-api.d.ts +5 -0
- package/dist/build-api.js +557 -0
- package/dist/cell-render.d.ts +15 -0
- package/dist/cell-render.js +246 -0
- package/dist/cell-render.test.d.ts +1 -0
- package/dist/cell-render.test.js +338 -0
- package/dist/cell-values.d.ts +28 -0
- package/dist/cell-values.js +89 -0
- package/dist/chart-export.test.d.ts +1 -0
- package/dist/chart-export.test.js +302 -0
- package/dist/chart.coverage.test.d.ts +1 -0
- package/dist/chart.coverage.test.js +748 -0
- package/dist/chart.d.ts +370 -3
- package/dist/chart.js +1135 -42
- package/dist/clipboard.d.ts +15 -0
- package/dist/clipboard.js +420 -0
- package/dist/clipboard.test.d.ts +1 -0
- package/dist/clipboard.test.js +700 -0
- package/dist/collaboration.coverage.test.d.ts +1 -0
- package/dist/collaboration.coverage.test.js +200 -0
- package/dist/column-groups.d.ts +19 -0
- package/dist/column-groups.js +62 -0
- package/dist/column-groups.test.d.ts +1 -0
- package/dist/column-groups.test.js +56 -0
- package/dist/column-types.d.ts +10 -0
- package/dist/column-types.js +63 -0
- package/dist/column-types.test.d.ts +1 -0
- package/dist/column-types.test.js +62 -0
- package/dist/columns.d.ts +30 -0
- package/dist/columns.js +277 -0
- package/dist/columns.test.d.ts +1 -0
- package/dist/columns.test.js +625 -0
- package/dist/core.d.ts +86 -1
- package/dist/css.d.ts +3 -0
- package/dist/editing.d.ts +31 -0
- package/dist/editing.js +529 -0
- package/dist/editing.test.d.ts +1 -0
- package/dist/editing.test.js +732 -0
- package/dist/editors/cell-editors.coverage.test.d.ts +1 -0
- package/dist/editors/cell-editors.coverage.test.js +139 -0
- package/dist/editors/cell-editors.d.ts +1 -1
- package/dist/facet-buckets.d.ts +13 -0
- package/dist/facet-buckets.js +54 -0
- package/dist/facet-buckets.test.d.ts +1 -0
- package/dist/facet-buckets.test.js +296 -0
- package/dist/features.d.ts +5 -0
- package/dist/features.js +30 -0
- package/dist/filter-operators.d.ts +16 -0
- package/dist/filter-operators.js +69 -0
- package/dist/filter-operators.test.d.ts +1 -0
- package/dist/filter-operators.test.js +135 -0
- package/dist/hyperformula-adapter.d.ts +82 -0
- package/dist/hyperformula-adapter.js +73 -0
- package/dist/hyperformula-adapter.test.d.ts +1 -0
- package/dist/hyperformula-adapter.test.js +205 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +8 -2
- package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
- package/dist/keyboard-handlers.coverage.test.js +495 -0
- package/dist/keyboard-handlers.d.ts +7 -0
- package/dist/keyboard-handlers.js +202 -0
- package/dist/menus.d.ts +40 -0
- package/dist/menus.js +390 -0
- package/dist/menus.test.d.ts +1 -0
- package/dist/menus.test.js +560 -0
- package/dist/named-views.coverage.test.d.ts +1 -0
- package/dist/named-views.coverage.test.js +180 -0
- package/dist/named-views.d.ts +27 -0
- package/dist/named-views.js +39 -0
- package/dist/row-drag.d.ts +49 -0
- package/dist/row-drag.js +221 -0
- package/dist/row-drag.test.d.ts +1 -0
- package/dist/row-drag.test.js +142 -0
- package/dist/row-resize.d.ts +43 -0
- package/dist/row-resize.js +158 -0
- package/dist/row-resize.test.d.ts +1 -0
- package/dist/row-resize.test.js +329 -0
- package/dist/scroll-sync.d.ts +9 -0
- package/dist/scroll-sync.js +89 -0
- package/dist/scroll-sync.test.d.ts +1 -0
- package/dist/scroll-sync.test.js +290 -0
- package/dist/selection.d.ts +32 -0
- package/dist/selection.js +427 -0
- package/dist/selection.multi-range.test.d.ts +1 -0
- package/dist/selection.multi-range.test.js +55 -0
- package/dist/selection.test.d.ts +1 -0
- package/dist/selection.test.js +647 -0
- package/dist/server-data-source.coverage.test.d.ts +1 -0
- package/dist/server-data-source.coverage.test.js +154 -0
- package/dist/spreadsheet.d.ts +110 -0
- package/dist/spreadsheet.js +242 -0
- package/dist/spreadsheet.test.d.ts +1 -0
- package/dist/spreadsheet.test.js +446 -0
- package/dist/summaries.d.ts +12 -0
- package/dist/summaries.js +65 -0
- package/dist/sv-grid-scrollbar.js +13 -1
- package/dist/svgrid-wrapper.types.d.ts +31 -1
- package/dist/svgrid.behavior.test.js +20 -0
- package/dist/svgrid.comments-autocomplete.test.d.ts +1 -0
- package/dist/svgrid.comments-autocomplete.test.js +96 -0
- package/dist/svgrid.context-menu.test.d.ts +1 -0
- package/dist/svgrid.context-menu.test.js +102 -0
- package/dist/svgrid.interaction.test.js +31 -0
- package/dist/svgrid.new-features.wrapper.test.js +64 -6
- package/dist/svgrid.wrapper.test.js +27 -1
- package/dist/test-setup.js +9 -3
- package/dist/virtualization/column-virtualizer.d.ts +2 -0
- package/dist/virtualization/scroll-scaling.d.ts +45 -0
- package/dist/virtualization/scroll-scaling.js +99 -0
- package/dist/virtualization/scroll-scaling.test.d.ts +1 -0
- package/dist/virtualization/scroll-scaling.test.js +127 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +2 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +2 -0
- package/dist/virtualization/virtualizer.d.ts +7 -0
- package/dist/virtualization/virtualizer.js +30 -0
- package/package.json +2 -1
- package/src/GridFooter.svelte +164 -0
- package/src/GridMenus.svelte +648 -0
- package/src/SvGrid.controller.svelte.ts +2352 -0
- package/src/SvGrid.css +2012 -0
- package/src/SvGrid.helpers.test.ts +415 -0
- package/src/SvGrid.helpers.ts +185 -0
- package/src/SvGrid.svelte +2346 -8742
- package/src/SvGrid.types.ts +537 -0
- package/src/SvGridChart.svelte +1060 -23
- package/src/aligned-grids.test.ts +80 -0
- package/src/aligned-grids.ts +87 -0
- package/src/build-api.coverage.test.ts +532 -0
- package/src/build-api.ts +683 -0
- package/src/cell-render.test.ts +451 -0
- package/src/cell-render.ts +426 -0
- package/src/cell-values.ts +114 -0
- package/src/chart-export.test.ts +370 -0
- package/src/chart.coverage.test.ts +814 -0
- package/src/chart.ts +1352 -47
- package/src/clipboard.test.ts +780 -0
- package/src/clipboard.ts +552 -0
- package/src/collaboration.coverage.test.ts +220 -0
- package/src/column-groups.test.ts +59 -0
- package/src/column-groups.ts +80 -0
- package/src/column-types.test.ts +68 -0
- package/src/column-types.ts +82 -0
- package/src/columns.test.ts +702 -0
- package/src/columns.ts +419 -0
- package/src/core.ts +86 -0
- package/src/css.d.ts +3 -0
- package/src/editing.test.ts +837 -0
- package/src/editing.ts +669 -0
- package/src/editors/cell-editors.coverage.test.ts +156 -0
- package/src/editors/cell-editors.ts +1 -0
- package/src/facet-buckets.test.ts +353 -0
- package/src/facet-buckets.ts +67 -0
- package/src/features.ts +128 -0
- package/src/filter-operators.test.ts +174 -0
- package/src/filter-operators.ts +87 -0
- package/src/hyperformula-adapter.test.ts +256 -0
- package/src/hyperformula-adapter.ts +124 -0
- package/src/index.ts +49 -0
- package/src/keyboard-handlers.coverage.test.ts +560 -0
- package/src/keyboard-handlers.ts +353 -0
- package/src/keyboard.ts +97 -97
- package/src/menus.test.ts +620 -0
- package/src/menus.ts +555 -0
- package/src/named-views.coverage.test.ts +210 -0
- package/src/named-views.ts +48 -0
- package/src/row-drag.test.ts +168 -0
- package/src/row-drag.ts +255 -0
- package/src/row-resize.test.ts +369 -0
- package/src/row-resize.ts +171 -0
- package/src/scroll-sync.test.ts +330 -0
- package/src/scroll-sync.ts +218 -0
- package/src/selection.multi-range.test.ts +61 -0
- package/src/selection.test.ts +722 -0
- package/src/selection.ts +579 -0
- package/src/server-data-source.coverage.test.ts +180 -0
- package/src/spreadsheet.test.ts +489 -0
- package/src/spreadsheet.ts +304 -0
- package/src/summaries.ts +204 -0
- package/src/sv-grid-scrollbar.ts +13 -1
- package/src/svgrid-wrapper.types.ts +31 -1
- package/src/svgrid.behavior.test.ts +22 -0
- package/src/svgrid.comments-autocomplete.test.ts +112 -0
- package/src/svgrid.context-menu.test.ts +126 -0
- package/src/svgrid.interaction.test.ts +30 -0
- package/src/svgrid.new-features.wrapper.test.ts +67 -6
- package/src/svgrid.wrapper.test.ts +27 -1
- package/src/test-setup.ts +9 -6
- package/src/virtualization/scroll-scaling.test.ts +148 -0
- package/src/virtualization/scroll-scaling.ts +121 -0
- package/src/virtualization/svelte-virtualizer.svelte.ts +2 -0
- package/src/virtualization/virtualizer.ts +26 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spreadsheet-style layout helpers — cell merging + per-cell borders.
|
|
3
|
+
*
|
|
4
|
+
* These are *layout-only* augmentations on top of `<SvGrid>`. They don't
|
|
5
|
+
* change the grid's data model, sorting, or filtering — they just decorate
|
|
6
|
+
* the rendered body cells. Bind the action to the wrapper around your
|
|
7
|
+
* SvGrid and pass merge / border specs; the helper observes DOM mutations
|
|
8
|
+
* and re-applies the layout whenever the grid re-renders.
|
|
9
|
+
*
|
|
10
|
+
* Limitations (worth knowing up front):
|
|
11
|
+
* - Merges + borders index into the CURRENTLY-DISPLAYED rows (after
|
|
12
|
+
* sort / filter), via the `data-svgrid-row` index the grid sets on
|
|
13
|
+
* every body cell. If your data sorts or filters, recompute the
|
|
14
|
+
* specs against the new display order.
|
|
15
|
+
* - Merges use real `colspan` / `rowspan` plus `display:none` on
|
|
16
|
+
* covered TDs. Column widths come from the inline styles SvGrid
|
|
17
|
+
* emits on each TD; we don't fight those.
|
|
18
|
+
* - For long row-merges across a virtualised window, only rows that
|
|
19
|
+
* are actually rendered get the rowspan applied. Disable
|
|
20
|
+
* virtualisation on grids that need a continuous merge.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** A single edge of a cell border. */
|
|
24
|
+
export type BorderSpec = {
|
|
25
|
+
/** Thickness in pixels. Default 2. */
|
|
26
|
+
width?: number
|
|
27
|
+
/** CSS border-style. Default 'solid'. */
|
|
28
|
+
style?: 'solid' | 'dashed' | 'dotted' | 'double'
|
|
29
|
+
/** CSS color. Falls back to currentColor (i.e. text color). */
|
|
30
|
+
color?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A merge declaration. The cell at (rowIndex, columnId) is the ORIGIN;
|
|
34
|
+
* it spans `colspan` columns to the right + `rowspan` rows downward.
|
|
35
|
+
* Covered cells are hidden so the origin visually fills the region. */
|
|
36
|
+
export type MergeSpec = {
|
|
37
|
+
/** Display-row index — the same value the grid puts on
|
|
38
|
+
* `data-svgrid-row`. After sorting/filtering, recompute the spec
|
|
39
|
+
* against the new display order. */
|
|
40
|
+
rowIndex: number
|
|
41
|
+
columnId: string
|
|
42
|
+
/** Default 1. */
|
|
43
|
+
rowspan?: number
|
|
44
|
+
/** Default 1. */
|
|
45
|
+
colspan?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A column with declarative spanning callbacks, as accepted by
|
|
49
|
+
* `spansToMerges`. Matches the relevant slice of `ColumnDef`. */
|
|
50
|
+
export type SpanColumn<TData = Record<string, unknown>> = {
|
|
51
|
+
id: string
|
|
52
|
+
field?: string
|
|
53
|
+
colSpan?: (params: { data: TData; rowIndex: number; columnId: string; value: unknown }) => number
|
|
54
|
+
rowSpan?: (params: { data: TData; rowIndex: number; columnId: string; value: unknown }) => number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Turn declarative per-column `colSpan` / `rowSpan` callbacks into a
|
|
59
|
+
* `MergeSpec[]` you can hand to `spreadsheetLayout` - so value-driven,
|
|
60
|
+
* AG-Grid-style spanning runs on the SAME real colspan/rowspan merge engine
|
|
61
|
+
* instead of a second code path. Recompute after sort/filter (indexes are
|
|
62
|
+
* display-row indexes). A common pattern is "merge runs of equal values":
|
|
63
|
+
*
|
|
64
|
+
* { field: 'region', rowSpan: ({ data, rowIndex }) =>
|
|
65
|
+
* rows.filter((r, i) => i >= rowIndex && r.region === data.region &&
|
|
66
|
+
* (i === rowIndex || rows[i-1].region === data.region)).length }
|
|
67
|
+
*/
|
|
68
|
+
export function spansToMerges<TData = Record<string, unknown>>(
|
|
69
|
+
rows: ReadonlyArray<TData>,
|
|
70
|
+
columns: ReadonlyArray<SpanColumn<TData>>,
|
|
71
|
+
getValue?: (row: TData, columnId: string) => unknown,
|
|
72
|
+
): MergeSpec[] {
|
|
73
|
+
const merges: MergeSpec[] = []
|
|
74
|
+
const covered = new Set<string>()
|
|
75
|
+
for (let r = 0; r < rows.length; r += 1) {
|
|
76
|
+
const row = rows[r]!
|
|
77
|
+
for (let ci = 0; ci < columns.length; ci += 1) {
|
|
78
|
+
const col = columns[ci]!
|
|
79
|
+
if (!col.colSpan && !col.rowSpan) continue
|
|
80
|
+
const key = `${r}:${ci}`
|
|
81
|
+
if (covered.has(key)) continue
|
|
82
|
+
const value = getValue
|
|
83
|
+
? getValue(row, col.id)
|
|
84
|
+
: (row as Record<string, unknown>)[col.field ?? col.id]
|
|
85
|
+
const params = { data: row, rowIndex: r, columnId: col.id, value }
|
|
86
|
+
const cs = Math.max(1, Math.floor(col.colSpan?.(params) ?? 1))
|
|
87
|
+
const rs = Math.max(1, Math.floor(col.rowSpan?.(params) ?? 1))
|
|
88
|
+
if (cs <= 1 && rs <= 1) continue
|
|
89
|
+
merges.push({
|
|
90
|
+
rowIndex: r,
|
|
91
|
+
columnId: col.id,
|
|
92
|
+
colspan: cs > 1 ? cs : undefined,
|
|
93
|
+
rowspan: rs > 1 ? rs : undefined,
|
|
94
|
+
})
|
|
95
|
+
for (let dr = 0; dr < rs; dr += 1) {
|
|
96
|
+
for (let dc = 0; dc < cs; dc += 1) {
|
|
97
|
+
if (dr === 0 && dc === 0) continue
|
|
98
|
+
covered.add(`${r + dr}:${ci + dc}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return merges
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Borders for one cell. Edges left unset render as the default
|
|
107
|
+
* cell border (i.e. no override). */
|
|
108
|
+
export type CellBorderSpec = {
|
|
109
|
+
rowIndex: number
|
|
110
|
+
columnId: string
|
|
111
|
+
top?: BorderSpec
|
|
112
|
+
right?: BorderSpec
|
|
113
|
+
bottom?: BorderSpec
|
|
114
|
+
left?: BorderSpec
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What the Svelte action receives. Pass new values to update; pass
|
|
118
|
+
* `null` / empty arrays to clear. */
|
|
119
|
+
export type SpreadsheetActionOptions = {
|
|
120
|
+
merges?: ReadonlyArray<MergeSpec> | null
|
|
121
|
+
borders?: ReadonlyArray<CellBorderSpec> | null
|
|
122
|
+
/** Column id order the grid uses, in left-to-right order. Required
|
|
123
|
+
* to translate `colspan` into the right set of covered column ids.
|
|
124
|
+
* Pass `columns.map((c) => c.id)` from the consumer. */
|
|
125
|
+
columnOrder: ReadonlyArray<string>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const BORDER_KEYS = ['top', 'right', 'bottom', 'left'] as const
|
|
129
|
+
type EdgeKey = (typeof BORDER_KEYS)[number]
|
|
130
|
+
const MARK = 'data-svgrid-sheet'
|
|
131
|
+
|
|
132
|
+
function borderCss(spec: BorderSpec | undefined): string | null {
|
|
133
|
+
if (!spec) return null
|
|
134
|
+
const w = spec.width ?? 2
|
|
135
|
+
const s = spec.style ?? 'solid'
|
|
136
|
+
const c = spec.color ?? 'currentColor'
|
|
137
|
+
return `${w}px ${s} ${c}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Look up a body cell by its display-row + column id. Falls back to
|
|
141
|
+
* null if the cell isn't in the current render window (virtualisation,
|
|
142
|
+
* scrolled out, or filtered away). */
|
|
143
|
+
function findCell(
|
|
144
|
+
root: HTMLElement,
|
|
145
|
+
rowIndex: number,
|
|
146
|
+
columnId: string,
|
|
147
|
+
): HTMLTableCellElement | null {
|
|
148
|
+
return root.querySelector<HTMLTableCellElement>(
|
|
149
|
+
`td[data-svgrid-row="${rowIndex}"][data-col-id="${CSS.escape(columnId)}"]`,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const OVERLAY_CLASS = 'sv-cell-border-overlay'
|
|
154
|
+
|
|
155
|
+
/** Apply / re-apply layout decorations. Called once on mount, again on
|
|
156
|
+
* every relevant DOM mutation inside the grid, and whenever the action
|
|
157
|
+
* options update. */
|
|
158
|
+
function apply(root: HTMLElement, opts: SpreadsheetActionOptions): void {
|
|
159
|
+
// ---- 1. Clean up anything the LAST run added -----------------------
|
|
160
|
+
for (const td of root.querySelectorAll<HTMLTableCellElement>(`td[${MARK}]`)) {
|
|
161
|
+
td.removeAttribute('colspan')
|
|
162
|
+
td.removeAttribute('rowspan')
|
|
163
|
+
if (td.style.display === 'none') td.style.display = ''
|
|
164
|
+
// Remove the overlay child we previously injected for borders.
|
|
165
|
+
const overlay = td.querySelector(`:scope > .${OVERLAY_CLASS}`)
|
|
166
|
+
if (overlay) overlay.remove()
|
|
167
|
+
td.classList.remove('sv-merge-edge-right', 'sv-merge-edge-bottom', 'sv-merge-in-range')
|
|
168
|
+
td.removeAttribute(MARK)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const colOrder = opts.columnOrder
|
|
172
|
+
const colIndexOf = new Map<string, number>()
|
|
173
|
+
for (let i = 0; i < colOrder.length; i += 1) colIndexOf.set(colOrder[i]!, i)
|
|
174
|
+
|
|
175
|
+
// ---- 2. Merges -----------------------------------------------------
|
|
176
|
+
for (const m of opts.merges ?? []) {
|
|
177
|
+
const startCol = colIndexOf.get(m.columnId)
|
|
178
|
+
if (startCol === undefined) continue
|
|
179
|
+
const rs = Math.max(1, m.rowspan ?? 1)
|
|
180
|
+
const cs = Math.max(1, m.colspan ?? 1)
|
|
181
|
+
const origin = findCell(root, m.rowIndex, m.columnId)
|
|
182
|
+
if (!origin) continue
|
|
183
|
+
if (cs > 1) origin.setAttribute('colspan', String(cs))
|
|
184
|
+
if (rs > 1) origin.setAttribute('rowspan', String(rs))
|
|
185
|
+
origin.setAttribute(MARK, '')
|
|
186
|
+
for (let dr = 0; dr < rs; dr += 1) {
|
|
187
|
+
for (let dc = 0; dc < cs; dc += 1) {
|
|
188
|
+
if (dr === 0 && dc === 0) continue
|
|
189
|
+
const colId = colOrder[startCol + dc]
|
|
190
|
+
if (!colId) continue
|
|
191
|
+
const td = findCell(root, m.rowIndex + dr, colId)
|
|
192
|
+
if (!td) continue
|
|
193
|
+
td.style.display = 'none'
|
|
194
|
+
td.setAttribute(MARK, '')
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Selection-edge inheritance:
|
|
199
|
+
// The grid sets `data-range-*` per cell. The merge's RIGHTMOST /
|
|
200
|
+
// BOTTOMMOST covered cells are `display:none`, so the borders
|
|
201
|
+
// they would draw never render. Mirror those edge flags onto the
|
|
202
|
+
// origin via dedicated CSS classes (so we can toggle them
|
|
203
|
+
// independently of Svelte's own data-range-* updates without a
|
|
204
|
+
// ping-pong loop).
|
|
205
|
+
const lastColId = colOrder[startCol + cs - 1]
|
|
206
|
+
const rightCell = lastColId ? findCell(root, m.rowIndex, lastColId) : null
|
|
207
|
+
const bottomCell = findCell(root, m.rowIndex + rs - 1, m.columnId)
|
|
208
|
+
const farCell = lastColId ? findCell(root, m.rowIndex + rs - 1, lastColId) : null
|
|
209
|
+
|
|
210
|
+
const wantRight =
|
|
211
|
+
rightCell?.getAttribute('data-range-right') === 'true' ||
|
|
212
|
+
farCell ?.getAttribute('data-range-right') === 'true'
|
|
213
|
+
const wantBottom =
|
|
214
|
+
bottomCell?.getAttribute('data-range-bottom') === 'true' ||
|
|
215
|
+
farCell ?.getAttribute('data-range-bottom') === 'true'
|
|
216
|
+
const inRange =
|
|
217
|
+
rightCell ?.getAttribute('data-selected-range') === 'true' ||
|
|
218
|
+
bottomCell?.getAttribute('data-selected-range') === 'true' ||
|
|
219
|
+
farCell ?.getAttribute('data-selected-range') === 'true' ||
|
|
220
|
+
origin.getAttribute('data-selected-range') === 'true'
|
|
221
|
+
|
|
222
|
+
origin.classList.toggle('sv-merge-edge-right', wantRight)
|
|
223
|
+
origin.classList.toggle('sv-merge-edge-bottom', wantBottom)
|
|
224
|
+
origin.classList.toggle('sv-merge-in-range', inRange)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- 3. Borders ---------------------------------------------------
|
|
228
|
+
// Use an absolute-positioned overlay div so each edge renders
|
|
229
|
+
// independently of the grid's own border-collapse rules. Adjacent
|
|
230
|
+
// TDs no longer "eat" the right / bottom edges of a bordered cell.
|
|
231
|
+
for (const b of opts.borders ?? []) {
|
|
232
|
+
const td = findCell(root, b.rowIndex, b.columnId)
|
|
233
|
+
if (!td) continue
|
|
234
|
+
let any = false
|
|
235
|
+
const overlay = document.createElement('div')
|
|
236
|
+
overlay.className = OVERLAY_CLASS
|
|
237
|
+
overlay.style.cssText =
|
|
238
|
+
'position:absolute;inset:0;pointer-events:none;box-sizing:border-box;z-index:1;'
|
|
239
|
+
for (const e of BORDER_KEYS) {
|
|
240
|
+
const css = borderCss(b[e as EdgeKey])
|
|
241
|
+
if (!css) continue
|
|
242
|
+
overlay.style.setProperty(`border-${e}`, css)
|
|
243
|
+
any = true
|
|
244
|
+
}
|
|
245
|
+
if (!any) continue
|
|
246
|
+
// Ensure the TD is a positioning context for the overlay.
|
|
247
|
+
if (getComputedStyle(td).position === 'static') td.style.position = 'relative'
|
|
248
|
+
td.appendChild(overlay)
|
|
249
|
+
td.setAttribute(MARK, '')
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Svelte action. Attach to the element that hosts your `<SvGrid>` so
|
|
254
|
+
* the action can watch its DOM for re-renders.
|
|
255
|
+
*
|
|
256
|
+
* ```svelte
|
|
257
|
+
* <div use:spreadsheetLayout={{ merges, borders, columnOrder }}>
|
|
258
|
+
* <SvGrid {data} {columns} ... />
|
|
259
|
+
* </div>
|
|
260
|
+
* ```
|
|
261
|
+
*
|
|
262
|
+
* The action re-applies the layout whenever the grid's body mutates
|
|
263
|
+
* (new rows, column reorder, virtualization scroll) and whenever the
|
|
264
|
+
* options change. */
|
|
265
|
+
export function spreadsheetLayout(node: HTMLElement, opts: SpreadsheetActionOptions) {
|
|
266
|
+
let current = opts
|
|
267
|
+
let frame = 0
|
|
268
|
+
// The MutationObserver fires for EVERY DOM change inside the grid -
|
|
269
|
+
// batch them into one rAF so a big virtualization scroll doesn't run
|
|
270
|
+
// apply() dozens of times in a tick.
|
|
271
|
+
function schedule() {
|
|
272
|
+
if (frame) return
|
|
273
|
+
frame = requestAnimationFrame(() => {
|
|
274
|
+
frame = 0
|
|
275
|
+
apply(node, current)
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
const observer = new MutationObserver(schedule)
|
|
279
|
+
observer.observe(node, {
|
|
280
|
+
childList: true,
|
|
281
|
+
subtree: true,
|
|
282
|
+
attributes: true,
|
|
283
|
+
attributeFilter: [
|
|
284
|
+
'data-svgrid-row', 'data-col-id', 'style',
|
|
285
|
+
// Watch selection-range attrs so we can transfer them from
|
|
286
|
+
// hidden covered cells to merge origins.
|
|
287
|
+
'data-range-top', 'data-range-bottom', 'data-range-left',
|
|
288
|
+
'data-range-right', 'data-selected-range',
|
|
289
|
+
],
|
|
290
|
+
})
|
|
291
|
+
// First-paint pass: schedule the same way so we don't run before the
|
|
292
|
+
// grid's initial render landed.
|
|
293
|
+
schedule()
|
|
294
|
+
return {
|
|
295
|
+
update(next: SpreadsheetActionOptions) {
|
|
296
|
+
current = next
|
|
297
|
+
schedule()
|
|
298
|
+
},
|
|
299
|
+
destroy() {
|
|
300
|
+
observer.disconnect()
|
|
301
|
+
if (frame) cancelAnimationFrame(frame)
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
}
|
package/src/summaries.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// summaries handlers extracted from the controller. Imperative event handlers
|
|
2
|
+
// reading/writing controller state via the `ctx` handle; the reactive core
|
|
3
|
+
// ($state/$derived/$effect) stays in the controller.
|
|
4
|
+
import {
|
|
5
|
+
applyExcelFilter,
|
|
6
|
+
normalizeForFilter,
|
|
7
|
+
createColumnVirtualizer,
|
|
8
|
+
createCoreRowModel,
|
|
9
|
+
createExpandedRowModel,
|
|
10
|
+
createFilteredRowModel,
|
|
11
|
+
createGroupedRowModel,
|
|
12
|
+
createPaginatedRowModel,
|
|
13
|
+
createSvelteVirtualizer,
|
|
14
|
+
createSortedRowModel,
|
|
15
|
+
createSvGrid,
|
|
16
|
+
filterFns,
|
|
17
|
+
getGridCellA11yProps,
|
|
18
|
+
getGridCellDomId,
|
|
19
|
+
getGridHeaderA11yProps,
|
|
20
|
+
getGridRootA11yProps,
|
|
21
|
+
getGridRowA11yProps,
|
|
22
|
+
parseEditorValue,
|
|
23
|
+
normalizeEditorOptions,
|
|
24
|
+
sortFns,
|
|
25
|
+
tableFeatures,
|
|
26
|
+
rowSortingFeature,
|
|
27
|
+
columnFilteringFeature,
|
|
28
|
+
columnGroupingFeature,
|
|
29
|
+
type CellContext,
|
|
30
|
+
type EditorContext,
|
|
31
|
+
type CellEditorOption,
|
|
32
|
+
type CellEditorType,
|
|
33
|
+
type CellFormatter,
|
|
34
|
+
type CellFormatConfig,
|
|
35
|
+
type Column,
|
|
36
|
+
type ColumnDef,
|
|
37
|
+
type Row,
|
|
38
|
+
type RowData,
|
|
39
|
+
type SvGridApi,
|
|
40
|
+
type TableFeatures,
|
|
41
|
+
} from "./index";
|
|
42
|
+
import "./sv-grid-scrollbar";
|
|
43
|
+
import type { Snippet } from "svelte";
|
|
44
|
+
import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
|
|
45
|
+
import {
|
|
46
|
+
formatNumericWithConfig,
|
|
47
|
+
getDateFormatter,
|
|
48
|
+
resolveDatePattern,
|
|
49
|
+
} from "./cell-formatting";
|
|
50
|
+
import {
|
|
51
|
+
RenderSnippetConfig,
|
|
52
|
+
RenderComponentConfig,
|
|
53
|
+
} from "./render-component";
|
|
54
|
+
import { buildFillPattern } from "./fill-patterns";
|
|
55
|
+
import { buildSparkline, toSparklineValues } from "./sparkline";
|
|
56
|
+
import {
|
|
57
|
+
resolveCellFormat,
|
|
58
|
+
computeColumnStat,
|
|
59
|
+
formatsNeedingStats,
|
|
60
|
+
type ColumnStat,
|
|
61
|
+
type ResolvedCellFormat,
|
|
62
|
+
} from "./conditional-formatting";
|
|
63
|
+
import SvGridDropdown from "./SvGridDropdown.svelte";
|
|
64
|
+
import type {
|
|
65
|
+
Props,
|
|
66
|
+
SelectionPoint,
|
|
67
|
+
SelectionRange,
|
|
68
|
+
CellEditState,
|
|
69
|
+
FilterOperator,
|
|
70
|
+
FilterOption,
|
|
71
|
+
MenuPosition,
|
|
72
|
+
} from "./SvGrid.types";
|
|
73
|
+
import {
|
|
74
|
+
cfTextStyle,
|
|
75
|
+
fmtStat,
|
|
76
|
+
getCellKey,
|
|
77
|
+
resolveClassList,
|
|
78
|
+
toDateInputValue,
|
|
79
|
+
toDateTimeLocalInputValue,
|
|
80
|
+
getEditableInputValue,
|
|
81
|
+
getEditorInputType,
|
|
82
|
+
toValueArray,
|
|
83
|
+
getOptionLabel,
|
|
84
|
+
getOptionColor,
|
|
85
|
+
colorfulChipStyle,
|
|
86
|
+
getEditorClass,
|
|
87
|
+
asDate,
|
|
88
|
+
clampMenuX,
|
|
89
|
+
cssEscape,
|
|
90
|
+
rawToNumber,
|
|
91
|
+
formatFacetNumber,
|
|
92
|
+
formatFacetDate,
|
|
93
|
+
} from "./SvGrid.helpers";
|
|
94
|
+
import { createMenus } from "./menus";
|
|
95
|
+
import { createCellRender } from "./cell-render";
|
|
96
|
+
import { createEditing } from "./editing";
|
|
97
|
+
import { createSelection } from "./selection";
|
|
98
|
+
import { createColumns } from "./columns";
|
|
99
|
+
import { createGridApi } from "./build-api";
|
|
100
|
+
import { createClipboard } from "./clipboard";
|
|
101
|
+
import {
|
|
102
|
+
filterOperatorOptions,
|
|
103
|
+
fallbackOperatorOption,
|
|
104
|
+
TEXT_OPERATORS,
|
|
105
|
+
NUMBER_OPERATORS,
|
|
106
|
+
DATE_OPERATORS,
|
|
107
|
+
CHECKBOX_OPERATORS,
|
|
108
|
+
operatorOption,
|
|
109
|
+
operatorsForColumn,
|
|
110
|
+
defaultOperatorFor,
|
|
111
|
+
operatorLabelFor,
|
|
112
|
+
} from "./filter-operators";
|
|
113
|
+
import {
|
|
114
|
+
type FacetBucket,
|
|
115
|
+
isBucketableColumn,
|
|
116
|
+
buildBuckets,
|
|
117
|
+
isInBucket,
|
|
118
|
+
} from "./facet-buckets";
|
|
119
|
+
import {
|
|
120
|
+
getColumnBaseValue,
|
|
121
|
+
isGroupRow,
|
|
122
|
+
toolPanelHeaderLabel,
|
|
123
|
+
formatSummaryNumeric,
|
|
124
|
+
getColumnAlign,
|
|
125
|
+
getPinnedCellValue,
|
|
126
|
+
getColumnAccessorValue,
|
|
127
|
+
columnDefMatchesId,
|
|
128
|
+
} from "./cell-values";
|
|
129
|
+
|
|
130
|
+
export function createSummaries<
|
|
131
|
+
TFeatures extends TableFeatures = TableFeatures,
|
|
132
|
+
TData extends RowData = RowData,
|
|
133
|
+
>(ctx: any) {
|
|
134
|
+
/**
|
|
135
|
+
* Aggregate every row into a per-column footer summary (sum for numeric
|
|
136
|
+
* columns, `Count: N` otherwise). This loop is the hottest path on a
|
|
137
|
+
* large grid - it is rows x columns iterations - so two things keep the
|
|
138
|
+
* constant factor down:
|
|
139
|
+
*
|
|
140
|
+
* 1. The edited-cell overlay is only consulted when an edit actually
|
|
141
|
+
* exists. `key in editedCellValues` hits a reactive-proxy `has`
|
|
142
|
+
* trap for every cell otherwise - 5M no-op trap calls on a
|
|
143
|
+
* 100k x 50 grid. Skipping it when the map is empty is the single
|
|
144
|
+
* biggest win here.
|
|
145
|
+
* 2. The column's accessor (`accessorFn` / `field`) is resolved once
|
|
146
|
+
* per column, not re-read off `columnDef` for every cell.
|
|
147
|
+
*/
|
|
148
|
+
function computeSummaries(
|
|
149
|
+
rows: ReadonlyArray<Row<TData>>,
|
|
150
|
+
columns: ReadonlyArray<Column<TData>>,
|
|
151
|
+
): Record<string, string> {
|
|
152
|
+
const summary: Record<string, string> = {};
|
|
153
|
+
const rowCount = rows.length;
|
|
154
|
+
const hasEdits = Object.keys(ctx.editedCellValues).length > 0;
|
|
155
|
+
for (const column of columns) {
|
|
156
|
+
const def = column.columnDef;
|
|
157
|
+
const accessorFn = def.accessorFn;
|
|
158
|
+
const field = def.field;
|
|
159
|
+
const columnId = column.id;
|
|
160
|
+
let numericSum = 0;
|
|
161
|
+
let numericCount = 0;
|
|
162
|
+
for (let i = 0; i < rowCount; i += 1) {
|
|
163
|
+
const row = rows[i]!;
|
|
164
|
+
let value: unknown;
|
|
165
|
+
const base = accessorFn
|
|
166
|
+
? accessorFn(row.original)
|
|
167
|
+
: field
|
|
168
|
+
? (row.original as Record<string, unknown>)[field]
|
|
169
|
+
: row.getCellValueByColumnId(columnId);
|
|
170
|
+
if (hasEdits) {
|
|
171
|
+
const key = getCellKey(row.id, columnId);
|
|
172
|
+
value = key in ctx.editedCellValues ? ctx.editedCellValues[key] : base;
|
|
173
|
+
} else {
|
|
174
|
+
value = base;
|
|
175
|
+
}
|
|
176
|
+
const asNumber = Number(value);
|
|
177
|
+
if (Number.isFinite(asNumber)) {
|
|
178
|
+
numericSum += asNumber;
|
|
179
|
+
numericCount += 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
summary[columnId] =
|
|
183
|
+
numericCount > 0
|
|
184
|
+
? formatSummaryNumeric(column, numericSum)
|
|
185
|
+
: `Count: ${rowCount}`;
|
|
186
|
+
}
|
|
187
|
+
return summary;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hasRenderedColumn(entry: {
|
|
191
|
+
item: (typeof ctx.renderedColumnItems)[number];
|
|
192
|
+
column: (typeof ctx.allColumns)[number] | undefined;
|
|
193
|
+
}): entry is {
|
|
194
|
+
item: (typeof ctx.renderedColumnItems)[number];
|
|
195
|
+
column: (typeof ctx.allColumns)[number];
|
|
196
|
+
} {
|
|
197
|
+
return entry.column !== undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
computeSummaries,
|
|
202
|
+
hasRenderedColumn,
|
|
203
|
+
};
|
|
204
|
+
}
|
package/src/sv-grid-scrollbar.ts
CHANGED
|
@@ -34,7 +34,11 @@ const STYLE_TEMPLATE = `
|
|
|
34
34
|
display: flex;
|
|
35
35
|
user-select: none;
|
|
36
36
|
background: var(--sg-scrollbar-bg, #eef2f8);
|
|
37
|
-
|
|
37
|
+
/* The 1px separator is drawn on the INNER edge only (the edge facing
|
|
38
|
+
the grid body) via the orientation rules below. A full inset box
|
|
39
|
+
here doubled up with the grid's own frame border on the outer edge,
|
|
40
|
+
reading as a 2px border - most visible in dense-gridline themes
|
|
41
|
+
like Excel, which should show a single 1px line. */
|
|
38
42
|
/* Start hidden + non-interactive - viewport/content sizes are 0 until
|
|
39
43
|
after the first layout pass, so without this default the scrollbar
|
|
40
44
|
paints visible for one frame and then JS hides it on mount. That
|
|
@@ -48,10 +52,18 @@ const STYLE_TEMPLATE = `
|
|
|
48
52
|
:host([orientation="vertical"]) {
|
|
49
53
|
flex-direction: column;
|
|
50
54
|
width: ${ARROW_SIZE}px;
|
|
55
|
+
/* LTR: vertical scrollbar sits at the right, inner edge is the left. */
|
|
56
|
+
box-shadow: inset 1px 0 0 0 var(--sg-scrollbar-border, rgba(15, 23, 42, 0.04));
|
|
57
|
+
}
|
|
58
|
+
:host([orientation="vertical"]:dir(rtl)) {
|
|
59
|
+
/* RTL flips the scrollbar to the left, so the inner edge is the right. */
|
|
60
|
+
box-shadow: inset -1px 0 0 0 var(--sg-scrollbar-border, rgba(15, 23, 42, 0.04));
|
|
51
61
|
}
|
|
52
62
|
:host([orientation="horizontal"]) {
|
|
53
63
|
flex-direction: row;
|
|
54
64
|
height: ${ARROW_SIZE}px;
|
|
65
|
+
/* Horizontal scrollbar sits at the bottom, inner edge is the top. */
|
|
66
|
+
box-shadow: inset 0 1px 0 0 var(--sg-scrollbar-border, rgba(15, 23, 42, 0.04));
|
|
55
67
|
}
|
|
56
68
|
.arrow {
|
|
57
69
|
flex: none;
|
|
@@ -64,6 +64,16 @@ export type SvGridApi<
|
|
|
64
64
|
getCellValue(rowIndex: number, columnId: string): unknown
|
|
65
65
|
/** Write a cell value through the column's field. */
|
|
66
66
|
setCellValue(rowIndex: number, columnId: string, value: unknown): void
|
|
67
|
+
/**
|
|
68
|
+
* Programmatically begin editing a cell (as a double-click would). Returns
|
|
69
|
+
* `true` if editing started (cell exists, editable, editing enabled).
|
|
70
|
+
*/
|
|
71
|
+
startEditing(rowIndex: number, columnId: string): boolean
|
|
72
|
+
/**
|
|
73
|
+
* Commit (default) or, with `cancel: true`, discard the active edit.
|
|
74
|
+
* Returns `true` if there was an edit in progress.
|
|
75
|
+
*/
|
|
76
|
+
stopEditing(cancel?: boolean): boolean
|
|
67
77
|
|
|
68
78
|
// ----- Cell selection -----
|
|
69
79
|
/**
|
|
@@ -130,6 +140,15 @@ export type SvGridApi<
|
|
|
130
140
|
value?: string
|
|
131
141
|
/** Upper bound for the `between` operator. Required when `operator === 'between'`. */
|
|
132
142
|
valueTo?: string
|
|
143
|
+
/**
|
|
144
|
+
* Optional SECOND condition on the same column, joined by `join`
|
|
145
|
+
* (multi-condition filtering, e.g. "> 100 AND < 500").
|
|
146
|
+
*/
|
|
147
|
+
operator2?: SvGridFilterOperator
|
|
148
|
+
value2?: string
|
|
149
|
+
valueTo2?: string
|
|
150
|
+
/** How to combine the two conditions. Defaults to `'AND'`. */
|
|
151
|
+
join?: 'AND' | 'OR'
|
|
133
152
|
}
|
|
134
153
|
| null,
|
|
135
154
|
): void
|
|
@@ -196,6 +215,15 @@ export type SvGridApi<
|
|
|
196
215
|
* default. Useful for "save view" + URL persistence.
|
|
197
216
|
*/
|
|
198
217
|
getColumnWidths(): Record<string, number>
|
|
218
|
+
/**
|
|
219
|
+
* Snap one column's width to its widest visible cell (header text +
|
|
220
|
+
* any rendered body cell). Equivalent to double-clicking the column's
|
|
221
|
+
* resize handle. The grid also exposes this through the column menu's
|
|
222
|
+
* "Autosize" item.
|
|
223
|
+
*/
|
|
224
|
+
autosizeColumn(columnId: string): void
|
|
225
|
+
/** Run `autosizeColumn` on every column. */
|
|
226
|
+
autosizeAllColumns(): void
|
|
199
227
|
/**
|
|
200
228
|
* Replace the column-pinning state in one call. Each entry is a
|
|
201
229
|
* column id; the order in the array becomes the visible order along
|
|
@@ -366,7 +394,9 @@ export type SvGridWrapperProps<
|
|
|
366
394
|
showRowSelection?: boolean
|
|
367
395
|
showPagination?: boolean
|
|
368
396
|
virtualization?: boolean
|
|
369
|
-
|
|
397
|
+
/** Row height in pixels. Pass a function `(rowIndex) => px` for per-row
|
|
398
|
+
* variable heights (e.g. when wiring up an interactive row-resize). */
|
|
399
|
+
rowHeight?: number | ((rowIndex: number) => number)
|
|
370
400
|
overscan?: number
|
|
371
401
|
containerHeight?: number
|
|
372
402
|
columnVirtualization?: boolean
|
|
@@ -441,6 +441,28 @@ describe('SvGrid - column add / remove / visibility', () => {
|
|
|
441
441
|
destroy()
|
|
442
442
|
}
|
|
443
443
|
})
|
|
444
|
+
|
|
445
|
+
it('columns marked `visible: false` start hidden but stay listed', async () => {
|
|
446
|
+
const columns: ColumnDef<typeof fullFeatures, Person>[] = personColumns.map(
|
|
447
|
+
(c) => (c.field === 'team' ? { ...c, visible: false } : c),
|
|
448
|
+
)
|
|
449
|
+
const { api, destroy } = await mountGrid({ columns })
|
|
450
|
+
try {
|
|
451
|
+
// Starts hidden...
|
|
452
|
+
expect(api.isColumnVisible('team')).toBe(false)
|
|
453
|
+
// ...but is still listed in getColumns() (so a Choose Columns UI
|
|
454
|
+
// can offer it), just with visible=false.
|
|
455
|
+
const team = api.getColumns().find((c) => c.id === 'team')
|
|
456
|
+
expect(team).toBeDefined()
|
|
457
|
+
expect(team?.visible).toBe(false)
|
|
458
|
+
// A later user toggle wins over the initial flag.
|
|
459
|
+
api.setColumnVisible('team', true)
|
|
460
|
+
await tick()
|
|
461
|
+
expect(api.isColumnVisible('team')).toBe(true)
|
|
462
|
+
} finally {
|
|
463
|
+
destroy()
|
|
464
|
+
}
|
|
465
|
+
})
|
|
444
466
|
})
|
|
445
467
|
|
|
446
468
|
describe('SvGrid - row add / remove', () => {
|