quill-resizable-table 1.1.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.
@@ -0,0 +1,705 @@
1
+ /**
2
+ * quill-resizable-table
3
+ * Drag-to-resize columns, rows, and entire tables inside a Quill editor.
4
+ * Right-click context menu & floating edge buttons for adding/removing rows & columns.
5
+ */
6
+ const DEFAULTS = {
7
+ handleSize: 5,
8
+ minColumnWidth: 30,
9
+ minRowHeight: 20,
10
+ minTableWidth: 50,
11
+ minTableHeight: 30,
12
+ };
13
+ class ResizableTable {
14
+ constructor(quill, options = {}) {
15
+ this.drag = null;
16
+ this.overlay = null;
17
+ // Context menu
18
+ this.contextMenu = null;
19
+ this.contextCell = null;
20
+ // Edge buttons
21
+ this.addColBtn = null;
22
+ this.addRowBtn = null;
23
+ this.deleteTableBtn = null;
24
+ this.hoveredTable = null;
25
+ this.hideEdgeBtnTimer = null;
26
+ this.quill = quill;
27
+ this.options = { ...DEFAULTS, ...(typeof options === 'object' ? options : {}) };
28
+ this.doc = quill.root.ownerDocument;
29
+ this.onMouseMoveBound = this.onDocumentMouseMove.bind(this);
30
+ this.onMouseUpBound = this.onDocumentMouseUp.bind(this);
31
+ this.onEditorMouseMoveBound = this.onEditorMouseMove.bind(this);
32
+ this.onEditorMouseDownBound = this.onEditorMouseDown.bind(this);
33
+ this.onContextMenuBound = this.onContextMenu.bind(this);
34
+ this.onDismissMenuBound = this.dismissContextMenu.bind(this);
35
+ this.onDismissMenuKeyBound = this.onKeyDown.bind(this);
36
+ this.onEditorMouseOverBound = this.onEditorMouseOver.bind(this);
37
+ this.onEditorMouseOutBound = this.onEditorMouseOut.bind(this);
38
+ this.onScrollBound = this.onScroll.bind(this);
39
+ this.attach();
40
+ this.registerToolbarHandler();
41
+ }
42
+ /** Hook into Quill's toolbar to handle the "table" button */
43
+ registerToolbarHandler() {
44
+ var _a, _b;
45
+ const toolbar = (_b = (_a = this.quill).getModule) === null || _b === void 0 ? void 0 : _b.call(_a, 'toolbar');
46
+ if (toolbar) {
47
+ toolbar.addHandler('table', () => this.insertNewTable());
48
+ }
49
+ }
50
+ /** Wire up listeners on the editor root */
51
+ attach() {
52
+ const root = this.quill.root;
53
+ root.addEventListener('mousemove', this.onEditorMouseMoveBound);
54
+ root.addEventListener('mousedown', this.onEditorMouseDownBound);
55
+ root.addEventListener('contextmenu', this.onContextMenuBound);
56
+ root.addEventListener('mouseover', this.onEditorMouseOverBound);
57
+ root.addEventListener('mouseout', this.onEditorMouseOutBound);
58
+ }
59
+ /** Remove all listeners (call if you ever destroy the module) */
60
+ destroy() {
61
+ const root = this.quill.root;
62
+ root.removeEventListener('mousemove', this.onEditorMouseMoveBound);
63
+ root.removeEventListener('mousedown', this.onEditorMouseDownBound);
64
+ root.removeEventListener('contextmenu', this.onContextMenuBound);
65
+ root.removeEventListener('mouseover', this.onEditorMouseOverBound);
66
+ root.removeEventListener('mouseout', this.onEditorMouseOutBound);
67
+ this.doc.removeEventListener('mousemove', this.onMouseMoveBound);
68
+ this.doc.removeEventListener('mouseup', this.onMouseUpBound);
69
+ this.doc.removeEventListener('scroll', this.onScrollBound, true);
70
+ this.removeOverlay();
71
+ this.dismissContextMenu();
72
+ this.removeEdgeButtons();
73
+ }
74
+ // ─── Cursor & hit-testing ────────────────────────────────────────
75
+ /**
76
+ * Detect which resize edge (if any) the mouse is near.
77
+ * Returns null when the cursor isn't on a resize boundary.
78
+ */
79
+ detectEdge(e) {
80
+ const target = e.target;
81
+ const td = target.closest('td, th');
82
+ if (!td)
83
+ return null;
84
+ const table = td.closest('table');
85
+ if (!table)
86
+ return null;
87
+ const hs = this.options.handleSize;
88
+ const rect = td.getBoundingClientRect();
89
+ const nearRight = e.clientX >= rect.right - hs;
90
+ const nearBottom = e.clientY >= rect.bottom - hs;
91
+ if (!nearRight && !nearBottom)
92
+ return null;
93
+ // Determine col / row indices
94
+ const colIndex = this.getCellColIndex(td);
95
+ const rowIndex = this.getCellRowIndex(td, table);
96
+ if (nearRight && nearBottom) {
97
+ // Corner of a cell on the last row & last col → table resize
98
+ const isLastCol = colIndex + (td.colSpan || 1) - 1 === this.getColumnCount(table) - 1;
99
+ const isLastRow = rowIndex + (td.rowSpan || 1) - 1 === table.rows.length - 1;
100
+ if (isLastCol && isLastRow) {
101
+ return { edge: 'corner', table, colIndex, rowIndex };
102
+ }
103
+ // Otherwise prefer column resize (feels more natural)
104
+ return { edge: 'col', table, colIndex: colIndex + (td.colSpan || 1) - 1, rowIndex };
105
+ }
106
+ if (nearRight) {
107
+ return { edge: 'col', table, colIndex: colIndex + (td.colSpan || 1) - 1, rowIndex };
108
+ }
109
+ // nearBottom
110
+ return { edge: 'row', table, colIndex, rowIndex: rowIndex + (td.rowSpan || 1) - 1 };
111
+ }
112
+ // ─── Mouse handlers (editor) ────────────────────────────────────
113
+ /** Update cursor style as the mouse moves over cells */
114
+ onEditorMouseMove(e) {
115
+ if (this.drag)
116
+ return; // already dragging
117
+ // Clear all resize cursor classes
118
+ const cells = this.quill.root.querySelectorAll('td.qrt-resize-col, td.qrt-resize-row, td.qrt-resize-corner, th.qrt-resize-col, th.qrt-resize-row, th.qrt-resize-corner');
119
+ cells.forEach(cell => {
120
+ cell.classList.remove('qrt-resize-col', 'qrt-resize-row', 'qrt-resize-corner');
121
+ });
122
+ const hit = this.detectEdge(e);
123
+ if (!hit)
124
+ return;
125
+ // Add the appropriate cursor class to the cell
126
+ const target = e.target;
127
+ const cell = target.closest('td, th');
128
+ if (!cell)
129
+ return;
130
+ switch (hit.edge) {
131
+ case 'col':
132
+ cell.classList.add('qrt-resize-col');
133
+ break;
134
+ case 'row':
135
+ cell.classList.add('qrt-resize-row');
136
+ break;
137
+ case 'corner':
138
+ cell.classList.add('qrt-resize-corner');
139
+ break;
140
+ }
141
+ }
142
+ /** Start a drag operation */
143
+ onEditorMouseDown(e) {
144
+ const hit = this.detectEdge(e);
145
+ if (!hit)
146
+ return;
147
+ e.preventDefault();
148
+ e.stopPropagation();
149
+ const { table } = hit;
150
+ // Snapshot current geometry
151
+ const colWidths = this.getColumnWidths(table);
152
+ const rowHeights = this.getRowHeights(table);
153
+ this.drag = {
154
+ edge: hit.edge,
155
+ table,
156
+ startX: e.clientX,
157
+ startY: e.clientY,
158
+ colIndex: hit.colIndex,
159
+ rowIndex: hit.rowIndex,
160
+ colWidths,
161
+ rowHeights,
162
+ tableWidth: table.offsetWidth,
163
+ tableHeight: table.offsetHeight,
164
+ };
165
+ // Apply explicit sizes so relative sizing doesn't shift
166
+ this.applyColumnWidths(table, colWidths);
167
+ this.applyRowHeights(table, rowHeights);
168
+ this.addOverlay();
169
+ this.doc.addEventListener('mousemove', this.onMouseMoveBound);
170
+ this.doc.addEventListener('mouseup', this.onMouseUpBound);
171
+ }
172
+ // ─── Mouse handlers (document, during drag) ─────────────────────
173
+ onDocumentMouseMove(e) {
174
+ if (!this.drag)
175
+ return;
176
+ e.preventDefault();
177
+ const dx = e.clientX - this.drag.startX;
178
+ const dy = e.clientY - this.drag.startY;
179
+ const { edge, table, colIndex, rowIndex, colWidths, rowHeights } = this.drag;
180
+ if (edge === 'col' || edge === 'corner') {
181
+ const newWidth = Math.max(this.options.minColumnWidth, colWidths[colIndex] + dx);
182
+ this.resizeColumnDirect(table, colIndex, newWidth);
183
+ }
184
+ if (edge === 'row' || edge === 'corner') {
185
+ const newHeight = Math.max(this.options.minRowHeight, rowHeights[rowIndex] + dy);
186
+ const updated = [...rowHeights];
187
+ updated[rowIndex] = newHeight;
188
+ this.applyRowHeights(table, updated);
189
+ }
190
+ }
191
+ onDocumentMouseUp(_e) {
192
+ this.doc.removeEventListener('mousemove', this.onMouseMoveBound);
193
+ this.doc.removeEventListener('mouseup', this.onMouseUpBound);
194
+ this.drag = null;
195
+ this.removeOverlay();
196
+ this.quill.root.style.cursor = '';
197
+ }
198
+ // ─── Overlay (prevents text selection during drag) ──────────────
199
+ addOverlay() {
200
+ if (this.overlay)
201
+ return;
202
+ this.overlay = this.doc.createElement('div');
203
+ Object.assign(this.overlay.style, {
204
+ position: 'fixed',
205
+ top: '0',
206
+ left: '0',
207
+ width: '100vw',
208
+ height: '100vh',
209
+ zIndex: '9999',
210
+ cursor: this.drag
211
+ ? this.drag.edge === 'col'
212
+ ? 'col-resize'
213
+ : this.drag.edge === 'row'
214
+ ? 'row-resize'
215
+ : 'nwse-resize'
216
+ : 'default',
217
+ });
218
+ this.doc.body.appendChild(this.overlay);
219
+ }
220
+ removeOverlay() {
221
+ if (this.overlay) {
222
+ this.overlay.remove();
223
+ this.overlay = null;
224
+ }
225
+ }
226
+ // ─── Geometry helpers ───────────────────────────────────────────
227
+ /** Get the visual column index of a cell accounting for previous colSpans */
228
+ getCellColIndex(td) {
229
+ let index = 0;
230
+ let sibling = td.previousElementSibling;
231
+ while (sibling) {
232
+ index += sibling.colSpan || 1;
233
+ sibling = sibling.previousElementSibling;
234
+ }
235
+ return index;
236
+ }
237
+ /** Get the row index of a cell */
238
+ getCellRowIndex(td, table) {
239
+ const row = td.parentElement;
240
+ return Array.from(table.rows).indexOf(row);
241
+ }
242
+ /** Total number of visual columns in the first row */
243
+ getColumnCount(table) {
244
+ if (table.rows.length === 0)
245
+ return 0;
246
+ const firstRow = table.rows[0];
247
+ let count = 0;
248
+ for (let i = 0; i < firstRow.cells.length; i++) {
249
+ count += firstRow.cells[i].colSpan || 1;
250
+ }
251
+ return count;
252
+ }
253
+ /** Read current column widths from the first row's cells */
254
+ getColumnWidths(table) {
255
+ if (table.rows.length === 0)
256
+ return [];
257
+ const firstRow = table.rows[0];
258
+ const widths = [];
259
+ for (let i = 0; i < firstRow.cells.length; i++) {
260
+ const cell = firstRow.cells[i];
261
+ const span = cell.colSpan || 1;
262
+ const w = cell.offsetWidth / span;
263
+ for (let s = 0; s < span; s++) {
264
+ widths.push(w);
265
+ }
266
+ }
267
+ return widths;
268
+ }
269
+ /** Read current row heights */
270
+ getRowHeights(table) {
271
+ return Array.from(table.rows).map((row) => row.offsetHeight);
272
+ }
273
+ /** Apply column widths via a <colgroup> */
274
+ applyColumnWidths(table, widths) {
275
+ // Ensure table has layout=fixed for predictable sizing
276
+ table.style.tableLayout = 'fixed';
277
+ table.style.width = widths.reduce((a, b) => a + b, 0) + 'px';
278
+ const doc = table.ownerDocument;
279
+ let colgroup = table.querySelector('colgroup');
280
+ if (!colgroup) {
281
+ colgroup = doc.createElement('colgroup');
282
+ table.insertBefore(colgroup, table.firstChild);
283
+ }
284
+ // Sync <col> elements
285
+ while (colgroup.children.length > widths.length) {
286
+ colgroup.removeChild(colgroup.lastChild);
287
+ }
288
+ while (colgroup.children.length < widths.length) {
289
+ colgroup.appendChild(doc.createElement('col'));
290
+ }
291
+ for (let i = 0; i < widths.length; i++) {
292
+ colgroup.children[i].style.width = widths[i] + 'px';
293
+ }
294
+ }
295
+ /** Resize a single column independently (only that column changes width) */
296
+ resizeColumnDirect(table, colIndex, newWidth) {
297
+ const doc = table.ownerDocument;
298
+ table.style.tableLayout = 'fixed';
299
+ // Get or create colgroup
300
+ let colgroup = table.querySelector('colgroup');
301
+ if (!colgroup) {
302
+ colgroup = doc.createElement('colgroup');
303
+ table.insertBefore(colgroup, table.firstChild);
304
+ // Initialize all cols with current widths
305
+ const colWidths = this.getColumnWidths(table);
306
+ for (const width of colWidths) {
307
+ const col = doc.createElement('col');
308
+ col.style.width = width + 'px';
309
+ colgroup.appendChild(col);
310
+ }
311
+ }
312
+ // Ensure we have enough col elements
313
+ while (colgroup.children.length < colIndex + 1) {
314
+ colgroup.appendChild(doc.createElement('col'));
315
+ }
316
+ // Resize just this column
317
+ colgroup.children[colIndex].style.width = newWidth + 'px';
318
+ // Update table width to be sum of all columns
319
+ const allCols = Array.from(colgroup.children);
320
+ const totalWidth = allCols.reduce((sum, col) => {
321
+ const w = col.style.width;
322
+ return sum + (w ? parseFloat(w) : 0);
323
+ }, 0);
324
+ table.style.width = totalWidth + 'px';
325
+ this.syncQuill();
326
+ }
327
+ /** Apply row heights directly on <tr> elements */
328
+ applyRowHeights(table, heights) {
329
+ const rows = table.rows;
330
+ for (let i = 0; i < heights.length && i < rows.length; i++) {
331
+ rows[i].style.height = heights[i] + 'px';
332
+ }
333
+ }
334
+ // ─── Context menu ─────────────────────────────────────────────
335
+ onContextMenu(e) {
336
+ const target = e.target;
337
+ const td = target.closest('td, th');
338
+ if (!td)
339
+ return;
340
+ const table = td.closest('table');
341
+ if (!table)
342
+ return;
343
+ e.preventDefault();
344
+ e.stopPropagation();
345
+ this.contextCell = td;
346
+ this.showContextMenu(e.clientX, e.clientY, td, table);
347
+ }
348
+ showContextMenu(x, y, td, table) {
349
+ this.dismissContextMenu();
350
+ const menu = this.doc.createElement('div');
351
+ menu.className = 'qrt-context-menu';
352
+ Object.assign(menu.style, {
353
+ position: 'fixed',
354
+ left: x + 'px',
355
+ top: y + 'px',
356
+ zIndex: '10000',
357
+ });
358
+ const colIndex = this.getCellColIndex(td);
359
+ const rowIndex = this.getCellRowIndex(td, table);
360
+ const colCount = this.getColumnCount(table);
361
+ const rowCount = table.rows.length;
362
+ const items = [
363
+ { label: 'Insert Column Left', action: () => this.insertColumn(table, colIndex, 'before') },
364
+ { label: 'Insert Column Right', action: () => this.insertColumn(table, colIndex, 'after') },
365
+ { label: 'Delete Column', action: () => this.deleteColumn(table, colIndex), dividerAfter: true, disabled: colCount <= 1 },
366
+ { label: 'Insert Row Above', action: () => this.insertRow(table, rowIndex, 'before') },
367
+ { label: 'Insert Row Below', action: () => this.insertRow(table, rowIndex, 'after') },
368
+ { label: 'Delete Row', action: () => this.deleteRow(table, rowIndex), dividerAfter: true, disabled: rowCount <= 1 },
369
+ { label: 'Delete Table', action: () => this.deleteTable(table) },
370
+ ];
371
+ for (const item of items) {
372
+ const el = this.doc.createElement('div');
373
+ el.className = 'qrt-context-menu-item' + (item.disabled ? ' qrt-disabled' : '');
374
+ el.textContent = item.label;
375
+ if (!item.disabled) {
376
+ el.addEventListener('mousedown', (ev) => {
377
+ ev.preventDefault();
378
+ ev.stopPropagation();
379
+ item.action();
380
+ this.dismissContextMenu();
381
+ });
382
+ }
383
+ menu.appendChild(el);
384
+ if (item.dividerAfter) {
385
+ const divider = this.doc.createElement('div');
386
+ divider.className = 'qrt-context-menu-divider';
387
+ menu.appendChild(divider);
388
+ }
389
+ }
390
+ this.doc.body.appendChild(menu);
391
+ this.contextMenu = menu;
392
+ // Dismiss on click outside or Escape
393
+ setTimeout(() => {
394
+ this.doc.addEventListener('mousedown', this.onDismissMenuBound);
395
+ this.doc.addEventListener('keydown', this.onDismissMenuKeyBound);
396
+ }, 0);
397
+ }
398
+ dismissContextMenu() {
399
+ if (this.contextMenu) {
400
+ this.contextMenu.remove();
401
+ this.contextMenu = null;
402
+ this.contextCell = null;
403
+ this.doc.removeEventListener('mousedown', this.onDismissMenuBound);
404
+ this.doc.removeEventListener('keydown', this.onDismissMenuKeyBound);
405
+ }
406
+ }
407
+ onKeyDown(e) {
408
+ if (e.key === 'Escape') {
409
+ this.dismissContextMenu();
410
+ }
411
+ }
412
+ // ─── Edge buttons (+ buttons on table hover) ─────────────────
413
+ cancelHideEdgeButtons() {
414
+ if (this.hideEdgeBtnTimer) {
415
+ clearTimeout(this.hideEdgeBtnTimer);
416
+ this.hideEdgeBtnTimer = null;
417
+ }
418
+ }
419
+ scheduleHideEdgeButtons() {
420
+ this.cancelHideEdgeButtons();
421
+ this.hideEdgeBtnTimer = setTimeout(() => {
422
+ this.removeEdgeButtons();
423
+ this.hoveredTable = null;
424
+ }, 200);
425
+ }
426
+ onEditorMouseOver(e) {
427
+ const target = e.target;
428
+ const table = target.closest('table');
429
+ // Mouse re-entered the same table or a button — cancel any pending hide
430
+ if (table && table === this.hoveredTable) {
431
+ this.cancelHideEdgeButtons();
432
+ return;
433
+ }
434
+ if (!table)
435
+ return;
436
+ this.cancelHideEdgeButtons();
437
+ this.removeEdgeButtons();
438
+ this.hoveredTable = table;
439
+ this.showEdgeButtons(table);
440
+ }
441
+ onEditorMouseOut(e) {
442
+ var _a, _b, _c;
443
+ const related = e.relatedTarget;
444
+ if (!this.hoveredTable)
445
+ return;
446
+ // Still inside the table or on a button? keep showing
447
+ if (related && (this.hoveredTable.contains(related) ||
448
+ ((_a = this.addColBtn) === null || _a === void 0 ? void 0 : _a.contains(related)) ||
449
+ ((_b = this.addRowBtn) === null || _b === void 0 ? void 0 : _b.contains(related)) ||
450
+ ((_c = this.deleteTableBtn) === null || _c === void 0 ? void 0 : _c.contains(related)))) {
451
+ return;
452
+ }
453
+ // Delay removal so the user can cross the gap to reach the button
454
+ this.scheduleHideEdgeButtons();
455
+ }
456
+ /** Update button positions on scroll to keep them anchored to the table */
457
+ onScroll() {
458
+ if (!this.hoveredTable)
459
+ return;
460
+ const rect = this.hoveredTable.getBoundingClientRect();
461
+ if (this.addColBtn) {
462
+ Object.assign(this.addColBtn.style, {
463
+ left: (rect.right + 4) + 'px',
464
+ top: (rect.top + rect.height / 2 - 12) + 'px',
465
+ });
466
+ }
467
+ if (this.addRowBtn) {
468
+ Object.assign(this.addRowBtn.style, {
469
+ left: (rect.left + rect.width / 2 - 12) + 'px',
470
+ top: (rect.bottom + 4) + 'px',
471
+ });
472
+ }
473
+ if (this.deleteTableBtn) {
474
+ Object.assign(this.deleteTableBtn.style, {
475
+ right: (window.innerWidth - rect.right + 4) + 'px',
476
+ top: (rect.top - 20) + 'px',
477
+ });
478
+ }
479
+ }
480
+ showEdgeButtons(table) {
481
+ const rect = table.getBoundingClientRect();
482
+ // Attach scroll listener to reposition buttons
483
+ this.doc.addEventListener('scroll', this.onScrollBound, true);
484
+ // + Column button (right edge, vertically centered)
485
+ this.addColBtn = this.doc.createElement('div');
486
+ this.addColBtn.className = 'qrt-edge-btn';
487
+ this.addColBtn.textContent = '+';
488
+ this.addColBtn.title = 'Add column';
489
+ Object.assign(this.addColBtn.style, {
490
+ position: 'fixed',
491
+ left: (rect.right + 4) + 'px',
492
+ top: (rect.top + rect.height / 2 - 12) + 'px',
493
+ zIndex: '10000',
494
+ cursor: 'pointer',
495
+ });
496
+ this.addColBtn.addEventListener('mousedown', (ev) => {
497
+ ev.preventDefault();
498
+ ev.stopPropagation();
499
+ this.cancelHideEdgeButtons();
500
+ const colCount = this.getColumnCount(table);
501
+ this.insertColumn(table, colCount - 1, 'after');
502
+ this.removeEdgeButtons();
503
+ this.showEdgeButtons(table);
504
+ });
505
+ this.addColBtn.addEventListener('mouseenter', () => {
506
+ this.cancelHideEdgeButtons();
507
+ });
508
+ this.addColBtn.addEventListener('mouseleave', (ev) => {
509
+ var _a;
510
+ const related = ev.relatedTarget;
511
+ if (related && (table.contains(related) || ((_a = this.addRowBtn) === null || _a === void 0 ? void 0 : _a.contains(related))))
512
+ return;
513
+ this.scheduleHideEdgeButtons();
514
+ });
515
+ this.doc.body.appendChild(this.addColBtn);
516
+ // + Row button (bottom edge, horizontally centered)
517
+ this.addRowBtn = this.doc.createElement('div');
518
+ this.addRowBtn.className = 'qrt-edge-btn';
519
+ this.addRowBtn.textContent = '+';
520
+ this.addRowBtn.title = 'Add row';
521
+ Object.assign(this.addRowBtn.style, {
522
+ position: 'fixed',
523
+ left: (rect.left + rect.width / 2 - 12) + 'px',
524
+ top: (rect.bottom + 4) + 'px',
525
+ zIndex: '10000',
526
+ cursor: 'pointer',
527
+ });
528
+ this.addRowBtn.addEventListener('mousedown', (ev) => {
529
+ ev.preventDefault();
530
+ ev.stopPropagation();
531
+ this.cancelHideEdgeButtons();
532
+ this.insertRow(table, table.rows.length - 1, 'after');
533
+ this.removeEdgeButtons();
534
+ this.showEdgeButtons(table);
535
+ });
536
+ this.addRowBtn.addEventListener('mouseenter', () => {
537
+ this.cancelHideEdgeButtons();
538
+ });
539
+ this.addRowBtn.addEventListener('mouseleave', (ev) => {
540
+ var _a;
541
+ const related = ev.relatedTarget;
542
+ if (related && (table.contains(related) || ((_a = this.addColBtn) === null || _a === void 0 ? void 0 : _a.contains(related))))
543
+ return;
544
+ this.scheduleHideEdgeButtons();
545
+ });
546
+ this.doc.body.appendChild(this.addRowBtn);
547
+ // Delete Table button (top-right corner)
548
+ this.deleteTableBtn = this.doc.createElement('div');
549
+ this.deleteTableBtn.className = 'qrt-delete-table-btn';
550
+ this.deleteTableBtn.innerHTML = '✕';
551
+ this.deleteTableBtn.title = 'Delete table';
552
+ Object.assign(this.deleteTableBtn.style, {
553
+ position: 'fixed',
554
+ right: (window.innerWidth - rect.right + 4) + 'px',
555
+ top: (rect.top - 20) + 'px',
556
+ zIndex: '10000',
557
+ cursor: 'pointer',
558
+ });
559
+ this.deleteTableBtn.addEventListener('mousedown', (ev) => {
560
+ ev.preventDefault();
561
+ ev.stopPropagation();
562
+ this.cancelHideEdgeButtons();
563
+ this.deleteTable(table);
564
+ this.removeEdgeButtons();
565
+ });
566
+ this.deleteTableBtn.addEventListener('mouseenter', () => {
567
+ this.cancelHideEdgeButtons();
568
+ });
569
+ this.deleteTableBtn.addEventListener('mouseleave', (ev) => {
570
+ var _a, _b;
571
+ const related = ev.relatedTarget;
572
+ if (related && (table.contains(related) || ((_a = this.addColBtn) === null || _a === void 0 ? void 0 : _a.contains(related)) || ((_b = this.addRowBtn) === null || _b === void 0 ? void 0 : _b.contains(related))))
573
+ return;
574
+ this.scheduleHideEdgeButtons();
575
+ });
576
+ this.doc.body.appendChild(this.deleteTableBtn);
577
+ }
578
+ removeEdgeButtons() {
579
+ this.cancelHideEdgeButtons();
580
+ if (this.addColBtn) {
581
+ this.addColBtn.remove();
582
+ this.addColBtn = null;
583
+ }
584
+ if (this.addRowBtn) {
585
+ this.addRowBtn.remove();
586
+ this.addRowBtn = null;
587
+ }
588
+ if (this.deleteTableBtn) {
589
+ this.deleteTableBtn.remove();
590
+ this.deleteTableBtn = null;
591
+ }
592
+ // Remove scroll listener
593
+ this.doc.removeEventListener('scroll', this.onScrollBound, true);
594
+ }
595
+ // ─── Table creation ─────────────────────────────────────────
596
+ /** Insert a new 3×3 table at the current cursor position */
597
+ insertNewTable(rows = 3, cols = 3) {
598
+ var _a, _b;
599
+ const range = (_b = (_a = this.quill).getSelection) === null || _b === void 0 ? void 0 : _b.call(_a, true);
600
+ if (!range)
601
+ return;
602
+ // Build table HTML
603
+ const cellHTML = '<td><br></td>';
604
+ const rowHTML = `<tr>${cellHTML.repeat(cols)}</tr>`;
605
+ const tableHTML = `<table><tbody>${rowHTML.repeat(rows)}</tbody></table>`;
606
+ // Insert at cursor via clipboard (preserves Quill delta consistency)
607
+ this.quill.clipboard.dangerouslyPasteHTML(range.index, tableHTML, 'user');
608
+ this.syncQuill();
609
+ }
610
+ // ─── Table mutation methods ───────────────────────────────────
611
+ /**
612
+ * Consume pending MutationObserver records so Quill's async handler
613
+ * never processes our structural DOM changes (which it would corrupt).
614
+ * Style-only changes (resize) don't need this — only structural ones.
615
+ */
616
+ syncQuill() {
617
+ var _a;
618
+ try {
619
+ const scroll = (_a = this.quill) === null || _a === void 0 ? void 0 : _a.scroll;
620
+ if (scroll === null || scroll === void 0 ? void 0 : scroll.observer) {
621
+ // Grab and discard all pending mutation records before
622
+ // Quill's microtask callback can process them
623
+ scroll.observer.takeRecords();
624
+ }
625
+ }
626
+ catch (_b) {
627
+ // Quill internals not accessible — ignore silently
628
+ }
629
+ }
630
+ /** Insert a column before or after colIndex */
631
+ insertColumn(table, colIndex, position) {
632
+ const targetIndex = position === 'after' ? colIndex + 1 : colIndex;
633
+ for (let r = 0; r < table.rows.length; r++) {
634
+ const row = table.rows[r];
635
+ const newCell = row.insertCell(Math.min(targetIndex, row.cells.length));
636
+ newCell.innerHTML = '<br>';
637
+ }
638
+ // Update colgroup if it exists
639
+ const colgroup = table.querySelector('colgroup');
640
+ if (colgroup) {
641
+ const col = table.ownerDocument.createElement('col');
642
+ col.style.width = this.options.minColumnWidth + 'px';
643
+ if (targetIndex < colgroup.children.length) {
644
+ colgroup.insertBefore(col, colgroup.children[targetIndex]);
645
+ }
646
+ else {
647
+ colgroup.appendChild(col);
648
+ }
649
+ // Recalculate table width
650
+ table.style.width = (table.offsetWidth + this.options.minColumnWidth) + 'px';
651
+ }
652
+ this.syncQuill();
653
+ }
654
+ /** Delete column at colIndex (no-op if only 1 column remains) */
655
+ deleteColumn(table, colIndex) {
656
+ if (this.getColumnCount(table) <= 1)
657
+ return;
658
+ for (let r = 0; r < table.rows.length; r++) {
659
+ const row = table.rows[r];
660
+ if (colIndex < row.cells.length) {
661
+ row.deleteCell(colIndex);
662
+ }
663
+ }
664
+ // Update colgroup if it exists
665
+ const colgroup = table.querySelector('colgroup');
666
+ if (colgroup && colIndex < colgroup.children.length) {
667
+ colgroup.removeChild(colgroup.children[colIndex]);
668
+ // Recalculate table width from remaining cols
669
+ let total = 0;
670
+ for (let i = 0; i < colgroup.children.length; i++) {
671
+ total += parseInt(colgroup.children[i].style.width, 10) || this.options.minColumnWidth;
672
+ }
673
+ table.style.width = total + 'px';
674
+ }
675
+ this.syncQuill();
676
+ }
677
+ /** Insert a row before or after rowIndex */
678
+ insertRow(table, rowIndex, position) {
679
+ const targetIndex = position === 'after' ? rowIndex + 1 : rowIndex;
680
+ const colCount = this.getColumnCount(table);
681
+ const newRow = table.insertRow(Math.min(targetIndex, table.rows.length));
682
+ for (let c = 0; c < colCount; c++) {
683
+ const cell = newRow.insertCell();
684
+ cell.innerHTML = '<br>';
685
+ }
686
+ this.syncQuill();
687
+ }
688
+ /** Delete row at rowIndex (no-op if only 1 row remains) */
689
+ deleteRow(table, rowIndex) {
690
+ if (table.rows.length <= 1)
691
+ return;
692
+ table.deleteRow(rowIndex);
693
+ this.syncQuill();
694
+ }
695
+ /** Delete the entire table */
696
+ deleteTable(table) {
697
+ table.remove();
698
+ this.syncQuill();
699
+ }
700
+ }
701
+ /** Table icon SVG for the Quill toolbar */
702
+ ResizableTable.TABLE_ICON = '<svg viewBox="0 0 18 18"><rect class="ql-stroke" height="12" width="12" x="3" y="3" fill="none"/><line class="ql-stroke" x1="3" y1="7" x2="15" y2="7"/><line class="ql-stroke" x1="3" y1="11" x2="15" y2="11"/><line class="ql-stroke" x1="7" y1="3" x2="7" y2="15"/><line class="ql-stroke" x1="11" y1="3" x2="11" y2="15"/></svg>';
703
+
704
+ export { ResizableTable };
705
+ //# sourceMappingURL=quill-resizable-table.esm.js.map