quill-table-up 3.1.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +7 -0
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.ts +168 -146
  4. package/dist/index.js +47 -47
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.umd.js +52 -52
  7. package/dist/index.umd.js.map +1 -1
  8. package/package.json +22 -24
  9. package/src/__tests__/e2e/custom-creator.test.ts +44 -44
  10. package/src/__tests__/e2e/editor-page.ts +77 -77
  11. package/src/__tests__/e2e/table-align.test.ts +104 -104
  12. package/src/__tests__/e2e/table-blots.test.ts +169 -169
  13. package/src/__tests__/e2e/table-caption.test.ts +134 -134
  14. package/src/__tests__/e2e/table-clipboard.test.ts +20 -20
  15. package/src/__tests__/e2e/table-hack.test.ts +151 -151
  16. package/src/__tests__/e2e/table-keyboard-handler.test.ts +12 -3
  17. package/src/__tests__/e2e/table-menu.test.ts +172 -172
  18. package/src/__tests__/e2e/table-resize.test.ts +654 -9
  19. package/src/__tests__/e2e/table-scrollbar.test.ts +144 -144
  20. package/src/__tests__/e2e/table-selection.test.ts +563 -563
  21. package/src/__tests__/e2e/types.d.ts +7 -7
  22. package/src/__tests__/e2e/utils.ts +52 -52
  23. package/src/__tests__/unit/table-blots.test.ts +720 -720
  24. package/src/__tests__/unit/table-caption.test.ts +234 -234
  25. package/src/__tests__/unit/table-cell-merge.test.ts +724 -724
  26. package/src/__tests__/unit/table-clipboard.test.ts +2176 -2176
  27. package/src/__tests__/unit/table-hack.test.ts +1014 -1014
  28. package/src/__tests__/unit/table-insert.test.ts +926 -926
  29. package/src/__tests__/unit/table-redo-undo.test.ts +2429 -2429
  30. package/src/__tests__/unit/table-remove.test.ts +343 -343
  31. package/src/__tests__/unit/utils.test-d.ts +49 -49
  32. package/src/__tests__/unit/utils.test.ts +711 -711
  33. package/src/__tests__/unit/utils.ts +307 -307
  34. package/src/__tests__/unit/vitest.d.ts +14 -14
  35. package/src/formats/container-format.ts +107 -107
  36. package/src/formats/overrides/block-embed.ts +72 -72
  37. package/src/formats/overrides/block.ts +95 -95
  38. package/src/formats/overrides/index.ts +3 -3
  39. package/src/formats/overrides/scroll.ts +70 -70
  40. package/src/formats/table-body-format.ts +52 -52
  41. package/src/formats/table-caption-format.ts +116 -116
  42. package/src/formats/table-cell-format.ts +304 -304
  43. package/src/formats/table-cell-inner-format.ts +403 -398
  44. package/src/formats/table-colgroup-format.ts +136 -136
  45. package/src/formats/table-foot-format.ts +7 -7
  46. package/src/formats/table-head-format.ts +7 -7
  47. package/src/formats/table-main-format.ts +1 -1
  48. package/src/formats/table-row-format.ts +218 -210
  49. package/src/formats/utils.ts +6 -6
  50. package/src/index.ts +19 -19
  51. package/src/modules/index.ts +7 -7
  52. package/src/modules/table-align.ts +131 -131
  53. package/src/modules/table-clipboard/table-clipboard.ts +6 -8
  54. package/src/modules/table-dom-selector.ts +33 -33
  55. package/src/modules/table-menu/constants.ts +223 -223
  56. package/src/modules/table-menu/index.ts +4 -4
  57. package/src/modules/table-menu/table-menu-common.ts +330 -329
  58. package/src/modules/table-menu/table-menu-contextmenu.ts +111 -118
  59. package/src/modules/table-menu/table-menu-select.ts +96 -94
  60. package/src/modules/table-resize/index.ts +5 -5
  61. package/src/modules/table-resize/table-resize-box.ts +714 -363
  62. package/src/modules/table-resize/table-resize-common.ts +246 -382
  63. package/src/modules/table-resize/table-resize-drag.ts +241 -0
  64. package/src/modules/table-resize/table-resize-line.ts +244 -182
  65. package/src/modules/table-resize/table-resize-scale.ts +174 -173
  66. package/src/modules/table-resize/utils.ts +84 -3
  67. package/src/modules/table-scrollbar.ts +292 -292
  68. package/src/modules/table-selection.ts +613 -669
  69. package/src/style/button.less +45 -45
  70. package/src/style/color-picker.less +136 -136
  71. package/src/style/dialog.less +53 -53
  72. package/src/style/functions.less +9 -9
  73. package/src/style/index.less +120 -120
  74. package/src/style/input.less +64 -64
  75. package/src/style/select-box.less +52 -52
  76. package/src/style/table-creator.less +56 -56
  77. package/src/style/table-menu.less +125 -125
  78. package/src/style/table-resize-scale.less +31 -31
  79. package/src/style/table-resize.less +249 -202
  80. package/src/style/table-scrollbar.less +49 -49
  81. package/src/style/table-selection.less +23 -23
  82. package/src/style/tooltip.less +19 -19
  83. package/src/style/variables.less +1 -1
  84. package/src/svg/arrow-up-down.svg +11 -11
  85. package/src/svg/convert-cell.svg +7 -7
  86. package/src/table-up.ts +1360 -1360
  87. package/src/types.d.ts +4 -4
  88. package/src/utils/bem.ts +23 -23
  89. package/src/utils/blot-helper.ts +101 -105
  90. package/src/utils/color.ts +109 -109
  91. package/src/utils/components/button.ts +22 -22
  92. package/src/utils/components/color-picker.ts +236 -236
  93. package/src/utils/components/dialog.ts +83 -41
  94. package/src/utils/components/index.ts +6 -6
  95. package/src/utils/components/input.ts +74 -74
  96. package/src/utils/components/table/creator.ts +89 -89
  97. package/src/utils/components/table/index.ts +2 -2
  98. package/src/utils/components/table/select-box.ts +78 -78
  99. package/src/utils/components/tooltip.ts +179 -189
  100. package/src/utils/constants.ts +125 -124
  101. package/src/utils/drag-helper.ts +112 -0
  102. package/src/utils/index.ts +15 -14
  103. package/src/utils/is.ts +9 -9
  104. package/src/utils/position.ts +60 -60
  105. package/src/utils/resize-observer-helper.ts +47 -47
  106. package/src/utils/scroll.ts +145 -47
  107. package/src/utils/style-helper.ts +47 -47
  108. package/src/utils/transformer.ts +10 -10
  109. package/src/utils/transition-event-helper.ts +8 -8
  110. package/src/utils/types.ts +156 -157
  111. package/src/utils/utils.ts +12 -12
