@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.
package/table/index.js CHANGED
@@ -7,6 +7,7 @@ export { useTableStateContext } from './hooks/useTableStateContext.js';
7
7
  export { useTableCellToolbarActions } from './hooks/useTableCellToolbarActions.js';
8
8
  import 'react';
9
9
  import '@quadrats/core';
10
+ import './contexts/TableDragContext.js';
10
11
  import '@quadrats/common/table';
11
12
  import 'slate-react';
12
13
  import '@quadrats/react';
@@ -19,6 +20,8 @@ import 'slate';
19
20
  import './contexts/TableScrollContext.js';
20
21
  import '@quadrats/react/utils';
21
22
  import '@quadrats/common/list';
23
+ import 'react-dnd';
22
24
  import './contexts/TableHeaderContext.js';
25
+ import 'react-dnd-html5-backend';
23
26
  export { defaultRenderTableElements } from './defaultRenderTableElements.js';
24
27
  export { createReactTable } from './createReactTable.js';
package/table/table.css CHANGED
@@ -1 +1 @@
1
- .qdr-table__table-toolbar{right:unset;left:50%;top:-4px;transform:translate(-50%, -100%)}.qdr-table{display:flex;flex-flow:column;align-items:flex-start;gap:var(--qdr-spacing-4);margin:var(--qdr-spacing-11) 0;position:relative}.qdr-table__selection{padding:0;position:absolute;left:-22px;top:0;z-index:0;width:20px;height:20px;color:var(--qdr-action-inactive);border:none;border-radius:var(--qdr-radius-1);background-color:rgba(0,0,0,0);display:flex;align-items:center;justify-content:center;cursor:pointer}.qdr-table__selection:hover{background-color:var(--qdr-secondary-active-bg);color:var(--qdr-action-active)}.qdr-table__portal-container{position:relative;pointer-events:none}.qdr-table__title{width:100%;display:block;position:relative;margin:0;font-size:var(--qdr-typography-article-h6-font-size);line-height:var(--qdr-typography-article-h6-line-height);letter-spacing:var(--qdr-typography-article-h6-letter-spacing);font-weight:var(--qdr-typography-article-h6-font-weight);color:var(--qdr-text-primary);background-color:var(--qdr-surface-secondary)}.qdr-table__title__placeholder{position:absolute;left:0;top:0;color:var(--qdr-text-secondary);pointer-events:none}.qdr-table__header{background-color:var(--qdr-bg)}.qdr-table__header--pinned{position:sticky;z-index:2;top:0;box-shadow:0 2px 4px -2px rgba(0,0,0,.1490196078)}.qdr-table__scrollContainer{width:100%;max-width:100%;max-height:420px;overflow:auto;scrollbar-width:thin;position:relative}.qdr-table__main{margin:0;border-collapse:separate;border-spacing:0;table-layout:fixed}.qdr-table__main th:not(:first-child),.qdr-table__main td:not(:first-child){border-left:none}.qdr-table__main thead tr:not(:last-child) th,.qdr-table__main tbody tr:not(:last-child) td{border-bottom:none}.qdr-table__inline-table-toolbar,.qdr-table__cell__inline-table-toolbar{pointer-events:none;top:-50px;left:0;right:unset}.qdr-table__mainWrapper{width:100%;max-width:100%;height:100%;position:relative}.qdr-table__mainWrapper--selected .qdr-table__main{border:2px solid var(--qdr-primary)}.qdr-table__mainWrapper--selected .qdr-table__table-toolbar{opacity:1;pointer-events:auto}.qdr-table__body{background-color:var(--qdr-surface-primary)}.qdr-table__cell{position:relative;padding:var(--qdr-spacing-3) var(--qdr-spacing-4);font-size:var(--qdr-typography-article-body1-font-size);line-height:var(--qdr-typography-article-body1-line-height);letter-spacing:var(--qdr-typography-article-body1-letter-spacing);font-weight:var(--qdr-typography-article-body1-font-weight);color:var(--qdr-text-primary);text-align:left;border-top:1px solid var(--qdr-border);border-right:1px solid var(--qdr-border);border-bottom:1px solid var(--qdr-border);border-left:1px solid var(--qdr-border);vertical-align:top;word-break:break-all}.qdr-table__cell p,.qdr-table__cell ul,.qdr-table__cell ol{margin:0}.qdr-table__cell--resizing::after{content:"";position:absolute;right:0;top:0;width:2px;height:100%;background-color:var(--qdr-primary-light);pointer-events:none;opacity:1}.qdr-table__cell__resize-handle{position:absolute;right:0;top:0;width:6px;height:100%;transform:translateX(0);cursor:col-resize;z-index:1}.qdr-table__cell--header{font-weight:var(--qdr-typography-article-h6-font-weight);color:var(--qdr-text-primary);background-color:var(--qdr-bg)}.qdr-table__cell--pinned{position:sticky;z-index:1;left:0;box-shadow:2px 0 4px -2px rgba(0,0,0,.1490196078)}.qdr-table__cell--top-active{border-top:2px solid var(--qdr-primary-light) !important}.qdr-table__cell--right-active{border-right:2px solid var(--qdr-primary-light) !important}.qdr-table__cell--bottom-active{border-bottom:2px solid var(--qdr-primary-light) !important}.qdr-table__cell--left-active{border-left:2px solid var(--qdr-primary-light) !important}.qdr-table__btn-icon{color:var(--qdr-action-disabled);pointer-events:none}.qdr-table__add-column{position:absolute;z-index:0;cursor:pointer;padding:0;background-color:var(--qdr-block);border-radius:var(--qdr-radius-1);border:1px solid var(--qdr-divider);display:flex;align-items:center;justify-content:center;right:-24px;top:0;width:20px;height:100%}.qdr-table__add-column:hover{background-color:var(--qdr-bg)}.qdr-table__add-column:hover .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__add-column:active{background-color:var(--qdr-divider);border:1px solid var(--qdr-border)}.qdr-table__add-column:active .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__add-row{position:absolute;z-index:0;cursor:pointer;padding:0;background-color:var(--qdr-block);border-radius:var(--qdr-radius-1);border:1px solid var(--qdr-divider);display:flex;align-items:center;justify-content:center;bottom:-24px;left:0;width:100%;height:20px}.qdr-table__add-row:hover{background-color:var(--qdr-bg)}.qdr-table__add-row:hover .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__add-row:active{background-color:var(--qdr-divider);border:1px solid var(--qdr-border)}.qdr-table__add-row:active .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__add-both{position:absolute;z-index:0;cursor:pointer;padding:0;background-color:var(--qdr-block);border-radius:var(--qdr-radius-1);border:1px solid var(--qdr-divider);display:flex;align-items:center;justify-content:center;right:-24px;bottom:-24px;width:20px;height:20px}.qdr-table__add-both:hover{background-color:var(--qdr-bg)}.qdr-table__add-both:hover .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__add-both:active{background-color:var(--qdr-divider);border:1px solid var(--qdr-border)}.qdr-table__add-both:active .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__delete{color:var(--qdr-error)}@keyframes fade-in{0%{opacity:0}100%{opacity:1}}.qdr-table__cell__focus-toolbar,.qdr-table__cell__inline-table-toolbar{position:absolute;right:unset;transform:translate(-10%, -100%);padding:4px;border-radius:10px;border:1px solid var(--qdr-border);background:var(--qdr-bg);box-shadow:0 4px 8px -2px rgba(0,0,0,.1019607843),0 2px 4px -2px rgba(0,0,0,.0392156863);animation-name:fade-in;animation-duration:.1s;animation-delay:.1s;animation-iteration-count:1;animation-fill-mode:forwards;animation-timing-function:linear;pointer-events:auto;z-index:10}.qdr-table__cell-row-action{position:absolute;z-index:9;width:20px;height:24px;padding:0;border-radius:var(--qdr-radius-1);border:1px solid var(--qdr-primary-active-bg);background-color:var(--qdr-primary-hover-bg);display:flex;align-items:center;justify-content:center;pointer-events:auto;cursor:pointer;opacity:1}.qdr-table__cell-column-action{position:absolute;z-index:9;width:20px;height:24px;padding:0;border-radius:var(--qdr-radius-1);border:1px solid var(--qdr-primary-active-bg);background-color:var(--qdr-primary-hover-bg);display:flex;align-items:center;justify-content:center;pointer-events:auto;cursor:pointer;opacity:1;transform:rotate(90deg);transform-origin:center}.qdr-table__size-indicators{display:none;overflow:hidden;position:absolute;z-index:1;left:0;top:0;width:100%;max-width:100%;padding:8px 0;pointer-events:none;transform:translateY(-100%);flex-flow:row nowrap;align-items:center}.qdr-table__size-indicator{display:flex;align-items:center;justify-content:center;position:relative}.qdr-table__size{background-color:var(--qdr-action-inactive);color:var(--qdr-on-primary);padding:4px 8px;font-size:12px;white-space:pre-wrap;position:relative}.qdr-table__size::after{position:absolute;content:"";z-index:-1;top:calc(100% - 2px);left:50%;width:10px;height:10px;background-color:var(--qdr-action-inactive);border-right:1px solid var(--qdr-action-inactive);border-bottom:1px solid var(--qdr-action-inactive);transform:translateX(-50%) translateY(-50%) rotate(45deg)}
1
+ .qdr-table__table-toolbar{right:unset;left:50%;top:-4px;transform:translate(-50%, -100%)}.qdr-table{display:flex;flex-flow:column;align-items:flex-start;gap:var(--qdr-spacing-4);margin:var(--qdr-spacing-11) 0;position:relative}.qdr-table__selection{padding:0;position:absolute;left:-22px;top:0;z-index:0;width:20px;height:20px;color:var(--qdr-action-inactive);border:none;border-radius:var(--qdr-radius-1);background-color:rgba(0,0,0,0);display:flex;align-items:center;justify-content:center;cursor:pointer}.qdr-table__selection:hover{background-color:var(--qdr-secondary-active-bg);color:var(--qdr-action-active)}.qdr-table__portal-container{position:relative;pointer-events:none}.qdr-table__title{width:100%;display:block;position:relative;margin:0;font-size:var(--qdr-typography-article-h6-font-size);line-height:var(--qdr-typography-article-h6-line-height);letter-spacing:var(--qdr-typography-article-h6-letter-spacing);font-weight:var(--qdr-typography-article-h6-font-weight);color:var(--qdr-text-primary);background-color:var(--qdr-surface-secondary)}.qdr-table__title__placeholder{position:absolute;left:0;top:0;color:var(--qdr-text-secondary);pointer-events:none}.qdr-table__header{background-color:var(--qdr-bg)}.qdr-table__header--pinned{position:sticky;z-index:2;top:0;box-shadow:0 2px 4px -2px rgba(0,0,0,.1490196078)}.qdr-table__scrollContainer{width:100%;max-width:100%;max-height:420px;overflow:auto;scrollbar-width:thin;position:relative}.qdr-table__main{margin:0;border-collapse:separate;border-spacing:0;table-layout:fixed}.qdr-table__main th:not(:first-child),.qdr-table__main td:not(:first-child){border-left:none}.qdr-table__main thead tr:not(:last-child) th,.qdr-table__main tbody tr:not(:last-child) td{border-bottom:none}.qdr-table__inline-table-toolbar,.qdr-table__cell__inline-table-toolbar{pointer-events:none;top:-50px;left:0;right:unset}.qdr-table__mainWrapper{width:100%;max-width:100%;height:100%;position:relative}.qdr-table__mainWrapper--selected .qdr-table__main{border:2px solid var(--qdr-primary)}.qdr-table__mainWrapper--selected .qdr-table__table-toolbar{opacity:1;pointer-events:auto}.qdr-table__body{background-color:var(--qdr-surface-primary)}.qdr-table__cell{position:relative;padding:var(--qdr-spacing-3) var(--qdr-spacing-4);font-size:var(--qdr-typography-article-body1-font-size);line-height:var(--qdr-typography-article-body1-line-height);letter-spacing:var(--qdr-typography-article-body1-letter-spacing);font-weight:var(--qdr-typography-article-body1-font-weight);color:var(--qdr-text-primary);text-align:left;border-top:1px solid var(--qdr-border);border-right:1px solid var(--qdr-border);border-bottom:1px solid var(--qdr-border);border-left:1px solid var(--qdr-border);vertical-align:top;word-break:break-all}.qdr-table__cell p,.qdr-table__cell ul,.qdr-table__cell ol{margin:0}.qdr-table__cell--resizing::after{content:"";position:absolute;right:0;top:0;width:2px;height:100%;background-color:var(--qdr-primary-light);pointer-events:none;opacity:1}.qdr-table__cell__resize-handle{position:absolute;right:0;top:0;width:6px;height:100%;transform:translateX(0);cursor:col-resize;z-index:1}.qdr-table__cell--header{font-weight:var(--qdr-typography-article-h6-font-weight);color:var(--qdr-text-primary);background-color:var(--qdr-bg)}.qdr-table__cell--pinned{position:sticky;z-index:1;left:0;box-shadow:2px 0 4px -2px rgba(0,0,0,.1490196078)}.qdr-table__cell--top-active{border-top:2px solid var(--qdr-primary-light) !important}.qdr-table__cell--right-active{border-right:2px solid var(--qdr-primary-light) !important}.qdr-table__cell--bottom-active{border-bottom:2px solid var(--qdr-primary-light) !important}.qdr-table__cell--left-active{border-left:2px solid var(--qdr-primary-light) !important}.qdr-table__cell--drag-row-target-top{border-top:2px solid var(--qdr-error) !important}.qdr-table__cell--drag-row-target-bottom{border-bottom:2px solid var(--qdr-error) !important}.qdr-table__cell--drag-column-target-left{border-left:2px solid var(--qdr-error) !important}.qdr-table__cell--drag-column-target-right{border-right:2px solid var(--qdr-error) !important}.qdr-table__drag-overlay{position:fixed;pointer-events:none;z-index:9999;will-change:transform}.qdr-table__drag-overlay .qdr-table__drag-overlay-content{width:100%;height:100%;background-color:var(--qdr-primary-hover-bg)}.qdr-table__btn-icon{color:var(--qdr-action-disabled);pointer-events:none}.qdr-table__add-column{position:absolute;z-index:0;cursor:pointer;padding:0;background-color:var(--qdr-block);border-radius:var(--qdr-radius-1);border:1px solid var(--qdr-divider);display:flex;align-items:center;justify-content:center;right:-24px;top:0;width:20px;height:100%}.qdr-table__add-column:hover{background-color:var(--qdr-bg)}.qdr-table__add-column:hover .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__add-column:active{background-color:var(--qdr-divider);border:1px solid var(--qdr-border)}.qdr-table__add-column:active .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__add-row{position:absolute;z-index:0;cursor:pointer;padding:0;background-color:var(--qdr-block);border-radius:var(--qdr-radius-1);border:1px solid var(--qdr-divider);display:flex;align-items:center;justify-content:center;bottom:-24px;left:0;width:100%;height:20px}.qdr-table__add-row:hover{background-color:var(--qdr-bg)}.qdr-table__add-row:hover .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__add-row:active{background-color:var(--qdr-divider);border:1px solid var(--qdr-border)}.qdr-table__add-row:active .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__add-both{position:absolute;z-index:0;cursor:pointer;padding:0;background-color:var(--qdr-block);border-radius:var(--qdr-radius-1);border:1px solid var(--qdr-divider);display:flex;align-items:center;justify-content:center;right:-24px;bottom:-24px;width:20px;height:20px}.qdr-table__add-both:hover{background-color:var(--qdr-bg)}.qdr-table__add-both:hover .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__add-both:active{background-color:var(--qdr-divider);border:1px solid var(--qdr-border)}.qdr-table__add-both:active .qdr-table__btn-icon{color:var(--qdr-text-secondary)}.qdr-table__delete{color:var(--qdr-error)}@keyframes fade-in{0%{opacity:0}100%{opacity:1}}.qdr-table__cell__focus-toolbar,.qdr-table__cell__inline-table-toolbar{position:absolute;right:unset;transform:translate(-10%, -100%);padding:4px;border-radius:10px;border:1px solid var(--qdr-border);background:var(--qdr-bg);box-shadow:0 4px 8px -2px rgba(0,0,0,.1019607843),0 2px 4px -2px rgba(0,0,0,.0392156863);animation-name:fade-in;animation-duration:.1s;animation-delay:.1s;animation-iteration-count:1;animation-fill-mode:forwards;animation-timing-function:linear;pointer-events:auto;z-index:10}.qdr-table__cell-row-action{position:absolute;z-index:9;width:20px;height:24px;padding:0;border-radius:var(--qdr-radius-1);border:1px solid var(--qdr-primary-active-bg);background-color:var(--qdr-primary-hover-bg);display:flex;align-items:center;justify-content:center;pointer-events:auto;cursor:pointer;opacity:1}.qdr-table__cell-row-action--dragging{border:1px solid var(--qdr-divider);background-color:var(--qdr-bg);cursor:grabbing}.qdr-table__cell-column-action{position:absolute;z-index:9;width:20px;height:24px;padding:0;border-radius:var(--qdr-radius-1);border:1px solid var(--qdr-primary-active-bg);background-color:var(--qdr-primary-hover-bg);display:flex;align-items:center;justify-content:center;pointer-events:auto;cursor:pointer;opacity:1;transform:rotate(90deg);transform-origin:center}.qdr-table__cell-column-action--dragging{border:1px solid var(--qdr-divider);background-color:var(--qdr-bg);cursor:grabbing}.qdr-table__size-indicators{display:none;overflow:hidden;position:absolute;z-index:1;left:0;top:0;width:100%;max-width:100%;padding:8px 0;pointer-events:none;transform:translateY(-100%);flex-flow:row nowrap;align-items:center}.qdr-table__size-indicator{display:flex;align-items:center;justify-content:center;position:relative}.qdr-table__size{background-color:var(--qdr-action-inactive);color:var(--qdr-on-primary);padding:4px 8px;font-size:12px;white-space:pre-wrap;position:relative}.qdr-table__size::after{position:absolute;content:"";z-index:-1;top:calc(100% - 2px);left:50%;width:10px;height:10px;background-color:var(--qdr-action-inactive);border-right:1px solid var(--qdr-action-inactive);border-bottom:1px solid var(--qdr-action-inactive);transform:translateX(-50%) translateY(-50%) rotate(45deg)}
package/table/table.scss CHANGED
@@ -46,6 +46,12 @@ $toolbar-z-index: 10;
46
46
  pointer-events: auto;
