quill-table-up 3.1.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +7 -0
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.ts +168 -146
  4. package/dist/index.js +47 -47
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.umd.js +52 -52
  7. package/dist/index.umd.js.map +1 -1
  8. package/package.json +22 -24
  9. package/src/__tests__/e2e/custom-creator.test.ts +44 -44
  10. package/src/__tests__/e2e/editor-page.ts +77 -77
  11. package/src/__tests__/e2e/table-align.test.ts +104 -104
  12. package/src/__tests__/e2e/table-blots.test.ts +169 -169
  13. package/src/__tests__/e2e/table-caption.test.ts +134 -134
  14. package/src/__tests__/e2e/table-clipboard.test.ts +20 -20
  15. package/src/__tests__/e2e/table-hack.test.ts +151 -151
  16. package/src/__tests__/e2e/table-keyboard-handler.test.ts +12 -3
  17. package/src/__tests__/e2e/table-menu.test.ts +172 -172
  18. package/src/__tests__/e2e/table-resize.test.ts +654 -9
  19. package/src/__tests__/e2e/table-scrollbar.test.ts +144 -144
  20. package/src/__tests__/e2e/table-selection.test.ts +563 -563
  21. package/src/__tests__/e2e/types.d.ts +7 -7
  22. package/src/__tests__/e2e/utils.ts +52 -52
  23. package/src/__tests__/unit/table-blots.test.ts +720 -720
  24. package/src/__tests__/unit/table-caption.test.ts +234 -234
  25. package/src/__tests__/unit/table-cell-merge.test.ts +724 -724
  26. package/src/__tests__/unit/table-clipboard.test.ts +2176 -2176
  27. package/src/__tests__/unit/table-hack.test.ts +1014 -1014
  28. package/src/__tests__/unit/table-insert.test.ts +926 -926
  29. package/src/__tests__/unit/table-redo-undo.test.ts +2429 -2429
  30. package/src/__tests__/unit/table-remove.test.ts +343 -343
  31. package/src/__tests__/unit/utils.test-d.ts +49 -49
  32. package/src/__tests__/unit/utils.test.ts +711 -711
  33. package/src/__tests__/unit/utils.ts +307 -307
  34. package/src/__tests__/unit/vitest.d.ts +14 -14
  35. package/src/formats/container-format.ts +107 -107
  36. package/src/formats/overrides/block-embed.ts +72 -72
  37. package/src/formats/overrides/block.ts +95 -95
  38. package/src/formats/overrides/index.ts +3 -3
  39. package/src/formats/overrides/scroll.ts +70 -70
  40. package/src/formats/table-body-format.ts +52 -52
  41. package/src/formats/table-caption-format.ts +116 -116
  42. package/src/formats/table-cell-format.ts +304 -304
  43. package/src/formats/table-cell-inner-format.ts +403 -398
  44. package/src/formats/table-colgroup-format.ts +136 -136
  45. package/src/formats/table-foot-format.ts +7 -7
  46. package/src/formats/table-head-format.ts +7 -7
  47. package/src/formats/table-main-format.ts +1 -1
  48. package/src/formats/table-row-format.ts +218 -210
  49. package/src/formats/utils.ts +6 -6
  50. package/src/index.ts +19 -19
  51. package/src/modules/index.ts +7 -7
  52. package/src/modules/table-align.ts +131 -131
  53. package/src/modules/table-clipboard/table-clipboard.ts +6 -8
  54. package/src/modules/table-dom-selector.ts +33 -33
  55. package/src/modules/table-menu/constants.ts +223 -223
  56. package/src/modules/table-menu/index.ts +4 -4
  57. package/src/modules/table-menu/table-menu-common.ts +330 -329
  58. package/src/modules/table-menu/table-menu-contextmenu.ts +111 -118
  59. package/src/modules/table-menu/table-menu-select.ts +96 -94
  60. package/src/modules/table-resize/index.ts +5 -5
  61. package/src/modules/table-resize/table-resize-box.ts +714 -363
  62. package/src/modules/table-resize/table-resize-common.ts +246 -382
  63. package/src/modules/table-resize/table-resize-drag.ts +241 -0
  64. package/src/modules/table-resize/table-resize-line.ts +244 -182
  65. package/src/modules/table-resize/table-resize-scale.ts +174 -173
  66. package/src/modules/table-resize/utils.ts +84 -3
  67. package/src/modules/table-scrollbar.ts +292 -292
  68. package/src/modules/table-selection.ts +613 -669
  69. package/src/style/button.less +45 -45
  70. package/src/style/color-picker.less +136 -136
  71. package/src/style/dialog.less +53 -53
  72. package/src/style/functions.less +9 -9
  73. package/src/style/index.less +120 -120
  74. package/src/style/input.less +64 -64
  75. package/src/style/select-box.less +52 -52
  76. package/src/style/table-creator.less +56 -56
  77. package/src/style/table-menu.less +125 -125
  78. package/src/style/table-resize-scale.less +31 -31
  79. package/src/style/table-resize.less +249 -202
  80. package/src/style/table-scrollbar.less +49 -49
  81. package/src/style/table-selection.less +23 -23
  82. package/src/style/tooltip.less +19 -19
  83. package/src/style/variables.less +1 -1
  84. package/src/svg/arrow-up-down.svg +11 -11
  85. package/src/svg/convert-cell.svg +7 -7
  86. package/src/table-up.ts +1360 -1360
  87. package/src/types.d.ts +4 -4
  88. package/src/utils/bem.ts +23 -23
  89. package/src/utils/blot-helper.ts +101 -105
  90. package/src/utils/color.ts +109 -109
  91. package/src/utils/components/button.ts +22 -22
  92. package/src/utils/components/color-picker.ts +236 -236
  93. package/src/utils/components/dialog.ts +83 -41
  94. package/src/utils/components/index.ts +6 -6
  95. package/src/utils/components/input.ts +74 -74
  96. package/src/utils/components/table/creator.ts +89 -89
  97. package/src/utils/components/table/index.ts +2 -2
  98. package/src/utils/components/table/select-box.ts +78 -78
  99. package/src/utils/components/tooltip.ts +179 -189
  100. package/src/utils/constants.ts +125 -124
  101. package/src/utils/drag-helper.ts +112 -0
  102. package/src/utils/index.ts +15 -14
  103. package/src/utils/is.ts +9 -9
  104. package/src/utils/position.ts +60 -60
  105. package/src/utils/resize-observer-helper.ts +47 -47
  106. package/src/utils/scroll.ts +145 -47
  107. package/src/utils/style-helper.ts +47 -47
  108. package/src/utils/transformer.ts +10 -10
  109. package/src/utils/transition-event-helper.ts +8 -8
  110. package/src/utils/types.ts +156 -157
  111. package/src/utils/utils.ts +12 -12
