@quadrats/react 1.1.0 → 1.1.2
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/core/components/Quadrats.js +5 -2
- package/core/contexts/modal/CarouselModal/CarouselModal.js +14 -17
- package/index.cjs.js +19 -19
- package/package.json +5 -5
- package/table/components/ColumnDragButton.d.ts +10 -0
- package/table/components/ColumnDragButton.js +41 -0
- package/table/components/RowDragButton.d.ts +10 -0
- package/table/components/RowDragButton.js +42 -0
- package/table/components/Table.js +22 -17
- package/table/components/TableCell.js +129 -23
- package/table/components/TableDragLayer.d.ts +6 -0
- package/table/components/TableDragLayer.js +89 -0
- package/table/components/TableMain.js +14 -6
- package/table/contexts/TableActionsContext.d.ts +1 -1
- package/table/contexts/TableDragContext.d.ts +26 -0
- package/table/contexts/TableDragContext.js +26 -0
- package/table/hooks/useColumnResize.js +29 -0
- package/table/hooks/useTableActions.d.ts +2 -0
- package/table/hooks/useTableActions.js +217 -11
- package/table/hooks/useTableCellToolbarActions.js +126 -4
- package/table/index.cjs.js +873 -125
- package/table/index.js +3 -0
- package/table/table.css +1 -1
- package/table/table.scss +35 -0
- package/table/typings.d.ts +2 -0
- package/table/utils/helper.d.ts +34 -8
- package/table/utils/helper.js +178 -72
package/table/index.cjs.js
CHANGED
|
@@ -13,6 +13,8 @@ var react = require('@quadrats/react');
|
|
|
13
13
|
var components = require('@quadrats/react/components');
|
|
14
14
|
var clsx = require('clsx');
|
|
15
15
|
var slate = require('slate');
|
|
16
|
+
var reactDnd = require('react-dnd');
|
|
17
|
+
var reactDndHtml5Backend = require('react-dnd-html5-backend');
|
|
16
18
|
|
|
17
19
|
const TableActionsContext = React.createContext(undefined);
|
|
18
20
|
|
|
@@ -44,6 +46,58 @@ function useTableStateContext() {
|
|
|
44
46
|
return context;
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/**
|
|
50
|
+
* 分配百分比寬度,確保總和為 100%
|
|
51
|
+
* 前 n-1 個欄位使用四捨五入的平均值,最後一個欄位使用剩餘寬度
|
|
52
|
+
* @param count - 欄位數量
|
|
53
|
+
* @returns 百分比陣列,總和為 100%
|
|
54
|
+
*/
|
|
55
|
+
function distributeEqualPercentages(count) {
|
|
56
|
+
if (count <= 0)
|
|
57
|
+
return [];
|
|
58
|
+
if (count === 1)
|
|
59
|
+
return [100];
|
|
60
|
+
const percentages = [];
|
|
61
|
+
const averagePercentage = Math.round((100 / count) * 10) / 10;
|
|
62
|
+
// 前 n-1 個欄位使用四捨五入的平均值
|
|
63
|
+
for (let i = 0; i < count - 1; i++) {
|
|
64
|
+
percentages.push(averagePercentage);
|
|
65
|
+
}
|
|
66
|
+
// 最後一個欄位使用剩餘寬度,確保總和為 100%
|
|
67
|
+
const sumOfFirst = percentages.reduce((sum, val) => sum + val, 0);
|
|
68
|
+
const lastPercentage = Math.round((100 - sumOfFirst) * 10) / 10;
|
|
69
|
+
percentages.push(lastPercentage);
|
|
70
|
+
return percentages;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 等比例縮減現有百分比並加入新欄位
|
|
74
|
+
* 前 n-1 個縮減後的欄位使用四捨五入,最後一個縮減欄位使用剩餘寬度
|
|
75
|
+
* @param currentPercentages - 當前的百分比陣列
|
|
76
|
+
* @param newColumnPercentage - 新欄位要佔的百分比
|
|
77
|
+
* @param insertIndex - 新欄位插入的位置(0-based)
|
|
78
|
+
* @returns 新的百分比陣列,總和為 100%
|
|
79
|
+
*/
|
|
80
|
+
function scalePercentagesWithNewColumn(currentPercentages, newColumnPercentage, insertIndex) {
|
|
81
|
+
if (currentPercentages.length === 0)
|
|
82
|
+
return [100];
|
|
83
|
+
const currentTotal = currentPercentages.reduce((sum, val) => sum + val, 0);
|
|
84
|
+
const targetTotal = 100 - newColumnPercentage;
|
|
85
|
+
const scaleFactor = targetTotal / currentTotal;
|
|
86
|
+
const scaledPercentages = [];
|
|
87
|
+
// 前 n-1 個欄位使用縮放後四捨五入的值
|
|
88
|
+
for (let i = 0; i < currentPercentages.length - 1; i++) {
|
|
89
|
+
const scaledValue = Math.round(currentPercentages[i] * scaleFactor * 10) / 10;
|
|
90
|
+
scaledPercentages.push(scaledValue);
|
|
91
|
+
}
|
|
92
|
+
// 最後一個現有欄位使用剩餘寬度,確保總和正確
|
|
93
|
+
const sumOfScaled = scaledPercentages.reduce((sum, val) => sum + val, 0);
|
|
94
|
+
const lastScaledValue = Math.round((targetTotal - sumOfScaled) * 10) / 10;
|
|
95
|
+
scaledPercentages.push(lastScaledValue);
|
|
96
|
+
// 在指定位置插入新欄位
|
|
97
|
+
const result = [...scaledPercentages];
|
|
98
|
+
result.splice(insertIndex, 0, newColumnPercentage);
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
47
101
|
/**
|
|
48
102
|
* 提取表格的所有基本元素
|
|
49
103
|
*/
|
|
@@ -370,11 +424,9 @@ function getColumnWidths(tableElement, tableWidth) {
|
|
|
370
424
|
}
|
|
371
425
|
return widths;
|
|
372
426
|
}
|
|
373
|
-
// 否則返回平均分配的 percentage
|
|
374
|
-
const
|
|
375
|
-
return
|
|
376
|
-
.fill(null)
|
|
377
|
-
.map(() => ({ type: 'percentage', value: equalPercentage }));
|
|
427
|
+
// 否則返回平均分配的 percentage(總和為 100%)
|
|
428
|
+
const percentages = distributeEqualPercentages(columnCount);
|
|
429
|
+
return percentages.map((value) => ({ type: 'percentage', value }));
|
|
378
430
|
}
|
|
379
431
|
/**
|
|
380
432
|
* 設定表格的欄位寬度
|
|
@@ -390,16 +442,17 @@ function setColumnWidths(editor, tableElement, columnWidths) {
|
|
|
390
442
|
* 計算新增欄位後的欄位寬度
|
|
391
443
|
* - 如果所有欄位都是 percentage:按比例縮減現有欄位,新欄位佔平均寬度
|
|
392
444
|
* - 如果有混合模式(percentage + pixel):
|
|
393
|
-
* * percentage
|
|
394
|
-
* *
|
|
445
|
+
* * 如果用戶操作的欄位是 pinned column:新欄位使用 percentage(需要調整 pinned columns 的百分比)
|
|
446
|
+
* * 如果用戶操作的欄位是 unpinned column:新欄位使用 pixel(與其他 pixel 欄位相同寬度)
|
|
395
447
|
*
|
|
396
448
|
* @param currentWidths - 當前的欄位寬度陣列
|
|
397
449
|
* @param insertIndex - 新欄位插入的位置(0-based)
|
|
450
|
+
* @param pinnedColumnIndices - 當前釘選欄位的索引陣列(插入前的索引)
|
|
451
|
+
* @param operatingColumnIndex - 用戶實際操作的欄位索引(用於判斷是在 pinned 還是 unpinned column 操作)
|
|
398
452
|
* @returns 新的欄位寬度陣列
|
|
399
453
|
*/
|
|
400
|
-
function calculateColumnWidthsAfterAdd(currentWidths, insertIndex) {
|
|
454
|
+
function calculateColumnWidthsAfterAdd(currentWidths, insertIndex, pinnedColumnIndices = [], operatingColumnIndex) {
|
|
401
455
|
const newColumnCount = currentWidths.length + 1;
|
|
402
|
-
const averagePercentage = Math.round((100 / newColumnCount) * 10) / 10;
|
|
403
456
|
// 分離 percentage 和 pixel 欄位
|
|
404
457
|
const percentageColumns = [];
|
|
405
458
|
const pixelColumns = [];
|
|
@@ -413,46 +466,68 @@ function calculateColumnWidthsAfterAdd(currentWidths, insertIndex) {
|
|
|
413
466
|
});
|
|
414
467
|
// 如果所有欄位都是 percentage(正常模式,無 pinned columns)
|
|
415
468
|
if (percentageColumns.length === currentWidths.length) {
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
// 按比例縮減現有欄位
|
|
425
|
-
const scaledValue = Math.round(width.value * scaleFactor * 10) / 10;
|
|
426
|
-
newWidths.push({ type: 'percentage', value: scaledValue });
|
|
427
|
-
});
|
|
428
|
-
if (insertIndex >= currentWidths.length) {
|
|
429
|
-
newWidths.push({ type: 'percentage', value: averagePercentage });
|
|
430
|
-
}
|
|
431
|
-
return newWidths;
|
|
469
|
+
// 計算新欄位的平均百分比
|
|
470
|
+
const averagePercentage = Math.round((100 / newColumnCount) * 10) / 10;
|
|
471
|
+
// 提取當前的百分比值
|
|
472
|
+
const currentPercentages = currentWidths.map((w) => w.value);
|
|
473
|
+
// 等比例縮減現有欄位並插入新欄位
|
|
474
|
+
const newPercentages = scalePercentagesWithNewColumn(currentPercentages, averagePercentage, insertIndex);
|
|
475
|
+
return newPercentages.map((value) => ({ type: 'percentage', value }));
|
|
432
476
|
}
|
|
433
477
|
// 如果有混合的 pixel 和 percentage 欄位(有 pinned columns)
|
|
434
|
-
// percentage 欄位(pinned)保持不變
|
|
435
|
-
// 新欄位應維持 pixel(此時一般欄位必定是 pixel)
|
|
436
478
|
if (percentageColumns.length && pixelColumns.length) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
479
|
+
// 判斷新欄位是否應該是 pinned column
|
|
480
|
+
const isNewColumnPinned = typeof operatingColumnIndex === 'number' ? pinnedColumnIndices.includes(operatingColumnIndex) : false;
|
|
481
|
+
if (isNewColumnPinned) {
|
|
482
|
+
// 新欄位應該是 pinned column,使用 percentage
|
|
483
|
+
// 需要調整所有 pinned columns 的百分比
|
|
484
|
+
const newWidths = [];
|
|
485
|
+
const pinnedColumnCount = pinnedColumnIndices.length + 1; // 加上新欄位
|
|
486
|
+
const pinnedPercentagePerColumn = Math.min(Math.round((table.MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE / pinnedColumnCount) * 10) / 10, table.MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE);
|
|
487
|
+
currentWidths.forEach((width, index) => {
|
|
488
|
+
if (index === insertIndex) {
|
|
489
|
+
newWidths.push({ type: 'percentage', value: pinnedPercentagePerColumn });
|
|
490
|
+
}
|
|
491
|
+
if (pinnedColumnIndices.includes(index)) {
|
|
492
|
+
// 調整現有 pinned column 的百分比
|
|
493
|
+
newWidths.push({ type: 'percentage', value: pinnedPercentagePerColumn });
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
// 保持非 pinned column (pixel) 不變
|
|
497
|
+
newWidths.push(Object.assign({}, width));
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
// 如果插入位置在最後(但仍在 pinned 區域內)
|
|
501
|
+
if (insertIndex >= currentWidths.length) {
|
|
502
|
+
newWidths.push({ type: 'percentage', value: pinnedPercentagePerColumn });
|
|
503
|
+
}
|
|
504
|
+
return newWidths;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// 新欄位應該是 unpinned column,使用 pixel
|
|
508
|
+
const newWidths = [];
|
|
509
|
+
// 找到最後一個 pixel 欄位的寬度,新欄位將複製這個寬度
|
|
510
|
+
const lastPixelWidth = pixelColumns.length > 0 ? pixelColumns[pixelColumns.length - 1].value : 150;
|
|
511
|
+
currentWidths.forEach((width, index) => {
|
|
512
|
+
if (index === insertIndex) {
|
|
513
|
+
newWidths.push({ type: 'pixel', value: lastPixelWidth });
|
|
514
|
+
}
|
|
515
|
+
newWidths.push(Object.assign({}, width));
|
|
516
|
+
});
|
|
517
|
+
// 如果插入位置在最後
|
|
518
|
+
if (insertIndex >= currentWidths.length) {
|
|
442
519
|
newWidths.push({ type: 'pixel', value: lastPixelWidth });
|
|
443
520
|
}
|
|
444
|
-
newWidths
|
|
445
|
-
});
|
|
446
|
-
// 如果插入位置在最後
|
|
447
|
-
if (insertIndex >= currentWidths.length) {
|
|
448
|
-
newWidths.push({ type: 'pixel', value: lastPixelWidth });
|
|
521
|
+
return newWidths;
|
|
449
522
|
}
|
|
450
|
-
return newWidths;
|
|
451
523
|
}
|
|
452
|
-
// Fallback:
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
524
|
+
// Fallback: 等比例縮減並插入新欄位
|
|
525
|
+
const averagePercentage = Math.round((100 / newColumnCount) * 10) / 10;
|
|
526
|
+
// 提取當前的百分比值(假設都是 percentage,否則轉為平均分配)
|
|
527
|
+
const currentPercentages = currentWidths.map((w) => (w.type === 'percentage' ? w.value : 100 / currentWidths.length));
|
|
528
|
+
// 等比例縮減現有欄位並插入新欄位
|
|
529
|
+
const newPercentages = scalePercentagesWithNewColumn(currentPercentages, averagePercentage, insertIndex);
|
|
530
|
+
return newPercentages.map((value) => ({ type: 'percentage', value }));
|
|
456
531
|
}
|
|
457
532
|
/**
|
|
458
533
|
* 計算刪除欄位後的欄位寬度
|
|
@@ -647,19 +722,33 @@ function calculateResizedColumnWidths(currentWidths, columnIndex, deltaPercentag
|
|
|
647
722
|
return newWidths;
|
|
648
723
|
}
|
|
649
724
|
/**
|
|
650
|
-
*
|
|
725
|
+
* 移動或交換欄位寬度設定
|
|
651
726
|
* @param currentWidths - 當前的欄位寬度陣列
|
|
652
|
-
* @param
|
|
653
|
-
* @param
|
|
654
|
-
* @
|
|
727
|
+
* @param sourceIndex - 來源欄位的索引
|
|
728
|
+
* @param targetIndex - 目標欄位的索引
|
|
729
|
+
* @param mode - 'swap' 為交換兩個位置,'move' 為移動到目標位置
|
|
730
|
+
* @returns 處理後的欄位寬度陣列
|
|
655
731
|
*/
|
|
656
|
-
function
|
|
657
|
-
if (
|
|
732
|
+
function moveOrSwapColumnWidth(currentWidths, sourceIndex, targetIndex, mode = 'move') {
|
|
733
|
+
if (sourceIndex === targetIndex ||
|
|
734
|
+
sourceIndex < 0 ||
|
|
735
|
+
targetIndex < 0 ||
|
|
736
|
+
sourceIndex >= currentWidths.length ||
|
|
737
|
+
targetIndex >= currentWidths.length) {
|
|
658
738
|
return currentWidths;
|
|
659
739
|
}
|
|
660
740
|
const newWidths = [...currentWidths];
|
|
661
|
-
|
|
662
|
-
|
|
741
|
+
if (mode === 'swap') {
|
|
742
|
+
// swap 邏輯:直接交換兩個位置的值
|
|
743
|
+
const temp = newWidths[sourceIndex];
|
|
744
|
+
newWidths[sourceIndex] = newWidths[targetIndex];
|
|
745
|
+
newWidths[targetIndex] = temp;
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
// move 邏輯:移除再插入
|
|
749
|
+
const [movedWidth] = newWidths.splice(sourceIndex, 1);
|
|
750
|
+
newWidths.splice(targetIndex, 0, movedWidth);
|
|
751
|
+
}
|
|
663
752
|
return newWidths;
|
|
664
753
|
}
|
|
665
754
|
/**
|
|
@@ -690,28 +779,28 @@ function convertToMixedWidthMode(currentWidths, pinnedColumnIndices, tableWidth)
|
|
|
690
779
|
}
|
|
691
780
|
});
|
|
692
781
|
// 確保釘選欄位總和不超過指定範圍
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
else {
|
|
706
|
-
const percentage = (width.value / tableWidth) * 100;
|
|
707
|
-
currentWidths[index] = {
|
|
708
|
-
type: 'percentage',
|
|
709
|
-
value: Math.round(percentage * scaleFactor * 10) / 10,
|
|
710
|
-
};
|
|
711
|
-
}
|
|
782
|
+
const scaleFactor = totalPinnedPercentage > table.MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE
|
|
783
|
+
? table.MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE / totalPinnedPercentage
|
|
784
|
+
: 1;
|
|
785
|
+
totalPinnedPercentage = Math.min(totalPinnedPercentage, table.MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE);
|
|
786
|
+
// 調整釘選欄位的百分比
|
|
787
|
+
currentWidths.forEach((width, index) => {
|
|
788
|
+
if (pinnedColumnIndices.includes(index)) {
|
|
789
|
+
if (width.type === 'percentage') {
|
|
790
|
+
currentWidths[index] = {
|
|
791
|
+
type: 'percentage',
|
|
792
|
+
value: Math.round(width.value * scaleFactor * 10) / 10,
|
|
793
|
+
};
|
|
712
794
|
}
|
|
713
|
-
|
|
714
|
-
|
|
795
|
+
else {
|
|
796
|
+
const percentage = (width.value / tableWidth) * 100;
|
|
797
|
+
currentWidths[index] = {
|
|
798
|
+
type: 'percentage',
|
|
799
|
+
value: Math.round(percentage * scaleFactor * 10) / 10,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
});
|
|
715
804
|
// 計算剩餘空間(用於未釘選欄位)
|
|
716
805
|
const remainingPercentage = 100 - totalPinnedPercentage;
|
|
717
806
|
const remainingPixelWidth = (tableWidth * remainingPercentage) / 100;
|
|
@@ -730,6 +819,25 @@ function convertToMixedWidthMode(currentWidths, pinnedColumnIndices, tableWidth)
|
|
|
730
819
|
});
|
|
731
820
|
return newWidths;
|
|
732
821
|
}
|
|
822
|
+
/**
|
|
823
|
+
* 將混合模式的欄位寬度轉換回純百分比模式
|
|
824
|
+
* 當所有 pinned columns 都被 unpin 後,需要將 pixel 欄位轉換回 percentage
|
|
825
|
+
* @param currentWidths - 當前的欄位寬度陣列(混合模式)
|
|
826
|
+
* @returns 轉換後的純百分比欄位寬度陣列,總和為 100%
|
|
827
|
+
*/
|
|
828
|
+
function convertToPercentageMode(currentWidths) {
|
|
829
|
+
if (currentWidths.length === 0)
|
|
830
|
+
return [];
|
|
831
|
+
// 檢查是否有 pixel 欄位
|
|
832
|
+
const hasPixelColumns = currentWidths.some((width) => width.type === 'pixel');
|
|
833
|
+
// 如果沒有 pixel 欄位,表示已經是純百分比模式,直接返回
|
|
834
|
+
if (!hasPixelColumns) {
|
|
835
|
+
return currentWidths;
|
|
836
|
+
}
|
|
837
|
+
// 使用 distributeEqualPercentages 重新分配百分比,確保總和為 100%
|
|
838
|
+
const percentages = distributeEqualPercentages(currentWidths.length);
|
|
839
|
+
return percentages.map((value) => ({ type: 'percentage', value }));
|
|
840
|
+
}
|
|
733
841
|
|
|
734
842
|
/** 檢查 table cell 是否在 focused 狀態 */
|
|
735
843
|
function useTableCellFocused(element, editor) {
|
|
@@ -893,10 +1001,10 @@ function useTableCellAlignStatus(tableElement, editor) {
|
|
|
893
1001
|
function useTableCellToolbarActions({ element, cellPosition, isHeader, transformCellContent, }) {
|
|
894
1002
|
const editor = slateReact.useSlateStatic();
|
|
895
1003
|
const { tableSelectedOn, setTableSelectedOn } = useTableStateContext();
|
|
896
|
-
const { tableElement, isReachMaximumColumns, isReachMinimumNormalColumns, isReachMinimumBodyRows, isColumnPinned, isRowPinned, } = useTableMetadata();
|
|
1004
|
+
const { tableElement, columnCount, rowCount, isReachMaximumColumns, isReachMinimumNormalColumns, isReachMinimumBodyRows, isColumnPinned, isRowPinned, } = useTableMetadata();
|
|
897
1005
|
const setAlign = useTableCellAlign(tableElement, editor);
|
|
898
1006
|
const getAlign = useTableCellAlignStatus(tableElement, editor);
|
|
899
|
-
const { deleteRow, deleteColumn, addRow, addColumn, moveRowToBody, moveRowToHeader, unsetColumnAsTitle, setColumnAsTitle, pinColumn, unpinColumn, pinRow, unpinRow, } = useTableActionsContext();
|
|
1007
|
+
const { deleteRow, deleteColumn, addRow, addColumn, moveRowToBody, moveRowToHeader, unsetColumnAsTitle, setColumnAsTitle, pinColumn, unpinColumn, pinRow, unpinRow, moveOrSwapRow, moveOrSwapColumn, } = useTableActionsContext();
|
|
900
1008
|
// 獲取當前 cell 內容的類型(用於顯示對應的 icon)
|
|
901
1009
|
const getCurrentContentType = React.useMemo(() => {
|
|
902
1010
|
// 檢查 cell 的第一個 child 的類型
|
|
@@ -1237,6 +1345,116 @@ function useTableCellToolbarActions({ element, cellPosition, isHeader, transform
|
|
|
1237
1345
|
},
|
|
1238
1346
|
].filter((i) => i !== undefined);
|
|
1239
1347
|
}, [tableSelectedOn, addColumn, setTableSelectedOn, isReachMaximumColumns]);
|
|
1348
|
+
// Row move actions
|
|
1349
|
+
const rowMoveActions = React.useMemo(() => {
|
|
1350
|
+
if ((tableSelectedOn === null || tableSelectedOn === void 0 ? void 0 : tableSelectedOn.region) !== 'row' || typeof tableSelectedOn.index !== 'number') {
|
|
1351
|
+
return null;
|
|
1352
|
+
}
|
|
1353
|
+
const rowIndex = tableSelectedOn.index;
|
|
1354
|
+
// 從 tableElement 獲取 header 和 body 的資訊
|
|
1355
|
+
const tableMainElement = tableElement.children.find((child) => core.Element.isElement(child) && child.type.includes('table_main'));
|
|
1356
|
+
if (!tableMainElement || !core.Element.isElement(tableMainElement)) {
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
const tableHeaderElement = tableMainElement.children.find((child) => core.Element.isElement(child) && child.type.includes('table_header'));
|
|
1360
|
+
const headerRowCount = tableHeaderElement && core.Element.isElement(tableHeaderElement) ? tableHeaderElement.children.length : 0;
|
|
1361
|
+
// 判斷當前列是在 header 還是 body 中
|
|
1362
|
+
const currentInHeader = rowIndex < headerRowCount;
|
|
1363
|
+
// 判斷上方和下方是否可以互換
|
|
1364
|
+
const canMoveUp = rowIndex > 0; // 不在第一列
|
|
1365
|
+
const canMoveDown = rowIndex < rowCount - 1; // 不在最後一列
|
|
1366
|
+
// 檢查目標位置是否在相同的容器中(header 或 body)
|
|
1367
|
+
const targetUpInHeader = rowIndex - 1 < headerRowCount;
|
|
1368
|
+
const targetDownInHeader = rowIndex + 1 < headerRowCount;
|
|
1369
|
+
// 標題列只能與標題列互換,一般列只能與一般列互換
|
|
1370
|
+
const canSwapUp = canMoveUp && currentInHeader === targetUpInHeader;
|
|
1371
|
+
const canSwapDown = canMoveDown && currentInHeader === targetDownInHeader;
|
|
1372
|
+
return [
|
|
1373
|
+
canSwapDown
|
|
1374
|
+
? {
|
|
1375
|
+
icon: icons.TableMoveDown,
|
|
1376
|
+
onClick: () => {
|
|
1377
|
+
if (typeof tableSelectedOn.index === 'number') {
|
|
1378
|
+
moveOrSwapRow(tableSelectedOn.index, tableSelectedOn.index + 1, 'swap');
|
|
1379
|
+
setTableSelectedOn(undefined);
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
}
|
|
1383
|
+
: undefined,
|
|
1384
|
+
canSwapUp
|
|
1385
|
+
? {
|
|
1386
|
+
icon: icons.TableMoveUp,
|
|
1387
|
+
onClick: () => {
|
|
1388
|
+
if (typeof tableSelectedOn.index === 'number') {
|
|
1389
|
+
moveOrSwapRow(tableSelectedOn.index, tableSelectedOn.index - 1, 'swap');
|
|
1390
|
+
setTableSelectedOn(undefined);
|
|
1391
|
+
}
|
|
1392
|
+
},
|
|
1393
|
+
}
|
|
1394
|
+
: undefined,
|
|
1395
|
+
].filter((i) => i !== undefined);
|
|
1396
|
+
}, [tableSelectedOn, setTableSelectedOn, rowCount, tableElement, moveOrSwapRow]);
|
|
1397
|
+
// Column move actions
|
|
1398
|
+
const columnMoveActions = React.useMemo(() => {
|
|
1399
|
+
if ((tableSelectedOn === null || tableSelectedOn === void 0 ? void 0 : tableSelectedOn.region) !== 'column' || typeof tableSelectedOn.index !== 'number') {
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
const columnIndex = tableSelectedOn.index;
|
|
1403
|
+
// 檢查當前行是否為標題行
|
|
1404
|
+
const currentIsTitle = !!element.treatAsTitle;
|
|
1405
|
+
// 判斷左右是否可以移動
|
|
1406
|
+
const canMoveLeft = columnIndex > 0;
|
|
1407
|
+
const canMoveRight = columnIndex < columnCount - 1;
|
|
1408
|
+
// 檢查相鄰行是否為相同類型(都是標題行或都不是標題行)
|
|
1409
|
+
const checkAdjacentColumnType = (adjacentIndex) => {
|
|
1410
|
+
// 從 tableElement 中獲取相鄰行的 treatAsTitle 狀態
|
|
1411
|
+
const tableMainElement = tableElement.children.find((child) => core.Element.isElement(child) && child.type.includes('table_main'));
|
|
1412
|
+
if (!tableMainElement || !core.Element.isElement(tableMainElement)) {
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
const tableBodyElement = tableMainElement.children.find((child) => core.Element.isElement(child) && child.type.includes('table_body'));
|
|
1416
|
+
if (!tableBodyElement || !core.Element.isElement(tableBodyElement)) {
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
// 從 body 的第一列獲取相鄰 cell 的 treatAsTitle 屬性
|
|
1420
|
+
const firstRow = tableBodyElement.children[0];
|
|
1421
|
+
if (!core.Element.isElement(firstRow)) {
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
const adjacentCell = firstRow.children[adjacentIndex];
|
|
1425
|
+
if (!core.Element.isElement(adjacentCell)) {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
return !!adjacentCell.treatAsTitle;
|
|
1429
|
+
};
|
|
1430
|
+
// 標題行只能與標題行互換,一般行只能與一般行互換
|
|
1431
|
+
const canSwapLeft = canMoveLeft && currentIsTitle === checkAdjacentColumnType(columnIndex - 1);
|
|
1432
|
+
const canSwapRight = canMoveRight && currentIsTitle === checkAdjacentColumnType(columnIndex + 1);
|
|
1433
|
+
return [
|
|
1434
|
+
canSwapRight
|
|
1435
|
+
? {
|
|
1436
|
+
icon: icons.TableMoveRight,
|
|
1437
|
+
onClick: () => {
|
|
1438
|
+
if (typeof tableSelectedOn.index === 'number') {
|
|
1439
|
+
moveOrSwapColumn(tableSelectedOn.index, tableSelectedOn.index + 1, 'swap');
|
|
1440
|
+
setTableSelectedOn(undefined);
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
}
|
|
1444
|
+
: undefined,
|
|
1445
|
+
canSwapLeft
|
|
1446
|
+
? {
|
|
1447
|
+
icon: icons.TableMoveLeft,
|
|
1448
|
+
onClick: () => {
|
|
1449
|
+
if (typeof tableSelectedOn.index === 'number') {
|
|
1450
|
+
moveOrSwapColumn(tableSelectedOn.index, tableSelectedOn.index - 1, 'swap');
|
|
1451
|
+
setTableSelectedOn(undefined);
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
}
|
|
1455
|
+
: undefined,
|
|
1456
|
+
].filter((i) => i !== undefined);
|
|
1457
|
+
}, [tableSelectedOn, setTableSelectedOn, columnCount, element.treatAsTitle, tableElement, moveOrSwapColumn]);
|
|
1240
1458
|
// Delete actions
|
|
1241
1459
|
const deleteActions = React.useMemo(() => {
|
|
1242
1460
|
return [
|
|
@@ -1268,17 +1486,52 @@ function useTableCellToolbarActions({ element, cellPosition, isHeader, transform
|
|
|
1268
1486
|
{
|
|
1269
1487
|
icons: rowAddActions || columnAddActions || [],
|
|
1270
1488
|
},
|
|
1489
|
+
{
|
|
1490
|
+
icons: rowMoveActions || columnMoveActions || [],
|
|
1491
|
+
},
|
|
1271
1492
|
{
|
|
1272
1493
|
icons: deleteActions,
|
|
1273
1494
|
},
|
|
1274
1495
|
];
|
|
1275
|
-
}, [
|
|
1496
|
+
}, [
|
|
1497
|
+
rowActions,
|
|
1498
|
+
columnActions,
|
|
1499
|
+
columnAlignActions,
|
|
1500
|
+
rowAddActions,
|
|
1501
|
+
columnAddActions,
|
|
1502
|
+
rowMoveActions,
|
|
1503
|
+
columnMoveActions,
|
|
1504
|
+
deleteActions,
|
|
1505
|
+
]);
|
|
1276
1506
|
return {
|
|
1277
1507
|
focusToolbarIconGroups,
|
|
1278
1508
|
inlineToolbarIconGroups,
|
|
1279
1509
|
};
|
|
1280
1510
|
}
|
|
1281
1511
|
|
|
1512
|
+
const TableDragContext = React.createContext(null);
|
|
1513
|
+
const TableDragProvider = ({ children }) => {
|
|
1514
|
+
const [dragState, setDragState] = React.useState(null);
|
|
1515
|
+
const [dropTargetIndex, setDropTargetIndex] = React.useState(null);
|
|
1516
|
+
const [dragDirection, setDragDirection] = React.useState(null);
|
|
1517
|
+
const value = React.useMemo(() => ({
|
|
1518
|
+
dragState,
|
|
1519
|
+
setDragState,
|
|
1520
|
+
dropTargetIndex,
|
|
1521
|
+
setDropTargetIndex,
|
|
1522
|
+
dragDirection,
|
|
1523
|
+
setDragDirection,
|
|
1524
|
+
}), [dragState, dropTargetIndex, dragDirection]);
|
|
1525
|
+
return React.createElement(TableDragContext.Provider, { value: value }, children);
|
|
1526
|
+
};
|
|
1527
|
+
const useTableDragContext = () => {
|
|
1528
|
+
const context = React.useContext(TableDragContext);
|
|
1529
|
+
if (!context) {
|
|
1530
|
+
throw new Error('useTableDragContext must be used within TableDragProvider');
|
|
1531
|
+
}
|
|
1532
|
+
return context;
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1282
1535
|
function useTableActions(element) {
|
|
1283
1536
|
const editor = react.useQuadrats();
|
|
1284
1537
|
const isColumnPinned = React.useCallback((columnIndex) => {
|
|
@@ -1394,7 +1647,9 @@ function useTableActions(element) {
|
|
|
1394
1647
|
// 調整欄位寬度
|
|
1395
1648
|
const currentWidths = getColumnWidths(element);
|
|
1396
1649
|
if (currentWidths.length > 0) {
|
|
1397
|
-
|
|
1650
|
+
// 獲取當前的 pinned columns 資訊
|
|
1651
|
+
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
1652
|
+
const newWidths = calculateColumnWidthsAfterAdd(currentWidths, insertIndex, pinnedColumnIndices, columnIndex);
|
|
1398
1653
|
setColumnWidths(editor, element, newWidths);
|
|
1399
1654
|
}
|
|
1400
1655
|
});
|
|
@@ -1530,7 +1785,9 @@ function useTableActions(element) {
|
|
|
1530
1785
|
if (currentWidths.length > 0) {
|
|
1531
1786
|
// 新欄位插入在最後(columnCount 位置)
|
|
1532
1787
|
const insertIndex = columnCount;
|
|
1533
|
-
|
|
1788
|
+
// 獲取當前的 pinned columns 資訊
|
|
1789
|
+
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
1790
|
+
const newWidths = calculateColumnWidthsAfterAdd(currentWidths, insertIndex, pinnedColumnIndices);
|
|
1534
1791
|
setColumnWidths(editor, element, newWidths);
|
|
1535
1792
|
}
|
|
1536
1793
|
});
|
|
@@ -1795,7 +2052,7 @@ function useTableActions(element) {
|
|
|
1795
2052
|
// 調整 columnWidths:將 columnIndex 的寬度移動到 actualTargetIndex
|
|
1796
2053
|
const currentWidths = getColumnWidths(element);
|
|
1797
2054
|
if (currentWidths.length > 0) {
|
|
1798
|
-
const movedWidths =
|
|
2055
|
+
const movedWidths = moveOrSwapColumnWidth(currentWidths, columnIndex, actualTargetIndex, 'move');
|
|
1799
2056
|
// 檢查移動後是否還有 pinned columns
|
|
1800
2057
|
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
1801
2058
|
// 更新釘選欄位索引(移除當前欄位,並調整其他欄位的索引)
|
|
@@ -1853,7 +2110,7 @@ function useTableActions(element) {
|
|
|
1853
2110
|
if (!tableStructure)
|
|
1854
2111
|
return;
|
|
1855
2112
|
const { tableHeaderElement, tableBodyElement, tableMainElement } = tableStructure;
|
|
1856
|
-
// 檢查是否已有 pinned columns
|
|
2113
|
+
// 檢查是否已有 pinned columns
|
|
1857
2114
|
const hasExistingPinnedColumns = hasAnyPinnedColumns(tableStructure);
|
|
1858
2115
|
// 如果有現有的 pinned columns 且沒有提供自定義屬性,自動設置 pinned 以保持一致性
|
|
1859
2116
|
const finalProps = customProps || (hasExistingPinnedColumns ? { pinned: true } : undefined);
|
|
@@ -1900,10 +2157,9 @@ function useTableActions(element) {
|
|
|
1900
2157
|
});
|
|
1901
2158
|
}
|
|
1902
2159
|
});
|
|
1903
|
-
//
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
if (columnIndex !== targetColumnIndex) {
|
|
2160
|
+
// 檢查是否需要移動位置
|
|
2161
|
+
const needsMove = columnIndex >= targetColumnIndex && columnIndex !== targetColumnIndex;
|
|
2162
|
+
if (needsMove) {
|
|
1907
2163
|
for (let rowIndex = containerElement.children.length - 1; rowIndex >= 0; rowIndex--) {
|
|
1908
2164
|
const row = containerElement.children[rowIndex];
|
|
1909
2165
|
if (core.Element.isElement(row) && row.type.includes(table.TABLE_ROW_TYPE)) {
|
|
@@ -1918,7 +2174,7 @@ function useTableActions(element) {
|
|
|
1918
2174
|
// 調整 columnWidths:將 columnIndex 的寬度移動到 targetColumnIndex
|
|
1919
2175
|
const currentWidths = getColumnWidths(element);
|
|
1920
2176
|
if (currentWidths.length > 0) {
|
|
1921
|
-
const movedWidths =
|
|
2177
|
+
const movedWidths = moveOrSwapColumnWidth(currentWidths, columnIndex, targetColumnIndex, 'move');
|
|
1922
2178
|
// 如果設定了 pinned,需要轉換為混合模式
|
|
1923
2179
|
if ((finalProps === null || finalProps === void 0 ? void 0 : finalProps.pinned) && tableWidth > 0) {
|
|
1924
2180
|
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
@@ -1947,7 +2203,23 @@ function useTableActions(element) {
|
|
|
1947
2203
|
const currentWidths = getColumnWidths(element);
|
|
1948
2204
|
if (currentWidths.length > 0) {
|
|
1949
2205
|
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
1950
|
-
|
|
2206
|
+
// 找出所有已經是 title 的 columns
|
|
2207
|
+
const titleColumnIndices = new Set();
|
|
2208
|
+
const firstRow = containerElement.children[0];
|
|
2209
|
+
if (core.Element.isElement(firstRow) && firstRow.type.includes(table.TABLE_ROW_TYPE)) {
|
|
2210
|
+
firstRow.children.forEach((cell, colIndex) => {
|
|
2211
|
+
if (core.Element.isElement(cell) &&
|
|
2212
|
+
cell.type.includes(table.TABLE_CELL_TYPE) &&
|
|
2213
|
+
cell.treatAsTitle) {
|
|
2214
|
+
titleColumnIndices.add(colIndex);
|
|
2215
|
+
}
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
// 將當前 column 加入 title columns
|
|
2219
|
+
titleColumnIndices.add(columnIndex);
|
|
2220
|
+
// 合併所有 pinned columns 和 title columns
|
|
2221
|
+
const allPinnedIndices = new Set([...pinnedColumnIndices, ...Array.from(titleColumnIndices)]);
|
|
2222
|
+
const updatedPinnedIndices = Array.from(allPinnedIndices).sort((a, b) => a - b);
|
|
1951
2223
|
const mixedWidths = convertToMixedWidthMode(currentWidths, updatedPinnedIndices, tableWidth);
|
|
1952
2224
|
setColumnWidths(editor, element, mixedWidths);
|
|
1953
2225
|
}
|
|
@@ -2027,6 +2299,10 @@ function useTableActions(element) {
|
|
|
2027
2299
|
if (tableBodyElement) {
|
|
2028
2300
|
processContainer(tableBodyElement);
|
|
2029
2301
|
}
|
|
2302
|
+
// 轉換回純百分比模式
|
|
2303
|
+
const currentWidths = getColumnWidths(element);
|
|
2304
|
+
const percentageWidths = convertToPercentageMode(currentWidths);
|
|
2305
|
+
setColumnWidths(editor, element, percentageWidths);
|
|
2030
2306
|
}
|
|
2031
2307
|
catch (error) {
|
|
2032
2308
|
console.warn('Failed to unpin column:', error);
|
|
@@ -2138,6 +2414,187 @@ function useTableActions(element) {
|
|
|
2138
2414
|
console.warn('Failed to unpin row:', error);
|
|
2139
2415
|
}
|
|
2140
2416
|
}, [editor, element]);
|
|
2417
|
+
/**
|
|
2418
|
+
* 內部函數:移動或交換列的位置
|
|
2419
|
+
* @param mode 'swap' 為交換相鄰位置(toolbar 按鈕),'move' 為移動到任意位置(拖曳)
|
|
2420
|
+
*/
|
|
2421
|
+
const moveOrSwapRow = React.useCallback((sourceRowIndex, targetRowIndex, mode = 'move') => {
|
|
2422
|
+
try {
|
|
2423
|
+
const tableStructure = getTableStructure(editor, element);
|
|
2424
|
+
if (!tableStructure)
|
|
2425
|
+
return;
|
|
2426
|
+
const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, headerRowCount } = tableStructure;
|
|
2427
|
+
// 確定當前列和目標列所屬的容器
|
|
2428
|
+
const sourceInHeader = sourceRowIndex < headerRowCount;
|
|
2429
|
+
const targetInHeader = targetRowIndex < headerRowCount;
|
|
2430
|
+
// 標題列只能與標題列互換/移動,一般列只能與一般列互換/移動
|
|
2431
|
+
if (sourceInHeader !== targetInHeader) {
|
|
2432
|
+
console.warn(`Cannot ${mode} row between header and body`);
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
// 檢查邊界
|
|
2436
|
+
if (sourceRowIndex === targetRowIndex) {
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
let containerPath;
|
|
2440
|
+
let sourceLocalIndex;
|
|
2441
|
+
let targetLocalIndex;
|
|
2442
|
+
if (sourceInHeader) {
|
|
2443
|
+
// 在 header 中
|
|
2444
|
+
if (!tableHeaderElement || !tableHeaderPath)
|
|
2445
|
+
return;
|
|
2446
|
+
containerPath = tableHeaderPath;
|
|
2447
|
+
sourceLocalIndex = sourceRowIndex;
|
|
2448
|
+
targetLocalIndex = targetRowIndex;
|
|
2449
|
+
}
|
|
2450
|
+
else {
|
|
2451
|
+
// 在 body 中
|
|
2452
|
+
if (!tableBodyElement)
|
|
2453
|
+
return;
|
|
2454
|
+
containerPath = tableBodyPath;
|
|
2455
|
+
sourceLocalIndex = sourceRowIndex - headerRowCount;
|
|
2456
|
+
targetLocalIndex = targetRowIndex - headerRowCount;
|
|
2457
|
+
}
|
|
2458
|
+
core.Editor.withoutNormalizing(editor, () => {
|
|
2459
|
+
if (mode === 'swap') {
|
|
2460
|
+
// swap 邏輯:交換兩個相鄰位置
|
|
2461
|
+
if (sourceRowIndex < targetRowIndex) {
|
|
2462
|
+
// 向下移動:先將源列移到目標位置之後
|
|
2463
|
+
const sourcePath = [...containerPath, sourceLocalIndex];
|
|
2464
|
+
const afterTargetPath = [...containerPath, targetLocalIndex];
|
|
2465
|
+
core.Transforms.moveNodes(editor, {
|
|
2466
|
+
at: sourcePath,
|
|
2467
|
+
to: afterTargetPath,
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
else {
|
|
2471
|
+
// 向上移動:先將目標列移到源位置之後
|
|
2472
|
+
const targetPath = [...containerPath, targetLocalIndex];
|
|
2473
|
+
const afterSourcePath = [...containerPath, sourceLocalIndex];
|
|
2474
|
+
core.Transforms.moveNodes(editor, {
|
|
2475
|
+
at: targetPath,
|
|
2476
|
+
to: afterSourcePath,
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
else {
|
|
2481
|
+
// move 邏輯:直接移動到目標位置
|
|
2482
|
+
const sourcePath = [...containerPath, sourceLocalIndex];
|
|
2483
|
+
const targetPath = [...containerPath, targetLocalIndex];
|
|
2484
|
+
core.Transforms.moveNodes(editor, {
|
|
2485
|
+
at: sourcePath,
|
|
2486
|
+
to: targetPath,
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
catch (error) {
|
|
2492
|
+
console.warn(`Failed to ${mode} row:`, error);
|
|
2493
|
+
}
|
|
2494
|
+
}, [editor, element]);
|
|
2495
|
+
/**
|
|
2496
|
+
* 內部函數:移動或交換行的位置
|
|
2497
|
+
* @param mode 'swap' 為交換相鄰位置(toolbar 按鈕),'move' 為移動到任意位置(拖曳)
|
|
2498
|
+
*/
|
|
2499
|
+
const moveOrSwapColumn = React.useCallback((sourceColumnIndex, targetColumnIndex, mode = 'move') => {
|
|
2500
|
+
try {
|
|
2501
|
+
const tableStructure = getTableStructure(editor, element);
|
|
2502
|
+
if (!tableStructure)
|
|
2503
|
+
return;
|
|
2504
|
+
const { tableHeaderElement, tableBodyElement, columnCount } = tableStructure;
|
|
2505
|
+
// 檢查邊界
|
|
2506
|
+
if (targetColumnIndex < 0 || targetColumnIndex >= columnCount) {
|
|
2507
|
+
console.warn('Target column index out of bounds');
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
// 檢查是否為同一行
|
|
2511
|
+
if (sourceColumnIndex === targetColumnIndex) {
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
// 檢查當前行和目標行是否都是標題行或都是一般行
|
|
2515
|
+
// 透過檢查第一個 cell 的 treatAsTitle 屬性來判斷
|
|
2516
|
+
const checkIsTitleColumn = (container, colIndex) => {
|
|
2517
|
+
if (!core.Element.isElement(container))
|
|
2518
|
+
return false;
|
|
2519
|
+
for (const row of container.children) {
|
|
2520
|
+
if (core.Element.isElement(row) && row.type.includes(table.TABLE_ROW_TYPE)) {
|
|
2521
|
+
const cell = row.children[colIndex];
|
|
2522
|
+
if (core.Element.isElement(cell) && cell.type.includes(table.TABLE_CELL_TYPE)) {
|
|
2523
|
+
return !!cell.treatAsTitle;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
return false;
|
|
2528
|
+
};
|
|
2529
|
+
// 檢查兩個 container 中的第一列來確定是否為標題行
|
|
2530
|
+
let sourceIsTitle = false;
|
|
2531
|
+
let targetIsTitle = false;
|
|
2532
|
+
if (tableHeaderElement) {
|
|
2533
|
+
sourceIsTitle = sourceIsTitle || checkIsTitleColumn(tableHeaderElement, sourceColumnIndex);
|
|
2534
|
+
targetIsTitle = targetIsTitle || checkIsTitleColumn(tableHeaderElement, targetColumnIndex);
|
|
2535
|
+
}
|
|
2536
|
+
if (tableBodyElement) {
|
|
2537
|
+
sourceIsTitle = sourceIsTitle || checkIsTitleColumn(tableBodyElement, sourceColumnIndex);
|
|
2538
|
+
targetIsTitle = targetIsTitle || checkIsTitleColumn(tableBodyElement, targetColumnIndex);
|
|
2539
|
+
}
|
|
2540
|
+
// 標題行只能與標題行互換/移動,一般行只能與一般行互換/移動
|
|
2541
|
+
if (sourceIsTitle !== targetIsTitle) {
|
|
2542
|
+
console.warn(`Cannot ${mode} column between title and normal columns`);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
// 根據模式選擇不同的 columnWidths 處理方式
|
|
2546
|
+
const currentWidths = getColumnWidths(element);
|
|
2547
|
+
const newWidths = moveOrSwapColumnWidth(currentWidths, sourceColumnIndex, targetColumnIndex, mode);
|
|
2548
|
+
setColumnWidths(editor, element, newWidths);
|
|
2549
|
+
// 對 header 和 body 中的所有列進行操作
|
|
2550
|
+
core.Editor.withoutNormalizing(editor, () => {
|
|
2551
|
+
const containers = [tableHeaderElement, tableBodyElement].filter((c) => c && core.Element.isElement(c));
|
|
2552
|
+
for (const container of containers) {
|
|
2553
|
+
// 對每一列進行操作
|
|
2554
|
+
for (let rowIndex = 0; rowIndex < container.children.length; rowIndex++) {
|
|
2555
|
+
const row = container.children[rowIndex];
|
|
2556
|
+
if (!core.Element.isElement(row) || !row.type.includes(table.TABLE_ROW_TYPE))
|
|
2557
|
+
continue;
|
|
2558
|
+
const containerPath = slateReact.ReactEditor.findPath(editor, container);
|
|
2559
|
+
const rowPath = [...containerPath, rowIndex];
|
|
2560
|
+
if (mode === 'swap') {
|
|
2561
|
+
// swap 邏輯:交換兩個相鄰位置
|
|
2562
|
+
if (sourceColumnIndex < targetColumnIndex) {
|
|
2563
|
+
// 向右移動:將源 cell 移到目標位置之後
|
|
2564
|
+
const sourceCellPath = [...rowPath, sourceColumnIndex];
|
|
2565
|
+
const afterTargetCellPath = [...rowPath, targetColumnIndex];
|
|
2566
|
+
core.Transforms.moveNodes(editor, {
|
|
2567
|
+
at: sourceCellPath,
|
|
2568
|
+
to: afterTargetCellPath,
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
else {
|
|
2572
|
+
// 向左移動:將目標 cell 移到源位置之後
|
|
2573
|
+
const targetCellPath = [...rowPath, targetColumnIndex];
|
|
2574
|
+
const afterSourceCellPath = [...rowPath, sourceColumnIndex];
|
|
2575
|
+
core.Transforms.moveNodes(editor, {
|
|
2576
|
+
at: targetCellPath,
|
|
2577
|
+
to: afterSourceCellPath,
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
else {
|
|
2582
|
+
// move 邏輯:直接移動到目標位置
|
|
2583
|
+
const sourceCellPath = [...rowPath, sourceColumnIndex];
|
|
2584
|
+
const targetCellPath = [...rowPath, targetColumnIndex];
|
|
2585
|
+
core.Transforms.moveNodes(editor, {
|
|
2586
|
+
at: sourceCellPath,
|
|
2587
|
+
to: targetCellPath,
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
catch (error) {
|
|
2595
|
+
console.warn(`Failed to ${mode} column:`, error);
|
|
2596
|
+
}
|
|
2597
|
+
}, [editor, element]);
|
|
2141
2598
|
return {
|
|
2142
2599
|
addColumn,
|
|
2143
2600
|
addRow,
|
|
@@ -2154,6 +2611,8 @@ function useTableActions(element) {
|
|
|
2154
2611
|
unpinRow,
|
|
2155
2612
|
isColumnPinned,
|
|
2156
2613
|
isRowPinned,
|
|
2614
|
+
moveOrSwapRow,
|
|
2615
|
+
moveOrSwapColumn,
|
|
2157
2616
|
};
|
|
2158
2617
|
}
|
|
2159
2618
|
|
|
@@ -2169,10 +2628,9 @@ function useTableStates() {
|
|
|
2169
2628
|
}
|
|
2170
2629
|
|
|
2171
2630
|
function Table({ attributes, children, element, }) {
|
|
2172
|
-
const { addColumn, addRow, addColumnAndRow, deleteRow, deleteColumn, moveRowToBody, moveRowToHeader, unsetColumnAsTitle, setColumnAsTitle, pinColumn, pinRow, unpinColumn, unpinRow, } = useTableActions(element);
|
|
2631
|
+
const { addColumn, addRow, addColumnAndRow, deleteRow, deleteColumn, moveRowToBody, moveRowToHeader, unsetColumnAsTitle, setColumnAsTitle, pinColumn, pinRow, unpinColumn, unpinRow, moveOrSwapRow, moveOrSwapColumn, } = useTableActions(element);
|
|
2173
2632
|
const { tableSelectedOn, setTableSelectedOn, tableHoveredOn, setTableHoveredOn } = useTableStates();
|
|
2174
2633
|
const portalContainerRef = React.useRef(null);
|
|
2175
|
-
// 優化表格結構計算 - 使用 getTableElements 重用邏輯
|
|
2176
2634
|
const { columnCount, rowCount, normalCols, bodyCount, tableElements } = React.useMemo(() => {
|
|
2177
2635
|
const elements = getTableElements(element);
|
|
2178
2636
|
if (!elements.tableMainElement) {
|
|
@@ -2319,6 +2777,8 @@ function Table({ attributes, children, element, }) {
|
|
|
2319
2777
|
pinRow,
|
|
2320
2778
|
unpinColumn,
|
|
2321
2779
|
unpinRow,
|
|
2780
|
+
moveOrSwapRow,
|
|
2781
|
+
moveOrSwapColumn,
|
|
2322
2782
|
}), [
|
|
2323
2783
|
addColumn,
|
|
2324
2784
|
addRow,
|
|
@@ -2333,6 +2793,8 @@ function Table({ attributes, children, element, }) {
|
|
|
2333
2793
|
pinRow,
|
|
2334
2794
|
unpinColumn,
|
|
2335
2795
|
unpinRow,
|
|
2796
|
+
moveOrSwapRow,
|
|
2797
|
+
moveOrSwapColumn,
|
|
2336
2798
|
]);
|
|
2337
2799
|
const metadataValue = React.useMemo(() => ({
|
|
2338
2800
|
tableElement: element,
|
|
@@ -2369,21 +2831,22 @@ function Table({ attributes, children, element, }) {
|
|
|
2369
2831
|
tableHoveredOn,
|
|
2370
2832
|
setTableHoveredOn,
|
|
2371
2833
|
}), [tableSelectedOn, setTableSelectedOn, tableHoveredOn, setTableHoveredOn]);
|
|
2372
|
-
return (React.createElement(
|
|
2373
|
-
React.createElement(
|
|
2374
|
-
React.createElement(
|
|
2375
|
-
React.createElement(
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
evt
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2834
|
+
return (React.createElement(TableDragProvider, null,
|
|
2835
|
+
React.createElement(TableActionsContext.Provider, { value: actionsValue },
|
|
2836
|
+
React.createElement(TableMetadataContext.Provider, { value: metadataValue },
|
|
2837
|
+
React.createElement(TableStateContext.Provider, { value: stateValue },
|
|
2838
|
+
React.createElement("div", Object.assign({}, attributes, { className: "qdr-table" }),
|
|
2839
|
+
children,
|
|
2840
|
+
React.createElement("button", { type: "button", onClick: (evt) => {
|
|
2841
|
+
evt.preventDefault();
|
|
2842
|
+
evt.stopPropagation();
|
|
2843
|
+
setTableSelectedOn((prev) => ((prev === null || prev === void 0 ? void 0 : prev.region) === 'table' ? undefined : { region: 'table' }));
|
|
2844
|
+
}, onMouseDown: (evt) => {
|
|
2845
|
+
evt.preventDefault();
|
|
2846
|
+
evt.stopPropagation();
|
|
2847
|
+
}, className: "qdr-table__selection", title: (tableSelectedOn === null || tableSelectedOn === void 0 ? void 0 : tableSelectedOn.region) === 'table' ? 'Deselect Table' : 'Select Table' },
|
|
2848
|
+
React.createElement(components.Icon, { icon: icons.Drag, width: 20, height: 20 })),
|
|
2849
|
+
React.createElement("div", { ref: portalContainerRef, className: "qdr-table__portal-container", "data-slate-editor": false, contentEditable: false })))))));
|
|
2387
2850
|
}
|
|
2388
2851
|
|
|
2389
2852
|
function TableTitle(props) {
|
|
@@ -2405,6 +2868,90 @@ const TableScrollContext = React.createContext({
|
|
|
2405
2868
|
scrollLeft: 0,
|
|
2406
2869
|
});
|
|
2407
2870
|
|
|
2871
|
+
const TableDragLayer = ({ scrollRef }) => {
|
|
2872
|
+
const { dragState } = useTableDragContext();
|
|
2873
|
+
const [columnWidths, setColumnWidths] = React.useState([]);
|
|
2874
|
+
const [rowHeights, setRowHeights] = React.useState([]);
|
|
2875
|
+
const { isDragging, currentOffset } = reactDnd.useDragLayer((monitor) => ({
|
|
2876
|
+
isDragging: monitor.isDragging(),
|
|
2877
|
+
currentOffset: monitor.getClientOffset(),
|
|
2878
|
+
}));
|
|
2879
|
+
// 計算所有 column 的寬度和 row 的高度
|
|
2880
|
+
React.useEffect(() => {
|
|
2881
|
+
if (!scrollRef.current || !isDragging)
|
|
2882
|
+
return;
|
|
2883
|
+
const tableContainer = scrollRef.current;
|
|
2884
|
+
const cells = tableContainer.querySelectorAll('.qdr-table__cell');
|
|
2885
|
+
if ((dragState === null || dragState === void 0 ? void 0 : dragState.type) === 'column') {
|
|
2886
|
+
// 計算每個 column 的寬度
|
|
2887
|
+
const widths = [];
|
|
2888
|
+
const columnCells = Array.from(cells).filter((cell) => {
|
|
2889
|
+
const cellElement = cell;
|
|
2890
|
+
return cellElement.dataset.columnIndex !== undefined;
|
|
2891
|
+
});
|
|
2892
|
+
const columnCount = Math.max(...columnCells.map((cell) => {
|
|
2893
|
+
const cellElement = cell;
|
|
2894
|
+
return parseInt(cellElement.dataset.columnIndex || '0', 10);
|
|
2895
|
+
})) + 1;
|
|
2896
|
+
for (let i = 0; i < columnCount; i++) {
|
|
2897
|
+
const cellsInColumn = columnCells.filter((cell) => {
|
|
2898
|
+
const cellElement = cell;
|
|
2899
|
+
return parseInt(cellElement.dataset.columnIndex || '0', 10) === i;
|
|
2900
|
+
});
|
|
2901
|
+
if (cellsInColumn.length > 0) {
|
|
2902
|
+
const firstCell = cellsInColumn[0];
|
|
2903
|
+
widths[i] = firstCell.getBoundingClientRect().width;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
setColumnWidths(widths);
|
|
2907
|
+
}
|
|
2908
|
+
else if ((dragState === null || dragState === void 0 ? void 0 : dragState.type) === 'row') {
|
|
2909
|
+
// 計算每個 row 的高度
|
|
2910
|
+
const heights = [];
|
|
2911
|
+
const rowCells = Array.from(cells).filter((cell) => {
|
|
2912
|
+
const cellElement = cell;
|
|
2913
|
+
return cellElement.dataset.rowIndex !== undefined;
|
|
2914
|
+
});
|
|
2915
|
+
const rowCount = Math.max(...rowCells.map((cell) => {
|
|
2916
|
+
const cellElement = cell;
|
|
2917
|
+
return parseInt(cellElement.dataset.rowIndex || '0', 10);
|
|
2918
|
+
})) + 1;
|
|
2919
|
+
for (let i = 0; i < rowCount; i++) {
|
|
2920
|
+
const cellsInRow = rowCells.filter((cell) => {
|
|
2921
|
+
const cellElement = cell;
|
|
2922
|
+
return parseInt(cellElement.dataset.rowIndex || '0', 10) === i;
|
|
2923
|
+
});
|
|
2924
|
+
if (cellsInRow.length > 0) {
|
|
2925
|
+
const firstCell = cellsInRow[0];
|
|
2926
|
+
heights[i] = firstCell.getBoundingClientRect().height;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
setRowHeights(heights);
|
|
2930
|
+
}
|
|
2931
|
+
}, [isDragging, dragState, scrollRef]);
|
|
2932
|
+
if (!isDragging || !dragState || !currentOffset || !scrollRef.current) {
|
|
2933
|
+
return null;
|
|
2934
|
+
}
|
|
2935
|
+
const tableContainer = scrollRef.current;
|
|
2936
|
+
const tableRect = tableContainer.getBoundingClientRect();
|
|
2937
|
+
if (dragState.type) {
|
|
2938
|
+
const sourceIndex = dragState.type === 'column' ? dragState.columnIndex : dragState.rowIndex;
|
|
2939
|
+
const rowHeight = dragState.type === 'column' ? tableRect.height : rowHeights[sourceIndex];
|
|
2940
|
+
const columnWidth = dragState.type === 'column' ? columnWidths[sourceIndex] : tableRect.width;
|
|
2941
|
+
return (React.createElement("div", { className: "qdr-table__drag-overlay", style: {
|
|
2942
|
+
left: tableRect.left,
|
|
2943
|
+
top: tableRect.top,
|
|
2944
|
+
width: columnWidth,
|
|
2945
|
+
height: rowHeight,
|
|
2946
|
+
transform: dragState.type === 'column'
|
|
2947
|
+
? `translateX(${currentOffset.x - tableRect.left - columnWidth / 2}px)`
|
|
2948
|
+
: `translateY(${currentOffset.y - tableRect.top - rowHeight / 2}px)`,
|
|
2949
|
+
} },
|
|
2950
|
+
React.createElement("div", { className: "qdr-table__drag-overlay-content" })));
|
|
2951
|
+
}
|
|
2952
|
+
return null;
|
|
2953
|
+
};
|
|
2954
|
+
|
|
2408
2955
|
function TableMain(props) {
|
|
2409
2956
|
const { attributes, children } = props;
|
|
2410
2957
|
const { setConfirmModalConfig } = react.useModal();
|
|
@@ -2412,6 +2959,7 @@ function TableMain(props) {
|
|
|
2412
2959
|
const { addColumn, addRow, addColumnAndRow } = useTableActionsContext();
|
|
2413
2960
|
const { isReachMaximumColumns, isReachMaximumRows, tableElement } = useTableMetadata();
|
|
2414
2961
|
const { tableSelectedOn, setTableSelectedOn } = useTableStateContext();
|
|
2962
|
+
const { dragState } = useTableDragContext();
|
|
2415
2963
|
// Table align functions
|
|
2416
2964
|
const setAlign = useTableCellAlign(tableElement, editor);
|
|
2417
2965
|
const getAlign = useTableCellAlignStatus(tableElement, editor);
|
|
@@ -2455,10 +3003,14 @@ function TableMain(props) {
|
|
|
2455
3003
|
const handleScroll = () => {
|
|
2456
3004
|
setScrollTop(scrollContainer.scrollTop);
|
|
2457
3005
|
setScrollLeft(scrollContainer.scrollLeft);
|
|
2458
|
-
//
|
|
3006
|
+
// 如果正在更新滾動位置,不要觸發 Slate 更新
|
|
2459
3007
|
if (isUpdatingScrollRef.current) {
|
|
2460
3008
|
return;
|
|
2461
3009
|
}
|
|
3010
|
+
// 如果正在拖曳,不要觸發 Slate 更新(避免 transform 導致事件遺失)
|
|
3011
|
+
if (dragState) {
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
2462
3014
|
// 使用 debounce 來減少 Slate 更新頻率
|
|
2463
3015
|
if (scrollUpdateTimerRef.current) {
|
|
2464
3016
|
clearTimeout(scrollUpdateTimerRef.current);
|
|
@@ -2481,7 +3033,7 @@ function TableMain(props) {
|
|
|
2481
3033
|
clearTimeout(scrollUpdateTimerRef.current);
|
|
2482
3034
|
}
|
|
2483
3035
|
};
|
|
2484
|
-
}, [editor, tableElement]);
|
|
3036
|
+
}, [editor, tableElement, dragState]);
|
|
2485
3037
|
// 只在 columnWidths 改變時恢復滾動位置
|
|
2486
3038
|
React.useEffect(() => {
|
|
2487
3039
|
const { current: scrollContainer } = scrollRef;
|
|
@@ -2525,7 +3077,7 @@ function TableMain(props) {
|
|
|
2525
3077
|
// 獲取當前 table 的 align 狀態
|
|
2526
3078
|
const currentTableAlign = getAlign('table');
|
|
2527
3079
|
// 根據當前 table align 狀態選擇對應的 icon
|
|
2528
|
-
const getCurrentTableAlignIcon = () => {
|
|
3080
|
+
const getCurrentTableAlignIcon = React.useCallback(() => {
|
|
2529
3081
|
switch (currentTableAlign) {
|
|
2530
3082
|
case 'left':
|
|
2531
3083
|
return icons.AlignLeft;
|
|
@@ -2536,7 +3088,7 @@ function TableMain(props) {
|
|
|
2536
3088
|
default:
|
|
2537
3089
|
return icons.AlignLeft;
|
|
2538
3090
|
}
|
|
2539
|
-
};
|
|
3091
|
+
}, [currentTableAlign]);
|
|
2540
3092
|
return (React.createElement("div", { className: clsx('qdr-table__mainWrapper', {
|
|
2541
3093
|
'qdr-table__mainWrapper--selected': (tableSelectedOn === null || tableSelectedOn === void 0 ? void 0 : tableSelectedOn.region) === 'table',
|
|
2542
3094
|
}) },
|
|
@@ -2598,7 +3150,8 @@ function TableMain(props) {
|
|
|
2598
3150
|
width: table.columnWidthToCSS(width),
|
|
2599
3151
|
minWidth: table.columnWidthToCSS(width),
|
|
2600
3152
|
} })))),
|
|
2601
|
-
children))
|
|
3153
|
+
children)),
|
|
3154
|
+
React.createElement(TableDragLayer, { scrollRef: scrollRef })),
|
|
2602
3155
|
React.createElement("div", { className: "qdr-table__size-indicators" }, firstRowCells === null || firstRowCells === void 0 ? void 0 : firstRowCells.map((cell, colIndex) => (React.createElement("div", { key: colIndex, className: "qdr-table__size-indicator", style: {
|
|
2603
3156
|
width: table.columnWidthToCSS(columnWidths[colIndex]),
|
|
2604
3157
|
minWidth: table.columnWidthToCSS(columnWidths[colIndex]),
|
|
@@ -2706,6 +3259,35 @@ function useColumnResize({ tableElement, columnIndex, cellRef }) {
|
|
|
2706
3259
|
}
|
|
2707
3260
|
});
|
|
2708
3261
|
}
|
|
3262
|
+
// 如果有 pinned columns,重新計算所有 pinned cells 的 left 位置
|
|
3263
|
+
if (pinnedColumnIndices.length > 0) {
|
|
3264
|
+
const allRows = tableDOMElement.querySelectorAll('tr');
|
|
3265
|
+
const scrollContainer = tableDOMElement.closest('.qdr-table__scrollContainer');
|
|
3266
|
+
const containerRect = scrollContainer === null || scrollContainer === void 0 ? void 0 : scrollContainer.getBoundingClientRect();
|
|
3267
|
+
const containerWidth = (containerRect === null || containerRect === void 0 ? void 0 : containerRect.width) || tableWidth;
|
|
3268
|
+
// 為每個 pinned column 計算應該的絕對位置
|
|
3269
|
+
pinnedColumnIndices.forEach((pinnedIndex) => {
|
|
3270
|
+
// 計算此 column 之前所有 pinned columns 的累積寬度
|
|
3271
|
+
let accumulatedLeft = 0;
|
|
3272
|
+
for (let i = 0; i < pinnedIndex; i++) {
|
|
3273
|
+
if (pinnedColumnIndices.includes(i)) {
|
|
3274
|
+
const width = newWidths[i];
|
|
3275
|
+
if (width.type === 'percentage') {
|
|
3276
|
+
const pixelWidth = (containerWidth * width.value) / 100;
|
|
3277
|
+
accumulatedLeft += pixelWidth;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
// 更新該 column 所有 cells 的 left 位置
|
|
3282
|
+
allRows.forEach((row) => {
|
|
3283
|
+
const cells = row.querySelectorAll('td, th');
|
|
3284
|
+
const targetCell = cells[pinnedIndex];
|
|
3285
|
+
if (targetCell && targetCell.classList.contains('qdr-table__cell--pinned')) {
|
|
3286
|
+
targetCell.style.left = `${accumulatedLeft}px`;
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
2709
3291
|
// 更新 size indicators
|
|
2710
3292
|
const sizeIndicatorsContainer = mainWrapper === null || mainWrapper === void 0 ? void 0 : mainWrapper.querySelector('.qdr-table__size-indicators');
|
|
2711
3293
|
if (sizeIndicatorsContainer) {
|
|
@@ -2762,11 +3344,78 @@ function useColumnResize({ tableElement, columnIndex, cellRef }) {
|
|
|
2762
3344
|
};
|
|
2763
3345
|
}
|
|
2764
3346
|
|
|
3347
|
+
const ROW_DRAG_TYPE = 'TABLE_ROW';
|
|
3348
|
+
const RowDragButton = ({ rowIndex, headerRowCount, style, onClick }) => {
|
|
3349
|
+
const buttonRef = React.useRef(null);
|
|
3350
|
+
const { setDragState, setDropTargetIndex, setDragDirection } = useTableDragContext();
|
|
3351
|
+
// 判斷當前 row 是否在 header 中
|
|
3352
|
+
const isInHeader = rowIndex < headerRowCount;
|
|
3353
|
+
const [{ isDragging }, drag, preview] = reactDnd.useDrag(() => ({
|
|
3354
|
+
type: ROW_DRAG_TYPE,
|
|
3355
|
+
item: () => {
|
|
3356
|
+
const dragItem = { rowIndex, isInHeader };
|
|
3357
|
+
setDragState({ type: 'row', rowIndex, isInHeader });
|
|
3358
|
+
return dragItem;
|
|
3359
|
+
},
|
|
3360
|
+
collect: (monitor) => ({
|
|
3361
|
+
isDragging: monitor.isDragging(),
|
|
3362
|
+
}),
|
|
3363
|
+
end: () => {
|
|
3364
|
+
setDragState(null);
|
|
3365
|
+
setDropTargetIndex(null);
|
|
3366
|
+
setDragDirection(null);
|
|
3367
|
+
},
|
|
3368
|
+
}), [rowIndex, isInHeader, setDragState, setDropTargetIndex, setDragDirection]);
|
|
3369
|
+
// 使用空白圖片作為 drag preview,避免顯示預設的拖曳圖像
|
|
3370
|
+
React.useEffect(() => {
|
|
3371
|
+
preview(reactDndHtml5Backend.getEmptyImage(), { captureDraggingState: true });
|
|
3372
|
+
}, [preview]);
|
|
3373
|
+
drag(buttonRef);
|
|
3374
|
+
return (React.createElement("button", { ref: buttonRef, type: "button", contentEditable: false, style: style, onClick: onClick, title: "\u9EDE\u64CA\u958B\u555F\u9078\u55AE\uFF0C\u62D6\u66F3\u4EE5\u79FB\u52D5", className: clsx('qdr-table__cell-row-action', {
|
|
3375
|
+
'qdr-table__cell-row-action--dragging': isDragging,
|
|
3376
|
+
}) },
|
|
3377
|
+
React.createElement(components.Icon, { icon: icons.Drag, width: 20, height: 20 })));
|
|
3378
|
+
};
|
|
3379
|
+
|
|
3380
|
+
const COLUMN_DRAG_TYPE = 'TABLE_COLUMN';
|
|
3381
|
+
const ColumnDragButton = ({ columnIndex, style, onClick, checkIsTitleColumn, }) => {
|
|
3382
|
+
const buttonRef = React.useRef(null);
|
|
3383
|
+
const { setDragState, setDropTargetIndex, setDragDirection } = useTableDragContext();
|
|
3384
|
+
const isTitle = checkIsTitleColumn(columnIndex);
|
|
3385
|
+
const [{ isDragging }, drag, preview] = reactDnd.useDrag(() => ({
|
|
3386
|
+
type: COLUMN_DRAG_TYPE,
|
|
3387
|
+
item: () => {
|
|
3388
|
+
const dragItem = { columnIndex, isTitle };
|
|
3389
|
+
setDragState({ type: 'column', columnIndex, isTitle });
|
|
3390
|
+
return dragItem;
|
|
3391
|
+
},
|
|
3392
|
+
collect: (monitor) => ({
|
|
3393
|
+
isDragging: monitor.isDragging(),
|
|
3394
|
+
}),
|
|
3395
|
+
end: () => {
|
|
3396
|
+
setDragState(null);
|
|
3397
|
+
setDropTargetIndex(null);
|
|
3398
|
+
setDragDirection(null);
|
|
3399
|
+
},
|
|
3400
|
+
}), [columnIndex, isTitle, setDragState, setDropTargetIndex, setDragDirection]);
|
|
3401
|
+
// 使用空白圖片作為 drag preview,避免顯示預設的拖曳圖像
|
|
3402
|
+
React.useEffect(() => {
|
|
3403
|
+
preview(reactDndHtml5Backend.getEmptyImage(), { captureDraggingState: true });
|
|
3404
|
+
}, [preview]);
|
|
3405
|
+
drag(buttonRef);
|
|
3406
|
+
return (React.createElement("button", { ref: buttonRef, type: "button", contentEditable: false, style: style, onClick: onClick, title: "\u9EDE\u64CA\u958B\u555F\u9078\u55AE\uFF0C\u62D6\u66F3\u4EE5\u79FB\u52D5", className: clsx('qdr-table__cell-column-action', {
|
|
3407
|
+
'qdr-table__cell-column-action--dragging': isDragging,
|
|
3408
|
+
}) },
|
|
3409
|
+
React.createElement(components.Icon, { icon: icons.Drag, width: 20, height: 20 })));
|
|
3410
|
+
};
|
|
3411
|
+
|
|
2765
3412
|
function TableCell(props) {
|
|
2766
|
-
var _a;
|
|
3413
|
+
var _a, _b;
|
|
2767
3414
|
const { attributes, children, element } = props;
|
|
2768
3415
|
const { tableSelectedOn, setTableSelectedOn, tableHoveredOn, setTableHoveredOn } = useTableStateContext();
|
|
2769
3416
|
const { columnCount, rowCount, portalContainerRef, isColumnPinned, tableElement } = useTableMetadata();
|
|
3417
|
+
const { moveOrSwapRow, moveOrSwapColumn } = useTableActionsContext();
|
|
3418
|
+
const { dragState, dropTargetIndex, setDropTargetIndex, dragDirection, setDragDirection } = useTableDragContext();
|
|
2770
3419
|
// Component context
|
|
2771
3420
|
const { isHeader } = React.useContext(TableHeaderContext);
|
|
2772
3421
|
const { scrollTop, scrollLeft, scrollRef } = React.useContext(TableScrollContext);
|
|
@@ -2775,6 +3424,20 @@ function TableCell(props) {
|
|
|
2775
3424
|
const focused = useTableCellFocused(element, editor);
|
|
2776
3425
|
const cellPosition = useTableCellPosition(element);
|
|
2777
3426
|
const transformCellContent = useTableCellTransformContent(element, editor);
|
|
3427
|
+
// Get header row count from table structure
|
|
3428
|
+
const tableElements = getTableElements(tableElement);
|
|
3429
|
+
const headerRowCount = ((_a = tableElements.tableHeaderElement) === null || _a === void 0 ? void 0 : _a.children.length) || 0;
|
|
3430
|
+
// Helper to check if column is title column
|
|
3431
|
+
const checkIsTitleColumn = React.useCallback((colIndex) => {
|
|
3432
|
+
const elements = getTableElements(tableElement);
|
|
3433
|
+
if (!elements.tableBodyElement)
|
|
3434
|
+
return false;
|
|
3435
|
+
const firstRow = elements.tableBodyElement.children[0];
|
|
3436
|
+
if (!core.Element.isElement(firstRow))
|
|
3437
|
+
return false;
|
|
3438
|
+
const cell = firstRow.children[colIndex];
|
|
3439
|
+
return core.Element.isElement(cell) && !!cell.treatAsTitle;
|
|
3440
|
+
}, [tableElement]);
|
|
2778
3441
|
// Toolbar actions
|
|
2779
3442
|
const { focusToolbarIconGroups, inlineToolbarIconGroups } = useTableCellToolbarActions({
|
|
2780
3443
|
element,
|
|
@@ -2784,10 +3447,13 @@ function TableCell(props) {
|
|
|
2784
3447
|
});
|
|
2785
3448
|
const isSelectedInSameRow = (tableSelectedOn === null || tableSelectedOn === void 0 ? void 0 : tableSelectedOn.region) === 'row' && (tableSelectedOn === null || tableSelectedOn === void 0 ? void 0 : tableSelectedOn.index) === cellPosition.rowIndex;
|
|
2786
3449
|
const isSelectedInSameColumn = (tableSelectedOn === null || tableSelectedOn === void 0 ? void 0 : tableSelectedOn.region) === 'column' && (tableSelectedOn === null || tableSelectedOn === void 0 ? void 0 : tableSelectedOn.index) === cellPosition.columnIndex;
|
|
3450
|
+
// 判斷是否為正在被拖曳的 row/column
|
|
3451
|
+
const isDraggingThisRow = (dragState === null || dragState === void 0 ? void 0 : dragState.type) === 'row' && dragState.rowIndex === cellPosition.rowIndex;
|
|
3452
|
+
const isDraggingThisColumn = (dragState === null || dragState === void 0 ? void 0 : dragState.type) === 'column' && dragState.columnIndex === cellPosition.columnIndex;
|
|
2787
3453
|
const isSelectionTriggerByMe = (isSelectedInSameRow && cellPosition.columnIndex === 0) || (isSelectedInSameColumn && cellPosition.rowIndex === 0);
|
|
2788
|
-
const
|
|
3454
|
+
const showColumnActionButton = React.useMemo(() => cellPosition.rowIndex === 0 &&
|
|
2789
3455
|
(isSelectedInSameColumn || ((tableHoveredOn === null || tableHoveredOn === void 0 ? void 0 : tableHoveredOn.columnIndex) === cellPosition.columnIndex && !tableSelectedOn)), [cellPosition, isSelectedInSameColumn, tableHoveredOn, tableSelectedOn]);
|
|
2790
|
-
const
|
|
3456
|
+
const showRowActionButton = React.useMemo(() => cellPosition.columnIndex === 0 &&
|
|
2791
3457
|
(isSelectedInSameRow || ((tableHoveredOn === null || tableHoveredOn === void 0 ? void 0 : tableHoveredOn.rowIndex) === cellPosition.rowIndex && !tableSelectedOn)), [cellPosition, isSelectedInSameRow, tableHoveredOn, tableSelectedOn]);
|
|
2792
3458
|
// 用於定位 InlineToolbar 的 ref
|
|
2793
3459
|
const cellRef = React.useRef(null);
|
|
@@ -2795,6 +3461,64 @@ function TableCell(props) {
|
|
|
2795
3461
|
const [rowButtonPosition, setRowButtonPosition] = React.useState(null);
|
|
2796
3462
|
const [columnButtonPosition, setColumnButtonPosition] = React.useState(null);
|
|
2797
3463
|
const [cellStuckAtLeft, setCellStuckAtLeft] = React.useState(undefined);
|
|
3464
|
+
const [, dropRow] = reactDnd.useDrop(() => ({
|
|
3465
|
+
accept: ROW_DRAG_TYPE,
|
|
3466
|
+
canDrop: (item) => {
|
|
3467
|
+
if (!dragState || dragState.type !== 'row')
|
|
3468
|
+
return false;
|
|
3469
|
+
if (item.rowIndex === cellPosition.rowIndex)
|
|
3470
|
+
return false;
|
|
3471
|
+
const sourceInHeader = item.isInHeader;
|
|
3472
|
+
const targetInHeader = cellPosition.rowIndex < headerRowCount;
|
|
3473
|
+
return sourceInHeader === targetInHeader;
|
|
3474
|
+
},
|
|
3475
|
+
hover: (item, monitor) => {
|
|
3476
|
+
if (!monitor.canDrop()) {
|
|
3477
|
+
setDropTargetIndex(null);
|
|
3478
|
+
setDragDirection(null);
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
const direction = item.rowIndex < cellPosition.rowIndex ? 'down' : 'up';
|
|
3482
|
+
setDropTargetIndex(cellPosition.rowIndex);
|
|
3483
|
+
setDragDirection(direction);
|
|
3484
|
+
},
|
|
3485
|
+
drop: (item) => {
|
|
3486
|
+
moveOrSwapRow(item.rowIndex, cellPosition.rowIndex, 'move');
|
|
3487
|
+
setDropTargetIndex(null);
|
|
3488
|
+
setDragDirection(null);
|
|
3489
|
+
},
|
|
3490
|
+
}), [dragState, cellPosition.rowIndex, headerRowCount, moveOrSwapRow, setDropTargetIndex, setDragDirection]);
|
|
3491
|
+
// Drop target logic for columns
|
|
3492
|
+
const [, dropColumn] = reactDnd.useDrop(() => ({
|
|
3493
|
+
accept: COLUMN_DRAG_TYPE,
|
|
3494
|
+
canDrop: (item) => {
|
|
3495
|
+
if (!dragState || dragState.type !== 'column')
|
|
3496
|
+
return false;
|
|
3497
|
+
if (item.columnIndex === cellPosition.columnIndex)
|
|
3498
|
+
return false;
|
|
3499
|
+
const sourceIsTitle = item.isTitle;
|
|
3500
|
+
const targetIsTitle = checkIsTitleColumn(cellPosition.columnIndex);
|
|
3501
|
+
return sourceIsTitle === targetIsTitle;
|
|
3502
|
+
},
|
|
3503
|
+
hover: (item, monitor) => {
|
|
3504
|
+
if (!monitor.canDrop()) {
|
|
3505
|
+
setDropTargetIndex(null);
|
|
3506
|
+
setDragDirection(null);
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
// 計算拖曳方向
|
|
3510
|
+
const direction = item.columnIndex < cellPosition.columnIndex ? 'right' : 'left';
|
|
3511
|
+
setDropTargetIndex(cellPosition.columnIndex);
|
|
3512
|
+
setDragDirection(direction);
|
|
3513
|
+
},
|
|
3514
|
+
drop: (item) => {
|
|
3515
|
+
moveOrSwapColumn(item.columnIndex, cellPosition.columnIndex, 'move');
|
|
3516
|
+
setDropTargetIndex(null);
|
|
3517
|
+
setDragDirection(null);
|
|
3518
|
+
},
|
|
3519
|
+
}), [dragState, cellPosition.columnIndex, checkIsTitleColumn, moveOrSwapColumn, setDropTargetIndex, setDragDirection]);
|
|
3520
|
+
// Combine refs
|
|
3521
|
+
dropRow(dropColumn(cellRef));
|
|
2798
3522
|
// Column resize
|
|
2799
3523
|
const { isResizing, handleResizeStart } = useColumnResize({
|
|
2800
3524
|
tableElement,
|
|
@@ -2874,12 +3598,40 @@ function TableCell(props) {
|
|
|
2874
3598
|
}, className: clsx('qdr-table__cell', {
|
|
2875
3599
|
'qdr-table__cell--header': isHeader || element.treatAsTitle,
|
|
2876
3600
|
'qdr-table__cell--pinned': myColumnIsPinned,
|
|
2877
|
-
'qdr-table__cell--top-active': isSelectedInSameRow ||
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
3601
|
+
'qdr-table__cell--top-active': isSelectedInSameRow ||
|
|
3602
|
+
(isSelectedInSameColumn && cellPosition.rowIndex === 0) ||
|
|
3603
|
+
isDraggingThisRow ||
|
|
3604
|
+
(isDraggingThisColumn && cellPosition.rowIndex === 0),
|
|
3605
|
+
'qdr-table__cell--right-active': isSelectedInSameColumn ||
|
|
3606
|
+
(isSelectedInSameRow && cellPosition.columnIndex === columnCount - 1) ||
|
|
3607
|
+
isDraggingThisColumn ||
|
|
3608
|
+
(isDraggingThisRow && cellPosition.columnIndex === columnCount - 1),
|
|
3609
|
+
'qdr-table__cell--bottom-active': isSelectedInSameRow ||
|
|
3610
|
+
(isSelectedInSameColumn && cellPosition.rowIndex === rowCount - 1) ||
|
|
3611
|
+
isDraggingThisRow ||
|
|
3612
|
+
(isDraggingThisColumn && cellPosition.rowIndex === rowCount - 1),
|
|
3613
|
+
'qdr-table__cell--left-active': isSelectedInSameColumn ||
|
|
3614
|
+
(isSelectedInSameRow && cellPosition.columnIndex === 0) ||
|
|
3615
|
+
isDraggingThisColumn ||
|
|
3616
|
+
(isDraggingThisRow && cellPosition.columnIndex === 0),
|
|
2881
3617
|
'qdr-table__cell--is-selection-trigger-by-me': isSelectionTriggerByMe,
|
|
2882
|
-
|
|
3618
|
+
'qdr-table__cell--drag-row-target-top': (dragState === null || dragState === void 0 ? void 0 : dragState.type) === 'row' &&
|
|
3619
|
+
dropTargetIndex === cellPosition.rowIndex &&
|
|
3620
|
+
dropTargetIndex !== dragState.rowIndex &&
|
|
3621
|
+
dragDirection === 'up',
|
|
3622
|
+
'qdr-table__cell--drag-row-target-bottom': (dragState === null || dragState === void 0 ? void 0 : dragState.type) === 'row' &&
|
|
3623
|
+
dropTargetIndex === cellPosition.rowIndex &&
|
|
3624
|
+
dropTargetIndex !== dragState.rowIndex &&
|
|
3625
|
+
dragDirection === 'down',
|
|
3626
|
+
'qdr-table__cell--drag-column-target-left': (dragState === null || dragState === void 0 ? void 0 : dragState.type) === 'column' &&
|
|
3627
|
+
dropTargetIndex === cellPosition.columnIndex &&
|
|
3628
|
+
dropTargetIndex !== dragState.columnIndex &&
|
|
3629
|
+
dragDirection === 'left',
|
|
3630
|
+
'qdr-table__cell--drag-column-target-right': (dragState === null || dragState === void 0 ? void 0 : dragState.type) === 'column' &&
|
|
3631
|
+
dropTargetIndex === cellPosition.columnIndex &&
|
|
3632
|
+
dropTargetIndex !== dragState.columnIndex &&
|
|
3633
|
+
dragDirection === 'right',
|
|
3634
|
+
}), "data-row-index": cellPosition.rowIndex, "data-column-index": cellPosition.columnIndex, style: myColumnIsPinned
|
|
2883
3635
|
? {
|
|
2884
3636
|
left: cellStuckAtLeft,
|
|
2885
3637
|
}
|
|
@@ -2893,14 +3645,13 @@ function TableCell(props) {
|
|
|
2893
3645
|
top: toolbarPosition === null || toolbarPosition === void 0 ? void 0 : toolbarPosition.top,
|
|
2894
3646
|
left: toolbarPosition === null || toolbarPosition === void 0 ? void 0 : toolbarPosition.left,
|
|
2895
3647
|
}, iconGroups: focusToolbarIconGroups }))),
|
|
2896
|
-
|
|
2897
|
-
React.createElement(
|
|
3648
|
+
showRowActionButton && (React.createElement(components.Portal, { getContainer: () => portalContainerRef.current || document.body },
|
|
3649
|
+
React.createElement(RowDragButton, { rowIndex: cellPosition.rowIndex, headerRowCount: headerRowCount, style: {
|
|
2898
3650
|
top: rowButtonPosition === null || rowButtonPosition === void 0 ? void 0 : rowButtonPosition.top,
|
|
2899
3651
|
left: rowButtonPosition === null || rowButtonPosition === void 0 ? void 0 : rowButtonPosition.left,
|
|
2900
3652
|
}, onClick: (e) => {
|
|
2901
3653
|
e.preventDefault();
|
|
2902
3654
|
e.stopPropagation();
|
|
2903
|
-
// Clear focus by removing selection
|
|
2904
3655
|
core.Transforms.deselect(editor);
|
|
2905
3656
|
setTableSelectedOn((prev) => {
|
|
2906
3657
|
if ((prev === null || prev === void 0 ? void 0 : prev.region) === 'row' && prev.index === cellPosition.rowIndex) {
|
|
@@ -2908,17 +3659,15 @@ function TableCell(props) {
|
|
|
2908
3659
|
}
|
|
2909
3660
|
return { region: 'row', index: cellPosition.rowIndex };
|
|
2910
3661
|
});
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
React.createElement("button", { type: "button", contentEditable: false, style: {
|
|
3662
|
+
} }))),
|
|
3663
|
+
showColumnActionButton && (React.createElement(components.Portal, { getContainer: () => portalContainerRef.current || document.body },
|
|
3664
|
+
React.createElement(ColumnDragButton, { columnIndex: cellPosition.columnIndex, style: {
|
|
2915
3665
|
// pinned 時因為 sticky 所以要扣掉 scrollTop
|
|
2916
|
-
top: ((
|
|
3666
|
+
top: ((_b = columnButtonPosition === null || columnButtonPosition === void 0 ? void 0 : columnButtonPosition.top) !== null && _b !== void 0 ? _b : 0) - (element.pinned ? scrollTop : 0),
|
|
2917
3667
|
left: columnButtonPosition === null || columnButtonPosition === void 0 ? void 0 : columnButtonPosition.left,
|
|
2918
3668
|
}, onClick: (e) => {
|
|
2919
3669
|
e.preventDefault();
|
|
2920
3670
|
e.stopPropagation();
|
|
2921
|
-
// Clear focus by removing selection
|
|
2922
3671
|
core.Transforms.deselect(editor);
|
|
2923
3672
|
setTableSelectedOn((prev) => {
|
|
2924
3673
|
if ((prev === null || prev === void 0 ? void 0 : prev.region) === 'column' && prev.index === cellPosition.columnIndex) {
|
|
@@ -2926,8 +3675,7 @@ function TableCell(props) {
|
|
|
2926
3675
|
}
|
|
2927
3676
|
return { region: 'column', index: cellPosition.columnIndex };
|
|
2928
3677
|
});
|
|
2929
|
-
},
|
|
2930
|
-
React.createElement(components.Icon, { icon: icons.Drag, width: 20, height: 20 })))),
|
|
3678
|
+
}, checkIsTitleColumn: checkIsTitleColumn }))),
|
|
2931
3679
|
isSelectionTriggerByMe && (cellPosition.columnIndex === 0 || cellPosition.rowIndex === 0) ? (React.createElement(components.Portal, { getContainer: () => portalContainerRef.current || document.body },
|
|
2932
3680
|
React.createElement(toolbar.InlineToolbar, { className: "qdr-table__cell__inline-table-toolbar", style: {
|
|
2933
3681
|
top: toolbarPosition === null || toolbarPosition === void 0 ? void 0 : toolbarPosition.top,
|