@quadrats/react 1.1.0 → 1.1.1

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.
@@ -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 equalPercentage = Math.round((100 / columnCount) * 10) / 10;
375
- return Array(columnCount)
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 欄位(pinned)保持不變
394
- * * 新欄位使用 pixel(與其他 pixel 欄位平均分配剩餘空間)
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
- const currentTotal = percentageColumns.reduce((sum, col) => sum + col.value, 0);
417
- const targetTotal = 100 - averagePercentage;
418
- const scaleFactor = targetTotal / currentTotal;
419
- const newWidths = [];
420
- currentWidths.forEach((width, index) => {
421
- if (index === insertIndex) {
422
- newWidths.push({ type: 'percentage', value: averagePercentage });
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
- const newWidths = [];
438
- // 找到最後一個 pixel 欄位的寬度,新欄位將複製這個寬度
439
- const lastPixelWidth = pixelColumns.length > 0 ? pixelColumns[pixelColumns.length - 1].value : 150;
440
- currentWidths.forEach((width, index) => {
441
- if (index === insertIndex) {
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.push(Object.assign({}, width));
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: 返回原始寬度加一個平均 percentage 欄位
453
- const newWidths = [...currentWidths];
454
- newWidths.splice(insertIndex, 0, { type: 'percentage', value: averagePercentage });
455
- return newWidths;
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
- * 移動 columnWidths 陣列中的元素位置
725
+ * 移動或交換欄位寬度設定
651
726
  * @param currentWidths - 當前的欄位寬度陣列
652
- * @param fromIndex - 來源索引
653
- * @param toIndex - 目標索引
654
- * @returns 新的欄位寬度陣列
727
+ * @param sourceIndex - 來源欄位的索引
728
+ * @param targetIndex - 目標欄位的索引
729
+ * @param mode - 'swap' 為交換兩個位置,'move' 為移動到目標位置
730
+ * @returns 處理後的欄位寬度陣列
655
731
  */
656
- function moveColumnWidth(currentWidths, fromIndex, toIndex) {
657
- if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= currentWidths.length) {
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
- const [movedWidth] = newWidths.splice(fromIndex, 1);
662
- newWidths.splice(toIndex, 0, movedWidth);
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
- if (totalPinnedPercentage > table.MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE) {
694
- const scaleFactor = table.MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE / totalPinnedPercentage;
695
- totalPinnedPercentage = table.MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE;
696
- // 調整釘選欄位的百分比
697
- currentWidths.forEach((width, index) => {
698
- if (pinnedColumnIndices.includes(index)) {
699
- if (width.type === 'percentage') {
700
- currentWidths[index] = {
701
- type: 'percentage',
702
- value: Math.round(width.value * scaleFactor * 10) / 10,
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
- }, [rowActions, columnActions, columnAlignActions, rowAddActions, columnAddActions, deleteActions]);
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
- const newWidths = calculateColumnWidthsAfterAdd(currentWidths, insertIndex);
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
- const newWidths = calculateColumnWidthsAfterAdd(currentWidths, insertIndex);
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 = moveColumnWidth(currentWidths, columnIndex, actualTargetIndex);
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
- if (columnIndex < targetColumnIndex)
1905
- return;
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 = moveColumnWidth(currentWidths, columnIndex, targetColumnIndex);
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
- const updatedPinnedIndices = [...new Set([...pinnedColumnIndices, columnIndex])].sort((a, b) => a - b);
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(TableActionsContext.Provider, { value: actionsValue },
2373
- React.createElement(TableMetadataContext.Provider, { value: metadataValue },
2374
- React.createElement(TableStateContext.Provider, { value: stateValue },
2375
- React.createElement("div", Object.assign({}, attributes, { className: "qdr-table" }),
2376
- children,
2377
- React.createElement("button", { type: "button", onClick: (evt) => {
2378
- evt.preventDefault();
2379
- evt.stopPropagation();
2380
- setTableSelectedOn((prev) => ((prev === null || prev === void 0 ? void 0 : prev.region) === 'table' ? undefined : { region: 'table' }));
2381
- }, onMouseDown: (evt) => {
2382
- evt.preventDefault();
2383
- evt.stopPropagation();
2384
- }, className: "qdr-table__selection", title: (tableSelectedOn === null || tableSelectedOn === void 0 ? void 0 : tableSelectedOn.region) === 'table' ? 'Deselect Table' : 'Select Table' },
2385
- React.createElement(components.Icon, { icon: icons.Drag, width: 20, height: 20 })),
2386
- React.createElement("div", { ref: portalContainerRef, className: "qdr-table__portal-container", "data-slate-editor": false, contentEditable: false }))))));
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
- // 如果正在程式化更新滾動位置,不要觸發 Slate 更新
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 showRowActionButton = React.useMemo(() => cellPosition.rowIndex === 0 &&
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 showColumnActionButton = React.useMemo(() => cellPosition.columnIndex === 0 &&
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 || (isSelectedInSameColumn && cellPosition.rowIndex === 0),
2878
- 'qdr-table__cell--right-active': isSelectedInSameColumn || (isSelectedInSameRow && cellPosition.columnIndex === columnCount - 1),
2879
- 'qdr-table__cell--bottom-active': isSelectedInSameRow || (isSelectedInSameColumn && cellPosition.rowIndex === rowCount - 1),
2880
- 'qdr-table__cell--left-active': isSelectedInSameColumn || (isSelectedInSameRow && cellPosition.columnIndex === 0),
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
- }), style: myColumnIsPinned
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
- showColumnActionButton && (React.createElement(components.Portal, { getContainer: () => portalContainerRef.current || document.body },
2897
- React.createElement("button", { type: "button", contentEditable: false, style: {
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
- }, className: "qdr-table__cell-row-action" },
2912
- React.createElement(components.Icon, { icon: icons.Drag, width: 20, height: 20 })))),
2913
- showRowActionButton && (React.createElement(components.Portal, { getContainer: () => portalContainerRef.current || document.body },
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: ((_a = columnButtonPosition === null || columnButtonPosition === void 0 ? void 0 : columnButtonPosition.top) !== null && _a !== void 0 ? _a : 0) - (element.pinned ? scrollTop : 0),
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
- }, className: "qdr-table__cell-column-action" },
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,