@svgrid/grid 2.6.22 → 2.6.23
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 +184 -6
- package/dist/SvAutoComplete.svelte +4 -0
- package/dist/SvComboBox.svelte +5 -1
- package/dist/SvDateRangeInput.svelte +25 -3
- package/dist/SvDropDownList.svelte +4 -0
- package/dist/SvForm.svelte +8 -8
- package/dist/SvGrid.controller.svelte.d.ts +48 -1
- package/dist/SvGrid.controller.svelte.js +214 -52
- package/dist/SvGrid.css +221 -0
- package/dist/SvGrid.helpers.d.ts +55 -0
- package/dist/SvGrid.helpers.js +83 -0
- package/dist/SvGrid.svelte +145 -0
- package/dist/SvGrid.types.d.ts +105 -0
- package/dist/SvGridCellEditor.svelte +99 -17
- package/dist/SvGridSelect.svelte +6 -2
- package/dist/SvGridSelect.svelte.d.ts +1 -1
- package/dist/SvListBox.svelte +8 -0
- package/dist/SvMultiSelect.svelte +6 -2
- package/dist/SvMultiSelect.svelte.d.ts +1 -1
- package/dist/SvNumberInput.svelte +4 -0
- package/dist/SvRichCell.svelte +125 -0
- package/dist/SvRichCell.svelte.d.ts +15 -0
- package/dist/SvTextArea.svelte +5 -1
- package/dist/SvTreeSelect.svelte +6 -2
- package/dist/SvTreeSelect.svelte.d.ts +1 -1
- package/dist/builtin-editors.d.ts +2 -2
- package/dist/builtin-editors.js +19 -3
- package/dist/cdn/GridMenus-BzWXQjv3.js +600 -0
- package/dist/cdn/GridMenus-xr_rHx8F.js +601 -0
- package/dist/cdn/SvDateRangeInput-CaOuMs8O.js +198 -0
- package/dist/cdn/SvDateRangeInput-DMLKmGEc.js +199 -0
- package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +816 -0
- package/dist/cdn/SvDateTimePicker-sonaH0oh.js +812 -0
- package/dist/cdn/SvGridCellEditor-BAPSC7EL.js +550 -0
- package/dist/cdn/SvGridCellEditor-BwzmUWtL.js +549 -0
- package/dist/cdn/date-format-BNii4zeD.js +1393 -0
- package/dist/cdn/date-format-BnnHlqGw.js +1395 -0
- package/dist/cdn/{editor-registry-CiI0xlKg.js → editor-registry-BhgE3S84.js} +51 -25
- package/dist/cdn/{src-D1lXwq1l.js → src-DGo7IHug.js} +5730 -5198
- package/dist/cdn/{src-C9Hihx1W.js → src-skQN5eqh.js} +7216 -6684
- package/dist/cdn/svgrid.js +14 -12
- package/dist/cdn/svgrid.svelte-external.js +14 -12
- package/dist/cell-values.d.ts +16 -0
- package/dist/cell-values.js +28 -0
- package/dist/clipboard.d.ts +22 -0
- package/dist/clipboard.js +405 -27
- package/dist/editing.js +12 -7
- package/dist/editors/cell-editors.d.ts +11 -0
- package/dist/editors/cell-editors.js +18 -1
- package/dist/grid-messages.d.ts +15 -0
- package/dist/grid-messages.js +15 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/list-option.d.ts +6 -0
- package/dist/menus.js +40 -7
- package/dist/sanitize-html.d.ts +37 -0
- package/dist/sanitize-html.js +234 -0
- package/dist/selection-bar-view.svelte.d.ts +25 -0
- package/dist/selection-bar-view.svelte.js +13 -0
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +129 -13
- package/package.json +1 -1
- package/src/GridMenus.svelte +184 -6
- package/src/SvAutoComplete.svelte +4 -0
- package/src/SvComboBox.svelte +5 -1
- package/src/SvDateRangeInput.svelte +25 -3
- package/src/SvDropDownList.svelte +4 -0
- package/src/SvForm.svelte +8 -8
- package/src/SvGrid.controller.svelte.ts +251 -61
- package/src/SvGrid.css +221 -0
- package/src/SvGrid.helpers.ts +87 -0
- package/src/SvGrid.svelte +145 -0
- package/src/SvGrid.types.ts +112 -0
- package/src/SvGridCellEditor.svelte +99 -17
- package/src/SvGridSelect.svelte +6 -2
- package/src/SvListBox.svelte +8 -0
- package/src/SvMultiSelect.svelte +6 -2
- package/src/SvNumberInput.svelte +4 -0
- package/src/SvRichCell.svelte +125 -0
- package/src/SvTextArea.svelte +5 -1
- package/src/SvTreeSelect.svelte +6 -2
- package/src/builtin-editors.test.ts +18 -1
- package/src/builtin-editors.ts +19 -3
- package/src/cell-values.ts +30 -0
- package/src/clipboard.test.ts +93 -20
- package/src/clipboard.ts +433 -33
- package/src/date-string-column.test.ts +87 -0
- package/src/editing.test.ts +25 -0
- package/src/editing.ts +12 -9
- package/src/editor-block-prop.test.ts +132 -0
- package/src/editors/cell-editors.ts +27 -1
- package/src/grid-messages.ts +34 -0
- package/src/index.ts +13 -0
- package/src/list-option.ts +6 -0
- package/src/menus.test.ts +43 -0
- package/src/menus.ts +37 -7
- package/src/move-cells.test.ts +850 -0
- package/src/number-editor-literal.test.ts +84 -0
- package/src/number-editor.grid.test.ts +160 -0
- package/src/sanitize-html.test.ts +182 -0
- package/src/sanitize-html.ts +235 -0
- package/src/selection-bar-view.svelte.ts +36 -0
- package/src/selection.test.ts +157 -0
- package/src/selection.ts +131 -13
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +56 -10
- package/src/svgrid.in-suggest-snapshot.svelte.test.ts +91 -0
- package/src/svgrid.menu-scroll-close.test.ts +204 -0
- package/src/svgrid.selection-bar-seam.test.ts +185 -0
- package/src/svgrid.upsell-license.test.ts +117 -0
- package/dist/cdn/GridMenus-BuoBPqxx.js +0 -493
- package/dist/cdn/GridMenus-n4llxoOI.js +0 -494
- package/dist/cdn/SvDateTimePicker-CHnUkWcH.js +0 -2206
- package/dist/cdn/SvDateTimePicker-CRQH_U7E.js +0 -2201
- package/dist/cdn/SvGridCellEditor-BGUCzuPB.js +0 -523
- package/dist/cdn/SvGridCellEditor-G8IMWIws.js +0 -522
package/src/SvForm.svelte
CHANGED
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
{#if o.type === 'textarea'}
|
|
178
178
|
<textarea id={o.id} class="sv-form__control sv-form__textarea" rows="3" disabled={o.disabled} readonly={o.readonly} placeholder={o.placeholder} value={o.value ?? ''} aria-invalid={o.invalid ? 'true' : undefined} aria-describedby={o.describedby} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur}></textarea>
|
|
179
179
|
{:else if o.type === 'number'}
|
|
180
|
-
<SvNumberInput id={o.id} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block min={o.field?.min ?? -Infinity} max={o.field?.max ?? Infinity} step={o.field?.step ?? 1} precision={o.field?.precision} prefix={o.field?.prefix ?? ''} suffix={o.field?.suffix ?? ''} onChange={o.onChange} />
|
|
180
|
+
<SvNumberInput id={o.id} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} min={o.field?.min ?? -Infinity} max={o.field?.max ?? Infinity} step={o.field?.step ?? 1} precision={o.field?.precision} prefix={o.field?.prefix ?? ''} suffix={o.field?.suffix ?? ''} onChange={o.onChange} />
|
|
181
181
|
{:else if o.type === 'password'}
|
|
182
182
|
<SvPasswordInput id={o.id} value={o.value ?? ''} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block onChange={o.onChange} />
|
|
183
183
|
{:else if o.type === 'mask'}
|
|
@@ -185,17 +185,17 @@
|
|
|
185
185
|
{:else if o.type === 'phone'}
|
|
186
186
|
<SvPhoneInput id={o.id} value={o.value ?? ''} disabled={o.disabled} invalid={o.invalid} block placeholder={o.placeholder} onChange={(v) => o.onChange(v)} />
|
|
187
187
|
{:else if o.type === 'country'}
|
|
188
|
-
<SvCountryInput id={o.id} value={o.value ?? null} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
|
|
188
|
+
<SvCountryInput id={o.id} value={o.value ?? null} disabled={o.disabled} invalid={o.invalid} block onChange={o.onChange} />
|
|
189
189
|
{:else if o.type === 'select'}
|
|
190
|
-
<SvDropDownList id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
190
|
+
<SvDropDownList id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} onChange={o.onChange} />
|
|
191
191
|
{:else if o.type === 'combobox'}
|
|
192
|
-
<SvComboBox id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} clearable placeholder={o.placeholder} loadOptions={o.field?.loadOptions ? (q) => o.field!.loadOptions!(q, form.values) : undefined} onChange={o.onChange} />
|
|
192
|
+
<SvComboBox id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block clearable placeholder={o.placeholder} loadOptions={o.field?.loadOptions ? (q) => o.field!.loadOptions!(q, form.values) : undefined} onChange={o.onChange} />
|
|
193
193
|
{:else if o.type === 'multiselect'}
|
|
194
|
-
<SvMultiSelect id={o.id} options={o.options ?? []} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
194
|
+
<SvMultiSelect id={o.id} options={o.options ?? []} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} onChange={o.onChange} />
|
|
195
195
|
{:else if o.type === 'radio'}
|
|
196
196
|
<SvRadioGroup id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} orientation="horizontal" onChange={o.onChange} />
|
|
197
197
|
{:else if o.type === 'tags'}
|
|
198
|
-
<SvTagsInput id={o.id} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
198
|
+
<SvTagsInput id={o.id} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} invalid={o.invalid} block placeholder={o.placeholder} onChange={o.onChange} />
|
|
199
199
|
{:else if o.type === 'slider'}
|
|
200
200
|
<SvSlider id={o.id} value={o.value ?? o.field?.min ?? 0} min={o.field?.min ?? 0} max={o.field?.max ?? 100} step={o.field?.step ?? 1} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} showValue onChange={o.onChange} />
|
|
201
201
|
{:else if o.type === 'checkbox'}
|
|
@@ -203,9 +203,9 @@
|
|
|
203
203
|
{:else if o.type === 'switch'}
|
|
204
204
|
<div class="sv-form__inline"><SvSwitchButton checked={!!o.value} disabled={o.disabled} readonly={o.readonly} onChange={o.onChange} ariaLabel={o.label} /><span>{o.label}</span></div>
|
|
205
205
|
{:else if o.type === 'date'}
|
|
206
|
-
<SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" disabled={o.disabled} onChange={o.onChange} />
|
|
206
|
+
<SvDateTimePicker id={o.id} value={dateVal(o.value)} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder ?? 'Select date'} onChange={o.onChange} />
|
|
207
207
|
{:else if o.type === 'datetime'}
|
|
208
|
-
<SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="both" disabled={o.disabled} onChange={o.onChange} />
|
|
208
|
+
<SvDateTimePicker id={o.id} value={dateVal(o.value)} dropDownDisplayMode="both" disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} onChange={o.onChange} />
|
|
209
209
|
{:else if o.type === 'color'}
|
|
210
210
|
<SvColorInput id={o.id} value={o.value ?? '#3b82f6'} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
|
|
211
211
|
{:else if o.type === 'file'}
|
|
@@ -390,6 +390,29 @@ export function createSvGridController<
|
|
|
390
390
|
targetRow: number;
|
|
391
391
|
targetCol: number;
|
|
392
392
|
} | null>(null);
|
|
393
|
+
/** An in-flight Excel-style range MOVE (drag the selection's border to
|
|
394
|
+
* relocate its values). Same source rectangle as `fillDrag`, plus where
|
|
395
|
+
* inside that rectangle the pointer grabbed it - the drop offset is
|
|
396
|
+
* `target - grab`, which is what lets a drag started on the range's LEFT
|
|
397
|
+
* edge land the range where the pointer is rather than jumping it.
|
|
398
|
+
* `copy` mirrors the Ctrl / Cmd key and is re-read on every move, so the
|
|
399
|
+
* user can change their mind mid-drag exactly as in a spreadsheet. */
|
|
400
|
+
let moveDrag = $state<{
|
|
401
|
+
sourceMinRow: number;
|
|
402
|
+
sourceMaxRow: number;
|
|
403
|
+
sourceMinCol: number;
|
|
404
|
+
sourceMaxCol: number;
|
|
405
|
+
grabRow: number;
|
|
406
|
+
grabCol: number;
|
|
407
|
+
targetRow: number;
|
|
408
|
+
targetCol: number;
|
|
409
|
+
copy: boolean;
|
|
410
|
+
} | null>(null);
|
|
411
|
+
/** True while the pointer hovers the grab strip on a selection border, so
|
|
412
|
+
* the root can switch to a move cursor. Kept as its own flag rather than
|
|
413
|
+
* recomputed per cell: it flips at most twice per border crossing and only
|
|
414
|
+
* one attribute on one element reads it. */
|
|
415
|
+
let moveGrabHover = $state(false);
|
|
393
416
|
let activeAtPointerDown: { rowIndex: number; colIndex: number } | null = null;
|
|
394
417
|
let editingCell = $state<CellEditState>(null);
|
|
395
418
|
// Full-row editing: the row currently in whole-row edit + its per-column
|
|
@@ -626,6 +649,14 @@ export function createSvGridController<
|
|
|
626
649
|
(props.selectionMode ?? "both") === "both"),
|
|
627
650
|
);
|
|
628
651
|
|
|
652
|
+
// Range drag-and-drop rides on cell selection: there is no range to grab
|
|
653
|
+
// without it, so it can never be on alone. Default ON, matching the fill
|
|
654
|
+
// handle - both are spreadsheet affordances users arrive expecting, and
|
|
655
|
+
// neither is reachable until something is actually selected.
|
|
656
|
+
const moveCellsEffective = $derived(
|
|
657
|
+
(props.moveCells ?? true) && enableCellSelectionEffective,
|
|
658
|
+
);
|
|
659
|
+
|
|
629
660
|
// The aggregate footer row. `summary` is the shortcut alias, so it wins over
|
|
630
661
|
// the fine-grained prop - the same precedence `selectable` uses.
|
|
631
662
|
//
|
|
@@ -1326,35 +1357,22 @@ export function createSvGridController<
|
|
|
1326
1357
|
* compute the correct "X to Y of Z" range and total page count when
|
|
1327
1358
|
* filters reduce the dataset.
|
|
1328
1359
|
*/
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
let rows = rawRows;
|
|
1346
|
-
if (globalFilter.trim()) {
|
|
1347
|
-
const needle = normalizeForFilter(globalFilter, (props.filterLocale ?? props.localization?.locale));
|
|
1348
|
-
rows = rows.filter((row) =>
|
|
1349
|
-
row
|
|
1350
|
-
.getAllCells()
|
|
1351
|
-
.some((cell) =>
|
|
1352
|
-
normalizeForFilter(String(cell.getValue() ?? ""), (props.filterLocale ?? props.localization?.locale))
|
|
1353
|
-
.includes(needle),
|
|
1354
|
-
),
|
|
1355
|
-
);
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1360
|
+
/**
|
|
1361
|
+
* The per-column filter stages: the menu's (up to two) conditions, then the
|
|
1362
|
+
* set-filter checklists. Split out of `allRowsBeforePagination` so the facet
|
|
1363
|
+
* list can run the SAME pipeline with its own column left out - a checklist
|
|
1364
|
+
* that offers values excluded by the other columns' filters sends you to an
|
|
1365
|
+
* empty grid. Re-implementing the stages beside the original would have let
|
|
1366
|
+
* the two drift.
|
|
1367
|
+
*
|
|
1368
|
+
* `excludeColumnId` omits that column's own filters, which is what "every
|
|
1369
|
+
* other filter" means for the column whose menu is open.
|
|
1370
|
+
*/
|
|
1371
|
+
function applyColumnFilters(
|
|
1372
|
+
inputRows: Array<Row<TData>>,
|
|
1373
|
+
excludeColumnId?: string,
|
|
1374
|
+
): Array<Row<TData>> {
|
|
1375
|
+
let rows = inputRows;
|
|
1358
1376
|
/**
|
|
1359
1377
|
* Resolve a column ONCE and return a reader for it.
|
|
1360
1378
|
*
|
|
@@ -1396,7 +1414,8 @@ export function createSvGridController<
|
|
|
1396
1414
|
{ locale: (props.filterLocale ?? props.localization?.locale) },
|
|
1397
1415
|
);
|
|
1398
1416
|
// A column filter is active if either of its (up to two) conditions is.
|
|
1399
|
-
const menuFilters = Object.entries(filterMenuValues).filter(([
|
|
1417
|
+
const menuFilters = Object.entries(filterMenuValues).filter(([id, f]) => {
|
|
1418
|
+
if (id === excludeColumnId) return false;
|
|
1400
1419
|
const a = condActive(f.operator, f.value, f.valueTo);
|
|
1401
1420
|
const b = !!f.operator2 && condActive(f.operator2, f.value2 ?? "", f.valueTo2);
|
|
1402
1421
|
return a || b;
|
|
@@ -1431,7 +1450,9 @@ export function createSvGridController<
|
|
|
1431
1450
|
);
|
|
1432
1451
|
}
|
|
1433
1452
|
|
|
1434
|
-
const valueFilterEntries = Object.entries(valueFilters)
|
|
1453
|
+
const valueFilterEntries = Object.entries(valueFilters).filter(
|
|
1454
|
+
([id]) => id !== excludeColumnId,
|
|
1455
|
+
);
|
|
1435
1456
|
if (valueFilterEntries.length) {
|
|
1436
1457
|
// Resolve bucket defs up front so we don't re-hit the derived map
|
|
1437
1458
|
// for every row × column combination. Columns without bucketing
|
|
@@ -1461,6 +1482,40 @@ export function createSvGridController<
|
|
|
1461
1482
|
);
|
|
1462
1483
|
}
|
|
1463
1484
|
|
|
1485
|
+
return rows;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const allRowsBeforePagination = $derived.by(function allRowsBeforePagination_d() {
|
|
1489
|
+
// Depend on dataStateVersion (row-model-affecting store changes) NOT
|
|
1490
|
+
// gridStateVersion - so moving the active cell / selection does not force
|
|
1491
|
+
// this O(rows) pipeline to re-run. Filter-input state (globalFilter etc.)
|
|
1492
|
+
// and internalData are read below and tracked as their own dependencies.
|
|
1493
|
+
dataStateVersion;
|
|
1494
|
+
// Touch internalData + internalColumns so the row model re-derives when
|
|
1495
|
+
// the consumer replaces the data array (e.g. via a "Reset" button).
|
|
1496
|
+
void internalData;
|
|
1497
|
+
void internalColumns;
|
|
1498
|
+
const rawRows = grid.getRowModel().rows;
|
|
1499
|
+
// External-filter mode: the consumer fetched / pre-filtered the rows
|
|
1500
|
+
// themselves (server-side data sources). Skip every local filter pass
|
|
1501
|
+
// so the data isn't double-filtered against the visible page.
|
|
1502
|
+
if (externalFilterEnabled) return rawRows;
|
|
1503
|
+
|
|
1504
|
+
let rows = rawRows;
|
|
1505
|
+
if (globalFilter.trim()) {
|
|
1506
|
+
const needle = normalizeForFilter(globalFilter, (props.filterLocale ?? props.localization?.locale));
|
|
1507
|
+
rows = rows.filter((row) =>
|
|
1508
|
+
row
|
|
1509
|
+
.getAllCells()
|
|
1510
|
+
.some((cell) =>
|
|
1511
|
+
normalizeForFilter(String(cell.getValue() ?? ""), (props.filterLocale ?? props.localization?.locale))
|
|
1512
|
+
.includes(needle),
|
|
1513
|
+
),
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
rows = applyColumnFilters(rows);
|
|
1518
|
+
|
|
1464
1519
|
// --- Advanced filter (Pro) ---------------------------------------------
|
|
1465
1520
|
// Runs LAST for two reasons: it evaluates over the smallest row set, and
|
|
1466
1521
|
// its aggregates (`SUM(amount) > 1000`) fold over what the user is
|
|
@@ -1647,6 +1702,48 @@ export function createSvGridController<
|
|
|
1647
1702
|
return grid.getState().rowSelection ?? {};
|
|
1648
1703
|
});
|
|
1649
1704
|
|
|
1705
|
+
// ---- Selection bar ----------------------------------------------------
|
|
1706
|
+
// The floating bulk-action bar. Off unless asked for: it overlaps a row, and
|
|
1707
|
+
// what belongs on it is app-specific. The RENDERER is Enterprise and arrives
|
|
1708
|
+
// through the selection-bar-view seam; everything here is free.
|
|
1709
|
+
const selectionBarOn = $derived(props.selectionBar !== undefined && props.selectionBar !== false);
|
|
1710
|
+
/** The prop in its three shapes - `true`, an action array, or a full config -
|
|
1711
|
+
* normalised to the config object the renderer reads. */
|
|
1712
|
+
const selectionBarConfig = $derived.by(function selectionBarConfig_d() {
|
|
1713
|
+
const raw = props.selectionBar;
|
|
1714
|
+
if (Array.isArray(raw)) return { actions: raw };
|
|
1715
|
+
if (raw && typeof raw === "object") return raw as Record<string, unknown>;
|
|
1716
|
+
return {};
|
|
1717
|
+
});
|
|
1718
|
+
const selectionBarActions = $derived(
|
|
1719
|
+
(selectionBarConfig.actions as unknown[] | undefined) ?? [],
|
|
1720
|
+
);
|
|
1721
|
+
const selectionBarPosition = $derived(
|
|
1722
|
+
selectionBarConfig.position === "top" ? "top" : "bottom",
|
|
1723
|
+
);
|
|
1724
|
+
const selectionBarMaxVisible = $derived(
|
|
1725
|
+
typeof selectionBarConfig.maxVisible === "number" && selectionBarConfig.maxVisible > 0
|
|
1726
|
+
? selectionBarConfig.maxVisible
|
|
1727
|
+
: 6,
|
|
1728
|
+
);
|
|
1729
|
+
const selectionBarHideClear = $derived(selectionBarConfig.hideClear === true);
|
|
1730
|
+
/** Selected rows in DISPLAY order, plus their ids - what every action on the
|
|
1731
|
+
* bar receives, and what its count shows. Built from `allRows` rather than
|
|
1732
|
+
* the selection map so the order matches what the user is looking at. */
|
|
1733
|
+
const selectionBarTarget = $derived.by(function selectionBarTarget_d() {
|
|
1734
|
+
const rows: TData[] = [];
|
|
1735
|
+
const ids: string[] = [];
|
|
1736
|
+
if (!selectionBarOn) return { rows, ids };
|
|
1737
|
+
for (const row of allRows) {
|
|
1738
|
+
if (isGroupRow(row)) continue;
|
|
1739
|
+
if (rowSelectionState[row.id]) {
|
|
1740
|
+
rows.push(row.original as TData);
|
|
1741
|
+
ids.push(row.id);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return { rows, ids };
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1650
1747
|
// Forward selection changes to the consumer. Skips the very first invocation
|
|
1651
1748
|
// (the initial empty state) so consumers don't get a spurious callback on mount.
|
|
1652
1749
|
let lastSelectionSerialized = "";
|
|
@@ -3297,22 +3394,60 @@ export function createSvGridController<
|
|
|
3297
3394
|
* column id (not just the open menu) so the filter row can drive it too.
|
|
3298
3395
|
*/
|
|
3299
3396
|
function facetValuesForColumn(columnId: string): Array<string> {
|
|
3397
|
+
return facetEntriesForColumn(columnId).map((entry) => entry.value);
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
/**
|
|
3401
|
+
* The values a column's checklist offers, with the row count behind each.
|
|
3402
|
+
*
|
|
3403
|
+
* Scoped to the rows passing every OTHER column's filters, not to the whole
|
|
3404
|
+
* dataset. Offering the full set means a checklist full of values that
|
|
3405
|
+
* cannot appear: filter Country to Germany and the City list still showed
|
|
3406
|
+
* every city on file, most of them a one-click route to an empty grid.
|
|
3407
|
+
* The column's OWN filter is excluded, so unticking a value never makes the
|
|
3408
|
+
* rest of the list vanish underneath the pointer.
|
|
3409
|
+
*/
|
|
3410
|
+
function facetEntriesForColumn(
|
|
3411
|
+
columnId: string,
|
|
3412
|
+
): Array<{ value: string; count: number | null }> {
|
|
3300
3413
|
if (!columnId) return [];
|
|
3301
|
-
// Server-provided distinct values win (fetched + cached above).
|
|
3302
|
-
|
|
3414
|
+
// Server-provided distinct values win (fetched + cached above). The grid
|
|
3415
|
+
// has not seen the rows behind them, so there is nothing to count.
|
|
3416
|
+
if (props.serverFilterValues)
|
|
3417
|
+
return (serverFacetValues[columnId] ?? []).map((value) => ({
|
|
3418
|
+
value,
|
|
3419
|
+
count: null,
|
|
3420
|
+
}));
|
|
3303
3421
|
const column = allColumns.find((entry) => entry.id === columnId);
|
|
3304
3422
|
if (!column) return [];
|
|
3305
|
-
|
|
3423
|
+
|
|
3424
|
+
const scope = applyColumnFilters(grid.getRowModel().rows, columnId);
|
|
3306
3425
|
const buckets = facetBucketsByColumn.get(columnId);
|
|
3307
|
-
if (buckets)
|
|
3426
|
+
if (buckets) {
|
|
3427
|
+
// Range-bucketed facets for numeric / date columns with many values.
|
|
3428
|
+
const counts = new Map<string, number>(buckets.map((b) => [b.label, 0]));
|
|
3429
|
+
for (const row of scope) {
|
|
3430
|
+
const num = rawToNumber(getColumnBaseValue(row, column), buckets[0]!.isDate);
|
|
3431
|
+
if (!Number.isFinite(num)) continue;
|
|
3432
|
+
for (const bucket of buckets) {
|
|
3433
|
+
if (isInBucket(num, bucket)) {
|
|
3434
|
+
counts.set(bucket.label, (counts.get(bucket.label) ?? 0) + 1);
|
|
3435
|
+
break;
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
return buckets.map((b) => ({ value: b.label, count: counts.get(b.label) ?? 0 }));
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3308
3442
|
// Default: distinct-value facets.
|
|
3309
|
-
const
|
|
3310
|
-
for (const
|
|
3311
|
-
|
|
3443
|
+
const counts = new Map<string, number>();
|
|
3444
|
+
for (const row of scope) {
|
|
3445
|
+
const value = String(getColumnBaseValue(row, column) ?? "");
|
|
3446
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
3312
3447
|
}
|
|
3313
|
-
return Array.from(
|
|
3314
|
-
a.localeCompare(b, undefined, { numeric: true })
|
|
3315
|
-
|
|
3448
|
+
return Array.from(counts.keys())
|
|
3449
|
+
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
3450
|
+
.map((value) => ({ value, count: counts.get(value) ?? 0 }));
|
|
3316
3451
|
}
|
|
3317
3452
|
|
|
3318
3453
|
/**
|
|
@@ -3334,10 +3469,12 @@ export function createSvGridController<
|
|
|
3334
3469
|
// out a fresh empty array/set on every read.
|
|
3335
3470
|
const EMPTY_FACETS: Array<string> = [];
|
|
3336
3471
|
const EMPTY_FACET_SET: ReadonlySet<string> = new Set<string>();
|
|
3472
|
+
const EMPTY_FACET_ENTRIES: Array<{ value: string; count: number | null }> = [];
|
|
3337
3473
|
let facetCache: {
|
|
3338
3474
|
columnId: string;
|
|
3339
3475
|
source: Array<string> | null;
|
|
3340
3476
|
values: Array<string>;
|
|
3477
|
+
counts: Map<string, number>;
|
|
3341
3478
|
} | null = null;
|
|
3342
3479
|
const columnMenuFacetValues = $derived.by(function columnMenuFacetValues_d() {
|
|
3343
3480
|
// The funnel popover drives via `filterMenuFor`; the column menu's Filter
|
|
@@ -3353,10 +3490,14 @@ export function createSvGridController<
|
|
|
3353
3490
|
if (facetCache?.columnId === columnId && facetCache.source === source) {
|
|
3354
3491
|
return facetCache.values;
|
|
3355
3492
|
}
|
|
3493
|
+
const entries = untrack(() => facetEntriesForColumn(columnId));
|
|
3356
3494
|
facetCache = {
|
|
3357
3495
|
columnId,
|
|
3358
3496
|
source,
|
|
3359
|
-
values:
|
|
3497
|
+
values: entries.map((e) => e.value),
|
|
3498
|
+
counts: new Map(
|
|
3499
|
+
entries.flatMap((e) => (e.count == null ? [] : [[e.value, e.count] as const])),
|
|
3500
|
+
),
|
|
3360
3501
|
};
|
|
3361
3502
|
return facetCache.values;
|
|
3362
3503
|
});
|
|
@@ -3378,12 +3519,22 @@ export function createSvGridController<
|
|
|
3378
3519
|
});
|
|
3379
3520
|
|
|
3380
3521
|
/** Visible facets as listbox options ((Blanks) label for the empty value). */
|
|
3381
|
-
const columnMenuFacetOptions = $derived(
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3522
|
+
const columnMenuFacetOptions = $derived.by(function columnMenuFacetOptions_d() {
|
|
3523
|
+
// Reading the values first materializes the snapshot (and its counts).
|
|
3524
|
+
const values = columnMenuVisibleFacets;
|
|
3525
|
+
const counts = facetCache?.counts;
|
|
3526
|
+
return values.map((value) => {
|
|
3527
|
+
const count = counts?.get(value);
|
|
3528
|
+
return {
|
|
3529
|
+
value,
|
|
3530
|
+
label: value === "" ? "(Blanks)" : value,
|
|
3531
|
+
// How many rows this value would keep, given the other columns'
|
|
3532
|
+
// filters. Absent in server-values mode, where the grid never sees
|
|
3533
|
+
// the rows behind the list.
|
|
3534
|
+
hint: count == null ? undefined : String(count),
|
|
3535
|
+
};
|
|
3536
|
+
});
|
|
3537
|
+
});
|
|
3387
3538
|
|
|
3388
3539
|
/**
|
|
3389
3540
|
* Checked facets for the open menu. An ABSENT `valueFilters` entry means
|
|
@@ -3396,26 +3547,45 @@ export function createSvGridController<
|
|
|
3396
3547
|
return valueFilters[columnId] ?? new Set(columnMenuFacetValues);
|
|
3397
3548
|
});
|
|
3398
3549
|
|
|
3550
|
+
/**
|
|
3551
|
+
* The offered values, in their own derived so the QUERY does not re-run the
|
|
3552
|
+
* scan. It used to sit in `inSuggestValues` beside the `inSuggestQuery`
|
|
3553
|
+
* read, so every keystroke in a chip input rebuilt the distinct-value set -
|
|
3554
|
+
* and once the scan started narrowing itself against the other columns'
|
|
3555
|
+
* filters, every keystroke ran the filter pipeline too. Split out, it
|
|
3556
|
+
* re-runs only when the dropdown moves to another column.
|
|
3557
|
+
*/
|
|
3558
|
+
const inSuggestEntries = $derived.by(function inSuggestEntries_d() {
|
|
3559
|
+
const columnId = inSuggestFor;
|
|
3560
|
+
if (!columnId) return EMPTY_FACET_ENTRIES;
|
|
3561
|
+
return untrack(() => facetEntriesForColumn(columnId));
|
|
3562
|
+
});
|
|
3563
|
+
|
|
3399
3564
|
// Distinct values offered in the `in` / `notIn` suggestions dropdown for the
|
|
3400
3565
|
// active chip input, narrowed by whatever the user has typed. Uncapped - the
|
|
3401
3566
|
// dropdown windows its rows, so a high-cardinality column costs a list of
|
|
3402
3567
|
// strings, not thousands of nodes.
|
|
3403
3568
|
const inSuggestValues = $derived.by(function inSuggestValues_d() {
|
|
3404
|
-
if (!inSuggestFor) return EMPTY_FACETS;
|
|
3405
3569
|
const query = inSuggestQuery.trim().toLowerCase();
|
|
3406
|
-
const all =
|
|
3407
|
-
return
|
|
3408
|
-
? all.filter((
|
|
3409
|
-
|
|
3570
|
+
const all = inSuggestEntries;
|
|
3571
|
+
return (
|
|
3572
|
+
query ? all.filter((e) => e.value.toLowerCase().includes(query)) : all
|
|
3573
|
+
).map((e) => e.value);
|
|
3410
3574
|
});
|
|
3411
3575
|
|
|
3412
3576
|
/** `in` / `notIn` suggestions as listbox options. */
|
|
3413
|
-
const inSuggestOptions = $derived(
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3577
|
+
const inSuggestOptions = $derived.by(function inSuggestOptions_d() {
|
|
3578
|
+
const query = inSuggestQuery.trim().toLowerCase();
|
|
3579
|
+
const shown = query
|
|
3580
|
+
? inSuggestEntries.filter((e) => e.value.toLowerCase().includes(query))
|
|
3581
|
+
: inSuggestEntries;
|
|
3582
|
+
return shown.map((e) => ({
|
|
3583
|
+
value: e.value,
|
|
3584
|
+
label: e.value === "" ? "(Blanks)" : e.value,
|
|
3585
|
+
// Same match count the menu's checklist shows.
|
|
3586
|
+
hint: e.count == null ? undefined : String(e.count),
|
|
3587
|
+
}));
|
|
3588
|
+
});
|
|
3419
3589
|
|
|
3420
3590
|
|
|
3421
3591
|
|
|
@@ -3482,6 +3652,10 @@ export function createSvGridController<
|
|
|
3482
3652
|
set isDraggingSelection(v) { isDraggingSelection = v as never; },
|
|
3483
3653
|
get fillDrag() { return fillDrag; },
|
|
3484
3654
|
set fillDrag(v) { fillDrag = v as never; },
|
|
3655
|
+
get moveDrag() { return moveDrag; },
|
|
3656
|
+
set moveDrag(v) { moveDrag = v as never; },
|
|
3657
|
+
get moveGrabHover() { return moveGrabHover; },
|
|
3658
|
+
set moveGrabHover(v) { moveGrabHover = v as never; },
|
|
3485
3659
|
get activeAtPointerDown() { return activeAtPointerDown; },
|
|
3486
3660
|
set activeAtPointerDown(v) { activeAtPointerDown = v as never; },
|
|
3487
3661
|
get editingCell() { return editingCell; },
|
|
@@ -3585,6 +3759,7 @@ export function createSvGridController<
|
|
|
3585
3759
|
get showRowSelectionEffective() { return showRowSelectionEffective; },
|
|
3586
3760
|
get rowSummariesEnabled() { return rowSummariesEnabled; },
|
|
3587
3761
|
get enableCellSelectionEffective() { return enableCellSelectionEffective; },
|
|
3762
|
+
get moveCellsEffective() { return moveCellsEffective; },
|
|
3588
3763
|
get flushScheduledScrollSync() { return flushScheduledScrollSync; },
|
|
3589
3764
|
get scheduleScrollSync() { return scheduleScrollSync; },
|
|
3590
3765
|
get internalData() { return internalData; },
|
|
@@ -3666,6 +3841,12 @@ export function createSvGridController<
|
|
|
3666
3841
|
get allRowsBeforePagination() { return allRowsBeforePagination; },
|
|
3667
3842
|
get allRows() { return allRows; },
|
|
3668
3843
|
get rowSelectionState() { return rowSelectionState; },
|
|
3844
|
+
get selectionBarOn() { return selectionBarOn; },
|
|
3845
|
+
get selectionBarActions() { return selectionBarActions; },
|
|
3846
|
+
get selectionBarPosition() { return selectionBarPosition; },
|
|
3847
|
+
get selectionBarMaxVisible() { return selectionBarMaxVisible; },
|
|
3848
|
+
get selectionBarHideClear() { return selectionBarHideClear; },
|
|
3849
|
+
get selectionBarTarget() { return selectionBarTarget; },
|
|
3669
3850
|
get lastSelectionSerialized() { return lastSelectionSerialized; },
|
|
3670
3851
|
set lastSelectionSerialized(v) { lastSelectionSerialized = v as never; },
|
|
3671
3852
|
get lastCellRangeSerialized() { return lastCellRangeSerialized; },
|
|
@@ -3914,6 +4095,7 @@ export function createSvGridController<
|
|
|
3914
4095
|
get getSelectionRects() { return getSelectionRects; },
|
|
3915
4096
|
get fillHandleCell() { return fillHandleCell; },
|
|
3916
4097
|
get isInFillPreview() { return isInFillPreview; },
|
|
4098
|
+
get dragPreviewRect() { return dragPreviewRect; },
|
|
3917
4099
|
get fillMarqueeEdges() { return fillMarqueeEdges; },
|
|
3918
4100
|
get findColumnById() { return findColumnById; },
|
|
3919
4101
|
/**
|
|
@@ -3932,6 +4114,14 @@ export function createSvGridController<
|
|
|
3932
4114
|
get startFillDrag() { return startFillDrag; },
|
|
3933
4115
|
get onFillPointerMove() { return onFillPointerMove; },
|
|
3934
4116
|
get onFillPointerUp() { return onFillPointerUp; },
|
|
4117
|
+
get isOnMoveGrabStrip() { return isOnMoveGrabStrip; },
|
|
4118
|
+
get startMoveDrag() { return startMoveDrag; },
|
|
4119
|
+
get onMovePointerMove() { return onMovePointerMove; },
|
|
4120
|
+
get onMovePointerUp() { return onMovePointerUp; },
|
|
4121
|
+
get trackEdgeScroll() { return trackEdgeScroll; },
|
|
4122
|
+
get stopEdgeScroll() { return stopEdgeScroll; },
|
|
4123
|
+
get applyMoveRange() { return applyMoveRange; },
|
|
4124
|
+
get moveDestRect() { return moveDestRect; },
|
|
3935
4125
|
get toggleBooleanCell() { return toggleBooleanCell; },
|
|
3936
4126
|
get onCellPointerDown() { return onCellPointerDown; },
|
|
3937
4127
|
get onCellPointerEnter() { return onCellPointerEnter; },
|
|
@@ -4034,12 +4224,12 @@ export function createSvGridController<
|
|
|
4034
4224
|
const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, addFilterToken, removeFilterToken, toggleFilterToken, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openInSuggest, closeInSuggest, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, setFacetSelection, setFilterTokens, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus<TFeatures, TData>(ctx);
|
|
4035
4225
|
const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, areEditorOptionsLoading, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender<TFeatures, TData>(ctx);
|
|
4036
4226
|
const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing<TFeatures, TData>(ctx);
|
|
4037
|
-
const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
|
|
4227
|
+
const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, dragPreviewRect, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
|
|
4038
4228
|
const { cellPinStyle, isColumnPinned, getCurrentColumnOrder, emitColumnOrder, setColumnOrderInternal, applyColumnDrop, onColumnHeaderDragStart, onColumnHeaderDragOver, onColumnHeaderDragLeave, onColumnHeaderDrop, onColumnHeaderDragEnd, pinColumnLeft, pinColumnRight, unpinColumn, toggleColumnVisibleInPanel, moveColumnInPanel, toggleGroupInPanel, getColumnBaseWidth, getColumnWidth, measureText, autosizeColumn, autosizeAllColumns, resetColumns } = createColumns<TFeatures, TData>(ctx);
|
|
4039
4229
|
const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd, onRowPointerDown, destroyRowDrag } = createRowDrag<TFeatures, TData>(ctx);
|
|
4040
4230
|
const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids<TFeatures, TData>(ctx);
|
|
4041
4231
|
const { buildApi } = createGridApi<TFeatures, TData>(ctx);
|
|
4042
|
-
const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
|
|
4232
|
+
const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, isOnMoveGrabStrip, startMoveDrag, onMovePointerMove, onMovePointerUp, applyMoveRange, moveDestRect, trackEdgeScroll, stopEdgeScroll, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
|
|
4043
4233
|
|
|
4044
4234
|
// Dev-time configuration checks. Silent misconfiguration was the grid's
|
|
4045
4235
|
// biggest usability gap - a misspelled `field` rendered a column of blank
|