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.
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/ResizableTable.d.ts +109 -0
- package/dist/index.d.ts +2 -0
- package/dist/quill-resizable-table.cjs.js +707 -0
- package/dist/quill-resizable-table.cjs.js.map +1 -0
- package/dist/quill-resizable-table.css +173 -0
- package/dist/quill-resizable-table.esm.js +705 -0
- package/dist/quill-resizable-table.esm.js.map +1 -0
- package/dist/quill-resizable-table.umd.js +713 -0
- package/dist/quill-resizable-table.umd.js.map +1 -0
- package/package.json +55 -0
- package/src/ResizableTable.ts +841 -0
- package/src/index.ts +2 -0
- package/src/quill-resizable-table.css +173 -0
|
@@ -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
|