@@ -1,669 +1,613 @@
1
- import type { EmitterSource, Parchment as TypeParchment, Range as TypeRange } from 'quill';
2
- import type { TableMainFormat, TableWrapperFormat } from '../formats';
3
- import type { TableUp } from '../table-up';
4
- import type { RelactiveRect, TableSelectionOptions } from '../utils';
5
- import Quill from 'quill';
6
- import { getTableMainRect, TableCellFormat, TableCellInnerFormat } from '../formats';
7
- import { addScrollEvent, blotName, clearScrollEvent, createBEM, createResizeObserver, findAllParentBlot, findParentBlot, getElementScrollPosition, getRelativeRect, isRectanglesIntersect, tableUpEvent } from '../utils';
8
- import { pasteCells } from './table-clipboard';
9
- import { TableDomSelector } from './table-dom-selector';
10
- import { copyCell } from './table-menu/constants';
11
-
12
- const ERROR_LIMIT = 0;
13
-
14
- export interface SelectionData {
15
- anchorNode: Node | null;
16
- anchorOffset: number;
17
- focusNode: Node | null;
18
- focusOffset: number;
19
- }
20
-
21
- export class TableSelection extends TableDomSelector {
22
- static moduleName: string = 'table-selection';
23
-
24
- options: TableSelectionOptions;
25
- boundary: RelactiveRect | null = null;
26
- scrollRecordEls: HTMLElement[] = [];
27
- startScrollRecordPosition: { x: number; y: number }[] = [];
28
- selectedTableScrollX: number = 0;
29
- selectedTableScrollY: number = 0;
30
- selectedEditorScrollX: number = 0;
31
- selectedEditorScrollY: number = 0;
32
- selectedTds: TableCellInnerFormat[] = [];
33
- cellSelectWrap: HTMLElement;
34
- cellSelect: HTMLElement;
35
- scrollHandler: [HTMLElement, (...args: any[]) => void][] = [];
36
- resizeObserver: ResizeObserver;
37
- isDisplaySelection = false;
38
- bem = createBEM('selection');
39
- lastSelection: SelectionData = {
40
- anchorNode: null,
41
- anchorOffset: 0,
42
- focusNode: null,
43
- focusOffset: 0,
44
- };
45
-
46
- _dragging: boolean = false;
47
- set dragging(val: boolean) {
48
- if (this._dragging === val) return;
49
- this._dragging = val;
50
- this.quill.emitter.emit(val ? tableUpEvent.TABLE_SELECTION_DRAG_START : tableUpEvent.TABLE_SELECTION_DRAG_END, this);
51
- }
52
-
53
- get dragging() {
54
- return this._dragging;
55
- }
56
-
57
- constructor(public tableModule: TableUp, public quill: Quill, options: Partial<TableSelectionOptions> = {}) {
58
- super(tableModule, quill);
59
- this.options = this.resolveOptions(options);
60
- this.scrollRecordEls = [this.quill.root, document.documentElement];
61
-
62
- this.cellSelectWrap = tableModule.addContainer(this.bem.b());
63
- this.cellSelect = this.helpLinesInitial();
64
-
65
- this.resizeObserver = createResizeObserver(this.updateAfterEvent, { ignoreFirstBind: true });
66
- this.resizeObserver.observe(this.quill.root);
67
-
68
- document.addEventListener('paste', this.handlePaste);
69
- this.quill.emitter.listenDOM('selectionchange', document, this.selectionChangeHandler.bind(this));
70
- this.quill.on(tableUpEvent.AFTER_TABLE_RESIZE, this.updateAfterEvent);
71
- this.quill.on(Quill.events.SELECTION_CHANGE, this.quillSelectionChangeHandler);
72
- this.quill.on(Quill.events.EDITOR_CHANGE, this.updateWhenTextChange);
73
- this.hide();
74
- }
75
-
76
- handlePaste = (event: ClipboardEvent) => {
77
- const activeElement = document.activeElement && this.quill.root.contains(document.activeElement);
78
- if (!activeElement || this.quill.getSelection()) return;
79
-
80
- const clipboardData = event.clipboardData;
81
- if (!clipboardData) return;
82
- event.preventDefault();
83
-
84
- const currentSelectedTds = this.selectedTds;
85
- if (currentSelectedTds.length <= 0) return;
86
-
87
- const html = clipboardData.getData('text/html');
88
- const delta = this.quill.clipboard.convert({ html }).ops.filter(op => op.attributes && op.attributes[blotName.tableCellInner]);
89
-
90
- if (delta.length === 0) return;
91
-
92
- pasteCells(
93
- { quill: this.quill, talbeModule: this.tableModule },
94
- currentSelectedTds,
95
- delta,
96
- );
97
- };
98
-
99
- keyboardHandler = async (e: KeyboardEvent) => {
100
- if (e.ctrlKey) {
101
- switch (e.key) {
102
- case 'c':
103
- case 'x': {
104
- await copyCell(this.tableModule, this.selectedTds, e.key === 'x');
105
- break;
106
- }
107
- }
108
- }
109
- else if (e.key === 'Backspace' || e.key === 'Delete') {
110
- this.removeCellBySelectedTds();
111
- }
112
- };
113
-
114
- updateWhenTextChange = (eventName: string) => {
115
- if (eventName === Quill.events.TEXT_CHANGE) {
116
- if (this.table && !this.quill.root.contains(this.table)) {
117
- this.setSelectionTable(undefined);
118
- }
119
- else {
120
- this.updateAfterEvent();
121
- }
122
- }
123
- };
124
-
125
- updateAfterEvent = () => {
126
- // if any cell doesn't exist, selection will be cleared
127
- for (let i = 0; i < this.selectedTds.length; i++) {
128
- const td = this.selectedTds[i];
129
- if (!td.domNode.isConnected) {
130
- this.selectedTds = [];
131
- break;
132
- }
133
- }
134
- this.updateWithSelectedTds();
135
- };
136
-
137
- removeCellBySelectedTds() {
138
- const range = this.quill.getSelection();
139
- const activeElement = document.activeElement;
140
- if (range || !this.quill.root.contains(activeElement)) return;
141
-
142
- if (this.table) {
143
- const tableMain = Quill.find(this.table) as TableMainFormat;
144
- const cells = tableMain.descendants(TableCellInnerFormat);
145
- if (this.selectedTds.length === cells.length) {
146
- tableMain.remove();
147
- return;
148
- }
149
- }
150
- for (const td of this.selectedTds) {
151
- const clearTd = td.clone() as TypeParchment.Parent;
152
- clearTd.appendChild(td.scroll.create('block'));
153
- td.parent.insertBefore(clearTd, td);
154
- td.remove();
155
- }
156
- }
157
-
158
- setSelectedTds(tds: TableCellInnerFormat[]) {
159
- const currentSelectedTds = new Set(this.selectedTds);
160
- const isSame = this.selectedTds.length === tds.length && tds.every(td => currentSelectedTds.has(td));
161
-
162
- this.selectedTds = tds;
163
- if (!isSame) {
164
- this.quill.emitter.emit(tableUpEvent.TABLE_SELECTION_CHANGE, this, this.selectedTds);
165
- }
166
- }
167
-
168
- getFirstTextNode(dom: HTMLElement | Node): Node {
169
- for (const node of Array.from(dom.childNodes)) {
170
- if (node.nodeType === Node.TEXT_NODE) {
171
- return node;
172
- }
173
- }
174
- return dom;
175
- }
176
-
177
- getLastTextNode(dom: HTMLElement | Node): Node {
178
- for (let i = dom.childNodes.length - 1; i >= 0; i--) {
179
- const node = dom.childNodes[i];
180
- if (node.nodeType === Node.TEXT_NODE) {
181
- return node;
182
- }
183
- }
184
- return dom;
185
- }
186
-
187
- getNodeTailOffset(node: Node) {
188
- const tempRange = document.createRange();
189
- tempRange.selectNodeContents(node);
190
- tempRange.collapse(false);
191
- return tempRange.startOffset;
192
- }
193
-
194
- quillSelectionChangeHandler = (range: TypeRange | null, _oldRange: TypeRange | null, source: EmitterSource) => {
195
- if (source === Quill.sources.API) return;
196
- if (range && !this.quill.composition.isComposing && this.selectedTds.length > 0) {
197
- const formats = this.quill.getFormat(range);
198
- const [line] = this.quill.getLine(range.index);
199
- const isInCell = !!formats[blotName.tableCellInner] && !!line;
200
- // if the selection is in the cell inner, should not update
201
- const containsLine = line && this.selectedTds.some(td => td.domNode.contains(line.domNode));
202
-
203
- if (isInCell && !containsLine) {
204
- try {
205
- const cellInner = findParentBlot(line!, blotName.tableCellInner) as TableCellInnerFormat;
206
- this.setSelectedTds([cellInner]);
207
- this.updateWithSelectedTds();
208
- }
209
- catch {
210
- // do nothing. should not into here
211
- }
212
- }
213
- else if (!(isInCell && containsLine)) {
214
- this.hide();
215
- }
216
- }
217
- };
218
-
219
- setSelectionData(selection: Selection, selectionData: SelectionData) {
220
- const { anchorNode, anchorOffset, focusNode, focusOffset } = selectionData;
221
- if (!anchorNode || !focusNode) return;
222
- const range = document.createRange();
223
- const isUpFromDown = this.selectionDirectionUp(selectionData);
224
- if (isUpFromDown) {
225
- range.setStart(anchorNode, anchorOffset);
226
- range.setEnd(anchorNode, anchorOffset);
227
- }
228
- else {
229
- range.setStart(anchorNode, anchorOffset);
230
- range.setEnd(focusNode, focusOffset);
231
- }
232
- selection.removeAllRanges();
233
- selection.addRange(range);
234
- if (isUpFromDown) {
235
- selection.extend(focusNode, focusOffset);
236
- }
237
- }
238
-
239
- selectionDirectionUp(selection: SelectionData) {
240
- const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;
241
- if (!anchorNode || !focusNode) return false;
242
-
243
- if (anchorNode === focusNode) {
244
- return anchorOffset > focusOffset;
245
- }
246
-
247
- const nodePosition = anchorNode.compareDocumentPosition(focusNode);
248
- // focus contains anchor
249
- if (nodePosition & Node.DOCUMENT_POSITION_CONTAINS) {
250
- // is anchor before focus
251
- return (nodePosition & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
252
- }
253
-
254
- // anchor contains focus
255
- if (nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY) {
256
- // is focus before anchor
257
- return (nodePosition & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
258
- }
259
-
260
- // compare position
261
- return (nodePosition & Node.DOCUMENT_POSITION_PRECEDING) !== 0;
262
- }
263
-
264
- findWrapSelection(points: { node: Node | null; offset: number }[]) {
265
- let startNode: Node | null = null;
266
- let startOffset = 0;
267
- let endNode: Node | null = null;
268
- let endOffset = 0;
269
-
270
- for (const { node, offset } of points) {
271
- if (node) {
272
- if (
273
- !startNode
274
- || this.selectionDirectionUp({
275
- anchorNode: startNode,
276
- anchorOffset: startOffset,
277
- focusNode: node,
278
- focusOffset: offset,
279
- })
280
- ) {
281
- startNode = node;
282
- startOffset = offset;
283
- }
284
-
285
- if (
286
- !endNode
287
- || this.selectionDirectionUp({
288
- anchorNode: node,
289
- anchorOffset: offset,
290
- focusNode: endNode,
291
- focusOffset: endOffset,
292
- })
293
- ) {
294
- endNode = node;
295
- endOffset = offset;
296
- }
297
- }
298
- }
299
-
300
- return { startNode, startOffset, endNode, endOffset };
301
- }
302
-
303
- resolveOptions(options: Partial<TableSelectionOptions>): TableSelectionOptions {
304
- return Object.assign({
305
- selectColor: '#0589f340',
306
- } as TableSelectionOptions, options);
307
- }
308
-
309
- selectionChangeHandler() {
310
- const selection = window.getSelection();
311
- if (!selection) return;
312
- const { anchorNode, focusNode, anchorOffset, focusOffset } = selection;
313
- if (!anchorNode || !focusNode) return;
314
-
315
- const anchorBlot = Quill.find(anchorNode) as TypeParchment.Blot;
316
- const focusBlot = Quill.find(focusNode) as TypeParchment.Blot;
317
- if (!anchorBlot || !focusBlot || anchorBlot.scroll !== this.quill.scroll || focusBlot.scroll !== this.quill.scroll) return;
318
-
319
- const anchorNames = findAllParentBlot(anchorBlot);
320
- const focusNames = findAllParentBlot(focusBlot);
321
-
322
- // if cursor into colgourp should into table or out table by lastSelection
323
- const isAnchorInColgroup = anchorNames.has(blotName.tableColgroup);
324
- const isFocusInColgroup = focusNames.has(blotName.tableColgroup);
325
- if (isAnchorInColgroup || isFocusInColgroup) {
326
- let newAnchorNode = anchorNode;
327
- let newAnchorOffset = anchorOffset;
328
- let newFocusNode = focusNode;
329
- let newFocusOffset = focusOffset;
330
- // default move cursor to first cell
331
- if (isAnchorInColgroup) {
332
- const tableWrapperBlot = anchorNames.get(blotName.tableWrapper) as TableWrapperFormat;
333
- const cellInner = tableWrapperBlot.descendants(TableCellInnerFormat);
334
- if (cellInner.length > 0) {
335
- newAnchorNode = cellInner[0].domNode;
336
- newAnchorOffset = 0;
337
- }
338
- }
339
- if (isFocusInColgroup) {
340
- const tableWrapperBlot = focusNames.get(blotName.tableWrapper) as TableWrapperFormat;
341
- const cellInner = tableWrapperBlot.descendants(TableCellInnerFormat);
342
- if (cellInner.length > 0) {
343
- newFocusNode = cellInner[0].domNode;
344
- newFocusOffset = 0;
345
- }
346
- }
347
- this.setSelectionData(selection, {
348
- anchorNode: newAnchorNode,
349
- anchorOffset: newAnchorOffset,
350
- focusNode: newFocusNode,
351
- focusOffset: newFocusOffset,
352
- });
353
- return;
354
- }
355
-
356
- // if the selection in the table partial
357
- const isAnchorInCellInner = anchorNames.has(blotName.tableCellInner);
358
- const isFocusInCellInner = focusNames.has(blotName.tableCellInner);
359
- let isNotSameCellInner = isAnchorInCellInner && isFocusInCellInner;
360
- if (isNotSameCellInner) {
361
- const anchorCellBlot = anchorNames.get(blotName.tableCellInner) as TableCellInnerFormat;
362
- const focusCellBlot = focusNames.get(blotName.tableCellInner) as TableCellInnerFormat;
363
- isNotSameCellInner &&= (anchorCellBlot !== focusCellBlot);
364
- }
365
- if (
366
- (isAnchorInCellInner && isFocusInCellInner && isNotSameCellInner)
367
- || (!isAnchorInCellInner && isFocusInCellInner)
368
- || (!isFocusInCellInner && isAnchorInCellInner)
369
- ) {
370
- this.setSelectionData(selection, this.lastSelection);
371
- if (this.selectedTds.length > 0) {
372
- this.hide();
373
- }
374
- return;
375
- }
376
-
377
- this.lastSelection = {
378
- anchorNode,
379
- anchorOffset,
380
- focusNode,
381
- focusOffset,
382
- };
383
- }
384
-
385
- helpLinesInitial() {
386
- this.cellSelectWrap.style.setProperty('--select-color', this.options.selectColor);
387
- const cellSelect = document.createElement('div');
388
- cellSelect.classList.add(this.bem.be('line'));
389
- this.cellSelectWrap.appendChild(cellSelect);
390
- return cellSelect;
391
- }
392
-
393
- computeSelectedTds(startPoint: { x: number; y: number }, endPoint: { x: number; y: number }) {
394
- if (!this.table) return [];
395
- type TempSortedTableCellFormat = TableCellFormat & { index?: number; __rect?: DOMRect };
396
-
397
- const tableMainBlot = Quill.find(this.table) as TableMainFormat;
398
- if (!tableMainBlot) return [];
399
- // Use TableCell to calculation selected range, because TableCellInner is scrollable, the width will effect calculate
400
- const tableCells = new Set(
401
- // reverse cell. search from bottom.
402
- // when mouse click on the cell border. the selection will be in the lower cell.
403
- // but `isRectanglesIntersect` judge intersect include border. the upper cell bottom border will intersect with boundary
404
- // so need to search the cell from bottom
405
- (tableMainBlot.descendants(TableCellFormat) as TempSortedTableCellFormat[]).map((cell, i) => {
406
- cell.index = i;
407
- return cell;
408
- }),
409
- );
410
-
411
- const scrollDiff = this.getScrollPositionDiff();
412
- // set boundary to initially mouse move rectangle
413
- const { rect: tableRect } = getTableMainRect(tableMainBlot);
414
- if (!tableRect) return [];
415
- const startPointX = startPoint.x + scrollDiff.x;
416
- const startPointY = startPoint.y + scrollDiff.y;
417
- let boundary = {
418
- x: Math.max(tableRect.left, Math.min(endPoint.x, startPointX)),
419
- y: Math.max(tableRect.top, Math.min(endPoint.y, startPointY)),
420
- x1: Math.min(tableRect.right, Math.max(endPoint.x, startPointX)),
421
- y1: Math.min(tableRect.bottom, Math.max(endPoint.y, startPointY)),
422
- };
423
-
424
- const selectedCells = new Set<TempSortedTableCellFormat>();
425
- let findEnd = true;
426
- // loop all cells to find correct boundary
427
- while (findEnd) {
428
- findEnd = false;
429
- for (const cell of tableCells) {
430
- if (!cell.__rect) {
431
- cell.__rect = cell.domNode.getBoundingClientRect();
432
- }
433
- // Determine whether the cell intersects with the current boundary
434
- const { x, y, right, bottom } = cell.__rect;
435
- // bowser MouseEvent clientY\clientX is floored. judge data need floored too
436
- if (
437
- isRectanglesIntersect(
438
- { x: Math.floor(boundary.x), y: Math.floor(boundary.y), x1: Math.floor(boundary.x1), y1: Math.floor(boundary.y1) },
439
- { x: Math.floor(x), y: Math.floor(y), x1: Math.floor(right), y1: Math.floor(bottom) },
440
- ERROR_LIMIT,
441
- selectedCells.size === 0,
442
- )
443
- ) {
444
- // add cell to selected
445
- selectedCells.add(cell);
446
- tableCells.delete(cell);
447
- // update boundary
448
- boundary = {
449
- x: Math.min(boundary.x, x),
450
- y: Math.min(boundary.y, y),
451
- x1: Math.max(boundary.x1, right),
452
- y1: Math.max(boundary.y1, bottom),
453
- };
454
- // recalculate boundary last cells
455
- findEnd = true;
456
- break;
457
- }
458
- }
459
- }
460
- for (const cell of [...selectedCells, ...tableCells]) {
461
- delete cell.__rect;
462
- }
463
- // save result boundary relative to the editor
464
- this.boundary = getRelativeRect({
465
- ...boundary,
466
- width: boundary.x1 - boundary.x,
467
- height: boundary.y1 - boundary.y,
468
- }, this.quill.root);
469
- return Array.from(selectedCells).toSorted((a, b) => a.index! - b.index!).map((cell) => {
470
- delete cell.index;
471
- return cell.getCellInner();
472
- });
473
- }
474
-
475
- getScrollPositionDiff() {
476
- const { x: tableScrollX, y: tableScrollY } = this.getTableViewScroll();
477
- const { x: editorScrollX, y: editorScrollY } = getElementScrollPosition(this.quill.root);
478
- this.selectedTableScrollX = tableScrollX;
479
- this.selectedTableScrollY = tableScrollY;
480
- this.selectedEditorScrollX = editorScrollX;
481
- this.selectedEditorScrollY = editorScrollY;
482
-
483
- return this.startScrollRecordPosition.reduce((pre, { x, y }, i) => {
484
- const { x: currentX, y: currentY } = getElementScrollPosition(this.scrollRecordEls[i]);
485
- pre.x += x - currentX;
486
- pre.y += y - currentY;
487
- return pre;
488
- }, { x: 0, y: 0 });
489
- }
490
-
491
- recordScrollPosition() {
492
- this.clearRecordScrollPosition();
493
- for (const el of this.scrollRecordEls) {
494
- this.startScrollRecordPosition.push(getElementScrollPosition(el));
495
- }
496
- }
497
-
498
- clearRecordScrollPosition() {
499
- this.startScrollRecordPosition = [];
500
- }
501
-
502
- tableSelectHandler(mousedownEvent: MouseEvent) {
503
- const { button, target, clientX, clientY } = mousedownEvent;
504
- const closestTable = (target as HTMLElement).closest<HTMLTableElement>('table');
505
- const closestTableCaption = (target as HTMLElement).closest('caption');
506
- if (button !== 0 || !closestTable || closestTableCaption) return;
507
-
508
- this.setSelectionTable(closestTable);
509
- const startTableId = closestTable.dataset.tableId;
510
- const startPoint = { x: clientX, y: clientY };
511
-
512
- this.recordScrollPosition();
513
- this.setSelectedTds(this.computeSelectedTds(startPoint, startPoint));
514
- this.show();
515
-
516
- const mouseMoveHandler = (mousemoveEvent: MouseEvent) => {
517
- this.dragging = true;
518
- const { button, target, clientX, clientY } = mousemoveEvent;
519
- const closestTable = (target as HTMLElement).closest<HTMLTableElement>('.ql-table');
520
- const closestTableCaption = (target as HTMLElement).closest('caption');
521
- if (
522
- button !== 0
523
- || closestTableCaption
524
- || !closestTable || closestTable.dataset.tableId !== startTableId
525
- ) {
526
- return;
527
- }
528
-
529
- const movePoint = { x: clientX, y: clientY };
530
- this.setSelectedTds(this.computeSelectedTds(startPoint, movePoint));
531
- if (this.selectedTds.length > 1) {
532
- this.quill.blur();
533
- }
534
- this.update();
535
- };
536
- const mouseUpHandler = () => {
537
- document.body.removeEventListener('mousemove', mouseMoveHandler, false);
538
- document.body.removeEventListener('mouseup', mouseUpHandler, false);
539
- this.dragging = false;
540
- this.clearRecordScrollPosition();
541
- };
542
-
543
- document.body.addEventListener('mousemove', mouseMoveHandler, false);
544
- document.body.addEventListener('mouseup', mouseUpHandler, false);
545
- }
546
-
547
- updateWithSelectedTds() {
548
- if (this.selectedTds.length <= 0) {
549
- this.hide();
550
- return;
551
- }
552
- const startPoint = { x: Infinity, y: Infinity };
553
- const endPoint = { x: -Infinity, y: -Infinity };
554
- for (const td of this.selectedTds) {
555
- const rect = td.domNode.getBoundingClientRect();
556
- startPoint.x = Math.min(startPoint.x, rect.left);
557
- startPoint.y = Math.min(startPoint.y, rect.top);
558
- endPoint.x = Math.max(endPoint.x, rect.right);
559
- endPoint.y = Math.max(endPoint.y, rect.bottom);
560
- }
561
- this.setSelectedTds(this.computeSelectedTds(startPoint, endPoint));
562
- if (this.selectedTds.length > 0) {
563
- this.update();
564
- }
565
- else {
566
- this.hide();
567
- }
568
- }
569
-
570
- update() {
571
- if (!this.table) {
572
- this.hide();
573
- return;
574
- }
575
- if (this.selectedTds.length === 0 || !this.boundary) return;
576
- const { x: editorScrollX, y: editorScrollY } = getElementScrollPosition(this.quill.root);
577
- const { x: tableScrollX, y: tableScrollY } = this.getTableViewScroll();
578
- const tableWrapperRect = this.table.parentElement!.getBoundingClientRect();
579
- const rootRect = this.quill.root.getBoundingClientRect();
580
- const wrapLeft = tableWrapperRect.x - rootRect.x;
581
- const wrapTop = tableWrapperRect.y - rootRect.y;
582
-
583
- Object.assign(this.cellSelect.style, {
584
- left: `${this.selectedEditorScrollX * 2 - editorScrollX + this.boundary.x + this.selectedTableScrollX - tableScrollX - wrapLeft}px`,
585
- top: `${this.selectedEditorScrollY * 2 - editorScrollY + this.boundary.y + this.selectedTableScrollY - tableScrollY - wrapTop}px`,
586
- width: `${this.boundary.width}px`,
587
- height: `${this.boundary.height}px`,
588
- });
589
- Object.assign(this.cellSelectWrap.style, {
590
- left: `${wrapLeft}px`,
591
- top: `${wrapTop}px`,
592
- width: `${tableWrapperRect.width}px`,
593
- height: `${tableWrapperRect.height}px`,
594
- });
595
- this.quill.emitter.emit(tableUpEvent.TABLE_SELECTION_DISPLAY_CHANGE, this);
596
- }
597
-
598
- getTableViewScroll() {
599
- if (!this.table) {
600
- return {
601
- x: 0,
602
- y: 0,
603
- };
604
- }
605
- return getElementScrollPosition(this.table.parentElement!);
606
- }
607
-
608
- setSelectionTable(table: HTMLTableElement | undefined) {
609
- if (this.table === table) return;
610
- this.table = table;
611
- if (this.table) {
612
- this.scrollRecordEls.push(this.table.parentElement!);
613
- }
614
- else {
615
- this.scrollRecordEls.pop();
616
- }
617
- }
618
-
619
- showDisplay() {
620
- Object.assign(this.cellSelectWrap.style, { display: 'block' });
621
- this.isDisplaySelection = true;
622
- if (!this.table) return;
623
- this.resizeObserver.observe(this.table);
624
- }
625
-
626
- show() {
627
- if (!this.table) return;
628
- clearScrollEvent.call(this);
629
-
630
- this.showDisplay();
631
- this.update();
632
- this.quill.root.addEventListener('keydown', this.keyboardHandler);
633
- addScrollEvent.call(this, this.quill.root, () => {
634
- this.update();
635
- });
636
- addScrollEvent.call(this, this.table.parentElement!, () => {
637
- this.update();
638
- });
639
- }
640
-
641
- hideDisplay() {
642
- Object.assign(this.cellSelectWrap.style, { display: 'none' });
643
- this.isDisplaySelection = false;
644
- if (!this.table) return;
645
- this.resizeObserver.unobserve(this.table);
646
- }
647
-
648
- hide() {
649
- clearScrollEvent.call(this);
650
- this.quill.root.removeEventListener('keydown', this.keyboardHandler);
651
- this.hideDisplay();
652
- this.boundary = null;
653
- this.setSelectedTds([]);
654
- this.setSelectionTable(undefined);
655
- }
656
-
657
- destroy() {
658
- this.resizeObserver.disconnect();
659
-
660
- this.hide();
661
- this.cellSelectWrap.remove();
662
- clearScrollEvent.call(this);
663
-
664
- document.removeEventListener('paste', this.handlePaste);
665
- this.quill.off(tableUpEvent.AFTER_TABLE_RESIZE, this.updateAfterEvent);
666
- this.quill.off(Quill.events.EDITOR_CHANGE, this.updateWhenTextChange);
667
- this.quill.off(Quill.events.SELECTION_CHANGE, this.quillSelectionChangeHandler);
668
- }
669
- }
1
+ import type { EmitterSource, Parchment as TypeParchment, Range as TypeRange } from 'quill';
2
+ import type { TableMainFormat, TableWrapperFormat } from '../formats';
3
+ import type { TableUp } from '../table-up';
4
+ import type { Position, RelactiveRect, TableSelectionOptions } from '../utils';
5
+ import Quill from 'quill';
6
+ import { getTableMainRect, TableCellFormat, TableCellInnerFormat } from '../formats';
7
+ import { addScrollEvent, AutoScroller, blotName, clearScrollEvent, createBEM, createResizeObserver, findAllParentBlot, findParentBlot, getElementScrollPosition, getRelativeRect, isRectanglesIntersect, tableUpEvent, tableUpInternal } from '../utils';
8
+ import { pasteCells } from './table-clipboard';
9
+ import { TableDomSelector } from './table-dom-selector';
10
+ import { copyCell } from './table-menu/constants';
11
+
12
+ const ERROR_LIMIT = 0;
13
+
14
+ export interface SelectionData {
15
+ anchorNode: Node | null;
16
+ anchorOffset: number;
17
+ focusNode: Node | null;
18
+ focusOffset: number;
19
+ }
20
+
21
+ export class TableSelection extends TableDomSelector {
22
+ static moduleName: string = tableUpInternal.tableSelectionName;
23
+
24
+ options: TableSelectionOptions;
25
+ boundary: RelactiveRect | null = null;
26
+ scrollRecordEls: HTMLElement[] = [];
27
+ startScrollRecordPosition: Position[] = [];
28
+ selectedTableScrollX: number = 0;
29
+ selectedTableScrollY: number = 0;
30
+ selectedEditorScrollX: number = 0;
31
+ selectedEditorScrollY: number = 0;
32
+ selectedTds: TableCellInnerFormat[] = [];
33
+ cellSelectWrap: HTMLElement;
34
+ cellSelect: HTMLElement;
35
+ scrollHandler: [HTMLElement, (...args: any[]) => void][] = [];
36
+ resizeObserver: ResizeObserver;
37
+ isDisplaySelection = false;
38
+ bem = createBEM('selection');
39
+ autoScroller: AutoScroller;
40
+ lastSelection: SelectionData = {
41
+ anchorNode: null,
42
+ anchorOffset: 0,
43
+ focusNode: null,
44
+ focusOffset: 0,
45
+ };
46
+
47
+ _dragging: boolean = false;
48
+ set dragging(val: boolean) {
49
+ if (this._dragging === val) return;
50
+ this._dragging = val;
51
+ this.quill.emitter.emit(val ? tableUpEvent.TABLE_SELECTION_DRAG_START : tableUpEvent.TABLE_SELECTION_DRAG_END, this);
52
+ }
53
+
54
+ get dragging() {
55
+ return this._dragging;
56
+ }
57
+
58
+ constructor(public tableModule: TableUp, public quill: Quill, options: Partial<TableSelectionOptions> = {}) {
59
+ super(tableModule, quill);
60
+ this.options = this.resolveOptions(options);
61
+ this.scrollRecordEls = [this.quill.root, document.documentElement];
62
+
63
+ this.cellSelectWrap = tableModule.addContainer(this.bem.b());
64
+ this.cellSelect = this.helpLinesInitial();
65
+
66
+ this.resizeObserver = createResizeObserver(this.updateAfterEvent, { ignoreFirstBind: true });
67
+ this.resizeObserver.observe(this.quill.root);
68
+
69
+ document.addEventListener('paste', this.handlePaste);
70
+ this.quill.emitter.listenDOM('selectionchange', document, this.selectionChangeHandler.bind(this));
71
+ this.quill.on(tableUpEvent.AFTER_TABLE_RESIZE, this.updateAfterEvent);
72
+ this.quill.on(Quill.events.SELECTION_CHANGE, this.quillSelectionChangeHandler);
73
+ this.quill.on(Quill.events.EDITOR_CHANGE, this.updateWhenTextChange);
74
+
75
+ this.autoScroller = new AutoScroller(50, 40);
76
+ this.hide();
77
+ }
78
+
79
+ handlePaste = (event: ClipboardEvent) => {
80
+ const activeElement = document.activeElement && this.quill.root.contains(document.activeElement);
81
+ if (!activeElement || this.quill.getSelection()) return;
82
+
83
+ const clipboardData = event.clipboardData;
84
+ if (!clipboardData) return;
85
+ event.preventDefault();
86
+
87
+ const currentSelectedTds = this.selectedTds;
88
+ if (currentSelectedTds.length <= 0) return;
89
+
90
+ const html = clipboardData.getData('text/html');
91
+ const delta = this.quill.clipboard.convert({ html }).ops.filter(op => op.attributes?.[blotName.tableCellInner]);
92
+
93
+ if (delta.length === 0) return;
94
+
95
+ pasteCells(
96
+ { quill: this.quill, talbeModule: this.tableModule },
97
+ currentSelectedTds,
98
+ delta,
99
+ );
100
+ };
101
+
102
+ keyboardHandler = async (e: KeyboardEvent) => {
103
+ if (e.ctrlKey) {
104
+ switch (e.key) {
105
+ case 'c':
106
+ case 'x': {
107
+ await copyCell(this.tableModule, this.selectedTds, e.key === 'x');
108
+ break;
109
+ }
110
+ }
111
+ }
112
+ else if (e.key === 'Backspace' || e.key === 'Delete') {
113
+ this.removeCellBySelectedTds();
114
+ }
115
+ };
116
+
117
+ updateWhenTextChange = (eventName: string) => {
118
+ if (eventName === Quill.events.TEXT_CHANGE) {
119
+ if (this.table && !this.quill.root.contains(this.table)) {
120
+ this.setSelectionTable(undefined);
121
+ }
122
+ else {
123
+ this.updateAfterEvent();
124
+ }
125
+ }
126
+ };
127
+
128
+ updateAfterEvent = () => {
129
+ // if any cell doesn't exist, selection will be cleared
130
+ for (const td of this.selectedTds) {
131
+ if (!td.domNode.isConnected) {
132
+ this.selectedTds = [];
133
+ break;
134
+ }
135
+ }
136
+ this.updateWithSelectedTds();
137
+ };
138
+
139
+ removeCellBySelectedTds() {
140
+ const range = this.quill.getSelection();
141
+ const activeElement = document.activeElement;
142
+ if (range || !this.quill.root.contains(activeElement)) return;
143
+
144
+ if (this.table) {
145
+ const tableMain = Quill.find(this.table) as TableMainFormat;
146
+ const cells = tableMain.descendants(TableCellInnerFormat);
147
+ if (this.selectedTds.length === cells.length) {
148
+ tableMain.remove();
149
+ return;
150
+ }
151
+ }
152
+ for (const td of this.selectedTds) {
153
+ const clearTd = td.clone() as TypeParchment.Parent;
154
+ clearTd.appendChild(td.scroll.create('block'));
155
+ td.parent.insertBefore(clearTd, td);
156
+ td.remove();
157
+ }
158
+ }
159
+
160
+ setSelectedTds(tds: TableCellInnerFormat[]) {
161
+ const currentSelectedTds = new Set(this.selectedTds);
162
+ const isSame = this.selectedTds.length === tds.length && tds.every(td => currentSelectedTds.has(td));
163
+
164
+ this.selectedTds = tds;
165
+ if (!isSame) {
166
+ this.quill.emitter.emit(tableUpEvent.TABLE_SELECTION_CHANGE, this, this.selectedTds);
167
+ }
168
+ }
169
+
170
+ quillSelectionChangeHandler = (range: TypeRange | null, _oldRange: TypeRange | null, source: EmitterSource) => {
171
+ if (source === Quill.sources.API) return;
172
+ if (range && !this.quill.composition.isComposing && this.selectedTds.length > 0) {
173
+ const formats = this.quill.getFormat(range);
174
+ const [line] = this.quill.getLine(range.index);
175
+ const isInCell = !!formats[blotName.tableCellInner] && !!line;
176
+ // if the selection is in the cell inner, should not update
177
+ const containsLine = line && this.selectedTds.some(td => td.domNode.contains(line.domNode));
178
+
179
+ if (isInCell && !containsLine) {
180
+ try {
181
+ const cellInner = findParentBlot(line!, blotName.tableCellInner) as TableCellInnerFormat;
182
+ this.setSelectedTds([cellInner]);
183
+ this.updateWithSelectedTds();
184
+ }
185
+ catch {
186
+ // do nothing. should not into here
187
+ }
188
+ }
189
+ else if (!(isInCell && containsLine)) {
190
+ this.hide();
191
+ }
192
+ }
193
+ };
194
+
195
+ setSelectionData(selection: Selection, selectionData: SelectionData) {
196
+ const { anchorNode, anchorOffset, focusNode, focusOffset } = selectionData;
197
+ if (!anchorNode || !focusNode) return;
198
+ const range = document.createRange();
199
+ const isUpFromDown = this.selectionDirectionUp(selectionData);
200
+ if (isUpFromDown) {
201
+ range.setStart(anchorNode, anchorOffset);
202
+ range.setEnd(anchorNode, anchorOffset);
203
+ }
204
+ else {
205
+ range.setStart(anchorNode, anchorOffset);
206
+ range.setEnd(focusNode, focusOffset);
207
+ }
208
+ selection.removeAllRanges();
209
+ selection.addRange(range);
210
+ if (isUpFromDown) {
211
+ selection.extend(focusNode, focusOffset);
212
+ }
213
+ }
214
+
215
+ selectionDirectionUp(selection: SelectionData) {
216
+ const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;
217
+ if (!anchorNode || !focusNode) return false;
218
+
219
+ if (anchorNode === focusNode) {
220
+ return anchorOffset > focusOffset;
221
+ }
222
+
223
+ const nodePosition = anchorNode.compareDocumentPosition(focusNode);
224
+ // focus contains anchor
225
+ if (nodePosition & Node.DOCUMENT_POSITION_CONTAINS) {
226
+ // is anchor before focus
227
+ return (nodePosition & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
228
+ }
229
+
230
+ // anchor contains focus
231
+ if (nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY) {
232
+ // is focus before anchor
233
+ return (nodePosition & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
234
+ }
235
+
236
+ // compare position
237
+ return (nodePosition & Node.DOCUMENT_POSITION_PRECEDING) !== 0;
238
+ }
239
+
240
+ resolveOptions(options: Partial<TableSelectionOptions>): TableSelectionOptions {
241
+ return Object.assign({
242
+ selectColor: '#0589f340',
243
+ } as TableSelectionOptions, options);
244
+ }
245
+
246
+ selectionChangeHandler() {
247
+ const selection = window.getSelection();
248
+ if (!selection) return;
249
+ const { anchorNode, focusNode, anchorOffset, focusOffset } = selection;
250
+ if (!anchorNode || !focusNode) return;
251
+
252
+ const anchorBlot = Quill.find(anchorNode) as TypeParchment.Blot;
253
+ const focusBlot = Quill.find(focusNode) as TypeParchment.Blot;
254
+ if (!anchorBlot || !focusBlot || anchorBlot.scroll !== this.quill.scroll || focusBlot.scroll !== this.quill.scroll) return;
255
+
256
+ const anchorNames = findAllParentBlot(anchorBlot);
257
+ const focusNames = findAllParentBlot(focusBlot);
258
+
259
+ // if cursor into colgourp should into table or out table by lastSelection
260
+ const isAnchorInColgroup = anchorNames.has(blotName.tableColgroup);
261
+ const isFocusInColgroup = focusNames.has(blotName.tableColgroup);
262
+ if (isAnchorInColgroup || isFocusInColgroup) {
263
+ let newAnchorNode = anchorNode;
264
+ let newAnchorOffset = anchorOffset;
265
+ let newFocusNode = focusNode;
266
+ let newFocusOffset = focusOffset;
267
+ // default move cursor to first cell
268
+ if (isAnchorInColgroup) {
269
+ const tableWrapperBlot = anchorNames.get(blotName.tableWrapper) as TableWrapperFormat;
270
+ const cellInner = tableWrapperBlot.descendants(TableCellInnerFormat);
271
+ if (cellInner.length > 0) {
272
+ newAnchorNode = cellInner[0].domNode;
273
+ newAnchorOffset = 0;
274
+ }
275
+ }
276
+ if (isFocusInColgroup) {
277
+ const tableWrapperBlot = focusNames.get(blotName.tableWrapper) as TableWrapperFormat;
278
+ const cellInner = tableWrapperBlot.descendants(TableCellInnerFormat);
279
+ if (cellInner.length > 0) {
280
+ newFocusNode = cellInner[0].domNode;
281
+ newFocusOffset = 0;
282
+ }
283
+ }
284
+ this.setSelectionData(selection, {
285
+ anchorNode: newAnchorNode,
286
+ anchorOffset: newAnchorOffset,
287
+ focusNode: newFocusNode,
288
+ focusOffset: newFocusOffset,
289
+ });
290
+ return;
291
+ }
292
+
293
+ // if the selection in the table partial
294
+ const isAnchorInCellInner = anchorNames.has(blotName.tableCellInner);
295
+ const isFocusInCellInner = focusNames.has(blotName.tableCellInner);
296
+ let isNotSameCellInner = isAnchorInCellInner && isFocusInCellInner;
297
+ if (isNotSameCellInner) {
298
+ const anchorCellBlot = anchorNames.get(blotName.tableCellInner) as TableCellInnerFormat;
299
+ const focusCellBlot = focusNames.get(blotName.tableCellInner) as TableCellInnerFormat;
300
+ isNotSameCellInner &&= (anchorCellBlot !== focusCellBlot);
301
+ }
302
+ if (
303
+ (isAnchorInCellInner && isFocusInCellInner && isNotSameCellInner)
304
+ || (!isAnchorInCellInner && isFocusInCellInner)
305
+ || (!isFocusInCellInner && isAnchorInCellInner)
306
+ ) {
307
+ this.setSelectionData(selection, this.lastSelection);
308
+ if (this.selectedTds.length > 0) {
309
+ this.hide();
310
+ }
311
+ return;
312
+ }
313
+
314
+ this.lastSelection = {
315
+ anchorNode,
316
+ anchorOffset,
317
+ focusNode,
318
+ focusOffset,
319
+ };
320
+ }
321
+
322
+ helpLinesInitial() {
323
+ this.cellSelectWrap.style.setProperty('--select-color', this.options.selectColor);
324
+ const cellSelect = document.createElement('div');
325
+ cellSelect.classList.add(this.bem.be('line'));
326
+ this.cellSelectWrap.appendChild(cellSelect);
327
+ return cellSelect;
328
+ }
329
+
330
+ computeSelectedTds(startPoint: Position, endPoint: Position) {
331
+ if (!this.table) return [];
332
+ type TempSortedTableCellFormat = TableCellFormat & { index?: number; __rect?: DOMRect };
333
+
334
+ const tableMainBlot = Quill.find(this.table) as TableMainFormat;
335
+ if (!tableMainBlot) return [];
336
+ // Use TableCell to calculation selected range, because TableCellInner is scrollable, the width will effect calculate
337
+ const tableCells = new Set(
338
+ // reverse cell. search from bottom.
339
+ // when mouse click on the cell border. the selection will be in the lower cell.
340
+ // but `isRectanglesIntersect` judge intersect include border. the upper cell bottom border will intersect with boundary
341
+ // so need to search the cell from bottom
342
+ (tableMainBlot.descendants(TableCellFormat) as TempSortedTableCellFormat[]).map((cell, i) => {
343
+ cell.index = i;
344
+ return cell;
345
+ }),
346
+ );
347
+
348
+ const scrollDiff = this.getScrollPositionDiff();
349
+ // set boundary to initially mouse move rectangle
350
+ const { rect: tableRect } = getTableMainRect(tableMainBlot);
351
+ if (!tableRect) return [];
352
+ const startPointX = startPoint.x + scrollDiff.x;
353
+ const startPointY = startPoint.y + scrollDiff.y;
354
+ let boundary = {
355
+ x: Math.max(tableRect.left, Math.min(endPoint.x, startPointX)),
356
+ y: Math.max(tableRect.top, Math.min(endPoint.y, startPointY)),
357
+ x1: Math.min(tableRect.right, Math.max(endPoint.x, startPointX)),
358
+ y1: Math.min(tableRect.bottom, Math.max(endPoint.y, startPointY)),
359
+ };
360
+
361
+ const selectedCells = new Set<TempSortedTableCellFormat>();
362
+ let findEnd = true;
363
+ // loop all cells to find correct boundary
364
+ while (findEnd) {
365
+ findEnd = false;
366
+ for (const cell of tableCells) {
367
+ if (!cell.__rect) {
368
+ cell.__rect = cell.domNode.getBoundingClientRect();
369
+ }
370
+ // Determine whether the cell intersects with the current boundary
371
+ const { x, y, right, bottom } = cell.__rect;
372
+ // bowser MouseEvent clientY\clientX is floored. judge data need floored too
373
+ if (
374
+ isRectanglesIntersect(
375
+ { x: Math.floor(boundary.x), y: Math.floor(boundary.y), x1: Math.floor(boundary.x1), y1: Math.floor(boundary.y1) },
376
+ { x: Math.floor(x), y: Math.floor(y), x1: Math.floor(right), y1: Math.floor(bottom) },
377
+ ERROR_LIMIT,
378
+ selectedCells.size === 0,
379
+ )
380
+ ) {
381
+ // add cell to selected
382
+ selectedCells.add(cell);
383
+ tableCells.delete(cell);
384
+ // update boundary
385
+ boundary = {
386
+ x: Math.min(boundary.x, x),
387
+ y: Math.min(boundary.y, y),
388
+ x1: Math.max(boundary.x1, right),
389
+ y1: Math.max(boundary.y1, bottom),
390
+ };
391
+ // recalculate boundary last cells
392
+ findEnd = true;
393
+ break;
394
+ }
395
+ }
396
+ }
397
+ for (const cell of [...selectedCells, ...tableCells]) {
398
+ delete cell.__rect;
399
+ }
400
+ // save result boundary relative to the editor
401
+ this.boundary = getRelativeRect({
402
+ ...boundary,
403
+ width: boundary.x1 - boundary.x,
404
+ height: boundary.y1 - boundary.y,
405
+ }, this.quill.root);
406
+ return Array.from(selectedCells).toSorted((a, b) => a.index! - b.index!).map((cell) => {
407
+ delete cell.index;
408
+ return cell.getCellInner();
409
+ });
410
+ }
411
+
412
+ getScrollPositionDiff() {
413
+ const { x: tableScrollX, y: tableScrollY } = this.getTableViewScroll();
414
+ const { x: editorScrollX, y: editorScrollY } = getElementScrollPosition(this.quill.root);
415
+ this.selectedTableScrollX = tableScrollX;
416
+ this.selectedTableScrollY = tableScrollY;
417
+ this.selectedEditorScrollX = editorScrollX;
418
+ this.selectedEditorScrollY = editorScrollY;
419
+
420
+ return this.startScrollRecordPosition.reduce((pre, { x, y }, i) => {
421
+ const { x: currentX, y: currentY } = getElementScrollPosition(this.scrollRecordEls[i]);
422
+ pre.x += x - currentX;
423
+ pre.y += y - currentY;
424
+ return pre;
425
+ }, { x: 0, y: 0 });
426
+ }
427
+
428
+ recordScrollPosition() {
429
+ this.clearRecordScrollPosition();
430
+ for (const el of this.scrollRecordEls) {
431
+ this.startScrollRecordPosition.push(getElementScrollPosition(el));
432
+ }
433
+ }
434
+
435
+ clearRecordScrollPosition() {
436
+ this.startScrollRecordPosition = [];
437
+ }
438
+
439
+ tableSelectHandler(mousedownEvent: MouseEvent) {
440
+ const { button, target, clientX, clientY } = mousedownEvent;
441
+ const closestTable = (target as HTMLElement).closest<HTMLTableElement>('table');
442
+ const closestTableCaption = (target as HTMLElement).closest('caption');
443
+ if (button !== 0 || !closestTable || closestTableCaption) return;
444
+
445
+ this.setSelectionTable(closestTable);
446
+ const startTableId = closestTable.dataset.tableId;
447
+ const startPoint = { x: clientX, y: clientY };
448
+
449
+ this.recordScrollPosition();
450
+ this.setSelectedTds(this.computeSelectedTds(startPoint, startPoint));
451
+ this.show();
452
+
453
+ const mouseMoveHandler = (mousemoveEvent: MouseEvent) => {
454
+ this.dragging = true;
455
+ const { button, target, clientX, clientY } = mousemoveEvent;
456
+ const closestTable = (target as HTMLElement).closest<HTMLTableElement>('.ql-table');
457
+ const closestTableCaption = (target as HTMLElement).closest('caption');
458
+ if (
459
+ button !== 0
460
+ || closestTableCaption
461
+ || !closestTable || closestTable.dataset.tableId !== startTableId
462
+ ) {
463
+ return;
464
+ }
465
+
466
+ const movePoint = { x: clientX, y: clientY };
467
+ this.setSelectedTds(this.computeSelectedTds(startPoint, movePoint));
468
+ if (this.selectedTds.length > 1) {
469
+ this.quill.blur();
470
+ }
471
+ this.update();
472
+ this.autoScroller.updateMousePosition(clientX, clientY);
473
+ };
474
+ const mouseUpHandler = () => {
475
+ document.body.removeEventListener('mousemove', mouseMoveHandler, false);
476
+ document.body.removeEventListener('mouseup', mouseUpHandler, false);
477
+ this.autoScroller.stop();
478
+ this.dragging = false;
479
+ this.clearRecordScrollPosition();
480
+ };
481
+
482
+ document.body.addEventListener('mousemove', mouseMoveHandler, false);
483
+ document.body.addEventListener('mouseup', mouseUpHandler, false);
484
+ const tableMain = Quill.find(closestTable) as TableMainFormat;
485
+ if (!tableMain) return;
486
+ const tableWrapper = tableMain.parent!.domNode as HTMLElement;
487
+ this.autoScroller.updateMousePosition(clientX, clientY);
488
+ this.autoScroller.start(tableWrapper);
489
+ }
490
+
491
+ updateWithSelectedTds() {
492
+ if (this.selectedTds.length <= 0) {
493
+ this.hide();
494
+ return;
495
+ }
496
+ const startPoint = { x: Infinity, y: Infinity };
497
+ const endPoint = { x: -Infinity, y: -Infinity };
498
+ for (const td of this.selectedTds) {
499
+ const rect = td.domNode.getBoundingClientRect();
500
+ startPoint.x = Math.min(startPoint.x, rect.left);
501
+ startPoint.y = Math.min(startPoint.y, rect.top);
502
+ endPoint.x = Math.max(endPoint.x, rect.right);
503
+ endPoint.y = Math.max(endPoint.y, rect.bottom);
504
+ }
505
+ this.setSelectedTds(this.computeSelectedTds(startPoint, endPoint));
506
+ if (this.selectedTds.length > 0) {
507
+ this.update();
508
+ }
509
+ else {
510
+ this.hide();
511
+ }
512
+ }
513
+
514
+ update() {
515
+ if (!this.table) {
516
+ this.hide();
517
+ return;
518
+ }
519
+ if (this.selectedTds.length === 0 || !this.boundary) return;
520
+ const { x: editorScrollX, y: editorScrollY } = getElementScrollPosition(this.quill.root);
521
+ const { x: tableScrollX, y: tableScrollY } = this.getTableViewScroll();
522
+ const tableWrapperRect = this.table.parentElement!.getBoundingClientRect();
523
+ const rootRect = this.quill.root.getBoundingClientRect();
524
+ const wrapLeft = tableWrapperRect.x - rootRect.x;
525
+ const wrapTop = tableWrapperRect.y - rootRect.y;
526
+
527
+ Object.assign(this.cellSelect.style, {
528
+ left: `${this.selectedEditorScrollX * 2 - editorScrollX + this.boundary.x + this.selectedTableScrollX - tableScrollX - wrapLeft}px`,
529
+ top: `${this.selectedEditorScrollY * 2 - editorScrollY + this.boundary.y + this.selectedTableScrollY - tableScrollY - wrapTop}px`,
530
+ width: `${this.boundary.width}px`,
531
+ height: `${this.boundary.height}px`,
532
+ });
533
+ Object.assign(this.cellSelectWrap.style, {
534
+ left: `${wrapLeft}px`,
535
+ top: `${wrapTop}px`,
536
+ width: `${tableWrapperRect.width}px`,
537
+ height: `${tableWrapperRect.height}px`,
538
+ });
539
+ this.quill.emitter.emit(tableUpEvent.TABLE_SELECTION_DISPLAY_CHANGE, this);
540
+ }
541
+
542
+ getTableViewScroll() {
543
+ if (!this.table) {
544
+ return {
545
+ x: 0,
546
+ y: 0,
547
+ };
548
+ }
549
+ return getElementScrollPosition(this.table.parentElement!);
550
+ }
551
+
552
+ setSelectionTable(table: HTMLTableElement | undefined) {
553
+ if (this.table === table) return;
554
+ this.table = table;
555
+ if (this.table) {
556
+ this.scrollRecordEls.push(this.table.parentElement!);
557
+ }
558
+ else {
559
+ this.scrollRecordEls.pop();
560
+ }
561
+ }
562
+
563
+ showDisplay() {
564
+ Object.assign(this.cellSelectWrap.style, { display: 'block' });
565
+ this.isDisplaySelection = true;
566
+ if (!this.table) return;
567
+ this.resizeObserver.observe(this.table);
568
+ }
569
+
570
+ show() {
571
+ if (!this.table) return;
572
+ clearScrollEvent.call(this);
573
+
574
+ this.showDisplay();
575
+ this.update();
576
+ this.quill.root.addEventListener('keydown', this.keyboardHandler);
577
+ addScrollEvent.call(this, this.quill.root, () => {
578
+ this.update();
579
+ });
580
+ addScrollEvent.call(this, this.table.parentElement!, () => {
581
+ this.update();
582
+ });
583
+ }
584
+
585
+ hideDisplay() {
586
+ Object.assign(this.cellSelectWrap.style, { display: 'none' });
587
+ this.isDisplaySelection = false;
588
+ if (!this.table) return;
589
+ this.resizeObserver.unobserve(this.table);
590
+ }
591
+
592
+ hide() {
593
+ clearScrollEvent.call(this);
594
+ this.quill.root.removeEventListener('keydown', this.keyboardHandler);
595
+ this.hideDisplay();
596
+ this.boundary = null;
597
+ this.setSelectedTds([]);
598
+ this.setSelectionTable(undefined);
599
+ }
600
+
601
+ destroy() {
602
+ this.resizeObserver.disconnect();
603
+
604
+ this.hide();
605
+ this.cellSelectWrap.remove();
606
+ clearScrollEvent.call(this);
607
+
608
+ document.removeEventListener('paste', this.handlePaste);
609
+ this.quill.off(tableUpEvent.AFTER_TABLE_RESIZE, this.updateAfterEvent);
610
+ this.quill.off(Quill.events.EDITOR_CHANGE, this.updateWhenTextChange);
611
+ this.quill.off(Quill.events.SELECTION_CHANGE, this.quillSelectionChangeHandler);
612
+ }
613
+ }