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.
- package/README.md +7 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +168 -146
- package/dist/index.js +47 -47
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +52 -52
- package/dist/index.umd.js.map +1 -1
- package/package.json +22 -24
- package/src/__tests__/e2e/custom-creator.test.ts +44 -44
- package/src/__tests__/e2e/editor-page.ts +77 -77
- package/src/__tests__/e2e/table-align.test.ts +104 -104
- package/src/__tests__/e2e/table-blots.test.ts +169 -169
- package/src/__tests__/e2e/table-caption.test.ts +134 -134
- package/src/__tests__/e2e/table-clipboard.test.ts +20 -20
- package/src/__tests__/e2e/table-hack.test.ts +151 -151
- package/src/__tests__/e2e/table-keyboard-handler.test.ts +12 -3
- package/src/__tests__/e2e/table-menu.test.ts +172 -172
- package/src/__tests__/e2e/table-resize.test.ts +654 -9
- package/src/__tests__/e2e/table-scrollbar.test.ts +144 -144
- package/src/__tests__/e2e/table-selection.test.ts +563 -563
- package/src/__tests__/e2e/types.d.ts +7 -7
- package/src/__tests__/e2e/utils.ts +52 -52
- package/src/__tests__/unit/table-blots.test.ts +720 -720
- package/src/__tests__/unit/table-caption.test.ts +234 -234
- package/src/__tests__/unit/table-cell-merge.test.ts +724 -724
- package/src/__tests__/unit/table-clipboard.test.ts +2176 -2176
- package/src/__tests__/unit/table-hack.test.ts +1014 -1014
- package/src/__tests__/unit/table-insert.test.ts +926 -926
- package/src/__tests__/unit/table-redo-undo.test.ts +2429 -2429
- package/src/__tests__/unit/table-remove.test.ts +343 -343
- package/src/__tests__/unit/utils.test-d.ts +49 -49
- package/src/__tests__/unit/utils.test.ts +711 -711
- package/src/__tests__/unit/utils.ts +307 -307
- package/src/__tests__/unit/vitest.d.ts +14 -14
- package/src/formats/container-format.ts +107 -107
- package/src/formats/overrides/block-embed.ts +72 -72
- package/src/formats/overrides/block.ts +95 -95
- package/src/formats/overrides/index.ts +3 -3
- package/src/formats/overrides/scroll.ts +70 -70
- package/src/formats/table-body-format.ts +52 -52
- package/src/formats/table-caption-format.ts +116 -116
- package/src/formats/table-cell-format.ts +304 -304
- package/src/formats/table-cell-inner-format.ts +403 -398
- package/src/formats/table-colgroup-format.ts +136 -136
- package/src/formats/table-foot-format.ts +7 -7
- package/src/formats/table-head-format.ts +7 -7
- package/src/formats/table-main-format.ts +1 -1
- package/src/formats/table-row-format.ts +218 -210
- package/src/formats/utils.ts +6 -6
- package/src/index.ts +19 -19
- package/src/modules/index.ts +7 -7
- package/src/modules/table-align.ts +131 -131
- package/src/modules/table-clipboard/table-clipboard.ts +6 -8
- package/src/modules/table-dom-selector.ts +33 -33
- package/src/modules/table-menu/constants.ts +223 -223
- package/src/modules/table-menu/index.ts +4 -4
- package/src/modules/table-menu/table-menu-common.ts +330 -329
- package/src/modules/table-menu/table-menu-contextmenu.ts +111 -118
- package/src/modules/table-menu/table-menu-select.ts +96 -94
- package/src/modules/table-resize/index.ts +5 -5
- package/src/modules/table-resize/table-resize-box.ts +714 -363
- package/src/modules/table-resize/table-resize-common.ts +246 -382
- package/src/modules/table-resize/table-resize-drag.ts +241 -0
- package/src/modules/table-resize/table-resize-line.ts +244 -182
- package/src/modules/table-resize/table-resize-scale.ts +174 -173
- package/src/modules/table-resize/utils.ts +84 -3
- package/src/modules/table-scrollbar.ts +292 -292
- package/src/modules/table-selection.ts +613 -669
- package/src/style/button.less +45 -45
- package/src/style/color-picker.less +136 -136
- package/src/style/dialog.less +53 -53
- package/src/style/functions.less +9 -9
- package/src/style/index.less +120 -120
- package/src/style/input.less +64 -64
- package/src/style/select-box.less +52 -52
- package/src/style/table-creator.less +56 -56
- package/src/style/table-menu.less +125 -125
- package/src/style/table-resize-scale.less +31 -31
- package/src/style/table-resize.less +249 -202
- package/src/style/table-scrollbar.less +49 -49
- package/src/style/table-selection.less +23 -23
- package/src/style/tooltip.less +19 -19
- package/src/style/variables.less +1 -1
- package/src/svg/arrow-up-down.svg +11 -11
- package/src/svg/convert-cell.svg +7 -7
- package/src/table-up.ts +1360 -1360
- package/src/types.d.ts +4 -4
- package/src/utils/bem.ts +23 -23
- package/src/utils/blot-helper.ts +101 -105
- package/src/utils/color.ts +109 -109
- package/src/utils/components/button.ts +22 -22
- package/src/utils/components/color-picker.ts +236 -236
- package/src/utils/components/dialog.ts +83 -41
- package/src/utils/components/index.ts +6 -6
- package/src/utils/components/input.ts +74 -74
- package/src/utils/components/table/creator.ts +89 -89
- package/src/utils/components/table/index.ts +2 -2
- package/src/utils/components/table/select-box.ts +78 -78
- package/src/utils/components/tooltip.ts +179 -189
- package/src/utils/constants.ts +125 -124
- package/src/utils/drag-helper.ts +112 -0
- package/src/utils/index.ts +15 -14
- package/src/utils/is.ts +9 -9
- package/src/utils/position.ts +60 -60
- package/src/utils/resize-observer-helper.ts +47 -47
- package/src/utils/scroll.ts +145 -47
- package/src/utils/style-helper.ts +47 -47
- package/src/utils/transformer.ts +10 -10
- package/src/utils/transition-event-helper.ts +8 -8
- package/src/utils/types.ts +156 -157
- 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>(
|
|
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
|
|
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>(
|
|
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
|
|
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
|
+
}
|