@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,427 @@
1
+ import { isNodesTypeIn, Element, Editor, Transforms, PARAGRAPH_TYPE, Node, Text } from '@quadrats/core';
2
+ import { TABLE_TYPES } from './constants.js';
3
+ import { LIST_TYPES } from '@quadrats/common/list';
4
+ import { getCellLocation, tryMoveToAdjacentRow, getTableContainers, tryCrossBoundaryMove, tryExtendSelectionHorizontal, tryMoveToNextCell } from './utils.js';
5
+
6
+ function createTable(options = {}) {
7
+ const { types: typesOptions } = options;
8
+ const types = Object.assign(Object.assign({}, TABLE_TYPES), typesOptions);
9
+ const createTableElement = (rows, cols) => {
10
+ return [
11
+ {
12
+ type: types.table,
13
+ children: [
14
+ { type: types.table_title, children: [{ text: '' }] },
15
+ {
16
+ type: types.table_main,
17
+ children: [
18
+ {
19
+ type: types.table_body,
20
+ children: Array.from({ length: rows }, () => ({
21
+ type: types.table_row,
22
+ children: Array.from({ length: cols }, () => ({
23
+ type: types.table_cell,
24
+ children: [
25
+ {
26
+ type: PARAGRAPH_TYPE,
27
+ children: [{ text: '' }],
28
+ },
29
+ ],
30
+ })),
31
+ })),
32
+ },
33
+ ],
34
+ },
35
+ ],
36
+ },
37
+ {
38
+ type: PARAGRAPH_TYPE,
39
+ children: [{ text: '' }],
40
+ },
41
+ ];
42
+ };
43
+ const isSelectionInTableMain = (editor) => isNodesTypeIn(editor, [types.table_main]);
44
+ const isSelectionInTableCell = (editor) => isNodesTypeIn(editor, [types.table_cell]);
45
+ const isSelectionInTableRow = (editor) => isNodesTypeIn(editor, [types.table_row]);
46
+ const isSelectionInTableHeader = (editor) => isNodesTypeIn(editor, [types.table_header]);
47
+ const isSelectionInTableBody = (editor) => isNodesTypeIn(editor, [types.table_body]);
48
+ const isSelectionInTableList = (editor) => isNodesTypeIn(editor, [LIST_TYPES.ol, LIST_TYPES.ul]);
49
+ const insertTable = (editor, rows, cols) => {
50
+ Transforms.insertNodes(editor, createTableElement(rows, cols));
51
+ };
52
+ const moveToNextCell = (editor, types) => {
53
+ if (!editor.selection)
54
+ return;
55
+ try {
56
+ const location = getCellLocation(editor, types);
57
+ if (!location)
58
+ return;
59
+ const selectFn = (cellPath, position) => {
60
+ const point = Editor[position](editor, cellPath);
61
+ Transforms.select(editor, point);
62
+ };
63
+ // 嘗試移動到下一個 cell(同一列或下一列)
64
+ if (tryMoveToNextCell(location, selectFn))
65
+ return;
66
+ // 如果在 header,嘗試移動到 body 的第一個 cell(第一行)
67
+ const containers = getTableContainers(editor, types, location.containerPath);
68
+ if (!containers)
69
+ return;
70
+ // 使用 tryCrossBoundaryMove,指定 targetColumn 為 0(Tab 導航總是移動到第一行)
71
+ if (tryCrossBoundaryMove(containers, location, 'down', selectFn, 0))
72
+ return;
73
+ }
74
+ catch (error) {
75
+ console.warn('Failed to move to next cell:', error);
76
+ }
77
+ };
78
+ const moveToRowAbove = (editor, types) => {
79
+ if (!editor.selection)
80
+ return;
81
+ try {
82
+ const location = getCellLocation(editor, types);
83
+ if (!location)
84
+ return;
85
+ const selectFn = (cellPath, position) => {
86
+ const point = Editor[position](editor, cellPath);
87
+ Transforms.select(editor, point);
88
+ };
89
+ // 嘗試移動到相鄰列(同一行)
90
+ if (tryMoveToAdjacentRow(location, 'up', selectFn))
91
+ return;
92
+ // 嘗試跨容器移動
93
+ const containers = getTableContainers(editor, types, location.containerPath);
94
+ if (!containers)
95
+ return;
96
+ if (tryCrossBoundaryMove(containers, location, 'up', selectFn))
97
+ return;
98
+ }
99
+ catch (error) {
100
+ console.warn('Failed to move to row above:', error);
101
+ }
102
+ };
103
+ const moveToRowBelow = (editor, types) => {
104
+ if (!editor.selection)
105
+ return;
106
+ try {
107
+ const location = getCellLocation(editor, types);
108
+ if (!location)
109
+ return;
110
+ const selectFn = (cellPath, position) => {
111
+ const point = Editor[position](editor, cellPath);
112
+ Transforms.select(editor, point);
113
+ };
114
+ // 嘗試移動到相鄰列(同一行)
115
+ if (tryMoveToAdjacentRow(location, 'down', selectFn))
116
+ return;
117
+ // 嘗試跨容器移動
118
+ const containers = getTableContainers(editor, types, location.containerPath);
119
+ if (!containers)
120
+ return;
121
+ if (tryCrossBoundaryMove(containers, location, 'down', selectFn))
122
+ return;
123
+ }
124
+ catch (error) {
125
+ console.warn('Failed to move to row below:', error);
126
+ }
127
+ };
128
+ const extendSelectionLeft = (editor, types) => {
129
+ if (!editor.selection)
130
+ return;
131
+ try {
132
+ const { anchor, focus } = editor.selection;
133
+ const location = getCellLocation(editor, types, focus);
134
+ if (!location)
135
+ return;
136
+ tryExtendSelectionHorizontal(editor, location, 'left', anchor);
137
+ }
138
+ catch (error) {
139
+ console.warn('Failed to extend selection left:', error);
140
+ }
141
+ };
142
+ const extendSelectionRight = (editor, types) => {
143
+ if (!editor.selection)
144
+ return;
145
+ try {
146
+ const { anchor, focus } = editor.selection;
147
+ const location = getCellLocation(editor, types, focus);
148
+ if (!location)
149
+ return;
150
+ tryExtendSelectionHorizontal(editor, location, 'right', anchor);
151
+ }
152
+ catch (error) {
153
+ console.warn('Failed to extend selection right:', error);
154
+ }
155
+ };
156
+ const extendSelectionUp = (editor, types) => {
157
+ if (!editor.selection)
158
+ return;
159
+ try {
160
+ const { anchor } = editor.selection;
161
+ const location = getCellLocation(editor, types, editor.selection.focus);
162
+ if (!location)
163
+ return;
164
+ const selectFn = (cellPath, position) => {
165
+ const point = Editor[position](editor, cellPath);
166
+ Transforms.select(editor, { anchor, focus: point });
167
+ };
168
+ // 嘗試移動到相鄰列(同一行)
169
+ if (tryMoveToAdjacentRow(location, 'up', selectFn))
170
+ return;
171
+ // 嘗試跨容器移動
172
+ const containers = getTableContainers(editor, types, location.containerPath);
173
+ if (!containers)
174
+ return;
175
+ if (tryCrossBoundaryMove(containers, location, 'up', selectFn))
176
+ return;
177
+ }
178
+ catch (error) {
179
+ console.warn('Failed to extend selection up:', error);
180
+ }
181
+ };
182
+ const extendSelectionDown = (editor, types) => {
183
+ if (!editor.selection)
184
+ return;
185
+ try {
186
+ const { anchor } = editor.selection;
187
+ const location = getCellLocation(editor, types, editor.selection.focus);
188
+ if (!location)
189
+ return;
190
+ const selectFn = (cellPath, position) => {
191
+ const point = Editor[position](editor, cellPath);
192
+ Transforms.select(editor, { anchor, focus: point });
193
+ };
194
+ // 嘗試移動到相鄰列(同一行)
195
+ if (tryMoveToAdjacentRow(location, 'down', selectFn))
196
+ return;
197
+ // 嘗試跨容器移動
198
+ const containers = getTableContainers(editor, types, location.containerPath);
199
+ if (!containers)
200
+ return;
201
+ if (tryCrossBoundaryMove(containers, location, 'down', selectFn))
202
+ return;
203
+ }
204
+ catch (error) {
205
+ console.warn('Failed to extend selection down:', error);
206
+ }
207
+ };
208
+ return {
209
+ types,
210
+ createTableElement,
211
+ insertTable,
212
+ isSelectionInTableMain,
213
+ isSelectionInTableCell,
214
+ isSelectionInTableRow,
215
+ isSelectionInTableHeader,
216
+ isSelectionInTableBody,
217
+ isSelectionInTableList,
218
+ moveToNextCell,
219
+ moveToRowAbove,
220
+ moveToRowBelow,
221
+ extendSelectionLeft,
222
+ extendSelectionRight,
223
+ extendSelectionUp,
224
+ extendSelectionDown,
225
+ with(editor) {
226
+ const { insertFragment, deleteBackward } = editor;
227
+ editor.normalizeNode = (entry) => {
228
+ const [node, path] = entry;
229
+ if (Element.isElement(node)) {
230
+ const type = node.type;
231
+ // 1. 防止巢狀 table
232
+ if (type === types.table) {
233
+ for (const [, childPath] of Editor.nodes(editor, {
234
+ at: path,
235
+ match: (n) => Element.isElement(n) && n.type === types.table,
236
+ })) {
237
+ if (childPath.length > path.length) {
238
+ Transforms.removeNodes(editor, { at: childPath });
239
+ return;
240
+ }
241
+ }
242
+ // 確保 table 有必要的結構:title + main
243
+ const children = node.children.filter((child) => Element.isElement(child));
244
+ const titleChild = children.find((child) => child.type === types.table_title);
245
+ const mainChild = children.find((child) => child.type === types.table_main);
246
+ if (!titleChild) {
247
+ Transforms.insertNodes(editor, { type: types.table_title, children: [{ text: '' }] }, {
248
+ at: [...path, 0],
249
+ });
250
+ return;
251
+ }
252
+ if (!mainChild) {
253
+ const mainIndex = titleChild ? 1 : 0;
254
+ const tableMain = {
255
+ type: types.table_main,
256
+ children: [
257
+ {
258
+ type: types.table_body,
259
+ children: [
260
+ {
261
+ type: types.table_row,
262
+ children: [
263
+ {
264
+ type: types.table_cell,
265
+ children: [
266
+ {
267
+ type: PARAGRAPH_TYPE,
268
+ children: [{ text: '' }],
269
+ },
270
+ ],
271
+ },
272
+ ],
273
+ },
274
+ ],
275
+ },
276
+ ],
277
+ };
278
+ Transforms.insertNodes(editor, tableMain, { at: [...path, mainIndex] });
279
+ return;
280
+ }
281
+ }
282
+ // 2. table_main 必須至少有一個 table_body
283
+ if (type === types.table_main) {
284
+ const children = node.children.filter((child) => Element.isElement(child));
285
+ const bodyChild = children.find((child) => child.type === types.table_body);
286
+ if (!bodyChild) {
287
+ const tableBody = {
288
+ type: types.table_body,
289
+ children: [
290
+ {
291
+ type: types.table_row,
292
+ children: [
293
+ {
294
+ type: types.table_cell,
295
+ children: [
296
+ {
297
+ type: PARAGRAPH_TYPE,
298
+ children: [{ text: '' }],
299
+ },
300
+ ],
301
+ },
302
+ ],
303
+ },
304
+ ],
305
+ };
306
+ Transforms.insertNodes(editor, tableBody, { at: [...path, children.length] });
307
+ return;
308
+ }
309
+ }
310
+ // 3. table_header 和 table_body 必須有合理的 row 結構
311
+ if (type === types.table_header) {
312
+ const children = node.children.filter((child) => Element.isElement(child));
313
+ const rowChildren = children.filter((child) => child.type === types.table_row);
314
+ // 如果 header 沒有任何 row,移除整個 header
315
+ if (rowChildren.length === 0) {
316
+ Transforms.removeNodes(editor, { at: path });
317
+ return;
318
+ }
319
+ }
320
+ if (type === types.table_body) {
321
+ const children = node.children.filter((child) => Element.isElement(child));
322
+ const rowChildren = children.filter((child) => child.type === types.table_row);
323
+ // body 必須至少有一個 row
324
+ if (rowChildren.length === 0) {
325
+ const tableRow = {
326
+ type: types.table_row,
327
+ children: [
328
+ {
329
+ type: types.table_cell,
330
+ children: [
331
+ {
332
+ type: PARAGRAPH_TYPE,
333
+ children: [{ text: '' }],
334
+ },
335
+ ],
336
+ },
337
+ ],
338
+ };
339
+ Transforms.insertNodes(editor, tableRow, { at: [...path, 0] });
340
+ return;
341
+ }
342
+ }
343
+ // 4. table_cell 只允許 paragraph、list
344
+ if (type === types.table_cell) {
345
+ const allowedTypes = [PARAGRAPH_TYPE, LIST_TYPES.ul, LIST_TYPES.ol];
346
+ for (const [child, childPath] of Node.children(editor, path)) {
347
+ if (Element.isElement(child)) {
348
+ const childType = child.type;
349
+ // 如果不在白名單中,直接移除
350
+ if (!allowedTypes.includes(childType)) {
351
+ Transforms.removeNodes(editor, { at: childPath });
352
+ return;
353
+ }
354
+ }
355
+ else if (!Text.isText(child)) {
356
+ // 如果不是 Element 也不是 Text,移除
357
+ Transforms.removeNodes(editor, { at: childPath });
358
+ return;
359
+ }
360
+ }
361
+ }
362
+ }
363
+ };
364
+ editor.deleteBackward = (unit) => {
365
+ const { selection } = editor;
366
+ if (selection) {
367
+ // 檢查是否在 table_title 中
368
+ const titleEntry = Editor.above(editor, {
369
+ match: (n) => Element.isElement(n) && n.type === types.table_title,
370
+ });
371
+ if (titleEntry) {
372
+ const [, titlePath] = titleEntry;
373
+ if (Editor.isStart(editor, selection.anchor, titlePath)) {
374
+ // 在 table_title 開頭按 backspace,不執行任何操作
375
+ return;
376
+ }
377
+ }
378
+ // 檢查是否在 table_cell 中
379
+ const cellEntry = Editor.above(editor, {
380
+ match: (n) => Element.isElement(n) && n.type === types.table_cell,
381
+ });
382
+ if (cellEntry) {
383
+ const [, cellPath] = cellEntry;
384
+ if (Editor.isStart(editor, selection.anchor, cellPath)) {
385
+ // 在 table_cell 開頭按 backspace,不執行任何操作
386
+ return;
387
+ }
388
+ }
389
+ }
390
+ // 執行預設的 deleteBackward 行為
391
+ deleteBackward(unit);
392
+ };
393
+ /** 複製 Table Cell 內文字時觸發 */
394
+ editor.insertFragment = (fragment) => {
395
+ const cellEntry = Editor.above(editor, {
396
+ match: (n) => Element.isElement(n) && n.type === types.table_cell,
397
+ });
398
+ if (cellEntry) {
399
+ // 在 table cell 中貼上時,只保留文字內容,不保留結構
400
+ const textNodes = [];
401
+ const extractText = (nodes) => {
402
+ for (const node of nodes) {
403
+ if (Element.isElement(node)) {
404
+ extractText(node.children);
405
+ }
406
+ else if (node.text !== undefined) {
407
+ textNodes.push(node);
408
+ }
409
+ }
410
+ };
411
+ extractText(fragment);
412
+ // 如果有文字節點,將它們包裝成一個 paragraph 插入
413
+ if (textNodes.length) {
414
+ const textContent = textNodes.map((node) => node.text).join('');
415
+ Transforms.insertText(editor, textContent);
416
+ return;
417
+ }
418
+ }
419
+ // 預設行為
420
+ insertFragment(fragment);
421
+ };
422
+ return editor;
423
+ },
424
+ };
425
+ }
426
+
427
+ export { createTable };