quill-table-up 2.0.1 → 2.0.3

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 (99) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/index.umd.js.map +1 -1
  6. package/dist/table-creator.css +1 -1
  7. package/package.json +9 -14
  8. package/src/__tests__/e2e/custom-creator.test.ts +44 -0
  9. package/src/__tests__/e2e/table-align.test.ts +39 -0
  10. package/src/__tests__/e2e/table-resize.test.ts +152 -0
  11. package/src/__tests__/e2e/table-scrollbar.test.ts +31 -0
  12. package/src/__tests__/e2e/table-selection.test.ts +83 -0
  13. package/src/__tests__/e2e/utils.ts +6 -0
  14. package/src/__tests__/unit/table-insert-blot.test.ts +464 -0
  15. package/src/__tests__/unit/table-insert-remove-merge.test.ts +1270 -0
  16. package/src/__tests__/unit/table-redo-undo.test.ts +909 -0
  17. package/src/__tests__/unit/utils.test-d.ts +49 -0
  18. package/src/__tests__/unit/utils.test.ts +715 -0
  19. package/src/__tests__/unit/utils.ts +216 -0
  20. package/src/__tests__/unit/vitest.d.ts +12 -0
  21. package/src/formats/container-format.ts +52 -0
  22. package/src/formats/index.ts +10 -0
  23. package/src/formats/overrides/block.ts +93 -0
  24. package/src/formats/overrides/blockquote.ts +8 -0
  25. package/src/formats/overrides/code.ts +8 -0
  26. package/src/formats/overrides/header.ts +8 -0
  27. package/src/formats/overrides/index.ts +6 -0
  28. package/src/formats/overrides/list.ts +10 -0
  29. package/src/formats/overrides/scroll.ts +51 -0
  30. package/src/formats/table-body-format.ts +92 -0
  31. package/src/formats/table-cell-format.ts +139 -0
  32. package/src/formats/table-cell-inner-format.ts +251 -0
  33. package/src/formats/table-col-format.ts +174 -0
  34. package/src/formats/table-colgroup-format.ts +133 -0
  35. package/src/formats/table-main-format.ts +143 -0
  36. package/src/formats/table-row-format.ts +147 -0
  37. package/src/formats/table-wrapper-format.ts +55 -0
  38. package/src/formats/utils.ts +3 -0
  39. package/src/index.ts +1157 -0
  40. package/src/modules/index.ts +5 -0
  41. package/src/modules/table-align.ts +116 -0
  42. package/src/modules/table-menu/constants.ts +140 -0
  43. package/src/modules/table-menu/index.ts +3 -0
  44. package/src/modules/table-menu/table-menu-common.ts +249 -0
  45. package/src/modules/table-menu/table-menu-contextmenu.ts +94 -0
  46. package/src/modules/table-menu/table-menu-select.ts +28 -0
  47. package/src/modules/table-resize/index.ts +5 -0
  48. package/src/modules/table-resize/table-resize-box.ts +293 -0
  49. package/src/modules/table-resize/table-resize-common.ts +343 -0
  50. package/src/modules/table-resize/table-resize-line.ts +163 -0
  51. package/src/modules/table-resize/table-resize-scale.ts +154 -0
  52. package/src/modules/table-resize/utils.ts +3 -0
  53. package/src/modules/table-scrollbar.ts +255 -0
  54. package/src/modules/table-selection.ts +262 -0
  55. package/src/style/button.less +45 -0
  56. package/src/style/color-picker.less +134 -0
  57. package/src/style/dialog.less +53 -0
  58. package/src/style/functions.less +9 -0
  59. package/src/style/index.less +89 -0
  60. package/src/style/input.less +64 -0
  61. package/src/style/select-box.less +51 -0
  62. package/src/style/table-creator.less +68 -0
  63. package/src/style/table-menu.less +122 -0
  64. package/src/style/table-resize-scale.less +31 -0
  65. package/src/style/table-resize.less +183 -0
  66. package/src/style/table-scrollbar.less +49 -0
  67. package/src/style/table-selection.less +15 -0
  68. package/src/style/tooltip.less +19 -0
  69. package/src/style/variables.less +1 -0
  70. package/src/svg/background.svg +1 -0
  71. package/src/svg/border.svg +1 -0
  72. package/src/svg/color.svg +1 -0
  73. package/src/svg/insert-bottom.svg +1 -0
  74. package/src/svg/insert-left.svg +1 -0
  75. package/src/svg/insert-right.svg +1 -0
  76. package/src/svg/insert-top.svg +1 -0
  77. package/src/svg/merge-cell.svg +1 -0
  78. package/src/svg/remove-column.svg +1 -0
  79. package/src/svg/remove-row.svg +1 -0
  80. package/src/svg/remove-table.svg +1 -0
  81. package/src/svg/split-cell.svg +1 -0
  82. package/src/types.d.ts +4 -0
  83. package/src/utils/bem.ts +23 -0
  84. package/src/utils/color.ts +109 -0
  85. package/src/utils/components/button.ts +22 -0
  86. package/src/utils/components/color-picker.ts +236 -0
  87. package/src/utils/components/dialog.ts +41 -0
  88. package/src/utils/components/index.ts +6 -0
  89. package/src/utils/components/input.ts +74 -0
  90. package/src/utils/components/table/creator.ts +86 -0
  91. package/src/utils/components/table/index.ts +2 -0
  92. package/src/utils/components/table/select-box.ts +83 -0
  93. package/src/utils/components/tooltip.ts +186 -0
  94. package/src/utils/constants.ts +99 -0
  95. package/src/utils/index.ts +7 -0
  96. package/src/utils/is.ts +6 -0
  97. package/src/utils/position.ts +21 -0
  98. package/src/utils/types.ts +131 -0
  99. package/src/utils/utils.ts +139 -0
