@svgrid/grid 1.1.0 → 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/dist/GridMenus.svelte +205 -127
- package/dist/SvGrid.controller.svelte.d.ts +44 -1
- package/dist/SvGrid.controller.svelte.js +223 -38
- package/dist/SvGrid.css +2012 -1709
- package/dist/SvGrid.helpers.test.d.ts +1 -0
- package/dist/SvGrid.helpers.test.js +298 -0
- package/dist/SvGrid.svelte +2346 -2043
- package/dist/SvGrid.types.d.ts +91 -4
- 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.js +59 -29
- package/dist/cell-render.test.d.ts +1 -0
- package/dist/cell-render.test.js +338 -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/clipboard.js +88 -24
- 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.test.d.ts +1 -0
- package/dist/columns.test.js +625 -0
- package/dist/core.d.ts +85 -0
- package/dist/editing.d.ts +7 -0
- package/dist/editing.js +191 -5
- 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/facet-buckets.test.d.ts +1 -0
- package/dist/facet-buckets.test.js +296 -0
- package/dist/filter-operators.test.d.ts +1 -0
- package/dist/filter-operators.test.js +135 -0
- package/dist/hyperformula-adapter.test.d.ts +1 -0
- package/dist/hyperformula-adapter.test.js +205 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
- package/dist/keyboard-handlers.coverage.test.js +495 -0
- package/dist/keyboard-handlers.js +7 -2
- package/dist/menus.js +1 -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/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.test.d.ts +1 -0
- package/dist/row-resize.test.js +329 -0
- package/dist/scroll-sync.js +3 -0
- package/dist/scroll-sync.test.d.ts +1 -0
- package/dist/scroll-sync.test.js +290 -0
- package/dist/selection.d.ts +7 -1
- package/dist/selection.js +76 -36
- 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 +30 -0
- package/dist/spreadsheet.js +48 -0
- package/dist/spreadsheet.test.d.ts +1 -0
- package/dist/spreadsheet.test.js +446 -0
- package/dist/sv-grid-scrollbar.js +13 -1
- package/dist/svgrid-wrapper.types.d.ts +19 -0
- package/dist/svgrid.behavior.test.js +20 -0
- package/dist/svgrid.interaction.test.js +31 -0
- package/dist/svgrid.new-features.wrapper.test.js +34 -2
- package/dist/test-setup.js +9 -3
- package/dist/virtualization/scroll-scaling.d.ts +17 -0
- package/dist/virtualization/scroll-scaling.js +35 -0
- package/dist/virtualization/scroll-scaling.test.js +42 -1
- package/package.json +2 -1
- package/src/GridMenus.svelte +205 -127
- package/src/SvGrid.controller.svelte.ts +2352 -2195
- package/src/SvGrid.css +2012 -1747
- package/src/SvGrid.svelte +2346 -2047
- package/src/SvGrid.types.ts +537 -456
- package/src/aligned-grids.test.ts +80 -0
- package/src/aligned-grids.ts +87 -0
- package/src/build-api.ts +683 -663
- package/src/clipboard.test.ts +49 -0
- package/src/clipboard.ts +51 -23
- 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/core.ts +78 -0
- package/src/editing.ts +669 -513
- package/src/index.ts +12 -0
- package/src/menus.ts +1 -0
- package/src/row-drag.test.ts +168 -0
- package/src/row-drag.ts +255 -0
- package/src/scroll-sync.ts +2 -0
- package/src/selection.multi-range.test.ts +61 -0
- package/src/selection.ts +71 -37
- package/src/spreadsheet.test.ts +489 -445
- package/src/spreadsheet.ts +304 -246
- package/src/svgrid-wrapper.types.ts +19 -0
- package/src/svgrid.new-features.wrapper.test.ts +2 -2
|
@@ -8,6 +8,23 @@ export type RowScrollScaling = {
|
|
|
8
8
|
/** Map a virtualizer logical offset back into a DOM scrollTop. */
|
|
9
9
|
logicalToDom(logical: number): number;
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the usable max element height from the two raw DOM signals.
|
|
13
|
+
*
|
|
14
|
+
* `layoutCap` is a tall probe's clamped `offsetHeight` - the height layout
|
|
15
|
+
* assigns the element. `scrollCap` is the `scrollHeight` a real `overflow:auto`
|
|
16
|
+
* container exposes for that same probe - the height the user can actually
|
|
17
|
+
* scroll through. They differ on mobile / high-DPR engines, which report a
|
|
18
|
+
* generous `offsetHeight` but then expose a SMALLER scrollable range (the
|
|
19
|
+
* physical limit is in device px, so a 3x-DPR phone has ~1/3 the CSS-px scroll
|
|
20
|
+
* cap). Trusting `offsetHeight` alone is exactly what strands the last rows of
|
|
21
|
+
* a huge grid on a phone, so we take the smaller of the two.
|
|
22
|
+
*
|
|
23
|
+
* A junk reading (<= the sanity floor, or so large the browser clearly didn't
|
|
24
|
+
* clamp) is dropped; if neither signal survives, the caller's conservative
|
|
25
|
+
* `fallback` is returned unchanged.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveMaxDomHeight(layoutCap: number, scrollCap: number, fallback: number): number;
|
|
11
28
|
/**
|
|
12
29
|
* Build the scaling mapping for one axis.
|
|
13
30
|
*
|
|
@@ -21,6 +21,41 @@ function clamp01(value) {
|
|
|
21
21
|
return 1;
|
|
22
22
|
return value;
|
|
23
23
|
}
|
|
24
|
+
/** Below this a reading is junk (0 from jsdom, a sub-cap layout quirk, etc). */
|
|
25
|
+
const MAX_DOM_HEIGHT_SANITY_FLOOR = 100000;
|
|
26
|
+
/** At/above this the browser didn't clamp (returned ~the 1e9 we asked for). */
|
|
27
|
+
const MAX_DOM_HEIGHT_SANITY_CEIL = 900000000;
|
|
28
|
+
/**
|
|
29
|
+
* Shave a hair off the detected cap so the spacer never sits at the exact
|
|
30
|
+
* physical edge, where sub-pixel rounding could leave the final row a touch
|
|
31
|
+
* out of reach. 0.5% is invisible at desktop caps (~33.5M -> ~33.3M) yet
|
|
32
|
+
* keeps a safety gap proportional to the cap.
|
|
33
|
+
*/
|
|
34
|
+
const MAX_DOM_HEIGHT_SAFETY = 0.995;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the usable max element height from the two raw DOM signals.
|
|
37
|
+
*
|
|
38
|
+
* `layoutCap` is a tall probe's clamped `offsetHeight` - the height layout
|
|
39
|
+
* assigns the element. `scrollCap` is the `scrollHeight` a real `overflow:auto`
|
|
40
|
+
* container exposes for that same probe - the height the user can actually
|
|
41
|
+
* scroll through. They differ on mobile / high-DPR engines, which report a
|
|
42
|
+
* generous `offsetHeight` but then expose a SMALLER scrollable range (the
|
|
43
|
+
* physical limit is in device px, so a 3x-DPR phone has ~1/3 the CSS-px scroll
|
|
44
|
+
* cap). Trusting `offsetHeight` alone is exactly what strands the last rows of
|
|
45
|
+
* a huge grid on a phone, so we take the smaller of the two.
|
|
46
|
+
*
|
|
47
|
+
* A junk reading (<= the sanity floor, or so large the browser clearly didn't
|
|
48
|
+
* clamp) is dropped; if neither signal survives, the caller's conservative
|
|
49
|
+
* `fallback` is returned unchanged.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveMaxDomHeight(layoutCap, scrollCap, fallback) {
|
|
52
|
+
const usable = [layoutCap, scrollCap].filter((v) => Number.isFinite(v) &&
|
|
53
|
+
v > MAX_DOM_HEIGHT_SANITY_FLOOR &&
|
|
54
|
+
v < MAX_DOM_HEIGHT_SANITY_CEIL);
|
|
55
|
+
if (usable.length === 0)
|
|
56
|
+
return fallback;
|
|
57
|
+
return Math.floor(Math.min(...usable) * MAX_DOM_HEIGHT_SAFETY);
|
|
58
|
+
}
|
|
24
59
|
/**
|
|
25
60
|
* Build the scaling mapping for one axis.
|
|
26
61
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createRowScrollScaling } from './scroll-scaling';
|
|
2
|
+
import { createRowScrollScaling, resolveMaxDomHeight } from './scroll-scaling';
|
|
3
3
|
const VIEWPORT = 600;
|
|
4
|
+
const FALLBACK = 8000000;
|
|
4
5
|
describe('createRowScrollScaling - inert (content fits under the cap)', () => {
|
|
5
6
|
// 1M rows * 18px = 18M, under a 33.5M (Chrome) cap -> no scaling.
|
|
6
7
|
const s = createRowScrollScaling(18000000, 33554400, VIEWPORT);
|
|
@@ -62,6 +63,46 @@ describe('createRowScrollScaling - active (content exceeds the cap)', () => {
|
|
|
62
63
|
}
|
|
63
64
|
});
|
|
64
65
|
});
|
|
66
|
+
describe('resolveMaxDomHeight - picks the smaller, real scrollable cap', () => {
|
|
67
|
+
it('keeps the desktop cap when both signals agree', () => {
|
|
68
|
+
// Chrome ~33.5M; offsetHeight and scrollHeight match. 0.5% safety shave.
|
|
69
|
+
const cap = resolveMaxDomHeight(33554400, 33554400, FALLBACK);
|
|
70
|
+
expect(cap).toBeLessThan(33554400);
|
|
71
|
+
expect(cap).toBeGreaterThan(33000000);
|
|
72
|
+
});
|
|
73
|
+
it('uses the SCROLL cap when a phone over-reports offsetHeight', () => {
|
|
74
|
+
// High-DPR mobile: layout says 24M, but the container only scrolls ~6M.
|
|
75
|
+
// Trusting offsetHeight is exactly what stranded the last rows.
|
|
76
|
+
const cap = resolveMaxDomHeight(24000000, 6000000, FALLBACK);
|
|
77
|
+
expect(cap).toBeLessThanOrEqual(6000000);
|
|
78
|
+
expect(cap).toBeGreaterThan(5900000);
|
|
79
|
+
});
|
|
80
|
+
it('shaves a safety margin so the spacer never sits at the exact edge', () => {
|
|
81
|
+
const cap = resolveMaxDomHeight(10000000, 10000000, FALLBACK);
|
|
82
|
+
expect(cap).toBeLessThan(10000000);
|
|
83
|
+
});
|
|
84
|
+
it('falls back when both readings are junk (jsdom returns 0)', () => {
|
|
85
|
+
expect(resolveMaxDomHeight(0, 0, FALLBACK)).toBe(FALLBACK);
|
|
86
|
+
});
|
|
87
|
+
it('falls back when the browser did not clamp (returned ~1e9)', () => {
|
|
88
|
+
expect(resolveMaxDomHeight(1000000000, 1000000000, FALLBACK)).toBe(FALLBACK);
|
|
89
|
+
});
|
|
90
|
+
it('ignores a junk signal but trusts the valid one', () => {
|
|
91
|
+
// offsetHeight unclamped/garbage, scrollHeight is the real cap.
|
|
92
|
+
const cap = resolveMaxDomHeight(1000000000, 5000000, FALLBACK);
|
|
93
|
+
expect(cap).toBeLessThanOrEqual(5000000);
|
|
94
|
+
expect(cap).toBeGreaterThan(4900000);
|
|
95
|
+
});
|
|
96
|
+
it('feeds straight into the scaler so the last row stays reachable', () => {
|
|
97
|
+
const maxDom = resolveMaxDomHeight(24000000, 6000000, FALLBACK);
|
|
98
|
+
const trueTotal = 500000 * 32; // 16M px of rows, over the phone cap
|
|
99
|
+
const s = createRowScrollScaling(trueTotal, maxDom, VIEWPORT);
|
|
100
|
+
expect(s.active).toBe(true);
|
|
101
|
+
expect(s.domTotal).toBeLessThanOrEqual(maxDom);
|
|
102
|
+
const domMax = s.domTotal - VIEWPORT;
|
|
103
|
+
expect(s.domToLogical(domMax)).toBeCloseTo(trueTotal - VIEWPORT, 0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
65
106
|
describe('createRowScrollScaling - edge cases', () => {
|
|
66
107
|
it('handles an empty grid without dividing by zero', () => {
|
|
67
108
|
const s = createRowScrollScaling(0, 17895697, VIEWPORT);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svgrid/grid",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "SvGrid - a headless-first, Svelte 5-native data grid engine and render component.",
|
|
5
5
|
"author": "jQWidgets <boikom@jqwidgets.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"@vitest/coverage-v8": "^4.1.5",
|
|
61
61
|
"eslint-plugin-svelte": "^3.17.1",
|
|
62
62
|
"jsdom": "^29.1.1",
|
|
63
|
+
"publint": "^0.3.21",
|
|
63
64
|
"svelte": "^5.55.5",
|
|
64
65
|
"svelte-check": "^4.4.6"
|
|
65
66
|
},
|
package/src/GridMenus.svelte
CHANGED
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
const operatorMenuPos = $derived(ctrl.operatorMenuPos);
|
|
109
109
|
const chooseColumnsPos = $derived(ctrl.chooseColumnsPos);
|
|
110
110
|
const showColumnFiltersEffective = $derived(ctrl.showColumnFiltersEffective);
|
|
111
|
+
const columnMenuTabsEnabled = $derived(ctrl.props.columnMenuTabs === true);
|
|
111
112
|
const grid = $derived(ctrl.grid);
|
|
112
113
|
const allColumns = $derived(ctrl.allColumns);
|
|
113
114
|
const isColumnPinned = $derived(ctrl.isColumnPinned);
|
|
@@ -123,6 +124,15 @@
|
|
|
123
124
|
const operatorsForColumn = $derived(ctrl.operatorsForColumn);
|
|
124
125
|
const defaultOperatorFor = $derived(ctrl.defaultOperatorFor);
|
|
125
126
|
const operatorLabelFor = $derived(ctrl.operatorLabelFor);
|
|
127
|
+
|
|
128
|
+
// Patch a column's menu filter (used for the optional 2nd condition + join
|
|
129
|
+
// of multi-condition filtering). Merges into the existing entry, creating a
|
|
130
|
+
// base entry from the column's default operator when none exists yet.
|
|
131
|
+
function patchFilter(columnId: string, patch: Record<string, unknown>) {
|
|
132
|
+
const col = allColumns.find((c) => c.id === columnId);
|
|
133
|
+
const cur = ctrl.filterMenuValues[columnId] ?? { operator: defaultOperatorFor(col), value: "" };
|
|
134
|
+
ctrl.filterMenuValues = { ...ctrl.filterMenuValues, [columnId]: { ...cur, ...patch } };
|
|
135
|
+
}
|
|
126
136
|
const isColumnFiltered = $derived(ctrl.isColumnFiltered);
|
|
127
137
|
const closeMenus = $derived(ctrl.closeMenus);
|
|
128
138
|
const autosizeColumn = $derived(ctrl.autosizeColumn);
|
|
@@ -169,15 +179,190 @@
|
|
|
169
179
|
></div>
|
|
170
180
|
{/if}
|
|
171
181
|
|
|
182
|
+
<!-- Shared filter UI: rendered both in the funnel popover and in the column
|
|
183
|
+
menu's Filter tab, so the two never diverge. -->
|
|
184
|
+
{#snippet filterPanelBody(colId: string)}
|
|
185
|
+
{@const menuColumn = allColumns.find((c) => c.id === colId)}
|
|
186
|
+
{@const menuOperatorOptions = operatorsForColumn(menuColumn)}
|
|
187
|
+
{@const menuActiveOperator = filterMenuValues[colId]?.operator ?? defaultOperatorFor(menuColumn)}
|
|
188
|
+
{@const menuInputType = getEditorInputType((menuColumn?.columnDef.editorType ?? "text") as CellEditorType)}
|
|
189
|
+
<div class="sv-grid-menu-filter">
|
|
190
|
+
<div class="sv-grid-menu-filter-head">
|
|
191
|
+
<span class="sv-grid-header-icon">{@render icon("filter")}</span> Filter
|
|
192
|
+
condition
|
|
193
|
+
</div>
|
|
194
|
+
<select
|
|
195
|
+
class="sv-grid-menu-operator-select"
|
|
196
|
+
aria-label="Filter condition"
|
|
197
|
+
value={menuActiveOperator}
|
|
198
|
+
onchange={(event) =>
|
|
199
|
+
updateFilterOperator(colId, (event.currentTarget as HTMLSelectElement).value as FilterOperator)}
|
|
200
|
+
>
|
|
201
|
+
{#each menuOperatorOptions as option (option.value)}
|
|
202
|
+
<option value={option.value}>{operatorLabelFor(option, menuColumn)}</option>
|
|
203
|
+
{/each}
|
|
204
|
+
</select>
|
|
205
|
+
{#if menuActiveOperator !== "isBlank"}
|
|
206
|
+
<input
|
|
207
|
+
class="sv-grid-menu-condition-value"
|
|
208
|
+
type={menuInputType}
|
|
209
|
+
value={filterMenuValues[colId]?.value ?? ""}
|
|
210
|
+
placeholder={menuActiveOperator === "between" ? "From" : "Filter value..."}
|
|
211
|
+
oninput={(event) => updateFilterMenuValue(colId, (event.currentTarget as HTMLInputElement).value)}
|
|
212
|
+
/>
|
|
213
|
+
{#if menuActiveOperator === "between"}
|
|
214
|
+
<input
|
|
215
|
+
class="sv-grid-menu-condition-value"
|
|
216
|
+
type={menuInputType}
|
|
217
|
+
value={filterMenuValues[colId]?.valueTo ?? ""}
|
|
218
|
+
placeholder="To"
|
|
219
|
+
oninput={(event) => updateFilterMenuValueTo(colId, (event.currentTarget as HTMLInputElement).value)}
|
|
220
|
+
/>
|
|
221
|
+
{/if}
|
|
222
|
+
{@const mf = filterMenuValues[colId]}
|
|
223
|
+
{#if mf?.operator2}
|
|
224
|
+
{@const op2 = mf.operator2 ?? defaultOperatorFor(menuColumn)}
|
|
225
|
+
<div class="sv-grid-menu-join" role="radiogroup" aria-label="Combine conditions">
|
|
226
|
+
<button type="button" class:is-on={(mf.join ?? "AND") === "AND"}
|
|
227
|
+
onclick={() => patchFilter(colId, { join: "AND" })}>AND</button>
|
|
228
|
+
<button type="button" class:is-on={mf.join === "OR"}
|
|
229
|
+
onclick={() => patchFilter(colId, { join: "OR" })}>OR</button>
|
|
230
|
+
<button type="button" class="sv-grid-menu-join-x" title="Remove second condition"
|
|
231
|
+
aria-label="Remove second condition"
|
|
232
|
+
onclick={() => patchFilter(colId, { operator2: undefined, value2: undefined, valueTo2: undefined, join: undefined })}>×</button>
|
|
233
|
+
</div>
|
|
234
|
+
<select
|
|
235
|
+
class="sv-grid-menu-operator-select"
|
|
236
|
+
aria-label="Second filter condition"
|
|
237
|
+
value={op2}
|
|
238
|
+
onchange={(event) =>
|
|
239
|
+
patchFilter(colId, { operator2: (event.currentTarget as HTMLSelectElement).value as FilterOperator })}
|
|
240
|
+
>
|
|
241
|
+
{#each menuOperatorOptions as option (option.value)}
|
|
242
|
+
<option value={option.value}>{operatorLabelFor(option, menuColumn)}</option>
|
|
243
|
+
{/each}
|
|
244
|
+
</select>
|
|
245
|
+
{#if op2 !== "isBlank"}
|
|
246
|
+
<input
|
|
247
|
+
class="sv-grid-menu-condition-value"
|
|
248
|
+
type={menuInputType}
|
|
249
|
+
value={mf.value2 ?? ""}
|
|
250
|
+
placeholder={op2 === "between" ? "From" : "Filter value..."}
|
|
251
|
+
oninput={(event) => patchFilter(colId, { value2: (event.currentTarget as HTMLInputElement).value })}
|
|
252
|
+
/>
|
|
253
|
+
{#if op2 === "between"}
|
|
254
|
+
<input
|
|
255
|
+
class="sv-grid-menu-condition-value"
|
|
256
|
+
type={menuInputType}
|
|
257
|
+
value={mf.valueTo2 ?? ""}
|
|
258
|
+
placeholder="To"
|
|
259
|
+
oninput={(event) => patchFilter(colId, { valueTo2: (event.currentTarget as HTMLInputElement).value })}
|
|
260
|
+
/>
|
|
261
|
+
{/if}
|
|
262
|
+
{/if}
|
|
263
|
+
{:else}
|
|
264
|
+
<button type="button" class="sv-grid-menu-add-cond"
|
|
265
|
+
onclick={() => patchFilter(colId, { operator2: defaultOperatorFor(menuColumn), value2: "", join: "AND" })}>
|
|
266
|
+
+ Add condition
|
|
267
|
+
</button>
|
|
268
|
+
{/if}
|
|
269
|
+
{/if}
|
|
270
|
+
<div class="sv-grid-menu-sep"></div>
|
|
271
|
+
<div class="sv-grid-menu-filter-head">Values</div>
|
|
272
|
+
<input
|
|
273
|
+
class="sv-grid-menu-search"
|
|
274
|
+
placeholder="Search values..."
|
|
275
|
+
bind:value={ctrl.columnMenuSearch}
|
|
276
|
+
/>
|
|
277
|
+
<label class="sv-grid-facet sv-grid-facet-all">
|
|
278
|
+
<input type="checkbox" checked={isAllFacetsChecked(colId)} onchange={() => toggleAllFacets(colId)} />
|
|
279
|
+
<span class="sv-grid-facet-label">(Select all)</span>
|
|
280
|
+
</label>
|
|
281
|
+
<div class="sv-grid-facet-list">
|
|
282
|
+
{#each columnMenuVisibleFacets as value (value)}
|
|
283
|
+
<label class="sv-grid-facet">
|
|
284
|
+
<input type="checkbox" checked={isFacetChecked(colId, value)} onchange={() => toggleFacetValue(colId, value)} />
|
|
285
|
+
<span class="sv-grid-facet-label">{value === "" ? "(Blanks)" : value}</span>
|
|
286
|
+
</label>
|
|
287
|
+
{:else}
|
|
288
|
+
<div class="sv-grid-facet-empty">No values</div>
|
|
289
|
+
{/each}
|
|
290
|
+
</div>
|
|
291
|
+
{#if columnMenuFacetValues.length > columnMenuVisibleFacets.length}
|
|
292
|
+
<div class="sv-grid-facet-note">
|
|
293
|
+
Showing {columnMenuVisibleFacets.length} of {columnMenuFacetValues.length}
|
|
294
|
+
</div>
|
|
295
|
+
{/if}
|
|
296
|
+
<div class="sv-grid-menu-actions">
|
|
297
|
+
<button
|
|
298
|
+
type="button"
|
|
299
|
+
class="sv-grid-menu-btn"
|
|
300
|
+
disabled={!isColumnFiltered(colId)}
|
|
301
|
+
onclick={() => clearColumnFilter(colId)}
|
|
302
|
+
>
|
|
303
|
+
Clear filter
|
|
304
|
+
</button>
|
|
305
|
+
<button type="button" class="sv-grid-menu-btn sv-grid-menu-btn-primary" onclick={closeMenus}>
|
|
306
|
+
Done
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
{/snippet}
|
|
311
|
+
|
|
172
312
|
{#if columnMenuFor}
|
|
173
313
|
{@const menuColumnId = columnMenuFor}
|
|
174
314
|
{@const menuCol = allColumns.find((c) => c.id === menuColumnId)}
|
|
175
315
|
{@const menuCanSort = menuCol?.getCanSort?.() ?? false}
|
|
316
|
+
{@const menuCanFilter = (menuCol?.columnDef.field != null) && (menuCol?.columnDef.filterable !== false)}
|
|
317
|
+
{@const menuTab = ctrl.columnMenuTab}
|
|
176
318
|
<div
|
|
177
319
|
class="sv-grid-menu sv-grid-column-menu"
|
|
178
320
|
role="menu"
|
|
179
|
-
style={`left: ${columnMenuPos.x}px; top: ${columnMenuPos.y}px;`}
|
|
321
|
+
style={`left: ${columnMenuPos.x}px; top: ${columnMenuPos.y}px; max-height: calc(100vh - ${columnMenuPos.y}px - 12px);`}
|
|
180
322
|
>
|
|
323
|
+
{#if columnMenuTabsEnabled}
|
|
324
|
+
<div class="sv-grid-menu-tabs" role="tablist">
|
|
325
|
+
<button type="button" class="sv-grid-menu-tab" class:is-active={menuTab === "general"}
|
|
326
|
+
role="tab" aria-selected={menuTab === "general"}
|
|
327
|
+
onclick={() => (ctrl.columnMenuTab = "general")}>General</button>
|
|
328
|
+
{#if menuCanFilter && showColumnFiltersEffective}
|
|
329
|
+
<button type="button" class="sv-grid-menu-tab" class:is-active={menuTab === "filter"}
|
|
330
|
+
role="tab" aria-selected={menuTab === "filter"}
|
|
331
|
+
onclick={() => (ctrl.columnMenuTab = "filter")}>Filter</button>
|
|
332
|
+
{/if}
|
|
333
|
+
<button type="button" class="sv-grid-menu-tab" class:is-active={menuTab === "columns"}
|
|
334
|
+
role="tab" aria-selected={menuTab === "columns"}
|
|
335
|
+
onclick={() => (ctrl.columnMenuTab = "columns")}>Columns</button>
|
|
336
|
+
</div>
|
|
337
|
+
{/if}
|
|
338
|
+
{#if columnMenuTabsEnabled && menuTab === "filter" && menuCanFilter && showColumnFiltersEffective}
|
|
339
|
+
{@render filterPanelBody(menuColumnId)}
|
|
340
|
+
{:else if columnMenuTabsEnabled && menuTab === "columns"}
|
|
341
|
+
<div class="sv-grid-menu-filter-head">Visible columns</div>
|
|
342
|
+
<div class="sv-grid-facet-list">
|
|
343
|
+
{#each grid.getAllColumns() as column (column.id)}
|
|
344
|
+
<label class="sv-grid-facet">
|
|
345
|
+
<input
|
|
346
|
+
type="checkbox"
|
|
347
|
+
checked={!ctrl.hiddenColumns[column.id]}
|
|
348
|
+
onchange={(event) => {
|
|
349
|
+
const visible = (event.currentTarget as HTMLInputElement).checked;
|
|
350
|
+
if (visible) {
|
|
351
|
+
const next = { ...ctrl.hiddenColumns };
|
|
352
|
+
delete next[column.id];
|
|
353
|
+
ctrl.hiddenColumns = next;
|
|
354
|
+
} else {
|
|
355
|
+
ctrl.hiddenColumns = { ...ctrl.hiddenColumns, [column.id]: true };
|
|
356
|
+
}
|
|
357
|
+
}}
|
|
358
|
+
/>
|
|
359
|
+
<span class="sv-grid-facet-label"
|
|
360
|
+
>{typeof column.columnDef.header === "string" ? column.columnDef.header : column.id}</span
|
|
361
|
+
>
|
|
362
|
+
</label>
|
|
363
|
+
{/each}
|
|
364
|
+
</div>
|
|
365
|
+
{:else}
|
|
181
366
|
{#if menuCanSort}
|
|
182
367
|
<button
|
|
183
368
|
type="button"
|
|
@@ -290,21 +475,23 @@
|
|
|
290
475
|
</button>
|
|
291
476
|
{/if}
|
|
292
477
|
<div class="sv-grid-menu-sep"></div>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
<span class="sv-grid-header-icon sv-grid-menu-item-chevron"
|
|
305
|
-
>{@render icon("chevron-down")}</span
|
|
478
|
+
{#if !columnMenuTabsEnabled}
|
|
479
|
+
<!-- Flat menu: reach the column chooser via a submenu (tabbed mode has
|
|
480
|
+
a Columns tab instead). -->
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
class="sv-grid-menu-item"
|
|
484
|
+
class:is-open={chooseColumnsPos !== null}
|
|
485
|
+
role="menuitem"
|
|
486
|
+
aria-haspopup="menu"
|
|
487
|
+
aria-expanded={chooseColumnsPos !== null}
|
|
488
|
+
onclick={(event) => openChooseColumns(event)}
|
|
306
489
|
>
|
|
307
|
-
|
|
490
|
+
<span class="sv-grid-header-icon">{@render icon("columns")}</span>
|
|
491
|
+
Choose columns
|
|
492
|
+
<span class="sv-grid-header-icon sv-grid-menu-item-chevron">{@render icon("chevron-down")}</span>
|
|
493
|
+
</button>
|
|
494
|
+
{/if}
|
|
308
495
|
<button
|
|
309
496
|
type="button"
|
|
310
497
|
class="sv-grid-menu-item"
|
|
@@ -316,126 +503,17 @@
|
|
|
316
503
|
>
|
|
317
504
|
<span class="sv-grid-header-icon">{@render icon("reset")}</span> Reset columns
|
|
318
505
|
</button>
|
|
506
|
+
{/if}
|
|
319
507
|
</div>
|
|
320
508
|
{/if}
|
|
321
509
|
|
|
322
510
|
{#if filterMenuFor && showColumnFiltersEffective}
|
|
323
|
-
{@const menuColumnId = filterMenuFor}
|
|
324
|
-
{@const menuColumn = allColumns.find((c) => c.id === menuColumnId)}
|
|
325
|
-
{@const menuOperatorOptions = operatorsForColumn(menuColumn)}
|
|
326
|
-
{@const menuActiveOperator =
|
|
327
|
-
filterMenuValues[menuColumnId]?.operator ??
|
|
328
|
-
defaultOperatorFor(menuColumn)}
|
|
329
|
-
{@const menuInputType = getEditorInputType(
|
|
330
|
-
(menuColumn?.columnDef.editorType ?? "text") as CellEditorType,
|
|
331
|
-
)}
|
|
332
511
|
<div
|
|
333
512
|
class="sv-grid-menu sv-grid-filter-menu"
|
|
334
513
|
role="menu"
|
|
335
|
-
style={`left: ${filterMenuPos.x}px; top: ${filterMenuPos.y}px;`}
|
|
514
|
+
style={`left: ${filterMenuPos.x}px; top: ${filterMenuPos.y}px; max-height: calc(100vh - ${filterMenuPos.y}px - 12px);`}
|
|
336
515
|
>
|
|
337
|
-
|
|
338
|
-
<div class="sv-grid-menu-filter-head">
|
|
339
|
-
<span class="sv-grid-header-icon">{@render icon("filter")}</span> Filter
|
|
340
|
-
condition
|
|
341
|
-
</div>
|
|
342
|
-
<select
|
|
343
|
-
class="sv-grid-menu-operator-select"
|
|
344
|
-
aria-label="Filter condition"
|
|
345
|
-
value={menuActiveOperator}
|
|
346
|
-
onchange={(event) =>
|
|
347
|
-
updateFilterOperator(
|
|
348
|
-
menuColumnId,
|
|
349
|
-
(event.currentTarget as HTMLSelectElement)
|
|
350
|
-
.value as FilterOperator,
|
|
351
|
-
)}
|
|
352
|
-
>
|
|
353
|
-
{#each menuOperatorOptions as option (option.value)}
|
|
354
|
-
<option value={option.value}
|
|
355
|
-
>{operatorLabelFor(option, menuColumn)}</option
|
|
356
|
-
>
|
|
357
|
-
{/each}
|
|
358
|
-
</select>
|
|
359
|
-
{#if menuActiveOperator !== "isBlank"}
|
|
360
|
-
<input
|
|
361
|
-
class="sv-grid-menu-condition-value"
|
|
362
|
-
type={menuInputType}
|
|
363
|
-
value={filterMenuValues[menuColumnId]?.value ?? ""}
|
|
364
|
-
placeholder={menuActiveOperator === "between" ? "From" : "Filter value..."}
|
|
365
|
-
oninput={(event) =>
|
|
366
|
-
updateFilterMenuValue(
|
|
367
|
-
menuColumnId,
|
|
368
|
-
(event.currentTarget as HTMLInputElement).value,
|
|
369
|
-
)}
|
|
370
|
-
/>
|
|
371
|
-
{#if menuActiveOperator === "between"}
|
|
372
|
-
<input
|
|
373
|
-
class="sv-grid-menu-condition-value"
|
|
374
|
-
type={menuInputType}
|
|
375
|
-
value={filterMenuValues[menuColumnId]?.valueTo ?? ""}
|
|
376
|
-
placeholder="To"
|
|
377
|
-
oninput={(event) =>
|
|
378
|
-
updateFilterMenuValueTo(
|
|
379
|
-
menuColumnId,
|
|
380
|
-
(event.currentTarget as HTMLInputElement).value,
|
|
381
|
-
)}
|
|
382
|
-
/>
|
|
383
|
-
{/if}
|
|
384
|
-
{/if}
|
|
385
|
-
<div class="sv-grid-menu-sep"></div>
|
|
386
|
-
<div class="sv-grid-menu-filter-head">Values</div>
|
|
387
|
-
<input
|
|
388
|
-
class="sv-grid-menu-search"
|
|
389
|
-
placeholder="Search values..."
|
|
390
|
-
bind:value={ctrl.columnMenuSearch}
|
|
391
|
-
/>
|
|
392
|
-
<label class="sv-grid-facet sv-grid-facet-all">
|
|
393
|
-
<input
|
|
394
|
-
type="checkbox"
|
|
395
|
-
checked={isAllFacetsChecked(menuColumnId)}
|
|
396
|
-
onchange={() => toggleAllFacets(menuColumnId)}
|
|
397
|
-
/>
|
|
398
|
-
<span class="sv-grid-facet-label">(Select all)</span>
|
|
399
|
-
</label>
|
|
400
|
-
<div class="sv-grid-facet-list">
|
|
401
|
-
{#each columnMenuVisibleFacets as value (value)}
|
|
402
|
-
<label class="sv-grid-facet">
|
|
403
|
-
<input
|
|
404
|
-
type="checkbox"
|
|
405
|
-
checked={isFacetChecked(menuColumnId, value)}
|
|
406
|
-
onchange={() => toggleFacetValue(menuColumnId, value)}
|
|
407
|
-
/>
|
|
408
|
-
<span class="sv-grid-facet-label"
|
|
409
|
-
>{value === "" ? "(Blanks)" : value}</span
|
|
410
|
-
>
|
|
411
|
-
</label>
|
|
412
|
-
{:else}
|
|
413
|
-
<div class="sv-grid-facet-empty">No values</div>
|
|
414
|
-
{/each}
|
|
415
|
-
</div>
|
|
416
|
-
{#if columnMenuFacetValues.length > columnMenuVisibleFacets.length}
|
|
417
|
-
<div class="sv-grid-facet-note">
|
|
418
|
-
Showing {columnMenuVisibleFacets.length} of {columnMenuFacetValues.length}
|
|
419
|
-
</div>
|
|
420
|
-
{/if}
|
|
421
|
-
<div class="sv-grid-menu-actions">
|
|
422
|
-
<button
|
|
423
|
-
type="button"
|
|
424
|
-
class="sv-grid-menu-btn"
|
|
425
|
-
disabled={!isColumnFiltered(menuColumnId)}
|
|
426
|
-
onclick={() => clearColumnFilter(menuColumnId)}
|
|
427
|
-
>
|
|
428
|
-
Clear filter
|
|
429
|
-
</button>
|
|
430
|
-
<button
|
|
431
|
-
type="button"
|
|
432
|
-
class="sv-grid-menu-btn sv-grid-menu-btn-primary"
|
|
433
|
-
onclick={closeMenus}
|
|
434
|
-
>
|
|
435
|
-
Done
|
|
436
|
-
</button>
|
|
437
|
-
</div>
|
|
438
|
-
</div>
|
|
516
|
+
{@render filterPanelBody(filterMenuFor)}
|
|
439
517
|
</div>
|
|
440
518
|
{/if}
|
|
441
519
|
|