47
47
  cursor: pointer;
48
48
  opacity: 1;
49
+
50
+ &--dragging {
51
+ border: 1px solid var(--qdr-divider);
52
+ background-color: var(--qdr-bg);
53
+ cursor: grabbing;
54
+ }
49
55
  }
50
56
 
51
57
  @mixin cell-selected($border-position) {
@@ -262,6 +268,35 @@ $toolbar-z-index: 10;
262
268
  &--left-active {
263
269
  @include cell-selected('left');
264
270
  }
271
+
272
+ &--drag-row-target-top {
273
+ border-top: 2px solid var(--qdr-error) !important;
274
+ }
275
+
276
+ &--drag-row-target-bottom {
277
+ border-bottom: 2px solid var(--qdr-error) !important;
278
+ }
279
+
280
+ &--drag-column-target-left {
281
+ border-left: 2px solid var(--qdr-error) !important;
282
+ }
283
+
284
+ &--drag-column-target-right {
285
+ border-right: 2px solid var(--qdr-error) !important;
286
+ }
287
+ }
288
+
289
+ &__drag-overlay {
290
+ position: fixed;
291
+ pointer-events: none;
292
+ z-index: 9999;
293
+ will-change: transform;
294
+
295
+ .qdr-table__drag-overlay-content {
296
+ width: 100%;
297
+ height: 100%;
298
+ background-color: var(--qdr-primary-hover-bg);
299
+ }
265
300
  }
