@quadrats/react 1.0.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/card/components/Card.js +83 -75
- package/card/index.cjs.js +82 -74
- package/carousel/components/Carousel.js +32 -28
- package/carousel/index.cjs.js +32 -28
- package/components/Tooltip/index.js +5 -2
- package/components/index.cjs.js +5 -2
- package/core/components/Quadrats.js +5 -2
- package/core/contexts/modal/CarouselModal/CarouselModal.js +14 -17
- package/embed/renderers/base/components/BaseEmbedElement.js +51 -43
- package/embed/renderers/base/index.cjs.js +50 -42
- package/image/components/Image.js +34 -26
- package/image/createReactImage.js +1 -1
- package/image/index.cjs.js +34 -26
- package/index.cjs.js +19 -19
- package/package.json +4 -4
- package/table/components/ColumnDragButton.d.ts +10 -0
- package/table/components/ColumnDragButton.js +41 -0
- package/table/components/RowDragButton.d.ts +10 -0
- package/table/components/RowDragButton.js +42 -0
- package/table/components/Table.d.ts +9 -0
- package/table/components/Table.js +236 -0
- package/table/components/TableBody.d.ts +5 -0
- package/table/components/TableBody.js +8 -0
- package/table/components/TableCell.d.ts +5 -0
- package/table/components/TableCell.js +297 -0
- package/table/components/TableDragLayer.d.ts +6 -0
- package/table/components/TableDragLayer.js +89 -0
- package/table/components/TableHeader.d.ts +5 -0
- package/table/components/TableHeader.js +13 -0
- package/table/components/TableMain.d.ts +5 -0
- package/table/components/TableMain.js +233 -0
- package/table/components/TableRow.d.ts +5 -0
- package/table/components/TableRow.js +8 -0
- package/table/components/TableTitle.d.ts +5 -0
- package/table/components/TableTitle.js +18 -0
- package/table/contexts/TableActionsContext.d.ts +3 -0
- package/table/contexts/TableActionsContext.js +5 -0
- package/table/contexts/TableDragContext.d.ts +26 -0
- package/table/contexts/TableDragContext.js +26 -0
- package/table/contexts/TableHeaderContext.d.ts +2 -0
- package/table/contexts/TableHeaderContext.js +7 -0
- package/table/contexts/TableMetadataContext.d.ts +3 -0
- package/table/contexts/TableMetadataContext.js +5 -0
- package/table/contexts/TableScrollContext.d.ts +2 -0
- package/table/contexts/TableScrollContext.js +9 -0
- package/table/contexts/TableStateContext.d.ts +3 -0
- package/table/contexts/TableStateContext.js +5 -0
- package/table/createReactTable.d.ts +4 -0
- package/table/createReactTable.js +297 -0
- package/table/defaultRenderTableElements.d.ts +2 -0
- package/table/defaultRenderTableElements.js +20 -0
- package/table/hooks/useColumnResize.d.ts +12 -0
- package/table/hooks/useColumnResize.js +168 -0
- package/table/hooks/useTableActions.d.ts +27 -0
- package/table/hooks/useTableActions.js +1092 -0
- package/table/hooks/useTableActionsContext.d.ts +1 -0
- package/table/hooks/useTableActionsContext.js +12 -0
- package/table/hooks/useTableCell.d.ts +16 -0
- package/table/hooks/useTableCell.js +166 -0
- package/table/hooks/useTableCellToolbarActions.d.ts +34 -0
- package/table/hooks/useTableCellToolbarActions.js +526 -0
- package/table/hooks/useTableMetadata.d.ts +1 -0
- package/table/hooks/useTableMetadata.js +12 -0
- package/table/hooks/useTableStateContext.d.ts +1 -0
- package/table/hooks/useTableStateContext.js +12 -0
- package/table/hooks/useTableStates.d.ts +18 -0
- package/table/hooks/useTableStates.js +14 -0
- package/table/index.cjs.js +4002 -0
- package/table/index.d.ts +16 -0
- package/table/index.js +27 -0
- package/table/jsx-serializer/components/Table.d.ts +3 -0
- package/table/jsx-serializer/components/Table.js +7 -0
- package/table/jsx-serializer/components/TableBody.d.ts +3 -0
- package/table/jsx-serializer/components/TableBody.js +7 -0
- package/table/jsx-serializer/components/TableCell.d.ts +5 -0
- package/table/jsx-serializer/components/TableCell.js +33 -0
- package/table/jsx-serializer/components/TableHeader.d.ts +3 -0
- package/table/jsx-serializer/components/TableHeader.js +10 -0
- package/table/jsx-serializer/components/TableMain.d.ts +6 -0
- package/table/jsx-serializer/components/TableMain.js +18 -0
- package/table/jsx-serializer/components/TableRow.d.ts +3 -0
- package/table/jsx-serializer/components/TableRow.js +7 -0
- package/table/jsx-serializer/components/TableTitle.d.ts +3 -0
- package/table/jsx-serializer/components/TableTitle.js +7 -0
- package/table/jsx-serializer/contexts/TableHeaderContext.d.ts +1 -0
- package/table/jsx-serializer/contexts/TableHeaderContext.js +5 -0
- package/table/jsx-serializer/contexts/TableScrollContext.d.ts +2 -0
- package/table/jsx-serializer/contexts/TableScrollContext.js +7 -0
- package/table/jsx-serializer/createJsxSerializeTable.d.ts +5 -0
- package/table/jsx-serializer/createJsxSerializeTable.js +113 -0
- package/table/jsx-serializer/defaultRenderTableElements.d.ts +2 -0
- package/table/jsx-serializer/defaultRenderTableElements.js +20 -0
- package/table/jsx-serializer/index.cjs.js +195 -0
- package/table/jsx-serializer/index.d.ts +3 -0
- package/table/jsx-serializer/index.js +2 -0
- package/table/jsx-serializer/package.json +7 -0
- package/table/jsx-serializer/typings.d.ts +12 -0
- package/table/package.json +10 -0
- package/table/table.css +1 -0
- package/table/table.scss +428 -0
- package/table/toolbar/TableToolbarIcon.d.ts +8 -0
- package/table/toolbar/TableToolbarIcon.js +12 -0
- package/table/toolbar/index.cjs.js +24 -0
- package/table/toolbar/index.d.ts +2 -0
- package/table/toolbar/index.js +2 -0
- package/table/toolbar/package.json +7 -0
- package/table/toolbar/useTableTool.d.ts +4 -0
- package/table/toolbar/useTableTool.js +13 -0
- package/table/typings.d.ts +68 -0
- package/table/utils/helper.d.ts +186 -0
- package/table/utils/helper.js +799 -0
- package/toolbar/components/InlineToolbar.d.ts +12 -11
- package/toolbar/components/InlineToolbar.js +23 -19
- package/toolbar/components/Toolbar.js +2 -2
- package/toolbar/index.cjs.js +24 -21
- package/toolbar/toolbar.css +1 -1
- package/toolbar/toolbar.scss +4 -1
- package/utils/index.cjs.js +7 -1
- package/utils/removePreviousElement.js +7 -1
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import { Element, Transforms, PARAGRAPH_TYPE } from '@quadrats/core';
|
|
2
|
+
import { ReactEditor } from 'slate-react';
|
|
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
|
+
import { ALIGNABLE_TYPES, ALIGN_TYPE } from '@quadrats/common/align';
|
|
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
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 提取表格的所有基本元素
|
|
60
|
+
*/
|
|
61
|
+
function getTableElements(element) {
|
|
62
|
+
const tableMainElement = element.children.find((child) => Element.isElement(child) && child.type.includes(TABLE_MAIN_TYPE));
|
|
63
|
+
if (!tableMainElement || !Element.isElement(tableMainElement)) {
|
|
64
|
+
return {
|
|
65
|
+
tableMainElement: null,
|
|
66
|
+
tableBodyElement: null,
|
|
67
|
+
tableHeaderElement: null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const tableBodyElement = tableMainElement.children.find((child) => Element.isElement(child) && child.type.includes(TABLE_BODY_TYPE));
|
|
71
|
+
const tableHeaderElement = tableMainElement.children.find((child) => Element.isElement(child) && child.type.includes(TABLE_HEADER_TYPE));
|
|
72
|
+
return {
|
|
73
|
+
tableMainElement: Element.isElement(tableMainElement) ? tableMainElement : null,
|
|
74
|
+
tableBodyElement: Element.isElement(tableBodyElement) ? tableBodyElement : null,
|
|
75
|
+
tableHeaderElement: Element.isElement(tableHeaderElement) ? tableHeaderElement : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 獲取表格的完整結構
|
|
80
|
+
*/
|
|
81
|
+
function getTableStructure(editor, element) {
|
|
82
|
+
var _a;
|
|
83
|
+
const elements = getTableElements(element);
|
|
84
|
+
if (!elements.tableMainElement || !elements.tableBodyElement) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const tableMainPath = ReactEditor.findPath(editor, elements.tableMainElement);
|
|
88
|
+
const tableBodyPath = ReactEditor.findPath(editor, elements.tableBodyElement);
|
|
89
|
+
const tableHeaderPath = elements.tableHeaderElement
|
|
90
|
+
? ReactEditor.findPath(editor, elements.tableHeaderElement)
|
|
91
|
+
: null;
|
|
92
|
+
const headerRowCount = ((_a = elements.tableHeaderElement) === null || _a === void 0 ? void 0 : _a.children.length) || 0;
|
|
93
|
+
const firstRow = elements.tableBodyElement.children[0];
|
|
94
|
+
const columnCount = Element.isElement(firstRow) ? firstRow.children.length : 0;
|
|
95
|
+
return Object.assign(Object.assign({}, elements), { tableMainPath,
|
|
96
|
+
tableBodyPath,
|
|
97
|
+
tableHeaderPath,
|
|
98
|
+
headerRowCount,
|
|
99
|
+
columnCount, firstRow: Element.isElement(firstRow) ? firstRow : null });
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 檢查是否有任何 pinned columns
|
|
103
|
+
*/
|
|
104
|
+
function hasAnyPinnedColumns(tableStructure) {
|
|
105
|
+
if (Element.isElement(tableStructure.tableBodyElement)) {
|
|
106
|
+
const lastRow = tableStructure.tableBodyElement.children[tableStructure.tableBodyElement.children.length - 1];
|
|
107
|
+
if (Element.isElement(lastRow) && lastRow.type.includes(TABLE_ROW_TYPE)) {
|
|
108
|
+
if (lastRow.children[0] &&
|
|
109
|
+
Element.isElement(lastRow.children[0]) &&
|
|
110
|
+
lastRow.children[0].type.includes(TABLE_CELL_TYPE)) {
|
|
111
|
+
// 如果最後一行 body row 的第一個 cell 是 pinned,代表有任何 pinned columns
|
|
112
|
+
return lastRow.children[0].pinned === true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 檢查是否有任何 pinned rows
|
|
120
|
+
*/
|
|
121
|
+
function hasAnyPinnedRows(tableStructure) {
|
|
122
|
+
if (!tableStructure.tableHeaderElement)
|
|
123
|
+
return false;
|
|
124
|
+
if (tableStructure.tableHeaderElement.children[0]) {
|
|
125
|
+
const firstRow = tableStructure.tableHeaderElement.children[0];
|
|
126
|
+
if (Element.isElement(firstRow) && firstRow.type.includes(TABLE_ROW_TYPE)) {
|
|
127
|
+
// 如果第一個 header row 的所有 cell 都是 pinned,代表有任何 pinned rows
|
|
128
|
+
return firstRow.children.every((cell) => Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE) && cell.pinned);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 創建新的 table cell
|
|
135
|
+
*/
|
|
136
|
+
function createTableCell(referenceCell, overrideProps) {
|
|
137
|
+
var _a;
|
|
138
|
+
const baseCell = {
|
|
139
|
+
type: TABLE_CELL_TYPE,
|
|
140
|
+
children: [
|
|
141
|
+
{
|
|
142
|
+
type: PARAGRAPH_TYPE,
|
|
143
|
+
children: [{ text: '' }],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
if (referenceCell) {
|
|
148
|
+
if (referenceCell.treatAsTitle) {
|
|
149
|
+
baseCell.treatAsTitle = true;
|
|
150
|
+
}
|
|
151
|
+
if (referenceCell.pinned) {
|
|
152
|
+
baseCell.pinned = true;
|
|
153
|
+
}
|
|
154
|
+
if ((_a = referenceCell.children[0]) === null || _a === void 0 ? void 0 : _a.align) {
|
|
155
|
+
baseCell.children[0].align = referenceCell.children[0].align;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return Object.assign(Object.assign({}, baseCell), overrideProps);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 獲取參考 row
|
|
162
|
+
*/
|
|
163
|
+
function getReferenceRowFromHeaderOrBody(HeaderOrBodyContainer, rowIndex) {
|
|
164
|
+
const row = HeaderOrBodyContainer.children[rowIndex];
|
|
165
|
+
if (!Element.isElement(row) || !row.type.includes(TABLE_ROW_TYPE)) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
return row;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 收集指定範圍的 cells
|
|
172
|
+
* @param tableStructure - 表格結構
|
|
173
|
+
* @param scope - 'table' | 'column'
|
|
174
|
+
* @param columnIndex - 當 scope 為 'column' 時需要指定
|
|
175
|
+
* @returns cells 陣列
|
|
176
|
+
*/
|
|
177
|
+
function collectCells(tableStructure, scope, columnIndex) {
|
|
178
|
+
const containers = [tableStructure.tableBodyElement, tableStructure.tableHeaderElement].filter(Boolean);
|
|
179
|
+
const cells = [];
|
|
180
|
+
for (const container of containers) {
|
|
181
|
+
if (!Element.isElement(container))
|
|
182
|
+
continue;
|
|
183
|
+
for (const row of container.children) {
|
|
184
|
+
if (!Element.isElement(row))
|
|
185
|
+
continue;
|
|
186
|
+
if (scope === 'column' && typeof columnIndex === 'number') {
|
|
187
|
+
// 收集指定 column 的 cells
|
|
188
|
+
const cell = row.children[columnIndex];
|
|
189
|
+
if (Element.isElement(cell)) {
|
|
190
|
+
cells.push(cell);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (scope === 'table') {
|
|
194
|
+
// 收集所有 cells
|
|
195
|
+
for (const cell of row.children) {
|
|
196
|
+
if (Element.isElement(cell)) {
|
|
197
|
+
cells.push(cell);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return cells;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* 設定指定 cells 的 align
|
|
207
|
+
* @param editor - Slate editor
|
|
208
|
+
* @param cells - 要設定 align 的 cell 元素陣列
|
|
209
|
+
* @param alignValue - align 值
|
|
210
|
+
*/
|
|
211
|
+
function setAlignForCells(editor, cells, alignValue) {
|
|
212
|
+
for (const cell of cells) {
|
|
213
|
+
if (!Element.isElement(cell))
|
|
214
|
+
continue;
|
|
215
|
+
const cellPath = ReactEditor.findPath(editor, cell);
|
|
216
|
+
// 對 cell 內的所有可 align 的元素設定 align
|
|
217
|
+
for (let contentIndex = 0; contentIndex < cell.children.length; contentIndex++) {
|
|
218
|
+
const content = cell.children[contentIndex];
|
|
219
|
+
if (Element.isElement(content) &&
|
|
220
|
+
content.type &&
|
|
221
|
+
ALIGNABLE_TYPES.includes(content.type)) {
|
|
222
|
+
const contentPath = [...cellPath, contentIndex];
|
|
223
|
+
Transforms.setNodes(editor, { [ALIGN_TYPE]: alignValue }, {
|
|
224
|
+
at: contentPath,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 獲取指定 cells 的 align 狀態
|
|
232
|
+
* @param cells - 要檢查的 cell 元素陣列
|
|
233
|
+
* @returns 如果所有 cell 的 align 都相同則返回該值,否則返回 'left'
|
|
234
|
+
*/
|
|
235
|
+
function getAlignFromCells(cells) {
|
|
236
|
+
const alignValues = [];
|
|
237
|
+
for (const cell of cells) {
|
|
238
|
+
if (!Element.isElement(cell))
|
|
239
|
+
continue;
|
|
240
|
+
// 檢查 cell 內的第一個可 align 元素
|
|
241
|
+
for (const content of cell.children) {
|
|
242
|
+
if (Element.isElement(content) &&
|
|
243
|
+
content.type &&
|
|
244
|
+
ALIGNABLE_TYPES.includes(content.type)) {
|
|
245
|
+
const alignValue = content[ALIGN_TYPE];
|
|
246
|
+
if (alignValue) {
|
|
247
|
+
alignValues.push(alignValue);
|
|
248
|
+
}
|
|
249
|
+
break; // 只檢查第一個可 align 元素
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// 如果所有 align 值都相同,返回該值;否則返回預設的 'left'
|
|
254
|
+
if (alignValues.length > 0) {
|
|
255
|
+
const firstAlign = alignValues[0];
|
|
256
|
+
const allSame = alignValues.every((align) => align === firstAlign);
|
|
257
|
+
if (allSame) {
|
|
258
|
+
return firstAlign;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return 'left';
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 獲取釘選欄位的資訊
|
|
265
|
+
* @param tableElement - 表格最外層元素
|
|
266
|
+
* @returns 釘選欄位的索引陣列和總寬度百分比
|
|
267
|
+
*/
|
|
268
|
+
function getPinnedColumnsInfo(tableElement) {
|
|
269
|
+
const { tableBodyElement } = getTableElements(tableElement);
|
|
270
|
+
if (!tableBodyElement || !Element.isElement(tableBodyElement)) {
|
|
271
|
+
return { pinnedColumnIndices: [], totalPinnedPercentage: 0 };
|
|
272
|
+
}
|
|
273
|
+
const firstRow = tableBodyElement.children[0];
|
|
274
|
+
if (!Element.isElement(firstRow)) {
|
|
275
|
+
return { pinnedColumnIndices: [], totalPinnedPercentage: 0 };
|
|
276
|
+
}
|
|
277
|
+
const pinnedColumnIndices = [];
|
|
278
|
+
let totalPinnedPercentage = 0;
|
|
279
|
+
firstRow.children.forEach((cell, index) => {
|
|
280
|
+
if (Element.isElement(cell) && cell.treatAsTitle && cell.pinned) {
|
|
281
|
+
pinnedColumnIndices.push(index);
|
|
282
|
+
// 如果有設定 columnWidths,使用設定的寬度
|
|
283
|
+
if (tableElement.columnWidths && tableElement.columnWidths[index]) {
|
|
284
|
+
const width = tableElement.columnWidths[index];
|
|
285
|
+
if (width.type === 'percentage') {
|
|
286
|
+
totalPinnedPercentage += width.value;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return { pinnedColumnIndices, totalPinnedPercentage };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 強制調整釘選欄位寬度以符合最大限制
|
|
295
|
+
* @param columnWidths - 當前欄位寬度陣列
|
|
296
|
+
* @param pinnedColumnIndices - 釘選欄位索引陣列
|
|
297
|
+
* @returns 調整後的欄位寬度陣列
|
|
298
|
+
*/
|
|
299
|
+
function enforcePinnedColumnsMaxWidth(columnWidths, pinnedColumnIndices) {
|
|
300
|
+
if (pinnedColumnIndices.length === 0) {
|
|
301
|
+
return columnWidths;
|
|
302
|
+
}
|
|
303
|
+
// 計算釘選欄位的總寬度
|
|
304
|
+
let totalPinnedPercentage = 0;
|
|
305
|
+
pinnedColumnIndices.forEach((index) => {
|
|
306
|
+
const width = columnWidths[index];
|
|
307
|
+
if (width && width.type === 'percentage') {
|
|
308
|
+
totalPinnedPercentage += width.value;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// 如果超過最大限制,按比例縮減
|
|
312
|
+
if (totalPinnedPercentage > MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE) {
|
|
313
|
+
const scaleFactor = MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE / totalPinnedPercentage;
|
|
314
|
+
const newWidths = [...columnWidths];
|
|
315
|
+
pinnedColumnIndices.forEach((index) => {
|
|
316
|
+
const width = newWidths[index];
|
|
317
|
+
if (width && width.type === 'percentage') {
|
|
318
|
+
newWidths[index] = {
|
|
319
|
+
type: 'percentage',
|
|
320
|
+
value: Math.round(width.value * scaleFactor * 10) / 10,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
return newWidths;
|
|
325
|
+
}
|
|
326
|
+
return columnWidths;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 獲取表格的欄位寬度陣列
|
|
330
|
+
* 當有釘選欄位時:
|
|
331
|
+
* - 釘選欄位使用 percentage
|
|
332
|
+
* - 未釘選欄位使用 pixel(基於剩餘空間平均分配)
|
|
333
|
+
* @param tableElement - 表格最外層元素
|
|
334
|
+
* @param tableWidth - 表格容器的實際寬度(pixel),用於計算 pixel 寬度
|
|
335
|
+
* @returns 欄位寬度陣列
|
|
336
|
+
*/
|
|
337
|
+
function getColumnWidths(tableElement, tableWidth) {
|
|
338
|
+
const tableStructure = getTableElements(tableElement);
|
|
339
|
+
const { tableBodyElement } = tableStructure;
|
|
340
|
+
if (!tableBodyElement || !Element.isElement(tableBodyElement)) {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
const firstRow = tableBodyElement.children[0];
|
|
344
|
+
if (!Element.isElement(firstRow)) {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
const columnCount = firstRow.children.length;
|
|
348
|
+
// 獲取釘選欄位資訊
|
|
349
|
+
const { pinnedColumnIndices } = getPinnedColumnsInfo(tableElement);
|
|
350
|
+
const hasPinnedColumns = pinnedColumnIndices.length > 0;
|
|
351
|
+
// 如果 tableElement 有 columnWidths
|
|
352
|
+
if (tableElement.columnWidths && tableElement.columnWidths.length === columnCount) {
|
|
353
|
+
let widths = [...tableElement.columnWidths];
|
|
354
|
+
// 強制檢查釘選欄位是否超過 40%,如果超過則調整
|
|
355
|
+
if (hasPinnedColumns) {
|
|
356
|
+
widths = enforcePinnedColumnsMaxWidth(widths, pinnedColumnIndices);
|
|
357
|
+
}
|
|
358
|
+
return widths;
|
|
359
|
+
}
|
|
360
|
+
// 如果沒有設定 columnWidths,需要初始化
|
|
361
|
+
// 如果有釘選欄位,使用混合模式
|
|
362
|
+
if (hasPinnedColumns && tableWidth) {
|
|
363
|
+
const widths = [];
|
|
364
|
+
// 先計算釘選欄位的總寬度
|
|
365
|
+
const pinnedPercentagePerColumn = Math.min(Math.round((MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE / pinnedColumnIndices.length) * 10) / 10, MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE);
|
|
366
|
+
// 計算實際釘選欄位總百分比
|
|
367
|
+
const actualPinnedPercentage = Math.min(pinnedPercentagePerColumn * pinnedColumnIndices.length, MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE);
|
|
368
|
+
// 計算剩餘空間(pixel)
|
|
369
|
+
const remainingPercentage = 100 - actualPinnedPercentage;
|
|
370
|
+
const remainingPixelWidth = (tableWidth * remainingPercentage) / 100;
|
|
371
|
+
const unpinnedColumnCount = columnCount - pinnedColumnIndices.length;
|
|
372
|
+
const pixelWidthPerColumn = unpinnedColumnCount > 0 ? Math.floor(remainingPixelWidth / unpinnedColumnCount) : 0;
|
|
373
|
+
// 建立寬度陣列
|
|
374
|
+
for (let i = 0; i < columnCount; i++) {
|
|
375
|
+
if (pinnedColumnIndices.includes(i)) {
|
|
376
|
+
widths.push({ type: 'percentage', value: pinnedPercentagePerColumn });
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
widths.push({ type: 'pixel', value: pixelWidthPerColumn });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return widths;
|
|
383
|
+
}
|
|
384
|
+
// 否則返回平均分配的 percentage(總和為 100%)
|
|
385
|
+
const percentages = distributeEqualPercentages(columnCount);
|
|
386
|
+
return percentages.map((value) => ({ type: 'percentage', value }));
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* 設定表格的欄位寬度
|
|
390
|
+
* @param editor - Slate editor
|
|
391
|
+
* @param tableElement - 表格最外層元素
|
|
392
|
+
* @param columnWidths - 欄位寬度陣列
|
|
393
|
+
*/
|
|
394
|
+
function setColumnWidths(editor, tableElement, columnWidths) {
|
|
395
|
+
const tablePath = ReactEditor.findPath(editor, tableElement);
|
|
396
|
+
Transforms.setNodes(editor, { columnWidths: [...columnWidths] }, { at: tablePath });
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* 計算新增欄位後的欄位寬度
|
|
400
|
+
* - 如果所有欄位都是 percentage:按比例縮減現有欄位,新欄位佔平均寬度
|
|
401
|
+
* - 如果有混合模式(percentage + pixel):
|
|
402
|
+
* * 如果用戶操作的欄位是 pinned column:新欄位使用 percentage(需要調整 pinned columns 的百分比)
|
|
403
|
+
* * 如果用戶操作的欄位是 unpinned column:新欄位使用 pixel(與其他 pixel 欄位相同寬度)
|
|
404
|
+
*
|
|
405
|
+
* @param currentWidths - 當前的欄位寬度陣列
|
|
406
|
+
* @param insertIndex - 新欄位插入的位置(0-based)
|
|
407
|
+
* @param pinnedColumnIndices - 當前釘選欄位的索引陣列(插入前的索引)
|
|
408
|
+
* @param operatingColumnIndex - 用戶實際操作的欄位索引(用於判斷是在 pinned 還是 unpinned column 操作)
|
|
409
|
+
* @returns 新的欄位寬度陣列
|
|
410
|
+
*/
|
|
411
|
+
function calculateColumnWidthsAfterAdd(currentWidths, insertIndex, pinnedColumnIndices = [], operatingColumnIndex) {
|
|
412
|
+
const newColumnCount = currentWidths.length + 1;
|
|
413
|
+
// 分離 percentage 和 pixel 欄位
|
|
414
|
+
const percentageColumns = [];
|
|
415
|
+
const pixelColumns = [];
|
|
416
|
+
currentWidths.forEach((width, index) => {
|
|
417
|
+
if (width.type === 'percentage') {
|
|
418
|
+
percentageColumns.push({ index, value: width.value });
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
pixelColumns.push({ index, value: width.value });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
// 如果所有欄位都是 percentage(正常模式,無 pinned columns)
|
|
425
|
+
if (percentageColumns.length === currentWidths.length) {
|
|
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 }));
|
|
433
|
+
}
|
|
434
|
+
// 如果有混合的 pixel 和 percentage 欄位(有 pinned columns)
|
|
435
|
+
if (percentageColumns.length && pixelColumns.length) {
|
|
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) {
|
|
476
|
+
newWidths.push({ type: 'pixel', value: lastPixelWidth });
|
|
477
|
+
}
|
|
478
|
+
return newWidths;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
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 }));
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* 計算刪除欄位後的欄位寬度
|
|
491
|
+
* 此函數會智慧處理欄位寬度的重新分配:
|
|
492
|
+
* - 如果所有欄位都是 percentage:按比例放大剩餘欄位
|
|
493
|
+
* - 如果有 pixel 欄位:保持 pixel 欄位不變,只調整 percentage 欄位
|
|
494
|
+
*
|
|
495
|
+
* @param currentWidths - 當前的欄位寬度陣列
|
|
496
|
+
* @param deleteIndex - 要刪除的欄位索引(0-based)
|
|
497
|
+
* @returns 新的欄位寬度陣列
|
|
498
|
+
*/
|
|
499
|
+
function calculateColumnWidthsAfterDelete(currentWidths, deleteIndex) {
|
|
500
|
+
if (currentWidths.length <= 1) {
|
|
501
|
+
return currentWidths;
|
|
502
|
+
}
|
|
503
|
+
const deletedWidth = currentWidths[deleteIndex];
|
|
504
|
+
const newWidths = currentWidths.filter((_, index) => index !== deleteIndex);
|
|
505
|
+
// 如果刪除的是 pixel 欄位,其他欄位保持不變
|
|
506
|
+
if (deletedWidth.type === 'pixel') {
|
|
507
|
+
return newWidths;
|
|
508
|
+
}
|
|
509
|
+
// 刪除的是 percentage 欄位
|
|
510
|
+
const deletedPercentage = deletedWidth.value;
|
|
511
|
+
// 分離 percentage 和 pixel 欄位
|
|
512
|
+
const percentageIndices = [];
|
|
513
|
+
newWidths.forEach((width, index) => {
|
|
514
|
+
if (width.type === 'percentage') {
|
|
515
|
+
percentageIndices.push(index);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
// 如果沒有 percentage 欄位,直接返回
|
|
519
|
+
if (percentageIndices.length === 0) {
|
|
520
|
+
return newWidths;
|
|
521
|
+
}
|
|
522
|
+
// 將刪除欄位的百分比按比例分配給其他 percentage 欄位
|
|
523
|
+
const currentPercentageTotal = percentageIndices.reduce((sum, index) => sum + newWidths[index].value, 0);
|
|
524
|
+
return newWidths.map((width) => {
|
|
525
|
+
if (width.type === 'percentage') {
|
|
526
|
+
const proportion = width.value / currentPercentageTotal;
|
|
527
|
+
const additionalPercentage = deletedPercentage * proportion;
|
|
528
|
+
const newValue = Math.round((width.value + additionalPercentage) * 10) / 10;
|
|
529
|
+
return { type: 'percentage', value: newValue };
|
|
530
|
+
}
|
|
531
|
+
return width;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* 計算拖曳後的欄位寬度
|
|
536
|
+
* 處理釘選欄位的特殊邏輯:
|
|
537
|
+
* - 情況 1:未釘選欄位(pixel)與未釘選欄位(pixel)之間:直接改變 pixel 值,允許超出容器
|
|
538
|
+
* - 情況 2:釘選欄位(percentage)與釘選欄位(percentage)之間:互相調整,且總和不超過 40%
|
|
539
|
+
* - 情況 3:釘選欄位(percentage)與未釘選欄位(pixel)之間:
|
|
540
|
+
* * 只調整釘選欄位的百分比(不超過 40% 總限制)
|
|
541
|
+
* * 當 table 未溢出(寬度 < container)時,重新計算所有未釘選欄位的 pixel 值以維持 100%
|
|
542
|
+
* * 當 table 已溢出時,只調整下一個欄位的 pixel 值
|
|
543
|
+
* * 當達到 40% 上限時,停止調整
|
|
544
|
+
* - 預設情況:兩個都是 percentage 但都不是釘選欄位(正常模式)
|
|
545
|
+
*
|
|
546
|
+
* @param currentWidths - 當前的欄位寬度陣列
|
|
547
|
+
* @param columnIndex - 被調整的欄位索引
|
|
548
|
+
* @param deltaPercentage - 寬度變化量(百分比)
|
|
549
|
+
* @param deltaPixel - 寬度變化量(pixel)
|
|
550
|
+
* @param pinnedColumnIndices - 釘選欄位的索引陣列
|
|
551
|
+
* @returns 新的欄位寬度陣列
|
|
552
|
+
*/
|
|
553
|
+
function calculateResizedColumnWidths(currentWidths, columnIndex, deltaPercentage, deltaPixel, pinnedColumnIndices = []) {
|
|
554
|
+
const newWidths = [...currentWidths];
|
|
555
|
+
const nextColumnIndex = columnIndex + 1;
|
|
556
|
+
const isLastColumn = nextColumnIndex >= newWidths.length;
|
|
557
|
+
const currentCol = newWidths[columnIndex];
|
|
558
|
+
const nextCol = isLastColumn ? null : newWidths[nextColumnIndex];
|
|
559
|
+
const isCurrentPinned = pinnedColumnIndices.includes(columnIndex);
|
|
560
|
+
const isNextPinned = nextCol ? pinnedColumnIndices.includes(nextColumnIndex) : false;
|
|
561
|
+
// **特殊情況:最後一欄,只調整當前欄位**
|
|
562
|
+
// 最後一欄必定不是 pinned column
|
|
563
|
+
if (isLastColumn) {
|
|
564
|
+
if (currentCol.type === 'pixel') {
|
|
565
|
+
// pixel 欄位(有 pinned column 情境):直接調整寬度
|
|
566
|
+
const currentPixel = currentCol.value;
|
|
567
|
+
const newCurrentPixel = Math.max(MIN_COLUMN_WIDTH_PIXEL, currentPixel + deltaPixel);
|
|
568
|
+
newWidths[columnIndex] = { type: 'pixel', value: Math.floor(newCurrentPixel) };
|
|
569
|
+
}
|
|
570
|
+
else if (currentCol.type === 'percentage') {
|
|
571
|
+
// percentage 欄位(無 pinned column 情境):調整當前欄位並將差額分配給前一欄以確保總和為 100%
|
|
572
|
+
const currentWidth = currentCol.value;
|
|
573
|
+
let newCurrentWidth = Math.max(MIN_COLUMN_WIDTH_PERCENTAGE, Math.round((currentWidth + deltaPercentage) * 10) / 10);
|
|
574
|
+
// 計算其他欄位的總百分比
|
|
575
|
+
const otherColumnsTotal = newWidths
|
|
576
|
+
.filter((_, idx) => idx !== columnIndex)
|
|
577
|
+
.reduce((sum, width) => sum + (width.type === 'percentage' ? width.value : 0), 0);
|
|
578
|
+
// 確保總和為 100%
|
|
579
|
+
const maxAllowedWidth = 100 - otherColumnsTotal;
|
|
580
|
+
newCurrentWidth = Math.min(newCurrentWidth, maxAllowedWidth);
|
|
581
|
+
// 計算寬度變化量
|
|
582
|
+
const widthChange = currentWidth - newCurrentWidth;
|
|
583
|
+
// 如果有寬度變化且存在前一欄,將差額分配給前一欄
|
|
584
|
+
if (widthChange !== 0 && columnIndex > 0) {
|
|
585
|
+
const prevCol = newWidths[columnIndex - 1];
|
|
586
|
+
if (prevCol.type === 'percentage') {
|
|
587
|
+
const newPrevWidth = Math.max(MIN_COLUMN_WIDTH_PERCENTAGE, Math.round((prevCol.value + widthChange) * 10) / 10);
|
|
588
|
+
newWidths[columnIndex - 1] = { type: 'percentage', value: newPrevWidth };
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
newWidths[columnIndex] = { type: 'percentage', value: newCurrentWidth };
|
|
592
|
+
}
|
|
593
|
+
return newWidths;
|
|
594
|
+
}
|
|
595
|
+
// 以下是有 next column 的邏輯
|
|
596
|
+
if (!nextCol) {
|
|
597
|
+
return newWidths;
|
|
598
|
+
}
|
|
599
|
+
// 情況 1:當前欄位是 pixel(未釘選),下一欄位也是 pixel(未釘選)
|
|
600
|
+
if (currentCol.type === 'pixel' && nextCol.type === 'pixel') {
|
|
601
|
+
const currentPixel = currentCol.value;
|
|
602
|
+
const newCurrentPixel = Math.max(MIN_COLUMN_WIDTH_PIXEL, currentPixel + deltaPixel);
|
|
603
|
+
newWidths[columnIndex] = { type: 'pixel', value: Math.floor(newCurrentPixel) };
|
|
604
|
+
return newWidths;
|
|
605
|
+
}
|
|
606
|
+
// 情況 2:當前欄位是 percentage(釘選),下一欄位也是 percentage(釘選)
|
|
607
|
+
if (currentCol.type === 'percentage' && nextCol.type === 'percentage' && isCurrentPinned && isNextPinned) {
|
|
608
|
+
const currentWidth = currentCol.value;
|
|
609
|
+
const nextWidth = nextCol.value;
|
|
610
|
+
// 計算新寬度
|
|
611
|
+
let newCurrentWidth = Math.round((currentWidth + deltaPercentage) * 10) / 10;
|
|
612
|
+
let newNextWidth = Math.round((nextWidth - deltaPercentage) * 10) / 10;
|
|
613
|
+
if (newCurrentWidth < MIN_COLUMN_WIDTH_PERCENTAGE) {
|
|
614
|
+
newCurrentWidth = MIN_COLUMN_WIDTH_PERCENTAGE;
|
|
615
|
+
newNextWidth = Math.round((currentWidth + nextWidth - MIN_COLUMN_WIDTH_PERCENTAGE) * 10) / 10;
|
|
616
|
+
}
|
|
617
|
+
else if (newNextWidth < MIN_COLUMN_WIDTH_PERCENTAGE) {
|
|
618
|
+
newNextWidth = MIN_COLUMN_WIDTH_PERCENTAGE;
|
|
619
|
+
newCurrentWidth = Math.round((currentWidth + nextWidth - MIN_COLUMN_WIDTH_PERCENTAGE) * 10) / 10;
|
|
620
|
+
}
|
|
621
|
+
// 確保釘選欄位總和不超過 40%
|
|
622
|
+
const otherPinnedTotal = pinnedColumnIndices
|
|
623
|
+
.filter((idx) => idx !== columnIndex && idx !== nextColumnIndex)
|
|
624
|
+
.reduce((sum, idx) => sum + (newWidths[idx].type === 'percentage' ? newWidths[idx].value : 0), 0);
|
|
625
|
+
const twoColumnsTotal = newCurrentWidth + newNextWidth;
|
|
626
|
+
if (otherPinnedTotal + twoColumnsTotal > MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE) {
|
|
627
|
+
// 按比例縮減這兩個欄位
|
|
628
|
+
const allowedTotal = MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE - otherPinnedTotal;
|
|
629
|
+
const scale = allowedTotal / twoColumnsTotal;
|
|
630
|
+
newCurrentWidth = Math.round(newCurrentWidth * scale * 10) / 10;
|
|
631
|
+
newNextWidth = Math.round(newNextWidth * scale * 10) / 10;
|
|
632
|
+
}
|
|
633
|
+
newWidths[columnIndex] = { type: 'percentage', value: newCurrentWidth };
|
|
634
|
+
newWidths[nextColumnIndex] = { type: 'percentage', value: newNextWidth };
|
|
635
|
+
return newWidths;
|
|
636
|
+
}
|
|
637
|
+
// 情況 3:當前欄位是 percentage(釘選),下一欄位是 pixel(未釘選)
|
|
638
|
+
if (currentCol.type === 'percentage' && nextCol.type === 'pixel' && isCurrentPinned && !isNextPinned) {
|
|
639
|
+
const currentWidth = currentCol.value;
|
|
640
|
+
// 計算新的釘選欄位寬度
|
|
641
|
+
let newCurrentWidth = Math.round((currentWidth + deltaPercentage) * 10) / 10;
|
|
642
|
+
newCurrentWidth = Math.max(MIN_COLUMN_WIDTH_PERCENTAGE, newCurrentWidth);
|
|
643
|
+
// 確保所有釘選欄位總和不超過 40%
|
|
644
|
+
const otherPinnedTotal = pinnedColumnIndices
|
|
645
|
+
.filter((idx) => idx !== columnIndex)
|
|
646
|
+
.reduce((sum, idx) => sum + (newWidths[idx].type === 'percentage' ? newWidths[idx].value : 0), 0);
|
|
647
|
+
const maxAllowedWidth = MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE - otherPinnedTotal;
|
|
648
|
+
// 計算調整前後的實際寬度
|
|
649
|
+
const beforeAdjustWidth = Math.min(currentWidth, maxAllowedWidth);
|
|
650
|
+
const afterAdjustWidth = Math.min(newCurrentWidth, maxAllowedWidth);
|
|
651
|
+
// 如果調整前後的寬度相同(表示已經被限制在上限),則不調整任何欄位
|
|
652
|
+
if (Math.abs(afterAdjustWidth - beforeAdjustWidth) < 0.1) {
|
|
653
|
+
return newWidths;
|
|
654
|
+
}
|
|
655
|
+
// 應用上限限制
|
|
656
|
+
newCurrentWidth = afterAdjustWidth;
|
|
657
|
+
// 更新當前釘選欄位的寬度
|
|
658
|
+
newWidths[columnIndex] = { type: 'percentage', value: newCurrentWidth };
|
|
659
|
+
return newWidths;
|
|
660
|
+
}
|
|
661
|
+
// 預設情況:兩個都是 percentage 但都不是釘選欄位(正常模式)
|
|
662
|
+
if (currentCol.type === 'percentage' && nextCol.type === 'percentage') {
|
|
663
|
+
const currentWidth = currentCol.value;
|
|
664
|
+
const nextWidth = nextCol.value;
|
|
665
|
+
let newCurrentWidth = Math.round((currentWidth + deltaPercentage) * 10) / 10;
|
|
666
|
+
let newNextWidth = Math.round((nextWidth - deltaPercentage) * 10) / 10;
|
|
667
|
+
if (newCurrentWidth < MIN_COLUMN_WIDTH_PERCENTAGE) {
|
|
668
|
+
newCurrentWidth = MIN_COLUMN_WIDTH_PERCENTAGE;
|
|
669
|
+
newNextWidth = Math.round((currentWidth + nextWidth - MIN_COLUMN_WIDTH_PERCENTAGE) * 10) / 10;
|
|
670
|
+
}
|
|
671
|
+
else if (newNextWidth < MIN_COLUMN_WIDTH_PERCENTAGE) {
|
|
672
|
+
newNextWidth = MIN_COLUMN_WIDTH_PERCENTAGE;
|
|
673
|
+
newCurrentWidth = Math.round((currentWidth + nextWidth - MIN_COLUMN_WIDTH_PERCENTAGE) * 10) / 10;
|
|
674
|
+
}
|
|
675
|
+
newWidths[columnIndex] = { type: 'percentage', value: newCurrentWidth };
|
|
676
|
+
newWidths[nextColumnIndex] = { type: 'percentage', value: newNextWidth };
|
|
677
|
+
return newWidths;
|
|
678
|
+
}
|
|
679
|
+
return newWidths;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* 移動或交換欄位寬度設定
|
|
683
|
+
* @param currentWidths - 當前的欄位寬度陣列
|
|
684
|
+
* @param sourceIndex - 來源欄位的索引
|
|
685
|
+
* @param targetIndex - 目標欄位的索引
|
|
686
|
+
* @param mode - 'swap' 為交換兩個位置,'move' 為移動到目標位置
|
|
687
|
+
* @returns 處理後的欄位寬度陣列
|
|
688
|
+
*/
|
|
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) {
|
|
695
|
+
return currentWidths;
|
|
696
|
+
}
|
|
697
|
+
const newWidths = [...currentWidths];
|
|
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
|
+
}
|
|
709
|
+
return newWidths;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* 將 columnWidths 轉換為混合模式(釘選欄位用 percentage,未釘選欄位用 pixel)
|
|
713
|
+
* @param currentWidths - 當前的欄位寬度陣列
|
|
714
|
+
* @param pinnedColumnIndices - 釘選欄位的索引陣列
|
|
715
|
+
* @param tableWidth - 表格的實際寬度(pixel)
|
|
716
|
+
* @returns 轉換後的欄位寬度陣列
|
|
717
|
+
*/
|
|
718
|
+
function convertToMixedWidthMode(currentWidths, pinnedColumnIndices, tableWidth) {
|
|
719
|
+
if (pinnedColumnIndices.length === 0 || tableWidth === 0) {
|
|
720
|
+
return currentWidths;
|
|
721
|
+
}
|
|
722
|
+
const newWidths = [];
|
|
723
|
+
// 計算釘選欄位的總百分比
|
|
724
|
+
let totalPinnedPercentage = 0;
|
|
725
|
+
pinnedColumnIndices.forEach((index) => {
|
|
726
|
+
const width = currentWidths[index];
|
|
727
|
+
if (width) {
|
|
728
|
+
if (width.type === 'percentage') {
|
|
729
|
+
totalPinnedPercentage += width.value;
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
// 如果是 pixel,轉換為百分比
|
|
733
|
+
const percentage = (width.value / tableWidth) * 100;
|
|
734
|
+
totalPinnedPercentage += percentage;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
// 確保釘選欄位總和不超過指定範圍
|
|
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
|
+
};
|
|
751
|
+
}
|
|
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
|
+
});
|
|
761
|
+
// 計算剩餘空間(用於未釘選欄位)
|
|
762
|
+
const remainingPercentage = 100 - totalPinnedPercentage;
|
|
763
|
+
const remainingPixelWidth = (tableWidth * remainingPercentage) / 100;
|
|
764
|
+
const unpinnedColumnCount = currentWidths.length - pinnedColumnIndices.length;
|
|
765
|
+
const pixelWidthPerColumn = unpinnedColumnCount > 0 ? Math.floor(remainingPixelWidth / unpinnedColumnCount) : 0;
|
|
766
|
+
// 建立新的寬度陣列
|
|
767
|
+
currentWidths.forEach((width, index) => {
|
|
768
|
+
if (pinnedColumnIndices.includes(index)) {
|
|
769
|
+
// 釘選欄位:使用 percentage
|
|
770
|
+
newWidths.push(width);
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
// 未釘選欄位:使用 pixel
|
|
774
|
+
newWidths.push({ type: 'pixel', value: pixelWidthPerColumn });
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
return newWidths;
|
|
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
|
+
}
|
|
798
|
+
|
|
799
|
+
export { calculateColumnWidthsAfterAdd, calculateColumnWidthsAfterDelete, calculateResizedColumnWidths, collectCells, convertToMixedWidthMode, convertToPercentageMode, createTableCell, distributeEqualPercentages, enforcePinnedColumnsMaxWidth, getAlignFromCells, getColumnWidths, getPinnedColumnsInfo, getReferenceRowFromHeaderOrBody, getTableElements, getTableStructure, hasAnyPinnedColumns, hasAnyPinnedRows, moveOrSwapColumnWidth, scalePercentagesWithNewColumn, setAlignForCells, setColumnWidths };
|