@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/SvGrid.types.d.ts
CHANGED
|
@@ -191,6 +191,74 @@ export type ContextMenuItem<TData extends RowData = RowData> = string | {
|
|
|
191
191
|
/** Invoked on click. The menu closes afterwards. */
|
|
192
192
|
action: (target: ContextMenuTarget<TData>) => void;
|
|
193
193
|
};
|
|
194
|
+
/** What a {@link SelectionBarAction} is handed when it runs. */
|
|
195
|
+
export type SelectionBarTarget<TData extends RowData = RowData> = {
|
|
196
|
+
/** The selected rows, in the grid's current display order. */
|
|
197
|
+
rows: TData[];
|
|
198
|
+
/** Their row ids, same order as `rows`. */
|
|
199
|
+
ids: string[];
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* One button on the selection bar. Same shape as {@link ContextMenuItem}'s
|
|
203
|
+
* object form - key, label, optional hidden/disabled predicates, and an action -
|
|
204
|
+
* except everything here is handed the whole SELECTION rather than one cell.
|
|
205
|
+
*/
|
|
206
|
+
export type SelectionBarAction<TData extends RowData = RowData> = {
|
|
207
|
+
key: string;
|
|
208
|
+
label: string;
|
|
209
|
+
/**
|
|
210
|
+
* Inline SVG path data (the `d` of a single 24x24 path) drawn to the left of
|
|
211
|
+
* the label. A string rather than a component so an action array stays plain
|
|
212
|
+
* data that can live in a `.ts` module next to the columns.
|
|
213
|
+
*/
|
|
214
|
+
icon?: string;
|
|
215
|
+
/** Render in the destructive style (red). Does not add a confirmation. */
|
|
216
|
+
danger?: boolean;
|
|
217
|
+
/** Hide the button entirely for this selection. */
|
|
218
|
+
hidden?: (target: SelectionBarTarget<TData>) => boolean;
|
|
219
|
+
/** Render greyed-out and non-clickable for this selection. */
|
|
220
|
+
disabled?: (target: SelectionBarTarget<TData>) => boolean;
|
|
221
|
+
/** Invoked on click. The bar stays open; clear the selection yourself if the
|
|
222
|
+
* action should dismiss it. */
|
|
223
|
+
action: (target: SelectionBarTarget<TData>) => void;
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* A built-in bar button, named by key - the same convention
|
|
227
|
+
* {@link ContextMenuItem} uses for its string form.
|
|
228
|
+
*
|
|
229
|
+
* - `'selectAll'` selects every row in the current view.
|
|
230
|
+
* - `'editFields'` opens the bulk-edit dialog: pick a field, set one value,
|
|
231
|
+
* apply it to every selected row in a single undo step.
|
|
232
|
+
* - `'separator'` draws a divider.
|
|
233
|
+
*/
|
|
234
|
+
export type SelectionBarBuiltin = "selectAll" | "editFields" | "separator";
|
|
235
|
+
/**
|
|
236
|
+
* Full form of the {@link SvGridProps.selectionBar} prop, for when the array
|
|
237
|
+
* shorthand is not enough.
|
|
238
|
+
*/
|
|
239
|
+
export type SelectionBarConfig<TData extends RowData = RowData> = {
|
|
240
|
+
/**
|
|
241
|
+
* The buttons, left to right. Strings are {@link SelectionBarBuiltin} keys,
|
|
242
|
+
* objects are your own actions - so a Jira-shaped bar is
|
|
243
|
+
* `['selectAll', 'editFields', 'separator', myAction]`.
|
|
244
|
+
*/
|
|
245
|
+
actions?: ReadonlyArray<SelectionBarAction<TData> | SelectionBarBuiltin>;
|
|
246
|
+
/**
|
|
247
|
+
* Which edge of the grid the bar floats against. Default `'bottom'`, where
|
|
248
|
+
* the eye is already going after ticking a checkbox and where it covers the
|
|
249
|
+
* rows a user is least likely to be reading.
|
|
250
|
+
*/
|
|
251
|
+
position?: "bottom" | "top";
|
|
252
|
+
/**
|
|
253
|
+
* How many actions stay on the bar before the rest collapse into an overflow
|
|
254
|
+
* menu. Default 6, which fits a full issue-tracker set (select all, edit,
|
|
255
|
+
* status, watch, delete) without collapsing. A bar wide enough to need
|
|
256
|
+
* horizontal scrolling is a bar whose last button nobody finds.
|
|
257
|
+
*/
|
|
258
|
+
maxVisible?: number;
|
|
259
|
+
/** Hide the trailing clear (x) button. Default false. */
|
|
260
|
+
hideClear?: boolean;
|
|
261
|
+
};
|
|
194
262
|
/** A single Kanban lane (board column). {@link BoardConfig}. */
|
|
195
263
|
export type BoardLane = {
|
|
196
264
|
/** Lane id - equals the `groupBy` field value of the cards it holds. */
|
|
@@ -967,6 +1035,26 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
967
1035
|
* browser menu shows instead).
|
|
968
1036
|
*/
|
|
969
1037
|
contextMenu?: boolean | ReadonlyArray<ContextMenuItem<TData>>;
|
|
1038
|
+
/**
|
|
1039
|
+
* Floating bar that appears over the grid while rows are selected, showing
|
|
1040
|
+
* the count and the actions that apply to the whole selection - the pattern
|
|
1041
|
+
* an issue tracker uses for bulk edit.
|
|
1042
|
+
*
|
|
1043
|
+
* `true` gives the count and a Clear button. An array is the shorthand for
|
|
1044
|
+
* `{ actions }`. Pass {@link SelectionBarConfig} to also set `position`
|
|
1045
|
+
* (`'bottom'` default, or `'top'`), `maxVisible` and `hideClear`. Omitted /
|
|
1046
|
+
* `false` is off.
|
|
1047
|
+
*
|
|
1048
|
+
* Default **off**. This is chrome that floats over your content, not a
|
|
1049
|
+
* gesture: it overlaps a row, and what belongs on it is specific to the app.
|
|
1050
|
+
*
|
|
1051
|
+
* **The prop and its types are free; the bar itself is an Enterprise
|
|
1052
|
+
* feature.** Install `@svgrid/enterprise` and call `enableSelectionBar()`
|
|
1053
|
+
* (or `installEnterprise`, which does it for you) to render it - without
|
|
1054
|
+
* that the grid shows a short upsell note in its place, the same way the
|
|
1055
|
+
* scheduler and board views do.
|
|
1056
|
+
*/
|
|
1057
|
+
selectionBar?: boolean | ReadonlyArray<SelectionBarAction<TData> | SelectionBarBuiltin> | SelectionBarConfig<TData>;
|
|
970
1058
|
/**
|
|
971
1059
|
* The feature set built with `tableFeatures({ ... })`. Optional - the
|
|
972
1060
|
* `sortable` / `filterable` / `groupable` shortcuts below inject the
|
|
@@ -1424,6 +1512,23 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
1424
1512
|
* is the shortcut alias and wins over it.
|
|
1425
1513
|
*/
|
|
1426
1514
|
enableCellSelection?: boolean;
|
|
1515
|
+
/**
|
|
1516
|
+
* Excel-style drag-and-drop of a selected cell range: grab the range's
|
|
1517
|
+
* border and drag it somewhere else to MOVE the values there, or hold
|
|
1518
|
+
* Ctrl (Cmd on macOS) to COPY them instead. The modifier is read at drop
|
|
1519
|
+
* time, so it can be pressed or released mid-drag.
|
|
1520
|
+
*
|
|
1521
|
+
* On by default whenever cell selection is on, matching the fill handle.
|
|
1522
|
+
* Set `false` to keep a border pointerdown starting a fresh selection, which
|
|
1523
|
+
* is what it did before this existed.
|
|
1524
|
+
*
|
|
1525
|
+
* The grab strip is the outer 4px of the range border, so a pointerdown
|
|
1526
|
+
* anywhere further inside still starts a new selection. A drop is refused
|
|
1527
|
+
* outright - nothing changes - when the destination would fall outside the
|
|
1528
|
+
* grid, or when any source or destination cell is read-only. It does not
|
|
1529
|
+
* move the part that fits and silently drop the rest.
|
|
1530
|
+
*/
|
|
1531
|
+
moveCells?: boolean;
|
|
1427
1532
|
/**
|
|
1428
1533
|
* Highlight the row under the pointer. Default **false** - the hover tint can
|
|
1429
1534
|
* compete with the cell selection / fill marquee. Set `true` to opt in; when
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
getEditableInputValue,
|
|
27
27
|
getEditorClass,
|
|
28
28
|
getEditorInputType,
|
|
29
|
+
getCellEditorInputType,
|
|
30
|
+
isNumericEditorInput,
|
|
29
31
|
timeStringToDate,
|
|
30
32
|
toValueArray,
|
|
31
33
|
} from "./SvGrid.helpers";
|
|
@@ -75,6 +77,22 @@
|
|
|
75
77
|
const focusOnMount = $derived(ctrl.focusOnMount);
|
|
76
78
|
const toggleCheckboxWithKeyboard = $derived(ctrl.toggleCheckboxWithKeyboard);
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Focus a button-based editor on mount. `focusOnMount` is for text controls -
|
|
82
|
+
* it also places the caret - and the checkbox and rating editors, being
|
|
83
|
+
* <button>s, had no equivalent and so never took focus at all. Their own key
|
|
84
|
+
* handlers therefore never fired: neither could be operated from the
|
|
85
|
+
* keyboard, and Escape never reached them, so the cell stayed stuck in edit
|
|
86
|
+
* mode until the user clicked somewhere else.
|
|
87
|
+
*
|
|
88
|
+
* `enabled` lets a group of buttons (the rating stars) pick which one holds
|
|
89
|
+
* focus, since an action cannot be applied conditionally.
|
|
90
|
+
*/
|
|
91
|
+
function focusControlOnMount(node: HTMLElement, enabled: boolean = true) {
|
|
92
|
+
if (!enabled) return;
|
|
93
|
+
requestAnimationFrame(() => node.focus({ preventScroll: true }));
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
function fullRowKeydown(e: KeyboardEvent) {
|
|
79
97
|
if (e.key === "Enter") {
|
|
80
98
|
e.preventDefault();
|
|
@@ -337,6 +355,7 @@
|
|
|
337
355
|
})}
|
|
338
356
|
{:else if ctrl.editingCell?.editorType === "checkbox"}
|
|
339
357
|
<button
|
|
358
|
+
use:focusControlOnMount
|
|
340
359
|
type="button"
|
|
341
360
|
class="sv-grid-checkbox"
|
|
342
361
|
role="checkbox"
|
|
@@ -350,15 +369,23 @@
|
|
|
350
369
|
: ctrl.editingCell;
|
|
351
370
|
saveEditingCell();
|
|
352
371
|
}}
|
|
353
|
-
onkeydown={(event) =>
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
372
|
+
onkeydown={(event) => {
|
|
373
|
+
// Enter / Space are the checkbox's own activation keys. Everything
|
|
374
|
+
// else - Escape to cancel, Tab to commit and move on - goes to the
|
|
375
|
+
// shared editor handler, which this editor used to bypass entirely.
|
|
376
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
377
|
+
toggleCheckboxWithKeyboard(event, () => {
|
|
378
|
+
event.stopPropagation();
|
|
379
|
+
const nextValue = !ctrl.editingCell?.value;
|
|
380
|
+
ctrl.editingCell = ctrl.editingCell
|
|
381
|
+
? { ...ctrl.editingCell, value: nextValue }
|
|
382
|
+
: ctrl.editingCell;
|
|
383
|
+
saveEditingCell();
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
onEditorKeyDown(event);
|
|
388
|
+
}}
|
|
362
389
|
onblur={() => saveEditingCell()}
|
|
363
390
|
></button>
|
|
364
391
|
{:else if ctrl.editingCell?.editorType === "list"}
|
|
@@ -395,9 +422,15 @@
|
|
|
395
422
|
0,
|
|
396
423
|
Math.min(5, Math.round(Number(ctrl.editingCell?.value) || 0)),
|
|
397
424
|
)}
|
|
425
|
+
<!-- Focus the star for the current rating (the first one when unrated),
|
|
426
|
+
the radio a radiogroup is expected to enter on. Without it nothing
|
|
427
|
+
in this editor held focus, so `onEditorKeyDown` below never ran and
|
|
428
|
+
Escape could not close the cell. -->
|
|
429
|
+
{@const focusStar = Math.min(5, Math.max(1, ratingVal))}
|
|
398
430
|
<span class="sv-grid-rating-editor" role="radiogroup" aria-label="Rating">
|
|
399
431
|
{#each [1, 2, 3, 4, 5] as n (n)}
|
|
400
432
|
<button
|
|
433
|
+
use:focusControlOnMount={n === focusStar}
|
|
401
434
|
type="button"
|
|
402
435
|
role="radio"
|
|
403
436
|
aria-checked={ratingVal >= n}
|
|
@@ -411,7 +444,43 @@
|
|
|
411
444
|
: ctrl.editingCell;
|
|
412
445
|
saveEditingCell();
|
|
413
446
|
}}
|
|
414
|
-
onkeydown={
|
|
447
|
+
onkeydown={(event) => {
|
|
448
|
+
// Arrows move between the stars, the way a radiogroup is
|
|
449
|
+
// expected to work - without them the editor takes focus (see
|
|
450
|
+
// focusControlOnMount) but Space can only re-pick the star that
|
|
451
|
+
// already holds the value. Enter / Escape / Tab still fall
|
|
452
|
+
// through to the shared handler.
|
|
453
|
+
const key = event.key;
|
|
454
|
+
if (
|
|
455
|
+
key !== "ArrowLeft" &&
|
|
456
|
+
key !== "ArrowRight" &&
|
|
457
|
+
key !== "Home" &&
|
|
458
|
+
key !== "End"
|
|
459
|
+
) {
|
|
460
|
+
onEditorKeyDown(event);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
event.preventDefault();
|
|
464
|
+
event.stopPropagation();
|
|
465
|
+
const current = Math.min(5, Math.max(1, ratingVal));
|
|
466
|
+
const next =
|
|
467
|
+
key === "Home"
|
|
468
|
+
? 1
|
|
469
|
+
: key === "End"
|
|
470
|
+
? 5
|
|
471
|
+
: key === "ArrowLeft"
|
|
472
|
+
? Math.max(1, current - 1)
|
|
473
|
+
: Math.min(5, current + 1);
|
|
474
|
+
ctrl.editingCell = ctrl.editingCell
|
|
475
|
+
? { ...ctrl.editingCell, value: next }
|
|
476
|
+
: ctrl.editingCell;
|
|
477
|
+
const stars = (
|
|
478
|
+
event.currentTarget as HTMLElement
|
|
479
|
+
).parentElement?.querySelectorAll<HTMLElement>(
|
|
480
|
+
".sv-grid-rating-star",
|
|
481
|
+
);
|
|
482
|
+
stars?.[next - 1]?.focus({ preventScroll: true });
|
|
483
|
+
}}>★</button
|
|
415
484
|
>
|
|
416
485
|
{/each}
|
|
417
486
|
<button
|
|
@@ -643,18 +712,31 @@
|
|
|
643
712
|
{@const CustomCellEditor = _reg.component}
|
|
644
713
|
<CustomCellEditor {...resolveEditorProps(_reg, buildRegisteredEditorContext())} />
|
|
645
714
|
{:else}
|
|
715
|
+
{@const editorType = ctrl.editingCell?.editorType ?? "text"}
|
|
716
|
+
{@const isNumberEditor = getCellEditorInputType(editorType) === "text" &&
|
|
717
|
+
getEditorInputType(editorType) === "number"}
|
|
646
718
|
<input
|
|
647
719
|
use:focusOnMount
|
|
648
|
-
class={getEditorClass(
|
|
649
|
-
type={
|
|
720
|
+
class={getEditorClass(editorType)}
|
|
721
|
+
type={getCellEditorInputType(editorType)}
|
|
722
|
+
inputmode={isNumberEditor ? "decimal" : undefined}
|
|
650
723
|
value={getEditableInputValue(
|
|
651
|
-
|
|
724
|
+
editorType,
|
|
652
725
|
ctrl.editingCell?.value,
|
|
653
726
|
)}
|
|
654
|
-
oninput={(event) =>
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
)
|
|
727
|
+
oninput={(event) => {
|
|
728
|
+
const input = event.currentTarget as HTMLInputElement;
|
|
729
|
+
// The number editor is a TEXT input so intermediate values survive
|
|
730
|
+
// (see getCellEditorInputType). That means nothing stops a letter
|
|
731
|
+
// being typed, which type="number" used to - so reject it here and
|
|
732
|
+
// put the last good draft back, rather than letting "12x" reach the
|
|
733
|
+
// commit and coerce to null.
|
|
734
|
+
if (isNumberEditor && !isNumericEditorInput(input.value)) {
|
|
735
|
+
input.value = String(ctrl.editingCell?.value ?? "");
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
updateEditingCellValue(input.value);
|
|
739
|
+
}}
|
|
658
740
|
onblur={() => saveEditingCell()}
|
|
659
741
|
onkeydown={onEditorKeyDown}
|
|
660
742
|
/>
|
package/dist/SvGridSelect.svelte
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
type Props = Pick<
|
|
23
23
|
SvEditorProps,
|
|
24
|
-
'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
|
|
24
|
+
'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'block' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
|
|
25
25
|
> & {
|
|
26
26
|
columns: ReadonlyArray<GridSelectColumn>
|
|
27
27
|
options: ReadonlyArray<Row>
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
error,
|
|
69
69
|
required = false,
|
|
70
70
|
invalid = false,
|
|
71
|
+
block = false,
|
|
71
72
|
size = 'md',
|
|
72
73
|
id,
|
|
73
74
|
name,
|
|
@@ -133,7 +134,8 @@
|
|
|
133
134
|
</script>
|
|
134
135
|
|
|
135
136
|
<SvField id={uid} {label} {hint} {error} {required} {dir} loading={loadingProp || loading}>
|
|
136
|
-
<div class="sv-gs sv-gs--{size}"
|
|
137
|
+
<div class="sv-gs sv-gs--{size}"
|
|
138
|
+
class:is-block={block} class:is-disabled={disabled}>
|
|
137
139
|
<button
|
|
138
140
|
bind:this={triggerEl}
|
|
139
141
|
type="button"
|
|
@@ -263,4 +265,6 @@
|
|
|
263
265
|
:global(.sv-gs__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
|
|
264
266
|
:global(.sv-gs__cell) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
265
267
|
:global(.sv-gs__empty) { padding: 12px; text-align: center; color: var(--sg-muted, #94a3b8); }
|
|
268
|
+
/* Fill the container - see `block` in SvEditorProps. */
|
|
269
|
+
.sv-gs.is-block { width: 100%; max-width: 100%; }
|
|
266
270
|
</style>
|
|
@@ -10,7 +10,7 @@ export type { GridSelectColumn } from './ui-app.types.js';
|
|
|
10
10
|
import type { GridSelectColumn } from './ui-app.types.js';
|
|
11
11
|
import { type SvEditorProps } from './editor-contract.js';
|
|
12
12
|
import { type Row } from './grid-select.js';
|
|
13
|
-
type Props = Pick<SvEditorProps, 'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'> & {
|
|
13
|
+
type Props = Pick<SvEditorProps, 'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'block' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'> & {
|
|
14
14
|
columns: ReadonlyArray<GridSelectColumn>;
|
|
15
15
|
options: ReadonlyArray<Row>;
|
|
16
16
|
idField?: string;
|
package/dist/SvListBox.svelte
CHANGED
|
@@ -230,6 +230,7 @@
|
|
|
230
230
|
<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>
|
|
231
231
|
{/if}
|
|
232
232
|
{#if item ?? itemTemplate}{@render (item ?? itemTemplate)!(opt)}{:else}<span class="sv-listbox__label">{opt.label}</span>{/if}
|
|
233
|
+
{#if opt.hint && !(item ?? itemTemplate)}<span class="sv-listbox__hint" aria-hidden="true">{opt.hint}</span>{/if}
|
|
233
234
|
{/snippet}
|
|
234
235
|
|
|
235
236
|
{#snippet optionRow(opt: IndexedOption, index: number, h?: number, top?: number)}
|
|
@@ -302,6 +303,13 @@
|
|
|
302
303
|
}
|
|
303
304
|
.sv-listbox__check { width: 14px; text-align: center; color: var(--_accent); }
|
|
304
305
|
.sv-listbox__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
306
|
+
/* Trailing muted text (a count, a unit). `flex: none` so it never gives up
|
|
307
|
+
width - the label ellipsises instead, which is the half you can guess. */
|
|
308
|
+
.sv-listbox__hint {
|
|
309
|
+
flex: none; margin-inline-start: 8px;
|
|
310
|
+
color: var(--sg-muted, #64748b); font-variant-numeric: tabular-nums;
|
|
311
|
+
font-size: 0.9em;
|
|
312
|
+
}
|
|
305
313
|
|
|
306
314
|
/* ---- Checklist variant ---- */
|
|
307
315
|
.sv-listbox__box {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
type Props = Pick<
|
|
22
22
|
SvEditorProps,
|
|
23
|
-
'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
|
|
23
|
+
'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'block' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
|
|
24
24
|
> & {
|
|
25
25
|
options: ReadonlyArray<MultiSelectOption>
|
|
26
26
|
value?: ReadonlyArray<string | number>
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
error,
|
|
68
68
|
required = false,
|
|
69
69
|
invalid = false,
|
|
70
|
+
block = false,
|
|
70
71
|
size = 'md',
|
|
71
72
|
id,
|
|
72
73
|
name,
|
|
@@ -143,7 +144,8 @@
|
|
|
143
144
|
</script>
|
|
144
145
|
|
|
145
146
|
<SvField id={uid} {label} {hint} {error} {required} {dir} loading={loadingProp || loading}>
|
|
146
|
-
<div class="sv-ms sv-ms--{size}"
|
|
147
|
+
<div class="sv-ms sv-ms--{size}"
|
|
148
|
+
class:is-block={block} class:is-disabled={disabled}>
|
|
147
149
|
<button
|
|
148
150
|
bind:this={triggerEl}
|
|
149
151
|
type="button"
|
|
@@ -286,4 +288,6 @@
|
|
|
286
288
|
:global(.sv-ms__loading) { padding: 8px 10px; color: var(--sg-muted, #94a3b8); font-size: 12px; text-align: center; }
|
|
287
289
|
:global(.sv-ms__footer) { display: flex; justify-content: flex-end; padding: 6px 8px; border-top: 1px solid var(--sg-border, #e2e8f0); }
|
|
288
290
|
:global(.sv-ms__done) { font: inherit; font-size: 12.5px; padding: 4px 12px; border: 0; border-radius: 6px; background: var(--sg-accent, #2563eb); color: var(--sg-on-accent, #fff); cursor: pointer; }
|
|
291
|
+
/* Fill the container - see `block` in SvEditorProps. */
|
|
292
|
+
.sv-ms.is-block { width: 100%; max-width: 100%; }
|
|
289
293
|
</style>
|
|
@@ -9,7 +9,7 @@ export type { MultiSelectOption } from './ui-app.types.js';
|
|
|
9
9
|
*/
|
|
10
10
|
import type { MultiSelectOption } from './ui-app.types.js';
|
|
11
11
|
import { type SvEditorProps } from './editor-contract.js';
|
|
12
|
-
type Props = Pick<SvEditorProps, 'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'> & {
|
|
12
|
+
type Props = Pick<SvEditorProps, 'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'block' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'> & {
|
|
13
13
|
options: ReadonlyArray<MultiSelectOption>;
|
|
14
14
|
value?: ReadonlyArray<string | number>;
|
|
15
15
|
onChange?: (value: Array<string | number>) => void;
|
|
@@ -161,6 +161,10 @@
|
|
|
161
161
|
|
|
162
162
|
<style>
|
|
163
163
|
.sv-num__input { text-align: end; }
|
|
164
|
+
/* Digits line up on the right; a placeholder is prose, and right-aligning
|
|
165
|
+
it puts one field out of step with every other label-above-control row
|
|
166
|
+
in a form. */
|
|
167
|
+
.sv-num__input:placeholder-shown { text-align: start; }
|
|
164
168
|
.sv-num__clear { display: grid; place-items: center; width: 20px; align-self: center; flex: none; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; border-radius: 4px; }
|
|
165
169
|
.sv-num__clear:hover { color: var(--sg-danger, #dc2626); }
|
|
166
170
|
.sv-num__spin { display: flex; flex-direction: column; margin-inline-start: 2px; }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvRichCell - render rich text inside a grid cell, safely.
|
|
4
|
+
*
|
|
5
|
+
* Pair it with `editorType: 'richtext'` (or feed it Markdown) to get a column
|
|
6
|
+
* whose values carry formatting. The value is always run through
|
|
7
|
+
* `sanitizeHtml` before it is painted, so markup that arrived from a user
|
|
8
|
+
* cannot execute in the host page - see sanitize-html.ts for the allowlist.
|
|
9
|
+
*
|
|
10
|
+
* ```svelte
|
|
11
|
+
* <script>
|
|
12
|
+
* import { SvRichCell } from '@svgrid/grid'
|
|
13
|
+
* <\/script>
|
|
14
|
+
*
|
|
15
|
+
* {#snippet notesCell({ getValue })}
|
|
16
|
+
* <SvRichCell value={getValue()} />
|
|
17
|
+
* {/snippet}
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* A grid row is one line tall by default, so the content is clamped to
|
|
21
|
+
* `clamp` lines with an ellipsis and the full text goes on the `title`
|
|
22
|
+
* attribute. Set `clamp={0}` in a master/detail panel or a tall row where the
|
|
23
|
+
* whole value should show.
|
|
24
|
+
*/
|
|
25
|
+
import { sanitizeHtml, renderMarkdown, htmlToText } from './sanitize-html'
|
|
26
|
+
|
|
27
|
+
type Props = {
|
|
28
|
+
/** The cell value: an HTML string, or Markdown when `format="markdown"`. */
|
|
29
|
+
value?: unknown
|
|
30
|
+
/** How to read `value`. Markdown is converted first, then sanitized. */
|
|
31
|
+
format?: 'html' | 'markdown'
|
|
32
|
+
/** Lines to clamp to. 0 removes the clamp and lets the content wrap. */
|
|
33
|
+
clamp?: number
|
|
34
|
+
/** Native tooltip carrying the flattened text. Off when the grid already
|
|
35
|
+
* supplies its own cell tooltip. */
|
|
36
|
+
title?: boolean
|
|
37
|
+
class?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let { value = '', format = 'html', clamp = 1, title = true, class: className = '' }: Props = $props()
|
|
41
|
+
|
|
42
|
+
const source = $derived(value == null ? '' : String(value))
|
|
43
|
+
const html = $derived(format === 'markdown' ? renderMarkdown(source) : sanitizeHtml(source))
|
|
44
|
+
// The flattened text doubles as the tooltip and as the accessible name when
|
|
45
|
+
// the markup collapses to something a screen reader would read as noise.
|
|
46
|
+
const text = $derived(title ? htmlToText(html) : '')
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div
|
|
50
|
+
class="sv-rich-cell {className}"
|
|
51
|
+
class:is-clamped={clamp > 0}
|
|
52
|
+
style={clamp > 0 ? `--sv-rich-cell-lines: ${clamp}` : undefined}
|
|
53
|
+
title={text || undefined}
|
|
54
|
+
>
|
|
55
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
56
|
+
{@html html}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
.sv-rich-cell {
|
|
61
|
+
min-width: 0;
|
|
62
|
+
color: var(--sg-fg, #0f172a);
|
|
63
|
+
font: inherit;
|
|
64
|
+
}
|
|
65
|
+
/* Clamp to N lines. The grid row is a single line tall by default, so
|
|
66
|
+
unclamped rich text would push the row open and break virtualization's
|
|
67
|
+
fixed-height assumption. */
|
|
68
|
+
.sv-rich-cell.is-clamped {
|
|
69
|
+
display: -webkit-box;
|
|
70
|
+
-webkit-box-orient: vertical;
|
|
71
|
+
-webkit-line-clamp: var(--sv-rich-cell-lines, 1);
|
|
72
|
+
line-clamp: var(--sv-rich-cell-lines, 1);
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
}
|
|
75
|
+
/* Block elements inside a cell must not carry their document margins, or a
|
|
76
|
+
single <p> pushes the text out of the row box. */
|
|
77
|
+
.sv-rich-cell :global(p),
|
|
78
|
+
.sv-rich-cell :global(h1),
|
|
79
|
+
.sv-rich-cell :global(h2),
|
|
80
|
+
.sv-rich-cell :global(h3),
|
|
81
|
+
.sv-rich-cell :global(h4),
|
|
82
|
+
.sv-rich-cell :global(h5),
|
|
83
|
+
.sv-rich-cell :global(h6),
|
|
84
|
+
.sv-rich-cell :global(ul),
|
|
85
|
+
.sv-rich-cell :global(ol),
|
|
86
|
+
.sv-rich-cell :global(blockquote),
|
|
87
|
+
.sv-rich-cell :global(pre) {
|
|
88
|
+
margin: 0;
|
|
89
|
+
padding: 0;
|
|
90
|
+
}
|
|
91
|
+
.sv-rich-cell.is-clamped :global(ul),
|
|
92
|
+
.sv-rich-cell.is-clamped :global(ol) {
|
|
93
|
+
list-style-position: inside;
|
|
94
|
+
}
|
|
95
|
+
/* Headings keep their weight but drop to cell size: a real <h1> inside a row
|
|
96
|
+
would dwarf every neighbouring cell. */
|
|
97
|
+
.sv-rich-cell :global(h1),
|
|
98
|
+
.sv-rich-cell :global(h2),
|
|
99
|
+
.sv-rich-cell :global(h3),
|
|
100
|
+
.sv-rich-cell :global(h4),
|
|
101
|
+
.sv-rich-cell :global(h5),
|
|
102
|
+
.sv-rich-cell :global(h6) {
|
|
103
|
+
font-size: inherit;
|
|
104
|
+
font-weight: 650;
|
|
105
|
+
}
|
|
106
|
+
.sv-rich-cell :global(a) {
|
|
107
|
+
color: var(--sg-accent, #2563eb);
|
|
108
|
+
}
|
|
109
|
+
.sv-rich-cell :global(code) {
|
|
110
|
+
font-family: var(--sg-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
111
|
+
font-size: 0.92em;
|
|
112
|
+
background: var(--sg-code-bg, color-mix(in srgb, var(--sg-fg, #0f172a) 8%, transparent));
|
|
113
|
+
border-radius: var(--sg-radius-sm, 4px);
|
|
114
|
+
padding: 0 4px;
|
|
115
|
+
}
|
|
116
|
+
.sv-rich-cell :global(blockquote) {
|
|
117
|
+
border-inline-start: 2px solid var(--sg-border, #cbd5e1);
|
|
118
|
+
padding-inline-start: 8px;
|
|
119
|
+
color: var(--sg-muted, #64748b);
|
|
120
|
+
}
|
|
121
|
+
.sv-rich-cell :global(img) {
|
|
122
|
+
max-height: 1.4em;
|
|
123
|
+
vertical-align: text-bottom;
|
|
124
|
+
}
|
|
125
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
/** The cell value: an HTML string, or Markdown when `format="markdown"`. */
|
|
3
|
+
value?: unknown;
|
|
4
|
+
/** How to read `value`. Markdown is converted first, then sanitized. */
|
|
5
|
+
format?: 'html' | 'markdown';
|
|
6
|
+
/** Lines to clamp to. 0 removes the clamp and lets the content wrap. */
|
|
7
|
+
clamp?: number;
|
|
8
|
+
/** Native tooltip carrying the flattened text. Off when the grid already
|
|
9
|
+
* supplies its own cell tooltip. */
|
|
10
|
+
title?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const SvRichCell: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type SvRichCell = ReturnType<typeof SvRichCell>;
|
|
15
|
+
export default SvRichCell;
|
package/dist/SvTextArea.svelte
CHANGED
|
@@ -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,
|
|
@@ -73,7 +74,8 @@
|
|
|
73
74
|
</script>
|
|
74
75
|
|
|
75
76
|
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
76
|
-
<div class="sv-ta sv-ta--{size}"
|
|
77
|
+
<div class="sv-ta sv-ta--{size}"
|
|
78
|
+
class:is-block={block} class:is-disabled={disabled}>
|
|
77
79
|
<textarea
|
|
78
80
|
use:grow
|
|
79
81
|
use:mountFocus
|
|
@@ -110,4 +112,6 @@
|
|
|
110
112
|
.sv-ta__input:focus { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
111
113
|
.sv-ta__input.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
112
114
|
.sv-ta__count { align-self: flex-end; font-size: 11px; color: var(--sg-muted, #64748b); }
|
|
115
|
+
/* Fill the container - see `block` in SvEditorProps. */
|
|
116
|
+
.sv-ta.is-block { width: 100%; max-width: 100%; }
|
|
113
117
|
</style>
|
package/dist/SvTreeSelect.svelte
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
type Props = Pick<
|
|
22
22
|
SvEditorProps,
|
|
23
|
-
'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
|
|
23
|
+
'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'block' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
|
|
24
24
|
> & {
|
|
25
25
|
nodes: ReadonlyArray<TreeSelectNode>
|
|
26
26
|
value?: string | number | null
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
error,
|
|
47
47
|
required = false,
|
|
48
48
|
invalid = false,
|
|
49
|
+
block = false,
|
|
49
50
|
size = 'md',
|
|
50
51
|
id,
|
|
51
52
|
name,
|
|
@@ -129,7 +130,8 @@
|
|
|
129
130
|
</script>
|
|
130
131
|
|
|
131
132
|
<SvField id={uid} {label} {hint} {error} {required} {dir} {loading}>
|
|
132
|
-
<div class="sv-ts sv-ts--{size}"
|
|
133
|
+
<div class="sv-ts sv-ts--{size}"
|
|
134
|
+
class:is-block={block} class:is-disabled={disabled}>
|
|
133
135
|
<button
|
|
134
136
|
bind:this={triggerEl}
|
|
135
137
|
type="button"
|
|
@@ -232,4 +234,6 @@
|
|
|
232
234
|
:global(.sv-ts__chev.is-open) { transform: rotate(90deg); }
|
|
233
235
|
:global(.sv-ts__chev--leaf) { visibility: hidden; }
|
|
234
236
|
:global(.sv-ts__label) { flex: 1; white-space: nowrap; }
|
|
237
|
+
/* Fill the container - see `block` in SvEditorProps. */
|
|
238
|
+
.sv-ts.is-block { width: 100%; max-width: 100%; }
|
|
235
239
|
</style>
|
|
@@ -9,7 +9,7 @@ export type { TreeSelectNode } from './ui-app.types.js';
|
|
|
9
9
|
*/
|
|
10
10
|
import type { TreeSelectNode } from './ui-app.types.js';
|
|
11
11
|
import { type SvEditorProps } from './editor-contract.js';
|
|
12
|
-
type Props = Pick<SvEditorProps, 'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'> & {
|
|
12
|
+
type Props = Pick<SvEditorProps, 'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'block' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'> & {
|
|
13
13
|
nodes: ReadonlyArray<TreeSelectNode>;
|
|
14
14
|
value?: string | number | null;
|
|
15
15
|
onChange?: (value: string | number) => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Register the config-free UI-kit editors (`otp`, `duration`) as
|
|
3
|
-
* editors. Idempotent. Returns the list of type names it registered.
|
|
2
|
+
* Register the config-free UI-kit editors (`otp`, `duration`, `richtext`) as
|
|
3
|
+
* grid cell editors. Idempotent. Returns the list of type names it registered.
|
|
4
4
|
*/
|
|
5
5
|
export declare function registerBuiltinEditors(): string[];
|