266
301
 
267
302
  &__btn-icon {
@@ -35,6 +35,8 @@ export type TableContextType = {
35
35
  unpinColumn: () => void;
36
36
  pinRow: (rowIndex: number) => void;
37
37
  unpinRow: () => void;
38
+ moveOrSwapRow: (sourceRowIndex: number, targetRowIndex: number, mode?: 'swap' | 'move') => void;
39
+ moveOrSwapColumn: (sourceColumnIndex: number, targetColumnIndex: number, mode?: 'swap' | 'move') => void;
38
40
  isColumnPinned: (columnIndex: number) => boolean;
39
41
  isRowPinned: (rowIndex: number) => boolean;
40
42
  isReachMaximumColumns: boolean;
@@ -2,6 +2,22 @@ import { Element } from '@quadrats/core';
2
2
  import { TableElement, ColumnWidth } from '@quadrats/common/table';
3
3
  import { QuadratsReactEditor } from '@quadrats/react';
4
4
  import { AlignValue } from '@quadrats/common/align';
5
+ /**
6
+ * 分配百分比寬度,確保總和為 100%
7
+ * 前 n-1 個欄位使用四捨五入的平均值,最後一個欄位使用剩餘寬度
8
+ * @param count - 欄位數量
9
+ * @returns 百分比陣列,總和為 100%
10
+ */
11
+ export declare function distributeEqualPercentages(count: number): number[];
12
+ /**
13
+ * 等比例縮減現有百分比並加入新欄位
14
+ * 前 n-1 個縮減後的欄位使用四捨五入,最後一個縮減欄位使用剩餘寬度
15
+ * @param currentPercentages - 當前的百分比陣列
16
+ * @param newColumnPercentage - 新欄位要佔的百分比
17
+ * @param insertIndex - 新欄位插入的位置(0-based)
18
+ * @returns 新的百分比陣列,總和為 100%
19
+ */
20
+ export declare function scalePercentagesWithNewColumn(currentPercentages: number[], newColumnPercentage: number, insertIndex: number): number[];
5
21
  export interface TableElements {
6
22
  tableMainElement: TableElement | null;
7
23
  tableBodyElement: TableElement | null;
@@ -103,14 +119,16 @@ export declare function setColumnWidths(editor: QuadratsReactEditor, tableElemen
103
119
  * 計算新增欄位後的欄位寬度
104
120
  * - 如果所有欄位都是 percentage:按比例縮減現有欄位,新欄位佔平均寬度
105
121
  * - 如果有混合模式(percentage + pixel):
106
- * * percentage 欄位(pinned)保持不變
107
- * * 新欄位使用 pixel(與其他 pixel 欄位平均分配剩餘空間)
122
+ * * 如果用戶操作的欄位是 pinned column:新欄位使用 percentage(需要調整 pinned columns 的百分比)
123
+ * * 如果用戶操作的欄位是 unpinned column:新欄位使用 pixel(與其他 pixel 欄位相同寬度)
108
124
  *
109
125
  * @param currentWidths - 當前的欄位寬度陣列
110
126
  * @param insertIndex - 新欄位插入的位置(0-based)
127
+ * @param pinnedColumnIndices - 當前釘選欄位的索引陣列(插入前的索引)
128
+ * @param operatingColumnIndex - 用戶實際操作的欄位索引(用於判斷是在 pinned 還是 unpinned column 操作)
111
129
  * @returns 新的欄位寬度陣列
112
130
  */
113
- export declare function calculateColumnWidthsAfterAdd(currentWidths: ColumnWidth[], insertIndex: number): ColumnWidth[];
131
+ export declare function calculateColumnWidthsAfterAdd(currentWidths: ColumnWidth[], insertIndex: number, pinnedColumnIndices?: number[], operatingColumnIndex?: number): ColumnWidth[];
114
132
  /**
115
133
  * 計算刪除欄位後的欄位寬度
116
134
  * 此函數會智慧處理欄位寬度的重新分配:
@@ -143,13 +161,14 @@ export declare function calculateColumnWidthsAfterDelete(currentWidths: ColumnWi
143
161
  */
144
162
  export declare function calculateResizedColumnWidths(currentWidths: ColumnWidth[], columnIndex: number, deltaPercentage: number, deltaPixel: number, pinnedColumnIndices?: number[]): ColumnWidth[];
145
163
  /**
146
- * 移動 columnWidths 陣列中的元素位置
164
+ * 移動或交換欄位寬度設定
147
165
  * @param currentWidths - 當前的欄位寬度陣列
148
- * @param fromIndex - 來源索引
149
- * @param toIndex - 目標索引
150
- * @returns 新的欄位寬度陣列
166
+ * @param sourceIndex - 來源欄位的索引
167
+ * @param targetIndex - 目標欄位的索引
168
+ * @param mode - 'swap' 為交換兩個位置,'move' 為移動到目標位置
169
+ * @returns 處理後的欄位寬度陣列
151
170
  */
152
- export declare function moveColumnWidth(currentWidths: ColumnWidth[], fromIndex: number, toIndex: number): ColumnWidth[];
171
+ export declare function moveOrSwapColumnWidth(currentWidths: ColumnWidth[], sourceIndex: number, targetIndex: number, mode?: 'swap' | 'move'): ColumnWidth[];
153
172
  /**
154
173
  * 將 columnWidths 轉換為混合模式(釘選欄位用 percentage,未釘選欄位用 pixel)
155
174
  * @param currentWidths - 當前的欄位寬度陣列
@@ -158,3 +177,10 @@ export declare function moveColumnWidth(currentWidths: ColumnWidth[], fromIndex:
158
177
  * @returns 轉換後的欄位寬度陣列
159
178
  */
160
179
  export declare function convertToMixedWidthMode(currentWidths: ColumnWidth[], pinnedColumnIndices: number[], tableWidth: number): ColumnWidth[];
180
+ /**
181
+ * 將混合模式的欄位寬度轉換回純百分比模式
182
+ * 當所有 pinned columns 都被 unpin 後,需要將 pixel 欄位轉換回 percentage
183
+ * @param currentWidths - 當前的欄位寬度陣列(混合模式)
184
+ * @returns 轉換後的純百分比欄位寬度陣列,總和為 100%
185
+ */
186
+ export declare function convertToPercentageMode(currentWidths: ColumnWidth[]): ColumnWidth[];
@@ -3,6 +3,58 @@ import { ReactEditor } from 'slate-react';
3
3
  import { TABLE_MAIN_TYPE, TABLE_BODY_TYPE, TABLE_HEADER_TYPE, MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE, TABLE_CELL_TYPE, TABLE_ROW_TYPE, MIN_COLUMN_WIDTH_PERCENTAGE, MIN_COLUMN_WIDTH_PIXEL } from '@quadrats/common/table';
4
4
  import { ALIGNABLE_TYPES, ALIGN_TYPE } from '@quadrats/common/align';
5
5
 
6
+ /**
7
+ * 分配百分比寬度,確保總和為 100%
8
+ * 前 n-1 個欄位使用四捨五入的平均值,最後一個欄位使用剩餘寬度
9
+ * @param count - 欄位數量
10
+ * @returns 百分比陣列,總和為 100%
11
+ */
12
+ function distributeEqualPercentages(count) {
13
+ if (count <= 0)
14
+ return [];
15
+ if (count === 1)
16
+ return [100];
17
+ const percentages = [];
18
+ const averagePercentage = Math.round((100 / count) * 10) / 10;
19
+ // 前 n-1 個欄位使用四捨五入的平均值
20
+ for (let i = 0; i < count - 1; i++) {
21
+ percentages.push(averagePercentage);
22
+ }
23
+ // 最後一個欄位使用剩餘寬度,確保總和為 100%
24
+ const sumOfFirst = percentages.reduce((sum, val) => sum + val, 0);
25
+ const lastPercentage = Math.round((100 - sumOfFirst) * 10) / 10;
26
+ percentages.push(lastPercentage);
27
+ return percentages;
28
+ }
29
+ /**
30
+ * 等比例縮減現有百分比並加入新欄位
31
+ * 前 n-1 個縮減後的欄位使用四捨五入,最後一個縮減欄位使用剩餘寬度
32
+ * @param currentPercentages - 當前的百分比陣列
33
+ * @param newColumnPercentage - 新欄位要佔的百分比
34
+ * @param insertIndex - 新欄位插入的位置(0-based)
35
+ * @returns 新的百分比陣列,總和為 100%
36
+ */
37
+ function scalePercentagesWithNewColumn(currentPercentages, newColumnPercentage, insertIndex) {
38
+ if (currentPercentages.length === 0)
39
+ return [100];
40
+ const currentTotal = currentPercentages.reduce((sum, val) => sum + val, 0);
41
+ const targetTotal = 100 - newColumnPercentage;
42
+ const scaleFactor = targetTotal / currentTotal;
43
+ const scaledPercentages = [];
44
+ // 前 n-1 個欄位使用縮放後四捨五入的值
45
+ for (let i = 0; i < currentPercentages.length - 1; i++) {
46
+ const scaledValue = Math.round(currentPercentages[i] * scaleFactor * 10) / 10;
47
+ scaledPercentages.push(scaledValue);
48
+ }
49
+ // 最後一個現有欄位使用剩餘寬度,確保總和正確
50
+ const sumOfScaled = scaledPercentages.reduce((sum, val) => sum + val, 0);
51
+ const lastScaledValue = Math.round((targetTotal - sumOfScaled) * 10) / 10;
52
+ scaledPercentages.push(lastScaledValue);
53
+ // 在指定位置插入新欄位
54
+ const result = [...scaledPercentages];
55
+ result.splice(insertIndex, 0, newColumnPercentage);
56
+ return result;
57
+ }
6
58
  /**
7
59
  * 提取表格的所有基本元素
8
60
  */
@@ -329,11 +381,9 @@ function getColumnWidths(tableElement, tableWidth) {
329
381
  }
330
382
  return widths;
331
383
  }
332
- // 否則返回平均分配的 percentage(精確到小數點後一位)
333
- const equalPercentage = Math.round((100 / columnCount) * 10) / 10;
334
- return Array(columnCount)
335
- .fill(null)
336
- .map(() => ({ type: 'percentage', value: equalPercentage }));
384
+ // 否則返回平均分配的 percentage(總和為 100%)
385
+ const percentages = distributeEqualPercentages(columnCount);
386
+ return percentages.map((value) => ({ type: 'percentage', value }));
337
387
  }
338
388
  /**
339
389
  * 設定表格的欄位寬度
@@ -349,16 +399,17 @@ function setColumnWidths(editor, tableElement, columnWidths) {
349
399
  * 計算新增欄位後的欄位寬度
350
400
  * - 如果所有欄位都是 percentage:按比例縮減現有欄位,新欄位佔平均寬度
351
401
  * - 如果有混合模式(percentage + pixel):
352
- * * percentage 欄位(pinned)保持不變
353
- * * 新欄位使用 pixel(與其他 pixel 欄位平均分配剩餘空間)
402
+ * * 如果用戶操作的欄位是 pinned column:新欄位使用 percentage(需要調整 pinned columns 的百分比)
403
+ * * 如果用戶操作的欄位是 unpinned column:新欄位使用 pixel(與其他 pixel 欄位相同寬度)
354
404
  *
355
405
  * @param currentWidths - 當前的欄位寬度陣列
356
406
  * @param insertIndex - 新欄位插入的位置(0-based)
407
+ * @param pinnedColumnIndices - 當前釘選欄位的索引陣列(插入前的索引)
408
+ * @param operatingColumnIndex - 用戶實際操作的欄位索引(用於判斷是在 pinned 還是 unpinned column 操作)
357
409
  * @returns 新的欄位寬度陣列
358
410
  */
359
- function calculateColumnWidthsAfterAdd(currentWidths, insertIndex) {
411
+ function calculateColumnWidthsAfterAdd(currentWidths, insertIndex, pinnedColumnIndices = [], operatingColumnIndex) {
360
412
  const newColumnCount = currentWidths.length + 1;
361
- const averagePercentage = Math.round((100 / newColumnCount) * 10) / 10;
362
413
  // 分離 percentage 和 pixel 欄位
363
414
  const percentageColumns = [];
364
415
  const pixelColumns = [];
@@ -372,46 +423,68 @@ function calculateColumnWidthsAfterAdd(currentWidths, insertIndex) {
372
423
  });
373
424
  // 如果所有欄位都是 percentage(正常模式,無 pinned columns)
374
425
  if (percentageColumns.length === currentWidths.length) {
375
- const currentTotal = percentageColumns.reduce((sum, col) => sum + col.value, 0);
376
- const targetTotal = 100 - averagePercentage;
377
- const scaleFactor = targetTotal / currentTotal;
378
- const newWidths = [];
379
- currentWidths.forEach((width, index) => {
380
- if (index === insertIndex) {
381
- newWidths.push({ type: 'percentage', value: averagePercentage });
382
- }
383
- // 按比例縮減現有欄位
384
- const scaledValue = Math.round(width.value * scaleFactor * 10) / 10;
385
- newWidths.push({ type: 'percentage', value: scaledValue });
386
- });
387
- if (insertIndex >= currentWidths.length) {
388
- newWidths.push({ type: 'percentage', value: averagePercentage });
389
- }
390
- return newWidths;
426
+ // 計算新欄位的平均百分比
427
+ const averagePercentage = Math.round((100 / newColumnCount) * 10) / 10;
428
+ // 提取當前的百分比值
429
+ const currentPercentages = currentWidths.map((w) => w.value);
430
+ // 等比例縮減現有欄位並插入新欄位
431
+ const newPercentages = scalePercentagesWithNewColumn(currentPercentages, averagePercentage, insertIndex);
432
+ return newPercentages.map((value) => ({ type: 'percentage', value }));
391
433
  }
392
434
  // 如果有混合的 pixel 和 percentage 欄位(有 pinned columns)
393
- // percentage 欄位(pinned)保持不變
394
- // 新欄位應維持 pixel(此時一般欄位必定是 pixel)
395
435
  if (percentageColumns.length && pixelColumns.length) {
396
- const newWidths = [];
397
- // 找到最後一個 pixel 欄位的寬度,新欄位將複製這個寬度
398
- const lastPixelWidth = pixelColumns.length > 0 ? pixelColumns[pixelColumns.length - 1].value : 150;
399
- currentWidths.forEach((width, index) => {
400
- if (index === insertIndex) {
436
+ // 判斷新欄位是否應該是 pinned column
437
+ const isNewColumnPinned = typeof operatingColumnIndex === 'number' ? pinnedColumnIndices.includes(operatingColumnIndex) : false;
438
+ if (isNewColumnPinned) {
439
+ // 新欄位應該是 pinned column,使用 percentage
440
+ // 需要調整所有 pinned columns 的百分比
441
+ const newWidths = [];
442
+ const pinnedColumnCount = pinnedColumnIndices.length + 1; // 加上新欄位
443
+ const pinnedPercentagePerColumn = Math.min(Math.round((MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE / pinnedColumnCount) * 10) / 10, MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE);
444
+ currentWidths.forEach((width, index) => {
445
+ if (index === insertIndex) {
446
+ newWidths.push({ type: 'percentage', value: pinnedPercentagePerColumn });
447
+ }
448
+ if (pinnedColumnIndices.includes(index)) {
449
+ // 調整現有 pinned column 的百分比
450
+ newWidths.push({ type: 'percentage', value: pinnedPercentagePerColumn });
451
+ }
452
+ else {
453
+ // 保持非 pinned column (pixel) 不變
454
+ newWidths.push(Object.assign({}, width));
455
+ }
456
+ });
457
+ // 如果插入位置在最後(但仍在 pinned 區域內)
458
+ if (insertIndex >= currentWidths.length) {
459
+ newWidths.push({ type: 'percentage', value: pinnedPercentagePerColumn });
460
+ }
461
+ return newWidths;
462
+ }
463
+ else {
464
+ // 新欄位應該是 unpinned column,使用 pixel
465
+ const newWidths = [];
466
+ // 找到最後一個 pixel 欄位的寬度,新欄位將複製這個寬度
467
+ const lastPixelWidth = pixelColumns.length > 0 ? pixelColumns[pixelColumns.length - 1].value : 150;
468
+ currentWidths.forEach((width, index) => {
469
+ if (index === insertIndex) {
470
+ newWidths.push({ type: 'pixel', value: lastPixelWidth });
471
+ }
472
+ newWidths.push(Object.assign({}, width));
473
+ });
474
+ // 如果插入位置在最後
475
+ if (insertIndex >= currentWidths.length) {
401
476
  newWidths.push({ type: 'pixel', value: lastPixelWidth });
402
477
  }
403
- newWidths.push(Object.assign({}, width));
404
- });
405
- // 如果插入位置在最後
406
- if (insertIndex >= currentWidths.length) {
407
- newWidths.push({ type: 'pixel', value: lastPixelWidth });
478
+ return newWidths;
408
479
  }
409
- return newWidths;
410
480
  }
411
- // Fallback: 返回原始寬度加一個平均 percentage 欄位
412
- const newWidths = [...currentWidths];
413
- newWidths.splice(insertIndex, 0, { type: 'percentage', value: averagePercentage });
414
- return newWidths;
481
+ // Fallback: 等比例縮減並插入新欄位
482
+ const averagePercentage = Math.round((100 / newColumnCount) * 10) / 10;
483
+ // 提取當前的百分比值(假設都是 percentage,否則轉為平均分配)
484
+ const currentPercentages = currentWidths.map((w) => (w.type === 'percentage' ? w.value : 100 / currentWidths.length));
485
+ // 等比例縮減現有欄位並插入新欄位
486
+ const newPercentages = scalePercentagesWithNewColumn(currentPercentages, averagePercentage, insertIndex);
487
+ return newPercentages.map((value) => ({ type: 'percentage', value }));
415
488
  }
416
489
  /**
417
490
  * 計算刪除欄位後的欄位寬度
@@ -606,19 +679,33 @@ function calculateResizedColumnWidths(currentWidths, columnIndex, deltaPercentag
606
679
  return newWidths;
607
680
  }
608
681
  /**
609
- * 移動 columnWidths 陣列中的元素位置
682
+ * 移動或交換欄位寬度設定
610
683
  * @param currentWidths - 當前的欄位寬度陣列
611
- * @param fromIndex - 來源索引
612
- * @param toIndex - 目標索引
613
- * @returns 新的欄位寬度陣列
684
+ * @param sourceIndex - 來源欄位的索引
685
+ * @param targetIndex - 目標欄位的索引
686
+ * @param mode - 'swap' 為交換兩個位置,'move' 為移動到目標位置
687
+ * @returns 處理後的欄位寬度陣列
614
688
  */
615
- function moveColumnWidth(currentWidths, fromIndex, toIndex) {
616
- if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= currentWidths.length) {
689
+ function moveOrSwapColumnWidth(currentWidths, sourceIndex, targetIndex, mode = 'move') {
690
+ if (sourceIndex === targetIndex ||
691
+ sourceIndex < 0 ||
692
+ targetIndex < 0 ||
693
+ sourceIndex >= currentWidths.length ||
694
+ targetIndex >= currentWidths.length) {
617
695
  return currentWidths;
618
696
  }
619
697
  const newWidths = [...currentWidths];
620
- const [movedWidth] = newWidths.splice(fromIndex, 1);
621
- newWidths.splice(toIndex, 0, movedWidth);
698
+ if (mode === 'swap') {
699
+ // swap 邏輯:直接交換兩個位置的值
700
+ const temp = newWidths[sourceIndex];
701
+ newWidths[sourceIndex] = newWidths[targetIndex];
702
+ newWidths[targetIndex] = temp;
703
+ }
704
+ else {
705
+ // move 邏輯:移除再插入
706
+ const [movedWidth] = newWidths.splice(sourceIndex, 1);
707
+ newWidths.splice(targetIndex, 0, movedWidth);
708
+ }
622
709
  return newWidths;
623
710
  }
624
711
  /**
@@ -649,28 +736,28 @@ function convertToMixedWidthMode(currentWidths, pinnedColumnIndices, tableWidth)
649
736
  }
650
737
  });
651
738
  // 確保釘選欄位總和不超過指定範圍
652
- if (totalPinnedPercentage > MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE) {
653
- const scaleFactor = MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE / totalPinnedPercentage;
654
- totalPinnedPercentage = MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE;
655
- // 調整釘選欄位的百分比
656
- currentWidths.forEach((width, index) => {
657
- if (pinnedColumnIndices.includes(index)) {
658
- if (width.type === 'percentage') {
659
- currentWidths[index] = {
660
- type: 'percentage',
661
- value: Math.round(width.value * scaleFactor * 10) / 10,
662
- };
663
- }
664
- else {
665
- const percentage = (width.value / tableWidth) * 100;
666
- currentWidths[index] = {
667
- type: 'percentage',
668
- value: Math.round(percentage * scaleFactor * 10) / 10,
669
- };
670
- }
739
+ const scaleFactor = totalPinnedPercentage > MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE
740
+ ? MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE / totalPinnedPercentage
741
+ : 1;
742
+ totalPinnedPercentage = Math.min(totalPinnedPercentage, MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE);
743
+ // 調整釘選欄位的百分比
744
+ currentWidths.forEach((width, index) => {
745
+ if (pinnedColumnIndices.includes(index)) {
746
+ if (width.type === 'percentage') {
747
+ currentWidths[index] = {
748
+ type: 'percentage',
749
+ value: Math.round(width.value * scaleFactor * 10) / 10,
750
+ };
671
751
  }
672
- });
673
- }
752
+ else {
753
+ const percentage = (width.value / tableWidth) * 100;
754
+ currentWidths[index] = {
755
+ type: 'percentage',
756
+ value: Math.round(percentage * scaleFactor * 10) / 10,
757
+ };
758
+ }
759
+ }
760
+ });
674
761
  // 計算剩餘空間(用於未釘選欄位)
675
762
  const remainingPercentage = 100 - totalPinnedPercentage;
676
763
  const remainingPixelWidth = (tableWidth * remainingPercentage) / 100;
@@ -689,5 +776,24 @@ function convertToMixedWidthMode(currentWidths, pinnedColumnIndices, tableWidth)
689
776
  });
690
777
  return newWidths;
691
778
  }
779
+ /**
780
+ * 將混合模式的欄位寬度轉換回純百分比模式
781
+ * 當所有 pinned columns 都被 unpin 後,需要將 pixel 欄位轉換回 percentage
782
+ * @param currentWidths - 當前的欄位寬度陣列(混合模式)
783
+ * @returns 轉換後的純百分比欄位寬度陣列,總和為 100%
784
+ */
785
+ function convertToPercentageMode(currentWidths) {
786
+ if (currentWidths.length === 0)
787
+ return [];
788
+ // 檢查是否有 pixel 欄位
789
+ const hasPixelColumns = currentWidths.some((width) => width.type === 'pixel');
790
+ // 如果沒有 pixel 欄位,表示已經是純百分比模式,直接返回
791
+ if (!hasPixelColumns) {
792
+ return currentWidths;
793
+ }
794
+ // 使用 distributeEqualPercentages 重新分配百分比,確保總和為 100%
795
+ const percentages = distributeEqualPercentages(currentWidths.length);
796
+ return percentages.map((value) => ({ type: 'percentage', value }));
797
+ }
692
798
 
693
- export { calculateColumnWidthsAfterAdd, calculateColumnWidthsAfterDelete, calculateResizedColumnWidths, collectCells, convertToMixedWidthMode, createTableCell, enforcePinnedColumnsMaxWidth, getAlignFromCells, getColumnWidths, getPinnedColumnsInfo, getReferenceRowFromHeaderOrBody, getTableElements, getTableStructure, hasAnyPinnedColumns, hasAnyPinnedRows, moveColumnWidth, setAlignForCells, setColumnWidths };
799
+ export { calculateColumnWidthsAfterAdd, calculateColumnWidthsAfterDelete, calculateResizedColumnWidths, collectCells, convertToMixedWidthMode, convertToPercentageMode, createTableCell, distributeEqualPercentages, enforcePinnedColumnsMaxWidth, getAlignFromCells, getColumnWidths, getPinnedColumnsInfo, getReferenceRowFromHeaderOrBody, getTableElements, getTableStructure, hasAnyPinnedColumns, hasAnyPinnedRows, moveOrSwapColumnWidth, scalePercentagesWithNewColumn, setAlignForCells, setColumnWidths };