twico-ui 1.25.0 → 1.27.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 +14 -0
- package/dist/index.cjs +306 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.mjs +306 -24
- package/dist/index.mjs.map +1 -1
- package/llms.txt +4 -0
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1938,7 +1938,10 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
|
|
|
1938
1938
|
onStateChange?: (state: DatatableState) => void;
|
|
1939
1939
|
/** Show a "Go to" page jumper in the footer when there are more than 5 pages. @default true */
|
|
1940
1940
|
showPageJumper?: boolean;
|
|
1941
|
-
/** Click-to-select mode: "row" highlights the clicked row, "cell"
|
|
1941
|
+
/** Click-to-select mode: "row" highlights the clicked row, "cell" enables spreadsheet-like cell selection.
|
|
1942
|
+
* In "cell" mode the grid supports keyboard navigation and **rectangular range selection** (#317):
|
|
1943
|
+
* Shift+Click / Shift+Arrow extend a range from an anchor; selected cells expose `aria-selected` and the
|
|
1944
|
+
* grid is `aria-multiselectable` with `aria-activedescendant` on the active cell. @default "none" */
|
|
1942
1945
|
selectionMode?: "none" | "row" | "cell";
|
|
1943
1946
|
/** Fired when a row is clicked in "row" selection mode: (row, key). */
|
|
1944
1947
|
onRowClick?: (row: T, key: string | number) => void;
|
|
@@ -1946,6 +1949,22 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
|
|
|
1946
1949
|
onCellClick?: (value: any, row: T, field: string) => void;
|
|
1947
1950
|
/** Fired when the active cell changes: ({ key, field } | null). */
|
|
1948
1951
|
onActiveCellChange?: (cell: { key: string | number; field: string } | null) => void;
|
|
1952
|
+
/** #317: fired when the rectangular cell selection changes ("cell" mode) — every cell inside the
|
|
1953
|
+
* anchor→focus rectangle, in row-major order. Empty array when the selection clears. */
|
|
1954
|
+
onCellSelectionChange?: (cells: Array<{ key: string | number; field: string }>) => void;
|
|
1955
|
+
/** #318: enable spreadsheet clipboard on cells ("cell" mode): Ctrl/Cmd+C copies the active cell/range as
|
|
1956
|
+
* TSV, Ctrl/Cmd+X cuts (copy then clear), Ctrl/Cmd+V pastes onto the target rectangle from the active
|
|
1957
|
+
* cell. Paste is format-restricted by column `copyType` (incompatible cells are skipped) and commits
|
|
1958
|
+
* through `onRowUpdate`/`onRowsChange` in one batched change; outcomes are announced via `aria-live`.
|
|
1959
|
+
* Copy/cut/paste also show a brief **visible** in-grid confirmation toast + a flash on the affected cells
|
|
1960
|
+
* (#320), so sighted users get feedback too. @default false */
|
|
1961
|
+
enableClipboard?: boolean;
|
|
1962
|
+
/** #320: fired on a clipboard copy/cut with the copied cells (row-major `{key,field}[]`) and `{ cut }`.
|
|
1963
|
+
* Use it to surface your own toast / integrate an app notification system. */
|
|
1964
|
+
onCellsCopy?: (cells: Array<{ key: string | number; field: string }>, meta: { cut: boolean }) => void;
|
|
1965
|
+
/** #320: fired after a clipboard paste with the outcome — `written` cells committed, `skipped` cells
|
|
1966
|
+
* rejected (read-only or incompatible `copyType`). */
|
|
1967
|
+
onCellsPaste?: (result: { written: number; skipped: number }) => void;
|
|
1949
1968
|
/** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
|
|
1950
1969
|
* toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
|
|
1951
1970
|
* changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
|
|
@@ -2157,6 +2176,11 @@ interface DatatableColumn<T = any> {
|
|
|
2157
2176
|
loadValueOptions?: (query: string) => Promise<Array<string | { value: string; label: string }>>;
|
|
2158
2177
|
/** Format the raw value to a string/number for display. */
|
|
2159
2178
|
valueFormatter?: (value: any, row: T) => React.ReactNode;
|
|
2179
|
+
/** #318: opaque copy/paste compatibility token for the cell clipboard (`enableClipboard`). On an in-app
|
|
2180
|
+
* paste, a source cell is written to a target only when the two columns' `copyType` match — so e.g. a
|
|
2181
|
+
* "part-name" value never lands in a "measurement" column. Falls back to a number-vs-text bucket derived
|
|
2182
|
+
* from `type` when omitted. Examples: "text", "number", "part-name", "master:suppliers". */
|
|
2183
|
+
copyType?: string;
|
|
2160
2184
|
/** Fully custom cell renderer (badges, avatars, etc.). */
|
|
2161
2185
|
renderCell?: (value: any, row: T) => React.ReactNode;
|
|
2162
2186
|
/** Diff-mode (#239) per-column equality override — return true when the two values are equal.
|
package/dist/index.d.ts
CHANGED
|
@@ -1938,7 +1938,10 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
|
|
|
1938
1938
|
onStateChange?: (state: DatatableState) => void;
|
|
1939
1939
|
/** Show a "Go to" page jumper in the footer when there are more than 5 pages. @default true */
|
|
1940
1940
|
showPageJumper?: boolean;
|
|
1941
|
-
/** Click-to-select mode: "row" highlights the clicked row, "cell"
|
|
1941
|
+
/** Click-to-select mode: "row" highlights the clicked row, "cell" enables spreadsheet-like cell selection.
|
|
1942
|
+
* In "cell" mode the grid supports keyboard navigation and **rectangular range selection** (#317):
|
|
1943
|
+
* Shift+Click / Shift+Arrow extend a range from an anchor; selected cells expose `aria-selected` and the
|
|
1944
|
+
* grid is `aria-multiselectable` with `aria-activedescendant` on the active cell. @default "none" */
|
|
1942
1945
|
selectionMode?: "none" | "row" | "cell";
|
|
1943
1946
|
/** Fired when a row is clicked in "row" selection mode: (row, key). */
|
|
1944
1947
|
onRowClick?: (row: T, key: string | number) => void;
|
|
@@ -1946,6 +1949,22 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
|
|
|
1946
1949
|
onCellClick?: (value: any, row: T, field: string) => void;
|
|
1947
1950
|
/** Fired when the active cell changes: ({ key, field } | null). */
|
|
1948
1951
|
onActiveCellChange?: (cell: { key: string | number; field: string } | null) => void;
|
|
1952
|
+
/** #317: fired when the rectangular cell selection changes ("cell" mode) — every cell inside the
|
|
1953
|
+
* anchor→focus rectangle, in row-major order. Empty array when the selection clears. */
|
|
1954
|
+
onCellSelectionChange?: (cells: Array<{ key: string | number; field: string }>) => void;
|
|
1955
|
+
/** #318: enable spreadsheet clipboard on cells ("cell" mode): Ctrl/Cmd+C copies the active cell/range as
|
|
1956
|
+
* TSV, Ctrl/Cmd+X cuts (copy then clear), Ctrl/Cmd+V pastes onto the target rectangle from the active
|
|
1957
|
+
* cell. Paste is format-restricted by column `copyType` (incompatible cells are skipped) and commits
|
|
1958
|
+
* through `onRowUpdate`/`onRowsChange` in one batched change; outcomes are announced via `aria-live`.
|
|
1959
|
+
* Copy/cut/paste also show a brief **visible** in-grid confirmation toast + a flash on the affected cells
|
|
1960
|
+
* (#320), so sighted users get feedback too. @default false */
|
|
1961
|
+
enableClipboard?: boolean;
|
|
1962
|
+
/** #320: fired on a clipboard copy/cut with the copied cells (row-major `{key,field}[]`) and `{ cut }`.
|
|
1963
|
+
* Use it to surface your own toast / integrate an app notification system. */
|
|
1964
|
+
onCellsCopy?: (cells: Array<{ key: string | number; field: string }>, meta: { cut: boolean }) => void;
|
|
1965
|
+
/** #320: fired after a clipboard paste with the outcome — `written` cells committed, `skipped` cells
|
|
1966
|
+
* rejected (read-only or incompatible `copyType`). */
|
|
1967
|
+
onCellsPaste?: (result: { written: number; skipped: number }) => void;
|
|
1949
1968
|
/** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
|
|
1950
1969
|
* toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
|
|
1951
1970
|
* changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
|
|
@@ -2157,6 +2176,11 @@ interface DatatableColumn<T = any> {
|
|
|
2157
2176
|
loadValueOptions?: (query: string) => Promise<Array<string | { value: string; label: string }>>;
|
|
2158
2177
|
/** Format the raw value to a string/number for display. */
|
|
2159
2178
|
valueFormatter?: (value: any, row: T) => React.ReactNode;
|
|
2179
|
+
/** #318: opaque copy/paste compatibility token for the cell clipboard (`enableClipboard`). On an in-app
|
|
2180
|
+
* paste, a source cell is written to a target only when the two columns' `copyType` match — so e.g. a
|
|
2181
|
+
* "part-name" value never lands in a "measurement" column. Falls back to a number-vs-text bucket derived
|
|
2182
|
+
* from `type` when omitted. Examples: "text", "number", "part-name", "master:suppliers". */
|
|
2183
|
+
copyType?: string;
|
|
2160
2184
|
/** Fully custom cell renderer (badges, avatars, etc.). */
|
|
2161
2185
|
renderCell?: (value: any, row: T) => React.ReactNode;
|
|
2162
2186
|
/** Diff-mode (#239) per-column equality override — return true when the two values are equal.
|
package/dist/index.mjs
CHANGED
|
@@ -8138,7 +8138,14 @@ var DT_CSS = `
|
|
|
8138
8138
|
/* Stay inside the parent even in a flex/grid container: min-width:0 lets a wide
|
|
8139
8139
|
grid shrink (default min-width:auto would force it to its content width and
|
|
8140
8140
|
overflow), so the internal __scroll handles horizontal scrolling. */
|
|
8141
|
-
width: 100%; min-width: 0; max-width: 100%; }
|
|
8141
|
+
width: 100%; min-width: 0; max-width: 100%; position: relative; }
|
|
8142
|
+
/* #320: visible clipboard feedback \u2014 a transient in-grid toast + a brief flash on the copied/pasted cells,
|
|
8143
|
+
alongside the SR-only aria-live message (position: relative on the root anchors the toast; it does NOT
|
|
8144
|
+
create a containing block for the position: fixed popovers, so those stay viewport-anchored). */
|
|
8145
|
+
.twc-dt__clip-toast { position: absolute; inset-block-end: 12px; inset-inline-end: 12px; z-index: 6; background: var(--color-text); color: var(--color-surface); font-size: var(--text-xs); font-weight: var(--font-semibold); padding: 6px 12px; border-radius: var(--radius-md); box-shadow: var(--shadow-md); pointer-events: none; }
|
|
8146
|
+
@keyframes twc-dt-copyflash { from { box-shadow: inset 0 0 0 2px var(--color-primary); } to { box-shadow: inset 0 0 0 2px transparent; } }
|
|
8147
|
+
.twc-dt__td[data-copied="true"] { animation: twc-dt-copyflash 1.3s var(--ease-standard); }
|
|
8148
|
+
@media (prefers-reduced-motion: reduce) { .twc-dt__td[data-copied="true"] { animation: none; } }
|
|
8142
8149
|
|
|
8143
8150
|
/* Toolbar */
|
|
8144
8151
|
.twc-dt__toolbar { position: relative; z-index: 10; display: flex; align-items: center; gap: 6px; padding: 8px 10px;
|
|
@@ -8440,6 +8447,8 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
|
|
|
8440
8447
|
.twc-dt__row[data-selectable="true"] { cursor: pointer; }
|
|
8441
8448
|
.twc-dt__row[data-active="true"] .twc-dt__td { background: var(--color-primary-subtle); box-shadow: inset 0 0 0 9999px transparent; }
|
|
8442
8449
|
.twc-dt__row[data-active="true"] .twc-dt__td[data-pin] { background: var(--color-primary-subtle); }
|
|
8450
|
+
/* #317: data-cell-selected fills the range rectangle; data-cell-active keeps the ring on the moving corner. */
|
|
8451
|
+
.twc-dt__td[data-cell-selected="true"] { background: var(--color-primary-subtle); }
|
|
8443
8452
|
.twc-dt__td[data-cell-active="true"] { box-shadow: inset 0 0 0 2px var(--color-primary); background: var(--color-primary-subtle); }
|
|
8444
8453
|
.twc-dt__row:last-child .twc-dt__td { border-bottom: none; }
|
|
8445
8454
|
|
|
@@ -8594,6 +8603,9 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
|
|
|
8594
8603
|
.twc-dt__pop-grip:hover, .twc-dt__pop-grip:focus-visible, .twc-dt__pop-grip[data-active="true"] { opacity: 1; }
|
|
8595
8604
|
.twc-dt__pop-grip:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
|
|
8596
8605
|
[dir="rtl"] .twc-dt__pop-grip { cursor: nesw-resize; }
|
|
8606
|
+
/* #316: a second grip in the bottom-inline-start corner grows a right-anchored panel toward the empty side (right edge pinned). */
|
|
8607
|
+
.twc-dt__pop-grip[data-side="start"] { inset-inline-start: 2px; inset-inline-end: auto; cursor: nesw-resize; background: linear-gradient(225deg, transparent 0 55%, var(--color-border-strong) 55% 64%, transparent 64% 73%, var(--color-border-strong) 73% 82%, transparent 82%); }
|
|
8608
|
+
[dir="rtl"] .twc-dt__pop-grip[data-side="start"] { cursor: nwse-resize; }
|
|
8597
8609
|
@media (pointer: coarse) {
|
|
8598
8610
|
.twc-dt__f-rz { width: 16px; inset-inline-end: -6px; opacity: 0.4; }
|
|
8599
8611
|
.twc-dt__pop-grip { width: 20px; height: 20px; opacity: 0.6; }
|
|
@@ -9148,6 +9160,10 @@ function Datatable({
|
|
|
9148
9160
|
onRowClick,
|
|
9149
9161
|
onCellClick,
|
|
9150
9162
|
onActiveCellChange,
|
|
9163
|
+
onCellSelectionChange,
|
|
9164
|
+
enableClipboard = false,
|
|
9165
|
+
onCellsCopy,
|
|
9166
|
+
onCellsPaste,
|
|
9151
9167
|
showAggregation = false,
|
|
9152
9168
|
ariaLabel = "Data table",
|
|
9153
9169
|
"aria-label": ariaLabelAttr,
|
|
@@ -9278,6 +9294,8 @@ function Datatable({
|
|
|
9278
9294
|
const [selected, setSelected] = React51__default.useState(() => /* @__PURE__ */ new Set());
|
|
9279
9295
|
const [activeRow, setActiveRow] = React51__default.useState(null);
|
|
9280
9296
|
const [activeCell, setActiveCell] = React51__default.useState(null);
|
|
9297
|
+
const [anchorCell, setAnchorCell] = React51__default.useState(null);
|
|
9298
|
+
const gridId = React51__default.useId();
|
|
9281
9299
|
const [internalPage, setInternalPage] = React51__default.useState(0);
|
|
9282
9300
|
const [internalRpp, setInternalRpp] = React51__default.useState(pageSize > 0 ? pageSize : 10);
|
|
9283
9301
|
const pageControlled = page !== void 0;
|
|
@@ -9377,6 +9395,7 @@ function Datatable({
|
|
|
9377
9395
|
for (const [id, sz] of Object.entries(s.popoverSizes)) {
|
|
9378
9396
|
if (!sz || typeof sz !== "object" || typeof sz.w !== "number" || typeof sz.h !== "number") continue;
|
|
9379
9397
|
next[id] = { w: clampNum(sz.w, popMinW(id), fMaxW()), h: clampNum(sz.h, 120, fMaxH()) };
|
|
9398
|
+
if (typeof sz.left === "number") next[id].left = clampNum(sz.left, 8, fMaxW());
|
|
9380
9399
|
}
|
|
9381
9400
|
if (Object.keys(next).length) setPopSizes(next);
|
|
9382
9401
|
}
|
|
@@ -9828,6 +9847,38 @@ function Datatable({
|
|
|
9828
9847
|
middleRows.forEach((r2, i) => m.set(keyOf(r2, keyIndex.get(keyOf(r2)) ?? i), i));
|
|
9829
9848
|
return m;
|
|
9830
9849
|
}, [middleRows, keyIndex]);
|
|
9850
|
+
const cellColIndex = React51__default.useCallback((field) => ordered.findIndex((c) => c.field === field), [ordered]);
|
|
9851
|
+
const cellRect = React51__default.useMemo(() => {
|
|
9852
|
+
if (selectionMode !== "cell" || !activeCell) return null;
|
|
9853
|
+
const ar = keyIndex.get(activeCell.key), ac = cellColIndex(activeCell.field);
|
|
9854
|
+
if (ar == null || ac < 0) return null;
|
|
9855
|
+
const anchor = anchorCell || activeCell;
|
|
9856
|
+
const nr = keyIndex.get(anchor.key), nc = cellColIndex(anchor.field);
|
|
9857
|
+
const r0 = nr == null ? ar : Math.min(ar, nr), r1 = nr == null ? ar : Math.max(ar, nr);
|
|
9858
|
+
const c0 = nc < 0 ? ac : Math.min(ac, nc), c1 = nc < 0 ? ac : Math.max(ac, nc);
|
|
9859
|
+
return { r0, r1, c0, c1 };
|
|
9860
|
+
}, [selectionMode, activeCell, anchorCell, keyIndex, cellColIndex]);
|
|
9861
|
+
const activeCellId = React51__default.useMemo(() => {
|
|
9862
|
+
if (selectionMode !== "cell" || !activeCell) return void 0;
|
|
9863
|
+
const ar = keyIndex.get(activeCell.key), ac = cellColIndex(activeCell.field);
|
|
9864
|
+
return ar == null || ac < 0 ? void 0 : `${gridId}-${ar}-${ac}`;
|
|
9865
|
+
}, [selectionMode, activeCell, keyIndex, cellColIndex, gridId]);
|
|
9866
|
+
const onCellSelRef = React51__default.useRef(onCellSelectionChange);
|
|
9867
|
+
onCellSelRef.current = onCellSelectionChange;
|
|
9868
|
+
const prevSelSigRef = React51__default.useRef("");
|
|
9869
|
+
React51__default.useEffect(() => {
|
|
9870
|
+
const cb = onCellSelRef.current;
|
|
9871
|
+
if (selectionMode !== "cell" || !cb) return;
|
|
9872
|
+
const cells = [];
|
|
9873
|
+
if (cellRect) for (let r2 = cellRect.r0; r2 <= cellRect.r1; r2++) for (let c = cellRect.c0; c <= cellRect.c1; c++) {
|
|
9874
|
+
const row = leafRows[r2], col = ordered[c];
|
|
9875
|
+
if (row && col) cells.push({ key: keyOf(row, r2), field: col.field });
|
|
9876
|
+
}
|
|
9877
|
+
const sig = cells.map((x) => `${x.key}|${x.field}`).join(",");
|
|
9878
|
+
if (sig === prevSelSigRef.current) return;
|
|
9879
|
+
prevSelSigRef.current = sig;
|
|
9880
|
+
cb(cells);
|
|
9881
|
+
}, [cellRect, selectionMode, leafRows, ordered]);
|
|
9831
9882
|
const canReorderRows = rowReorder && !activeGroupBy.length && !sort;
|
|
9832
9883
|
function onRowDrop(targetKey) {
|
|
9833
9884
|
const fromKey = rowDrag.from;
|
|
@@ -10240,6 +10291,7 @@ function Datatable({
|
|
|
10240
10291
|
const next = { key: k, field: col.field };
|
|
10241
10292
|
setActiveCell(next);
|
|
10242
10293
|
setActiveRow(k);
|
|
10294
|
+
if (!(e.shiftKey && anchorCell)) setAnchorCell(next);
|
|
10243
10295
|
onCellClick?.(row[col.field], row, col.field);
|
|
10244
10296
|
onActiveCellChange?.(next);
|
|
10245
10297
|
}
|
|
@@ -10251,6 +10303,24 @@ function Datatable({
|
|
|
10251
10303
|
if (editing) return;
|
|
10252
10304
|
const td = e.target.closest(".twc-dt__td[data-r]");
|
|
10253
10305
|
if (!td) return;
|
|
10306
|
+
if (enableClipboard && selectionMode === "cell" && (e.ctrlKey || e.metaKey)) {
|
|
10307
|
+
const k = e.key.toLowerCase();
|
|
10308
|
+
if (k === "c") {
|
|
10309
|
+
e.preventDefault();
|
|
10310
|
+
copySelection(false);
|
|
10311
|
+
return;
|
|
10312
|
+
}
|
|
10313
|
+
if (k === "x") {
|
|
10314
|
+
e.preventDefault();
|
|
10315
|
+
copySelection(true);
|
|
10316
|
+
return;
|
|
10317
|
+
}
|
|
10318
|
+
if (k === "v") {
|
|
10319
|
+
e.preventDefault();
|
|
10320
|
+
pasteSelection();
|
|
10321
|
+
return;
|
|
10322
|
+
}
|
|
10323
|
+
}
|
|
10254
10324
|
let r2 = +td.getAttribute("data-r"), c = +td.getAttribute("data-c");
|
|
10255
10325
|
const maxR = leafRows.length - 1, maxC = ordered.length - 1;
|
|
10256
10326
|
let handled = true;
|
|
@@ -10303,6 +10373,16 @@ function Datatable({
|
|
|
10303
10373
|
e.preventDefault();
|
|
10304
10374
|
setFocus({ r: r2, c });
|
|
10305
10375
|
focusCell(r2, c);
|
|
10376
|
+
if (selectionMode === "cell") {
|
|
10377
|
+
const nk = keyOf(leafRows[r2], r2), col = ordered[c];
|
|
10378
|
+
if (col) {
|
|
10379
|
+
const nc = { key: nk, field: col.field };
|
|
10380
|
+
setActiveCell(nc);
|
|
10381
|
+
setActiveRow(nk);
|
|
10382
|
+
if (!e.shiftKey) setAnchorCell(nc);
|
|
10383
|
+
onActiveCellChange?.(nc);
|
|
10384
|
+
}
|
|
10385
|
+
}
|
|
10306
10386
|
}
|
|
10307
10387
|
}
|
|
10308
10388
|
React51__default.useEffect(() => {
|
|
@@ -10419,6 +10499,161 @@ function Datatable({
|
|
|
10419
10499
|
setBatchEdit(null);
|
|
10420
10500
|
closeBatchEdit();
|
|
10421
10501
|
}
|
|
10502
|
+
const [clipboardMsg, setClipboardMsg] = React51__default.useState("");
|
|
10503
|
+
const clipboardRef = React51__default.useRef(null);
|
|
10504
|
+
const [clipFx, setClipFx] = React51__default.useState(null);
|
|
10505
|
+
const clipFxTimer = React51__default.useRef(null);
|
|
10506
|
+
const onCellsCopyRef = React51__default.useRef(onCellsCopy);
|
|
10507
|
+
onCellsCopyRef.current = onCellsCopy;
|
|
10508
|
+
const onCellsPasteRef = React51__default.useRef(onCellsPaste);
|
|
10509
|
+
onCellsPasteRef.current = onCellsPaste;
|
|
10510
|
+
React51__default.useEffect(() => () => {
|
|
10511
|
+
if (clipFxTimer.current) clearTimeout(clipFxTimer.current);
|
|
10512
|
+
}, []);
|
|
10513
|
+
const announceClip = (msg, rect) => {
|
|
10514
|
+
setClipboardMsg(msg);
|
|
10515
|
+
setClipFx({ msg, rect: rect || null });
|
|
10516
|
+
if (clipFxTimer.current) clearTimeout(clipFxTimer.current);
|
|
10517
|
+
clipFxTimer.current = setTimeout(() => setClipFx(null), 1600);
|
|
10518
|
+
};
|
|
10519
|
+
const copyTypeOf = (col) => col && col.copyType != null ? col.copyType : col && col.type === "number" ? "number" : "text";
|
|
10520
|
+
const writeClipboard = (text) => {
|
|
10521
|
+
const execFallback = () => {
|
|
10522
|
+
try {
|
|
10523
|
+
if (typeof document !== "undefined") {
|
|
10524
|
+
const ta = document.createElement("textarea");
|
|
10525
|
+
ta.value = text;
|
|
10526
|
+
ta.style.position = "fixed";
|
|
10527
|
+
ta.style.opacity = "0";
|
|
10528
|
+
document.body.appendChild(ta);
|
|
10529
|
+
ta.focus();
|
|
10530
|
+
ta.select();
|
|
10531
|
+
document.execCommand("copy");
|
|
10532
|
+
document.body.removeChild(ta);
|
|
10533
|
+
}
|
|
10534
|
+
} catch {
|
|
10535
|
+
}
|
|
10536
|
+
};
|
|
10537
|
+
try {
|
|
10538
|
+
if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text).catch(execFallback);
|
|
10539
|
+
} catch {
|
|
10540
|
+
}
|
|
10541
|
+
execFallback();
|
|
10542
|
+
return Promise.resolve();
|
|
10543
|
+
};
|
|
10544
|
+
const writeCellPatches = (patchByKey) => {
|
|
10545
|
+
if (!patchByKey.size) return 0;
|
|
10546
|
+
const changed = [];
|
|
10547
|
+
const nextAll = rows.map((r2, i) => {
|
|
10548
|
+
const k = keyOf(r2, i);
|
|
10549
|
+
const patch = patchByKey.get(k);
|
|
10550
|
+
if (!patch) return r2;
|
|
10551
|
+
const updated = { ...r2, ...patch };
|
|
10552
|
+
changed.push([updated, r2, patch]);
|
|
10553
|
+
return updated;
|
|
10554
|
+
});
|
|
10555
|
+
changed.forEach(([updated, orig, patch]) => {
|
|
10556
|
+
for (const f of Object.keys(patch)) onRowUpdate?.(updated, orig, f);
|
|
10557
|
+
});
|
|
10558
|
+
if (onRowsChange && changed.length) onRowsChange(nextAll);
|
|
10559
|
+
return changed.length;
|
|
10560
|
+
};
|
|
10561
|
+
const rectCells = () => {
|
|
10562
|
+
if (!cellRect) return null;
|
|
10563
|
+
const out = [];
|
|
10564
|
+
for (let r2 = cellRect.r0; r2 <= cellRect.r1; r2++) {
|
|
10565
|
+
const row = leafRows[r2];
|
|
10566
|
+
if (!row) continue;
|
|
10567
|
+
const line = [];
|
|
10568
|
+
for (let c = cellRect.c0; c <= cellRect.c1; c++) {
|
|
10569
|
+
const col = ordered[c];
|
|
10570
|
+
if (col) line.push({ key: keyOf(row, r2), field: col.field, col, value: getColVal(col, row) });
|
|
10571
|
+
}
|
|
10572
|
+
out.push(line);
|
|
10573
|
+
}
|
|
10574
|
+
return out.length ? out : null;
|
|
10575
|
+
};
|
|
10576
|
+
const copySelection = (cut) => {
|
|
10577
|
+
const grid = rectCells();
|
|
10578
|
+
if (!grid) return;
|
|
10579
|
+
const tsv = grid.map((line) => line.map((cell) => cell.value == null ? "" : String(cell.value)).join(" ")).join("\n");
|
|
10580
|
+
clipboardRef.current = { tsv, colTypes: grid[0].map((cell) => copyTypeOf(cell.col)), matrix: grid.map((line) => line.map((cell) => cell.value)) };
|
|
10581
|
+
writeClipboard(tsv);
|
|
10582
|
+
const n = grid.reduce((a, line) => a + line.length, 0);
|
|
10583
|
+
onCellsCopyRef.current?.(grid.flatMap((line) => line.map((cell) => ({ key: cell.key, field: cell.field }))), { cut: !!cut });
|
|
10584
|
+
if (cut) {
|
|
10585
|
+
const patchByKey = /* @__PURE__ */ new Map();
|
|
10586
|
+
let clearable = 0;
|
|
10587
|
+
for (const line of grid) for (const cell of line) {
|
|
10588
|
+
if (!isColEditable(cell.col)) continue;
|
|
10589
|
+
clearable++;
|
|
10590
|
+
const p = patchByKey.get(cell.key) || {};
|
|
10591
|
+
p[cell.field] = cell.col.type === "number" ? null : "";
|
|
10592
|
+
patchByKey.set(cell.key, p);
|
|
10593
|
+
}
|
|
10594
|
+
writeCellPatches(patchByKey);
|
|
10595
|
+
announceClip(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`, cellRect);
|
|
10596
|
+
} else {
|
|
10597
|
+
announceClip(`Copied ${n} cell${n === 1 ? "" : "s"}`, cellRect);
|
|
10598
|
+
}
|
|
10599
|
+
};
|
|
10600
|
+
const pasteSelection = async () => {
|
|
10601
|
+
if (!cellRect) return;
|
|
10602
|
+
let text = null;
|
|
10603
|
+
try {
|
|
10604
|
+
if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.readText) text = await navigator.clipboard.readText();
|
|
10605
|
+
} catch {
|
|
10606
|
+
setClipboardMsg("Paste unavailable \u2014 clipboard permission denied");
|
|
10607
|
+
return;
|
|
10608
|
+
}
|
|
10609
|
+
if (text == null) {
|
|
10610
|
+
setClipboardMsg("Nothing to paste");
|
|
10611
|
+
return;
|
|
10612
|
+
}
|
|
10613
|
+
const inApp = clipboardRef.current && clipboardRef.current.tsv === text ? clipboardRef.current : null;
|
|
10614
|
+
let matrix;
|
|
10615
|
+
if (inApp) matrix = inApp.matrix;
|
|
10616
|
+
else {
|
|
10617
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
10618
|
+
if (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
10619
|
+
matrix = lines.map((line) => line.split(" "));
|
|
10620
|
+
}
|
|
10621
|
+
const patchByKey = /* @__PURE__ */ new Map();
|
|
10622
|
+
let written = 0, skipped = 0;
|
|
10623
|
+
for (let i = 0; i < matrix.length; i++) {
|
|
10624
|
+
const rr = cellRect.r0 + i;
|
|
10625
|
+
const row = leafRows[rr];
|
|
10626
|
+
if (!row) break;
|
|
10627
|
+
for (let j = 0; j < matrix[i].length; j++) {
|
|
10628
|
+
const cc = cellRect.c0 + j;
|
|
10629
|
+
const col = ordered[cc];
|
|
10630
|
+
if (!col) break;
|
|
10631
|
+
if (!isColEditable(col)) {
|
|
10632
|
+
skipped++;
|
|
10633
|
+
continue;
|
|
10634
|
+
}
|
|
10635
|
+
if (inApp && inApp.colTypes[j] != null && inApp.colTypes[j] !== copyTypeOf(col)) {
|
|
10636
|
+
skipped++;
|
|
10637
|
+
continue;
|
|
10638
|
+
}
|
|
10639
|
+
let v = matrix[i][j];
|
|
10640
|
+
if (col.type === "number") {
|
|
10641
|
+
v = v === "" ? null : Number(v);
|
|
10642
|
+
if (Number.isNaN(v)) v = null;
|
|
10643
|
+
}
|
|
10644
|
+
const k = keyOf(row, rr);
|
|
10645
|
+
const p = patchByKey.get(k) || {};
|
|
10646
|
+
p[col.field] = v;
|
|
10647
|
+
patchByKey.set(k, p);
|
|
10648
|
+
written++;
|
|
10649
|
+
}
|
|
10650
|
+
}
|
|
10651
|
+
writeCellPatches(patchByKey);
|
|
10652
|
+
onCellsPasteRef.current?.({ written, skipped });
|
|
10653
|
+
const maxCols = matrix.reduce((a, m) => Math.max(a, m.length), 0);
|
|
10654
|
+
const pasteRect = written ? { r0: cellRect.r0, c0: cellRect.c0, r1: Math.min(cellRect.r0 + matrix.length - 1, leafRows.length - 1), c1: Math.min(cellRect.c0 + maxCols - 1, ordered.length - 1) } : null;
|
|
10655
|
+
announceClip(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`, pasteRect);
|
|
10656
|
+
};
|
|
10422
10657
|
React51__default.useEffect(() => {
|
|
10423
10658
|
if (!editing) return;
|
|
10424
10659
|
const onDown = (e) => {
|
|
@@ -10675,6 +10910,7 @@ function Datatable({
|
|
|
10675
10910
|
if (node) {
|
|
10676
10911
|
node.style.removeProperty("--twc-dt-pop-w");
|
|
10677
10912
|
node.style.removeProperty("--twc-dt-pop-h");
|
|
10913
|
+
node.style.removeProperty("left");
|
|
10678
10914
|
node.removeAttribute("data-pop-sized");
|
|
10679
10915
|
}
|
|
10680
10916
|
setPopSizes((s) => {
|
|
@@ -10684,7 +10920,7 @@ function Datatable({
|
|
|
10684
10920
|
return n;
|
|
10685
10921
|
});
|
|
10686
10922
|
};
|
|
10687
|
-
const startPopResize = (id) => (e) => {
|
|
10923
|
+
const startPopResize = (id, side) => (e) => {
|
|
10688
10924
|
if (e.button != null && e.button !== 0) return;
|
|
10689
10925
|
e.preventDefault();
|
|
10690
10926
|
e.stopPropagation();
|
|
@@ -10693,21 +10929,27 @@ function Datatable({
|
|
|
10693
10929
|
const grip = e.currentTarget;
|
|
10694
10930
|
const startX = e.clientX, startY = e.clientY;
|
|
10695
10931
|
const r2 = pop.getBoundingClientRect();
|
|
10696
|
-
const startW = r2.width, startH = r2.height;
|
|
10932
|
+
const startW = r2.width, startH = r2.height, startLeft = r2.left, startRight = r2.right;
|
|
10697
10933
|
const dir = typeof getComputedStyle !== "undefined" && getComputedStyle(pop).direction === "rtl" ? -1 : 1;
|
|
10934
|
+
const toStart = side === "start";
|
|
10698
10935
|
pop.style.setProperty("--twc-dt-pop-w", `${startW}px`);
|
|
10699
10936
|
pop.style.setProperty("--twc-dt-pop-h", `${startH}px`);
|
|
10700
10937
|
grip.setAttribute("data-active", "true");
|
|
10701
10938
|
pop.setAttribute("data-pop-sized", "");
|
|
10702
|
-
let lw = startW, lh = startH;
|
|
10939
|
+
let lw = startW, lh = startH, ll = startLeft;
|
|
10703
10940
|
const onMove = (ev) => {
|
|
10704
|
-
|
|
10941
|
+
const dw = toStart ? (startX - ev.clientX) * dir : (ev.clientX - startX) * dir;
|
|
10942
|
+
lw = clampNum(startW + dw, popMinW(id), fMaxW());
|
|
10705
10943
|
lh = clampNum(startH + (ev.clientY - startY), 120, fMaxH());
|
|
10706
10944
|
pop.style.setProperty("--twc-dt-pop-w", `${lw}px`);
|
|
10707
10945
|
pop.style.setProperty("--twc-dt-pop-h", `${lh}px`);
|
|
10946
|
+
if (toStart) {
|
|
10947
|
+
ll = Math.max(8, startRight - lw);
|
|
10948
|
+
pop.style.left = `${ll}px`;
|
|
10949
|
+
}
|
|
10708
10950
|
};
|
|
10709
10951
|
const onUp = () => {
|
|
10710
|
-
setPopSizes((s) => ({ ...s, [id]: { w: lw, h: lh } }));
|
|
10952
|
+
setPopSizes((s) => ({ ...s, [id]: { ...s[id] || {}, w: lw, h: lh, ...toStart ? { left: ll } : {} } }));
|
|
10711
10953
|
grip.removeAttribute("data-active");
|
|
10712
10954
|
window.removeEventListener("pointermove", onMove);
|
|
10713
10955
|
window.removeEventListener("pointerup", onUp);
|
|
@@ -10715,7 +10957,7 @@ function Datatable({
|
|
|
10715
10957
|
window.addEventListener("pointermove", onMove);
|
|
10716
10958
|
window.addEventListener("pointerup", onUp);
|
|
10717
10959
|
};
|
|
10718
|
-
const onPopGripKey = (id) => (e) => {
|
|
10960
|
+
const onPopGripKey = (id, side) => (e) => {
|
|
10719
10961
|
if (e.key === "Enter" || e.key === " " || e.key === "Backspace") {
|
|
10720
10962
|
e.preventDefault();
|
|
10721
10963
|
clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
|
|
@@ -10740,25 +10982,54 @@ function Datatable({
|
|
|
10740
10982
|
w = fMaxW();
|
|
10741
10983
|
h = fMaxH();
|
|
10742
10984
|
}
|
|
10743
|
-
|
|
10985
|
+
w = clampNum(w, popMinW(id), fMaxW());
|
|
10986
|
+
h = clampNum(h, 120, fMaxH());
|
|
10987
|
+
const left = side === "start" ? Math.max(8, r2.right - w) : void 0;
|
|
10988
|
+
setPopSizes((s) => ({ ...s, [id]: { ...s[id] || {}, w, h, ...left != null ? { left } : {} } }));
|
|
10744
10989
|
};
|
|
10745
|
-
const popStyle = (id) =>
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
{
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10990
|
+
const popStyle = (id) => {
|
|
10991
|
+
const s = popSizes[id];
|
|
10992
|
+
if (!s) return null;
|
|
10993
|
+
const st = { "--twc-dt-pop-w": `${s.w}px`, "--twc-dt-pop-h": `${s.h}px` };
|
|
10994
|
+
if (s.left != null) st.left = `${s.left}px`;
|
|
10995
|
+
return st;
|
|
10996
|
+
};
|
|
10997
|
+
const popGrip = (id) => resizablePopovers ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
10998
|
+
/* @__PURE__ */ jsx(
|
|
10999
|
+
"span",
|
|
11000
|
+
{
|
|
11001
|
+
className: "twc-dt__pop-grip",
|
|
11002
|
+
"data-side": "end",
|
|
11003
|
+
role: "slider",
|
|
11004
|
+
tabIndex: 0,
|
|
11005
|
+
"aria-label": "Resize panel (Enter or double-click to reset)",
|
|
11006
|
+
"aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
|
|
11007
|
+
onPointerDown: startPopResize(id, "end"),
|
|
11008
|
+
onKeyDown: onPopGripKey(id, "end"),
|
|
11009
|
+
onDoubleClick: (e) => {
|
|
11010
|
+
e.preventDefault();
|
|
11011
|
+
clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
|
|
11012
|
+
}
|
|
10759
11013
|
}
|
|
10760
|
-
|
|
10761
|
-
|
|
11014
|
+
),
|
|
11015
|
+
/* @__PURE__ */ jsx(
|
|
11016
|
+
"span",
|
|
11017
|
+
{
|
|
11018
|
+
className: "twc-dt__pop-grip",
|
|
11019
|
+
"data-side": "start",
|
|
11020
|
+
role: "slider",
|
|
11021
|
+
tabIndex: 0,
|
|
11022
|
+
"aria-label": "Resize panel leftward (Enter or double-click to reset)",
|
|
11023
|
+
"aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
|
|
11024
|
+
onPointerDown: startPopResize(id, "start"),
|
|
11025
|
+
onKeyDown: onPopGripKey(id, "start"),
|
|
11026
|
+
onDoubleClick: (e) => {
|
|
11027
|
+
e.preventDefault();
|
|
11028
|
+
clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
|
|
11029
|
+
}
|
|
11030
|
+
}
|
|
11031
|
+
)
|
|
11032
|
+
] }) : null;
|
|
10762
11033
|
const fieldHandle = (field, rowIdx) => resizableFilters && rowIdx === 0 ? /* @__PURE__ */ jsx(
|
|
10763
11034
|
"span",
|
|
10764
11035
|
{
|
|
@@ -10914,22 +11185,29 @@ function Datatable({
|
|
|
10914
11185
|
const editable = isColEditable(c);
|
|
10915
11186
|
const isEditing = editing && editing.key === k && editing.field === c.field;
|
|
10916
11187
|
const cellActive = selectionMode === "cell" && activeCell && activeCell.key === k && activeCell.field === c.field;
|
|
11188
|
+
const cellSelected = selectionMode === "cell" && cellRect && ri >= cellRect.r0 && ri <= cellRect.r1 && ci >= cellRect.c0 && ci <= cellRect.c1;
|
|
11189
|
+
const cellFlash = clipFx && clipFx.rect && ri >= clipFx.rect.r0 && ri <= clipFx.rect.r1 && ci >= clipFx.rect.c0 && ci <= clipFx.rect.c1;
|
|
11190
|
+
const cellId = selectionMode === "cell" ? `${gridId}-${ri}-${ci}` : void 0;
|
|
10917
11191
|
const display = c.valueFormatter ? c.valueFormatter(val, row) : val;
|
|
10918
11192
|
const cellTitle = !isActions && !c.renderCell && !isEditing && (typeof display === "string" || typeof display === "number") ? String(display) : void 0;
|
|
10919
11193
|
return /* @__PURE__ */ jsxs(
|
|
10920
11194
|
"td",
|
|
10921
11195
|
{
|
|
11196
|
+
id: cellId,
|
|
10922
11197
|
className: "twc-dt__td",
|
|
10923
11198
|
role: "gridcell",
|
|
10924
11199
|
"data-r": ri,
|
|
10925
11200
|
"data-c": ci,
|
|
10926
11201
|
"aria-colindex": ci + 1 + (checkboxSelection ? 1 : 0),
|
|
10927
11202
|
tabIndex: focus.r === ri && focus.c === ci ? 0 : -1,
|
|
11203
|
+
"aria-selected": cellSelected || void 0,
|
|
10928
11204
|
"data-num": c.type === "number" || void 0,
|
|
10929
11205
|
"data-actions": isActions || void 0,
|
|
10930
11206
|
"data-editable": editable && !isEditing || void 0,
|
|
10931
11207
|
"data-editing": isEditing || void 0,
|
|
10932
11208
|
"data-cell-active": cellActive || void 0,
|
|
11209
|
+
"data-cell-selected": cellSelected || void 0,
|
|
11210
|
+
"data-copied": cellFlash || void 0,
|
|
10933
11211
|
"data-wrap": wrapped.has(c.field) || void 0,
|
|
10934
11212
|
"data-pin": st.pin,
|
|
10935
11213
|
"data-pin-edge": st.edge,
|
|
@@ -11291,6 +11569,8 @@ function Datatable({
|
|
|
11291
11569
|
/* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__groupbar-clear", onClick: () => setGroupBy([]), children: "Clear all" })
|
|
11292
11570
|
] }) : null,
|
|
11293
11571
|
/* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: reorderMsg }),
|
|
11572
|
+
/* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: clipboardMsg }),
|
|
11573
|
+
clipFx ? /* @__PURE__ */ jsx("div", { className: "twc-dt__clip-toast", "aria-hidden": "true", children: clipFx.msg }) : null,
|
|
11294
11574
|
pivotActive ? renderPivot() : /* @__PURE__ */ jsx(
|
|
11295
11575
|
"div",
|
|
11296
11576
|
{
|
|
@@ -11312,6 +11592,8 @@ function Datatable({
|
|
|
11312
11592
|
"aria-label": ariaLabelAttr || ariaLabel,
|
|
11313
11593
|
"aria-rowcount": totalRows + 1,
|
|
11314
11594
|
"aria-colcount": ordered.length + (checkboxSelection ? 1 : 0),
|
|
11595
|
+
"aria-multiselectable": selectionMode === "cell" || void 0,
|
|
11596
|
+
"aria-activedescendant": activeCellId,
|
|
11315
11597
|
"aria-busy": loading || void 0,
|
|
11316
11598
|
onKeyDown: onGridKeyDown,
|
|
11317
11599
|
children: [
|