@quadrats/common 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.
@@ -0,0 +1,712 @@
1
+ 'use strict';
2
+
3
+ var core = require('@quadrats/core');
4
+ var list = require('@quadrats/common/list');
5
+
6
+ const TABLE_TYPE = 'table';
7
+ const TABLE_TITLE_TYPE = 'table_title';
8
+ const TABLE_MAIN_TYPE = 'table_main';
9
+ const TABLE_HEADER_TYPE = 'table_header';
10
+ const TABLE_BODY_TYPE = 'table_body';
11
+ const TABLE_ROW_TYPE = 'table_row';
12
+ const TABLE_CELL_TYPE = 'table_cell';
13
+ const TABLE_TYPES = {
14
+ table: TABLE_TYPE,
15
+ table_title: TABLE_TITLE_TYPE,
16
+ table_main: TABLE_MAIN_TYPE,
17
+ table_header: TABLE_HEADER_TYPE,
18
+ table_body: TABLE_BODY_TYPE,
19
+ table_row: TABLE_ROW_TYPE,
20
+ table_cell: TABLE_CELL_TYPE,
21
+ };
22
+ // Table limits
23
+ const TABLE_DEFAULT_MAX_COLUMNS = 6;
24
+ const TABLE_DEFAULT_MAX_ROWS = -1;
25
+ // 釘選欄位的最大總寬度百分比
26
+ const MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE = 40;
27
+ const MIN_COLUMN_WIDTH_PIXEL = 60;
28
+ const MIN_COLUMN_WIDTH_PERCENTAGE = 5;
29
+
30
+ /**
31
+ * 將 ColumnWidth 轉換為 CSS 可用的字串
32
+ * @param width - 行寬定義(column width)
33
+ * @returns CSS 寬度字串(例如 "30%" 或 "200px")
34
+ */
35
+ function columnWidthToCSS(width) {
36
+ if (width.type === 'percentage') {
37
+ return `${width.value.toFixed(1)}%`;
38
+ }
39
+ return `${width.value}px`;
40
+ }
41
+ /**
42
+ * 計算 table 的總寬度(用於設定 min-width 以支援 overflow)
43
+ * 此函數會將所有 columnWidths 的百分比和 pixel 值加總:
44
+ * - percentage: 保留為百分比
45
+ * - pixel: 直接累加
46
+ *
47
+ * @param columnWidths - 行寬陣列(column widths)
48
+ * @returns 總寬度的 CSS 字串(例如 "calc(50% + 400px)" 或 "100%" 或 "800px")
49
+ */
50
+ function calculateTableMinWidth(columnWidths) {
51
+ if (columnWidths.length === 0) {
52
+ return '100%';
53
+ }
54
+ let totalPercentage = 0;
55
+ let totalPixels = 0;
56
+ columnWidths.forEach((width) => {
57
+ if (width.type === 'percentage') {
58
+ totalPercentage += width.value;
59
+ }
60
+ else {
61
+ totalPixels += width.value;
62
+ }
63
+ });
64
+ // 只有 percentage,沒有 pixel
65
+ if (totalPixels === 0) {
66
+ return `${totalPercentage.toFixed(1)}%`;
67
+ }
68
+ // 只有 pixel,沒有 percentage
69
+ if (totalPercentage === 0) {
70
+ return `${totalPixels}px`;
71
+ }
72
+ // 有 percentage 也有 pixel
73
+ // 使用 calc() 來結合兩者
74
+ return `calc(${totalPercentage.toFixed(1)}% + ${totalPixels}px)`;
75
+ }
76
+ /**
77
+ * 獲取 cell 的位置資訊
78
+ * @param editor - Slate editor
79
+ * @param types - Table types
80
+ * @param at - 可選的位置,預設使用 editor.selection
81
+ * @returns cell 的位置資訊,如果找不到則返回 null
82
+ */
83
+ function getCellLocation(editor, types, at) {
84
+ const cellEntry = core.Editor.above(editor, {
85
+ at,
86
+ match: (n) => core.Element.isElement(n) && n.type === types.table_cell,
87
+ });
88
+ if (!cellEntry)
89
+ return null;
90
+ const [, cellPath] = cellEntry;
91
+ const columnIndex = cellPath[cellPath.length - 1];
92
+ const rowEntry = core.Editor.above(editor, {
93
+ at: cellPath,
94
+ match: (n) => core.Element.isElement(n) && n.type === types.table_row,
95
+ });
96
+ if (!rowEntry)
97
+ return null;
98
+ const [row, rowPath] = rowEntry;
99
+ const rowIndex = rowPath[rowPath.length - 1];
100
+ const containerEntry = core.Editor.above(editor, {
101
+ at: rowPath,
102
+ match: (n) => core.Element.isElement(n) && [types.table_header, types.table_body].includes(n.type),
103
+ });
104
+ if (!containerEntry)
105
+ return null;
106
+ const [container, containerPath] = containerEntry;
107
+ return {
108
+ cellPath,
109
+ columnIndex,
110
+ row,
111
+ rowPath,
112
+ rowIndex,
113
+ container,
114
+ containerPath,
115
+ isHeader: core.Element.isElement(container) && container.type === types.table_header,
116
+ isBody: core.Element.isElement(container) && container.type === types.table_body,
117
+ };
118
+ }
119
+ /**
120
+ * 獲取 table main 和相關容器資訊
121
+ * @param editor - Slate editor
122
+ * @param types - Table types
123
+ * @param containerPath - 當前容器的路徑
124
+ * @returns table 容器資訊,如果找不到則返回 null
125
+ */
126
+ function getTableContainers(editor, types, containerPath) {
127
+ const tableMainEntry = core.Editor.above(editor, {
128
+ at: containerPath,
129
+ match: (n) => core.Element.isElement(n) && n.type === types.table_main,
130
+ });
131
+ if (!tableMainEntry)
132
+ return null;
133
+ const [tableMain, tableMainPath] = tableMainEntry;
134
+ const tableHeader = tableMain.children.find((child) => core.Element.isElement(child) && child.type === types.table_header);
135
+ const tableBody = tableMain.children.find((child) => core.Element.isElement(child) && child.type === types.table_body);
136
+ const tableHeaderIndex = tableHeader ? tableMain.children.findIndex((child) => child === tableHeader) : -1;
137
+ const tableBodyIndex = tableBody ? tableMain.children.findIndex((child) => child === tableBody) : -1;
138
+ return {
139
+ tableMain,
140
+ tableMainPath,
141
+ tableHeader,
142
+ tableBody,
143
+ tableHeaderIndex,
144
+ tableBodyIndex,
145
+ };
146
+ }
147
+ /**
148
+ * 嘗試移動到相鄰列的相同行
149
+ * @param location - 當前 cell 位置資訊
150
+ * @param direction - 移動方向('up' 或 'down')
151
+ * @param selectFn - 選擇函數(用於 move 或 extend 模式)
152
+ * @returns 是否成功移動
153
+ */
154
+ function tryMoveToAdjacentRow(location, direction, selectFn) {
155
+ const { container, containerPath, rowIndex, columnIndex } = location;
156
+ const targetRowIndex = direction === 'up' ? rowIndex - 1 : rowIndex + 1;
157
+ // 嘗試在當前容器中移動
158
+ if (targetRowIndex >= 0 && targetRowIndex < container.children.length) {
159
+ const targetRow = container.children[targetRowIndex];
160
+ if (core.Element.isElement(targetRow)) {
161
+ const targetColumnIndex = Math.min(columnIndex, targetRow.children.length - 1);
162
+ const targetCellPath = [...containerPath, targetRowIndex, targetColumnIndex];
163
+ selectFn(targetCellPath, direction === 'up' ? 'start' : 'end');
164
+ return true;
165
+ }
166
+ }
167
+ return false;
168
+ }
169
+ /**
170
+ * 嘗試跨容器移動(header <-> body)
171
+ * @param containers - Table 容器資訊
172
+ * @param location - 當前 cell 位置資訊
173
+ * @param direction - 移動方向('up' 或 'down')
174
+ * @param selectFn - 選擇函數(用於 move 或 extend 模式)
175
+ * @param targetColumn - 目標行索引,預設為保持當前行,設為 0 可強制移動到第一行
176
+ * @returns 是否成功移動
177
+ */
178
+ function tryCrossBoundaryMove(containers, location, direction, selectFn, targetColumn) {
179
+ const { columnIndex, isHeader, isBody } = location;
180
+ const { tableMainPath, tableHeader, tableBody, tableHeaderIndex, tableBodyIndex } = containers;
181
+ // 從 body 向上移動到 header
182
+ if (direction === 'up' && isBody && tableHeader && core.Element.isElement(tableHeader)) {
183
+ const lastRowIndex = tableHeader.children.length - 1;
184
+ const lastRow = tableHeader.children[lastRowIndex];
185
+ if (core.Element.isElement(lastRow)) {
186
+ const targetColumnIndex = targetColumn !== undefined ? targetColumn : Math.min(columnIndex, lastRow.children.length - 1);
187
+ const targetCellPath = [...tableMainPath, tableHeaderIndex, lastRowIndex, targetColumnIndex];
188
+ selectFn(targetCellPath, 'start');
189
+ return true;
190
+ }
191
+ }
192
+ // 從 header 向下移動到 body
193
+ if (direction === 'down' && isHeader && tableBody && core.Element.isElement(tableBody)) {
194
+ const firstRow = tableBody.children[0];
195
+ if (core.Element.isElement(firstRow)) {
196
+ const targetColumnIndex = targetColumn !== undefined ? targetColumn : Math.min(columnIndex, firstRow.children.length - 1);
197
+ const targetCellPath = [...tableMainPath, tableBodyIndex, 0, targetColumnIndex];
198
+ // Tab 導航時使用 'start',上下鍵導航時使用 'end'
199
+ const position = targetColumn === 0 ? 'start' : 'end';
200
+ selectFn(targetCellPath, position);
201
+ return true;
202
+ }
203
+ }
204
+ return false;
205
+ }
206
+ /**
207
+ * 嘗試移動到下一個 cell
208
+ * @param location - 當前 cell 位置資訊
209
+ * @param selectFn - 選擇函數
210
+ * @returns 是否成功移動
211
+ */
212
+ function tryMoveToNextCell(location, selectFn) {
213
+ const { cellPath, row, rowPath, container, containerPath, rowIndex } = location;
214
+ const currentColumnIndex = cellPath[cellPath.length - 1];
215
+ const nextColumnIndex = currentColumnIndex + 1;
216
+ if (nextColumnIndex < row.children.length) {
217
+ const targetCellPath = [...rowPath, nextColumnIndex];
218
+ selectFn(targetCellPath, 'start');
219
+ return true;
220
+ }
221
+ const nextRowIndex = rowIndex + 1;
222
+ if (nextRowIndex < container.children.length) {
223
+ const nextRow = container.children[nextRowIndex];
224
+ if (core.Element.isElement(nextRow) && nextRow.children.length > 0) {
225
+ const targetCellPath = [...containerPath, nextRowIndex, 0];
226
+ selectFn(targetCellPath, 'start');
227
+ return true;
228
+ }
229
+ }
230
+ return false;
231
+ }
232
+ /**
233
+ * 嘗試在水平方向擴展選擇(左右移動)
234
+ * @param editor - Slate editor
235
+ * @param location - 當前 cell 位置資訊
236
+ * @param direction - 移動方向('left' 或 'right')
237
+ * @param anchor - 選擇的起點
238
+ * @returns 是否成功擴展
239
+ */
240
+ function tryExtendSelectionHorizontal(editor, location, direction, anchor) {
241
+ var _a;
242
+ const { cellPath, columnIndex, row, rowPath } = location;
243
+ const focus = (_a = editor.selection) === null || _a === void 0 ? void 0 : _a.focus;
244
+ if (!focus)
245
+ return false;
246
+ const isLeftDirection = direction === 'left';
247
+ const isAtBoundary = isLeftDirection ? columnIndex === 0 : columnIndex >= row.children.length - 1;
248
+ // 如果已經在邊界,嘗試擴展到該 cell 的開頭或結尾
249
+ if (isAtBoundary) {
250
+ const boundaryPoint = isLeftDirection ? core.Editor.start(editor, cellPath) : core.Editor.end(editor, cellPath);
251
+ // 只有當 focus 還沒到邊界時才移動
252
+ const shouldMove = isLeftDirection
253
+ ? focus.offset > boundaryPoint.offset || focus.path.length !== boundaryPoint.path.length
254
+ : focus.offset < boundaryPoint.offset || focus.path.length !== boundaryPoint.path.length;
255
+ if (shouldMove) {
256
+ core.Transforms.select(editor, { anchor, focus: boundaryPoint });
257
+ }
258
+ return true;
259
+ }
260
+ // 找到目標 cell
261
+ const targetColumnIndex = isLeftDirection ? columnIndex - 1 : columnIndex + 1;
262
+ const targetCellPath = [...rowPath, targetColumnIndex];
263
+ // 根據方向選擇目標點(左邊用 end,右邊用 start)
264
+ const targetPoint = isLeftDirection ? core.Editor.end(editor, targetCellPath) : core.Editor.start(editor, targetCellPath);
265
+ // 保持 anchor 不變,移動 focus
266
+ core.Transforms.select(editor, { anchor, focus: targetPoint });
267
+ return true;
268
+ }
269
+
270
+ function createTable(options = {}) {
271
+ const { types: typesOptions } = options;
272
+ const types = Object.assign(Object.assign({}, TABLE_TYPES), typesOptions);
273
+ const createTableElement = (rows, cols) => {
274
+ return [
275
+ {
276
+ type: types.table,
277
+ children: [
278
+ { type: types.table_title, children: [{ text: '' }] },
279
+ {
280
+ type: types.table_main,
281
+ children: [
282
+ {
283
+ type: types.table_body,
284
+ children: Array.from({ length: rows }, () => ({
285
+ type: types.table_row,
286
+ children: Array.from({ length: cols }, () => ({
287
+ type: types.table_cell,
288
+ children: [
289
+ {
290
+ type: core.PARAGRAPH_TYPE,
291
+ children: [{ text: '' }],
292
+ },
293
+ ],
294
+ })),
295
+ })),
296
+ },
297
+ ],
298
+ },
299
+ ],
300
+ },
301
+ {
302
+ type: core.PARAGRAPH_TYPE,
303
+ children: [{ text: '' }],
304
+ },
305
+ ];
306
+ };
307
+ const isSelectionInTableMain = (editor) => core.isNodesTypeIn(editor, [types.table_main]);
308
+ const isSelectionInTableCell = (editor) => core.isNodesTypeIn(editor, [types.table_cell]);
309
+ const isSelectionInTableRow = (editor) => core.isNodesTypeIn(editor, [types.table_row]);
310
+ const isSelectionInTableHeader = (editor) => core.isNodesTypeIn(editor, [types.table_header]);
311
+ const isSelectionInTableBody = (editor) => core.isNodesTypeIn(editor, [types.table_body]);
312
+ const isSelectionInTableList = (editor) => core.isNodesTypeIn(editor, [list.LIST_TYPES.ol, list.LIST_TYPES.ul]);
313
+ const insertTable = (editor, rows, cols) => {
314
+ core.Transforms.insertNodes(editor, createTableElement(rows, cols));
315
+ };
316
+ const moveToNextCell = (editor, types) => {
317
+ if (!editor.selection)
318
+ return;
319
+ try {
320
+ const location = getCellLocation(editor, types);
321
+ if (!location)
322
+ return;
323
+ const selectFn = (cellPath, position) => {
324
+ const point = core.Editor[position](editor, cellPath);
325
+ core.Transforms.select(editor, point);
326
+ };
327
+ // 嘗試移動到下一個 cell(同一列或下一列)
328
+ if (tryMoveToNextCell(location, selectFn))
329
+ return;
330
+ // 如果在 header,嘗試移動到 body 的第一個 cell(第一行)
331
+ const containers = getTableContainers(editor, types, location.containerPath);
332
+ if (!containers)
333
+ return;
334
+ // 使用 tryCrossBoundaryMove,指定 targetColumn 為 0(Tab 導航總是移動到第一行)
335
+ if (tryCrossBoundaryMove(containers, location, 'down', selectFn, 0))
336
+ return;
337
+ }
338
+ catch (error) {
339
+ console.warn('Failed to move to next cell:', error);
340
+ }
341
+ };
342
+ const moveToRowAbove = (editor, types) => {
343
+ if (!editor.selection)
344
+ return;
345
+ try {
346
+ const location = getCellLocation(editor, types);
347
+ if (!location)
348
+ return;
349
+ const selectFn = (cellPath, position) => {
350
+ const point = core.Editor[position](editor, cellPath);
351
+ core.Transforms.select(editor, point);
352
+ };
353
+ // 嘗試移動到相鄰列(同一行)
354
+ if (tryMoveToAdjacentRow(location, 'up', selectFn))
355
+ return;
356
+ // 嘗試跨容器移動
357
+ const containers = getTableContainers(editor, types, location.containerPath);
358
+ if (!containers)
359
+ return;
360
+ if (tryCrossBoundaryMove(containers, location, 'up', selectFn))
361
+ return;
362
+ }
363
+ catch (error) {
364
+ console.warn('Failed to move to row above:', error);
365
+ }
366
+ };
367
+ const moveToRowBelow = (editor, types) => {
368
+ if (!editor.selection)
369
+ return;
370
+ try {
371
+ const location = getCellLocation(editor, types);
372
+ if (!location)
373
+ return;
374
+ const selectFn = (cellPath, position) => {
375
+ const point = core.Editor[position](editor, cellPath);
376
+ core.Transforms.select(editor, point);
377
+ };
378
+ // 嘗試移動到相鄰列(同一行)
379
+ if (tryMoveToAdjacentRow(location, 'down', selectFn))
380
+ return;
381
+ // 嘗試跨容器移動
382
+ const containers = getTableContainers(editor, types, location.containerPath);
383
+ if (!containers)
384
+ return;
385
+ if (tryCrossBoundaryMove(containers, location, 'down', selectFn))
386
+ return;
387
+ }
388
+ catch (error) {
389
+ console.warn('Failed to move to row below:', error);
390
+ }
391
+ };
392
+ const extendSelectionLeft = (editor, types) => {
393
+ if (!editor.selection)
394
+ return;
395
+ try {
396
+ const { anchor, focus } = editor.selection;
397
+ const location = getCellLocation(editor, types, focus);
398
+ if (!location)
399
+ return;
400
+ tryExtendSelectionHorizontal(editor, location, 'left', anchor);
401
+ }
402
+ catch (error) {
403
+ console.warn('Failed to extend selection left:', error);
404
+ }
405
+ };
406
+ const extendSelectionRight = (editor, types) => {
407
+ if (!editor.selection)
408
+ return;
409
+ try {
410
+ const { anchor, focus } = editor.selection;
411
+ const location = getCellLocation(editor, types, focus);
412
+ if (!location)
413
+ return;
414
+ tryExtendSelectionHorizontal(editor, location, 'right', anchor);
415
+ }
416
+ catch (error) {
417
+ console.warn('Failed to extend selection right:', error);
418
+ }
419
+ };
420
+ const extendSelectionUp = (editor, types) => {
421
+ if (!editor.selection)
422
+ return;
423
+ try {
424
+ const { anchor } = editor.selection;
425
+ const location = getCellLocation(editor, types, editor.selection.focus);
426
+ if (!location)
427
+ return;
428
+ const selectFn = (cellPath, position) => {
429
+ const point = core.Editor[position](editor, cellPath);
430
+ core.Transforms.select(editor, { anchor, focus: point });
431
+ };
432
+ // 嘗試移動到相鄰列(同一行)
433
+ if (tryMoveToAdjacentRow(location, 'up', selectFn))
434
+ return;
435
+ // 嘗試跨容器移動
436
+ const containers = getTableContainers(editor, types, location.containerPath);
437
+ if (!containers)
438
+ return;
439
+ if (tryCrossBoundaryMove(containers, location, 'up', selectFn))
440
+ return;
441
+ }
442
+ catch (error) {
443
+ console.warn('Failed to extend selection up:', error);
444
+ }
445
+ };
446
+ const extendSelectionDown = (editor, types) => {
447
+ if (!editor.selection)
448
+ return;
449
+ try {
450
+ const { anchor } = editor.selection;
451
+ const location = getCellLocation(editor, types, editor.selection.focus);
452
+ if (!location)
453
+ return;
454
+ const selectFn = (cellPath, position) => {
455
+ const point = core.Editor[position](editor, cellPath);
456
+ core.Transforms.select(editor, { anchor, focus: point });
457
+ };
458
+ // 嘗試移動到相鄰列(同一行)
459
+ if (tryMoveToAdjacentRow(location, 'down', selectFn))
460
+ return;
461
+ // 嘗試跨容器移動
462
+ const containers = getTableContainers(editor, types, location.containerPath);
463
+ if (!containers)
464
+ return;
465
+ if (tryCrossBoundaryMove(containers, location, 'down', selectFn))
466
+ return;
467
+ }
468
+ catch (error) {
469
+ console.warn('Failed to extend selection down:', error);
470
+ }
471
+ };
472
+ return {
473
+ types,
474
+ createTableElement,
475
+ insertTable,
476
+ isSelectionInTableMain,
477
+ isSelectionInTableCell,
478
+ isSelectionInTableRow,
479
+ isSelectionInTableHeader,
480
+ isSelectionInTableBody,
481
+ isSelectionInTableList,
482
+ moveToNextCell,
483
+ moveToRowAbove,
484
+ moveToRowBelow,
485
+ extendSelectionLeft,
486
+ extendSelectionRight,
487
+ extendSelectionUp,
488
+ extendSelectionDown,
489
+ with(editor) {
490
+ const { insertFragment, deleteBackward } = editor;
491
+ editor.normalizeNode = (entry) => {
492
+ const [node, path] = entry;
493
+ if (core.Element.isElement(node)) {
494
+ const type = node.type;
495
+ // 1. 防止巢狀 table
496
+ if (type === types.table) {
497
+ for (const [, childPath] of core.Editor.nodes(editor, {
498
+ at: path,
499
+ match: (n) => core.Element.isElement(n) && n.type === types.table,
500
+ })) {
501
+ if (childPath.length > path.length) {
502
+ core.Transforms.removeNodes(editor, { at: childPath });
503
+ return;
504
+ }
505
+ }
506
+ // 確保 table 有必要的結構:title + main
507
+ const children = node.children.filter((child) => core.Element.isElement(child));
508
+ const titleChild = children.find((child) => child.type === types.table_title);
509
+ const mainChild = children.find((child) => child.type === types.table_main);
510
+ if (!titleChild) {
511
+ core.Transforms.insertNodes(editor, { type: types.table_title, children: [{ text: '' }] }, {
512
+ at: [...path, 0],
513
+ });
514
+ return;
515
+ }
516
+ if (!mainChild) {
517
+ const mainIndex = titleChild ? 1 : 0;
518
+ const tableMain = {
519
+ type: types.table_main,
520
+ children: [
521
+ {
522
+ type: types.table_body,
523
+ children: [
524
+ {
525
+ type: types.table_row,
526
+ children: [
527
+ {
528
+ type: types.table_cell,
529
+ children: [
530
+ {
531
+ type: core.PARAGRAPH_TYPE,
532
+ children: [{ text: '' }],
533
+ },
534
+ ],
535
+ },
536
+ ],
537
+ },
538
+ ],
539
+ },
540
+ ],
541
+ };
542
+ core.Transforms.insertNodes(editor, tableMain, { at: [...path, mainIndex] });
543
+ return;
544
+ }
545
+ }
546
+ // 2. table_main 必須至少有一個 table_body
547
+ if (type === types.table_main) {
548
+ const children = node.children.filter((child) => core.Element.isElement(child));
549
+ const bodyChild = children.find((child) => child.type === types.table_body);
550
+ if (!bodyChild) {
551
+ const tableBody = {
552
+ type: types.table_body,
553
+ children: [
554
+ {
555
+ type: types.table_row,
556
+ children: [
557
+ {
558
+ type: types.table_cell,
559
+ children: [
560
+ {
561
+ type: core.PARAGRAPH_TYPE,
562
+ children: [{ text: '' }],
563
+ },
564
+ ],
565
+ },
566
+ ],
567
+ },
568
+ ],
569
+ };
570
+ core.Transforms.insertNodes(editor, tableBody, { at: [...path, children.length] });
571
+ return;
572
+ }
573
+ }
574
+ // 3. table_header 和 table_body 必須有合理的 row 結構
575
+ if (type === types.table_header) {
576
+ const children = node.children.filter((child) => core.Element.isElement(child));
577
+ const rowChildren = children.filter((child) => child.type === types.table_row);
578
+ // 如果 header 沒有任何 row,移除整個 header
579
+ if (rowChildren.length === 0) {
580
+ core.Transforms.removeNodes(editor, { at: path });
581
+ return;
582
+ }
583
+ }
584
+ if (type === types.table_body) {
585
+ const children = node.children.filter((child) => core.Element.isElement(child));
586
+ const rowChildren = children.filter((child) => child.type === types.table_row);
587
+ // body 必須至少有一個 row
588
+ if (rowChildren.length === 0) {
589
+ const tableRow = {
590
+ type: types.table_row,
591
+ children: [
592
+ {
593
+ type: types.table_cell,
594
+ children: [
595
+ {
596
+ type: core.PARAGRAPH_TYPE,
597
+ children: [{ text: '' }],
598
+ },
599
+ ],
600
+ },
601
+ ],
602
+ };
603
+ core.Transforms.insertNodes(editor, tableRow, { at: [...path, 0] });
604
+ return;
605
+ }
606
+ }
607
+ // 4. table_cell 只允許 paragraph、list
608
+ if (type === types.table_cell) {
609
+ const allowedTypes = [core.PARAGRAPH_TYPE, list.LIST_TYPES.ul, list.LIST_TYPES.ol];
610
+ for (const [child, childPath] of core.Node.children(editor, path)) {
611
+ if (core.Element.isElement(child)) {
612
+ const childType = child.type;
613
+ // 如果不在白名單中,直接移除
614
+ if (!allowedTypes.includes(childType)) {
615
+ core.Transforms.removeNodes(editor, { at: childPath });
616
+ return;
617
+ }
618
+ }
619
+ else if (!core.Text.isText(child)) {
620
+ // 如果不是 Element 也不是 Text,移除
621
+ core.Transforms.removeNodes(editor, { at: childPath });
622
+ return;
623
+ }
624
+ }
625
+ }
626
+ }
627
+ };
628
+ editor.deleteBackward = (unit) => {
629
+ const { selection } = editor;
630
+ if (selection) {
631
+ // 檢查是否在 table_title 中
632
+ const titleEntry = core.Editor.above(editor, {
633
+ match: (n) => core.Element.isElement(n) && n.type === types.table_title,
634
+ });
635
+ if (titleEntry) {
636
+ const [, titlePath] = titleEntry;
637
+ if (core.Editor.isStart(editor, selection.anchor, titlePath)) {
638
+ // 在 table_title 開頭按 backspace,不執行任何操作
639
+ return;
640
+ }
641
+ }
642
+ // 檢查是否在 table_cell 中
643
+ const cellEntry = core.Editor.above(editor, {
644
+ match: (n) => core.Element.isElement(n) && n.type === types.table_cell,
645
+ });
646
+ if (cellEntry) {
647
+ const [, cellPath] = cellEntry;
648
+ if (core.Editor.isStart(editor, selection.anchor, cellPath)) {
649
+ // 在 table_cell 開頭按 backspace,不執行任何操作
650
+ return;
651
+ }
652
+ }
653
+ }
654
+ // 執行預設的 deleteBackward 行為
655
+ deleteBackward(unit);
656
+ };
657
+ /** 複製 Table Cell 內文字時觸發 */
658
+ editor.insertFragment = (fragment) => {
659
+ const cellEntry = core.Editor.above(editor, {
660
+ match: (n) => core.Element.isElement(n) && n.type === types.table_cell,
661
+ });
662
+ if (cellEntry) {
663
+ // 在 table cell 中貼上時,只保留文字內容,不保留結構
664
+ const textNodes = [];
665
+ const extractText = (nodes) => {
666
+ for (const node of nodes) {
667
+ if (core.Element.isElement(node)) {
668
+ extractText(node.children);
669
+ }
670
+ else if (node.text !== undefined) {
671
+ textNodes.push(node);
672
+ }
673
+ }
674
+ };
675
+ extractText(fragment);
676
+ // 如果有文字節點,將它們包裝成一個 paragraph 插入
677
+ if (textNodes.length) {
678
+ const textContent = textNodes.map((node) => node.text).join('');
679
+ core.Transforms.insertText(editor, textContent);
680
+ return;
681
+ }
682
+ }
683
+ // 預設行為
684
+ insertFragment(fragment);
685
+ };
686
+ return editor;
687
+ },
688
+ };
689
+ }
690
+
691
+ exports.MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE = MAX_PINNED_COLUMNS_WIDTH_PERCENTAGE;
692
+ exports.MIN_COLUMN_WIDTH_PERCENTAGE = MIN_COLUMN_WIDTH_PERCENTAGE;
693
+ exports.MIN_COLUMN_WIDTH_PIXEL = MIN_COLUMN_WIDTH_PIXEL;
694
+ exports.TABLE_BODY_TYPE = TABLE_BODY_TYPE;
695
+ exports.TABLE_CELL_TYPE = TABLE_CELL_TYPE;
696
+ exports.TABLE_DEFAULT_MAX_COLUMNS = TABLE_DEFAULT_MAX_COLUMNS;
697
+ exports.TABLE_DEFAULT_MAX_ROWS = TABLE_DEFAULT_MAX_ROWS;
698
+ exports.TABLE_HEADER_TYPE = TABLE_HEADER_TYPE;
699
+ exports.TABLE_MAIN_TYPE = TABLE_MAIN_TYPE;
700
+ exports.TABLE_ROW_TYPE = TABLE_ROW_TYPE;
701
+ exports.TABLE_TITLE_TYPE = TABLE_TITLE_TYPE;
702
+ exports.TABLE_TYPE = TABLE_TYPE;
703
+ exports.TABLE_TYPES = TABLE_TYPES;
704
+ exports.calculateTableMinWidth = calculateTableMinWidth;
705
+ exports.columnWidthToCSS = columnWidthToCSS;
706
+ exports.createTable = createTable;
707
+ exports.getCellLocation = getCellLocation;
708
+ exports.getTableContainers = getTableContainers;
709
+ exports.tryCrossBoundaryMove = tryCrossBoundaryMove;
710
+ exports.tryExtendSelectionHorizontal = tryExtendSelectionHorizontal;
711
+ exports.tryMoveToAdjacentRow = tryMoveToAdjacentRow;
712
+ exports.tryMoveToNextCell = tryMoveToNextCell;