@rozie-ui/data-table-lit 0.8.1 → 0.9.0
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/CHANGELOG.md +11 -0
- package/README.md +4 -3
- package/dist/index.cjs +165 -14
- package/dist/index.d.cts +32 -4
- package/dist/index.d.mts +32 -4
- package/dist/index.mjs +165 -14
- package/package.json +1 -1
- package/src/DataTable.ts +198 -14
- package/src/helpers/rangeFill.ts +22 -0
- package/src/themes/base.css +12 -4
- package/src/themes/bootstrap.css +3 -0
- package/src/themes/material.css +3 -0
- package/src/themes/shadcn.css +3 -0
package/src/DataTable.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { columnRuleKeys } from './helpers/columnRuleUtils';
|
|
|
10
10
|
import { applyUpdater, clamp, focusables } from './helpers/indexMath';
|
|
11
11
|
import { resolveFollow, sameIdSequence } from './helpers/activeFollow';
|
|
12
12
|
import { escapeTsvField, parseTsv, tileGridToBox, tileIndex } from './helpers/tsvGrid';
|
|
13
|
+
import { rangeFillTargets } from './helpers/rangeFill';
|
|
13
14
|
import { replaceRowValue, indexOfRowIn, replaceRowValues } from './helpers/rowValueUtils';
|
|
14
15
|
import { createTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, getExpandedRowModel, getGroupedRowModel,
|
|
15
16
|
// Faceted filtering (phase 50 reqs 8-9, D-03). All three are supplied UNCONDITIONALLY
|
|
@@ -213,10 +214,13 @@ interface RozieDynamicEditorSlotCtx {
|
|
|
213
214
|
column: TableColumn<any, unknown>;
|
|
214
215
|
row: any;
|
|
215
216
|
value: any;
|
|
216
|
-
commit: (value: any
|
|
217
|
+
commit: (value: any, options?: {
|
|
218
|
+
toRange?: boolean;
|
|
219
|
+
}) => void;
|
|
217
220
|
cancel: () => void;
|
|
218
221
|
columnLabel: string;
|
|
219
222
|
autofocus: boolean;
|
|
223
|
+
rangeSize: number;
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
interface RozieDetailSlotCtx {
|
|
@@ -250,10 +254,13 @@ interface RozieEditorSlotCtx {
|
|
|
250
254
|
column: TableColumn<any, unknown>;
|
|
251
255
|
row: any;
|
|
252
256
|
value: any;
|
|
253
|
-
commit: (value: any
|
|
257
|
+
commit: (value: any, options?: {
|
|
258
|
+
toRange?: boolean;
|
|
259
|
+
}) => void;
|
|
254
260
|
cancel: () => void;
|
|
255
261
|
columnLabel: string;
|
|
256
262
|
autofocus: boolean;
|
|
263
|
+
rangeSize: number;
|
|
257
264
|
}
|
|
258
265
|
|
|
259
266
|
@customElement('rozie-data-table')
|
|
@@ -268,6 +275,8 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
268
275
|
--rdt-border: 1px solid var(--rozie-data-table-border-color, var(--rdt-ds-border-color));
|
|
269
276
|
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
|
|
270
277
|
--rdt-filter-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
|
|
278
|
+
--rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, var(--rdt-ds-column-border-color)));
|
|
279
|
+
--rdt-pinned-bg: var(--rozie-data-table-pinned-bg, var(--rdt-ds-pinned-bg));
|
|
271
280
|
--rdt-font: var(--rozie-data-table-font, var(--rdt-ds-font));
|
|
272
281
|
--rdt-cell-padding: var(--rozie-data-table-cell-padding, var(--rdt-ds-cell-padding));
|
|
273
282
|
--rdt-header-bg: var(--rozie-data-table-header-bg, var(--rdt-ds-header-bg));
|
|
@@ -289,7 +298,12 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
289
298
|
--rdt-page-size-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
|
|
290
299
|
}
|
|
291
300
|
.rozie-data-table[data-rozie-s-d5dcab4c] {
|
|
292
|
-
|
|
301
|
+
/* SEPARATE, not collapse. With collapsed borders the browser keeps a sticky cell's shared
|
|
302
|
+
border in the scrolling grid, so a 1px line of scrolled content shows beside pinned
|
|
303
|
+
columns and above the sticky header. Separate borders travel with their cell; zero
|
|
304
|
+
spacing keeps lines 1px because each cell draws only its own sides (see the cell rule). */
|
|
305
|
+
border-collapse: separate;
|
|
306
|
+
border-spacing: 0;
|
|
293
307
|
width: 100%;
|
|
294
308
|
font: var(--rdt-font, 14px system-ui, sans-serif);
|
|
295
309
|
color: var(--rdt-color, inherit);
|
|
@@ -348,8 +362,28 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
348
362
|
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td[data-rozie-s-d5dcab4c] {
|
|
349
363
|
padding: var(--rdt-cell-padding, 0.5rem 0.75rem);
|
|
350
364
|
text-align: left;
|
|
365
|
+
/* Reset every side, then draw only the row line. Under separate borders each cell owns its
|
|
366
|
+
lines, and the reset keeps a host's plain \`table td { border }\` rule from drawing a second
|
|
367
|
+
set inside the table (this class-scoped rule outranks it). */
|
|
368
|
+
border: 0;
|
|
351
369
|
border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
|
|
352
370
|
}
|
|
371
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-detail-cell[data-rozie-s-d5dcab4c] {
|
|
372
|
+
border: 0;
|
|
373
|
+
}
|
|
374
|
+
.rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-th[data-rozie-s-d5dcab4c],
|
|
375
|
+
.rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-td[data-rozie-s-d5dcab4c],
|
|
376
|
+
.rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c],
|
|
377
|
+
.rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-detail-cell[data-rozie-s-d5dcab4c] {
|
|
378
|
+
border-inline-end: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
|
|
379
|
+
}
|
|
380
|
+
.rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] tr[data-rozie-s-d5dcab4c] > [data-rozie-s-d5dcab4c]:first-child:not(.rdt-col-spacer[data-rozie-s-d5dcab4c]),
|
|
381
|
+
.rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] tr[data-rozie-s-d5dcab4c] > .rdt-col-spacer[data-rozie-s-d5dcab4c]:first-child + *[data-rozie-s-d5dcab4c] {
|
|
382
|
+
border-inline-start: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
|
|
383
|
+
}
|
|
384
|
+
.rozie-data-table.rdt-column-lines[data-rozie-s-d5dcab4c] .rdt-thead[data-rozie-s-d5dcab4c] > tr[data-rozie-s-d5dcab4c]:first-child > .rdt-th[data-rozie-s-d5dcab4c] {
|
|
385
|
+
border-top: var(--rdt-column-border, 1px solid rgba(0, 0, 0, 0.08));
|
|
386
|
+
}
|
|
353
387
|
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-thead[data-rozie-s-d5dcab4c] .rdt-th[data-rozie-s-d5dcab4c] {
|
|
354
388
|
font-weight: var(--rdt-header-weight, 600);
|
|
355
389
|
/* OPAQUE default (was rgba(0,0,0,0.03)): a translucent header lets the scrolling body
|
|
@@ -473,8 +507,21 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
473
507
|
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-pinned[data-rozie-s-d5dcab4c] {
|
|
474
508
|
box-sizing: border-box;
|
|
475
509
|
}
|
|
510
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td.rdt-pinned[data-rozie-s-d5dcab4c] {
|
|
511
|
+
background-color: var(--rdt-pinned-bg, #fff);
|
|
512
|
+
}
|
|
513
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-group-header[data-rozie-s-d5dcab4c] > .rdt-td.rdt-pinned[data-rozie-s-d5dcab4c] {
|
|
514
|
+
background: linear-gradient(var(--rdt-group-header-bg, rgba(0, 0, 0, 0.025)), var(--rdt-group-header-bg, rgba(0, 0, 0, 0.025))), var(--rdt-pinned-bg, #fff);
|
|
515
|
+
}
|
|
516
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell.rdt-pinned[data-rozie-s-d5dcab4c] {
|
|
517
|
+
background: linear-gradient(var(--rdt-filter-row-bg, rgba(0, 0, 0, 0.015)), var(--rdt-filter-row-bg, rgba(0, 0, 0, 0.015))), var(--rdt-pinned-bg, #fff);
|
|
518
|
+
}
|
|
519
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-td.rdt-pinned.rdt-in-range[data-rozie-s-d5dcab4c] {
|
|
520
|
+
background: linear-gradient(var(--rdt-range-bg, rgba(37, 99, 235, 0.12)), var(--rdt-range-bg, rgba(37, 99, 235, 0.12))), var(--rdt-pinned-bg, #fff);
|
|
521
|
+
}
|
|
476
522
|
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c] {
|
|
477
523
|
padding: var(--rdt-filter-cell-padding, 0.35rem 0.75rem);
|
|
524
|
+
border: 0;
|
|
478
525
|
border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
|
|
479
526
|
}
|
|
480
527
|
.rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-filter-row[data-rozie-s-d5dcab4c] .rdt-col-filter {
|
|
@@ -799,6 +846,10 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
799
846
|
* Pure-CSS sticky header: the `<thead>` sticks to the top of the scroll container.
|
|
800
847
|
*/
|
|
801
848
|
@property({ type: Boolean, reflect: true, attribute: 'sticky-header' }) stickyHeader: boolean = false;
|
|
849
|
+
/**
|
|
850
|
+
* Draw a line between columns, plus an outer line at the start of each row and above the header row. The colour follows `--rozie-data-table-border-color` unless `--rozie-data-table-column-border-color` is set.
|
|
851
|
+
*/
|
|
852
|
+
@property({ type: Boolean, reflect: true, attribute: 'column-lines' }) columnLines: boolean = false;
|
|
802
853
|
/**
|
|
803
854
|
* `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'` — lights up the full WAI-ARIA **[grid interaction mode](/components/data-table-grid-mode)**: `role="grid"`, a roving single tab-stop, 2-D APG arrow-key cell navigation, range selection, and clipboard support.
|
|
804
855
|
*/
|
|
@@ -816,7 +867,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
816
867
|
*/
|
|
817
868
|
@property({ type: Number, reflect: true, attribute: 'undo-limit' }) undoLimit: number = 100;
|
|
818
869
|
/**
|
|
819
|
-
* When `true`, the table holds its current view through its own writes (edit, paste, fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
|
|
870
|
+
* When `true`, the table holds its current view through its own writes (edit, paste, fill, range fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
|
|
820
871
|
*/
|
|
821
872
|
@property({ type: Boolean, reflect: true, attribute: 'hold-view' }) holdView: boolean = false;
|
|
822
873
|
/**
|
|
@@ -958,7 +1009,9 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
958
1009
|
@property({ attribute: false }) cell?: (scope: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any }) => unknown;
|
|
959
1010
|
@state() private _hasSlotEditor = false;
|
|
960
1011
|
@queryAssignedElements({ slot: 'editor', flatten: true }) private _slotEditorElements!: Element[];
|
|
961
|
-
@property({ attribute: false }) editor?: (scope: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any; commit: (value: any
|
|
1012
|
+
@property({ attribute: false }) editor?: (scope: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any; commit: (value: any, options?: {
|
|
1013
|
+
toRange?: boolean;
|
|
1014
|
+
}) => void; cancel: () => void; columnLabel: string; autofocus: boolean; rangeSize: number }) => unknown;
|
|
962
1015
|
// Phase 79 Plan 08 (R4) contract for 79-09: the record intake for
|
|
963
1016
|
// record-routed slot fills. 79-09's consumer-side emitSlotFiller
|
|
964
1017
|
// accumulates an object literal onto the SAME `.rozieSlots=${{ ... }}`
|
|
@@ -968,7 +1021,9 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
968
1021
|
// named function-prop / <slot> fallback (AC-9). Attribute
|
|
969
1022
|
// deserialization is disabled — this is a function-valued record,
|
|
970
1023
|
// never reflected to/from an HTML attribute.
|
|
971
|
-
@property({ attribute: false }) rozieSlots?: { [key: `colHeader-${string}`]: (scope: { columnId: string; column: TableColumn<any, unknown>; label: string }) => unknown; [key: `filter-${string}`]: (scope: { columnId: string; value: any; uniqueValues: any[]; minMax: [number, number] | null; columnLabel: string; setFilter: (columnId: string, value: any) => void }) => unknown; [key: `cell-${string}`]: (scope: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any }) => unknown; [key: `editor-${string}`]: (scope: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any; commit: (value: any
|
|
1024
|
+
@property({ attribute: false }) rozieSlots?: { [key: `colHeader-${string}`]: (scope: { columnId: string; column: TableColumn<any, unknown>; label: string }) => unknown; [key: `filter-${string}`]: (scope: { columnId: string; value: any; uniqueValues: any[]; minMax: [number, number] | null; columnLabel: string; setFilter: (columnId: string, value: any) => void }) => unknown; [key: `cell-${string}`]: (scope: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any }) => unknown; [key: `editor-${string}`]: (scope: { columnId: string; column: TableColumn<any, unknown>; row: any; value: any; commit: (value: any, options?: {
|
|
1025
|
+
toRange?: boolean;
|
|
1026
|
+
}) => void; cancel: () => void; columnLabel: string; autofocus: boolean; rangeSize: number }) => unknown; } & Record<string, (scope: any) => unknown>;
|
|
972
1027
|
|
|
973
1028
|
private _disconnectCleanups: Array<() => void> = [];
|
|
974
1029
|
// Re-parenting guard: set true once the deferred teardown has actually
|
|
@@ -1499,6 +1554,12 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1499
1554
|
};
|
|
1500
1555
|
document.addEventListener('pointerdown', this.docPointerDown, true);
|
|
1501
1556
|
}
|
|
1557
|
+
// Range fill: a CAPTURE-phase keydown on the grid root runs before any editor's own keydown
|
|
1558
|
+
// handler — built-in, drop-in or slotted (on Lit the slotted editor's composed path still
|
|
1559
|
+
// passes through the grid root). Registered here in script rather than as a template
|
|
1560
|
+
// `.capture` binding so it is the same code on all six targets. It reads no reactive state
|
|
1561
|
+
// (a mount-time closure would see stale state on React); the commit checks the range.
|
|
1562
|
+
if (this.gridRoot) this.gridRoot.addEventListener('keydown', this.onEditorDropinKeyDownCapture, true);
|
|
1502
1563
|
// WR-04: NO on-mount auto-focus of the entry cell. Auto-focusing here stole focus on
|
|
1503
1564
|
// page load AND was non-deterministic on React/Solid (the entry cell may not be
|
|
1504
1565
|
// committed to the DOM yet at the $onMount microtask). The roving tabindex="0" entry
|
|
@@ -1693,6 +1754,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1693
1754
|
document.removeEventListener('pointerdown', this.docPointerDown, true);
|
|
1694
1755
|
this.docPointerDown = null;
|
|
1695
1756
|
}
|
|
1757
|
+
if (this.gridRoot) this.gridRoot.removeEventListener('keydown', this.onEditorDropinKeyDownCapture, true);
|
|
1696
1758
|
if (this.virtualizerCleanup) this.virtualizerCleanup();
|
|
1697
1759
|
// Phase 87 87-04: tear down the column virtualizer's scroll-element ResizeObserver too.
|
|
1698
1760
|
// No-op when column windowing was off (cleanup stays null).
|
|
@@ -1757,7 +1819,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1757
1819
|
${repeat<any>(this.groupingKeys(), (gk, _idx) => gk, (gk, _idx) => html`<span class="rdt-group-token" data-group-token="" data-rozie-s-d5dcab4c>${rozieDisplay(gk)}</span>`)}
|
|
1758
1820
|
</slot>`}
|
|
1759
1821
|
</div>` : nothing}${this.isWindowed() ? html`<div class="rdt-scroll" style=${rozieStyle(this.rowsWindowed() && this.maxHeight ? 'max-height:' + this.maxHeight + ';overflow:auto;--rozie-data-table-max-height:' + this.maxHeight : 'overflow:auto')} data-rozie-s-d5dcab4c>
|
|
1760
|
-
<table class="${Object.entries({ "rozie-data-table": true, 'rdt-sticky': this.stickyHeader, 'rdt-col-windowed': this.colsWindowed() }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.tableRole())} aria-rowcount=${rozieAttr(this.gridAriaRowCount())} aria-colcount=${rozieAttr(this.gridAriaColCount())} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridKeyDown($event); }} @focusin=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.syncActiveFromEvent($event); }} @focusout=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridFocusOut($event); }} @mousedown=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridMouseDown($event); }} @dblclick=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridDblClick($event); }} @click=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridClick($event); }} data-rozie-s-d5dcab4c>
|
|
1822
|
+
<table class="${Object.entries({ "rozie-data-table": true, 'rdt-sticky': this.stickyHeader, 'rdt-col-windowed': this.colsWindowed(), 'rdt-column-lines': this.columnLines }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.tableRole())} aria-rowcount=${rozieAttr(this.gridAriaRowCount())} aria-colcount=${rozieAttr(this.gridAriaColCount())} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridKeyDown($event); }} @focusin=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.syncActiveFromEvent($event); }} @focusout=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridFocusOut($event); }} @mousedown=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridMouseDown($event); }} @dblclick=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridDblClick($event); }} @click=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridClick($event); }} data-rozie-s-d5dcab4c>
|
|
1761
1823
|
<thead class="rdt-thead" role="rowgroup" data-rozie-s-d5dcab4c>
|
|
1762
1824
|
${repeat<any>(this._headerGroups.value, (hg, hgLevel) => hg.id, (hg, hgLevel) => html`<tr class="rdt-tr" role="row" aria-rowindex=${rozieAttr(hgLevel + 1)} data-rozie-s-d5dcab4c>
|
|
1763
1825
|
|
|
@@ -1839,8 +1901,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1839
1901
|
${this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'select' ? html`<select class="rdt-cell-editor" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c>
|
|
1840
1902
|
${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} ?selected=${opt.value === this.editorValueFor(cell.column.id)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
|
|
1841
1903
|
</select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} ?checked=${this.editorCheckedFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorCheckbox(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'custom' ? html`<span style="display:contents" @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onEditorDropinKeyDown($event); }} data-rozie-s-d5dcab4c>
|
|
1842
|
-
${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}) : html`<slot name="${`editor-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}>
|
|
1843
|
-
${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}>
|
|
1904
|
+
${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index), rangeSize: this.inRowEdit() ? 1 : this.rangeSize()}) : html`<slot name="${`editor-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index), rangeSize: this.inRowEdit() ? 1 : this.rangeSize()}); } catch { return '{}'; } })()}>
|
|
1905
|
+
${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index), rangeSize: this.inRowEdit() ? 1 : this.rangeSize()}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index), rangeSize: this.inRowEdit() ? 1 : this.rangeSize()}); } catch { return '{}'; } })()}>
|
|
1844
1906
|
<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />
|
|
1845
1907
|
</slot>`}
|
|
1846
1908
|
</slot>`}
|
|
@@ -1863,7 +1925,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1863
1925
|
</tr>
|
|
1864
1926
|
</tbody>
|
|
1865
1927
|
</table>
|
|
1866
|
-
</div>` : html`<table class="${Object.entries({ "rozie-data-table": true, 'rdt-sticky': this.stickyHeader }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.tableRole())} aria-rowcount=${rozieAttr(this.gridAriaRowCount())} aria-colcount=${rozieAttr(this.gridAriaColCount())} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridKeyDown($event); }} @focusin=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.syncActiveFromEvent($event); }} @focusout=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridFocusOut($event); }} @mousedown=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridMouseDown($event); }} @dblclick=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridDblClick($event); }} @click=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridClick($event); }} data-rozie-s-d5dcab4c>
|
|
1928
|
+
</div>` : html`<table class="${Object.entries({ "rozie-data-table": true, 'rdt-sticky': this.stickyHeader, 'rdt-column-lines': this.columnLines }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.tableRole())} aria-rowcount=${rozieAttr(this.gridAriaRowCount())} aria-colcount=${rozieAttr(this.gridAriaColCount())} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridKeyDown($event); }} @focusin=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.syncActiveFromEvent($event); }} @focusout=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridFocusOut($event); }} @mousedown=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridMouseDown($event); }} @dblclick=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridDblClick($event); }} @click=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridClick($event); }} data-rozie-s-d5dcab4c>
|
|
1867
1929
|
<thead class="rdt-thead" role="rowgroup" data-rozie-s-d5dcab4c>
|
|
1868
1930
|
${repeat<any>(this._headerGroups.value, (hg, hgLevel) => hg.id, (hg, hgLevel) => html`<tr class="rdt-tr" role="row" aria-rowindex=${rozieAttr(hgLevel + 1)} data-rozie-s-d5dcab4c>
|
|
1869
1931
|
${repeat<any>(hg.headers, (header, _idx) => header.id, (header, _idx) => html`<th class="${Object.entries({ "rdt-th": true, 'rdt-select-th': this.isSelectColumn(header.column.id), 'rdt-expander-th': this.isExpanderColumn(header.column.id), 'rdt-pinned': this.columnPinSide(header.column.id) !== '', 'rdt-th-resizing': this.columnIsResizing(header.column.id), 'rdt-cell-active': this.isActiveCell('__header', this.headerColIndexOf(hg, header), hgLevel) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="columnheader" data-col=${rozieAttr(header.column.id)} data-grid-cell="" data-row="__header" data-header-level=${rozieAttr(hgLevel)} colspan=${rozieAttr(header.colSpan > 1 ? header.colSpan : null)} data-col-index=${rozieAttr(this.headerColIndexOf(hg, header))} aria-colindex=${rozieAttr(this.headerLeafStart(hg, header) + 1)} tabindex=${rozieAttr(this.cellTabindex('__header', this.headerColIndexOf(hg, header), hgLevel))} aria-sort=${rozieAttr(this.ariaSortFor(header.column.id))} style=${rozieStyle(this.thStyle(header))} data-rozie-s-d5dcab4c>
|
|
@@ -1936,8 +1998,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1936
1998
|
${this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'select' ? html`<select class="rdt-cell-editor" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c>
|
|
1937
1999
|
${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} ?selected=${opt.value === this.editorValueFor(cell.column.id)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
|
|
1938
2000
|
</select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} ?checked=${this.editorCheckedFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorCheckbox(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'custom' ? html`<span style="display:contents" @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onEditorDropinKeyDown($event); }} data-rozie-s-d5dcab4c>
|
|
1939
|
-
${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}) : html`<slot name="${`editor-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}>
|
|
1940
|
-
${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}>
|
|
2001
|
+
${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row)), rangeSize: this.inRowEdit() ? 1 : this.rangeSize()}) : html`<slot name="${`editor-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row)), rangeSize: this.inRowEdit() ? 1 : this.rangeSize()}); } catch { return '{}'; } })()}>
|
|
2002
|
+
${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row)), rangeSize: this.inRowEdit() ? 1 : this.rangeSize()}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row)), rangeSize: this.inRowEdit() ? 1 : this.rangeSize()}); } catch { return '{}'; } })()}>
|
|
1941
2003
|
<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />
|
|
1942
2004
|
</slot>`}
|
|
1943
2005
|
</slot>`}
|
|
@@ -6571,6 +6633,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6571
6633
|
// any OTHER cell falls through to clearRange() and clears the flag on the way past.
|
|
6572
6634
|
if (this.rangeTransition) {
|
|
6573
6635
|
this.rangeTransition = false;
|
|
6636
|
+
} else if (!isHeader && tgt !== cellEl && this._editingRow.value === movedRow && this._editingCol.value === col) {
|
|
6637
|
+
// Focus moving INTO the open editor of this cell (a built-in input, or a slot editor that
|
|
6638
|
+
// focuses itself) is not navigation: keep the range so the editor can commit to it with
|
|
6639
|
+
// Ctrl/Cmd+Enter.
|
|
6574
6640
|
} else if (this.rangeClickPending && this.rangeClickPending.isHeader === isHeader && this.rangeClickPending.r === movedRow && this.rangeClickPending.c === col) {
|
|
6575
6641
|
this.rangeClickPending = null;
|
|
6576
6642
|
} else {
|
|
@@ -7963,6 +8029,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7963
8029
|
this.applyGridToRange(grid, box.r0, box.c0, 'filled');
|
|
7964
8030
|
};
|
|
7965
8031
|
|
|
8032
|
+
// ══ Range fill (Ctrl/Cmd+Enter from an editor; Space on a checkbox range) ════════════════
|
|
8033
|
+
// rangeSize(): how many cells a range fill would write — the range's rows in the ACTIVE
|
|
8034
|
+
// cell's column, or 1 with no range. Exposed to editor slots as `rangeSize` so a custom
|
|
8035
|
+
// editor can say "Apply to 24 cells".
|
|
8036
|
+
fillTargets = () => rangeFillTargets(this.normalizedRange(), this._activeRow.value, this._activeColIndex.value);
|
|
8037
|
+
|
|
8038
|
+
rangeSize = () => this._activeIsHeader.value ? 1 : this.fillTargets().size;
|
|
8039
|
+
|
|
8040
|
+
// fillActiveColumn(value): write ONE value down the active cell's column for every row of the
|
|
8041
|
+
// range, through the same funnel as paste and fill (skip rule, one undo step, one
|
|
8042
|
+
// cell-edit-commit per changed cell, "N of M cells updated"). The value is written AS IS — a
|
|
8043
|
+
// one-column grid of the same value, never stringified — so an array or object survives.
|
|
8044
|
+
fillActiveColumn = (value: any) => {
|
|
8045
|
+
const t = this.fillTargets();
|
|
8046
|
+
const grid = [];
|
|
8047
|
+
for (let i = 0; i < t.size; i++) grid.push([value]);
|
|
8048
|
+
return this.applyGridToRange(grid, t.top, t.col, 'updated');
|
|
8049
|
+
};
|
|
8050
|
+
|
|
7966
8051
|
// onFillHandlePointerDown: begin a fill-handle drag (req-8 / D-04). The handle sits on the
|
|
7967
8052
|
// range's bottom-right cell; a pointer drag extends the range (reusing setRangeFocus off the
|
|
7968
8053
|
// cell under the pointer) and, on release, value-fills the dragged rectangle. Kept minimal:
|
|
@@ -8805,6 +8890,62 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
8805
8890
|
return true;
|
|
8806
8891
|
};
|
|
8807
8892
|
|
|
8893
|
+
// commitEditToRange(overrideValue): the Ctrl/Cmd+Enter commit. Writes the editor's value to
|
|
8894
|
+
// every selected cell in the editing cell's column through fillActiveColumn (the paste/fill
|
|
8895
|
+
// funnel: skip rule, one undo step, one cell-edit-commit per changed cell, "N of M cells
|
|
8896
|
+
// updated"), then closes the editor and returns focus to the active cell WITHOUT clearing the
|
|
8897
|
+
// range. The value is checked against the editing cell first: if the cell being edited rejects
|
|
8898
|
+
// it, the editor stays open with the error, exactly as a single-cell commit does.
|
|
8899
|
+
commitEditToRange = (overrideValue = undefined) => {
|
|
8900
|
+
if (this._editingRow.value < 0) return false;
|
|
8901
|
+
if (this.committedThisSession) return false;
|
|
8902
|
+
const colId = this.editingColumnId();
|
|
8903
|
+
if (colId == null) {
|
|
8904
|
+
this.endEdit();
|
|
8905
|
+
return false;
|
|
8906
|
+
}
|
|
8907
|
+
const rawValue = overrideValue !== undefined ? overrideValue : this._draftValue.value;
|
|
8908
|
+
const err = this.runValidator(colId, this.coerceCellValue(colId, rawValue), this.editingRowOriginal());
|
|
8909
|
+
if (err !== true) {
|
|
8910
|
+
this.setInvalid(err);
|
|
8911
|
+
this.focusEditorWhenReady(this._editingRow.value, this._editingCol.value);
|
|
8912
|
+
return false;
|
|
8913
|
+
}
|
|
8914
|
+
this.setInvalid('');
|
|
8915
|
+
const focusRow = this._editingRow.value;
|
|
8916
|
+
const focusCol = this._editingCol.value;
|
|
8917
|
+
const rowOriginal = this.editingRowOriginal();
|
|
8918
|
+
const rowId = this.editingRowId();
|
|
8919
|
+
// Same teardown guards as commitEdit: the unmount blur of the editor must not commit again.
|
|
8920
|
+
this.editTransition = true;
|
|
8921
|
+
this.committedThisSession = true;
|
|
8922
|
+
const result = this.fillActiveColumn(rawValue);
|
|
8923
|
+
this.endEdit();
|
|
8924
|
+
this.editTransition = false;
|
|
8925
|
+
// Keep the range when focus comes back to the active cell (the selection stays put after a
|
|
8926
|
+
// fill, as in a spreadsheet). Coordinate-matched, like a Shift+Click: if focus lands anywhere
|
|
8927
|
+
// else first (the user clicks another cell, or the focus return gives up), that focusin
|
|
8928
|
+
// clears the range as usual instead of inheriting a stale "keep" flag.
|
|
8929
|
+
this.rangeClickPending = {
|
|
8930
|
+
isHeader: false,
|
|
8931
|
+
r: focusRow,
|
|
8932
|
+
c: focusCol
|
|
8933
|
+
};
|
|
8934
|
+
if (result && result.changed > 0) {
|
|
8935
|
+
// A write can re-sort or re-filter the rows (holdView off), so follow the active cell's
|
|
8936
|
+
// RECORD rather than its old index — the same B23 path commitEdit uses. A re-order also
|
|
8937
|
+
// clears the range, so the coordinate match above only keeps it when nothing moved.
|
|
8938
|
+
this.pendingEditFollow = {
|
|
8939
|
+
rowOriginal,
|
|
8940
|
+
rowId,
|
|
8941
|
+
col: focusCol
|
|
8942
|
+
};
|
|
8943
|
+
} else {
|
|
8944
|
+
this.focusCellWhenReady(focusRow, focusCol);
|
|
8945
|
+
}
|
|
8946
|
+
return true;
|
|
8947
|
+
};
|
|
8948
|
+
|
|
8808
8949
|
// toggleActiveBooleanCell (design doc 2026-07-05, Change 1): the spreadsheet-standard
|
|
8809
8950
|
// single-keystroke boolean toggle. Flips the ACTIVE cell's value and commits it through the
|
|
8810
8951
|
// EXACT SAME write funnel commitEdit uses (replaceRowValue → writeData → single $emit) but
|
|
@@ -8833,6 +8974,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
8833
8974
|
const rowId = row.id;
|
|
8834
8975
|
const oldValue = this.cellValueAt(this._activeRow.value, this._activeColIndex.value);
|
|
8835
8976
|
const newValue = !oldValue;
|
|
8977
|
+
// With a range selected, set every selected cell in this column to the active cell's new
|
|
8978
|
+
// value (one value for all, not a per-cell flip) through the paste/fill funnel: invalid cells
|
|
8979
|
+
// skipped, one undo step, one cell-edit-commit per changed cell, "N of M cells updated".
|
|
8980
|
+
if (this.rangeSize() > 1) {
|
|
8981
|
+
this.setInvalid('');
|
|
8982
|
+
this.fillActiveColumn(newValue);
|
|
8983
|
+
return;
|
|
8984
|
+
}
|
|
8836
8985
|
// D-01: same discipline as commitEdit — a rejecting validator blocks the toggle. There is
|
|
8837
8986
|
// no editor to keep open here, so the toggle simply does not apply (no model write).
|
|
8838
8987
|
const err = this.runValidator(colId, newValue, rowOriginal);
|
|
@@ -9238,6 +9387,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
9238
9387
|
committed: boolean;
|
|
9239
9388
|
} | null = null;
|
|
9240
9389
|
|
|
9390
|
+
// rangeCommitIntent: set by the drop-in wrapper's CAPTURE-phase keydown when Ctrl/Cmd+Enter is
|
|
9391
|
+
// pressed while a range is selected, so the slot editor's own commit — which runs during that
|
|
9392
|
+
// same keypress — goes to the whole range. Cleared by the bubble-phase handler and by a
|
|
9393
|
+
// setTimeout(0), so an editor that does not commit during the keypress falls back to one cell.
|
|
9394
|
+
rangeCommitIntent = false;
|
|
9395
|
+
|
|
9241
9396
|
// ── Per-cell editor draft source (req-6) ──────────────────────────────────────────────
|
|
9242
9397
|
// In single-cell mode every editor binds the shared $data.draftValue. In full-row mode
|
|
9243
9398
|
// (editingRowIndex != null) each editable cell owns its OWN draft keyed by columnId in
|
|
@@ -9256,11 +9411,19 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
9256
9411
|
// only WRITES this column's draft (the row commits as a unit later — never per cell). cancel()
|
|
9257
9412
|
// reverts the cell (single) or the whole row (row mode). Factory-bound per columnId so the
|
|
9258
9413
|
// row-mode commit targets the right draft key.
|
|
9259
|
-
editorCommitFor = (colId: any) => (value: any) => {
|
|
9414
|
+
editorCommitFor = (colId: any) => (value: any, options: any) => {
|
|
9260
9415
|
if (this.inRowEdit()) {
|
|
9261
9416
|
this.setRowDraft(colId, value);
|
|
9262
9417
|
return;
|
|
9263
9418
|
}
|
|
9419
|
+
// Range fill: an explicit `commit(v, { toRange: true })`, or any commit made during a
|
|
9420
|
+
// Ctrl/Cmd+Enter keypress (rangeCommitIntent), writes v to every selected cell in this column.
|
|
9421
|
+
const wantsRange = options != null && options.toRange === true || this.rangeCommitIntent;
|
|
9422
|
+
this.rangeCommitIntent = false;
|
|
9423
|
+
if (wantsRange && !this.dropinTabFlush && this.rangeSize() > 1) {
|
|
9424
|
+
this.commitEditToRange(value);
|
|
9425
|
+
return;
|
|
9426
|
+
}
|
|
9264
9427
|
// C-10: during the drop-in wrapper's Tab flush, record the outcome and skip the focus
|
|
9265
9428
|
// return — the Tab handler advances focus to the next editor itself.
|
|
9266
9429
|
if (this.dropinTabFlush) {
|
|
@@ -9372,7 +9535,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
9372
9535
|
}
|
|
9373
9536
|
return;
|
|
9374
9537
|
}
|
|
9375
|
-
if (key === 'Enter') {
|
|
9538
|
+
if (key === 'Enter' && (e.ctrlKey || e.metaKey) && this.rangeSize() > 1) {
|
|
9539
|
+
// Ctrl/Cmd+Enter with a range selected: write the draft to every selected cell in this
|
|
9540
|
+
// column. Plain Enter (below) still commits the active cell only.
|
|
9541
|
+
e.preventDefault();
|
|
9542
|
+
this.commitEditToRange(undefined);
|
|
9543
|
+
} else if (key === 'Enter') {
|
|
9376
9544
|
e.preventDefault();
|
|
9377
9545
|
this.commitEdit(undefined);
|
|
9378
9546
|
} else if (key === 'Tab') {
|
|
@@ -9460,8 +9628,24 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
9460
9628
|
// advances. `dropinTabFlush` tells editorCommitFor to report the outcome and skip the focus
|
|
9461
9629
|
// return. A drop-in that does not commit on blur (EditorCheckbox commits on change, so it holds
|
|
9462
9630
|
// nothing pending) falls back to the host-draft path, which is exactly right in that case.
|
|
9631
|
+
// Capture-phase keydown on the grid root (registered at mount): runs BEFORE any editor's own
|
|
9632
|
+
// keydown handler. Ctrl/Cmd+Enter marks the commit an editor makes during this keypress as a
|
|
9633
|
+
// range commit; editorCommitFor decides whether a range is actually selected. The mark expires
|
|
9634
|
+
// after this event, so a commit that comes later (on blur, after an async step) writes one
|
|
9635
|
+
// cell, as usual. Reads NO reactive state: it is a mount-time closure (stale on React).
|
|
9636
|
+
onEditorDropinKeyDownCapture = (e: any) => {
|
|
9637
|
+
if (!e) return;
|
|
9638
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
9639
|
+
this.rangeCommitIntent = true;
|
|
9640
|
+
setTimeout(() => {
|
|
9641
|
+
this.rangeCommitIntent = false;
|
|
9642
|
+
}, 0);
|
|
9643
|
+
}
|
|
9644
|
+
};
|
|
9645
|
+
|
|
9463
9646
|
onEditorDropinKeyDown = (e: any) => {
|
|
9464
9647
|
if (!e) return;
|
|
9648
|
+
this.rangeCommitIntent = false;
|
|
9465
9649
|
const key = e.key;
|
|
9466
9650
|
if (this.inRowEdit()) {
|
|
9467
9651
|
if (key === 'Enter') {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// rangeFill.ts — which cells a range fill writes. Pure, sigil-free.
|
|
2
|
+
//
|
|
3
|
+
// A range fill (Ctrl/Cmd+Enter in an editor, or Space on a checkbox range) writes one value
|
|
4
|
+
// down the ACTIVE cell's column for every row the range covers. Other columns of a
|
|
5
|
+
// multi-column range are left alone: one typed value rarely makes sense in several columns.
|
|
6
|
+
// With no range, or an active cell outside it, the fill is the active cell alone (size 1).
|
|
7
|
+
|
|
8
|
+
export interface FillRange { r0: number; r1: number; c0: number; c1: number }
|
|
9
|
+
export interface FillTargets { top: number; bottom: number; col: number; size: number }
|
|
10
|
+
|
|
11
|
+
const rangeFillTargets = (range: FillRange | null, activeRow: number, activeCol: number): FillTargets => {
|
|
12
|
+
const single = { top: activeRow, bottom: activeRow, col: activeCol, size: 1 }
|
|
13
|
+
if (!range) return single
|
|
14
|
+
const top = Math.min(range.r0, range.r1)
|
|
15
|
+
const bottom = Math.max(range.r0, range.r1)
|
|
16
|
+
const left = Math.min(range.c0, range.c1)
|
|
17
|
+
const right = Math.max(range.c0, range.c1)
|
|
18
|
+
if (activeCol < left || activeCol > right || activeRow < top || activeRow > bottom) return single
|
|
19
|
+
return { top, bottom, col: activeCol, size: bottom - top + 1 }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { rangeFillTargets }
|
package/src/themes/base.css
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* @rozie-ui/data-table — base token reference.
|
|
3
3
|
*
|
|
4
|
-
* This file is the PUBLIC token reference. It wires each of the
|
|
4
|
+
* This file is the PUBLIC token reference. It wires each of the 20 public
|
|
5
5
|
* `--rozie-data-table-*` tokens, with its default, onto the short internal `--rdt-*`
|
|
6
|
-
* token the component's scoped `<style>` actually reads. The component reads
|
|
6
|
+
* token the component's scoped `<style>` actually reads. The component reads 84 internal `--rdt-*`
|
|
7
7
|
* tokens in total, each with an inline `var(--rdt-x, <fallback>)` default, so the
|
|
8
8
|
* table renders ZERO-CONFIG without importing this file.
|
|
9
9
|
*
|
|
10
10
|
* Scope, stated plainly: remapping the public tokens re-skins the table's primary
|
|
11
11
|
* surfaces — type, borders, background, header, row, cell padding, accent, and the
|
|
12
|
-
* max-height. It does NOT reach every cosmetic value. 58 of the
|
|
12
|
+
* max-height. It does NOT reach every cosmetic value. 58 of the 84 internal tokens
|
|
13
13
|
* have no public counterpart today, including the grid active-cell ring, the range
|
|
14
14
|
* highlight, the fill handle, the invalid-cell outline, the group tokens, the filter
|
|
15
15
|
* row, the per-header column ⋯ menu (its panel is a composed @rozie-ui/popover, so its
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* the element itself), never on the `.rozie-data-table` / `.rozie-data-table-wrap`
|
|
28
28
|
* classes — on that target both of those render inside the component's shadow root,
|
|
29
29
|
* where a document stylesheet cannot reach them. The tokens themselves inherit across
|
|
30
|
-
* the boundary, and the component carries the mapping internally, so every one of the
|
|
30
|
+
* the boundary, and the component carries the mapping internally, so every one of the 20
|
|
31
31
|
* works there.
|
|
32
32
|
*
|
|
33
33
|
* NOTE the split: the STRUCTURAL rules — `border-collapse`, the keyed-r-for
|
|
@@ -75,6 +75,12 @@
|
|
|
75
75
|
--rdt-border: 1px solid var(--rozie-data-table-border-color, rgba(0, 0, 0, 0.08));
|
|
76
76
|
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, rgba(0, 0, 0, 0.2));
|
|
77
77
|
--rdt-filter-radius: var(--rozie-data-table-radius, 4px);
|
|
78
|
+
/* Column lines are off unless the `columnLines` prop is set. Their colour follows the
|
|
79
|
+
row-line colour unless the column token is set, so one variable themes both. */
|
|
80
|
+
--rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, rgba(0, 0, 0, 0.08)));
|
|
81
|
+
/* Pinned body cells are opaque so scrolled content never shows through them. Set this
|
|
82
|
+
to the surface colour behind the table. */
|
|
83
|
+
--rdt-pinned-bg: var(--rozie-data-table-pinned-bg, #fff);
|
|
78
84
|
|
|
79
85
|
/* ── typography / cell metrics ──────────────────────────────────────────── */
|
|
80
86
|
--rdt-font: var(--rozie-data-table-font, 14px system-ui, sans-serif);
|
|
@@ -125,6 +131,8 @@
|
|
|
125
131
|
--rdt-ds-fg: currentColor;
|
|
126
132
|
--rdt-ds-select-accent: #0066cc;
|
|
127
133
|
--rdt-ds-border-color: rgba(0, 0, 0, 0.08);
|
|
134
|
+
--rdt-ds-column-border-color: var(--rdt-ds-border-color);
|
|
135
|
+
--rdt-ds-pinned-bg: #fff;
|
|
128
136
|
--rdt-ds-control-border-color: rgba(0, 0, 0, 0.2);
|
|
129
137
|
--rdt-ds-radius: 4px;
|
|
130
138
|
--rdt-ds-font: 14px system-ui, sans-serif;
|
package/src/themes/bootstrap.css
CHANGED
|
@@ -35,6 +35,8 @@ rozie-data-table {
|
|
|
35
35
|
--rdt-border: 1px solid var(--rozie-data-table-border-color, var(--bs-border-color, #dee2e6));
|
|
36
36
|
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--bs-border-color, #dee2e6));
|
|
37
37
|
--rdt-filter-radius: var(--rozie-data-table-radius, var(--bs-border-radius, 0.375rem));
|
|
38
|
+
--rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, var(--bs-border-color, #dee2e6)));
|
|
39
|
+
--rdt-pinned-bg: var(--rozie-data-table-pinned-bg, var(--bs-body-bg, #fff));
|
|
38
40
|
--rdt-header-bg: var(--rozie-data-table-header-bg, var(--bs-tertiary-bg, #f8f9fa));
|
|
39
41
|
--rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color, var(--bs-border-color, #dee2e6));
|
|
40
42
|
--rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active, var(--rozie-data-table-accent, var(--bs-primary, #0d6efd)));
|
|
@@ -60,6 +62,7 @@ rozie-data-table {
|
|
|
60
62
|
--rdt-ds-fg: var(--bs-body-color, #212529);
|
|
61
63
|
--rdt-ds-select-accent: var(--bs-primary, #0d6efd);
|
|
62
64
|
--rdt-ds-border-color: var(--bs-border-color, #dee2e6);
|
|
65
|
+
--rdt-ds-pinned-bg: var(--bs-body-bg, #fff);
|
|
63
66
|
--rdt-ds-control-border-color: var(--bs-border-color, #dee2e6);
|
|
64
67
|
--rdt-ds-radius: var(--bs-border-radius, 0.375rem);
|
|
65
68
|
--rdt-ds-header-bg: var(--bs-tertiary-bg, #f8f9fa);
|
package/src/themes/material.css
CHANGED
|
@@ -33,6 +33,8 @@ rozie-data-table {
|
|
|
33
33
|
--rdt-color: var(--rozie-data-table-fg, var(--md-sys-color-on-surface, #1d1b20));
|
|
34
34
|
--rdt-select-accent: var(--rozie-data-table-select-accent, var(--rozie-data-table-accent, var(--md-sys-color-primary, #6750a4)));
|
|
35
35
|
--rdt-border: 1px solid var(--rozie-data-table-border-color, var(--md-sys-color-outline-variant, #cac4d0));
|
|
36
|
+
--rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, var(--md-sys-color-outline-variant, #cac4d0)));
|
|
37
|
+
--rdt-pinned-bg: var(--rozie-data-table-pinned-bg, var(--md-sys-color-surface, #fef7ff));
|
|
36
38
|
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--md-sys-color-outline, #79747e));
|
|
37
39
|
--rdt-header-bg: var(--rozie-data-table-header-bg, var(--md-sys-color-surface-container-highest, #e6e0e9));
|
|
38
40
|
--rdt-header-weight: var(--rozie-data-table-header-weight, 500);
|
|
@@ -55,6 +57,7 @@ rozie-data-table {
|
|
|
55
57
|
--rdt-ds-fg: var(--md-sys-color-on-surface, #1d1b20);
|
|
56
58
|
--rdt-ds-select-accent: var(--md-sys-color-primary, #6750a4);
|
|
57
59
|
--rdt-ds-border-color: var(--md-sys-color-outline-variant, #cac4d0);
|
|
60
|
+
--rdt-ds-pinned-bg: var(--md-sys-color-surface, #fef7ff);
|
|
58
61
|
--rdt-ds-control-border-color: var(--md-sys-color-outline, #79747e);
|
|
59
62
|
--rdt-ds-radius: 4px;
|
|
60
63
|
--rdt-ds-header-bg: var(--md-sys-color-surface-container-highest, #e6e0e9);
|
package/src/themes/shadcn.css
CHANGED
|
@@ -39,6 +39,8 @@ rozie-data-table {
|
|
|
39
39
|
--rdt-color: var(--rozie-data-table-fg, hsl(var(--foreground, 222.2 47.4% 11.2%)));
|
|
40
40
|
--rdt-select-accent: var(--rozie-data-table-select-accent, var(--rozie-data-table-accent, hsl(var(--primary, 222.2 47.4% 11.2%))));
|
|
41
41
|
--rdt-border: 1px solid var(--rozie-data-table-border-color, hsl(var(--border, 214.3 31.8% 91.4%)));
|
|
42
|
+
--rdt-column-border: 1px solid var(--rozie-data-table-column-border-color, var(--rozie-data-table-border-color, hsl(var(--border, 214.3 31.8% 91.4%))));
|
|
43
|
+
--rdt-pinned-bg: var(--rozie-data-table-pinned-bg, hsl(var(--background, 0 0% 100%)));
|
|
42
44
|
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, hsl(var(--border, 214.3 31.8% 91.4%)));
|
|
43
45
|
--rdt-filter-radius: var(--rozie-data-table-radius, var(--radius, 0.5rem));
|
|
44
46
|
--rdt-header-bg: var(--rozie-data-table-header-bg, hsl(var(--muted, 210 40% 96.1%)));
|
|
@@ -65,6 +67,7 @@ rozie-data-table {
|
|
|
65
67
|
--rdt-ds-fg: hsl(var(--foreground, 222.2 47.4% 11.2%));
|
|
66
68
|
--rdt-ds-select-accent: hsl(var(--primary, 222.2 47.4% 11.2%));
|
|
67
69
|
--rdt-ds-border-color: hsl(var(--border, 214.3 31.8% 91.4%));
|
|
70
|
+
--rdt-ds-pinned-bg: hsl(var(--background, 0 0% 100%));
|
|
68
71
|
--rdt-ds-control-border-color: hsl(var(--border, 214.3 31.8% 91.4%));
|
|
69
72
|
--rdt-ds-radius: var(--radius, 0.5rem);
|
|
70
73
|
--rdt-ds-header-bg: hsl(var(--muted, 210 40% 96.1%));
|