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