beautiful-grid 1.0.16 → 1.0.17
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/README.md +26 -12
- package/cjs/components/Table.js +68 -23
- package/cjs/editors/createSelectEditorPlugin.js +98 -0
- package/esm/components/Table.js +64 -19
- package/esm/editors/createSelectEditorPlugin.js +74 -0
- package/package.json +1 -1
- package/types/editors/createSelectEditorPlugin.d.ts +37 -1
- package/types/types.d.ts +7 -0
package/README.md
CHANGED
|
@@ -583,6 +583,17 @@ const columns: BGridColumn<Item>[] = [
|
|
|
583
583
|
];
|
|
584
584
|
```
|
|
585
585
|
|
|
586
|
+
`createSelectEditorPlugin` options:
|
|
587
|
+
- `options`: List of `{ value, label, disabled? }` entries.
|
|
588
|
+
- `allowCustomValue?: boolean`: Set `true` to allow pasting arbitrary values not present in `options` (default: `false`, rejecting unlisted values with `parseValueFailed`). Automatically preserves numeric types when option values are numeric.
|
|
589
|
+
- `copyMode?: 'label' | 'value'`: Controls clipboard copy output (`'label'` by default when label is text, or `'value'`). Matching during paste prioritizes the configured `copyMode` to prevent label/value collisions.
|
|
590
|
+
- `allowEmpty?: boolean`: Set `true` to allow clearing cell values when pasting empty text (default: `false`).
|
|
591
|
+
- `emptyValue?: Value | null`: Value written when empty text is pasted with `allowEmpty: true` (default: `''`).
|
|
592
|
+
- `allowDisabledOptions?: boolean`: Set `true` to allow pasting options marked `disabled: true` (default: `false`, rejecting disabled options).
|
|
593
|
+
- `parseClipboardText?`: Override custom clipboard parsing and validation logic.
|
|
594
|
+
- `getClipboardText?`: Override custom clipboard text retrieval logic.
|
|
595
|
+
- `openOnMount?: boolean`: Automatically opens native picker on editor activation (default: `true`).
|
|
596
|
+
|
|
586
597
|
#### Editor Icons (Lookup & Quick Triggers)
|
|
587
598
|
|
|
588
599
|
Add inline trigger icons beside cell values using `editorIcon`:
|
|
@@ -649,16 +660,15 @@ Configure active cell handling and keyboard behavior with `cellNavigationOptions
|
|
|
649
660
|
|
|
650
661
|
---
|
|
651
662
|
|
|
652
|
-
### 5. Cell Selection & Clipboard
|
|
663
|
+
### 5. Cell Range Selection & Clipboard Integration
|
|
653
664
|
|
|
654
665
|
Cell range selection and clipboard copy/paste are enabled by default.
|
|
655
666
|
|
|
656
667
|
```typescript jsx
|
|
657
668
|
<BGrid
|
|
658
|
-
width={800}
|
|
659
|
-
height={400}
|
|
660
|
-
columns={columns}
|
|
661
669
|
data={data}
|
|
670
|
+
columns={columns}
|
|
671
|
+
editable
|
|
662
672
|
rowKey='id'
|
|
663
673
|
cellSelectionOptions={{
|
|
664
674
|
enabled: true,
|
|
@@ -666,20 +676,24 @@ Cell range selection and clipboard copy/paste are enabled by default.
|
|
|
666
676
|
clearOnEscape: true,
|
|
667
677
|
clearOnOutsideClick: true,
|
|
668
678
|
maxClipboardCells: 100000,
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
onPasteError: error => console.warn('Paste failed:', error),
|
|
672
|
-
createRowOnPaste: ({ rowIndex, clipboardRow, columns }) => ({
|
|
679
|
+
createRowOnPaste: ({ rowIndex, clipboardRow }) => ({
|
|
680
|
+
status: BGridDataItemStatus.new,
|
|
673
681
|
values: createNewRowModel(clipboardRow),
|
|
674
682
|
}),
|
|
675
683
|
}}
|
|
676
684
|
/>
|
|
677
685
|
```
|
|
678
686
|
|
|
679
|
-
- `Ctrl+C` / `Cmd+C`: Copies selected cells as Tab-separated (`\t`) values and CRLF (`\r\n`) lines, directly pasteable into Excel or Google Sheets.
|
|
680
|
-
- `Ctrl+V` / `Cmd+V`: Pastes clipboard
|
|
681
|
-
-
|
|
682
|
-
-
|
|
687
|
+
- `Ctrl+C` / `Cmd+C`: Copies selected cells as Tab-separated (`\t`) values and CRLF (`\r\n`) lines, directly pasteable into Excel or Google Sheets. Columns configured with `createSelectEditorPlugin` copy the display `label` by default.
|
|
688
|
+
- `Ctrl+V` / `Cmd+V`: Pastes clipboard values across editable columns:
|
|
689
|
+
- **Single cell fill**: Pasting a 1×1 value into a multi-cell selection fills every cell in the selected range.
|
|
690
|
+
- **Pattern tiling (Repeat)**: Pasting an $n \times m$ matrix into an $N \times M$ selection whose dimensions are exact integer multiples ($N \pmod n = 0$ and $M \pmod m = 0$) tiles/repeats the pattern across the entire selection.
|
|
691
|
+
- **Non-multiple fallback**: When the selection is not an exact multiple, the matrix pastes once starting at the top-left of the selected range (or focused cell if no range).
|
|
692
|
+
- `multiSelectFocusMode: 'last'` makes the last cell selected by a drag the active cell. The default is `'first'` for backward compatibility.
|
|
693
|
+
- **Paste validation & editor protection**:
|
|
694
|
+
- `createSelectEditorPlugin` validates pasted text against option values and string labels, mapping labels back to values with $O(1)$ indexed lookup.
|
|
695
|
+
- By default, unlisted values and disabled options are rejected and reported as `parseValueFailed` through `onPasteError`, preventing data corruption. Set `allowCustomValue: true` on the select editor plugin to allow arbitrary inputs.
|
|
696
|
+
- Customize copied text on a per-column basis with `getClipboardText: ({ value, values }) => string`, or restore/validate custom types with `parseClipboardText: (text, context) => parsedValue`.
|
|
683
697
|
|
|
684
698
|
---
|
|
685
699
|
|
package/cjs/components/Table.js
CHANGED
|
@@ -1226,7 +1226,7 @@ function Table(props) {
|
|
|
1226
1226
|
var rowHasNewLogicalCell = false;
|
|
1227
1227
|
var columnIndexes = Array.from((_f = selectedCells.get(ri)) !== null && _f !== void 0 ? _f : []).sort(function (a, b) { return a - b; });
|
|
1228
1228
|
columnIndexes.forEach(function (ci) {
|
|
1229
|
-
var _a;
|
|
1229
|
+
var _a, _b, _c;
|
|
1230
1230
|
var column = columns[ci];
|
|
1231
1231
|
if (!column)
|
|
1232
1232
|
return;
|
|
@@ -1240,8 +1240,10 @@ function Table(props) {
|
|
|
1240
1240
|
rowHasNewLogicalCell = true;
|
|
1241
1241
|
var logicalItem = (_a = data[logicalCell.cell.rowIndex]) !== null && _a !== void 0 ? _a : item;
|
|
1242
1242
|
var value = (0, utils_1.getCellValueByRowKey)(column.key, logicalItem.values);
|
|
1243
|
-
var
|
|
1244
|
-
|
|
1243
|
+
var pluginClipboardTextGetter = ((_b = column.editor) === null || _b === void 0 ? void 0 : _b.type) === 'plugin' ? column.editor.getClipboardText : undefined;
|
|
1244
|
+
var clipboardTextGetter = (_c = column.getClipboardText) !== null && _c !== void 0 ? _c : pluginClipboardTextGetter;
|
|
1245
|
+
var clipboardValue = clipboardTextGetter
|
|
1246
|
+
? clipboardTextGetter({
|
|
1245
1247
|
column: column,
|
|
1246
1248
|
index: logicalCell.cell.rowIndex,
|
|
1247
1249
|
columnIndex: ci,
|
|
@@ -1328,7 +1330,7 @@ function Table(props) {
|
|
|
1328
1330
|
cellSelectionOptions === null || cellSelectionOptions === void 0 ? void 0 : cellSelectionOptions.onCopyError,
|
|
1329
1331
|
]);
|
|
1330
1332
|
var pasteClipboardText = (0, react_1.useCallback)(function (text, clipboardTypes) {
|
|
1331
|
-
var _a, _b;
|
|
1333
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1332
1334
|
if (clipboardTypes === void 0) { clipboardTypes = []; }
|
|
1333
1335
|
if (!cellSelectionEnabled || !editable || !activeCell || data.length === 0 || columns.length === 0) {
|
|
1334
1336
|
return false;
|
|
@@ -1367,17 +1369,41 @@ function Table(props) {
|
|
|
1367
1369
|
});
|
|
1368
1370
|
return true;
|
|
1369
1371
|
}
|
|
1370
|
-
var
|
|
1371
|
-
var
|
|
1372
|
+
var primaryRange = cellSelectionRanges.length === 1 ? (0, cellSelectionGeometry_1.normalizeCellSelectionRange)(cellSelectionRanges[0]) : null;
|
|
1373
|
+
var selectionRowCount = primaryRange ? primaryRange.endRowIndex - primaryRange.startRowIndex + 1 : 1;
|
|
1374
|
+
var selectionColCount = primaryRange ? primaryRange.endColumnIndex - primaryRange.startColumnIndex + 1 : 1;
|
|
1375
|
+
// Determine fill rows and cols:
|
|
1376
|
+
// 1. Single cell (1x1) copied into multi-cell selection: fill the entire selection.
|
|
1377
|
+
// 2. nxm copied into exact multiple NxM selection: repeat pattern across selection.
|
|
1378
|
+
// 3. Otherwise: paste 1:1 matching matrix dimensions, anchored at selection top-left (or activeCell if no range).
|
|
1379
|
+
var isSingleClipboardCell = matrix.length === 1 && ((_c = matrix[0]) === null || _c === void 0 ? void 0 : _c.length) === 1;
|
|
1380
|
+
var matrixColCount = matrix.reduce(function (max, row) { return Math.max(max, row.length); }, 0);
|
|
1381
|
+
var isExactRowMultiple = primaryRange && matrix.length > 0 && selectionRowCount % matrix.length === 0;
|
|
1382
|
+
var isExactColMultiple = primaryRange &&
|
|
1383
|
+
matrixColCount > 0 &&
|
|
1384
|
+
selectionColCount % matrixColCount === 0;
|
|
1385
|
+
var canTileMatrix = primaryRange &&
|
|
1386
|
+
isExactRowMultiple &&
|
|
1387
|
+
isExactColMultiple &&
|
|
1388
|
+
(selectionRowCount > matrix.length || selectionColCount > matrixColCount);
|
|
1389
|
+
var fillRowCount = matrix.length;
|
|
1390
|
+
var fillColCount = matrixColCount;
|
|
1391
|
+
var startRowIndex = primaryRange ? primaryRange.startRowIndex : activeCell.rowIndex;
|
|
1392
|
+
var startColumnIndex = primaryRange ? primaryRange.startColumnIndex : activeCell.columnIndex;
|
|
1393
|
+
if (primaryRange && (isSingleClipboardCell || canTileMatrix)) {
|
|
1394
|
+
fillRowCount = selectionRowCount;
|
|
1395
|
+
fillColCount = selectionColCount;
|
|
1396
|
+
}
|
|
1372
1397
|
var nextData = __spreadArray([], __read(data), false);
|
|
1373
1398
|
var appendedRows = [];
|
|
1374
1399
|
var changes = [];
|
|
1375
1400
|
var createRowOnPaste = cellSelectionOptions === null || cellSelectionOptions === void 0 ? void 0 : cellSelectionOptions.createRowOnPaste;
|
|
1376
1401
|
if (createRowOnPaste) {
|
|
1377
|
-
var requiredRowCount = startRowIndex +
|
|
1402
|
+
var requiredRowCount = startRowIndex + fillRowCount;
|
|
1378
1403
|
while (nextData.length < requiredRowCount) {
|
|
1379
1404
|
var rowIndex = nextData.length;
|
|
1380
|
-
var
|
|
1405
|
+
var matrixRowOffset = isSingleClipboardCell ? 0 : (rowIndex - startRowIndex) % matrix.length;
|
|
1406
|
+
var clipboardRow = matrix[matrixRowOffset];
|
|
1381
1407
|
if (!clipboardRow)
|
|
1382
1408
|
break;
|
|
1383
1409
|
var createdItem = void 0;
|
|
@@ -1397,18 +1423,36 @@ function Table(props) {
|
|
|
1397
1423
|
}
|
|
1398
1424
|
var logicalResolutionData = __spreadArray([], __read(nextData), false);
|
|
1399
1425
|
var pasteTargets = new Map();
|
|
1400
|
-
|
|
1426
|
+
for (var rowOffset = 0; rowOffset < fillRowCount; rowOffset++) {
|
|
1401
1427
|
var rowIndex = startRowIndex + rowOffset;
|
|
1402
1428
|
var item = logicalResolutionData[rowIndex];
|
|
1403
1429
|
if (!item || item.status === types_1.BGridDataItemStatus.remove)
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1430
|
+
continue;
|
|
1431
|
+
var matrixRow = isSingleClipboardCell ? matrix[0] : matrix[rowOffset % matrix.length];
|
|
1432
|
+
if (!matrixRow || matrixRow.length === 0)
|
|
1433
|
+
continue;
|
|
1434
|
+
for (var colOffset = 0; colOffset < fillColCount; colOffset++) {
|
|
1435
|
+
var columnIndex = startColumnIndex + colOffset;
|
|
1407
1436
|
var column = columns[columnIndex];
|
|
1408
1437
|
// Pasting mutates row data without opening an editor, so only columns
|
|
1409
1438
|
// explicitly opted into editing may receive clipboard values.
|
|
1410
1439
|
if (!column || column.editable !== true)
|
|
1411
|
-
|
|
1440
|
+
continue;
|
|
1441
|
+
var clipboardValue = void 0;
|
|
1442
|
+
if (isSingleClipboardCell) {
|
|
1443
|
+
clipboardValue = (_e = (_d = matrix[0]) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : '';
|
|
1444
|
+
}
|
|
1445
|
+
else if (canTileMatrix) {
|
|
1446
|
+
var cellColOffset = matrixColCount > 0 ? colOffset % matrixColCount : 0;
|
|
1447
|
+
if (cellColOffset >= matrixRow.length)
|
|
1448
|
+
continue;
|
|
1449
|
+
clipboardValue = (_f = matrixRow[cellColOffset]) !== null && _f !== void 0 ? _f : '';
|
|
1450
|
+
}
|
|
1451
|
+
else {
|
|
1452
|
+
if (colOffset >= matrixRow.length)
|
|
1453
|
+
continue;
|
|
1454
|
+
clipboardValue = (_g = matrixRow[colOffset]) !== null && _g !== void 0 ? _g : '';
|
|
1455
|
+
}
|
|
1412
1456
|
var logicalCell = (0, utils_1.resolveLogicalCell)(logicalResolutionData, props.cellMergeOptions, {
|
|
1413
1457
|
rowIndex: rowIndex,
|
|
1414
1458
|
columnIndex: columnIndex,
|
|
@@ -1417,7 +1461,7 @@ function Table(props) {
|
|
|
1417
1461
|
return !logicalResolutionData[targetRowIndex] ||
|
|
1418
1462
|
logicalResolutionData[targetRowIndex].status === types_1.BGridDataItemStatus.remove;
|
|
1419
1463
|
})) {
|
|
1420
|
-
|
|
1464
|
+
continue;
|
|
1421
1465
|
}
|
|
1422
1466
|
var targetKey = "".concat(logicalCell.cell.rowIndex, ":").concat(columnIndex);
|
|
1423
1467
|
var existingTarget = pasteTargets.get(targetKey);
|
|
@@ -1430,7 +1474,7 @@ function Table(props) {
|
|
|
1430
1474
|
columnIndex: columnIndex,
|
|
1431
1475
|
});
|
|
1432
1476
|
}
|
|
1433
|
-
|
|
1477
|
+
continue;
|
|
1434
1478
|
}
|
|
1435
1479
|
pasteTargets.set(targetKey, {
|
|
1436
1480
|
canonicalRowIndex: logicalCell.cell.rowIndex,
|
|
@@ -1439,14 +1483,14 @@ function Table(props) {
|
|
|
1439
1483
|
clipboardValue: clipboardValue,
|
|
1440
1484
|
conflicted: false,
|
|
1441
1485
|
});
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1444
1488
|
var selectionStartRowIndex = Number.POSITIVE_INFINITY;
|
|
1445
1489
|
var selectionEndRowIndex = Number.NEGATIVE_INFINITY;
|
|
1446
1490
|
var selectionStartColumnIndex = Number.POSITIVE_INFINITY;
|
|
1447
1491
|
var selectionEndColumnIndex = Number.NEGATIVE_INFINITY;
|
|
1448
1492
|
pasteTargets.forEach(function (target) {
|
|
1449
|
-
var _a;
|
|
1493
|
+
var _a, _b, _c, _d;
|
|
1450
1494
|
if (target.conflicted)
|
|
1451
1495
|
return;
|
|
1452
1496
|
var column = columns[target.columnIndex];
|
|
@@ -1455,8 +1499,10 @@ function Table(props) {
|
|
|
1455
1499
|
return;
|
|
1456
1500
|
var currentValue = (0, utils_1.getCellValueByRowKey)(column.key, item.values);
|
|
1457
1501
|
var nextValue = target.clipboardValue;
|
|
1458
|
-
var
|
|
1459
|
-
|
|
1502
|
+
var pluginClipboardParser = ((_a = column.editor) === null || _a === void 0 ? void 0 : _a.type) === 'plugin' ? column.editor.parseClipboardText : undefined;
|
|
1503
|
+
var textEditorParser = ((_b = column.editor) === null || _b === void 0 ? void 0 : _b.type) === 'text' ? column.editor.parseValue : undefined;
|
|
1504
|
+
var clipboardParser = (_d = (_c = column.parseClipboardText) !== null && _c !== void 0 ? _c : pluginClipboardParser) !== null && _d !== void 0 ? _d : textEditorParser;
|
|
1505
|
+
if (clipboardParser) {
|
|
1460
1506
|
try {
|
|
1461
1507
|
var context = {
|
|
1462
1508
|
index: target.canonicalRowIndex,
|
|
@@ -1467,9 +1513,7 @@ function Table(props) {
|
|
|
1467
1513
|
value: currentValue,
|
|
1468
1514
|
text: target.clipboardValue,
|
|
1469
1515
|
};
|
|
1470
|
-
nextValue =
|
|
1471
|
-
? column.parseClipboardText(target.clipboardValue, context)
|
|
1472
|
-
: textEditorParser(target.clipboardValue, context);
|
|
1516
|
+
nextValue = clipboardParser(target.clipboardValue, context);
|
|
1473
1517
|
}
|
|
1474
1518
|
catch (error) {
|
|
1475
1519
|
notifyPasteError({
|
|
@@ -1533,6 +1577,7 @@ function Table(props) {
|
|
|
1533
1577
|
}, [
|
|
1534
1578
|
activeCell,
|
|
1535
1579
|
cellSelectionEnabled,
|
|
1580
|
+
cellSelectionRanges,
|
|
1536
1581
|
columns,
|
|
1537
1582
|
data,
|
|
1538
1583
|
editable,
|
|
@@ -32,12 +32,25 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __values = (this && this.__values) || function(o) {
|
|
36
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
37
|
+
if (m) return m.call(o);
|
|
38
|
+
if (o && typeof o.length === "number") return {
|
|
39
|
+
next: function () {
|
|
40
|
+
if (o && i >= o.length) o = void 0;
|
|
41
|
+
return { value: o && o[i++], done: !o };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
45
|
+
};
|
|
35
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
47
|
exports.createSelectEditorPlugin = createSelectEditorPlugin;
|
|
37
48
|
var React = __importStar(require("react"));
|
|
38
49
|
var defineEditorPlugin_1 = require("./defineEditorPlugin");
|
|
39
50
|
var getColumnId_1 = require("../utils/getColumnId");
|
|
40
51
|
function createSelectEditorPlugin(options) {
|
|
52
|
+
var e_1, _a;
|
|
53
|
+
var _b, _c;
|
|
41
54
|
function SelectEditor(_a) {
|
|
42
55
|
var _b, _c;
|
|
43
56
|
var value = _a.value, column = _a.column, commit = _a.commit, cancel = _a.cancel;
|
|
@@ -91,8 +104,93 @@ function createSelectEditorPlugin(options) {
|
|
|
91
104
|
React.createElement("path", { d: 'm4 6 4 4 4-4', stroke: 'currentColor', strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' })))));
|
|
92
105
|
}
|
|
93
106
|
SelectEditor.displayName = "BGridSelectEditor(".concat(options.id, ")");
|
|
107
|
+
// Precompute lookup maps for O(1) clipboard parsing and copying
|
|
108
|
+
var valueMap = new Map();
|
|
109
|
+
var labelMap = new Map();
|
|
110
|
+
var valueToLabelMap = new Map();
|
|
111
|
+
var isNumericValue = options.options.some(function (opt) { return typeof opt.value === 'number'; });
|
|
112
|
+
try {
|
|
113
|
+
for (var _d = __values(options.options), _e = _d.next(); !_e.done; _e = _d.next()) {
|
|
114
|
+
var opt = _e.value;
|
|
115
|
+
var valKey = String(opt.value);
|
|
116
|
+
if (!valueMap.has(valKey)) {
|
|
117
|
+
valueMap.set(valKey, opt);
|
|
118
|
+
}
|
|
119
|
+
if (typeof opt.label === 'string' || typeof opt.label === 'number') {
|
|
120
|
+
var labelText = String(opt.label);
|
|
121
|
+
var trimmedLabel = labelText.trim();
|
|
122
|
+
if (!labelMap.has(trimmedLabel)) {
|
|
123
|
+
labelMap.set(trimmedLabel, opt);
|
|
124
|
+
}
|
|
125
|
+
if (!valueToLabelMap.has(opt.value)) {
|
|
126
|
+
valueToLabelMap.set(opt.value, labelText);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
132
|
+
finally {
|
|
133
|
+
try {
|
|
134
|
+
if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
|
|
135
|
+
}
|
|
136
|
+
finally { if (e_1) throw e_1.error; }
|
|
137
|
+
}
|
|
138
|
+
var defaultParseClipboardText = function (text) {
|
|
139
|
+
var _a, _b, _c, _d;
|
|
140
|
+
var trimmed = text.trim();
|
|
141
|
+
// Check empty input
|
|
142
|
+
if (trimmed === '') {
|
|
143
|
+
var emptyOption = options.copyMode === 'value'
|
|
144
|
+
? ((_a = valueMap.get('')) !== null && _a !== void 0 ? _a : labelMap.get(''))
|
|
145
|
+
: ((_b = labelMap.get('')) !== null && _b !== void 0 ? _b : valueMap.get(''));
|
|
146
|
+
if (emptyOption) {
|
|
147
|
+
if (emptyOption.disabled && !options.allowDisabledOptions) {
|
|
148
|
+
throw new Error("Disabled select option for editor \"".concat(options.id, "\""));
|
|
149
|
+
}
|
|
150
|
+
return emptyOption.value;
|
|
151
|
+
}
|
|
152
|
+
if (options.allowEmpty) {
|
|
153
|
+
return options.emptyValue !== undefined ? options.emptyValue : '';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Match order depends on copyMode:
|
|
157
|
+
// If copyMode is 'label' (default), match by label first, then by value.
|
|
158
|
+
// If copyMode is 'value', match by value first, then by label.
|
|
159
|
+
var matched = options.copyMode === 'value'
|
|
160
|
+
? ((_c = valueMap.get(trimmed)) !== null && _c !== void 0 ? _c : labelMap.get(trimmed))
|
|
161
|
+
: ((_d = labelMap.get(trimmed)) !== null && _d !== void 0 ? _d : valueMap.get(trimmed));
|
|
162
|
+
if (matched) {
|
|
163
|
+
if (matched.disabled && !options.allowDisabledOptions) {
|
|
164
|
+
throw new Error("Disabled select option \"".concat(text, "\" for editor \"").concat(options.id, "\""));
|
|
165
|
+
}
|
|
166
|
+
return matched.value;
|
|
167
|
+
}
|
|
168
|
+
if (options.allowCustomValue) {
|
|
169
|
+
if (isNumericValue) {
|
|
170
|
+
var num = Number(trimmed);
|
|
171
|
+
if (!Number.isNaN(num)) {
|
|
172
|
+
return num;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return trimmed;
|
|
176
|
+
}
|
|
177
|
+
throw new Error("Invalid select value \"".concat(text, "\" for editor \"").concat(options.id, "\""));
|
|
178
|
+
};
|
|
179
|
+
var defaultGetClipboardText = function (params) {
|
|
180
|
+
var value = params.value;
|
|
181
|
+
if (options.copyMode === 'value') {
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
var label = valueToLabelMap.get(value);
|
|
185
|
+
if (label !== undefined) {
|
|
186
|
+
return label;
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
};
|
|
94
190
|
return (0, defineEditorPlugin_1.defineEditorPlugin)({
|
|
95
191
|
id: options.id,
|
|
96
192
|
component: SelectEditor,
|
|
193
|
+
parseClipboardText: (_b = options.parseClipboardText) !== null && _b !== void 0 ? _b : (function (text, _params) { return defaultParseClipboardText(text); }),
|
|
194
|
+
getClipboardText: (_c = options.getClipboardText) !== null && _c !== void 0 ? _c : defaultGetClipboardText,
|
|
97
195
|
});
|
|
98
196
|
}
|
package/esm/components/Table.js
CHANGED
|
@@ -1071,8 +1071,10 @@ function Table(props) {
|
|
|
1071
1071
|
rowHasNewLogicalCell = true;
|
|
1072
1072
|
const logicalItem = data[logicalCell.cell.rowIndex] ?? item;
|
|
1073
1073
|
const value = getCellValueByRowKey(column.key, logicalItem.values);
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1074
|
+
const pluginClipboardTextGetter = column.editor?.type === 'plugin' ? column.editor.getClipboardText : undefined;
|
|
1075
|
+
const clipboardTextGetter = column.getClipboardText ?? pluginClipboardTextGetter;
|
|
1076
|
+
const clipboardValue = clipboardTextGetter
|
|
1077
|
+
? clipboardTextGetter({
|
|
1076
1078
|
column,
|
|
1077
1079
|
index: logicalCell.cell.rowIndex,
|
|
1078
1080
|
columnIndex: ci,
|
|
@@ -1180,17 +1182,41 @@ function Table(props) {
|
|
|
1180
1182
|
});
|
|
1181
1183
|
return true;
|
|
1182
1184
|
}
|
|
1183
|
-
const
|
|
1184
|
-
const
|
|
1185
|
+
const primaryRange = cellSelectionRanges.length === 1 ? normalizeCellSelectionRange(cellSelectionRanges[0]) : null;
|
|
1186
|
+
const selectionRowCount = primaryRange ? primaryRange.endRowIndex - primaryRange.startRowIndex + 1 : 1;
|
|
1187
|
+
const selectionColCount = primaryRange ? primaryRange.endColumnIndex - primaryRange.startColumnIndex + 1 : 1;
|
|
1188
|
+
// Determine fill rows and cols:
|
|
1189
|
+
// 1. Single cell (1x1) copied into multi-cell selection: fill the entire selection.
|
|
1190
|
+
// 2. nxm copied into exact multiple NxM selection: repeat pattern across selection.
|
|
1191
|
+
// 3. Otherwise: paste 1:1 matching matrix dimensions, anchored at selection top-left (or activeCell if no range).
|
|
1192
|
+
const isSingleClipboardCell = matrix.length === 1 && matrix[0]?.length === 1;
|
|
1193
|
+
const matrixColCount = matrix.reduce((max, row) => Math.max(max, row.length), 0);
|
|
1194
|
+
const isExactRowMultiple = primaryRange && matrix.length > 0 && selectionRowCount % matrix.length === 0;
|
|
1195
|
+
const isExactColMultiple = primaryRange &&
|
|
1196
|
+
matrixColCount > 0 &&
|
|
1197
|
+
selectionColCount % matrixColCount === 0;
|
|
1198
|
+
const canTileMatrix = primaryRange &&
|
|
1199
|
+
isExactRowMultiple &&
|
|
1200
|
+
isExactColMultiple &&
|
|
1201
|
+
(selectionRowCount > matrix.length || selectionColCount > matrixColCount);
|
|
1202
|
+
let fillRowCount = matrix.length;
|
|
1203
|
+
let fillColCount = matrixColCount;
|
|
1204
|
+
let startRowIndex = primaryRange ? primaryRange.startRowIndex : activeCell.rowIndex;
|
|
1205
|
+
let startColumnIndex = primaryRange ? primaryRange.startColumnIndex : activeCell.columnIndex;
|
|
1206
|
+
if (primaryRange && (isSingleClipboardCell || canTileMatrix)) {
|
|
1207
|
+
fillRowCount = selectionRowCount;
|
|
1208
|
+
fillColCount = selectionColCount;
|
|
1209
|
+
}
|
|
1185
1210
|
const nextData = [...data];
|
|
1186
1211
|
const appendedRows = [];
|
|
1187
1212
|
const changes = [];
|
|
1188
1213
|
const createRowOnPaste = cellSelectionOptions?.createRowOnPaste;
|
|
1189
1214
|
if (createRowOnPaste) {
|
|
1190
|
-
const requiredRowCount = startRowIndex +
|
|
1215
|
+
const requiredRowCount = startRowIndex + fillRowCount;
|
|
1191
1216
|
while (nextData.length < requiredRowCount) {
|
|
1192
1217
|
const rowIndex = nextData.length;
|
|
1193
|
-
const
|
|
1218
|
+
const matrixRowOffset = isSingleClipboardCell ? 0 : (rowIndex - startRowIndex) % matrix.length;
|
|
1219
|
+
const clipboardRow = matrix[matrixRowOffset];
|
|
1194
1220
|
if (!clipboardRow)
|
|
1195
1221
|
break;
|
|
1196
1222
|
let createdItem;
|
|
@@ -1210,25 +1236,43 @@ function Table(props) {
|
|
|
1210
1236
|
}
|
|
1211
1237
|
const logicalResolutionData = [...nextData];
|
|
1212
1238
|
const pasteTargets = new Map();
|
|
1213
|
-
|
|
1239
|
+
for (let rowOffset = 0; rowOffset < fillRowCount; rowOffset++) {
|
|
1214
1240
|
const rowIndex = startRowIndex + rowOffset;
|
|
1215
1241
|
const item = logicalResolutionData[rowIndex];
|
|
1216
1242
|
if (!item || item.status === BGridDataItemStatus.remove)
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1243
|
+
continue;
|
|
1244
|
+
const matrixRow = isSingleClipboardCell ? matrix[0] : matrix[rowOffset % matrix.length];
|
|
1245
|
+
if (!matrixRow || matrixRow.length === 0)
|
|
1246
|
+
continue;
|
|
1247
|
+
for (let colOffset = 0; colOffset < fillColCount; colOffset++) {
|
|
1248
|
+
const columnIndex = startColumnIndex + colOffset;
|
|
1220
1249
|
const column = columns[columnIndex];
|
|
1221
1250
|
// Pasting mutates row data without opening an editor, so only columns
|
|
1222
1251
|
// explicitly opted into editing may receive clipboard values.
|
|
1223
1252
|
if (!column || column.editable !== true)
|
|
1224
|
-
|
|
1253
|
+
continue;
|
|
1254
|
+
let clipboardValue;
|
|
1255
|
+
if (isSingleClipboardCell) {
|
|
1256
|
+
clipboardValue = matrix[0]?.[0] ?? '';
|
|
1257
|
+
}
|
|
1258
|
+
else if (canTileMatrix) {
|
|
1259
|
+
const cellColOffset = matrixColCount > 0 ? colOffset % matrixColCount : 0;
|
|
1260
|
+
if (cellColOffset >= matrixRow.length)
|
|
1261
|
+
continue;
|
|
1262
|
+
clipboardValue = matrixRow[cellColOffset] ?? '';
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
if (colOffset >= matrixRow.length)
|
|
1266
|
+
continue;
|
|
1267
|
+
clipboardValue = matrixRow[colOffset] ?? '';
|
|
1268
|
+
}
|
|
1225
1269
|
const logicalCell = resolveLogicalCell(logicalResolutionData, props.cellMergeOptions, {
|
|
1226
1270
|
rowIndex,
|
|
1227
1271
|
columnIndex,
|
|
1228
1272
|
});
|
|
1229
1273
|
if (logicalCell.rowIndexes.some(targetRowIndex => !logicalResolutionData[targetRowIndex] ||
|
|
1230
1274
|
logicalResolutionData[targetRowIndex].status === BGridDataItemStatus.remove)) {
|
|
1231
|
-
|
|
1275
|
+
continue;
|
|
1232
1276
|
}
|
|
1233
1277
|
const targetKey = `${logicalCell.cell.rowIndex}:${columnIndex}`;
|
|
1234
1278
|
const existingTarget = pasteTargets.get(targetKey);
|
|
@@ -1241,7 +1285,7 @@ function Table(props) {
|
|
|
1241
1285
|
columnIndex,
|
|
1242
1286
|
});
|
|
1243
1287
|
}
|
|
1244
|
-
|
|
1288
|
+
continue;
|
|
1245
1289
|
}
|
|
1246
1290
|
pasteTargets.set(targetKey, {
|
|
1247
1291
|
canonicalRowIndex: logicalCell.cell.rowIndex,
|
|
@@ -1250,8 +1294,8 @@ function Table(props) {
|
|
|
1250
1294
|
clipboardValue,
|
|
1251
1295
|
conflicted: false,
|
|
1252
1296
|
});
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1255
1299
|
let selectionStartRowIndex = Number.POSITIVE_INFINITY;
|
|
1256
1300
|
let selectionEndRowIndex = Number.NEGATIVE_INFINITY;
|
|
1257
1301
|
let selectionStartColumnIndex = Number.POSITIVE_INFINITY;
|
|
@@ -1265,8 +1309,10 @@ function Table(props) {
|
|
|
1265
1309
|
return;
|
|
1266
1310
|
const currentValue = getCellValueByRowKey(column.key, item.values);
|
|
1267
1311
|
let nextValue = target.clipboardValue;
|
|
1312
|
+
const pluginClipboardParser = column.editor?.type === 'plugin' ? column.editor.parseClipboardText : undefined;
|
|
1268
1313
|
const textEditorParser = column.editor?.type === 'text' ? column.editor.parseValue : undefined;
|
|
1269
|
-
|
|
1314
|
+
const clipboardParser = column.parseClipboardText ?? pluginClipboardParser ?? textEditorParser;
|
|
1315
|
+
if (clipboardParser) {
|
|
1270
1316
|
try {
|
|
1271
1317
|
const context = {
|
|
1272
1318
|
index: target.canonicalRowIndex,
|
|
@@ -1277,9 +1323,7 @@ function Table(props) {
|
|
|
1277
1323
|
value: currentValue,
|
|
1278
1324
|
text: target.clipboardValue,
|
|
1279
1325
|
};
|
|
1280
|
-
nextValue =
|
|
1281
|
-
? column.parseClipboardText(target.clipboardValue, context)
|
|
1282
|
-
: textEditorParser(target.clipboardValue, context);
|
|
1326
|
+
nextValue = clipboardParser(target.clipboardValue, context);
|
|
1283
1327
|
}
|
|
1284
1328
|
catch (error) {
|
|
1285
1329
|
notifyPasteError({
|
|
@@ -1340,6 +1384,7 @@ function Table(props) {
|
|
|
1340
1384
|
}, [
|
|
1341
1385
|
activeCell,
|
|
1342
1386
|
cellSelectionEnabled,
|
|
1387
|
+
cellSelectionRanges,
|
|
1343
1388
|
columns,
|
|
1344
1389
|
data,
|
|
1345
1390
|
editable,
|
|
@@ -53,8 +53,82 @@ export function createSelectEditorPlugin(options) {
|
|
|
53
53
|
React.createElement("path", { d: 'm4 6 4 4 4-4', stroke: 'currentColor', strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' })))));
|
|
54
54
|
}
|
|
55
55
|
SelectEditor.displayName = `BGridSelectEditor(${options.id})`;
|
|
56
|
+
// Precompute lookup maps for O(1) clipboard parsing and copying
|
|
57
|
+
const valueMap = new Map();
|
|
58
|
+
const labelMap = new Map();
|
|
59
|
+
const valueToLabelMap = new Map();
|
|
60
|
+
const isNumericValue = options.options.some(opt => typeof opt.value === 'number');
|
|
61
|
+
for (const opt of options.options) {
|
|
62
|
+
const valKey = String(opt.value);
|
|
63
|
+
if (!valueMap.has(valKey)) {
|
|
64
|
+
valueMap.set(valKey, opt);
|
|
65
|
+
}
|
|
66
|
+
if (typeof opt.label === 'string' || typeof opt.label === 'number') {
|
|
67
|
+
const labelText = String(opt.label);
|
|
68
|
+
const trimmedLabel = labelText.trim();
|
|
69
|
+
if (!labelMap.has(trimmedLabel)) {
|
|
70
|
+
labelMap.set(trimmedLabel, opt);
|
|
71
|
+
}
|
|
72
|
+
if (!valueToLabelMap.has(opt.value)) {
|
|
73
|
+
valueToLabelMap.set(opt.value, labelText);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const defaultParseClipboardText = (text) => {
|
|
78
|
+
const trimmed = text.trim();
|
|
79
|
+
// Check empty input
|
|
80
|
+
if (trimmed === '') {
|
|
81
|
+
const emptyOption = options.copyMode === 'value'
|
|
82
|
+
? (valueMap.get('') ?? labelMap.get(''))
|
|
83
|
+
: (labelMap.get('') ?? valueMap.get(''));
|
|
84
|
+
if (emptyOption) {
|
|
85
|
+
if (emptyOption.disabled && !options.allowDisabledOptions) {
|
|
86
|
+
throw new Error(`Disabled select option for editor "${options.id}"`);
|
|
87
|
+
}
|
|
88
|
+
return emptyOption.value;
|
|
89
|
+
}
|
|
90
|
+
if (options.allowEmpty) {
|
|
91
|
+
return options.emptyValue !== undefined ? options.emptyValue : '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Match order depends on copyMode:
|
|
95
|
+
// If copyMode is 'label' (default), match by label first, then by value.
|
|
96
|
+
// If copyMode is 'value', match by value first, then by label.
|
|
97
|
+
const matched = options.copyMode === 'value'
|
|
98
|
+
? (valueMap.get(trimmed) ?? labelMap.get(trimmed))
|
|
99
|
+
: (labelMap.get(trimmed) ?? valueMap.get(trimmed));
|
|
100
|
+
if (matched) {
|
|
101
|
+
if (matched.disabled && !options.allowDisabledOptions) {
|
|
102
|
+
throw new Error(`Disabled select option "${text}" for editor "${options.id}"`);
|
|
103
|
+
}
|
|
104
|
+
return matched.value;
|
|
105
|
+
}
|
|
106
|
+
if (options.allowCustomValue) {
|
|
107
|
+
if (isNumericValue) {
|
|
108
|
+
const num = Number(trimmed);
|
|
109
|
+
if (!Number.isNaN(num)) {
|
|
110
|
+
return num;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return trimmed;
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`Invalid select value "${text}" for editor "${options.id}"`);
|
|
116
|
+
};
|
|
117
|
+
const defaultGetClipboardText = (params) => {
|
|
118
|
+
const { value } = params;
|
|
119
|
+
if (options.copyMode === 'value') {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
const label = valueToLabelMap.get(value);
|
|
123
|
+
if (label !== undefined) {
|
|
124
|
+
return label;
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
};
|
|
56
128
|
return defineEditorPlugin({
|
|
57
129
|
id: options.id,
|
|
58
130
|
component: SelectEditor,
|
|
131
|
+
parseClipboardText: options.parseClipboardText ?? ((text, _params) => defaultParseClipboardText(text)),
|
|
132
|
+
getClipboardText: options.getClipboardText ?? defaultGetClipboardText,
|
|
59
133
|
});
|
|
60
134
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { BGridPluginEditorConfig } from '../types';
|
|
2
|
+
import { BGridCellClipboardParseParams, BGridCellClipboardTextParams, BGridPluginEditorConfig } from '../types';
|
|
3
3
|
export interface BGridSelectEditorOption<Value extends string | number> {
|
|
4
4
|
value: Value;
|
|
5
5
|
label: React.ReactNode;
|
|
@@ -11,5 +11,41 @@ export interface BGridSelectEditorPluginOptions<Value extends string | number> {
|
|
|
11
11
|
ariaLabel?: string;
|
|
12
12
|
placeholder?: string;
|
|
13
13
|
openOnMount?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Whether to allow pasting values that do not match any option value or label.
|
|
16
|
+
* Defaults to false (unmatched values throw an error and report parseValueFailed).
|
|
17
|
+
*/
|
|
18
|
+
allowCustomValue?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Clipboard text mode when copying cells using this editor.
|
|
21
|
+
* 'label' (default): copies the option's label if it is a string or number.
|
|
22
|
+
* 'value': copies the option's raw value.
|
|
23
|
+
*/
|
|
24
|
+
copyMode?: 'label' | 'value';
|
|
25
|
+
/**
|
|
26
|
+
* Whether to allow clearing cell values by pasting empty or whitespace-only text.
|
|
27
|
+
* Defaults to false (unmatched empty text throws parseValueFailed).
|
|
28
|
+
* When true, pastes `emptyValue` into the cell.
|
|
29
|
+
*/
|
|
30
|
+
allowEmpty?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Value written when an empty text is pasted and `allowEmpty` is true.
|
|
33
|
+
* Defaults to undefined.
|
|
34
|
+
*/
|
|
35
|
+
emptyValue?: Value | null;
|
|
36
|
+
/**
|
|
37
|
+
* Whether to allow pasting options that have `disabled: true`.
|
|
38
|
+
* Defaults to false (pasting a disabled option throws an error).
|
|
39
|
+
*/
|
|
40
|
+
allowDisabledOptions?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Custom clipboard parser for this editor.
|
|
43
|
+
* Throw an error to reject the paste for this cell.
|
|
44
|
+
*/
|
|
45
|
+
parseClipboardText?: <T>(text: string, params: BGridCellClipboardParseParams<T>) => Value | unknown;
|
|
46
|
+
/**
|
|
47
|
+
* Custom clipboard text getter for this editor.
|
|
48
|
+
*/
|
|
49
|
+
getClipboardText?: <T>(params: BGridCellClipboardTextParams<T>) => any;
|
|
14
50
|
}
|
|
15
51
|
export declare function createSelectEditorPlugin<T, Value extends string | number = string>(options: BGridSelectEditorPluginOptions<Value>): BGridPluginEditorConfig<T>;
|
package/types/types.d.ts
CHANGED
|
@@ -122,6 +122,13 @@ export interface BGridPluginEditorConfig<T> {
|
|
|
122
122
|
type: 'plugin';
|
|
123
123
|
id: string;
|
|
124
124
|
component: React.ComponentType<BGridEditorPluginProps<T>>;
|
|
125
|
+
getClipboardText?: (params: BGridCellClipboardTextParams<T>) => any;
|
|
126
|
+
/**
|
|
127
|
+
* Restores text/plain clipboard input to the editor's stored value type.
|
|
128
|
+
* Takes precedence over built-in fallback parser during multi-cell paste.
|
|
129
|
+
* Throw to keep the current cell value and report parseValueFailed through onPasteError.
|
|
130
|
+
*/
|
|
131
|
+
parseClipboardText?: (text: string, params: BGridCellClipboardParseParams<T>) => unknown;
|
|
125
132
|
}
|
|
126
133
|
export type BGridCellEditorConfig<T> = BGridTextEditorConfig<T> | BGridCheckboxEditorConfig<T> | BGridPluginEditorConfig<T>;
|
|
127
134
|
export interface BGridCellClipboardTextParams<T> {
|