package/src/index.ts ADDED
@@ -0,0 +1,1157 @@
1
+ import type { Range, Parchment as TypeParchment } from 'quill';
2
+ import type { BlockEmbed as TypeBlockEmbed } from 'quill/blots/block';
3
+ import type TypeBlock from 'quill/blots/block';
4
+ import type { Delta as TypeDelta } from 'quill/core';
5
+ import type { Context } from 'quill/modules/keyboard';
6
+ import type Keyboard from 'quill/modules/keyboard';
7
+ import type Toolbar from 'quill/modules/toolbar';
8
+ import type { InternalModule, InternalTableSelectionModule, QuillTheme, QuillThemePicker, TableConstantsData, TableTextOptions, TableUpOptions } from './utils';
9
+ import Quill from 'quill';
10
+ import { BlockOverride, BlockquoteOverride, CodeBlockOverride, ContainerFormat, HeaderOverride, ListItemOverride, ScrollOverride, TableBodyFormat, TableCellFormat, TableCellInnerFormat, TableColFormat, TableColgroupFormat, TableMainFormat, TableRowFormat, TableWrapperFormat } from './formats';
11
+ import { blotName, createBEM, createSelectBox, debounce, findParentBlot, findParentBlots, isFunction, isObject, isString, limitDomInViewPort, randomId, tableUpEvent, tableUpSize } from './utils';
12
+
13
+ const Delta = Quill.import('delta');
14
+ const Break = Quill.import('blots/break') as TypeParchment.BlotConstructor;
15
+ const icons = Quill.import('ui/icons') as Record<string, any>;
16
+
17
+ const createCell = (scroll: TypeParchment.ScrollBlot, { tableId, rowId, colId }: { tableId: string; rowId: string; colId: string }) => {
18
+ const value = {
19
+ tableId,
20
+ rowId,
21
+ colId,
22
+ colspan: 1,
23
+ rowspan: 1,
24
+ };
25
+ const tableCell = scroll.create(blotName.tableCell, value) as TypeParchment.ParentBlot;
26
+ const tableCellInner = scroll.create(blotName.tableCellInner, value) as TypeParchment.ParentBlot;
27
+ const block = scroll.create('block') as TypeParchment.ParentBlot;
28
+ block.appendChild(scroll.create('break'));
29
+ tableCellInner.appendChild(block);
30
+ tableCell.appendChild(tableCellInner);
31
+
32
+ return tableCell;
33
+ };
34
+ const getCellWidth = (cell: HTMLElement): number => {
35
+ let width = Number.parseFloat(cell.getAttribute('width') || tableUpSize.colDefaultWidth);
36
+ if (Number.isNaN(width)) {
37
+ const styleWidth = cell.style.width;
38
+ width = styleWidth ? Number.parseFloat(styleWidth) : cell.offsetWidth;
39
+ }
40
+ return width;
41
+ };
42
+ const calculateCols = (tableNode: HTMLElement, colNums: number): number[] => {
43
+ const colWidths = new Array(colNums).fill(tableUpSize.colDefaultWidth);
44
+ // no need consider colspan
45
+ // word table will have a row at last <!--[if !supportMisalignedColumns]-->
46
+ // that tr doesn't have colspan and every td have width attribute. but set style "border:none"
47
+ const rows = Array.from(tableNode.querySelectorAll('tr'));
48
+ for (const row of rows) {
49
+ const cells = Array.from(row.querySelectorAll('td'));
50
+ for (const [index, cell] of cells.entries()) {
51
+ if (index < colNums) {
52
+ const cellWidth = getCellWidth(cell);
53
+ colWidths[index] = cellWidth || colWidths[index];
54
+ }
55
+ else {
56
+ break;
57
+ }
58
+ }
59
+ }
60
+ return colWidths;
61
+ };
62
+
63
+ // Blots that cannot be inserted into a table
64
+ export const tableCantInsert: Set<string> = new Set([blotName.tableCellInner]);
65
+ const isForbidInTableBlot = (blot: TypeParchment.Blot) => tableCantInsert.has(blot.statics.blotName);
66
+ const isForbidInTable = (current: TypeParchment.Blot): boolean =>
67
+ current && current.parent
68
+ ? isForbidInTableBlot(current.parent)
69
+ ? true
70
+ : isForbidInTable(current.parent)
71
+ : false;
72
+
73
+ export class TableUp {
74
+ static moduleName = 'table-up';
75
+ static toolName: string = blotName.tableWrapper;
76
+ static keyboradHandler = {
77
+ 'forbid remove table by backspace': {
78
+ bindInHead: true,
79
+ key: 'Backspace',
80
+ collapsed: true,
81
+ offset: 0,
82
+ handler(this: { quill: Quill }, range: Range, context: Context) {
83
+ const line = this.quill.getLine(range.index);
84
+ const blot = line[0] as TypeParchment.BlockBlot;
85
+ if (blot.prev instanceof TableWrapperFormat) {
86
+ blot.prev.remove();
87
+ return false;
88
+ }
89
+
90
+ if (context.format[blotName.tableCellInner]) {
91
+ const offset = blot.offset(findParentBlot(blot, blotName.tableCellInner));
92
+ if (offset === 0) {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ return true;
98
+ },
99
+ },
100
+ 'forbid remove table by delete': {
101
+ bindInHead: true,
102
+ key: 'Delete',
103
+ collapsed: true,
104
+ handler(this: { quill: Quill }, range: Range, context: Context) {
105
+ const line = this.quill.getLine(range.index);
106
+ const blot = line[0] as TypeParchment.BlockBlot;
107
+ const offsetInline = line[1];
108
+ if ((blot.next instanceof TableWrapperFormat || blot.next instanceof TableColFormat) && offsetInline === blot.length() - 1) return false;
109
+
110
+ if (context.format[blotName.tableCellInner]) {
111
+ const tableInnerBlot = findParentBlot(blot, blotName.tableCellInner);
112
+ if (blot === tableInnerBlot.children.tail && offsetInline === blot.length() - 1) {
113
+ return false;
114
+ }
115
+ }
116
+ return true;
117
+ },
118
+ },
119
+ 'after table insert new line': {
120
+ // lick 'code exit'
121
+ bindInHead: true,
122
+ key: 'Enter',
123
+ collapsed: true,
124
+ format: [blotName.tableCellInner],
125
+ prefix: /^$/,
126
+ suffix: /^\s*$/,
127
+ handler(this: { quill: Quill }, range: Range) {
128
+ const [line, offset] = this.quill.getLine(range.index);
129
+ const format = this.quill.getFormat(range.index + offset + 1, 1);
130
+ // next line still in table. not exit
131
+ if (format[blotName.tableCellInner]) {
132
+ return true;
133
+ }
134
+ // if have tow empty lines in table cell. enter will exit table and add a new line after table
135
+ let numLines = 2;
136
+ let cur = line;
137
+ while (cur !== null && cur.length() <= 1) {
138
+ cur = cur.prev as TypeBlock | TypeBlockEmbed | null;
139
+ numLines -= 1;
140
+ if (numLines <= 0) {
141
+ this.quill.insertText(range.index + 1, '\n');
142
+ this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
143
+ return false;
144
+ }
145
+ }
146
+ return true;
147
+ },
148
+ },
149
+ };
150
+
151
+ static register() {
152
+ TableWrapperFormat.allowedChildren = [TableMainFormat];
153
+
154
+ TableMainFormat.allowedChildren = [TableBodyFormat, TableColgroupFormat];
155
+ TableMainFormat.requiredContainer = TableWrapperFormat;
156
+
157
+ TableColgroupFormat.allowedChildren = [TableColFormat];
158
+ TableColgroupFormat.requiredContainer = TableMainFormat;
159
+
160
+ TableBodyFormat.allowedChildren = [TableRowFormat];
161
+ TableBodyFormat.requiredContainer = TableMainFormat;
162
+
163
+ TableRowFormat.allowedChildren = [TableCellFormat];
164
+ TableCellFormat.requiredContainer = TableBodyFormat;
165
+
166
+ TableCellFormat.allowedChildren = [TableCellInnerFormat, Break];
167
+ TableCellFormat.requiredContainer = TableRowFormat;
168
+
169
+ TableCellInnerFormat.requiredContainer = TableCellFormat;
170
+
171
+ Quill.register({
172
+ 'blots/scroll': ScrollOverride,
173
+ 'blots/block': BlockOverride,
174
+ [`blots/${blotName.container}`]: ContainerFormat,
175
+ 'formats/header': HeaderOverride,
176
+ 'formats/list': ListItemOverride,
177
+ 'formats/blockquote': BlockquoteOverride,
178
+ 'formats/code-block': CodeBlockOverride,
179
+ [`formats/${blotName.tableCell}`]: TableCellFormat,
180
+ [`formats/${blotName.tableCellInner}`]: TableCellInnerFormat,
181
+ [`formats/${blotName.tableRow}`]: TableRowFormat,
182
+ [`formats/${blotName.tableBody}`]: TableBodyFormat,
183
+ [`formats/${blotName.tableCol}`]: TableColFormat,
184
+ [`formats/${blotName.tableColgroup}`]: TableColgroupFormat,
185
+ [`formats/${blotName.tableMain}`]: TableMainFormat,
186
+ [`formats/${blotName.tableWrapper}`]: TableWrapperFormat,
187
+ }, true);
188
+ }
189
+
190
+ quill: Quill;
191
+ options: TableUpOptions;
192
+ toolBox: HTMLDivElement;
193
+ fixTableByLisenter = debounce(this.balanceTables, 100);
194
+ selector?: HTMLElement;
195
+ table?: HTMLElement;
196
+ tableSelection?: InternalTableSelectionModule;
197
+ tableResize?: InternalModule;
198
+ tableScrollbar?: InternalModule;
199
+ tableAlign?: InternalModule;
200
+ tableResizeScale?: InternalModule;
201
+
202
+ get statics(): any {
203
+ return this.constructor;
204
+ }
205
+
206
+ constructor(quill: Quill, options: Partial<TableUpOptions>) {
207
+ this.quill = quill;
208
+ this.options = this.resolveOptions(options || {});
209
+
210
+ if (!this.options.scrollbar) {
211
+ const scrollbarBEM = createBEM('scrollbar');
212
+ this.quill.container.classList.add(scrollbarBEM.bm('origin'));
213
+ }
214
+
215
+ const toolboxBEM = createBEM('toolbox');
216
+ this.toolBox = this.quill.addContainer(toolboxBEM.b());
217
+
218
+ const toolbar = this.quill.getModule('toolbar') as Toolbar;
219
+ if (toolbar && (this.quill.theme as QuillTheme).pickers) {
220
+ const [, select] = (toolbar.controls as [string, HTMLElement][] || []).find(([name]) => name === this.statics.toolName) || [];
221
+ if (select && select.tagName.toLocaleLowerCase() === 'select') {
222
+ const picker = (this.quill.theme as QuillTheme).pickers.find(picker => picker.select === select);
223
+ if (picker) {
224
+ picker.label.innerHTML = this.options.icon;
225
+ this.buildCustomSelect(this.options.customSelect, picker);
226
+ picker.label.addEventListener('mousedown', () => {
227
+ if (!this.selector || !picker) return;
228
+ const selectRect = this.selector.getBoundingClientRect();
229
+ const { leftLimited } = limitDomInViewPort(selectRect);
230
+ if (leftLimited) {
231
+ const labelRect = picker.label.getBoundingClientRect();
232
+ Object.assign(picker.options.style, { transform: `translateX(calc(-100% + ${labelRect.width}px))` });
233
+ }
234
+ else {
235
+ Object.assign(picker.options.style, { transform: undefined });
236
+ }
237
+ });
238
+ }
239
+ }
240
+ }
241
+
242
+ const keyboard = this.quill.getModule('keyboard') as Keyboard;
243
+ for (const handle of Object.values(TableUp.keyboradHandler)) {
244
+ // insert before default key handler
245
+ if (handle.bindInHead) {
246
+ keyboard.bindings[handle.key].unshift(handle);
247
+ }
248
+ else {
249
+ keyboard.addBinding(handle.key, handle);
250
+ }
251
+ }
252
+
253
+ this.quill.root.addEventListener(
254
+ 'click',
255
+ (evt: MouseEvent) => {
256
+ const path = evt.composedPath() as HTMLElement[];
257
+ if (!path || path.length <= 0) return;
258
+
259
+ const tableNode = path.find(node => node.tagName && node.tagName.toUpperCase() === 'TABLE' && node.classList.contains('ql-table'));
260
+ if (tableNode) {
261
+ if (this.table === tableNode) {
262
+ this.tableSelection && this.tableSelection.show();
263
+ this.tableAlign && this.tableAlign.update();
264
+ return;
265
+ }
266
+ if (this.table) this.hideTableTools();
267
+ this.showTableTools(tableNode, quill);
268
+ }
269
+ else if (this.table) {
270
+ this.hideTableTools();
271
+ }
272
+ },
273
+ false,
274
+ );
275
+ this.quill.on(Quill.events.EDITOR_CHANGE, (event: string, range: Range, oldRange: Range) => {
276
+ if (event === Quill.events.SELECTION_CHANGE && range) {
277
+ const [startBlot] = this.quill.getLine(range.index);
278
+ const [endBlot] = this.quill.getLine(range.index + range.length);
279
+ let startTableBlot;
280
+ let endTableBlot;
281
+ try {
282
+ startTableBlot = findParentBlot(startBlot!, blotName.tableMain);
283
+ }
284
+ catch {}
285
+ try {
286
+ endTableBlot = findParentBlot(endBlot!, blotName.tableMain);
287
+ }
288
+ catch {}
289
+
290
+ // only can select inside table or select all table
291
+ if (startBlot instanceof TableColFormat) {
292
+ if (!oldRange) {
293
+ oldRange = { index: 0, length: 0 };
294
+ }
295
+ return this.quill.setSelection(
296
+ range.index + (oldRange.index > range.index ? -1 : 1),
297
+ range.length + (oldRange.length === range.length ? 0 : oldRange.length > range.length ? -1 : 1),
298
+ Quill.sources.USER,
299
+ );
300
+ }
301
+ else if (endBlot instanceof TableColFormat) {
302
+ return this.quill.setSelection(range.index + 1, range.length + 1, Quill.sources.USER);
303
+ }
304
+
305
+ if (range.length > 0) {
306
+ if (startTableBlot && !endTableBlot) {
307
+ this.quill.setSelection(range.index - 1, range.length + 1, Quill.sources.USER);
308
+ }
309
+ else if (endTableBlot && !startTableBlot) {
310
+ this.quill.setSelection(range.index, range.length + 1, Quill.sources.USER);
311
+ }
312
+ }
313
+
314
+ // if range is not in table. hide table tools
315
+ if (!startTableBlot || !endTableBlot) {
316
+ this.hideTableTools();
317
+ }
318
+ }
319
+ });
320
+ this.quill.on(tableUpEvent.AFTER_TABLE_RESIZE, () => {
321
+ this.tableSelection && this.tableSelection.hide();
322
+ });
323
+
324
+ this.pasteTableHandler();
325
+ this.listenBalanceCells();
326
+ }
327
+
328
+ addContainer(classes: string | HTMLElement) {
329
+ if (isString(classes)) {
330
+ const el = document.createElement('div');
331
+ for (const classname of classes.split(' ')) {
332
+ el.classList.add(classname);
333
+ }
334
+ this.toolBox.appendChild(el);
335
+ return el;
336
+ }
337
+ else {
338
+ this.toolBox.appendChild(classes);
339
+ return classes;
340
+ }
341
+ }
342
+
343
+ resolveOptions(options: Partial<TableUpOptions>): TableUpOptions {
344
+ return Object.assign({
345
+ customBtn: false,
346
+ texts: this.resolveTexts(options.texts || {}),
347
+ full: false,
348
+ fullSwtich: true,
349
+ icon: icons.table,
350
+ selectionOptions: {},
351
+ alignOptions: {},
352
+ scrollbarOptions: {},
353
+ resizeOptions: {},
354
+ resizeScaleOptions: {},
355
+ } as TableUpOptions, options);
356
+ };
357
+
358
+ resolveTexts(options: Partial<TableTextOptions>) {
359
+ return Object.assign({
360
+ fullCheckboxText: 'Insert full width table',
361
+ customBtnText: 'Custom',
362
+ confirmText: 'Confirm',
363
+ cancelText: 'Cancel',
364
+ rowText: 'Row',
365
+ colText: 'Column',
366
+ notPositiveNumberError: 'Please enter a positive integer',
367
+ custom: 'Custom',
368
+ clear: 'Clear',
369
+ transparent: 'Transparent',
370
+ perWidthInsufficient: 'The percentage width is insufficient. To complete the operation, the table needs to be converted to a fixed width. Do you want to continue?',
371
+ }, options);
372
+ };
373
+
374
+ pasteTableHandler() {
375
+ let tableId = randomId();
376
+ let rowId = randomId();
377
+ let colIds: string[] = [];
378
+ let cellCount = 0;
379
+ let colCount = 0;
380
+
381
+ // handle paste html or text into table cell
382
+ const pasteElementIntoCell = (node: Node, delta: TypeDelta, _scroll: TypeParchment.ScrollBlot) => {
383
+ const range = this.quill.getSelection(true);
384
+ const formats = this.quill.getFormat(range);
385
+ const tableCellInnerValue = formats[blotName.tableCellInner];
386
+ if (tableCellInnerValue) {
387
+ for (const op of delta.ops) {
388
+ if (!op.attributes) op.attributes = {};
389
+ op.attributes[blotName.tableCellInner] = tableCellInnerValue;
390
+ }
391
+ }
392
+ return delta;
393
+ };
394
+ this.quill.clipboard.addMatcher(Node.TEXT_NODE, pasteElementIntoCell);
395
+ this.quill.clipboard.addMatcher(Node.ELEMENT_NODE, pasteElementIntoCell);
396
+
397
+ this.quill.clipboard.addMatcher('table', (node, delta) => {
398
+ if (delta.ops.length === 0) return delta;
399
+ // if current in table. prevent paste table
400
+ const format = this.quill.getFormat();
401
+ if (format[blotName.tableCellInner]) return new Delta();
402
+ // remove quill origin table format
403
+ const ops: Record<string, any>[] = [];
404
+ const cols: Record<string, any>[] = [];
405
+ for (let i = 0; i < delta.ops.length; i++) {
406
+ const { attributes, insert } = delta.ops[i];
407
+ const { table, [blotName.tableCell]: tableCell, ...attrs } = attributes || {};
408
+ if (insert && (insert as Record<string, any>)[blotName.tableCol]) {
409
+ cols.push({ insert });
410
+ }
411
+ else {
412
+ ops.push({ attributes: attrs, insert });
413
+ }
414
+ }
415
+
416
+ const colWidths = calculateCols(node as HTMLElement, colIds.length);
417
+ const newCols = colWidths.reduce((colOps, width, i) => {
418
+ if (!cols[i]) {
419
+ colOps.push({
420
+ insert: {
421
+ [blotName.tableCol]: {
422
+ tableId,
423
+ colId: colIds[i],
424
+ width,
425
+ full: false,
426
+ },
427
+ },
428
+ });
429
+ }
430
+ else {
431
+ colOps.push(cols[i]);
432
+ }
433
+ return colOps;
434
+ }, [] as Record<string, any>[]);
435
+ ops.unshift(...newCols);
436
+ // reset variable to avoid conflict with other table
437
+ tableId = randomId();
438
+ colIds = [];
439
+ cellCount = 0;
440
+ colCount = 0;
441
+ // insert break line before table and after table
442
+ ops.unshift({ insert: '\n' });
443
+ ops.push({ insert: '\n' });
444
+ return new Delta(ops);
445
+ });
446
+
447
+ this.quill.clipboard.addMatcher('colgroup', (node, delta) => {
448
+ const ops: Record<string, any>[] = [];
449
+ for (let i = 0; i < delta.ops.length; i++) {
450
+ const op = delta.ops[i];
451
+ if (op && isObject(op.insert) && op.insert[blotName.tableCol]) {
452
+ ops.push(op);
453
+ }
454
+ }
455
+ return new Delta(ops);
456
+ });
457
+ this.quill.clipboard.addMatcher('col', (node) => {
458
+ colIds[colCount] = randomId();
459
+ const delta = new Delta().insert({
460
+ [blotName.tableCol]: Object.assign(
461
+ TableColFormat.value(node as HTMLElement),
462
+ {
463
+ tableId,
464
+ colId: colIds[colCount],
465
+ },
466
+ ),
467
+ });
468
+ colCount += 1;
469
+ return delta;
470
+ });
471
+
472
+ this.quill.clipboard.addMatcher('tr', (node, delta) => {
473
+ rowId = randomId();
474
+ cellCount = 0;
475
+ for (const op of delta.ops) {
476
+ if (
477
+ op.attributes && op.attributes.background
478
+ && op.attributes[blotName.tableCellInner]
479
+ ) {
480
+ const cellAttrs = op.attributes[blotName.tableCellInner] as Record<string, any>;
481
+ if (!cellAttrs.style) cellAttrs.style = '';
482
+ (op.attributes[blotName.tableCellInner] as Record<string, any>).style = `background:${op.attributes.background};${cellAttrs.style}`;
483
+ }
484
+ }
485
+ return delta;
486
+ });
487
+
488
+ const matchCell = (node: Node, delta: TypeDelta) => {
489
+ const cell = node as HTMLElement;
490
+ const cellFormat = TableCellFormat.formats(cell);
491
+ if (!colIds[cellCount]) {
492
+ for (let i = cellCount; i >= 0; i--) {
493
+ if (!colIds[i]) colIds[i] = randomId();
494
+ }
495
+ }
496
+ const colId = colIds[cellCount];
497
+ cellCount += cellFormat.colspan;
498
+
499
+ // add each insert tableCellInner format
500
+ const value = Object.assign(
501
+ cellFormat,
502
+ {
503
+ tableId,
504
+ rowId,
505
+ colId,
506
+ },
507
+ );
508
+ // make sure <!--[if !supportMisalignedColumns]--> display border
509
+ if (cell.style.border === 'none') {
510
+ value.style = value.style.replaceAll(/border-(top|right|bottom|left)-style:none;?/g, '');
511
+ }
512
+ const ops = [];
513
+ for (const op of delta.ops) {
514
+ const { insert, attributes } = op;
515
+ if (op.insert) {
516
+ const attrs = { ...attributes };
517
+ delete attrs[blotName.tableCell];
518
+ ops.push({ insert, attributes: { ...attrs, [blotName.tableCellInner]: value } });
519
+ }
520
+ }
521
+ return new Delta(ops);
522
+ };
523
+
524
+ this.quill.clipboard.addMatcher('td', matchCell);
525
+ this.quill.clipboard.addMatcher('th', matchCell);
526
+ }
527
+
528
+ showTableTools(table: HTMLElement, quill: Quill) {
529
+ if (table) {
530
+ this.table = table;
531
+ if (this.options.selection) {
532
+ this.tableSelection = new this.options.selection(this, table, quill, this.options.selectionOptions);
533
+ }
534
+ if (this.options.align) {
535
+ this.tableAlign = new this.options.align(this, table, quill, this.options.alignOptions);
536
+ }
537
+ if (this.options.scrollbar) {
538
+ this.tableScrollbar = new this.options.scrollbar(this, table, quill, this.options.scrollbarOptions);
539
+ }
540
+ if (this.options.resize) {
541
+ this.tableResize = new this.options.resize(this, table, quill, this.options.resizeOptions);
542
+ }
543
+ if (this.options.resizeScale) {
544
+ this.tableResizeScale = new this.options.resizeScale(this, table, quill, this.options.resizeScaleOptions);
545
+ }
546
+ }
547
+ }
548
+
549
+ hideTableTools() {
550
+ if (this.tableSelection) {
551
+ this.tableSelection.destroy();
552
+ this.tableSelection = undefined;
553
+ }
554
+ if (this.tableScrollbar) {
555
+ this.tableScrollbar.destroy();
556
+ this.tableScrollbar = undefined;
557
+ }
558
+ if (this.tableAlign) {
559
+ this.tableAlign.destroy();
560
+ this.tableAlign = undefined;
561
+ }
562
+ if (this.tableResize) {
563
+ this.tableResize.destroy();
564
+ this.tableResize = undefined;
565
+ }
566
+ if (this.tableResizeScale) {
567
+ this.tableResizeScale.destroy();
568
+ }
569
+ this.table = undefined;
570
+ }
571
+
572
+ async buildCustomSelect(customSelect: ((module: TableUp, picker: QuillThemePicker) => HTMLElement | Promise<HTMLElement>) | undefined, picker: QuillThemePicker) {
573
+ if (!customSelect || !isFunction(customSelect)) return;
574
+ const dom = document.createElement('div');
575
+ dom.classList.add('ql-custom-select');
576
+ this.selector = await customSelect(this, picker);
577
+ dom.appendChild(this.selector);
578
+ if (this.options.fullSwtich) {
579
+ const bem = createBEM('creator');
580
+ const isFulllLabel = document.createElement('label');
581
+ isFulllLabel.classList.add(bem.be('checkbox'));
582
+ const isFullCheckbox = document.createElement('input');
583
+ isFullCheckbox.type = 'checkbox';
584
+ isFullCheckbox.checked = this.options.full;
585
+ isFullCheckbox.addEventListener('change', () => {
586
+ this.options.full = isFullCheckbox.checked;
587
+ });
588
+ const mark = document.createElement('div');
589
+ mark.classList.add(bem.be('mark'));
590
+ const isFullCheckboxText = document.createElement('span');
591
+ isFullCheckboxText.textContent = this.options.texts.fullCheckboxText;
592
+ isFulllLabel.appendChild(isFullCheckbox);
593
+ isFulllLabel.appendChild(mark);
594
+ isFulllLabel.appendChild(isFullCheckboxText);
595
+ dom.appendChild(isFulllLabel);
596
+ }
597
+ picker.options.appendChild(dom);
598
+ };
599
+
600
+ setCellAttrs(selectedTds: TableCellInnerFormat[], attr: string, value?: any, isStyle: boolean = false) {
601
+ if (selectedTds.length === 0) return;
602
+ for (const td of selectedTds) {
603
+ td.setFormatValue(attr, value, isStyle);
604
+ }
605
+ }
606
+
607
+ insertTable(rows: number, columns: number) {
608
+ if (rows >= 30 || columns >= 30) {
609
+ throw new Error('Both rows and columns must be less than 30.');
610
+ }
611
+
612
+ this.quill.focus();
613
+ const range = this.quill.getSelection();
614
+ if (range == null) return;
615
+ const [currentBlot] = this.quill.getLeaf(range.index);
616
+ if (!currentBlot) return;
617
+ if (isForbidInTable(currentBlot)) {
618
+ throw new Error(`Not supported ${currentBlot.statics.blotName} insert into table.`);
619
+ }
620
+
621
+ const borderWidth = this.calculateTableCellBorderWidth();
622
+ const rootStyle = getComputedStyle(this.quill.root);
623
+ const paddingLeft = Number.parseInt(rootStyle.paddingLeft);
624
+ const paddingRight = Number.parseInt(rootStyle.paddingRight);
625
+ const width = Number.parseInt(rootStyle.width) - paddingLeft - paddingRight - borderWidth;
626
+
627
+ const tableId = randomId();
628
+ const colIds = new Array(columns).fill(0).map(() => randomId());
629
+
630
+ // insert delta data to create table
631
+ const colWidth = !this.options.full ? `${Math.max(Math.floor(width / columns), tableUpSize.colMinWidthPx)}px` : `${Math.max((1 / columns) * 100, tableUpSize.colMinWidthPre)}%`;
632
+ const delta: Record<string, any>[] = [
633
+ { retain: range.index },
634
+ { insert: '\n' },
635
+ ];
636
+
637
+ for (let i = 0; i < columns; i++) {
638
+ delta.push({
639
+ insert: {
640
+ [blotName.tableCol]: {
641
+ width: colWidth,
642
+ tableId,
643
+ colId: colIds[i],
644
+ full: this.options.full,
645
+ },
646
+ },
647
+ });
648
+ }
649
+ for (let j = 0; j < rows; j++) {
650
+ const rowId = randomId();
651
+ for (let i = 0; i < columns; i++) {
652
+ delta.push({
653
+ insert: '\n',
654
+ attributes: {
655
+ [blotName.tableCellInner]: {
656
+ tableId,
657
+ rowId,
658
+ colId: colIds[i],
659
+ rowspan: 1,
660
+ colspan: 1,
661
+ },
662
+ },
663
+ });
664
+ }
665
+ }
666
+
667
+ this.quill.updateContents(new Delta(delta), Quill.sources.USER);
668
+ this.quill.setSelection(range.index + columns + columns * rows + 1, Quill.sources.SILENT);
669
+ this.quill.focus();
670
+ }
671
+
672
+ calculateTableCellBorderWidth() {
673
+ const tableStr = `
674
+ <table class="${TableMainFormat.className}">
675
+ <tbody>
676
+ <tr>
677
+ <td class="${TableCellFormat.className}"></td>
678
+ </tr>
679
+ </tbody>
680
+ </table>
681
+ `;
682
+ const div = document.createElement('div');
683
+ div.className = TableWrapperFormat.className;
684
+ div.innerHTML = tableStr;
685
+ div.style.position = 'absolute';
686
+ div.style.left = '-9999px';
687
+ div.style.top = '-9999px';
688
+ div.style.visibility = 'hidden';
689
+ this.quill.root.appendChild(div);
690
+ const tempTableStyle = window.getComputedStyle(div.querySelector('td')!);
691
+ const borderWidth = Number.parseFloat(tempTableStyle.borderWidth) || 0;
692
+ this.quill.root.removeChild(div);
693
+ return borderWidth;
694
+ }
695
+
696
+ // handle unusual delete cell
697
+ fixUnusuaDeletelTable(tableBlot: TableMainFormat) {
698
+ // calculate all cells
699
+ const trBlots = tableBlot.getRows();
700
+ const tableColIds = tableBlot.getColIds();
701
+ if (trBlots.length === 0) {
702
+ return tableBlot.remove();
703
+ }
704
+ if (tableColIds.length === 0) return;
705
+ // append by col
706
+ const cellSpanMap = new Array(trBlots.length).fill(0).map(() => new Array(tableColIds.length).fill(false));
707
+ const tableId = tableBlot.tableId;
708
+ for (const [indexTr, tr] of trBlots.entries()) {
709
+ let indexTd = 0;
710
+ let indexCol = 0;
711
+ const curCellSpan = cellSpanMap[indexTr];
712
+ const tds = tr.descendants(TableCellFormat);
713
+ // loop every row and column
714
+ while (indexCol < tableColIds.length) {
715
+ // skip when rowspan or colspan
716
+ if (curCellSpan[indexCol]) {
717
+ indexCol += 1;
718
+ continue;
719
+ }
720
+ const curTd = tds[indexTd];
721
+ // if colId does not match. insert a new one
722
+ if (!curTd || curTd.colId !== tableColIds[indexCol]) {
723
+ tr.insertBefore(
724
+ createCell(
725
+ this.quill.scroll,
726
+ {
727
+ tableId,
728
+ colId: tableColIds[indexCol],
729
+ rowId: tr.rowId,
730
+ },
731
+ ),
732
+ curTd,
733
+ );
734
+ }
735
+ else {
736
+ if (indexTr + curTd.rowspan - 1 >= trBlots.length) {
737
+ curTd.getCellInner().rowspan = trBlots.length - indexTr;
738
+ }
739
+
740
+ const { colspan, rowspan } = curTd;
741
+ // skip next column cell
742
+ if (colspan > 1) {
743
+ for (let c = 1; c < colspan; c++) {
744
+ curCellSpan[indexCol + c] = true;
745
+ }
746
+ }
747
+ // skip next rowspan cell
748
+ if (rowspan > 1) {
749
+ for (let r = indexTr + 1; r < indexTr + rowspan; r++) {
750
+ for (let c = 0; c < colspan; c++) {
751
+ cellSpanMap[r][indexCol + c] = true;
752
+ }
753
+ }
754
+ }
755
+ indexTd += 1;
756
+ }
757
+ indexCol += 1;
758
+ }
759
+
760
+ // if td not match all exist td. Indicates that a cell has been inserted
761
+ if (indexTd < tds.length) {
762
+ for (let i = indexTd; i < tds.length; i++) {
763
+ tds[i].remove();
764
+ }
765
+ }
766
+ }
767
+ }
768
+
769
+ balanceTables() {
770
+ for (const tableBlot of this.quill.scroll.descendants(TableMainFormat)) {
771
+ this.fixUnusuaDeletelTable(tableBlot);
772
+ }
773
+ }
774
+
775
+ listenBalanceCells() {
776
+ this.quill.on(
777
+ Quill.events.SCROLL_OPTIMIZE,
778
+ (mutations: MutationRecord[]) => {
779
+ mutations.some((mutation) => {
780
+ if (
781
+ // TODO: if need add ['COL', 'COLGROUP']
782
+ ['TD', 'TR', 'TBODY', 'TABLE'].includes((mutation.target as HTMLElement).tagName)
783
+ ) {
784
+ this.fixTableByLisenter();
785
+ return true;
786
+ }
787
+ return false;
788
+ });
789
+ },
790
+ );
791
+ }
792
+
793
+ deleteTable() {
794
+ if (!this.tableSelection || this.tableSelection.selectedTds.length === 0) return;
795
+ const selectedTds = this.tableSelection.selectedTds;
796
+ const tableBlot = findParentBlot(selectedTds[0], blotName.tableMain);
797
+ tableBlot && tableBlot.remove();
798
+ this.hideTableTools();
799
+ }
800
+
801
+ appendRow(isDown: boolean) {
802
+ if (!this.tableSelection) return;
803
+ const selectedTds = this.tableSelection.selectedTds;
804
+ if (selectedTds.length <= 0) return;
805
+ // find baseTd and baseTr
806
+ const baseTd = selectedTds[isDown ? selectedTds.length - 1 : 0];
807
+ const [tableBlot, tableBodyBlot, baseTdParentTr] = findParentBlots(baseTd, [blotName.tableMain, blotName.tableBody, blotName.tableRow] as const);
808
+ const tableTrs = tableBlot.getRows();
809
+ const i = tableTrs.indexOf(baseTdParentTr);
810
+ const insertRowIndex = i + (isDown ? baseTd.rowspan : 0);
811
+
812
+ tableBodyBlot.insertRow(insertRowIndex);
813
+ }
814
+
815
+ appendCol(isRight: boolean) {
816
+ if (!this.tableSelection) return;
817
+ const selectedTds = this.tableSelection.selectedTds;
818
+ if (selectedTds.length <= 0) return;
819
+
820
+ // find insert column index in row
821
+ const [baseTd] = selectedTds.reduce((pre, cur) => {
822
+ const columnIndex = cur.getColumnIndex();
823
+ if (!isRight && columnIndex <= pre[1]) {
824
+ pre = [cur, columnIndex];
825
+ }
826
+ else if (isRight && columnIndex >= pre[1]) {
827
+ pre = [cur, columnIndex];
828
+ }
829
+ return pre;
830
+ }, [selectedTds[0], selectedTds[0].getColumnIndex()]);
831
+ const columnIndex = baseTd.getColumnIndex() + (isRight ? baseTd.colspan : 0);
832
+
833
+ const tableBlot = findParentBlot(baseTd, blotName.tableMain);
834
+ const tableId = tableBlot.tableId;
835
+ const newColId = randomId();
836
+
837
+ const [colgroup] = tableBlot.descendants(TableColgroupFormat);
838
+ if (colgroup) {
839
+ colgroup.insertColByIndex(columnIndex, {
840
+ tableId,
841
+ colId: newColId,
842
+ width: tableBlot.full ? '6%' : '160px',
843
+ full: tableBlot.full,
844
+ });
845
+ }
846
+
847
+ // loop tr and insert cell at index
848
+ // if index is inner cell, skip next `rowspan` line
849
+ // if there are cells both have column span and row span before index cell, minus `colspan` cell for next line
850
+ const trs = tableBlot.getRows();
851
+ const spanCols: number[] = [];
852
+ let skipRowNum = 0;
853
+ for (const tr of Object.values(trs)) {
854
+ const spanCol = spanCols.shift() || 0;
855
+ if (skipRowNum > 0) {
856
+ skipRowNum -= 1;
857
+ continue;
858
+ }
859
+ const nextSpanCols = tr.insertCell(columnIndex - spanCol, {
860
+ tableId,
861
+ rowId: tr.rowId,
862
+ colId: newColId,
863
+ rowspan: 1,
864
+ colspan: 1,
865
+ });
866
+ if (nextSpanCols.skipRowNum) {
867
+ skipRowNum += nextSpanCols.skipRowNum;
868
+ }
869
+ for (const [i, n] of nextSpanCols.entries()) {
870
+ spanCols[i] = (spanCols[i] || 0) + n;
871
+ }
872
+ }
873
+ }
874
+
875
+ /**
876
+ * after insert or remove cell. handle cell colspan and rowspan merge
877
+ */
878
+ fixTableByRemove(tableBlot: TableMainFormat) {
879
+ // calculate all cells
880
+ // maybe will get empty tr
881
+ const trBlots = tableBlot.getRows();
882
+ const tableCols = tableBlot.getCols();
883
+ const colIdMap = tableCols.reduce((idMap, col) => {
884
+ idMap[col.colId] = 0;
885
+ return idMap;
886
+ }, {} as Record<string, number>);
887
+ // merge rowspan
888
+ const reverseTrBlots = [...trBlots].reverse();
889
+ const removeTr: number[] = [];
890
+ for (const [index, tr] of reverseTrBlots.entries()) {
891
+ const i = trBlots.length - index - 1;
892
+ if (tr.children.length <= 0) {
893
+ removeTr.push(i);
894
+ }
895
+ else {
896
+ // if have td rowspan across empty tr. minus rowspan
897
+ tr.foreachCellInner((td) => {
898
+ const sum = removeTr.reduce((sum, val) => td.rowspan + i > val ? sum + 1 : sum, 0);
899
+ td.rowspan -= sum;
900
+ // count exist col
901
+ colIdMap[td.colId] += 1;
902
+ });
903
+ }
904
+ }
905
+ // merge colspan
906
+ let index = 0;
907
+ for (const count of Object.values(colIdMap)) {
908
+ if (count === 0) {
909
+ const spanCols: number[] = [];
910
+ let skipRowNum = 0;
911
+ for (const tr of Object.values(trBlots)) {
912
+ const spanCol = spanCols.shift() || 0;
913
+ let nextSpanCols = [];
914
+ if (skipRowNum > 0) {
915
+ nextSpanCols = tr.getCellByColumIndex(index - spanCol)[2];
916
+ skipRowNum -= 1;
917
+ }
918
+ else {
919
+ nextSpanCols = tr.removeCell(index - spanCol);
920
+ if (nextSpanCols.skipRowNum) {
921
+ skipRowNum += nextSpanCols.skipRowNum;
922
+ }
923
+ }
924
+ for (const [i, n] of nextSpanCols.entries()) {
925
+ spanCols[i] = (spanCols[i] || 0) + n;
926
+ }
927
+ }
928
+ }
929
+ else {
930
+ index += 1;
931
+ }
932
+ }
933
+ // remove col
934
+ for (const col of tableCols) {
935
+ if (colIdMap[col.colId] === 0) {
936
+ if (col.prev) {
937
+ (col.prev as TableColFormat).width += col.width;
938
+ }
939
+ else if (col.next) {
940
+ (col.next as TableColFormat).width += col.width;
941
+ }
942
+ col.remove();
943
+ }
944
+ }
945
+ }
946
+
947
+ removeRow() {
948
+ if (!this.tableSelection) return;
949
+ const selectedTds = this.tableSelection.selectedTds;
950
+ if (selectedTds.length <= 0) return;
951
+ const baseTd = selectedTds[0];
952
+ const tableBlot = findParentBlot(baseTd, blotName.tableMain);
953
+ const trs = tableBlot.getRows();
954
+ let endTrIndex = trs.length;
955
+ let nextTrIndex = -1;
956
+ for (const td of selectedTds) {
957
+ const tr = findParentBlot(td, blotName.tableRow);
958
+ const index = trs.indexOf(tr);
959
+ if (index < endTrIndex) {
960
+ endTrIndex = index;
961
+ }
962
+ if (index + td.rowspan > nextTrIndex) {
963
+ nextTrIndex = index + td.rowspan;
964
+ }
965
+ }
966
+
967
+ const patchTds: {
968
+ [key: string]: {
969
+ rowspan: number;
970
+ colspan: number;
971
+ colIndex: number;
972
+ };
973
+ } = {};
974
+ for (let i = endTrIndex; i < Math.min(trs.length, nextTrIndex); i++) {
975
+ const tr = trs[i];
976
+ tr.foreachCellInner((td) => {
977
+ // find cells in rowspan that exceed the deletion range
978
+ if (td.rowspan + i > nextTrIndex) {
979
+ patchTds[td.colId] = {
980
+ rowspan: td.rowspan + i - nextTrIndex,
981
+ colspan: td.colspan,
982
+ colIndex: td.getColumnIndex(),
983
+ };
984
+ }
985
+ // only remove td. empty tr to calculate colspan and rowspan
986
+ td.parent.remove();
987
+ });
988
+ }
989
+
990
+ if (trs[nextTrIndex]) {
991
+ const nextTr = trs[nextTrIndex];
992
+ const tableId = tableBlot.tableId;
993
+ // insert cell in nextTr to patch exceed cell
994
+ for (const [colId, { colIndex, colspan, rowspan }] of Object.entries(patchTds)) {
995
+ nextTr.insertCell(colIndex, {
996
+ tableId,
997
+ rowId: nextTr.rowId,
998
+ colId,
999
+ colspan,
1000
+ rowspan,
1001
+ });
1002
+ }
1003
+ }
1004
+
1005
+ this.fixTableByRemove(tableBlot);
1006
+ }
1007
+
1008
+ removeCol() {
1009
+ if (!this.tableSelection) return;
1010
+ const selectedTds = this.tableSelection.selectedTds;
1011
+ if (selectedTds.length <= 0) return;
1012
+ const baseTd = selectedTds[0];
1013
+ const tableBlot = findParentBlot(baseTd, blotName.tableMain);
1014
+ const colspanMap: Record<string, number> = {};
1015
+ for (const td of selectedTds) {
1016
+ if (!colspanMap[td.rowId]) colspanMap[td.rowId] = 0;
1017
+ colspanMap[td.rowId] += td.colspan;
1018
+ }
1019
+ const colspanCount = Math.max(...Object.values(colspanMap));
1020
+ const columnIndex = baseTd.getColumnIndex();
1021
+
1022
+ const trs = tableBlot.descendants(TableRowFormat);
1023
+ for (let i = 0; i < colspanCount; i++) {
1024
+ const spanCols: number[] = [];
1025
+ let skipRowNum = 0;
1026
+ for (const tr of Object.values(trs)) {
1027
+ const spanCol = spanCols.shift() || 0;
1028
+ if (skipRowNum > 0) {
1029
+ skipRowNum -= 1;
1030
+ continue;
1031
+ }
1032
+ const nextSpanCols = tr.removeCell(columnIndex - spanCol);
1033
+ if (nextSpanCols.skipRowNum) {
1034
+ skipRowNum += nextSpanCols.skipRowNum;
1035
+ }
1036
+ for (const [i, n] of nextSpanCols.entries()) {
1037
+ spanCols[i] = (spanCols[i] || 0) + n;
1038
+ }
1039
+ }
1040
+ }
1041
+ // delete col need after remove cell. remove cell need all column id
1042
+ // manual delete col. use fixTableByRemove to delete col will delete extra cells
1043
+ const [colgroup] = tableBlot.descendants(TableColgroupFormat);
1044
+ if (colgroup) {
1045
+ for (let i = 0; i < colspanCount; i++) {
1046
+ colgroup.removeColByIndex(columnIndex);
1047
+ }
1048
+ }
1049
+
1050
+ this.fixTableByRemove(tableBlot);
1051
+ }
1052
+
1053
+ mergeCells() {
1054
+ if (!this.tableSelection) return;
1055
+ const selectedTds = this.tableSelection.selectedTds;
1056
+ if (selectedTds.length <= 1) return;
1057
+ const counts = selectedTds.reduce(
1058
+ (pre, selectTd, index) => {
1059
+ // count column span
1060
+ const colId = selectTd.colId;
1061
+ if (!pre[0][colId]) pre[0][colId] = 0;
1062
+ pre[0][colId] += selectTd.rowspan;
1063
+ // count row span
1064
+ const rowId = selectTd.rowId;
1065
+ if (!pre[1][rowId]) pre[1][rowId] = 0;
1066
+ pre[1][rowId] += selectTd.colspan;
1067
+ // merge select cell
1068
+ if (index !== 0) {
1069
+ selectTd.moveChildren(pre[2]);
1070
+ selectTd.parent.remove();
1071
+ }
1072
+ return pre;
1073
+ },
1074
+ [{} as Record<string, number>, {} as Record<string, number>, selectedTds[0]] as const,
1075
+ );
1076
+
1077
+ const rowCount = Math.max(...Object.values(counts[0]));
1078
+ const colCount = Math.max(...Object.values(counts[1]));
1079
+ const baseTd = counts[2];
1080
+ baseTd.colspan = colCount;
1081
+ baseTd.rowspan = rowCount;
1082
+
1083
+ const tableBlot = findParentBlot(baseTd, blotName.tableMain);
1084
+ this.fixTableByRemove(tableBlot);
1085
+ }
1086
+
1087
+ splitCell() {
1088
+ if (!this.tableSelection) return;
1089
+ const selectedTds = this.tableSelection.selectedTds;
1090
+ if (selectedTds.length !== 1) return;
1091
+ const baseTd = selectedTds[0];
1092
+ if (baseTd.colspan === 1 && baseTd.rowspan === 1) return;
1093
+ const [tableBlot, baseTr] = findParentBlots(baseTd, [blotName.tableMain, blotName.tableRow] as const);
1094
+ const tableId = tableBlot.tableId;
1095
+ const colIndex = baseTd.getColumnIndex();
1096
+ const colIds = tableBlot.getColIds().slice(colIndex, colIndex + baseTd.colspan).reverse();
1097
+
1098
+ let curTr = baseTr;
1099
+ let rowspan = baseTd.rowspan;
1100
+ // reset span first. insertCell need colspan to judge insert position
1101
+ baseTd.colspan = 1;
1102
+ baseTd.rowspan = 1;
1103
+ while (curTr && rowspan > 0) {
1104
+ for (const id of colIds) {
1105
+ // keep baseTd. baseTr should insert at baseTd's column index + 1
1106
+ if (curTr === baseTr && id === baseTd.colId) continue;
1107
+ curTr.insertCell(colIndex + (curTr === baseTr ? 1 : 0), {
1108
+ tableId,
1109
+ rowId: curTr.rowId,
1110
+ colId: id,
1111
+ rowspan: 1,
1112
+ colspan: 1,
1113
+ });
1114
+ }
1115
+
1116
+ rowspan -= 1;
1117
+ curTr = curTr.next as TableRowFormat;
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ export function updateTableConstants(data: Partial<TableConstantsData>) {
1123
+ tableCantInsert.delete(blotName.tableCellInner);
1124
+
1125
+ Object.assign(blotName, data.blotName || {});
1126
+ Object.assign(tableUpSize, data.tableUpSize || {});
1127
+ Object.assign(tableUpEvent, data.tableUpEvent || {});
1128
+
1129
+ TableUp.toolName = blotName.tableWrapper;
1130
+ ContainerFormat.blotName = blotName.container;
1131
+ TableWrapperFormat.blotName = blotName.tableWrapper;
1132
+ TableMainFormat.blotName = blotName.tableMain;
1133
+ TableColgroupFormat.blotName = blotName.tableColgroup;
1134
+ TableColFormat.blotName = blotName.tableCol;
1135
+ TableBodyFormat.blotName = blotName.tableBody;
1136
+ TableRowFormat.blotName = blotName.tableRow;
1137
+ TableCellFormat.blotName = blotName.tableCell;
1138
+ TableCellInnerFormat.blotName = blotName.tableCellInner;
1139
+ };
1140
+ export function defaultCustomSelect(tableModule: TableUp, picker: QuillThemePicker) {
1141
+ return createSelectBox({
1142
+ onSelect: (row: number, col: number) => {
1143
+ tableModule.insertTable(row, col);
1144
+ if (picker) {
1145
+ picker.close();
1146
+ }
1147
+ },
1148
+ customBtn: tableModule.options.customBtn,
1149
+ texts: tableModule.options.texts,
1150
+ });
1151
+ }
1152
+
1153
+ export default TableUp;
1154
+ export * from './formats';
1155
+ export * from './modules';
1156
+ export { blotName, findParentBlot, findParentBlots, randomId, tableUpEvent, tableUpSize } from './utils';
1157
+ export * from './utils/types';