@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.
- package/align/constants.d.ts +1 -0
- package/align/constants.js +5 -1
- package/align/createAlign.js +2 -4
- package/align/index.cjs.js +3 -2
- package/align/index.js +1 -1
- package/blockquote/createBlockquote.js +27 -10
- package/blockquote/index.cjs.js +26 -9
- package/file-uploader/createFileUploader.js +52 -7
- package/file-uploader/index.cjs.js +51 -6
- package/package.json +2 -2
- package/table/constants.d.ts +14 -0
- package/table/constants.js +25 -0
- package/table/createTable.d.ts +6 -0
- package/table/createTable.js +427 -0
- package/table/index.cjs.js +712 -0
- package/table/index.d.ts +4 -0
- package/table/index.js +3 -0
- package/table/package.json +7 -0
- package/table/typings.d.ts +70 -0
- package/table/utils.d.ts +68 -0
- package/table/utils.js +243 -0
|
@@ -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;
|