package/src/table-up.ts CHANGED
@@ -1,1360 +1,1360 @@
1
- import type { EmitterSource, Op, Parchment as TypeParchment, Range as TypeRange } from 'quill';
2
- import type { BlockEmbed as TypeBlockEmbed } from 'quill/blots/block';
3
- import type TypeBlock from 'quill/blots/block';
4
- import type { Context } from 'quill/modules/keyboard';
5
- import type TypeKeyboard from 'quill/modules/keyboard';
6
- import type TypeToolbar from 'quill/modules/toolbar';
7
- import type { TableSelection } from './modules';
8
- import type { Constructor, QuillTheme, QuillThemePicker, TableBodyTag, TableCellValue, TableConstantsData, TableTextOptions, TableUpOptions } from './utils';
9
- import Quill from 'quill';
10
- import { BlockEmbedOverride, BlockOverride, ContainerFormat, ScrollOverride, TableBodyFormat, TableCaptionFormat, TableCellFormat, TableCellInnerFormat, TableColFormat, TableColgroupFormat, TableFootFormat, TableHeadFormat, TableMainFormat, TableRowFormat, TableWrapperFormat } from './formats';
11
- import { TableClipboard } from './modules';
12
- import { blotName, createBEM, createSelectBox, cssTextToObject, debounce, findParentBlot, findParentBlots, getScrollBarWidth, isForbidInTable, isFunction, isNumber, isString, isSubclassOf, isUndefined, limitDomInViewPort, mixinClass, objectToCssText, randomId, tableCantInsert, tableUpEvent, tableUpInternal, tableUpSize, toCamelCase } from './utils';
13
-
14
- const Parchment = Quill.import('parchment');
15
- const Delta = Quill.import('delta');
16
- const icons = Quill.import('ui/icons') as Record<string, any>;
17
- const Break = Quill.import('blots/break') as TypeParchment.BlotConstructor;
18
- const Block = Quill.import('blots/block') as typeof TypeBlock;
19
- const BlockEmbed = Quill.import('blots/block/embed') as typeof TypeBlockEmbed;
20
-
21
- function createCell(scroll: TypeParchment.ScrollBlot, { tableId, rowId, colId }: { tableId: string; rowId: string; colId: string }) {
22
- const value = {
23
- tableId,
24
- rowId,
25
- colId,
26
- colspan: 1,
27
- rowspan: 1,
28
- };
29
- const tableCell = scroll.create(blotName.tableCell, value) as TypeParchment.ParentBlot;
30
- const tableCellInner = scroll.create(blotName.tableCellInner, value) as TypeParchment.ParentBlot;
31
- const block = scroll.create('block') as TypeParchment.ParentBlot;
32
- block.appendChild(scroll.create('break'));
33
- tableCellInner.appendChild(block);
34
- tableCell.appendChild(tableCellInner);
35
- return tableCell;
36
- }
37
- export function updateTableConstants(data: Partial<TableConstantsData>) {
38
- tableCantInsert.delete(blotName.tableCellInner);
39
-
40
- Object.assign(blotName, data.blotName || {});
41
- Object.assign(tableUpSize, data.tableUpSize || {});
42
- Object.assign(tableUpEvent, data.tableUpEvent || {});
43
- Object.assign(tableUpInternal, data.tableUpInternal || {});
44
-
45
- TableUp.moduleName = tableUpInternal.moduleName;
46
-
47
- TableUp.toolName = blotName.tableWrapper;
48
- ContainerFormat.blotName = blotName.container;
49
- TableCaptionFormat.blotName = blotName.tableCaption;
50
- TableWrapperFormat.blotName = blotName.tableWrapper;
51
- TableMainFormat.blotName = blotName.tableMain;
52
- TableColgroupFormat.blotName = blotName.tableColgroup;
53
- TableColFormat.blotName = blotName.tableCol;
54
- TableHeadFormat.blotName = blotName.tableHead;
55
- TableBodyFormat.blotName = blotName.tableBody;
56
- TableFootFormat.blotName = blotName.tableFoot;
57
- TableRowFormat.blotName = blotName.tableRow;
58
- TableCellFormat.blotName = blotName.tableCell;
59
- TableCellInnerFormat.blotName = blotName.tableCellInner;
60
-
61
- tableCantInsert.add(blotName.tableCellInner);
62
- }
63
- export function defaultCustomSelect(tableModule: TableUp, picker: QuillThemePicker) {
64
- return createSelectBox({
65
- onSelect: (row: number, col: number) => {
66
- tableModule.insertTable(row, col, Quill.sources.USER);
67
- if (picker) {
68
- picker.close();
69
- }
70
- },
71
- customBtn: tableModule.options.customBtn,
72
- texts: tableModule.options.texts,
73
- });
74
- }
75
-
76
- function generateTableArrowHandler(up: boolean) {
77
- return {
78
- bindInHead: false,
79
- key: up ? 'ArrowUp' : 'ArrowDown',
80
- collapsed: true,
81
- format: [blotName.tableCellInner],
82
- handler(this: { quill: Quill }, range: TypeRange, context: Context) {
83
- const direction = up ? 'prev' : 'next';
84
- const childDirection = up ? 'tail' : 'head';
85
- if (context.line[direction]) return true;
86
-
87
- // TODO: if there have a very long text in cell, the line will auto wrap
88
- // there is no good way to find the correct index of the last `line`
89
- const cursorRect = this.quill.selection.getBounds(range.index);
90
- const lineRect = context.line.domNode.getBoundingClientRect();
91
- if (!cursorRect || !lineRect) return true;
92
- if (up) {
93
- if (cursorRect.top - lineRect.top > 3) {
94
- return true;
95
- }
96
- }
97
- else {
98
- if (lineRect.bottom - cursorRect.bottom > 3) {
99
- return true;
100
- }
101
- }
102
-
103
- let tableBlot: TableWrapperFormat;
104
- let tableMain: TableMainFormat;
105
- let tableRow: TableRowFormat;
106
- let tableCell: TableCellFormat;
107
- try {
108
- [tableBlot, tableMain, tableRow, tableCell] = findParentBlots(context.line, [blotName.tableWrapper, blotName.tableMain, blotName.tableRow, blotName.tableCell] as const);
109
- }
110
- catch {
111
- return true;
112
- }
113
-
114
- const colIds = tableMain.getColIds();
115
- const tableCaption = tableBlot.descendants(TableCaptionFormat, 0)[0];
116
-
117
- let aroundLine;
118
- if (tableCaption) {
119
- const captionSide = window.getComputedStyle(tableCaption.domNode);
120
- if (direction === 'next' && captionSide.captionSide === 'bottom') {
121
- aroundLine = tableCaption;
122
- }
123
- else if (direction === 'next') {
124
- aroundLine = tableBlot.next;
125
- }
126
- else {
127
- aroundLine = tableCaption;
128
- }
129
- }
130
- else {
131
- aroundLine = tableBlot[direction];
132
- }
133
- if (context.line[direction] || !aroundLine) return true;
134
-
135
- const targetRow = tableRow[direction] as TableRowFormat;
136
- if (targetRow) {
137
- const cellIndex = colIds.indexOf(tableCell.colId);
138
- const targetCell = targetRow.getCellByColId(colIds[cellIndex], direction);
139
- if (!targetCell) return true;
140
- let targetChild = targetCell.children[childDirection] as TypeParchment.ParentBlot;
141
- if (targetChild.children) {
142
- targetChild = targetChild.children[childDirection] as TypeParchment.ParentBlot;
143
- }
144
- const index = targetChild.offset(this.quill.scroll) + Math.min(context.offset, targetChild.length() - 1);
145
- this.quill.setSelection(index, 0, Quill.sources.USER);
146
- }
147
- else {
148
- const index = aroundLine.offset(this.quill.scroll) + (up ? aroundLine.length() - 1 : 0);
149
- this.quill.setSelection(index, 0, Quill.sources.USER);
150
- }
151
- return false;
152
- },
153
- };
154
- }
155
-
156
- export class TableUp {
157
- static moduleName: string = tableUpInternal.moduleName;
158
- static toolName: string = blotName.tableWrapper;
159
- // TODO: add custom property `bindInHead`, but Quill doesn't export `BindingObject`
160
- static keyboradHandler = {
161
- 'forbid remove table by backspace': {
162
- bindInHead: true,
163
- key: 'Backspace',
164
- collapsed: true,
165
- offset: 0,
166
- handler(this: { quill: Quill }, range: TypeRange, context: Context) {
167
- const line = this.quill.getLine(range.index);
168
- const blot = line[0] as TypeParchment.BlockBlot;
169
- if (blot.prev instanceof TableWrapperFormat) {
170
- blot.prev.remove();
171
- return false;
172
- }
173
-
174
- if (context.format[blotName.tableCellInner]) {
175
- const offset = blot.offset(findParentBlot(blot, blotName.tableCellInner));
176
- if (offset === 0) {
177
- return false;
178
- }
179
- }
180
-
181
- return true;
182
- },
183
- },
184
- 'forbid remove table by delete': {
185
- bindInHead: true,
186
- key: 'Delete',
187
- collapsed: true,
188
- handler(this: { quill: Quill }, range: TypeRange, context: Context) {
189
- const line = this.quill.getLine(range.index);
190
- const blot = line[0] as TypeParchment.BlockBlot;
191
- const offsetInline = line[1];
192
- if ((blot.next instanceof TableWrapperFormat || blot.next instanceof TableColFormat) && offsetInline === blot.length() - 1) return false;
193
-
194
- if (context.format[blotName.tableCellInner]) {
195
- const tableInnerBlot = findParentBlot(blot, blotName.tableCellInner);
196
- if (blot === tableInnerBlot.children.tail && offsetInline === blot.length() - 1) {
197
- return false;
198
- }
199
- }
200
- return true;
201
- },
202
- },
203
- 'table up': generateTableArrowHandler(true),
204
- 'table down': generateTableArrowHandler(false),
205
- 'table caption break': {
206
- bindInHead: true,
207
- key: 'Enter',
208
- shiftKey: null,
209
- format: [blotName.tableCaption],
210
- handler(this: { quill: Quill }, _range: TypeRange, _context: Context) {
211
- return false;
212
- },
213
- },
214
- };
215
-
216
- static register() {
217
- TableWrapperFormat.allowedChildren = [TableMainFormat];
218
-
219
- TableMainFormat.allowedChildren = [TableColgroupFormat, TableCaptionFormat, TableHeadFormat, TableBodyFormat, TableFootFormat];
220
- TableMainFormat.requiredContainer = TableWrapperFormat;
221
-
222
- TableCaptionFormat.requiredContainer = TableMainFormat;
223
-
224
- TableColgroupFormat.allowedChildren = [TableColFormat];
225
- TableColgroupFormat.requiredContainer = TableMainFormat;
226
-
227
- TableHeadFormat.allowedChildren = [TableRowFormat];
228
- TableHeadFormat.requiredContainer = TableMainFormat;
229
- TableBodyFormat.allowedChildren = [TableRowFormat];
230
- TableBodyFormat.requiredContainer = TableMainFormat;
231
- TableFootFormat.allowedChildren = [TableRowFormat];
232
- TableFootFormat.requiredContainer = TableMainFormat;
233
-
234
- TableRowFormat.allowedChildren = [TableCellFormat];
235
-
236
- TableCellFormat.allowedChildren = [TableCellInnerFormat, Break];
237
- TableCellFormat.requiredContainer = TableRowFormat;
238
-
239
- TableCellInnerFormat.requiredContainer = TableCellFormat;
240
-
241
- // override Block and BlockEmbed
242
- const excludeFormat = new Set(['table']);
243
- const overrideFormats = Object.entries(Quill.imports as Record<string, Constructor>).filter(([name, blot]) => {
244
- const blotName = name.split('formats/')[1];
245
- return name.startsWith('formats/')
246
- && !excludeFormat.has(blotName)
247
- && (isSubclassOf(blot, Block) || isSubclassOf(blot, BlockEmbed));
248
- });
249
- const overrides = overrideFormats.reduce((pre, [name, blot]) => {
250
- const extendsClass = isSubclassOf(blot, BlockEmbed) ? BlockEmbedOverride : BlockOverride;
251
- pre[name] = class extends mixinClass(blot, [extendsClass]) {
252
- static register() {}
253
- };
254
- return pre;
255
- }, {} as Record<string, Constructor>);
256
-
257
- Quill.register({
258
- 'blots/scroll': ScrollOverride,
259
- 'blots/block': BlockOverride,
260
- 'blots/block/embed': BlockEmbedOverride,
261
- ...overrides,
262
- [`blots/${blotName.container}`]: ContainerFormat,
263
- [`formats/${blotName.tableCell}`]: TableCellFormat,
264
- [`formats/${blotName.tableCellInner}`]: TableCellInnerFormat,
265
- [`formats/${blotName.tableRow}`]: TableRowFormat,
266
- [`formats/${blotName.tableHead}`]: TableHeadFormat,
267
- [`formats/${blotName.tableBody}`]: TableBodyFormat,
268
- [`formats/${blotName.tableFoot}`]: TableFootFormat,
269
- [`formats/${blotName.tableCol}`]: TableColFormat,
270
- [`formats/${blotName.tableColgroup}`]: TableColgroupFormat,
271
- [`formats/${blotName.tableCaption}`]: TableCaptionFormat,
272
- [`formats/${blotName.tableMain}`]: TableMainFormat,
273
- [`formats/${blotName.tableWrapper}`]: TableWrapperFormat,
274
- 'modules/clipboard': TableClipboard,
275
- }, true);
276
- }
277
-
278
- quill: Quill;
279
- options: TableUpOptions;
280
- toolBox: HTMLDivElement;
281
- fixTableByLisenter = debounce(this.balanceTables, 100);
282
- selector?: HTMLElement;
283
- resizeOb!: ResizeObserver;
284
- modules: Record<string, Constructor> = {};
285
-
286
- get statics(): any {
287
- return this.constructor;
288
- }
289
-
290
- constructor(quill: Quill, options: Partial<TableUpOptions>) {
291
- this.quill = quill;
292
- this.options = this.resolveOptions(options || {});
293
- this.toolBox = this.initialContainer();
294
-
295
- const toolbar = this.quill.getModule('toolbar') as TypeToolbar;
296
- if (toolbar && (this.quill.theme as QuillTheme).pickers) {
297
- const [, select] = (toolbar.controls as [string, HTMLElement][] || []).find(([name]) => name === this.statics.toolName) || [];
298
- if (select && select.tagName.toLocaleLowerCase() === 'select') {
299
- const picker = (this.quill.theme as QuillTheme).pickers.find(picker => picker.select === select);
300
- if (picker) {
301
- picker.label.innerHTML = this.options.icon;
302
- this.buildCustomSelect(this.options.customSelect, picker);
303
- picker.label.addEventListener('mousedown', () => {
304
- if (!this.selector || !picker) return;
305
- const selectRect = this.selector.getBoundingClientRect();
306
- const { leftLimited } = limitDomInViewPort(selectRect);
307
- if (leftLimited) {
308
- const labelRect = picker.label.getBoundingClientRect();
309
- Object.assign(picker.options.style, { transform: `translateX(calc(-100% + ${labelRect.width}px))` });
310
- }
311
- else {
312
- Object.assign(picker.options.style, { transform: undefined });
313
- }
314
- });
315
- }
316
- }
317
- }
318
-
319
- const keyboard = this.quill.getModule('keyboard') as TypeKeyboard;
320
- for (const handle of Object.values(TableUp.keyboradHandler)) {
321
- // insert before default key handler
322
- if (handle.bindInHead) {
323
- keyboard.bindings[handle.key].unshift(handle);
324
- }
325
- else {
326
- keyboard.addBinding(handle.key, handle);
327
- }
328
- }
329
-
330
- this.initModules();
331
- this.quillHack();
332
- this.listenBalanceCells();
333
- }
334
-
335
- initialContainer() {
336
- const toolboxBEM = createBEM('toolbox');
337
- const container = this.quill.addContainer(toolboxBEM.b());
338
- const updateContainerStyle = () => {
339
- const quillRootRect = this.quill.root.getBoundingClientRect();
340
- const { offsetLeft, offsetTop } = this.quill.root;
341
- Object.assign(container.style, {
342
- top: `${offsetTop}px`,
343
- left: `${offsetLeft}px`,
344
- width: `${quillRootRect.width}px`,
345
- height: `${quillRootRect.height}px`,
346
- });
347
- };
348
- this.resizeOb = new ResizeObserver(updateContainerStyle);
349
- this.resizeOb.observe(this.quill.root);
350
- return container;
351
- }
352
-
353
- addContainer(classes: string | HTMLElement) {
354
- if (isString(classes)) {
355
- const el = document.createElement('div');
356
- for (const classname of classes.split(' ')) {
357
- el.classList.add(classname);
358
- }
359
- this.toolBox.appendChild(el);
360
- return el;
361
- }
362
- else {
363
- this.toolBox.appendChild(classes);
364
- return classes;
365
- }
366
- }
367
-
368
- resolveOptions(options: Partial<TableUpOptions>): TableUpOptions {
369
- return Object.assign({
370
- customBtn: false,
371
- texts: this.resolveTexts(options.texts || {}),
372
- full: false,
373
- fullSwitch: true,
374
- icon: icons.table,
375
- autoMergeCell: true,
376
- modules: [],
377
- } as TableUpOptions, options);
378
- }
379
-
380
- resolveTexts(options: Partial<TableTextOptions>) {
381
- return Object.assign({
382
- fullCheckboxText: 'Insert full width table',
383
- customBtnText: 'Custom',
384
- confirmText: 'Confirm',
385
- cancelText: 'Cancel',
386
- rowText: 'Row',
387
- colText: 'Column',
388
- notPositiveNumberError: 'Please enter a positive integer',
389
- custom: 'Custom',
390
- clear: 'Clear',
391
- transparent: 'Transparent',
392
- 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?',
393
- CopyCell: 'Copy cell',
394
- CutCell: 'Cut cell',
395
- InsertTop: 'Insert row above',
396
- InsertRight: 'Insert column right',
397
- InsertBottom: 'Insert row below',
398
- InsertLeft: 'Insert column Left',
399
- MergeCell: 'Merge Cell',
400
- SplitCell: 'Split Cell',
401
- DeleteRow: 'Delete Row',
402
- DeleteColumn: 'Delete Column',
403
- DeleteTable: 'Delete table',
404
- BackgroundColor: 'Set background color',
405
- BorderColor: 'Set border color',
406
- }, options);
407
- }
408
-
409
- initModules() {
410
- for (const item of this.options.modules) {
411
- this.modules[item.module.moduleName] = new item.module(this, this.quill, item.options);
412
- }
413
- }
414
-
415
- getModule<T>(name: string) {
416
- return this.modules[name] as T | undefined;
417
- }
418
-
419
- quillHack() {
420
- const originGetSemanticHTML = this.quill.getSemanticHTML;
421
- this.quill.getSemanticHTML = ((index: number = 0, length?: number) => {
422
- const html = originGetSemanticHTML.call(this.quill, index, length);
423
-
424
- const tableWrapperFormat = Quill.import(`formats/${blotName.tableWrapper}`) as typeof TableWrapperFormat;
425
- const parser = new DOMParser();
426
- const doc = parser.parseFromString(html, 'text/html');
427
- for (const node of Array.from(doc.querySelectorAll(`.${tableWrapperFormat.className} caption[contenteditable], .${tableWrapperFormat.className} .${TableCellFormat.className} > [contenteditable]`))) {
428
- node.removeAttribute('contenteditable');
429
- }
430
-
431
- return doc.body.innerHTML;
432
- }) as typeof originGetSemanticHTML;
433
-
434
- // make sure toolbar item can format selected cells
435
- const originFormat = this.quill.format;
436
- this.quill.format = function (name: string, value: unknown, source: EmitterSource = Quill.sources.API) {
437
- const blot = this.scroll.query(name);
438
- // filter embed blot
439
- if (!((blot as TypeParchment.BlotConstructor).prototype instanceof Parchment.EmbedBlot)) {
440
- const tableUpModule = this.getModule(tableUpInternal.moduleName) as TableUp;
441
- const range = this.getSelection(true);
442
- const formats = this.getFormat(range);
443
- // only when selection in cell and selectedTds > 1 can format all cells
444
- const tableSelection = tableUpModule.getModule<TableSelection>('table-selection');
445
- if (!formats[blotName.tableCellInner] || range.length > 0 || (tableUpModule && tableSelection && tableSelection.selectedTds.length <= 1)) {
446
- return originFormat.call(this, name, value, source);
447
- }
448
- // format in selected cells
449
- if (tableUpModule && tableSelection && tableSelection.selectedTds.length > 0) {
450
- const selectedTds = tableSelection.selectedTds;
451
- // calculate the format value. the format should be canceled when this value exists in all selected cells
452
- let setOrigin = false;
453
- const tdRanges = [];
454
- for (const innerTd of selectedTds) {
455
- const index = innerTd.offset(this.scroll);
456
- const length = innerTd.length();
457
- tdRanges.push({ index, length });
458
- const format = this.getFormat(index, length);
459
- if (format[name] !== value) {
460
- setOrigin = true;
461
- }
462
- }
463
- const resultValue = setOrigin ? value : false;
464
-
465
- const delta = new Delta();
466
- for (const [i, { index, length }] of tdRanges.entries()) {
467
- const lastIndex = i === 0 ? 0 : tdRanges[i - 1].index + tdRanges[i - 1].length;
468
- delta.retain(index - lastIndex).retain(length, { [name]: resultValue });
469
- }
470
-
471
- const updateDelta = this.updateContents(delta, source);
472
- this.blur();
473
- return updateDelta;
474
- }
475
- }
476
-
477
- return originFormat.call(this, name, value, source);
478
- };
479
-
480
- // handle clean
481
- const toolbar = this.quill.theme.modules.toolbar;
482
- if (toolbar) {
483
- const cleanHandler = toolbar.handlers?.clean;
484
- if (cleanHandler) {
485
- const cleanFormatExcludeTable = (index: number, length: number, changeCellStyle: false | ((styleStr: string | undefined) => string) = () => '') => {
486
- // base on `removeFormat`. but not remove tableCellInner
487
- const text = this.quill.getText(index, length);
488
- const [line, offset] = this.quill.getLine(index + length);
489
- let suffixLength = 0;
490
- let suffix = new Delta();
491
- if (line != null) {
492
- suffixLength = line.length() - offset;
493
- suffix = line.delta().slice(offset, offset + suffixLength - 1).insert('\n');
494
- }
495
- const contents = this.quill.getContents(index, length + suffixLength);
496
- const diff = contents.diff(new Delta().insert(text).concat(suffix));
497
-
498
- let deltaIndex = 0;
499
- const ops = diff.ops.map((op: Op) => {
500
- const { attributes, ...other } = op;
501
- if (op.insert) {
502
- deltaIndex -= isString(op.insert) ? op.insert.length : 1;
503
- }
504
- else if (op.retain) {
505
- deltaIndex += isNumber(op.retain) ? op.retain : 1;
506
- }
507
- else if (op.delete) {
508
- deltaIndex += op.delete;
509
- }
510
-
511
- if (attributes) {
512
- const { [blotName.tableCellInner]: nullValue, ...attrs } = attributes;
513
- if (changeCellStyle) {
514
- const tableCellInner = contents.slice(deltaIndex - 1, deltaIndex).ops[0];
515
- if (tableCellInner && tableCellInner.attributes && tableCellInner.attributes[blotName.tableCellInner]) {
516
- const tableCellInnerValue = tableCellInner.attributes[blotName.tableCellInner] as TableCellValue;
517
- const { style, ...value } = tableCellInnerValue;
518
- const newStyle = changeCellStyle(style);
519
- if (newStyle) {
520
- return { ...other, attributes: { ...attrs, [blotName.tableCellInner]: { style: newStyle, ...value } } };
521
- }
522
- return { ...other, attributes: { ...attrs, [blotName.tableCellInner]: value } };
523
- }
524
- }
525
- return { ...other, attributes: { ...attrs } };
526
- }
527
- return op;
528
- });
529
- return new Delta(ops);
530
- };
531
- toolbar.handlers!.clean = function (this: TypeToolbar, value: unknown): void {
532
- const tableUpModule = this.quill.getModule(tableUpInternal.moduleName) as TableUp;
533
- const range = this.quill.getSelection();
534
- if (range && range.length > 0) {
535
- const formats = this.quill.getFormat(range);
536
- if (formats[blotName.tableCellInner]) {
537
- const diff = cleanFormatExcludeTable(range.index, range.length, false);
538
- const delta = new Delta().retain(range.index).concat(diff);
539
- this.quill.updateContents(delta, Quill.sources.USER);
540
- return;
541
- }
542
- }
543
- // if selection range is not in table, but use the TableSelection selected cells
544
- // clean all other formats in cell
545
- const tableSelection = tableUpModule.getModule<TableSelection>('table-selection');
546
- if (tableUpModule && tableSelection && tableSelection.selectedTds.length > 0 && tableSelection.table) {
547
- const tableMain = Quill.find(tableSelection.table) as TableMainFormat;
548
- if (!tableMain) {
549
- console.warn('TableMainFormat not found');
550
- return;
551
- }
552
- const selectedTds = tableSelection.selectedTds;
553
-
554
- // get all need clean style cells. include border-right/border-bottom effect cells
555
- const editTds = new Set<TableCellFormat>();
556
- const tds: { td: TableCellFormat; cleanBorder: 'bottom' | 'right' | true }[] = [];
557
- for (const innerTd of selectedTds) {
558
- if (innerTd.parent instanceof TableCellFormat) {
559
- for (const td of innerTd.parent.getNearByCell('top')) {
560
- if (editTds.has(td)) continue;
561
- editTds.add(td);
562
- tds.push({ td, cleanBorder: 'bottom' });
563
- }
564
- for (const td of innerTd.parent.getNearByCell('left')) {
565
- if (editTds.has(td)) continue;
566
- editTds.add(td);
567
- tds.push({ td, cleanBorder: 'right' });
568
- }
569
-
570
- editTds.add(innerTd.parent);
571
- tds.push({ td: innerTd.parent, cleanBorder: true });
572
- }
573
- }
574
- // sort cells makesure index correct
575
- const allCells = tableMain.descendants(TableCellFormat);
576
- const cellIndexMap = new Map(allCells.map((cell, index) => [cell, index]));
577
- tds.sort((a, b) => cellIndexMap.get(a.td)! - cellIndexMap.get(b.td)!);
578
-
579
- // compute delta
580
- let delta = new Delta();
581
- let lastIndex = 0;
582
- for (const { td, cleanBorder } of tds) {
583
- const index = td.getCellInner().offset(this.quill.scroll);
584
- const length = td.getCellInner().length();
585
- // `line` length will include a break(\n) at the end. minus 1 to remove break
586
- const diff = cleanFormatExcludeTable(
587
- index,
588
- length - 1,
589
- (styleStr: string | undefined) => {
590
- if (!styleStr || cleanBorder === true) return '';
591
- // only clean border-right/border-bottom style
592
- const css = cssTextToObject(styleStr);
593
- const filterStyle = Object.keys(css).filter(key => !key.startsWith(toCamelCase(`border-${cleanBorder}`))).reduce((acc: Record<string, string>, key: string) => {
594
- acc[key] = css[key];
595
- return acc;
596
- }, {});
597
- return objectToCssText(filterStyle);
598
- },
599
- );
600
- const cellDiff = new Delta().retain(index - lastIndex).concat(diff);
601
- delta = delta.concat(cellDiff);
602
- lastIndex = index + length;
603
- }
604
- this.quill.updateContents(delta, Quill.sources.USER);
605
- if (selectedTds.length > 1) this.quill.blur();
606
- return;
607
- }
608
- return cleanHandler.call(this, value);
609
- };
610
- }
611
- }
612
- }
613
-
614
- async buildCustomSelect(customSelect: ((module: TableUp, picker: QuillThemePicker) => HTMLElement | Promise<HTMLElement>) | undefined, picker: QuillThemePicker) {
615
- if (!customSelect || !isFunction(customSelect)) return;
616
- const dom = document.createElement('div');
617
- dom.classList.add('ql-custom-select');
618
- this.selector = await customSelect(this, picker);
619
- dom.appendChild(this.selector);
620
- if (this.options.fullSwitch) {
621
- const bem = createBEM('creator');
622
- const isFulllLabel = document.createElement('label');
623
- isFulllLabel.classList.add(bem.be('checkbox'));
624
- const isFullCheckbox = document.createElement('input');
625
- isFullCheckbox.type = 'checkbox';
626
- isFullCheckbox.checked = this.options.full;
627
- isFullCheckbox.addEventListener('change', () => {
628
- this.options.full = isFullCheckbox.checked;
629
- });
630
- const isFullCheckboxText = document.createElement('span');
631
- isFullCheckboxText.textContent = this.options.texts.fullCheckboxText;
632
- isFulllLabel.appendChild(isFullCheckbox);
633
- isFulllLabel.appendChild(isFullCheckboxText);
634
- dom.appendChild(isFulllLabel);
635
- }
636
- picker.options.innerHTML = '';
637
- picker.options.appendChild(dom);
638
- }
639
-
640
- setCellAttrs(selectedTds: TableCellInnerFormat[], attr: string, value?: any, isStyle: boolean = false) {
641
- if (selectedTds.length === 0) return;
642
- for (const td of selectedTds) {
643
- td.setFormatValue(attr, value, isStyle);
644
- }
645
- }
646
-
647
- getTextByCell(tds: TableCellInnerFormat[]) {
648
- let text = '';
649
- for (const td of tds) {
650
- const index = td.offset(this.quill.scroll);
651
- const length = td.length();
652
- for (const op of this.quill.getContents(index, length).ops) {
653
- if (isString(op.insert)) {
654
- text += op.insert;
655
- }
656
- }
657
- }
658
- return text;
659
- }
660
-
661
- getHTMLByCell(tds: TableCellInnerFormat[], isCut = false) {
662
- if (tds.length === 0) return '';
663
- let tableMain: TableMainFormat | null = null;
664
- try {
665
- for (const td of tds) {
666
- const tdParentMain = findParentBlot(td, blotName.tableMain);
667
- if (!tableMain) {
668
- tableMain = tdParentMain;
669
- }
670
- if (tdParentMain !== tableMain) {
671
- console.error('tableMain is not same');
672
- return '';
673
- }
674
- }
675
- }
676
- catch {
677
- console.error('tds must be in same tableMain');
678
- return '';
679
- }
680
-
681
- if (!tableMain) return '';
682
- const tableIndex = this.quill.getIndex(tableMain);
683
- const tableLength = tableMain.length();
684
- const tableHTML = this.quill.getSemanticHTML(tableIndex, tableLength);
685
- const parser = new DOMParser();
686
- const doc = parser.parseFromString(tableHTML, 'text/html');
687
-
688
- const cols = Array.from(doc.querySelectorAll('col'));
689
- const colIds = cols.map(col => col.dataset.colId!);
690
- const cellColWidth: string[] = [];
691
- const cellColIds = new Set<string>();
692
- const cellIds = new Set<string>();
693
- for (const td of tds) {
694
- cellColIds.add(td.colId);
695
- const currentColId = td.colId;
696
- const colIndex = colIds.indexOf(currentColId);
697
- for (let i = 0; i < td.colspan; i++) {
698
- cellColIds.add(colIds[colIndex + i]);
699
- }
700
- cellIds.add(`${td.rowId}-${td.colId}`);
701
- }
702
- // filter col
703
- for (let index = 0; index < cols.length; index++) {
704
- const col = cols[index];
705
- if (!cellColIds.has(col.dataset.colId!)) {
706
- col.remove();
707
- cols.splice(index--, 1);
708
- }
709
- else {
710
- cellColWidth.push(col.getAttribute('width')!);
711
- }
712
- }
713
- // filter td
714
- let rowCount = 0;
715
- let lastRowId: string | null = null;
716
- for (const td of Array.from(doc.querySelectorAll('td, th')) as HTMLElement[]) {
717
- if (!cellIds.has(`${td.dataset.rowId}-${td.dataset.colId}`)) {
718
- const parent = td.parentElement;
719
- td.remove();
720
- if (parent && parent.children.length <= 0) {
721
- parent.remove();
722
- }
723
- }
724
- else {
725
- if (lastRowId !== td.dataset.rowId) {
726
- rowCount += 1;
727
- lastRowId = td.dataset.rowId!;
728
- }
729
- }
730
- }
731
- // calculate width
732
- const colsValue = cols.map(col => TableColFormat.value(col));
733
- if (tableMain.full) {
734
- const totalWidth = colsValue.reduce((total, col) => col.width + total, 0);
735
- for (const [i, col] of colsValue.entries()) {
736
- col.width = Math.round((col.width / totalWidth) * 100);
737
- cols[i].setAttribute('width', `${col.width}%`);
738
- }
739
- }
740
- else {
741
- let width = 0;
742
- for (const col of colsValue) {
743
- width += col.width;
744
- }
745
- const tableMainDom = doc.querySelector('table')!;
746
- tableMainDom.style.width = `${width}px`;
747
- }
748
-
749
- if (isCut) {
750
- const trs = tableMain.getRows();
751
- if (rowCount === trs.length) {
752
- this.removeCol(tds);
753
- }
754
- else {
755
- for (const td of tds) {
756
- td.domNode.innerHTML = '<p><br></p>';
757
- }
758
- }
759
- }
760
- return doc.body.innerHTML;
761
- }
762
-
763
- insertTable(rows: number, columns: number, source: EmitterSource = Quill.sources.API) {
764
- if (rows >= 30 || columns >= 30) {
765
- throw new Error('Both rows and columns must be less than 30.');
766
- }
767
-
768
- this.quill.focus();
769
- const range = this.quill.getSelection();
770
- if (range == null) return;
771
- const [currentBlot] = this.quill.getLeaf(range.index);
772
- if (!currentBlot) return;
773
- if (isForbidInTable(currentBlot)) {
774
- throw new Error(`Not supported ${currentBlot.statics.blotName} insert into table.`);
775
- }
776
-
777
- const tableId = randomId();
778
- const colIds = new Array(columns).fill(0).map(() => randomId());
779
-
780
- const borderWidth = this.calculateTableCellBorderWidth();
781
- const rootStyle = getComputedStyle(this.quill.root);
782
- const paddingLeft = Number.parseInt(rootStyle.paddingLeft);
783
- const paddingRight = Number.parseInt(rootStyle.paddingRight);
784
- const scrollBarWidth = this.quill.root.scrollHeight > this.quill.root.clientHeight ? getScrollBarWidth({ target: this.quill.root }) : 0;
785
- const width = Number.parseInt(rootStyle.width) - paddingLeft - paddingRight - borderWidth - scrollBarWidth;
786
-
787
- // insert delta data to create table
788
- const colWidth = !this.options.full ? `${Math.max(Math.floor(width / columns), tableUpSize.colMinWidthPx)}px` : `${Math.max((1 / columns) * 100, tableUpSize.colMinWidthPre)}%`;
789
- const delta: Record<string, any>[] = [{ retain: range.index }];
790
- const aroundContent = this.quill.getContents(range.index, 1);
791
- const [, offset] = this.quill.getLine(range.index);
792
- if (aroundContent.ops[0].insert !== '\n' && offset !== 0) delta.push({ insert: '\n' });
793
-
794
- for (let i = 0; i < columns; i++) {
795
- delta.push({
796
- insert: {
797
- [blotName.tableCol]: {
798
- width: colWidth,
799
- tableId,
800
- colId: colIds[i],
801
- full: this.options.full,
802
- },
803
- },
804
- });
805
- }
806
- for (let j = 0; j < rows; j++) {
807
- const rowId = randomId();
808
- for (let i = 0; i < columns; i++) {
809
- delta.push({
810
- insert: '\n',
811
- attributes: {
812
- [blotName.tableCellInner]: {
813
- tableId,
814
- rowId,
815
- colId: colIds[i],
816
- rowspan: 1,
817
- colspan: 1,
818
- },
819
- },
820
- });
821
- }
822
- }
823
-
824
- this.quill.updateContents(new Delta(delta), source);
825
- this.quill.setSelection(range.index + columns, Quill.sources.SILENT);
826
- this.quill.focus();
827
- }
828
-
829
- calculateTableCellBorderWidth() {
830
- const tableStr = `
831
- <table class="${TableMainFormat.className}">
832
- <tbody>
833
- <tr>
834
- <td class="${TableCellFormat.className}"></td>
835
- </tr>
836
- </tbody>
837
- </table>
838
- `;
839
- const div = document.createElement('div');
840
- div.className = TableWrapperFormat.className;
841
- div.innerHTML = tableStr;
842
- div.style.position = 'absolute';
843
- div.style.left = '-9999px';
844
- div.style.top = '-9999px';
845
- div.style.visibility = 'hidden';
846
- this.quill.root.appendChild(div);
847
- const tempTableStyle = window.getComputedStyle(div.querySelector('td')!);
848
- const borderWidth = Number.parseFloat(tempTableStyle.borderWidth) || 0;
849
- this.quill.root.removeChild(div);
850
- return borderWidth;
851
- }
852
-
853
- // handle unusual delete cell
854
- fixUnusuaDeletelTable(tableBlot: TableMainFormat) {
855
- const tableColIds = tableBlot.getColIds();
856
- if (tableColIds.length === 0) {
857
- tableBlot.remove();
858
- return;
859
- }
860
- const bodys = tableBlot.getBodys();
861
- const tableId = tableBlot.tableId;
862
- for (const body of bodys) {
863
- // calculate all cells in body
864
- const trBlots = body.getRows();
865
- if (trBlots.length === 0) {
866
- body.remove();
867
- continue;
868
- }
869
- // append by col
870
- const cellSpanMap = new Array(trBlots.length).fill(0).map(() => new Array(tableColIds.length).fill(false));
871
- for (const [indexTr, tr] of trBlots.entries()) {
872
- let indexTd = 0;
873
- let indexCol = 0;
874
- const curCellSpan = cellSpanMap[indexTr];
875
- const tds = tr.descendants(TableCellFormat);
876
- // loop every row and column
877
- while (indexCol < tableColIds.length) {
878
- // skip when rowspan or colspan
879
- if (curCellSpan[indexCol]) {
880
- indexCol += 1;
881
- continue;
882
- }
883
- const curTd = tds[indexTd];
884
- // if colId does not match. insert a new one
885
- if (!curTd || curTd.colId !== tableColIds[indexCol]) {
886
- tr.insertBefore(
887
- createCell(
888
- this.quill.scroll,
889
- {
890
- tableId,
891
- colId: tableColIds[indexCol],
892
- rowId: tr.rowId,
893
- },
894
- ),
895
- curTd,
896
- );
897
- }
898
- else {
899
- if (indexTr + curTd.rowspan - 1 >= trBlots.length) {
900
- curTd.getCellInner().rowspan = trBlots.length - indexTr;
901
- }
902
-
903
- const { colspan, rowspan } = curTd;
904
- // skip next column cell
905
- if (colspan > 1) {
906
- for (let c = 1; c < colspan; c++) {
907
- curCellSpan[indexCol + c] = true;
908
- }
909
- }
910
- // skip next rowspan cell
911
- if (rowspan > 1) {
912
- for (let r = indexTr + 1; r < indexTr + rowspan; r++) {
913
- for (let c = 0; c < colspan; c++) {
914
- cellSpanMap[r][indexCol + c] = true;
915
- }
916
- }
917
- }
918
- indexTd += 1;
919
- }
920
- indexCol += 1;
921
- }
922
-
923
- // if td not match all exist td. Indicates that a cell has been inserted
924
- if (indexTd < tds.length) {
925
- for (let i = indexTd; i < tds.length; i++) {
926
- tds[i].remove();
927
- }
928
- }
929
- }
930
- }
931
- }
932
-
933
- balanceTables() {
934
- for (const tableBlot of this.quill.scroll.descendants(TableMainFormat)) {
935
- tableBlot.checkEmptyCol(this.options.autoMergeCell);
936
- tableBlot.checkEmptyRow(this.options.autoMergeCell);
937
- this.fixUnusuaDeletelTable(tableBlot);
938
- }
939
- }
940
-
941
- listenBalanceCells() {
942
- this.quill.on(
943
- Quill.events.SCROLL_OPTIMIZE,
944
- (mutations: MutationRecord[]) => {
945
- mutations.some((mutation) => {
946
- // TODO: if need add ['COL', 'COLGROUP']
947
- if (['TD', 'TR', 'TBODY', 'TABLE'].includes((mutation.target as HTMLElement).tagName)) {
948
- this.fixTableByLisenter();
949
- return true;
950
- }
951
- return false;
952
- });
953
- for (const mutation of mutations) {
954
- const mutationTarget = mutation.target as HTMLElement;
955
- if (mutationTarget.tagName === 'TABLE') {
956
- const tableMain = Quill.find(mutationTarget) as TableMainFormat;
957
- if (tableMain) {
958
- tableMain.sortMergeChildren();
959
- break;
960
- }
961
- }
962
- }
963
- },
964
- );
965
- }
966
-
967
- deleteTable(selectedTds: TableCellInnerFormat[]) {
968
- if (selectedTds.length === 0) return;
969
- const tableBlot = findParentBlot(selectedTds[0], blotName.tableMain);
970
- tableBlot && tableBlot.remove();
971
- }
972
-
973
- appendRow(selectedTds: TableCellInnerFormat[], isDown: boolean) {
974
- if (selectedTds.length <= 0) return;
975
- // find baseTd and baseTr
976
- const baseTd = selectedTds[isDown ? selectedTds.length - 1 : 0];
977
- const [tableBlot, baseTdParentTr] = findParentBlots(baseTd, [blotName.tableMain, blotName.tableRow] as const);
978
- const tableTrs = tableBlot.getRows();
979
- const i = tableTrs.indexOf(baseTdParentTr);
980
- const insertRowIndex = i + (isDown ? baseTd.rowspan : 0);
981
-
982
- tableBlot.insertRow(insertRowIndex);
983
- }
984
-
985
- appendCol(selectedTds: TableCellInnerFormat[], isRight: boolean) {
986
- if (selectedTds.length <= 0) return;
987
-
988
- // find insert column index in row
989
- const [baseTd] = selectedTds.reduce((pre, cur) => {
990
- const columnIndex = cur.getColumnIndex();
991
- if (!isRight && columnIndex <= pre[1]) {
992
- pre = [cur, columnIndex];
993
- }
994
- else if (isRight && columnIndex >= pre[1]) {
995
- pre = [cur, columnIndex];
996
- }
997
- return pre;
998
- }, [selectedTds[0], selectedTds[0].getColumnIndex()]);
999
- const columnIndex = baseTd.getColumnIndex() + (isRight ? baseTd.colspan : 0);
1000
-
1001
- const tableBlot = findParentBlot(baseTd, blotName.tableMain);
1002
- const tableId = tableBlot.tableId;
1003
- const newColId = randomId();
1004
-
1005
- const [colgroup] = tableBlot.descendants(TableColgroupFormat);
1006
- if (colgroup) {
1007
- colgroup.insertColByIndex(columnIndex, {
1008
- tableId,
1009
- colId: newColId,
1010
- width: tableBlot.full ? 6 : 160,
1011
- full: tableBlot.full,
1012
- });
1013
- }
1014
-
1015
- // loop tr and insert cell at index
1016
- // if index is inner cell, skip next `rowspan` line
1017
- // if there are cells both have column span and row span before index cell, minus `colspan` cell for next line
1018
- const trs = tableBlot.getRows();
1019
- const spanCols: number[] = [];
1020
- let skipRowNum = 0;
1021
- for (const tr of Object.values(trs)) {
1022
- const spanCol = spanCols.shift() || 0;
1023
- if (skipRowNum > 0) {
1024
- skipRowNum -= 1;
1025
- continue;
1026
- }
1027
- const nextSpanCols = tr.insertCell(columnIndex - spanCol, {
1028
- tableId,
1029
- rowId: tr.rowId,
1030
- colId: newColId,
1031
- rowspan: 1,
1032
- colspan: 1,
1033
- });
1034
- if (nextSpanCols.skipRowNum) {
1035
- skipRowNum += nextSpanCols.skipRowNum;
1036
- }
1037
- for (const [i, n] of nextSpanCols.entries()) {
1038
- spanCols[i] = (spanCols[i] || 0) + n;
1039
- }
1040
- }
1041
- }
1042
-
1043
- /**
1044
- * after insert or remove cell. handle cell colspan and rowspan merge
1045
- */
1046
- fixTableByRemove(tableBlot: TableMainFormat) {
1047
- if (!this.options.autoMergeCell) return;
1048
- // calculate all cells
1049
- // maybe will get empty tr
1050
- const trBlots = tableBlot.getRows();
1051
- const tableCols = tableBlot.getCols();
1052
- const colIdMap = tableCols.reduce((idMap, col) => {
1053
- idMap[col.colId] = 0;
1054
- return idMap;
1055
- }, {} as Record<string, number>);
1056
- // merge rowspan
1057
- const reverseTrBlots = trBlots.toReversed();
1058
- const removeTr: number[] = [];
1059
- for (const [index, tr] of reverseTrBlots.entries()) {
1060
- const i = trBlots.length - index - 1;
1061
- if (tr.children.length <= 0) {
1062
- removeTr.push(i);
1063
- }
1064
- else {
1065
- // if have td rowspan across empty tr. minus rowspan
1066
- tr.foreachCellInner((td) => {
1067
- const sum = removeTr.reduce((sum, val) => td.rowspan + i > val ? sum + 1 : sum, 0);
1068
- td.rowspan -= sum;
1069
- // count exist col
1070
- colIdMap[td.colId] += 1;
1071
- });
1072
- }
1073
- }
1074
- // merge colspan
1075
- let index = 0;
1076
- for (const count of Object.values(colIdMap)) {
1077
- if (count === 0) {
1078
- const spanCols: number[] = [];
1079
- let skipRowNum = 0;
1080
- for (const tr of Object.values(trBlots)) {
1081
- const spanCol = spanCols.shift() || 0;
1082
- let nextSpanCols = [];
1083
- if (skipRowNum > 0) {
1084
- nextSpanCols = tr.getCellByColumIndex(index - spanCol)[2];
1085
- skipRowNum -= 1;
1086
- }
1087
- else {
1088
- nextSpanCols = tr.removeCell(index - spanCol);
1089
- if (nextSpanCols.skipRowNum) {
1090
- skipRowNum += nextSpanCols.skipRowNum;
1091
- }
1092
- }
1093
- for (const [i, n] of nextSpanCols.entries()) {
1094
- spanCols[i] = (spanCols[i] || 0) + n;
1095
- }
1096
- }
1097
- }
1098
- else {
1099
- index += 1;
1100
- }
1101
- }
1102
- // remove col
1103
- for (const col of tableCols) {
1104
- if (colIdMap[col.colId] === 0) {
1105
- if (col.prev) {
1106
- (col.prev as TableColFormat).width += col.width;
1107
- }
1108
- else if (col.next) {
1109
- (col.next as TableColFormat).width += col.width;
1110
- }
1111
- col.remove();
1112
- }
1113
- }
1114
- }
1115
-
1116
- removeRow(selectedTds: TableCellInnerFormat[]) {
1117
- if (selectedTds.length <= 0) return;
1118
- const baseTd = selectedTds[0];
1119
- const tableBlot = findParentBlot(baseTd, blotName.tableMain);
1120
- const trs = tableBlot.getRows();
1121
- let endTrIndex = trs.length;
1122
- let nextTrIndex = -1;
1123
- for (const td of selectedTds) {
1124
- const tr = findParentBlot(td, blotName.tableRow);
1125
- const index = trs.indexOf(tr);
1126
- if (index < endTrIndex) {
1127
- endTrIndex = index;
1128
- }
1129
- if (index + td.rowspan > nextTrIndex) {
1130
- nextTrIndex = index + td.rowspan;
1131
- }
1132
- }
1133
-
1134
- const patchTds: Record<string, {
1135
- rowspan: number;
1136
- colspan: number;
1137
- colIndex: number;
1138
- }> = {};
1139
- for (let i = endTrIndex; i < Math.min(trs.length, nextTrIndex); i++) {
1140
- const tr = trs[i];
1141
- tr.foreachCellInner((td) => {
1142
- // find cells in rowspan that exceed the deletion range
1143
- if (td.rowspan + i > nextTrIndex) {
1144
- patchTds[td.colId] = {
1145
- rowspan: td.rowspan + i - nextTrIndex,
1146
- colspan: td.colspan,
1147
- colIndex: td.getColumnIndex(),
1148
- };
1149
- }
1150
- // only remove td. empty tr to calculate colspan and rowspan
1151
- td.parent.remove();
1152
- });
1153
- if (tr.length() === 0) tr.remove();
1154
- }
1155
-
1156
- if (trs[nextTrIndex]) {
1157
- const nextTr = trs[nextTrIndex];
1158
- const tableId = tableBlot.tableId;
1159
- // insert cell in nextTr to patch exceed cell
1160
- for (const [colId, { colIndex, colspan, rowspan }] of Object.entries(patchTds)) {
1161
- nextTr.insertCell(colIndex, {
1162
- tableId,
1163
- rowId: nextTr.rowId,
1164
- colId,
1165
- colspan,
1166
- rowspan,
1167
- });
1168
- }
1169
- }
1170
-
1171
- this.fixTableByRemove(tableBlot);
1172
- }
1173
-
1174
- removeCol(selectedTds: TableCellInnerFormat[]) {
1175
- if (selectedTds.length <= 0) return;
1176
- const baseTd = selectedTds[0];
1177
- const tableBlot = findParentBlot(baseTd, blotName.tableMain);
1178
- const colspanMap: Record<string, number> = {};
1179
- for (const td of selectedTds) {
1180
- if (!colspanMap[td.rowId]) colspanMap[td.rowId] = 0;
1181
- colspanMap[td.rowId] += td.colspan;
1182
- }
1183
- const colspanCount = Math.max(...Object.values(colspanMap));
1184
- const columnIndex = baseTd.getColumnIndex();
1185
-
1186
- const trs = tableBlot.descendants(TableRowFormat);
1187
- for (let i = 0; i < colspanCount; i++) {
1188
- const spanCols: number[] = [];
1189
- let skipRowNum = 0;
1190
- for (const tr of Object.values(trs)) {
1191
- const spanCol = spanCols.shift() || 0;
1192
- if (skipRowNum > 0) {
1193
- skipRowNum -= 1;
1194
- continue;
1195
- }
1196
- const nextSpanCols = tr.removeCell(columnIndex - spanCol);
1197
- if (nextSpanCols.skipRowNum) {
1198
- skipRowNum += nextSpanCols.skipRowNum;
1199
- }
1200
- for (const [i, n] of nextSpanCols.entries()) {
1201
- spanCols[i] = (spanCols[i] || 0) + n;
1202
- }
1203
- }
1204
- }
1205
- // delete col need after remove cell. remove cell need all column id
1206
- // manual delete col. use fixTableByRemove to delete col will delete extra cells
1207
- const [colgroup] = tableBlot.descendants(TableColgroupFormat);
1208
- if (colgroup) {
1209
- for (let i = 0; i < colspanCount; i++) {
1210
- colgroup.removeColByIndex(columnIndex);
1211
- }
1212
- }
1213
-
1214
- this.fixTableByRemove(tableBlot);
1215
- }
1216
-
1217
- mergeCells(selectedTds: TableCellInnerFormat[]) {
1218
- if (selectedTds.length <= 1) return;
1219
- const baseCell = selectedTds[0];
1220
- // move selected cells in same table body
1221
- const baseCellBody = baseCell.getTableBody();
1222
- // insert base row
1223
- let baseRow = baseCell.getTableRow();
1224
- if (!baseCellBody || !baseRow) return;
1225
- for (let i = 1; i < selectedTds.length; i++) {
1226
- const selectTd = selectedTds[i];
1227
- const currentTdBody = selectTd.getTableBody();
1228
- if (currentTdBody && currentTdBody !== baseCellBody) {
1229
- const currentRow = selectTd.getTableRow();
1230
- if (currentRow) {
1231
- baseRow.parent.insertBefore(currentRow, baseRow.next);
1232
- baseRow = currentRow;
1233
- }
1234
- }
1235
- }
1236
- baseCellBody.convertBody(baseCell.wrapTag);
1237
-
1238
- const counts = selectedTds.reduce(
1239
- (pre, selectTd, index) => {
1240
- // count column span
1241
- const colId = selectTd.colId;
1242
- if (!pre[0][colId]) pre[0][colId] = 0;
1243
- pre[0][colId] += selectTd.rowspan;
1244
- // count row span
1245
- const rowId = selectTd.rowId;
1246
- if (!pre[1][rowId]) pre[1][rowId] = 0;
1247
- pre[1][rowId] += selectTd.colspan;
1248
- // merge select cell
1249
- if (index !== 0) {
1250
- selectTd.moveChildren(pre[2]);
1251
- selectTd.parent.remove();
1252
- }
1253
- return pre;
1254
- },
1255
- [{} as Record<string, number>, {} as Record<string, number>, baseCell] as const,
1256
- );
1257
-
1258
- const rowCount = Math.max(...Object.values(counts[0]));
1259
- const colCount = Math.max(...Object.values(counts[1]));
1260
- const baseTd = counts[2];
1261
- baseTd.colspan = colCount;
1262
- baseTd.rowspan = rowCount;
1263
-
1264
- // selection will move with cursor. make sure selection is in baseTd
1265
- const index = this.quill.getIndex(baseTd);
1266
- this.quill.setSelection({ index, length: 0 }, Quill.sources.SILENT);
1267
-
1268
- const tableBlot = findParentBlot(baseTd, blotName.tableMain);
1269
- this.fixTableByRemove(tableBlot);
1270
- }
1271
-
1272
- splitCell(selectedTds: TableCellInnerFormat[]) {
1273
- if (selectedTds.length !== 1) return;
1274
- const baseCell = selectedTds[0];
1275
- if (baseCell.colspan === 1 && baseCell.rowspan === 1) return;
1276
- const [tableBlot, baseTr] = findParentBlots(baseCell, [blotName.tableMain, blotName.tableRow] as const);
1277
- const rows = tableBlot.getRows();
1278
- const tableId = tableBlot.tableId;
1279
- const colIndex = baseCell.getColumnIndex();
1280
- const colIds = tableBlot.getColIds().slice(colIndex, colIndex + baseCell.colspan).toReversed();
1281
- const baseCellValue = baseCell.formats()[blotName.tableCellInner] as TableCellValue;
1282
- const { emptyRow, ...extendsBaseCellValue } = baseCellValue;
1283
-
1284
- let rowIndex = rows.indexOf(baseTr);
1285
- if (rowIndex === -1) return;
1286
- let curTr = rows[rowIndex];
1287
- let rowspan = baseCell.rowspan;
1288
- // reset span first. insertCell need colspan to judge insert position
1289
- baseCell.colspan = 1;
1290
- baseCell.rowspan = 1;
1291
- while (curTr && rowspan > 0) {
1292
- for (const id of colIds) {
1293
- // keep baseCell. baseTr should insert at baseCell's column index + 1
1294
- if (curTr === baseTr && id === baseCell.colId) continue;
1295
- curTr.insertCell(
1296
- colIndex + (curTr === baseTr ? 1 : 0),
1297
- {
1298
- ...extendsBaseCellValue,
1299
- tableId,
1300
- rowId: curTr.rowId,
1301
- colId: id,
1302
- rowspan: 1,
1303
- colspan: 1,
1304
- },
1305
- );
1306
- }
1307
-
1308
- rowspan -= 1;
1309
- rowIndex += 1;
1310
- curTr = rows[rowIndex];
1311
- }
1312
- }
1313
-
1314
- convertTableBodyByCells(tableBlot: TableMainFormat, selecteds: TableCellInnerFormat[], tag: TableBodyTag) {
1315
- let firstRowIndex: number | undefined;
1316
- let lastRowIndex: number | undefined;
1317
- const rows = tableBlot.getRows();
1318
- for (const cell of selecteds) {
1319
- const row = cell.getTableRow();
1320
- if (!row) continue;
1321
- const index = rows.indexOf(row);
1322
- if (isUndefined(firstRowIndex)) {
1323
- firstRowIndex = index;
1324
- }
1325
- if (isUndefined(lastRowIndex)) {
1326
- lastRowIndex = index;
1327
- }
1328
-
1329
- if (index < firstRowIndex) {
1330
- lastRowIndex = firstRowIndex;
1331
- firstRowIndex = index;
1332
- }
1333
- else if (index > lastRowIndex) {
1334
- lastRowIndex = index;
1335
- }
1336
- }
1337
- if (isUndefined(firstRowIndex) || isUndefined(lastRowIndex)) {
1338
- console.warn('TableRow not found');
1339
- return;
1340
- }
1341
- const firstRow = rows[firstRowIndex];
1342
- const lastRow = rows[lastRowIndex];
1343
- tableBlot.split(lastRow.offset(tableBlot) + lastRow.length());
1344
- const currentTable = tableBlot.split(firstRow.offset(tableBlot)) as TableMainFormat;
1345
- // selecteds may in different bodys
1346
- // create a new body, insert to current table, move all rows to new body, then call new body's convertBody
1347
- const currentTableRows = currentTable.getRows();
1348
- const [firstBody] = currentTable.getBodys();
1349
- const newBody = firstBody.clone() as TableBodyFormat;
1350
- currentTable.appendChild(newBody);
1351
- for (const row of currentTableRows) {
1352
- // only move the not empty row. the empty row will regenerate when `optimize`
1353
- if (row.length() > 0) {
1354
- newBody.appendChild(row);
1355
- }
1356
- }
1357
- newBody.convertBody(tag);
1358
- firstBody.remove();
1359
- }
1360
- }
1
+ import type { EmitterSource, Op, Parchment as TypeParchment, Range as TypeRange } from 'quill';
2
+ import type { BlockEmbed as TypeBlockEmbed } from 'quill/blots/block';
3
+ import type TypeBlock from 'quill/blots/block';
4
+ import type { Context } from 'quill/modules/keyboard';
5
+ import type TypeKeyboard from 'quill/modules/keyboard';
6
+ import type TypeToolbar from 'quill/modules/toolbar';
7
+ import type { TableSelection } from './modules';
8
+ import type { Constructor, QuillTheme, QuillThemePicker, TableBodyTag, TableCellValue, TableConstantsData, TableTextOptions, TableUpOptions } from './utils';
9
+ import Quill from 'quill';
10
+ import { BlockEmbedOverride, BlockOverride, ContainerFormat, ScrollOverride, TableBodyFormat, TableCaptionFormat, TableCellFormat, TableCellInnerFormat, TableColFormat, TableColgroupFormat, TableFootFormat, TableHeadFormat, TableMainFormat, TableRowFormat, TableWrapperFormat } from './formats';
11
+ import { TableClipboard } from './modules';
12
+ import { blotName, createBEM, createSelectBox, cssTextToObject, debounce, findParentBlot, findParentBlots, getScrollBarWidth, isForbidInTable, isFunction, isNumber, isString, isSubclassOf, isUndefined, limitDomInViewPort, mixinClass, objectToCssText, randomId, tableCantInsert, tableUpEvent, tableUpInternal, tableUpSize, toCamelCase } from './utils';
13
+
14
+ const Parchment = Quill.import('parchment');
15
+ const Delta = Quill.import('delta');
16
+ const icons = Quill.import('ui/icons') as Record<string, any>;
17
+ const Break = Quill.import('blots/break') as TypeParchment.BlotConstructor;
18
+ const Block = Quill.import('blots/block') as typeof TypeBlock;
19
+ const BlockEmbed = Quill.import('blots/block/embed') as typeof TypeBlockEmbed;
20
+
21
+ function createCell(scroll: TypeParchment.ScrollBlot, { tableId, rowId, colId }: { tableId: string; rowId: string; colId: string }) {
22
+ const value = {
23
+ tableId,
24
+ rowId,
25
+ colId,
26
+ colspan: 1,
27
+ rowspan: 1,
28
+ };
29
+ const tableCell = scroll.create(blotName.tableCell, value) as TypeParchment.ParentBlot;
30
+ const tableCellInner = scroll.create(blotName.tableCellInner, value) as TypeParchment.ParentBlot;
31
+ const block = scroll.create('block') as TypeParchment.ParentBlot;
32
+ block.appendChild(scroll.create('break'));
33
+ tableCellInner.appendChild(block);
34
+ tableCell.appendChild(tableCellInner);
35
+ return tableCell;
36
+ }
37
+ export function updateTableConstants(data: Partial<TableConstantsData>) {
38
+ tableCantInsert.delete(blotName.tableCellInner);
39
+
40
+ Object.assign(blotName, data.blotName || {});
41
+ Object.assign(tableUpSize, data.tableUpSize || {});
42
+ Object.assign(tableUpEvent, data.tableUpEvent || {});
43
+ Object.assign(tableUpInternal, data.tableUpInternal || {});
44
+
45
+ TableUp.moduleName = tableUpInternal.moduleName;
46
+
47
+ TableUp.toolName = blotName.tableWrapper;
48
+ ContainerFormat.blotName = blotName.container;
49
+ TableCaptionFormat.blotName = blotName.tableCaption;
50
+ TableWrapperFormat.blotName = blotName.tableWrapper;
51
+ TableMainFormat.blotName = blotName.tableMain;
52
+ TableColgroupFormat.blotName = blotName.tableColgroup;
53
+ TableColFormat.blotName = blotName.tableCol;
54
+ TableHeadFormat.blotName = blotName.tableHead;
55
+ TableBodyFormat.blotName = blotName.tableBody;
56
+ TableFootFormat.blotName = blotName.tableFoot;
57
+ TableRowFormat.blotName = blotName.tableRow;
58
+ TableCellFormat.blotName = blotName.tableCell;
59
+ TableCellInnerFormat.blotName = blotName.tableCellInner;
60
+
61
+ tableCantInsert.add(blotName.tableCellInner);
62
+ }
63
+ export function defaultCustomSelect(tableModule: TableUp, picker: QuillThemePicker) {
64
+ return createSelectBox({
65
+ onSelect: (row: number, col: number) => {
66
+ tableModule.insertTable(row, col, Quill.sources.USER);
67
+ if (picker) {
68
+ picker.close();
69
+ }
70
+ },
71
+ customBtn: tableModule.options.customBtn,
72
+ texts: tableModule.options.texts,
73
+ });
74
+ }
75
+
76
+ function generateTableArrowHandler(up: boolean) {
77
+ return {
78
+ bindInHead: false,
79
+ key: up ? 'ArrowUp' : 'ArrowDown',
80
+ collapsed: true,
81
+ format: [blotName.tableCellInner],
82
+ handler(this: { quill: Quill }, range: TypeRange, context: Context) {
83
+ const direction = up ? 'prev' : 'next';
84
+ const childDirection = up ? 'tail' : 'head';
85
+ if (context.line[direction]) return true;
86
+
87
+ // TODO: if there have a very long text in cell, the line will auto wrap
88
+ // there is no good way to find the correct index of the last `line`
89
+ const cursorRect = this.quill.selection.getBounds(range.index);
90
+ const lineRect = context.line.domNode.getBoundingClientRect();
91
+ if (!cursorRect || !lineRect) return true;
92
+ if (up) {
93
+ if (cursorRect.top - lineRect.top > 3) {
94
+ return true;
95
+ }
96
+ }
97
+ else {
98
+ if (lineRect.bottom - cursorRect.bottom > 3) {
99
+ return true;
100
+ }
101
+ }
102
+
103
+ let tableBlot: TableWrapperFormat;
104
+ let tableMain: TableMainFormat;
105
+ let tableRow: TableRowFormat;
106
+ let tableCell: TableCellFormat;
107
+ try {
108
+ [tableBlot, tableMain, tableRow, tableCell] = findParentBlots(context.line, [blotName.tableWrapper, blotName.tableMain, blotName.tableRow, blotName.tableCell] as const);
109
+ }
110
+ catch {
111
+ return true;
112
+ }
113
+
114
+ const colIds = tableMain.getColIds();
115
+ const tableCaption = tableBlot.descendants(TableCaptionFormat, 0)[0];
116
+
117
+ let aroundLine;
118
+ if (tableCaption) {
119
+ const captionSide = window.getComputedStyle(tableCaption.domNode);
120
+ if (direction === 'next' && captionSide.captionSide === 'bottom') {
121
+ aroundLine = tableCaption;
122
+ }
123
+ else if (direction === 'next') {
124
+ aroundLine = tableBlot.next;
125
+ }
126
+ else {
127
+ aroundLine = tableCaption;
128
+ }
129
+ }
130
+ else {
131
+ aroundLine = tableBlot[direction];
132
+ }
133
+ if (context.line[direction] || !aroundLine) return true;
134
+
135
+ const targetRow = tableRow[direction] as TableRowFormat;
136
+ if (targetRow) {
137
+ const cellIndex = colIds.indexOf(tableCell.colId);
138
+ const targetCell = targetRow.getCellByColId(colIds[cellIndex], direction);
139
+ if (!targetCell) return true;
140
+ let targetChild = targetCell.children[childDirection] as TypeParchment.ParentBlot;
141
+ if (targetChild.children) {
142
+ targetChild = targetChild.children[childDirection] as TypeParchment.ParentBlot;
143
+ }
144
+ const index = targetChild.offset(this.quill.scroll) + Math.min(context.offset, targetChild.length() - 1);
145
+ this.quill.setSelection(index, 0, Quill.sources.USER);
146
+ }
147
+ else {
148
+ const index = aroundLine.offset(this.quill.scroll) + (up ? aroundLine.length() - 1 : 0);
149
+ this.quill.setSelection(index, 0, Quill.sources.USER);
150
+ }
151
+ return false;
152
+ },
153
+ };
154
+ }
155
+
156
+ export class TableUp {
157
+ static moduleName: string = tableUpInternal.moduleName;
158
+ static toolName: string = blotName.tableWrapper;
159
+ // TODO: add custom property `bindInHead`, but Quill doesn't export `BindingObject`
160
+ static keyboradHandler = {
161
+ 'forbid remove table by backspace': {
162
+ bindInHead: true,
163
+ key: 'Backspace',
164
+ collapsed: true,
165
+ offset: 0,
166
+ handler(this: { quill: Quill }, range: TypeRange, context: Context) {
167
+ const line = this.quill.getLine(range.index);
168
+ const blot = line[0] as TypeParchment.BlockBlot;
169
+ if (blot.prev instanceof TableWrapperFormat) {
170
+ blot.prev.remove();
171
+ return false;
172
+ }
173
+
174
+ if (context.format[blotName.tableCellInner]) {
175
+ const offset = blot.offset(findParentBlot(blot, blotName.tableCellInner));
176
+ if (offset === 0) {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ return true;
182
+ },
183
+ },
184
+ 'forbid remove table by delete': {
185
+ bindInHead: true,
186
+ key: 'Delete',
187
+ collapsed: true,
188
+ handler(this: { quill: Quill }, range: TypeRange, context: Context) {
189
+ const line = this.quill.getLine(range.index);
190
+ const blot = line[0] as TypeParchment.BlockBlot;
191
+ const offsetInline = line[1];
192
+ if ((blot.next instanceof TableWrapperFormat || blot.next instanceof TableColFormat) && offsetInline === blot.length() - 1) return false;
193
+
194
+ if (context.format[blotName.tableCellInner]) {
195
+ const tableInnerBlot = findParentBlot(blot, blotName.tableCellInner);
196
+ if (blot === tableInnerBlot.children.tail && offsetInline === blot.length() - 1) {
197
+ return false;
198
+ }
199
+ }
200
+ return true;
201
+ },
202
+ },
203
+ 'table up': generateTableArrowHandler(true),
204
+ 'table down': generateTableArrowHandler(false),
205
+ 'table caption break': {
206
+ bindInHead: true,
207
+ key: 'Enter',
208
+ shiftKey: null,
209
+ format: [blotName.tableCaption],
210
+ handler(this: { quill: Quill }, _range: TypeRange, _context: Context) {
211
+ return false;
212
+ },
213
+ },
214
+ };
215
+
216
+ static register() {
217
+ TableWrapperFormat.allowedChildren = [TableMainFormat];
218
+
219
+ TableMainFormat.allowedChildren = [TableColgroupFormat, TableCaptionFormat, TableHeadFormat, TableBodyFormat, TableFootFormat];
220
+ TableMainFormat.requiredContainer = TableWrapperFormat;
221
+
222
+ TableCaptionFormat.requiredContainer = TableMainFormat;
223
+
224
+ TableColgroupFormat.allowedChildren = [TableColFormat];
225
+ TableColgroupFormat.requiredContainer = TableMainFormat;
226
+
227
+ TableHeadFormat.allowedChildren = [TableRowFormat];
228
+ TableHeadFormat.requiredContainer = TableMainFormat;
229
+ TableBodyFormat.allowedChildren = [TableRowFormat];
230
+ TableBodyFormat.requiredContainer = TableMainFormat;
231
+ TableFootFormat.allowedChildren = [TableRowFormat];
232
+ TableFootFormat.requiredContainer = TableMainFormat;
233
+
234
+ TableRowFormat.allowedChildren = [TableCellFormat];
235
+
236
+ TableCellFormat.allowedChildren = [TableCellInnerFormat, Break];
237
+ TableCellFormat.requiredContainer = TableRowFormat;
238
+
239
+ TableCellInnerFormat.requiredContainer = TableCellFormat;
240
+
241
+ // override Block and BlockEmbed
242
+ const excludeFormat = new Set(['table']);
243
+ const overrideFormats = Object.entries(Quill.imports as Record<string, Constructor>).filter(([name, blot]) => {
244
+ const blotName = name.split('formats/')[1];
245
+ return name.startsWith('formats/')
246
+ && !excludeFormat.has(blotName)
247
+ && (isSubclassOf(blot, Block) || isSubclassOf(blot, BlockEmbed));
248
+ });
249
+ const overrides = overrideFormats.reduce((pre, [name, blot]) => {
250
+ const extendsClass = isSubclassOf(blot, BlockEmbed) ? BlockEmbedOverride : BlockOverride;
251
+ pre[name] = class extends mixinClass(blot, [extendsClass]) {
252
+ static register() {}
253
+ };
254
+ return pre;
255
+ }, {} as Record<string, Constructor>);
256
+
257
+ Quill.register({
258
+ 'blots/scroll': ScrollOverride,
259
+ 'blots/block': BlockOverride,
260
+ 'blots/block/embed': BlockEmbedOverride,
261
+ ...overrides,
262
+ [`blots/${blotName.container}`]: ContainerFormat,
263
+ [`formats/${blotName.tableCell}`]: TableCellFormat,
264
+ [`formats/${blotName.tableCellInner}`]: TableCellInnerFormat,
265
+ [`formats/${blotName.tableRow}`]: TableRowFormat,
266
+ [`formats/${blotName.tableHead}`]: TableHeadFormat,
267
+ [`formats/${blotName.tableBody}`]: TableBodyFormat,
268
+ [`formats/${blotName.tableFoot}`]: TableFootFormat,
269
+ [`formats/${blotName.tableCol}`]: TableColFormat,
270
+ [`formats/${blotName.tableColgroup}`]: TableColgroupFormat,
271
+ [`formats/${blotName.tableCaption}`]: TableCaptionFormat,
272
+ [`formats/${blotName.tableMain}`]: TableMainFormat,
273
+ [`formats/${blotName.tableWrapper}`]: TableWrapperFormat,
274
+ 'modules/clipboard': TableClipboard,
275
+ }, true);
276
+ }
277
+
278
+ quill: Quill;
279
+ options: TableUpOptions;
280
+ toolBox: HTMLDivElement;
281
+ fixTableByLisenter = debounce(this.balanceTables, 100);
282
+ selector?: HTMLElement;
283
+ resizeOb!: ResizeObserver;
284
+ modules: Record<string, Constructor> = {};
285
+
286
+ get statics(): any {
287
+ return this.constructor;
288
+ }
289
+
290
+ constructor(quill: Quill, options: Partial<TableUpOptions>) {
291
+ this.quill = quill;
292
+ this.options = this.resolveOptions(options || {});
293
+ this.toolBox = this.initialContainer();
294
+
295
+ const toolbar = this.quill.getModule('toolbar') as TypeToolbar;
296
+ if (toolbar && (this.quill.theme as QuillTheme).pickers) {
297
+ const [, select] = (toolbar.controls as [string, HTMLElement][] || []).find(([name]) => name === this.statics.toolName) || [];
298
+ if (select && select.tagName.toLocaleLowerCase() === 'select') {
299
+ const picker = (this.quill.theme as QuillTheme).pickers.find(picker => picker.select === select);
300
+ if (picker) {
301
+ picker.label.innerHTML = this.options.icon;
302
+ this.buildCustomSelect(this.options.customSelect, picker);
303
+ picker.label.addEventListener('mousedown', () => {
304
+ if (!this.selector || !picker) return;
305
+ const selectRect = this.selector.getBoundingClientRect();
306
+ const { leftLimited } = limitDomInViewPort(selectRect);
307
+ if (leftLimited) {
308
+ const labelRect = picker.label.getBoundingClientRect();
309
+ Object.assign(picker.options.style, { transform: `translateX(calc(-100% + ${labelRect.width}px))` });
310
+ }
311
+ else {
312
+ Object.assign(picker.options.style, { transform: undefined });
313
+ }
314
+ });
315
+ }
316
+ }
317
+ }
318
+
319
+ const keyboard = this.quill.getModule('keyboard') as TypeKeyboard;
320
+ for (const handle of Object.values(TableUp.keyboradHandler)) {
321
+ // insert before default key handler
322
+ if (handle.bindInHead) {
323
+ keyboard.bindings[handle.key].unshift(handle);
324
+ }
325
+ else {
326
+ keyboard.addBinding(handle.key, handle);
327
+ }
328
+ }
329
+
330
+ this.initModules();
331
+ this.quillHack();
332
+ this.listenBalanceCells();
333
+ }
334
+
335
+ initialContainer() {
336
+ const toolboxBEM = createBEM('toolbox');
337
+ const container = this.quill.addContainer(toolboxBEM.b());
338
+ const updateContainerStyle = () => {
339
+ const quillRootRect = this.quill.root.getBoundingClientRect();
340
+ const { offsetLeft, offsetTop } = this.quill.root;
341
+ Object.assign(container.style, {
342
+ top: `${offsetTop}px`,
343
+ left: `${offsetLeft}px`,
344
+ width: `${quillRootRect.width}px`,
345
+ height: `${quillRootRect.height}px`,
346
+ });
347
+ };
348
+ this.resizeOb = new ResizeObserver(updateContainerStyle);
349
+ this.resizeOb.observe(this.quill.root);
350
+ return container;
351
+ }
352
+
353
+ addContainer(classes: string | HTMLElement) {
354
+ if (isString(classes)) {
355
+ const el = document.createElement('div');
356
+ for (const classname of classes.split(' ')) {
357
+ el.classList.add(classname);
358
+ }
359
+ this.toolBox.appendChild(el);
360
+ return el;
361
+ }
362
+ else {
363
+ this.toolBox.appendChild(classes);
364
+ return classes;
365
+ }
366
+ }
367
+
368
+ resolveOptions(options: Partial<TableUpOptions>): TableUpOptions {
369
+ return Object.assign({
370
+ customBtn: false,
371
+ texts: this.resolveTexts(options.texts || {}),
372
+ full: false,
373
+ fullSwitch: true,
374
+ icon: icons.table,
375
+ autoMergeCell: true,
376
+ modules: [],
377
+ } as TableUpOptions, options);
378
+ }
379
+
380
+ resolveTexts(options: Partial<TableTextOptions>) {
381
+ return Object.assign({
382
+ fullCheckboxText: 'Insert full width table',
383
+ customBtnText: 'Custom',
384
+ confirmText: 'Confirm',
385
+ cancelText: 'Cancel',
386
+ rowText: 'Row',
387
+ colText: 'Column',
388
+ notPositiveNumberError: 'Please enter a positive integer',
389
+ custom: 'Custom',
390
+ clear: 'Clear',
391
+ transparent: 'Transparent',
392
+ 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?',
393
+ CopyCell: 'Copy cell',
394
+ CutCell: 'Cut cell',
395
+ InsertTop: 'Insert row above',
396
+ InsertRight: 'Insert column right',
397
+ InsertBottom: 'Insert row below',
398
+ InsertLeft: 'Insert column Left',
399
+ MergeCell: 'Merge Cell',
400
+ SplitCell: 'Split Cell',
401
+ DeleteRow: 'Delete Row',
402
+ DeleteColumn: 'Delete Column',
403
+ DeleteTable: 'Delete table',
404
+ BackgroundColor: 'Set background color',
405
+ BorderColor: 'Set border color',
406
+ }, options);
407
+ }
408
+
409
+ initModules() {
410
+ for (const item of this.options.modules) {
411
+ this.modules[item.module.moduleName] = new item.module(this, this.quill, item.options);
412
+ }
413
+ }
414
+
415
+ getModule<T>(name: string) {
416
+ return this.modules[name] as T | undefined;
417
+ }
418
+
419
+ quillHack() {
420
+ const originGetSemanticHTML = this.quill.getSemanticHTML;
421
+ this.quill.getSemanticHTML = ((index: number = 0, length?: number) => {
422
+ const html = originGetSemanticHTML.call(this.quill, index, length);
423
+
424
+ const tableWrapperFormat = Quill.import(`formats/${blotName.tableWrapper}`) as typeof TableWrapperFormat;
425
+ const parser = new DOMParser();
426
+ const doc = parser.parseFromString(html, 'text/html');
427
+ for (const node of Array.from(doc.querySelectorAll(`.${tableWrapperFormat.className} caption[contenteditable], .${tableWrapperFormat.className} .${TableCellFormat.className} > [contenteditable]`))) {
428
+ node.removeAttribute('contenteditable');
429
+ }
430
+
431
+ return doc.body.innerHTML;
432
+ }) as typeof originGetSemanticHTML;
433
+
434
+ // make sure toolbar item can format selected cells
435
+ const originFormat = this.quill.format;
436
+ this.quill.format = function (name: string, value: unknown, source: EmitterSource = Quill.sources.API) {
437
+ const blot = this.scroll.query(name);
438
+ // filter embed blot
439
+ if (!((blot as TypeParchment.BlotConstructor).prototype instanceof Parchment.EmbedBlot)) {
440
+ const tableUpModule = this.getModule(tableUpInternal.moduleName) as TableUp;
441
+ const range = this.getSelection(true);
442
+ const formats = this.getFormat(range);
443
+ // only when selection in cell and selectedTds > 1 can format all cells
444
+ const tableSelection = tableUpModule.getModule<TableSelection>(tableUpInternal.tableSelectionName);
445
+ if (!formats[blotName.tableCellInner] || range.length > 0 || (tableUpModule && tableSelection && tableSelection.selectedTds.length <= 1)) {
446
+ return originFormat.call(this, name, value, source);
447
+ }
448
+ // format in selected cells
449
+ if (tableUpModule && tableSelection && tableSelection.selectedTds.length > 0) {
450
+ const selectedTds = tableSelection.selectedTds;
451
+ // calculate the format value. the format should be canceled when this value exists in all selected cells
452
+ let setOrigin = false;
453
+ const tdRanges = [];
454
+ for (const innerTd of selectedTds) {
455
+ const index = innerTd.offset(this.scroll);
456
+ const length = innerTd.length();
457
+ tdRanges.push({ index, length });
458
+ const format = this.getFormat(index, length);
459
+ if (format[name] !== value) {
460
+ setOrigin = true;
461
+ }
462
+ }
463
+ const resultValue = setOrigin ? value : false;
464
+
465
+ const delta = new Delta();
466
+ for (const [i, { index, length }] of tdRanges.entries()) {
467
+ const lastIndex = i === 0 ? 0 : tdRanges[i - 1].index + tdRanges[i - 1].length;
468
+ delta.retain(index - lastIndex).retain(length, { [name]: resultValue });
469
+ }
470
+
471
+ const updateDelta = this.updateContents(delta, source);
472
+ this.blur();
473
+ return updateDelta;
474
+ }
475
+ }
476
+
477
+ return originFormat.call(this, name, value, source);
478
+ };
479
+
480
+ // handle clean
481
+ const toolbar = this.quill.theme.modules.toolbar;
482
+ if (toolbar) {
483
+ const cleanHandler = toolbar.handlers?.clean;
484
+ if (cleanHandler) {
485
+ const cleanFormatExcludeTable = (index: number, length: number, changeCellStyle: false | ((styleStr: string | undefined) => string) = () => '') => {
486
+ // base on `removeFormat`. but not remove tableCellInner
487
+ const text = this.quill.getText(index, length);
488
+ const [line, offset] = this.quill.getLine(index + length);
489
+ let suffixLength = 0;
490
+ let suffix = new Delta();
491
+ if (line != null) {
492
+ suffixLength = line.length() - offset;
493
+ suffix = line.delta().slice(offset, offset + suffixLength - 1).insert('\n');
494
+ }
495
+ const contents = this.quill.getContents(index, length + suffixLength);
496
+ const diff = contents.diff(new Delta().insert(text).concat(suffix));
497
+
498
+ let deltaIndex = 0;
499
+ const ops = diff.ops.map((op: Op) => {
500
+ const { attributes, ...other } = op;
501
+ if (op.insert) {
502
+ deltaIndex -= isString(op.insert) ? op.insert.length : 1;
503
+ }
504
+ else if (op.retain) {
505
+ deltaIndex += isNumber(op.retain) ? op.retain : 1;
506
+ }
507
+ else if (op.delete) {
508
+ deltaIndex += op.delete;
509
+ }
510
+
511
+ if (attributes) {
512
+ const { [blotName.tableCellInner]: nullValue, ...attrs } = attributes;
513
+ if (changeCellStyle) {
514
+ const tableCellInner = contents.slice(deltaIndex - 1, deltaIndex).ops[0];
515
+ if (tableCellInner?.attributes?.[blotName.tableCellInner]) {
516
+ const tableCellInnerValue = tableCellInner.attributes[blotName.tableCellInner] as TableCellValue;
517
+ const { style, ...value } = tableCellInnerValue;
518
+ const newStyle = changeCellStyle(style);
519
+ if (newStyle) {
520
+ return { ...other, attributes: { ...attrs, [blotName.tableCellInner]: { style: newStyle, ...value } } };
521
+ }
522
+ return { ...other, attributes: { ...attrs, [blotName.tableCellInner]: value } };
523
+ }
524
+ }
525
+ return { ...other, attributes: { ...attrs } };
526
+ }
527
+ return op;
528
+ });
529
+ return new Delta(ops);
530
+ };
531
+ toolbar.handlers!.clean = function (this: TypeToolbar, value: unknown): void {
532
+ const tableUpModule = this.quill.getModule(tableUpInternal.moduleName) as TableUp;
533
+ const range = this.quill.getSelection();
534
+ if (range && range.length > 0) {
535
+ const formats = this.quill.getFormat(range);
536
+ if (formats[blotName.tableCellInner]) {
537
+ const diff = cleanFormatExcludeTable(range.index, range.length, false);
538
+ const delta = new Delta().retain(range.index).concat(diff);
539
+ this.quill.updateContents(delta, Quill.sources.USER);
540
+ return;
541
+ }
542
+ }
543
+ // if selection range is not in table, but use the TableSelection selected cells
544
+ // clean all other formats in cell
545
+ const tableSelection = tableUpModule.getModule<TableSelection>(tableUpInternal.tableSelectionName);
546
+ if (tableUpModule && tableSelection && tableSelection.selectedTds.length > 0 && tableSelection.table) {
547
+ const tableMain = Quill.find(tableSelection.table) as TableMainFormat;
548
+ if (!tableMain) {
549
+ console.warn('TableMainFormat not found');
550
+ return;
551
+ }
552
+ const selectedTds = tableSelection.selectedTds;
553
+
554
+ // get all need clean style cells. include border-right/border-bottom effect cells
555
+ const editTds = new Set<TableCellFormat>();
556
+ const tds: { td: TableCellFormat; cleanBorder: 'bottom' | 'right' | true }[] = [];
557
+ for (const innerTd of selectedTds) {
558
+ if (innerTd.parent instanceof TableCellFormat) {
559
+ for (const td of innerTd.parent.getNearByCell('top')) {
560
+ if (editTds.has(td)) continue;
561
+ editTds.add(td);
562
+ tds.push({ td, cleanBorder: 'bottom' });
563
+ }
564
+ for (const td of innerTd.parent.getNearByCell('left')) {
565
+ if (editTds.has(td)) continue;
566
+ editTds.add(td);
567
+ tds.push({ td, cleanBorder: 'right' });
568
+ }
569
+
570
+ editTds.add(innerTd.parent);
571
+ tds.push({ td: innerTd.parent, cleanBorder: true });
572
+ }
573
+ }
574
+ // sort cells makesure index correct
575
+ const allCells = tableMain.descendants(TableCellFormat);
576
+ const cellIndexMap = new Map(allCells.map((cell, index) => [cell, index]));
577
+ tds.sort((a, b) => cellIndexMap.get(a.td)! - cellIndexMap.get(b.td)!);
578
+
579
+ // compute delta
580
+ let delta = new Delta();
581
+ let lastIndex = 0;
582
+ for (const { td, cleanBorder } of tds) {
583
+ const index = td.getCellInner().offset(this.quill.scroll);
584
+ const length = td.getCellInner().length();
585
+ // `line` length will include a break(\n) at the end. minus 1 to remove break
586
+ const diff = cleanFormatExcludeTable(
587
+ index,
588
+ length - 1,
589
+ (styleStr: string | undefined) => {
590
+ if (!styleStr || cleanBorder === true) return '';
591
+ // only clean border-right/border-bottom style
592
+ const css = cssTextToObject(styleStr);
593
+ const filterStyle = Object.keys(css).filter(key => !key.startsWith(toCamelCase(`border-${cleanBorder}`))).reduce((acc: Record<string, string>, key: string) => {
594
+ acc[key] = css[key];
595
+ return acc;
596
+ }, {});
597
+ return objectToCssText(filterStyle);
598
+ },
599
+ );
600
+ const cellDiff = new Delta().retain(index - lastIndex).concat(diff);
601
+ delta = delta.concat(cellDiff);
602
+ lastIndex = index + length;
603
+ }
604
+ this.quill.updateContents(delta, Quill.sources.USER);
605
+ if (selectedTds.length > 1) this.quill.blur();
606
+ return;
607
+ }
608
+ return cleanHandler.call(this, value);
609
+ };
610
+ }
611
+ }
612
+ }
613
+
614
+ async buildCustomSelect(customSelect: ((module: TableUp, picker: QuillThemePicker) => HTMLElement | Promise<HTMLElement>) | undefined, picker: QuillThemePicker) {
615
+ if (!customSelect || !isFunction(customSelect)) return;
616
+ const dom = document.createElement('div');
617
+ dom.classList.add('ql-custom-select');
618
+ this.selector = await customSelect(this, picker);
619
+ dom.appendChild(this.selector);
620
+ if (this.options.fullSwitch) {
621
+ const bem = createBEM('creator');
622
+ const isFulllLabel = document.createElement('label');
623
+ isFulllLabel.classList.add(bem.be('checkbox'));
624
+ const isFullCheckbox = document.createElement('input');
625
+ isFullCheckbox.type = 'checkbox';
626
+ isFullCheckbox.checked = this.options.full;
627
+ isFullCheckbox.addEventListener('change', () => {
628
+ this.options.full = isFullCheckbox.checked;
629
+ });
630
+ const isFullCheckboxText = document.createElement('span');
631
+ isFullCheckboxText.textContent = this.options.texts.fullCheckboxText;
632
+ isFulllLabel.appendChild(isFullCheckbox);
633
+ isFulllLabel.appendChild(isFullCheckboxText);
634
+ dom.appendChild(isFulllLabel);
635
+ }
636
+ picker.options.innerHTML = '';
637
+ picker.options.appendChild(dom);
638
+ }
639
+
640
+ setCellAttrs(selectedTds: TableCellInnerFormat[], attr: string, value?: any, isStyle: boolean = false) {
641
+ if (selectedTds.length === 0) return;
642
+ for (const td of selectedTds) {
643
+ td.setFormatValue(attr, value, isStyle);
644
+ }
645
+ }
646
+
647
+ getTextByCell(tds: TableCellInnerFormat[]) {
648
+ let text = '';
649
+ for (const td of tds) {
650
+ const index = td.offset(this.quill.scroll);
651
+ const length = td.length();
652
+ for (const op of this.quill.getContents(index, length).ops) {
653
+ if (isString(op.insert)) {
654
+ text += op.insert;
655
+ }
656
+ }
657
+ }
658
+ return text;
659
+ }
660
+
661
+ getHTMLByCell(tds: TableCellInnerFormat[], isCut = false) {
662
+ if (tds.length === 0) return '';
663
+ let tableMain: TableMainFormat | null = null;
664
+ try {
665
+ for (const td of tds) {
666
+ const tdParentMain = findParentBlot(td, blotName.tableMain);
667
+ if (!tableMain) {
668
+ tableMain = tdParentMain;
669
+ }
670
+ if (tdParentMain !== tableMain) {
671
+ console.error('tableMain is not same');
672
+ return '';
673
+ }
674
+ }
675
+ }
676
+ catch {
677
+ console.error('tds must be in same tableMain');
678
+ return '';
679
+ }
680
+
681
+ if (!tableMain) return '';
682
+ const tableIndex = this.quill.getIndex(tableMain);
683
+ const tableLength = tableMain.length();
684
+ const tableHTML = this.quill.getSemanticHTML(tableIndex, tableLength);
685
+ const parser = new DOMParser();
686
+ const doc = parser.parseFromString(tableHTML, 'text/html');
687
+
688
+ const cols = Array.from(doc.querySelectorAll('col'));
689
+ const colIds = cols.map(col => col.dataset.colId!);
690
+ const cellColWidth: string[] = [];
691
+ const cellColIds = new Set<string>();
692
+ const cellIds = new Set<string>();
693
+ for (const td of tds) {
694
+ cellColIds.add(td.colId);
695
+ const currentColId = td.colId;
696
+ const colIndex = colIds.indexOf(currentColId);
697
+ for (let i = 0; i < td.colspan; i++) {
698
+ cellColIds.add(colIds[colIndex + i]);
699
+ }
700
+ cellIds.add(`${td.rowId}-${td.colId}`);
701
+ }
702
+ // filter col
703
+ for (let index = 0; index < cols.length; index++) {
704
+ const col = cols[index];
705
+ if (!cellColIds.has(col.dataset.colId!)) {
706
+ col.remove();
707
+ cols.splice(index--, 1);
708
+ }
709
+ else {
710
+ cellColWidth.push(col.getAttribute('width')!);
711
+ }
712
+ }
713
+ // filter td
714
+ let rowCount = 0;
715
+ let lastRowId: string | null = null;
716
+ for (const td of Array.from(doc.querySelectorAll('td, th')) as HTMLElement[]) {
717
+ if (!cellIds.has(`${td.dataset.rowId}-${td.dataset.colId}`)) {
718
+ const parent = td.parentElement;
719
+ td.remove();
720
+ if (parent && parent.children.length <= 0) {
721
+ parent.remove();
722
+ }
723
+ }
724
+ else {
725
+ if (lastRowId !== td.dataset.rowId) {
726
+ rowCount += 1;
727
+ lastRowId = td.dataset.rowId!;
728
+ }
729
+ }
730
+ }
731
+ // calculate width
732
+ const colsValue = cols.map(col => TableColFormat.value(col));
733
+ if (tableMain.full) {
734
+ const totalWidth = colsValue.reduce((total, col) => col.width + total, 0);
735
+ for (const [i, col] of colsValue.entries()) {
736
+ col.width = Math.round((col.width / totalWidth) * 100);
737
+ cols[i].setAttribute('width', `${col.width}%`);
738
+ }
739
+ }
740
+ else {
741
+ let width = 0;
742
+ for (const col of colsValue) {
743
+ width += col.width;
744
+ }
745
+ const tableMainDom = doc.querySelector('table')!;
746
+ tableMainDom.style.width = `${width}px`;
747
+ }
748
+
749
+ if (isCut) {
750
+ const trs = tableMain.getRows();
751
+ if (rowCount === trs.length) {
752
+ this.removeCol(tds);
753
+ }
754
+ else {
755
+ for (const td of tds) {
756
+ td.domNode.innerHTML = '<p><br></p>';
757
+ }
758
+ }
759
+ }
760
+ return doc.body.innerHTML;
761
+ }
762
+
763
+ insertTable(rows: number, columns: number, source: EmitterSource = Quill.sources.API) {
764
+ if (rows >= 30 || columns >= 30) {
765
+ throw new Error('Both rows and columns must be less than 30.');
766
+ }
767
+
768
+ this.quill.focus();
769
+ const range = this.quill.getSelection();
770
+ if (range == null) return;
771
+ const [currentBlot] = this.quill.getLeaf(range.index);
772
+ if (!currentBlot) return;
773
+ if (isForbidInTable(currentBlot)) {
774
+ throw new Error(`Not supported ${currentBlot.statics.blotName} insert into table.`);
775
+ }
776
+
777
+ const tableId = randomId();
778
+ const colIds = new Array(columns).fill(0).map(() => randomId());
779
+
780
+ const borderWidth = this.calculateTableCellBorderWidth();
781
+ const rootStyle = getComputedStyle(this.quill.root);
782
+ const paddingLeft = Number.parseInt(rootStyle.paddingLeft);
783
+ const paddingRight = Number.parseInt(rootStyle.paddingRight);
784
+ const scrollBarWidth = this.quill.root.scrollHeight > this.quill.root.clientHeight ? getScrollBarWidth({ target: this.quill.root }) : 0;
785
+ const width = Number.parseInt(rootStyle.width) - paddingLeft - paddingRight - borderWidth - scrollBarWidth;
786
+
787
+ // insert delta data to create table
788
+ const colWidth = !this.options.full ? `${Math.max(Math.floor(width / columns), tableUpSize.colMinWidthPx)}px` : `${Math.max((1 / columns) * 100, tableUpSize.colMinWidthPre)}%`;
789
+ const delta: Record<string, any>[] = [{ retain: range.index }];
790
+ const aroundContent = this.quill.getContents(range.index, 1);
791
+ const [, offset] = this.quill.getLine(range.index);
792
+ if (aroundContent.ops[0].insert !== '\n' && offset !== 0) delta.push({ insert: '\n' });
793
+
794
+ for (let i = 0; i < columns; i++) {
795
+ delta.push({
796
+ insert: {
797
+ [blotName.tableCol]: {
798
+ width: colWidth,
799
+ tableId,
800
+ colId: colIds[i],
801
+ full: this.options.full,
802
+ },
803
+ },
804
+ });
805
+ }
806
+ for (let j = 0; j < rows; j++) {
807
+ const rowId = randomId();
808
+ for (let i = 0; i < columns; i++) {
809
+ delta.push({
810
+ insert: '\n',
811
+ attributes: {
812
+ [blotName.tableCellInner]: {
813
+ tableId,
814
+ rowId,
815
+ colId: colIds[i],
816
+ rowspan: 1,
817
+ colspan: 1,
818
+ },
819
+ },
820
+ });
821
+ }
822
+ }
823
+
824
+ this.quill.updateContents(new Delta(delta), source);
825
+ this.quill.setSelection(range.index + columns, Quill.sources.SILENT);
826
+ this.quill.focus();
827
+ }
828
+
829
+ calculateTableCellBorderWidth() {
830
+ const tableStr = `
831
+ <table class="${TableMainFormat.className}">
832
+ <tbody>
833
+ <tr>
834
+ <td class="${TableCellFormat.className}"></td>
835
+ </tr>
836
+ </tbody>
837
+ </table>
838
+ `;
839
+ const div = document.createElement('div');
840
+ div.className = TableWrapperFormat.className;
841
+ div.innerHTML = tableStr;
842
+ div.style.position = 'absolute';
843
+ div.style.left = '-9999px';
844
+ div.style.top = '-9999px';
845
+ div.style.visibility = 'hidden';
846
+ this.quill.root.appendChild(div);
847
+ const tempTableStyle = window.getComputedStyle(div.querySelector('td')!);
848
+ const borderWidth = Number.parseFloat(tempTableStyle.borderWidth) || 0;
849
+ this.quill.root.removeChild(div);
850
+ return borderWidth;
851
+ }
852
+
853
+ // handle unusual delete cell
854
+ fixUnusuaDeletelTable(tableBlot: TableMainFormat) {
855
+ const tableColIds = tableBlot.getColIds();
856
+ if (tableColIds.length === 0) {
857
+ tableBlot.remove();
858
+ return;
859
+ }
860
+ const bodys = tableBlot.getBodys();
861
+ const tableId = tableBlot.tableId;
862
+ for (const body of bodys) {
863
+ // calculate all cells in body
864
+ const trBlots = body.getRows();
865
+ if (trBlots.length === 0) {
866
+ body.remove();
867
+ continue;
868
+ }
869
+ // append by col
870
+ const cellSpanMap = new Array(trBlots.length).fill(0).map(() => new Array(tableColIds.length).fill(false));
871
+ for (const [indexTr, tr] of trBlots.entries()) {
872
+ let indexTd = 0;
873
+ let indexCol = 0;
874
+ const curCellSpan = cellSpanMap[indexTr];
875
+ const tds = tr.descendants(TableCellFormat);
876
+ // loop every row and column
877
+ while (indexCol < tableColIds.length) {
878
+ // skip when rowspan or colspan
879
+ if (curCellSpan[indexCol]) {
880
+ indexCol += 1;
881
+ continue;
882
+ }
883
+ const curTd = tds[indexTd];
884
+ // if colId does not match. insert a new one
885
+ if (!curTd || curTd.colId !== tableColIds[indexCol]) {
886
+ tr.insertBefore(
887
+ createCell(
888
+ this.quill.scroll,
889
+ {
890
+ tableId,
891
+ colId: tableColIds[indexCol],
892
+ rowId: tr.rowId,
893
+ },
894
+ ),
895
+ curTd,
896
+ );
897
+ }
898
+ else {
899
+ if (indexTr + curTd.rowspan - 1 >= trBlots.length) {
900
+ curTd.getCellInner().rowspan = trBlots.length - indexTr;
901
+ }
902
+
903
+ const { colspan, rowspan } = curTd;
904
+ // skip next column cell
905
+ if (colspan > 1) {
906
+ for (let c = 1; c < colspan; c++) {
907
+ curCellSpan[indexCol + c] = true;
908
+ }
909
+ }
910
+ // skip next rowspan cell
911
+ if (rowspan > 1) {
912
+ for (let r = indexTr + 1; r < indexTr + rowspan; r++) {
913
+ for (let c = 0; c < colspan; c++) {
914
+ cellSpanMap[r][indexCol + c] = true;
915
+ }
916
+ }
917
+ }
918
+ indexTd += 1;
919
+ }
920
+ indexCol += 1;
921
+ }
922
+
923
+ // if td not match all exist td. Indicates that a cell has been inserted
924
+ if (indexTd < tds.length) {
925
+ for (let i = indexTd; i < tds.length; i++) {
926
+ tds[i].remove();
927
+ }
928
+ }
929
+ }
930
+ }
931
+ }
932
+
933
+ balanceTables() {
934
+ for (const tableBlot of this.quill.scroll.descendants(TableMainFormat)) {
935
+ tableBlot.checkEmptyCol(this.options.autoMergeCell);
936
+ tableBlot.checkEmptyRow(this.options.autoMergeCell);
937
+ this.fixUnusuaDeletelTable(tableBlot);
938
+ }
939
+ }
940
+
941
+ listenBalanceCells() {
942
+ this.quill.on(
943
+ Quill.events.SCROLL_OPTIMIZE,
944
+ (mutations: MutationRecord[]) => {
945
+ mutations.some((mutation) => {
946
+ // TODO: if need add ['COL', 'COLGROUP']
947
+ if (['TD', 'TR', 'TBODY', 'TABLE'].includes((mutation.target as HTMLElement).tagName)) {
948
+ this.fixTableByLisenter();
949
+ return true;
950
+ }
951
+ return false;
952
+ });
953
+ for (const mutation of mutations) {
954
+ const mutationTarget = mutation.target as HTMLElement;
955
+ if (mutationTarget.tagName === 'TABLE') {
956
+ const tableMain = Quill.find(mutationTarget) as TableMainFormat;
957
+ if (tableMain) {
958
+ tableMain.sortMergeChildren();
959
+ break;
960
+ }
961
+ }
962
+ }
963
+ },
964
+ );
965
+ }
966
+
967
+ deleteTable(selectedTds: TableCellInnerFormat[]) {
968
+ if (selectedTds.length === 0) return;
969
+ const tableBlot = findParentBlot(selectedTds[0], blotName.tableMain);
970
+ tableBlot?.remove();
971
+ }
972
+
973
+ appendRow(selectedTds: TableCellInnerFormat[], isDown: boolean) {
974
+ if (selectedTds.length <= 0) return;
975
+ // find baseTd and baseTr
976
+ const baseTd = selectedTds[isDown ? selectedTds.length - 1 : 0];
977
+ const [tableBlot, baseTdParentTr] = findParentBlots(baseTd, [blotName.tableMain, blotName.tableRow] as const);
978
+ const tableTrs = tableBlot.getRows();
979
+ const i = tableTrs.indexOf(baseTdParentTr);
980
+ const insertRowIndex = i + (isDown ? baseTd.rowspan : 0);
981
+
982
+ tableBlot.insertRow(insertRowIndex);
983
+ }
984
+
985
+ appendCol(selectedTds: TableCellInnerFormat[], isRight: boolean) {
986
+ if (selectedTds.length <= 0) return;
987
+
988
+ // find insert column index in row
989
+ const [baseTd] = selectedTds.reduce((pre, cur) => {
990
+ const columnIndex = cur.getColumnIndex();
991
+ if (!isRight && columnIndex <= pre[1]) {
992
+ pre = [cur, columnIndex];
993
+ }
994
+ else if (isRight && columnIndex >= pre[1]) {
995
+ pre = [cur, columnIndex];
996
+ }
997
+ return pre;
998
+ }, [selectedTds[0], selectedTds[0].getColumnIndex()]);
999
+ const columnIndex = baseTd.getColumnIndex() + (isRight ? baseTd.colspan : 0);
1000
+
1001
+ const tableBlot = findParentBlot(baseTd, blotName.tableMain);
1002
+ const tableId = tableBlot.tableId;
1003
+ const newColId = randomId();
1004
+
1005
+ const [colgroup] = tableBlot.descendants(TableColgroupFormat);
1006
+ if (colgroup) {
1007
+ colgroup.insertColByIndex(columnIndex, {
1008
+ tableId,
1009
+ colId: newColId,
1010
+ width: tableBlot.full ? 6 : 160,
1011
+ full: tableBlot.full,
1012
+ });
1013
+ }
1014
+
1015
+ // loop tr and insert cell at index
1016
+ // if index is inner cell, skip next `rowspan` line
1017
+ // if there are cells both have column span and row span before index cell, minus `colspan` cell for next line
1018
+ const trs = tableBlot.getRows();
1019
+ const spanCols: number[] = [];
1020
+ let skipRowNum = 0;
1021
+ for (const tr of Object.values(trs)) {
1022
+ const spanCol = spanCols.shift() || 0;
1023
+ if (skipRowNum > 0) {
1024
+ skipRowNum -= 1;
1025
+ continue;
1026
+ }
1027
+ const nextSpanCols = tr.insertCell(columnIndex - spanCol, {
1028
+ tableId,
1029
+ rowId: tr.rowId,
1030
+ colId: newColId,
1031
+ rowspan: 1,
1032
+ colspan: 1,
1033
+ });
1034
+ if (nextSpanCols.skipRowNum) {
1035
+ skipRowNum += nextSpanCols.skipRowNum;
1036
+ }
1037
+ for (const [i, n] of nextSpanCols.entries()) {
1038
+ spanCols[i] = (spanCols[i] || 0) + n;
1039
+ }
1040
+ }
1041
+ }
1042
+
1043
+ /**
1044
+ * after insert or remove cell. handle cell colspan and rowspan merge
1045
+ */
1046
+ fixTableByRemove(tableBlot: TableMainFormat) {
1047
+ if (!this.options.autoMergeCell) return;
1048
+ // calculate all cells
1049
+ // maybe will get empty tr
1050
+ const trBlots = tableBlot.getRows();
1051
+ const tableCols = tableBlot.getCols();
1052
+ const colIdMap = tableCols.reduce((idMap, col) => {
1053
+ idMap[col.colId] = 0;
1054
+ return idMap;
1055
+ }, {} as Record<string, number>);
1056
+ // merge rowspan
1057
+ const reverseTrBlots = trBlots.toReversed();
1058
+ const removeTr: number[] = [];
1059
+ for (const [index, tr] of reverseTrBlots.entries()) {
1060
+ const i = trBlots.length - index - 1;
1061
+ if (tr.children.length <= 0) {
1062
+ removeTr.push(i);
1063
+ }
1064
+ else {
1065
+ // if have td rowspan across empty tr. minus rowspan
1066
+ tr.foreachCellInner((td) => {
1067
+ const sum = removeTr.reduce((sum, val) => td.rowspan + i > val ? sum + 1 : sum, 0);
1068
+ td.rowspan -= sum;
1069
+ // count exist col
1070
+ colIdMap[td.colId] += 1;
1071
+ });
1072
+ }
1073
+ }
1074
+ // merge colspan
1075
+ let index = 0;
1076
+ for (const count of Object.values(colIdMap)) {
1077
+ if (count === 0) {
1078
+ const spanCols: number[] = [];
1079
+ let skipRowNum = 0;
1080
+ for (const tr of Object.values(trBlots)) {
1081
+ const spanCol = spanCols.shift() || 0;
1082
+ let nextSpanCols = [];
1083
+ if (skipRowNum > 0) {
1084
+ nextSpanCols = tr.getCellByColumIndex(index - spanCol)[2];
1085
+ skipRowNum -= 1;
1086
+ }
1087
+ else {
1088
+ nextSpanCols = tr.removeCell(index - spanCol);
1089
+ if (nextSpanCols.skipRowNum) {
1090
+ skipRowNum += nextSpanCols.skipRowNum;
1091
+ }
1092
+ }
1093
+ for (const [i, n] of nextSpanCols.entries()) {
1094
+ spanCols[i] = (spanCols[i] || 0) + n;
1095
+ }
1096
+ }
1097
+ }
1098
+ else {
1099
+ index += 1;
1100
+ }
1101
+ }
1102
+ // remove col
1103
+ for (const col of tableCols) {
1104
+ if (colIdMap[col.colId] === 0) {
1105
+ if (col.prev) {
1106
+ (col.prev as TableColFormat).width += col.width;
1107
+ }
1108
+ else if (col.next) {
1109
+ (col.next as TableColFormat).width += col.width;
1110
+ }
1111
+ col.remove();
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ removeRow(selectedTds: TableCellInnerFormat[]) {
1117
+ if (selectedTds.length <= 0) return;
1118
+ const baseTd = selectedTds[0];
1119
+ const tableBlot = findParentBlot(baseTd, blotName.tableMain);
1120
+ const trs = tableBlot.getRows();
1121
+ let endTrIndex = trs.length;
1122
+ let nextTrIndex = -1;
1123
+ for (const td of selectedTds) {
1124
+ const tr = findParentBlot(td, blotName.tableRow);
1125
+ const index = trs.indexOf(tr);
1126
+ if (index < endTrIndex) {
1127
+ endTrIndex = index;
1128
+ }
1129
+ if (index + td.rowspan > nextTrIndex) {
1130
+ nextTrIndex = index + td.rowspan;
1131
+ }
1132
+ }
1133
+
1134
+ const patchTds: Record<string, {
1135
+ rowspan: number;
1136
+ colspan: number;
1137
+ colIndex: number;
1138
+ }> = {};
1139
+ for (let i = endTrIndex; i < Math.min(trs.length, nextTrIndex); i++) {
1140
+ const tr = trs[i];
1141
+ tr.foreachCellInner((td) => {
1142
+ // find cells in rowspan that exceed the deletion range
1143
+ if (td.rowspan + i > nextTrIndex) {
1144
+ patchTds[td.colId] = {
1145
+ rowspan: td.rowspan + i - nextTrIndex,
1146
+ colspan: td.colspan,
1147
+ colIndex: td.getColumnIndex(),
1148
+ };
1149
+ }
1150
+ // only remove td. empty tr to calculate colspan and rowspan
1151
+ td.parent.remove();
1152
+ });
1153
+ if (tr.length() === 0) tr.remove();
1154
+ }
1155
+
1156
+ if (trs[nextTrIndex]) {
1157
+ const nextTr = trs[nextTrIndex];
1158
+ const tableId = tableBlot.tableId;
1159
+ // insert cell in nextTr to patch exceed cell
1160
+ for (const [colId, { colIndex, colspan, rowspan }] of Object.entries(patchTds)) {
1161
+ nextTr.insertCell(colIndex, {
1162
+ tableId,
1163
+ rowId: nextTr.rowId,
1164
+ colId,
1165
+ colspan,
1166
+ rowspan,
1167
+ });
1168
+ }
1169
+ }
1170
+
1171
+ this.fixTableByRemove(tableBlot);
1172
+ }
1173
+
1174
+ removeCol(selectedTds: TableCellInnerFormat[]) {
1175
+ if (selectedTds.length <= 0) return;
1176
+ const baseTd = selectedTds[0];
1177
+ const tableBlot = findParentBlot(baseTd, blotName.tableMain);
1178
+ const colspanMap: Record<string, number> = {};
1179
+ for (const td of selectedTds) {
1180
+ if (!colspanMap[td.rowId]) colspanMap[td.rowId] = 0;
1181
+ colspanMap[td.rowId] += td.colspan;
1182
+ }
1183
+ const colspanCount = Math.max(...Object.values(colspanMap));
1184
+ const columnIndex = baseTd.getColumnIndex();
1185
+
1186
+ const trs = tableBlot.descendants(TableRowFormat);
1187
+ for (let i = 0; i < colspanCount; i++) {
1188
+ const spanCols: number[] = [];
1189
+ let skipRowNum = 0;
1190
+ for (const tr of Object.values(trs)) {
1191
+ const spanCol = spanCols.shift() || 0;
1192
+ if (skipRowNum > 0) {
1193
+ skipRowNum -= 1;
1194
+ continue;
1195
+ }
1196
+ const nextSpanCols = tr.removeCell(columnIndex - spanCol);
1197
+ if (nextSpanCols.skipRowNum) {
1198
+ skipRowNum += nextSpanCols.skipRowNum;
1199
+ }
1200
+ for (const [i, n] of nextSpanCols.entries()) {
1201
+ spanCols[i] = (spanCols[i] || 0) + n;
1202
+ }
1203
+ }
1204
+ }
1205
+ // delete col need after remove cell. remove cell need all column id
1206
+ // manual delete col. use fixTableByRemove to delete col will delete extra cells
1207
+ const [colgroup] = tableBlot.descendants(TableColgroupFormat);
1208
+ if (colgroup) {
1209
+ for (let i = 0; i < colspanCount; i++) {
1210
+ colgroup.removeColByIndex(columnIndex);
1211
+ }
1212
+ }
1213
+
1214
+ this.fixTableByRemove(tableBlot);
1215
+ }
1216
+
1217
+ mergeCells(selectedTds: TableCellInnerFormat[]) {
1218
+ if (selectedTds.length <= 1) return;
1219
+ const baseCell = selectedTds[0];
1220
+ // move selected cells in same table body
1221
+ const baseCellBody = baseCell.getTableBody();
1222
+ // insert base row
1223
+ let baseRow = baseCell.getTableRow();
1224
+ if (!baseCellBody || !baseRow) return;
1225
+ for (let i = 1; i < selectedTds.length; i++) {
1226
+ const selectTd = selectedTds[i];
1227
+ const currentTdBody = selectTd.getTableBody();
1228
+ if (currentTdBody && currentTdBody !== baseCellBody) {
1229
+ const currentRow = selectTd.getTableRow();
1230
+ if (currentRow) {
1231
+ baseRow.parent.insertBefore(currentRow, baseRow.next);
1232
+ baseRow = currentRow;
1233
+ }
1234
+ }
1235
+ }
1236
+ baseCellBody.convertBody(baseCell.wrapTag);
1237
+
1238
+ const counts = selectedTds.reduce(
1239
+ (pre, selectTd, index) => {
1240
+ // count column span
1241
+ const colId = selectTd.colId;
1242
+ if (!pre[0][colId]) pre[0][colId] = 0;
1243
+ pre[0][colId] += selectTd.rowspan;
1244
+ // count row span
1245
+ const rowId = selectTd.rowId;
1246
+ if (!pre[1][rowId]) pre[1][rowId] = 0;
1247
+ pre[1][rowId] += selectTd.colspan;
1248
+ // merge select cell
1249
+ if (index !== 0) {
1250
+ selectTd.moveChildren(pre[2]);
1251
+ selectTd.parent.remove();
1252
+ }
1253
+ return pre;
1254
+ },
1255
+ [{} as Record<string, number>, {} as Record<string, number>, baseCell] as const,
1256
+ );
1257
+
1258
+ const rowCount = Math.max(...Object.values(counts[0]));
1259
+ const colCount = Math.max(...Object.values(counts[1]));
1260
+ const baseTd = counts[2];
1261
+ baseTd.colspan = colCount;
1262
+ baseTd.rowspan = rowCount;
1263
+
1264
+ // selection will move with cursor. make sure selection is in baseTd
1265
+ const index = this.quill.getIndex(baseTd);
1266
+ this.quill.setSelection({ index, length: 0 }, Quill.sources.SILENT);
1267
+
1268
+ const tableBlot = findParentBlot(baseTd, blotName.tableMain);
1269
+ this.fixTableByRemove(tableBlot);
1270
+ }
1271
+
1272
+ splitCell(selectedTds: TableCellInnerFormat[]) {
1273
+ if (selectedTds.length !== 1) return;
1274
+ const baseCell = selectedTds[0];
1275
+ if (baseCell.colspan === 1 && baseCell.rowspan === 1) return;
1276
+ const [tableBlot, baseTr] = findParentBlots(baseCell, [blotName.tableMain, blotName.tableRow] as const);
1277
+ const rows = tableBlot.getRows();
1278
+ const tableId = tableBlot.tableId;
1279
+ const colIndex = baseCell.getColumnIndex();
1280
+ const colIds = tableBlot.getColIds().slice(colIndex, colIndex + baseCell.colspan).toReversed();
1281
+ const baseCellValue = baseCell.formats()[blotName.tableCellInner] as TableCellValue;
1282
+ const { emptyRow, ...extendsBaseCellValue } = baseCellValue;
1283
+
1284
+ let rowIndex = rows.indexOf(baseTr);
1285
+ if (rowIndex === -1) return;
1286
+ let curTr = rows[rowIndex];
1287
+ let rowspan = baseCell.rowspan;
1288
+ // reset span first. insertCell need colspan to judge insert position
1289
+ baseCell.colspan = 1;
1290
+ baseCell.rowspan = 1;
1291
+ while (curTr && rowspan > 0) {
1292
+ for (const id of colIds) {
1293
+ // keep baseCell. baseTr should insert at baseCell's column index + 1
1294
+ if (curTr === baseTr && id === baseCell.colId) continue;
1295
+ curTr.insertCell(
1296
+ colIndex + (curTr === baseTr ? 1 : 0),
1297
+ {
1298
+ ...extendsBaseCellValue,
1299
+ tableId,
1300
+ rowId: curTr.rowId,
1301
+ colId: id,
1302
+ rowspan: 1,
1303
+ colspan: 1,
1304
+ },
1305
+ );
1306
+ }
1307
+
1308
+ rowspan -= 1;
1309
+ rowIndex += 1;
1310
+ curTr = rows[rowIndex];
1311
+ }
1312
+ }
1313
+
1314
+ convertTableBodyByCells(tableBlot: TableMainFormat, selecteds: TableCellInnerFormat[], tag: TableBodyTag) {
1315
+ let firstRowIndex: number | undefined;
1316
+ let lastRowIndex: number | undefined;
1317
+ const rows = tableBlot.getRows();
1318
+ for (const cell of selecteds) {
1319
+ const row = cell.getTableRow();
1320
+ if (!row) continue;
1321
+ const index = rows.indexOf(row);
1322
+ if (isUndefined(firstRowIndex)) {
1323
+ firstRowIndex = index;
1324
+ }
1325
+ if (isUndefined(lastRowIndex)) {
1326
+ lastRowIndex = index;
1327
+ }
1328
+
1329
+ if (index < firstRowIndex) {
1330
+ lastRowIndex = firstRowIndex;
1331
+ firstRowIndex = index;
1332
+ }
1333
+ else if (index > lastRowIndex) {
1334
+ lastRowIndex = index;
1335
+ }
1336
+ }
1337
+ if (isUndefined(firstRowIndex) || isUndefined(lastRowIndex)) {
1338
+ console.warn('TableRow not found');
1339
+ return;
1340
+ }
1341
+ const firstRow = rows[firstRowIndex];
1342
+ const lastRow = rows[lastRowIndex];
1343
+ tableBlot.split(lastRow.offset(tableBlot) + lastRow.length());
1344
+ const currentTable = tableBlot.split(firstRow.offset(tableBlot)) as TableMainFormat;
1345
+ // selecteds may in different bodys
1346
+ // create a new body, insert to current table, move all rows to new body, then call new body's convertBody
1347
+ const currentTableRows = currentTable.getRows();
1348
+ const [firstBody] = currentTable.getBodys();
1349
+ const newBody = firstBody.clone() as TableBodyFormat;
1350
+ currentTable.appendChild(newBody);
1351
+ for (const row of currentTableRows) {
1352
+ // only move the not empty row. the empty row will regenerate when `optimize`
1353
+ if (row.length() > 0) {
1354
+ newBody.appendChild(row);
1355
+ }
1356
+ }
1357
+ newBody.convertBody(tag);
1358
+ firstBody.remove();
1359
+ }
1360
+ }