@quadrats/react 1.0.0 → 1.1.0

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