@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/dist/selection.js
CHANGED
|
@@ -224,15 +224,21 @@ export function createSelection(ctx) {
|
|
|
224
224
|
* that contains the cell are returned (active range checked last so its
|
|
225
225
|
* outline wins on overlap). Returns null when the cell is in no range.
|
|
226
226
|
*/
|
|
227
|
+
/** Could `getSelectionRects()` return anything? An anchored active range or
|
|
228
|
+
* a committed one is the only way a rect can exist, and both are plain
|
|
229
|
+
* property reads - so this is the cheap guard in front of anything that
|
|
230
|
+
* would otherwise allocate a rect array to discover there is nothing. */
|
|
231
|
+
function hasAnySelectionRect() {
|
|
232
|
+
const active = ctx.selectionRange;
|
|
233
|
+
const committed = ctx.selectionRanges;
|
|
234
|
+
return Boolean(committed?.length) || Boolean(active?.anchor && active?.focus);
|
|
235
|
+
}
|
|
227
236
|
function getCellRangeEdges(rowIndex, colIndex) {
|
|
228
237
|
// Bail before `getSelectionRects()`, which allocates an array and resolves
|
|
229
238
|
// every range. This runs once per rendered CELL on every render - about 250
|
|
230
239
|
// times per frame on a default grid - and with nothing selected it did all
|
|
231
|
-
// that work to return null every time.
|
|
232
|
-
|
|
233
|
-
const active = ctx.selectionRange;
|
|
234
|
-
const committed = ctx.selectionRanges;
|
|
235
|
-
if (!committed?.length && !(active?.anchor && active?.focus))
|
|
240
|
+
// that work to return null every time.
|
|
241
|
+
if (!hasAnySelectionRect())
|
|
236
242
|
return null;
|
|
237
243
|
for (const rect of getSelectionRects()) {
|
|
238
244
|
if (rowIndex < rect.minRow ||
|
|
@@ -250,10 +256,40 @@ export function createSelection(ctx) {
|
|
|
250
256
|
}
|
|
251
257
|
return null;
|
|
252
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* The rectangle a drag is currently promising, or null when no drag is in
|
|
261
|
+
* flight. A fill grows the source rectangle out to the pointer; a move
|
|
262
|
+
* detaches an identically-sized rectangle and drops it at the offset the
|
|
263
|
+
* pointer has travelled. Both feed the same preview tint and marquee, so a
|
|
264
|
+
* range move looks like the fill handle users already know.
|
|
265
|
+
*/
|
|
266
|
+
function dragPreviewRect() {
|
|
267
|
+
const m = ctx.moveDrag;
|
|
268
|
+
if (m)
|
|
269
|
+
return ctx.moveDestRect(m);
|
|
270
|
+
const d = ctx.fillDrag;
|
|
271
|
+
if (!d)
|
|
272
|
+
return null;
|
|
273
|
+
return {
|
|
274
|
+
minRow: Math.min(d.sourceMinRow, d.targetRow),
|
|
275
|
+
maxRow: Math.max(d.sourceMaxRow, d.targetRow),
|
|
276
|
+
minCol: Math.min(d.sourceMinCol, d.targetCol),
|
|
277
|
+
maxCol: Math.max(d.sourceMaxCol, d.targetCol),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
253
280
|
/** Returns true when the given cell is inside the fill-drag preview
|
|
254
281
|
* range BUT outside the original source range. Used to paint a
|
|
255
282
|
* dashed-outline preview while the user is dragging the handle. */
|
|
256
283
|
function isInFillPreview(rowIndex, colIndex) {
|
|
284
|
+
if (ctx.moveDrag) {
|
|
285
|
+
const rect = dragPreviewRect();
|
|
286
|
+
if (!rect)
|
|
287
|
+
return false;
|
|
288
|
+
return (rowIndex >= rect.minRow &&
|
|
289
|
+
rowIndex <= rect.maxRow &&
|
|
290
|
+
colIndex >= rect.minCol &&
|
|
291
|
+
colIndex <= rect.maxCol);
|
|
292
|
+
}
|
|
257
293
|
const d = ctx.fillDrag;
|
|
258
294
|
if (!d)
|
|
259
295
|
return false;
|
|
@@ -279,13 +315,14 @@ export function createSelection(ctx) {
|
|
|
279
315
|
* border. Returns null when the cell isn't on the rectangle's boundary.
|
|
280
316
|
*/
|
|
281
317
|
function fillMarqueeEdges(rowIndex, colIndex) {
|
|
282
|
-
|
|
283
|
-
|
|
318
|
+
// Cheap bail before the shared rect helper, which allocates: this runs
|
|
319
|
+
// once per rendered cell and no drag is in flight almost all the time.
|
|
320
|
+
if (!ctx.fillDrag && !ctx.moveDrag)
|
|
284
321
|
return null;
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const maxC =
|
|
322
|
+
const rect = dragPreviewRect();
|
|
323
|
+
if (!rect)
|
|
324
|
+
return null;
|
|
325
|
+
const { minRow: minR, maxRow: maxR, minCol: minC, maxCol: maxC } = rect;
|
|
289
326
|
if (rowIndex < minR || rowIndex > maxR || colIndex < minC || colIndex > maxC) {
|
|
290
327
|
return null;
|
|
291
328
|
}
|
|
@@ -309,6 +346,18 @@ export function createSelection(ctx) {
|
|
|
309
346
|
const column = ctx.allColumns[colIndex];
|
|
310
347
|
if (!row || !column || isGroupRow(row))
|
|
311
348
|
return;
|
|
349
|
+
// A pointerdown on the outer few pixels of a selection border grabs the
|
|
350
|
+
// range to move it. Checked before anything else, because everything below
|
|
351
|
+
// would first collapse the selection this drag is about to relocate.
|
|
352
|
+
// Returns false unless the pointer really was on the strip, so a click
|
|
353
|
+
// anywhere further inside falls straight through to the old behaviour.
|
|
354
|
+
if (ctx.startMoveDrag(event, rowIndex, colIndex)) {
|
|
355
|
+
// A grab that never moves still ends in a `click` on the cell under the
|
|
356
|
+
// border. Left alone that click would collapse the range the user was
|
|
357
|
+
// clearly reaching for, and on a checkbox column it would flip the value.
|
|
358
|
+
moveGrabSwallowsClick = true;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
312
361
|
// NOTE: we no longer bail for checkbox columns. Bailing here meant a
|
|
313
362
|
// range drag could never START on a checkbox cell. A plain click still
|
|
314
363
|
// toggles the checkbox via onCellClick (a same-cell click), while a
|
|
@@ -344,27 +393,93 @@ export function createSelection(ctx) {
|
|
|
344
393
|
extendSelection(rowIndex, colIndex);
|
|
345
394
|
setActiveCell(rowIndex, colIndex);
|
|
346
395
|
}
|
|
347
|
-
|
|
396
|
+
/** `event` is the window `pointerup` when this comes from the real gesture,
|
|
397
|
+
* and absent when `onWindowPointerMove` calls it to recover from a release
|
|
398
|
+
* it never saw. A range move reads its copy modifier from it, so holding
|
|
399
|
+
* Ctrl between the last movement and the release still copies. */
|
|
400
|
+
function endDragSelection(event) {
|
|
348
401
|
ctx.isDraggingSelection = false;
|
|
402
|
+
// Every drag shares one edge-scroll loop, and it exits on its own once no
|
|
403
|
+
// drag is live - but stopping it here cancels the pending frame rather
|
|
404
|
+
// than letting one more fire after the gesture is over.
|
|
405
|
+
ctx.stopEdgeScroll();
|
|
349
406
|
// Also commit a fill-handle drag if one was in progress - the user
|
|
350
407
|
// released the mouse, time to apply the pattern.
|
|
351
408
|
if (ctx.fillDrag)
|
|
352
409
|
ctx.onFillPointerUp();
|
|
410
|
+
if (ctx.moveDrag)
|
|
411
|
+
ctx.onMovePointerUp(event);
|
|
353
412
|
}
|
|
354
413
|
function onWindowPointerMove(event) {
|
|
414
|
+
if (ctx.moveDrag) {
|
|
415
|
+
ctx.onMovePointerMove(event);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
355
418
|
if (ctx.fillDrag) {
|
|
356
419
|
ctx.onFillPointerMove(event);
|
|
357
420
|
return;
|
|
358
421
|
}
|
|
359
|
-
if (!ctx.isDraggingSelection)
|
|
422
|
+
if (!ctx.isDraggingSelection) {
|
|
423
|
+
updateMoveGrabHover(event);
|
|
360
424
|
return;
|
|
425
|
+
}
|
|
361
426
|
// Safety: if no mouse button is held the drag is over (we may have
|
|
362
427
|
// missed pointerup because the user lifted outside the window).
|
|
363
428
|
if (event.buttons === 0) {
|
|
364
429
|
endDragSelection();
|
|
430
|
+
return;
|
|
365
431
|
}
|
|
432
|
+
// Drag-select past the viewport edge scrolls the grid, same as the fill
|
|
433
|
+
// and move drags. Without it a range can only ever cover what was already
|
|
434
|
+
// on screen when the drag started. `onCellPointerEnter` keeps extending
|
|
435
|
+
// the range for cells the pointer actually crosses; this covers the case
|
|
436
|
+
// where the pointer has stopped at the edge and the CELLS are moving.
|
|
437
|
+
ctx.trackEdgeScroll(event);
|
|
366
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Switch the grid to a move cursor while the pointer sits on the grab strip
|
|
441
|
+
* of a selection border - without it the feature is invisible, since the
|
|
442
|
+
* border is painted, not a hit target.
|
|
443
|
+
*
|
|
444
|
+
* This runs on every window pointermove, so the order of the guards is the
|
|
445
|
+
* point: `moveCellsEffective` and the already-hovering flag are plain
|
|
446
|
+
* property reads, and `isOnMoveGrabStrip` bails on `getCellRangeEdges`
|
|
447
|
+
* before measuring anything when no range is selected.
|
|
448
|
+
*/
|
|
449
|
+
function updateMoveGrabHover(event) {
|
|
450
|
+
const wasHovering = ctx.moveGrabHover;
|
|
451
|
+
// Two property reads before anything touches the DOM. `closest()` walks
|
|
452
|
+
// ancestors on EVERY pointer move over the page, so it has to sit behind
|
|
453
|
+
// both "the feature is on" and "there is a range to grab".
|
|
454
|
+
if (!ctx.moveCellsEffective || !hasAnySelectionRect()) {
|
|
455
|
+
if (wasHovering)
|
|
456
|
+
ctx.moveGrabHover = false;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const target = event.target;
|
|
460
|
+
const cell = target?.closest?.("td[data-svgrid-row][data-svgrid-col]");
|
|
461
|
+
let hovering = false;
|
|
462
|
+
if (cell) {
|
|
463
|
+
const r = Number(cell.dataset.svgridRow);
|
|
464
|
+
const c = Number(cell.dataset.svgridCol);
|
|
465
|
+
if (Number.isFinite(r) && Number.isFinite(c)) {
|
|
466
|
+
hovering = ctx.isOnMoveGrabStrip(cell, r, c, event.clientX, event.clientY);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (hovering !== wasHovering)
|
|
470
|
+
ctx.moveGrabHover = hovering;
|
|
471
|
+
}
|
|
472
|
+
/** Set when a pointerdown started a range move, so the `click` that
|
|
473
|
+
* follows a zero-distance grab is discarded instead of collapsing the
|
|
474
|
+
* selection it grabbed. One gesture, one flag - cleared on the next click
|
|
475
|
+
* whether or not the grab turned into a real drag. */
|
|
476
|
+
let moveGrabSwallowsClick = false;
|
|
367
477
|
function onCellClick(rowIndex, colIndex) {
|
|
478
|
+
if (moveGrabSwallowsClick) {
|
|
479
|
+
moveGrabSwallowsClick = false;
|
|
480
|
+
ctx.gridRootEl?.focus({ preventScroll: true });
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
368
483
|
const row = ctx.allRows[rowIndex];
|
|
369
484
|
const column = ctx.allColumns[colIndex];
|
|
370
485
|
if (!row || !column)
|
|
@@ -450,6 +565,7 @@ export function createSelection(ctx) {
|
|
|
450
565
|
fillMarqueeEdges,
|
|
451
566
|
getSelectionRects,
|
|
452
567
|
isInFillPreview,
|
|
568
|
+
dragPreviewRect,
|
|
453
569
|
findColumnById,
|
|
454
570
|
onCellPointerDown,
|
|
455
571
|
onCellPointerEnter,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svgrid/grid",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.23",
|
|
4
4
|
"description": "Svelte 5-native data grid and data table. Headless-first engine with virtual scrolling for 100k+ rows, Excel-style filtering, inline editing, grouping, tree data, pivot, and server-side data. Drop-in <SvGrid> component. TypeScript, SSR-ready.",
|
|
5
5
|
"author": "jQWidgets <boikom@jqwidgets.com>",
|
|
6
6
|
"license": "MIT",
|
package/src/GridMenus.svelte
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
} from "./SvGrid.types";
|
|
17
17
|
import {
|
|
18
18
|
getEditorInputType,
|
|
19
|
+
usesRichDateFilter,
|
|
20
|
+
toIsoDateLocal,
|
|
21
|
+
fromIsoDateLocal,
|
|
19
22
|
} from "./SvGrid.helpers";
|
|
20
23
|
|
|
21
24
|
import type { SvGridController } from "./SvGrid.controller.svelte";
|
|
@@ -72,11 +75,45 @@
|
|
|
72
75
|
// anything OUTSIDE them scrolls, or on resize. Scrolling the menu's own body
|
|
73
76
|
// or its facet list has to keep it open - hence onScrollOutside rather than a
|
|
74
77
|
// bare capture-phase listener.
|
|
78
|
+
//
|
|
79
|
+
// The cell context menu and the comment editor were missing from this gate.
|
|
80
|
+
// Both are positioned from a one-time cursor measurement and rendered
|
|
81
|
+
// `position: fixed`, so after a scroll they float over unrelated cells - the
|
|
82
|
+
// exact defect the effect exists to prevent. Their backdrop swallows a wheel
|
|
83
|
+
// over the grid, but it does not stop the PAGE scrolling underneath them, and
|
|
84
|
+
// it does not stop a programmatic scroll.
|
|
85
|
+
//
|
|
86
|
+
// The comment editor is dismissed by SAVING rather than discarding. Clicking
|
|
87
|
+
// the backdrop or pressing Escape throws the draft away, and that is fine -
|
|
88
|
+
// both are the user saying "away with this". A scroll is not a dismissal
|
|
89
|
+
// gesture, so it must not destroy text the user has typed.
|
|
75
90
|
$effect(() => {
|
|
76
|
-
if (
|
|
77
|
-
|
|
91
|
+
if (
|
|
92
|
+
!(
|
|
93
|
+
columnMenuFor ||
|
|
94
|
+
filterMenuFor ||
|
|
95
|
+
operatorMenuFor ||
|
|
96
|
+
chooseColumnsPos ||
|
|
97
|
+
contextMenuFor ||
|
|
98
|
+
commentEditFor
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
return;
|
|
102
|
+
const close = () => {
|
|
103
|
+
closeMenus();
|
|
104
|
+
closeContextMenu();
|
|
105
|
+
if (ctrl.commentEditFor) saveComment();
|
|
106
|
+
};
|
|
78
107
|
const offScroll = onScrollOutside(
|
|
79
|
-
|
|
108
|
+
// Scoped to THIS grid's menus. A bare document query treats a second
|
|
109
|
+
// grid's open menu as "inside", so scrolling in that other menu would
|
|
110
|
+
// leave this one stranded at its stale coordinates.
|
|
111
|
+
() =>
|
|
112
|
+
Array.from(
|
|
113
|
+
document.querySelectorAll<HTMLElement>(
|
|
114
|
+
`.sv-grid-menu[data-svgrid-menu="${ctrl.gridDomId}"]`,
|
|
115
|
+
),
|
|
116
|
+
),
|
|
80
117
|
close,
|
|
81
118
|
);
|
|
82
119
|
window.addEventListener('resize', close);
|
|
@@ -96,6 +133,71 @@
|
|
|
96
133
|
const clearGroupingFromMenu = $derived(ctrl.clearGroupingFromMenu);
|
|
97
134
|
const columnMenuFacetValues = $derived(ctrl.columnMenuFacetValues);
|
|
98
135
|
const columnMenuVisibleFacets = $derived(ctrl.columnMenuVisibleFacets);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* How many value rows the checklist shows. It was pinned at 7 regardless of
|
|
139
|
+
* the window, so a tall screen left most of the space under the menu empty
|
|
140
|
+
* while the list stayed a 7-row porthole over a few hundred values.
|
|
141
|
+
*
|
|
142
|
+
* Read once per open rather than on resize: the popover is transient, and
|
|
143
|
+
* the menu is already capped by its own `max-height`.
|
|
144
|
+
*/
|
|
145
|
+
const FACET_ROW_HEIGHT = 24;
|
|
146
|
+
const facetRows = $derived.by(function facetRows_d() {
|
|
147
|
+
if (typeof window === "undefined") return 7;
|
|
148
|
+
const top = (filterMenuFor ? filterMenuPos.y : columnMenuPos.y) || 0;
|
|
149
|
+
// What is left under the menu's top edge once the condition block, the
|
|
150
|
+
// search row and the action bar have taken their share.
|
|
151
|
+
const room = window.innerHeight - top - 320;
|
|
152
|
+
return Math.max(7, Math.min(16, Math.floor(room / FACET_ROW_HEIGHT)));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The same date controls the filter ROW uses. Without them a date column
|
|
157
|
+
* offered two different date UIs depending on which surface you touched:
|
|
158
|
+
* a rich picker under the header, a bare `<input type="date">` in the menu.
|
|
159
|
+
*
|
|
160
|
+
* Lazy even though GridMenus is itself a lazy chunk - the picker drags in
|
|
161
|
+
* the date-format engine, and most grids that open a column menu have no
|
|
162
|
+
* date column at all.
|
|
163
|
+
*/
|
|
164
|
+
let MenuDatePicker = $state<typeof import("./SvDateTimePicker.svelte").default | null>(null);
|
|
165
|
+
let MenuRangePicker = $state<typeof import("./SvDateRangeInput.svelte").default | null>(null);
|
|
166
|
+
$effect(() => {
|
|
167
|
+
const colId = filterMenuFor ?? columnMenuFor;
|
|
168
|
+
if (!colId) return;
|
|
169
|
+
const column = allColumns.find((c) => c.id === colId);
|
|
170
|
+
if (!usesRichDateFilter(column?.columnDef.editorType)) return;
|
|
171
|
+
if (!MenuDatePicker)
|
|
172
|
+
import("./SvDateTimePicker.svelte").then((m) => (MenuDatePicker = m.default));
|
|
173
|
+
if (!MenuRangePicker)
|
|
174
|
+
import("./SvDateRangeInput.svelte").then((m) => (MenuRangePicker = m.default));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Opening the filter popover used to leave focus on the funnel button
|
|
179
|
+
* outside it, so a keyboard user landed nowhere and had to tab in past the
|
|
180
|
+
* whole menu. Focus the first control instead - the condition value if the
|
|
181
|
+
* column has one, else the value search.
|
|
182
|
+
*/
|
|
183
|
+
function focusFilterPanel(node: HTMLElement) {
|
|
184
|
+
requestAnimationFrame(() => {
|
|
185
|
+
const first = node.querySelector<HTMLElement>(
|
|
186
|
+
".sv-grid-menu-condition-value, .sv-dtp__input, .sv-dri__input, .sv-grid-menu-search",
|
|
187
|
+
);
|
|
188
|
+
first?.focus({ preventScroll: true });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Enter anywhere in the panel means "done" - filtering is already live. */
|
|
193
|
+
function onFilterPanelKeydown(event: KeyboardEvent) {
|
|
194
|
+
if (event.key !== "Enter" || event.shiftKey) return;
|
|
195
|
+
const target = event.target as HTMLElement | null;
|
|
196
|
+
// Let the listbox and real buttons keep Enter for their own activation.
|
|
197
|
+
if (target?.closest(".sv-listbox") || target?.tagName === "BUTTON") return;
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
closeMenus();
|
|
200
|
+
}
|
|
99
201
|
const columnMenuFacetOptions = $derived(ctrl.columnMenuFacetOptions);
|
|
100
202
|
const columnMenuSelectedFacets = $derived(ctrl.columnMenuSelectedFacets);
|
|
101
203
|
const setFacetSelection = $derived(ctrl.setFacetSelection);
|
|
@@ -187,6 +289,21 @@
|
|
|
187
289
|
{@const menuOperatorOptions = operatorsForColumn(menuColumn)}
|
|
188
290
|
{@const menuActiveOperator = filterMenuValues[colId]?.operator ?? defaultOperatorFor(menuColumn)}
|
|
189
291
|
{@const menuInputType = getEditorInputType((menuColumn?.columnDef.editorType ?? "text") as CellEditorType)}
|
|
292
|
+
{@const richDate =
|
|
293
|
+
usesRichDateFilter(menuColumn?.columnDef.editorType) &&
|
|
294
|
+
menuActiveOperator !== "isBlank" &&
|
|
295
|
+
menuActiveOperator !== "isNotBlank" &&
|
|
296
|
+
menuActiveOperator !== "regex" &&
|
|
297
|
+
menuActiveOperator !== "in" &&
|
|
298
|
+
menuActiveOperator !== "notIn"}
|
|
299
|
+
{@const searching =
|
|
300
|
+
columnMenuVisibleFacets.length !== columnMenuFacetValues.length}
|
|
301
|
+
{@const conditionActive =
|
|
302
|
+
menuActiveOperator === "isBlank" ||
|
|
303
|
+
menuActiveOperator === "isNotBlank" ||
|
|
304
|
+
(filterMenuValues[colId]?.value ?? "").trim().length > 0}
|
|
305
|
+
{@const pickedCount = columnMenuSelectedFacets.size}
|
|
306
|
+
{@const totalCount = columnMenuFacetValues.length}
|
|
190
307
|
<div class="sv-grid-menu-filter">
|
|
191
308
|
<div class="sv-grid-menu-filter-head">
|
|
192
309
|
<span class="sv-grid-header-icon">{@render icon("filter")}</span> Filter
|
|
@@ -204,6 +321,40 @@
|
|
|
204
321
|
{/each}
|
|
205
322
|
</select>
|
|
206
323
|
{#if menuActiveOperator !== "isBlank" && menuActiveOperator !== "isNotBlank"}
|
|
324
|
+
{#if richDate}
|
|
325
|
+
<!-- Same controls the filter row uses, so a date column behaves the
|
|
326
|
+
same wherever you touch it: a picker, or a range field for
|
|
327
|
+
`between`. `regex` and the set operators keep the text box - a
|
|
328
|
+
pattern and a token list are not dates. -->
|
|
329
|
+
{#if menuActiveOperator === "between"}
|
|
330
|
+
{#if MenuRangePicker}
|
|
331
|
+
{@const RangePicker = MenuRangePicker}
|
|
332
|
+
{@const from = fromIsoDateLocal(filterMenuValues[colId]?.value)}
|
|
333
|
+
{@const to = fromIsoDateLocal(filterMenuValues[colId]?.valueTo)}
|
|
334
|
+
<RangePicker
|
|
335
|
+
value={from && to ? [from, to] : null}
|
|
336
|
+
formatString="yyyy-MM-dd"
|
|
337
|
+
block
|
|
338
|
+
placeholder="Pick a range..."
|
|
339
|
+
onChange={(range) => {
|
|
340
|
+
updateFilterMenuValue(colId, range ? toIsoDateLocal(range[0]) : "");
|
|
341
|
+
updateFilterMenuValueTo(colId, range ? toIsoDateLocal(range[1]) : "");
|
|
342
|
+
}}
|
|
343
|
+
/>
|
|
344
|
+
{/if}
|
|
345
|
+
{:else if MenuDatePicker}
|
|
346
|
+
{@const DatePicker = MenuDatePicker}
|
|
347
|
+
<DatePicker
|
|
348
|
+
value={(filterMenuValues[colId]?.value ?? "") || null}
|
|
349
|
+
formatString="yyyy-MM-dd"
|
|
350
|
+
dropDownDisplayMode="calendar"
|
|
351
|
+
block
|
|
352
|
+
nullable
|
|
353
|
+
placeholder="Filter value..."
|
|
354
|
+
onChange={(d) => updateFilterMenuValue(colId, toIsoDateLocal(d))}
|
|
355
|
+
/>
|
|
356
|
+
{/if}
|
|
357
|
+
{:else}
|
|
207
358
|
{@const isSetOp =
|
|
208
359
|
menuActiveOperator === "in" || menuActiveOperator === "notIn"}
|
|
209
360
|
<!-- in/notIn drives the shared value-suggestions dropdown (a windowed
|
|
@@ -239,6 +390,9 @@
|
|
|
239
390
|
oninput={(event) => updateFilterMenuValueTo(colId, (event.currentTarget as HTMLInputElement).value)}
|
|
240
391
|
/>
|
|
241
392
|
{/if}
|
|
393
|
+
{/if}
|
|
394
|
+
<!-- A second condition is offered for every column type, date
|
|
395
|
+
included, so this sits outside the value-control branch. -->
|
|
242
396
|
{@const mf = filterMenuValues[colId]}
|
|
243
397
|
{#if mf?.operator2}
|
|
244
398
|
{@const op2 = mf.operator2 ?? defaultOperatorFor(menuColumn)}
|
|
@@ -288,7 +442,16 @@
|
|
|
288
442
|
{/if}
|
|
289
443
|
{/if}
|
|
290
444
|
<div class="sv-grid-menu-sep"></div>
|
|
291
|
-
<div class="sv-grid-menu-filter-head">
|
|
445
|
+
<div class="sv-grid-menu-filter-head">
|
|
446
|
+
Values
|
|
447
|
+
<!-- The condition above and this checklist are ANDed. Nothing said so,
|
|
448
|
+
so a row could vanish for a reason neither half showed on its own. -->
|
|
449
|
+
{#if conditionActive}
|
|
450
|
+
<span class="sv-grid-menu-head-note">and the condition above</span>
|
|
451
|
+
{:else if pickedCount < totalCount}
|
|
452
|
+
<span class="sv-grid-menu-head-note">{pickedCount} of {totalCount}</span>
|
|
453
|
+
{/if}
|
|
454
|
+
</div>
|
|
292
455
|
<input
|
|
293
456
|
class="sv-grid-menu-search"
|
|
294
457
|
placeholder="Search values..."
|
|
@@ -296,7 +459,11 @@
|
|
|
296
459
|
/>
|
|
297
460
|
<label class="sv-grid-facet sv-grid-facet-all">
|
|
298
461
|
<input type="checkbox" checked={isAllFacetsChecked(colId)} onchange={() => toggleAllFacets(colId)} />
|
|
299
|
-
|
|
462
|
+
<!-- Says what it will actually do: with a query typed it acts on the
|
|
463
|
+
matches only, leaving the rest of the selection untouched. -->
|
|
464
|
+
<span class="sv-grid-facet-label"
|
|
465
|
+
>{searching ? "(Select all results)" : "(Select all)"}</span
|
|
466
|
+
>
|
|
300
467
|
</label>
|
|
301
468
|
<!-- Windowed checklist. A high-cardinality column (10k distinct symbols
|
|
302
469
|
on a live feed) would otherwise mount a label + checkbox per value -
|
|
@@ -308,7 +475,7 @@
|
|
|
308
475
|
checkbox
|
|
309
476
|
multiple
|
|
310
477
|
virtual={columnMenuFacetOptions.length > FACET_VIRTUAL_THRESHOLD}
|
|
311
|
-
rows={
|
|
478
|
+
rows={facetRows}
|
|
312
479
|
rowHeight={24}
|
|
313
480
|
size="sm"
|
|
314
481
|
ariaLabel="Filter values"
|
|
@@ -348,6 +515,7 @@
|
|
|
348
515
|
{@const menuTab = ctrl.columnMenuTab}
|
|
349
516
|
<div
|
|
350
517
|
class="sv-grid-menu sv-grid-column-menu"
|
|
518
|
+
data-svgrid-menu={ctrl.gridDomId}
|
|
351
519
|
role="menu"
|
|
352
520
|
style={`left: ${columnMenuPos.x}px; top: ${columnMenuPos.y}px; max-height: calc(100vh - ${columnMenuPos.y}px - 12px);`}
|
|
353
521
|
>
|
|
@@ -535,10 +703,15 @@
|
|
|
535
703
|
{/if}
|
|
536
704
|
|
|
537
705
|
{#if filterMenuFor && showColumnFiltersEffective}
|
|
706
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
538
707
|
<div
|
|
539
708
|
class="sv-grid-menu sv-grid-filter-menu"
|
|
709
|
+
data-svgrid-menu={ctrl.gridDomId}
|
|
540
710
|
role="menu"
|
|
541
711
|
style={`left: ${filterMenuPos.x}px; top: ${filterMenuPos.y}px; max-height: calc(100vh - ${filterMenuPos.y}px - 12px);`}
|
|
712
|
+
tabindex="-1"
|
|
713
|
+
use:focusFilterPanel
|
|
714
|
+
onkeydown={onFilterPanelKeydown}
|
|
542
715
|
>
|
|
543
716
|
{@render filterPanelBody(filterMenuFor)}
|
|
544
717
|
</div>
|
|
@@ -548,6 +721,7 @@
|
|
|
548
721
|
{@const everyColumn = grid.getAllColumns()}
|
|
549
722
|
<div
|
|
550
723
|
class="sv-grid-menu sv-grid-choose-columns-menu"
|
|
724
|
+
data-svgrid-menu={ctrl.gridDomId}
|
|
551
725
|
role="menu"
|
|
552
726
|
style={`left: ${chooseColumnsPos.x}px; top: ${chooseColumnsPos.y}px;`}
|
|
553
727
|
>
|
|
@@ -576,6 +750,7 @@
|
|
|
576
750
|
filterMenuValues[opColumnId]?.operator ?? defaultOperatorFor(opColumn)}
|
|
577
751
|
<div
|
|
578
752
|
class="sv-grid-menu sv-grid-operator-menu"
|
|
753
|
+
data-svgrid-menu={ctrl.gridDomId}
|
|
579
754
|
role="menu"
|
|
580
755
|
style={`left: ${operatorMenuPos.x}px; top: ${operatorMenuPos.y}px;`}
|
|
581
756
|
>
|
|
@@ -613,6 +788,7 @@
|
|
|
613
788
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
614
789
|
<div
|
|
615
790
|
class="sv-grid-menu sv-grid-in-suggest"
|
|
791
|
+
data-svgrid-menu={ctrl.gridDomId}
|
|
616
792
|
style={`left: ${inSuggestPos.x}px; top: ${inSuggestPos.y}px;`}
|
|
617
793
|
onmousedown={(event) => event.preventDefault()}
|
|
618
794
|
>
|
|
@@ -656,6 +832,7 @@
|
|
|
656
832
|
></div>
|
|
657
833
|
<div
|
|
658
834
|
class="sv-grid-menu sv-grid-context-menu"
|
|
835
|
+
data-svgrid-menu={ctrl.gridDomId}
|
|
659
836
|
role="menu"
|
|
660
837
|
style={`left: ${contextMenuPos.x}px; top: ${contextMenuPos.y}px;`}
|
|
661
838
|
>
|
|
@@ -683,6 +860,7 @@
|
|
|
683
860
|
></div>
|
|
684
861
|
<div
|
|
685
862
|
class="sv-grid-menu sv-grid-comment-editor"
|
|
863
|
+
data-svgrid-menu={ctrl.gridDomId}
|
|
686
864
|
role="dialog"
|
|
687
865
|
aria-label="Edit comment"
|
|
688
866
|
style={`left: ${commentEditFor.x}px; top: ${commentEditFor.y}px;`}
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
size = 'md',
|
|
40
40
|
ariaLabel,
|
|
41
41
|
invalid = false,
|
|
42
|
+
block = false,
|
|
42
43
|
required = false,
|
|
43
44
|
error,
|
|
44
45
|
label,
|
|
@@ -111,6 +112,7 @@
|
|
|
111
112
|
<input
|
|
112
113
|
bind:this={fieldEl}
|
|
113
114
|
class="sv-ac sv-ac--{size}"
|
|
115
|
+
class:is-block={block}
|
|
114
116
|
class:is-invalid={invalid}
|
|
115
117
|
type="text"
|
|
116
118
|
{placeholder}
|
|
@@ -162,4 +164,6 @@
|
|
|
162
164
|
background-size: 6px 4px; background-position: center; background-repeat: repeat-x;
|
|
163
165
|
}
|
|
164
166
|
:global(.sv-ddl__grip:hover .sv-ddl__grip-dots) { opacity: 1; }
|
|
167
|
+
/* Fill the container - see `block` in SvEditorProps. */
|
|
168
|
+
.sv-ac.is-block { width: 100%; max-width: 100%; }
|
|
165
169
|
</style>
|
package/src/SvComboBox.svelte
CHANGED
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
size = 'md',
|
|
64
64
|
ariaLabel,
|
|
65
65
|
invalid = false,
|
|
66
|
+
block = false,
|
|
66
67
|
required = false,
|
|
67
68
|
error,
|
|
68
69
|
label,
|
|
@@ -182,7 +183,8 @@
|
|
|
182
183
|
</script>
|
|
183
184
|
|
|
184
185
|
<SvField id={uid} {label} {hint} {error} {required} {dir} loading={loadingProp || loading}>
|
|
185
|
-
<div bind:this={fieldEl} class="sv-combo sv-combo--{size}"
|
|
186
|
+
<div bind:this={fieldEl} class="sv-combo sv-combo--{size}"
|
|
187
|
+
class:is-block={block} class:is-open={combo.open} class:is-disabled={disabled} class:is-invalid={invalid}>
|
|
186
188
|
<input
|
|
187
189
|
bind:this={inputEl}
|
|
188
190
|
class="sv-combo__input"
|
|
@@ -272,4 +274,6 @@
|
|
|
272
274
|
background-size: 6px 4px; background-position: center; background-repeat: repeat-x;
|
|
273
275
|
}
|
|
274
276
|
:global(.sv-ddl__grip:hover .sv-ddl__grip-dots) { opacity: 1; }
|
|
277
|
+
/* Fill the container - see `block` in SvEditorProps. */
|
|
278
|
+
.sv-combo.is-block { width: 100%; max-width: 100%; }
|
|
275
279
|
</style>
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
size = 'md',
|
|
75
75
|
ariaLabel,
|
|
76
76
|
invalid = false,
|
|
77
|
+
block = false,
|
|
77
78
|
required = false,
|
|
78
79
|
error,
|
|
79
80
|
label,
|
|
@@ -117,6 +118,7 @@
|
|
|
117
118
|
|
|
118
119
|
// --- Portalled popover positioning (DOM-bound; stays in the component) -------
|
|
119
120
|
let triggerEl = $state<HTMLDivElement | null>(null)
|
|
121
|
+
let inputEl = $state<HTMLInputElement | null>(null)
|
|
120
122
|
let panelEl = $state<HTMLDivElement | null>(null)
|
|
121
123
|
let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
|
|
122
124
|
|
|
@@ -157,8 +159,26 @@
|
|
|
157
159
|
</script>
|
|
158
160
|
|
|
159
161
|
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
160
|
-
<div class="sv-dri" class:sv-dri--disabled={disabled}>
|
|
161
|
-
|
|
162
|
+
<div class="sv-dri" class:is-block={block} class:sv-dri--disabled={disabled}>
|
|
163
|
+
<!-- The whole field is the trigger, not just the <input>. The input is
|
|
164
|
+
readonly and carries the dialog semantics, but it is the flex child that
|
|
165
|
+
shrinks: squeezed into a narrow container (a grid filter cell, say) it
|
|
166
|
+
collapses to a couple of pixels and the icon, padding and range text
|
|
167
|
+
around it were all dead to a click. The input keeps the a11y role and
|
|
168
|
+
focus; this only widens the hit area. -->
|
|
169
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
170
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
171
|
+
<div
|
|
172
|
+
class="sv-dri__field sv-dri__field--{size}"
|
|
173
|
+
class:is-invalid={invalid}
|
|
174
|
+
bind:this={triggerEl}
|
|
175
|
+
dir={resolvedDir}
|
|
176
|
+
onclick={() => {
|
|
177
|
+
if (!isInteractive) return
|
|
178
|
+
inputEl?.focus()
|
|
179
|
+
toggle()
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
162
182
|
<svg class="sv-dri__icon" viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" /><path d="M16 2v4M8 2v4M3 10h18" /></svg>
|
|
163
183
|
<input
|
|
164
184
|
id={uid}
|
|
@@ -171,8 +191,8 @@
|
|
|
171
191
|
{...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
|
|
172
192
|
aria-haspopup="dialog"
|
|
173
193
|
aria-expanded={open}
|
|
174
|
-
onclick={() => isInteractive && toggle()}
|
|
175
194
|
onkeydown={onFieldKeydown}
|
|
195
|
+
bind:this={inputEl}
|
|
176
196
|
use:focusOpen
|
|
177
197
|
/>
|
|
178
198
|
{#if value && isInteractive}
|
|
@@ -253,4 +273,6 @@
|
|
|
253
273
|
background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
|
|
254
274
|
border-radius: 12px; box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35);
|
|
255
275
|
}
|
|
276
|
+
/* Fill the container - see `block` in SvEditorProps. */
|
|
277
|
+
.sv-dri.is-block, .sv-dri.is-block .sv-dri__field { width: 100%; max-width: 100%; }
|
|
256
278
|
</style>
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
size = 'md',
|
|
58
58
|
ariaLabel,
|
|
59
59
|
invalid = false,
|
|
60
|
+
block = false,
|
|
60
61
|
required = false,
|
|
61
62
|
error,
|
|
62
63
|
label,
|
|
@@ -231,6 +232,7 @@
|
|
|
231
232
|
<button
|
|
232
233
|
bind:this={triggerEl}
|
|
233
234
|
class="sv-ddl sv-ddl--{size}"
|
|
235
|
+
class:is-block={block}
|
|
234
236
|
class:is-open={ddl.open}
|
|
235
237
|
class:is-disabled={disabled}
|
|
236
238
|
class:is-invalid={invalid}
|
|
@@ -371,4 +373,6 @@
|
|
|
371
373
|
:global(.sv-ddl__empty) { padding: 12px 12px; font-size: 13px; color: var(--sg-muted, #64748b); text-align: center; }
|
|
372
374
|
:global(.sv-ddl__header) { padding: 8px 10px; border-bottom: 1px solid var(--sg-border, #e2e8f0); font-size: 12.5px; color: var(--sg-muted, #64748b); }
|
|
373
375
|
:global(.sv-ddl__footer) { padding: 8px 10px; border-top: 1px solid var(--sg-border, #e2e8f0); font-size: 12.5px; }
|
|
376
|
+
/* Fill the container - see `block` in SvEditorProps. */
|
|
377
|
+
.sv-ddl.is-block { width: 100%; max-width: 100%; }
|
|
374
378
|
</style>
|