@editora/plugin-table 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +35 -0
- package/dist/index.esm.js +498 -0
- package/dist/plugin-table.css +1 -0
- package/package.json +27 -0
- package/src/TablePlugin.native.ts +1053 -0
- package/src/index.ts +1 -0
- package/src/table.css +407 -0
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
import { Plugin } from '@editora/core';
|
|
2
|
+
import { findEditorContainerFromSelection, getContentElement } from '../../shared/editorContainerHelpers';
|
|
3
|
+
import './table.css';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Advanced Table Plugin - Native Implementation
|
|
7
|
+
*
|
|
8
|
+
* Exactly matches React version functionality:
|
|
9
|
+
* - Direct table insertion (3x3 with thead/tbody) - NO DIALOG
|
|
10
|
+
* - Floating contextual toolbar (appears when cursor in table)
|
|
11
|
+
* - 10 table operations (rows, columns, headers, merge, delete)
|
|
12
|
+
* - Column resizing with drag handles
|
|
13
|
+
* - Table-level resizing
|
|
14
|
+
* - Keyboard shortcuts (Ctrl+Shift+R, Ctrl+Shift+C)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// MODULE-LEVEL STATE
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
let toolbarElement: HTMLDivElement | null = null;
|
|
22
|
+
let currentTable: HTMLTableElement | null = null;
|
|
23
|
+
let selectionChangeHandler: (() => void) | null = null;
|
|
24
|
+
let mouseDownHandler: ((e: MouseEvent) => void) | null = null;
|
|
25
|
+
let tableDeletedHandler: (() => void) | null = null;
|
|
26
|
+
let scrollHandler: (() => void) | null = null;
|
|
27
|
+
let resizeHandler: (() => void) | null = null;
|
|
28
|
+
const DARK_THEME_SELECTOR = '[data-theme="dark"], .dark, .editora-theme-dark';
|
|
29
|
+
|
|
30
|
+
// Column resizing state
|
|
31
|
+
let isResizing = false;
|
|
32
|
+
let resizeColumn: number | null = null;
|
|
33
|
+
let startX = 0;
|
|
34
|
+
let startWidth = 0;
|
|
35
|
+
|
|
36
|
+
// Table resizing state
|
|
37
|
+
let isTableResizing = false;
|
|
38
|
+
let tableStartX = 0;
|
|
39
|
+
let tableStartY = 0;
|
|
40
|
+
let tableStartWidth = 0;
|
|
41
|
+
let tableStartHeight = 0;
|
|
42
|
+
|
|
43
|
+
declare global {
|
|
44
|
+
interface Window {
|
|
45
|
+
__tablePluginInitialized?: boolean;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// TABLE INSERTION - Direct (NO DIALOG)
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
export const insertTableCommand = () => {
|
|
54
|
+
|
|
55
|
+
// Find editor container from current selection instead of activeElement
|
|
56
|
+
const editorContainer = findEditorContainerFromSelection();
|
|
57
|
+
|
|
58
|
+
const contentEl = getContentElement(editorContainer);
|
|
59
|
+
|
|
60
|
+
if (!contentEl) {
|
|
61
|
+
alert('Please place your cursor in the editor before inserting a table');
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const selection = window.getSelection();
|
|
66
|
+
if (!selection || selection.rangeCount === 0) return;
|
|
67
|
+
|
|
68
|
+
const range = selection.getRangeAt(0);
|
|
69
|
+
|
|
70
|
+
// Create table
|
|
71
|
+
const table = document.createElement('table');
|
|
72
|
+
table.className = 'rte-table';
|
|
73
|
+
|
|
74
|
+
// ===== THEAD =====
|
|
75
|
+
const thead = document.createElement('thead');
|
|
76
|
+
const headerRow = document.createElement('tr');
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < 3; i++) {
|
|
79
|
+
const th = document.createElement('th');
|
|
80
|
+
const p = document.createElement('p');
|
|
81
|
+
p.appendChild(document.createElement('br'));
|
|
82
|
+
th.appendChild(p);
|
|
83
|
+
headerRow.appendChild(th);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
thead.appendChild(headerRow);
|
|
87
|
+
|
|
88
|
+
// ===== TBODY =====
|
|
89
|
+
const tbody = document.createElement('tbody');
|
|
90
|
+
|
|
91
|
+
for (let rowIndex = 0; rowIndex < 2; rowIndex++) {
|
|
92
|
+
const row = document.createElement('tr');
|
|
93
|
+
|
|
94
|
+
for (let colIndex = 0; colIndex < 3; colIndex++) {
|
|
95
|
+
const td = document.createElement('td');
|
|
96
|
+
const p = document.createElement('p');
|
|
97
|
+
p.appendChild(document.createElement('br'));
|
|
98
|
+
td.appendChild(p);
|
|
99
|
+
row.appendChild(td);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
tbody.appendChild(row);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
table.appendChild(thead);
|
|
106
|
+
table.appendChild(tbody);
|
|
107
|
+
|
|
108
|
+
// Insert table
|
|
109
|
+
range.deleteContents();
|
|
110
|
+
range.insertNode(table);
|
|
111
|
+
|
|
112
|
+
// Move cursor to first header cell paragraph
|
|
113
|
+
const firstParagraph = table.querySelector('th p');
|
|
114
|
+
if (firstParagraph) {
|
|
115
|
+
const newRange = document.createRange();
|
|
116
|
+
newRange.setStart(firstParagraph, 0);
|
|
117
|
+
newRange.collapse(true);
|
|
118
|
+
|
|
119
|
+
selection.removeAllRanges();
|
|
120
|
+
selection.addRange(newRange);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
contentEl.focus();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// ============================================
|
|
127
|
+
// TABLE OPERATIONS (10 COMMANDS)
|
|
128
|
+
// ============================================
|
|
129
|
+
|
|
130
|
+
export const addRowAboveCommand = () => {
|
|
131
|
+
const tableInfo = getTableInfoFromDOM();
|
|
132
|
+
if (!tableInfo) return;
|
|
133
|
+
|
|
134
|
+
const { table, rowIndex } = tableInfo;
|
|
135
|
+
|
|
136
|
+
// Create new row with same number of cells
|
|
137
|
+
const newRow = document.createElement('tr');
|
|
138
|
+
const cellCount = table.rows[0]?.cells.length || 0;
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < cellCount; i++) {
|
|
141
|
+
const cell = document.createElement('td');
|
|
142
|
+
const paragraph = document.createElement('p');
|
|
143
|
+
paragraph.innerHTML = '<br>';
|
|
144
|
+
cell.appendChild(paragraph);
|
|
145
|
+
newRow.appendChild(cell);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Find the correct tbody/thead element and insert row
|
|
149
|
+
const currentRow = table.rows[rowIndex];
|
|
150
|
+
if (currentRow && currentRow.parentElement) {
|
|
151
|
+
currentRow.parentElement.insertBefore(newRow, currentRow);
|
|
152
|
+
} else {
|
|
153
|
+
table.appendChild(newRow);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
updateTableInfo();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const addRowBelowCommand = () => {
|
|
160
|
+
const tableInfo = getTableInfoFromDOM();
|
|
161
|
+
if (!tableInfo) return;
|
|
162
|
+
|
|
163
|
+
const { table, rowIndex } = tableInfo;
|
|
164
|
+
|
|
165
|
+
// Create new row with same number of cells
|
|
166
|
+
const newRow = document.createElement('tr');
|
|
167
|
+
const cellCount = table.rows[0]?.cells.length || 0;
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < cellCount; i++) {
|
|
170
|
+
const cell = document.createElement('td');
|
|
171
|
+
const paragraph = document.createElement('p');
|
|
172
|
+
paragraph.innerHTML = '<br>';
|
|
173
|
+
cell.appendChild(paragraph);
|
|
174
|
+
newRow.appendChild(cell);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Insert row after current position
|
|
178
|
+
if (rowIndex >= table.rows.length - 1) {
|
|
179
|
+
table.appendChild(newRow);
|
|
180
|
+
} else {
|
|
181
|
+
table.insertBefore(newRow, table.rows[rowIndex + 1]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
updateTableInfo();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const addColumnLeftCommand = () => {
|
|
188
|
+
const tableInfo = getTableInfoFromDOM();
|
|
189
|
+
if (!tableInfo) return;
|
|
190
|
+
|
|
191
|
+
const { table, colIndex } = tableInfo;
|
|
192
|
+
|
|
193
|
+
// Add cell to each row at specified column index
|
|
194
|
+
for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex++) {
|
|
195
|
+
const row = table.rows[rowIndex];
|
|
196
|
+
const cell = document.createElement('td');
|
|
197
|
+
const paragraph = document.createElement('p');
|
|
198
|
+
paragraph.innerHTML = '<br>';
|
|
199
|
+
cell.appendChild(paragraph);
|
|
200
|
+
|
|
201
|
+
if (colIndex === 0) {
|
|
202
|
+
row.insertBefore(cell, row.cells[0]);
|
|
203
|
+
} else {
|
|
204
|
+
row.insertBefore(cell, row.cells[colIndex]);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
updateTableInfo();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const addColumnRightCommand = () => {
|
|
212
|
+
const tableInfo = getTableInfoFromDOM();
|
|
213
|
+
if (!tableInfo) return;
|
|
214
|
+
|
|
215
|
+
const { table, colIndex } = tableInfo;
|
|
216
|
+
|
|
217
|
+
// Add cell to each row after specified column index
|
|
218
|
+
for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex++) {
|
|
219
|
+
const row = table.rows[rowIndex];
|
|
220
|
+
const cell = document.createElement('td');
|
|
221
|
+
const paragraph = document.createElement('p');
|
|
222
|
+
paragraph.innerHTML = '<br>';
|
|
223
|
+
cell.appendChild(paragraph);
|
|
224
|
+
|
|
225
|
+
if (colIndex >= row.cells.length - 1) {
|
|
226
|
+
row.appendChild(cell);
|
|
227
|
+
} else {
|
|
228
|
+
row.insertBefore(cell, row.cells[colIndex + 1]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
updateTableInfo();
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const deleteRowCommand = () => {
|
|
236
|
+
const tableInfo = getTableInfoFromDOM();
|
|
237
|
+
if (!tableInfo || tableInfo.rowCount <= 1) return;
|
|
238
|
+
|
|
239
|
+
const { table, rowIndex } = tableInfo;
|
|
240
|
+
table.deleteRow(rowIndex);
|
|
241
|
+
|
|
242
|
+
updateTableInfo();
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const deleteColumnCommand = () => {
|
|
246
|
+
const tableInfo = getTableInfoFromDOM();
|
|
247
|
+
if (!tableInfo || tableInfo.cellCount <= 1) return;
|
|
248
|
+
|
|
249
|
+
const { table, colIndex } = tableInfo;
|
|
250
|
+
|
|
251
|
+
// Delete cell from each row at specified column index
|
|
252
|
+
for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex++) {
|
|
253
|
+
const row = table.rows[rowIndex];
|
|
254
|
+
if (row.cells[colIndex]) {
|
|
255
|
+
row.deleteCell(colIndex);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
updateTableInfo();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const toggleHeaderRowCommand = () => {
|
|
263
|
+
const tableInfo = getTableInfoFromDOM();
|
|
264
|
+
if (!tableInfo) return;
|
|
265
|
+
|
|
266
|
+
const { table, rowIndex } = tableInfo;
|
|
267
|
+
const targetRow = table.rows[rowIndex];
|
|
268
|
+
|
|
269
|
+
const isCurrentlyHeader = targetRow.parentElement?.tagName.toLowerCase() === 'thead';
|
|
270
|
+
|
|
271
|
+
if (isCurrentlyHeader) {
|
|
272
|
+
const tbody = table.querySelector('tbody') || table.appendChild(document.createElement('tbody'));
|
|
273
|
+
const thead = table.querySelector('thead');
|
|
274
|
+
if (thead) {
|
|
275
|
+
tbody.insertBefore(targetRow, tbody.firstChild);
|
|
276
|
+
if (thead.rows.length === 0) {
|
|
277
|
+
thead.remove();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
let thead = table.querySelector('thead');
|
|
282
|
+
if (!thead) {
|
|
283
|
+
thead = document.createElement('thead');
|
|
284
|
+
table.insertBefore(thead, table.firstChild);
|
|
285
|
+
}
|
|
286
|
+
thead.appendChild(targetRow);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
updateTableInfo();
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export const toggleHeaderColumnCommand = () => {
|
|
293
|
+
const tableInfo = getTableInfoFromDOM();
|
|
294
|
+
if (!tableInfo) return;
|
|
295
|
+
|
|
296
|
+
const { table, colIndex } = tableInfo;
|
|
297
|
+
|
|
298
|
+
for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex++) {
|
|
299
|
+
const cell = table.rows[rowIndex].cells[colIndex];
|
|
300
|
+
if (cell) {
|
|
301
|
+
const newTag = cell.tagName.toLowerCase() === 'th' ? 'td' : 'th';
|
|
302
|
+
const newCell = document.createElement(newTag);
|
|
303
|
+
newCell.innerHTML = cell.innerHTML;
|
|
304
|
+
|
|
305
|
+
for (let i = 0; i < cell.attributes.length; i++) {
|
|
306
|
+
const attr = cell.attributes[i];
|
|
307
|
+
newCell.setAttribute(attr.name, attr.value);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
cell.parentNode?.replaceChild(newCell, cell);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
updateTableInfo();
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export const deleteTableCommand = () => {
|
|
318
|
+
const tableInfo = getTableInfoFromDOM();
|
|
319
|
+
if (!tableInfo) return;
|
|
320
|
+
|
|
321
|
+
const table = tableInfo.table;
|
|
322
|
+
table.remove();
|
|
323
|
+
|
|
324
|
+
// Trigger toolbar hide event
|
|
325
|
+
document.dispatchEvent(new CustomEvent('tableDeleted'));
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const mergeCellsCommand = () => {
|
|
329
|
+
const selection = window.getSelection();
|
|
330
|
+
if (!selection || selection.rangeCount === 0) return;
|
|
331
|
+
|
|
332
|
+
const range = selection.getRangeAt(0);
|
|
333
|
+
const startContainer = range.startContainer;
|
|
334
|
+
|
|
335
|
+
let tableElement = startContainer.nodeType === Node.TEXT_NODE
|
|
336
|
+
? startContainer.parentElement?.closest('table')
|
|
337
|
+
: (startContainer as Element).closest('table');
|
|
338
|
+
|
|
339
|
+
if (!tableElement) return;
|
|
340
|
+
|
|
341
|
+
const table = tableElement as HTMLTableElement;
|
|
342
|
+
|
|
343
|
+
let firstCell: HTMLTableCellElement | null = null;
|
|
344
|
+
if (startContainer.nodeType === Node.TEXT_NODE) {
|
|
345
|
+
firstCell = startContainer.parentElement?.closest('td, th') as HTMLTableCellElement;
|
|
346
|
+
} else if (startContainer.nodeType === Node.ELEMENT_NODE) {
|
|
347
|
+
firstCell = (startContainer as Element).closest('td, th') as HTMLTableCellElement;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!firstCell) return;
|
|
351
|
+
|
|
352
|
+
const firstRow = firstCell.parentElement as HTMLTableRowElement;
|
|
353
|
+
if (!firstRow) return;
|
|
354
|
+
|
|
355
|
+
let cellIndex = -1;
|
|
356
|
+
for (let i = 0; i < firstRow.cells.length; i++) {
|
|
357
|
+
if (firstRow.cells[i] === firstCell) {
|
|
358
|
+
cellIndex = i;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (cellIndex === -1 || cellIndex === firstRow.cells.length - 1) return;
|
|
364
|
+
|
|
365
|
+
const secondCell = firstRow.cells[cellIndex + 1];
|
|
366
|
+
if (!secondCell) return;
|
|
367
|
+
|
|
368
|
+
const colspan1 = parseInt(firstCell.getAttribute('colspan') || '1');
|
|
369
|
+
const colspan2 = parseInt(secondCell.getAttribute('colspan') || '1');
|
|
370
|
+
firstCell.setAttribute('colspan', String(colspan1 + colspan2));
|
|
371
|
+
|
|
372
|
+
const secondCellContent = Array.from(secondCell.childNodes);
|
|
373
|
+
secondCellContent.forEach(node => {
|
|
374
|
+
firstCell.appendChild(node);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
secondCell.remove();
|
|
378
|
+
|
|
379
|
+
updateTableInfo();
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// ============================================
|
|
383
|
+
// UTILITY FUNCTIONS
|
|
384
|
+
// ============================================
|
|
385
|
+
|
|
386
|
+
interface TableDOMInfo {
|
|
387
|
+
table: HTMLTableElement;
|
|
388
|
+
rowIndex: number;
|
|
389
|
+
colIndex: number;
|
|
390
|
+
rowCount: number;
|
|
391
|
+
cellCount: number;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function getTableInfoFromDOM(): TableDOMInfo | null {
|
|
395
|
+
const selection = window.getSelection();
|
|
396
|
+
if (!selection || selection.rangeCount === 0) return null;
|
|
397
|
+
|
|
398
|
+
const range = selection.getRangeAt(0);
|
|
399
|
+
const startContainer = range.startContainer;
|
|
400
|
+
|
|
401
|
+
let tableElement = startContainer.nodeType === Node.TEXT_NODE
|
|
402
|
+
? startContainer.parentElement?.closest('table')
|
|
403
|
+
: (startContainer as Element).closest('table');
|
|
404
|
+
|
|
405
|
+
if (!tableElement) return null;
|
|
406
|
+
|
|
407
|
+
const table = tableElement as HTMLTableElement;
|
|
408
|
+
|
|
409
|
+
let rowIndex = 0;
|
|
410
|
+
let colIndex = 0;
|
|
411
|
+
|
|
412
|
+
const cellElement = startContainer.nodeType === Node.TEXT_NODE
|
|
413
|
+
? startContainer.parentElement?.closest('td, th')
|
|
414
|
+
: (startContainer as Element).closest('td, th');
|
|
415
|
+
|
|
416
|
+
if (cellElement) {
|
|
417
|
+
let currentRow = cellElement.parentElement as HTMLTableRowElement;
|
|
418
|
+
while (currentRow && currentRow !== table.rows[rowIndex]) {
|
|
419
|
+
rowIndex++;
|
|
420
|
+
if (rowIndex >= table.rows.length) break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const row = currentRow;
|
|
424
|
+
if (row) {
|
|
425
|
+
for (let i = 0; i < row.cells.length; i++) {
|
|
426
|
+
if (row.cells[i] === cellElement) {
|
|
427
|
+
colIndex = i;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
table,
|
|
436
|
+
rowIndex,
|
|
437
|
+
colIndex,
|
|
438
|
+
rowCount: table.rows.length,
|
|
439
|
+
cellCount: table.rows[0]?.cells.length || 0
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function updateTableInfo(): void {
|
|
444
|
+
if (!toolbarElement || !currentTable) return;
|
|
445
|
+
|
|
446
|
+
const tableInfo = getTableInfoFromDOM();
|
|
447
|
+
if (!tableInfo) return;
|
|
448
|
+
|
|
449
|
+
const canDeleteRow = tableInfo.rowCount > 1;
|
|
450
|
+
const canDeleteColumn = tableInfo.cellCount > 1;
|
|
451
|
+
|
|
452
|
+
updateToolbarButtonStates(canDeleteRow, canDeleteColumn);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ============================================
|
|
456
|
+
// FLOATING TOOLBAR MANAGEMENT
|
|
457
|
+
// ============================================
|
|
458
|
+
|
|
459
|
+
function initTableToolbar(): void {
|
|
460
|
+
selectionChangeHandler = () => {
|
|
461
|
+
const tableInfo = getTableInfoFromDOM();
|
|
462
|
+
if (tableInfo) {
|
|
463
|
+
showTableToolbar(tableInfo.table);
|
|
464
|
+
} else {
|
|
465
|
+
hideTableToolbar();
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
mouseDownHandler = (e: MouseEvent) => {
|
|
470
|
+
const target = e.target as Element;
|
|
471
|
+
const isInsideTable = target.closest('table');
|
|
472
|
+
const isInsideToolbar = target.closest('.table-toolbar');
|
|
473
|
+
|
|
474
|
+
if (!isInsideTable && !isInsideToolbar) {
|
|
475
|
+
hideTableToolbar();
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
tableDeletedHandler = () => {
|
|
480
|
+
hideTableToolbar();
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
scrollHandler = () => {
|
|
484
|
+
if (currentTable && toolbarElement && toolbarElement.style.display !== 'none') {
|
|
485
|
+
updateToolbarPosition(currentTable);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
resizeHandler = () => {
|
|
490
|
+
if (currentTable && toolbarElement && toolbarElement.style.display !== 'none') {
|
|
491
|
+
updateToolbarPosition(currentTable);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
document.addEventListener('selectionchange', selectionChangeHandler);
|
|
496
|
+
document.addEventListener('mousedown', mouseDownHandler);
|
|
497
|
+
document.addEventListener('tableDeleted', tableDeletedHandler as EventListener);
|
|
498
|
+
window.addEventListener('scroll', scrollHandler, true); // Use capture to catch all scroll events
|
|
499
|
+
window.addEventListener('resize', resizeHandler);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function cleanupTableToolbar(): void {
|
|
503
|
+
if (selectionChangeHandler) {
|
|
504
|
+
document.removeEventListener('selectionchange', selectionChangeHandler);
|
|
505
|
+
}
|
|
506
|
+
if (mouseDownHandler) {
|
|
507
|
+
document.removeEventListener('mousedown', mouseDownHandler);
|
|
508
|
+
}
|
|
509
|
+
if (tableDeletedHandler) {
|
|
510
|
+
document.removeEventListener('tableDeleted', tableDeletedHandler as EventListener);
|
|
511
|
+
}
|
|
512
|
+
if (scrollHandler) {
|
|
513
|
+
window.removeEventListener('scroll', scrollHandler, true);
|
|
514
|
+
}
|
|
515
|
+
if (resizeHandler) {
|
|
516
|
+
window.removeEventListener('resize', resizeHandler);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
hideTableToolbar();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function updateToolbarPosition(table: HTMLTableElement): void {
|
|
523
|
+
if (!toolbarElement) return;
|
|
524
|
+
|
|
525
|
+
const rect = table.getBoundingClientRect();
|
|
526
|
+
const toolbarRect = toolbarElement.getBoundingClientRect();
|
|
527
|
+
const toolbarHeight = toolbarRect.height || 40;
|
|
528
|
+
const toolbarWidth = toolbarRect.width || 280;
|
|
529
|
+
const padding = 10;
|
|
530
|
+
|
|
531
|
+
// Smart viewport collision detection
|
|
532
|
+
let top = rect.top - toolbarHeight - padding;
|
|
533
|
+
let left = rect.left + (rect.width / 2) - (toolbarWidth / 2);
|
|
534
|
+
|
|
535
|
+
// Adjust if off-screen (top) - show below if no room above
|
|
536
|
+
if (top < padding) {
|
|
537
|
+
top = rect.bottom + padding;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Adjust if off-screen (left)
|
|
541
|
+
if (left < padding) {
|
|
542
|
+
left = padding;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Adjust if off-screen (right)
|
|
546
|
+
const viewportWidth = window.innerWidth;
|
|
547
|
+
if (left + toolbarWidth > viewportWidth - padding) {
|
|
548
|
+
left = viewportWidth - toolbarWidth - padding;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Adjust if off-screen (bottom)
|
|
552
|
+
const viewportHeight = window.innerHeight;
|
|
553
|
+
if (top + toolbarHeight > viewportHeight - padding) {
|
|
554
|
+
top = viewportHeight - toolbarHeight - padding;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
toolbarElement.style.top = top + 'px';
|
|
558
|
+
toolbarElement.style.left = left + 'px';
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function showTableToolbar(table: HTMLTableElement): void {
|
|
562
|
+
currentTable = table;
|
|
563
|
+
|
|
564
|
+
if (!toolbarElement) {
|
|
565
|
+
toolbarElement = createTableToolbar();
|
|
566
|
+
document.body.appendChild(toolbarElement);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const isDarkTheme =
|
|
570
|
+
!!table.closest(DARK_THEME_SELECTOR) ||
|
|
571
|
+
document.body.matches(DARK_THEME_SELECTOR) ||
|
|
572
|
+
document.documentElement.matches(DARK_THEME_SELECTOR);
|
|
573
|
+
toolbarElement.classList.toggle('rte-theme-dark', isDarkTheme);
|
|
574
|
+
|
|
575
|
+
// Make toolbar visible temporarily to measure its dimensions
|
|
576
|
+
toolbarElement.style.display = 'flex';
|
|
577
|
+
toolbarElement.style.visibility = 'hidden';
|
|
578
|
+
|
|
579
|
+
// Small delay to ensure toolbar is rendered
|
|
580
|
+
requestAnimationFrame(() => {
|
|
581
|
+
updateToolbarPosition(table);
|
|
582
|
+
if (toolbarElement) {
|
|
583
|
+
toolbarElement.style.visibility = 'visible';
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Update button states
|
|
588
|
+
const tableInfo = getTableInfoFromDOM();
|
|
589
|
+
if (tableInfo) {
|
|
590
|
+
updateToolbarButtonStates(tableInfo.rowCount > 1, tableInfo.cellCount > 1);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Attach resize handles
|
|
594
|
+
attachResizeHandles(table);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function hideTableToolbar(): void {
|
|
598
|
+
if (toolbarElement) {
|
|
599
|
+
toolbarElement.style.display = 'none';
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Remove resize handles
|
|
603
|
+
if (currentTable) {
|
|
604
|
+
const handles = currentTable.querySelectorAll('.resize-handle');
|
|
605
|
+
handles.forEach(handle => handle.remove());
|
|
606
|
+
|
|
607
|
+
const tableResizeHandle = currentTable.querySelector('.table-resize-handle');
|
|
608
|
+
if (tableResizeHandle) {
|
|
609
|
+
tableResizeHandle.remove();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
currentTable = null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function updateToolbarButtonStates(canDeleteRow: boolean, canDeleteColumn: boolean): void {
|
|
617
|
+
if (!toolbarElement) return;
|
|
618
|
+
|
|
619
|
+
const deleteRowBtn = toolbarElement.querySelector('[data-action="deleteRow"]') as HTMLButtonElement;
|
|
620
|
+
const deleteColBtn = toolbarElement.querySelector('[data-action="deleteColumn"]') as HTMLButtonElement;
|
|
621
|
+
|
|
622
|
+
if (deleteRowBtn) deleteRowBtn.disabled = !canDeleteRow;
|
|
623
|
+
if (deleteColBtn) deleteColBtn.disabled = !canDeleteColumn;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function createTableToolbar(): HTMLDivElement {
|
|
627
|
+
const toolbar = document.createElement('div');
|
|
628
|
+
toolbar.className = 'table-toolbar';
|
|
629
|
+
toolbar.style.cssText = `
|
|
630
|
+
position: fixed;
|
|
631
|
+
z-index: 1000;
|
|
632
|
+
display: none;
|
|
633
|
+
`;
|
|
634
|
+
toolbar.setAttribute('role', 'toolbar');
|
|
635
|
+
toolbar.setAttribute('aria-label', 'Table editing toolbar');
|
|
636
|
+
|
|
637
|
+
// Helper function to create icon button
|
|
638
|
+
const createButton = (config: {
|
|
639
|
+
icon: string;
|
|
640
|
+
title: string;
|
|
641
|
+
action: string;
|
|
642
|
+
danger?: boolean;
|
|
643
|
+
delete?: boolean;
|
|
644
|
+
}) => {
|
|
645
|
+
const btn = document.createElement('button');
|
|
646
|
+
btn.className = 'toolbar-icon-btn';
|
|
647
|
+
if (config.danger) btn.classList.add('toolbar-icon-btn-danger');
|
|
648
|
+
if (config.delete) btn.classList.add('toolbar-icon-btn-delete');
|
|
649
|
+
btn.innerHTML = config.icon;
|
|
650
|
+
btn.title = config.title;
|
|
651
|
+
btn.setAttribute('aria-label', config.title);
|
|
652
|
+
btn.setAttribute('type', 'button');
|
|
653
|
+
btn.setAttribute('data-action', config.action);
|
|
654
|
+
btn.onclick = () => executeTableCommand(config.action);
|
|
655
|
+
return btn;
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const createDivider = () => {
|
|
659
|
+
const divider = document.createElement('div');
|
|
660
|
+
divider.className = 'toolbar-divider';
|
|
661
|
+
return divider;
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const createSection = (...buttons: HTMLButtonElement[]) => {
|
|
665
|
+
const section = document.createElement('div');
|
|
666
|
+
section.className = 'toolbar-section';
|
|
667
|
+
buttons.forEach(btn => section.appendChild(btn));
|
|
668
|
+
return section;
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// Row operations section
|
|
672
|
+
const rowSection = createSection(
|
|
673
|
+
createButton({
|
|
674
|
+
icon: getIconAddRowAbove(),
|
|
675
|
+
title: 'Add row above (Ctrl+Shift+R)',
|
|
676
|
+
action: 'addRowAbove'
|
|
677
|
+
}),
|
|
678
|
+
createButton({
|
|
679
|
+
icon: getIconAddRowBelow(),
|
|
680
|
+
title: 'Add row below',
|
|
681
|
+
action: 'addRowBelow'
|
|
682
|
+
}),
|
|
683
|
+
createButton({
|
|
684
|
+
icon: getIconDeleteRow(),
|
|
685
|
+
title: 'Delete row',
|
|
686
|
+
action: 'deleteRow',
|
|
687
|
+
danger: true
|
|
688
|
+
})
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
// Column operations section
|
|
692
|
+
const colSection = createSection(
|
|
693
|
+
createButton({
|
|
694
|
+
icon: getIconAddColumnLeft(),
|
|
695
|
+
title: 'Add column left',
|
|
696
|
+
action: 'addColumnLeft'
|
|
697
|
+
}),
|
|
698
|
+
createButton({
|
|
699
|
+
icon: getIconAddColumnRight(),
|
|
700
|
+
title: 'Add column right (Ctrl+Shift+C)',
|
|
701
|
+
action: 'addColumnRight'
|
|
702
|
+
}),
|
|
703
|
+
createButton({
|
|
704
|
+
icon: getIconDeleteColumn(),
|
|
705
|
+
title: 'Delete column',
|
|
706
|
+
action: 'deleteColumn',
|
|
707
|
+
danger: true
|
|
708
|
+
})
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
// Header operations section
|
|
712
|
+
const headerSection = createSection(
|
|
713
|
+
createButton({
|
|
714
|
+
icon: getIconToggleHeaderRow(),
|
|
715
|
+
title: 'Toggle header row',
|
|
716
|
+
action: 'toggleHeaderRow'
|
|
717
|
+
}),
|
|
718
|
+
createButton({
|
|
719
|
+
icon: getIconToggleHeaderColumn(),
|
|
720
|
+
title: 'Toggle header column',
|
|
721
|
+
action: 'toggleHeaderColumn'
|
|
722
|
+
})
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
// Merge section
|
|
726
|
+
const mergeSection = createSection(
|
|
727
|
+
createButton({
|
|
728
|
+
icon: getIconMergeCells(),
|
|
729
|
+
title: 'Merge cells (horizontally)',
|
|
730
|
+
action: 'mergeCells'
|
|
731
|
+
})
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
// Delete table section
|
|
735
|
+
const deleteSection = createSection(
|
|
736
|
+
createButton({
|
|
737
|
+
icon: getIconDeleteTable(),
|
|
738
|
+
title: 'Delete table',
|
|
739
|
+
action: 'deleteTable',
|
|
740
|
+
delete: true
|
|
741
|
+
})
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
// Assemble toolbar with dividers
|
|
745
|
+
toolbar.appendChild(rowSection);
|
|
746
|
+
toolbar.appendChild(createDivider());
|
|
747
|
+
toolbar.appendChild(colSection);
|
|
748
|
+
toolbar.appendChild(createDivider());
|
|
749
|
+
toolbar.appendChild(headerSection);
|
|
750
|
+
toolbar.appendChild(createDivider());
|
|
751
|
+
toolbar.appendChild(mergeSection);
|
|
752
|
+
toolbar.appendChild(createDivider());
|
|
753
|
+
toolbar.appendChild(deleteSection);
|
|
754
|
+
|
|
755
|
+
// Add keyboard shortcuts
|
|
756
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
757
|
+
if (!toolbarElement || toolbarElement.style.display === 'none') return;
|
|
758
|
+
|
|
759
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
|
|
760
|
+
if (e.key === 'r' || e.key === 'R') {
|
|
761
|
+
e.preventDefault();
|
|
762
|
+
addRowBelowCommand();
|
|
763
|
+
} else if (e.key === 'c' || e.key === 'C') {
|
|
764
|
+
e.preventDefault();
|
|
765
|
+
addColumnRightCommand();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
771
|
+
|
|
772
|
+
return toolbar;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function executeTableCommand(action: string): void {
|
|
776
|
+
switch (action) {
|
|
777
|
+
case 'addRowAbove': addRowAboveCommand(); break;
|
|
778
|
+
case 'addRowBelow': addRowBelowCommand(); break;
|
|
779
|
+
case 'addColumnLeft': addColumnLeftCommand(); break;
|
|
780
|
+
case 'addColumnRight': addColumnRightCommand(); break;
|
|
781
|
+
case 'deleteRow': deleteRowCommand(); break;
|
|
782
|
+
case 'deleteColumn': deleteColumnCommand(); break;
|
|
783
|
+
case 'toggleHeaderRow': toggleHeaderRowCommand(); break;
|
|
784
|
+
case 'toggleHeaderColumn': toggleHeaderColumnCommand(); break;
|
|
785
|
+
case 'deleteTable': deleteTableCommand(); break;
|
|
786
|
+
case 'mergeCells': mergeCellsCommand(); break;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ============================================
|
|
791
|
+
// COLUMN & TABLE RESIZING
|
|
792
|
+
// ============================================
|
|
793
|
+
|
|
794
|
+
function attachResizeHandles(table: HTMLTableElement): void {
|
|
795
|
+
// Remove existing handles first
|
|
796
|
+
const existingHandles = table.querySelectorAll('.resize-handle');
|
|
797
|
+
existingHandles.forEach(handle => handle.remove());
|
|
798
|
+
|
|
799
|
+
const existingTableHandle = table.querySelector('.table-resize-handle');
|
|
800
|
+
if (existingTableHandle) existingTableHandle.remove();
|
|
801
|
+
|
|
802
|
+
const headerRow = table.querySelector('thead tr, tbody tr:first-child') as HTMLTableRowElement;
|
|
803
|
+
if (!headerRow) return;
|
|
804
|
+
|
|
805
|
+
const cells = headerRow.querySelectorAll('td, th');
|
|
806
|
+
|
|
807
|
+
// Add column resize handles (skip last column)
|
|
808
|
+
cells.forEach((cell, index) => {
|
|
809
|
+
if (index === cells.length - 1) return;
|
|
810
|
+
|
|
811
|
+
const handle = document.createElement('div');
|
|
812
|
+
handle.className = 'resize-handle';
|
|
813
|
+
handle.style.cssText = `
|
|
814
|
+
position: absolute;
|
|
815
|
+
right: -4px;
|
|
816
|
+
top: 0;
|
|
817
|
+
bottom: 0;
|
|
818
|
+
width: 8px;
|
|
819
|
+
background: transparent;
|
|
820
|
+
cursor: col-resize;
|
|
821
|
+
z-index: 10;
|
|
822
|
+
transition: background 0.15s ease;
|
|
823
|
+
`;
|
|
824
|
+
|
|
825
|
+
handle.addEventListener('mouseenter', () => {
|
|
826
|
+
if (!isResizing) {
|
|
827
|
+
handle.style.background = 'rgba(0, 102, 204, 0.3)';
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
handle.addEventListener('mouseleave', () => {
|
|
832
|
+
if (!isResizing) {
|
|
833
|
+
handle.style.background = 'transparent';
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
handle.addEventListener('mousedown', (e) => {
|
|
838
|
+
e.preventDefault();
|
|
839
|
+
e.stopPropagation();
|
|
840
|
+
startColumnResize(e as MouseEvent, index);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
(cell as HTMLElement).style.position = 'relative';
|
|
844
|
+
cell.appendChild(handle);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// Add table-level resize handle
|
|
848
|
+
const tableResizeHandle = document.createElement('div');
|
|
849
|
+
tableResizeHandle.className = 'table-resize-handle';
|
|
850
|
+
tableResizeHandle.addEventListener('mousedown', (e) => {
|
|
851
|
+
e.preventDefault();
|
|
852
|
+
e.stopPropagation();
|
|
853
|
+
startTableResize(e as MouseEvent);
|
|
854
|
+
});
|
|
855
|
+
table.appendChild(tableResizeHandle);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function startColumnResize(e: MouseEvent, columnIndex: number): void {
|
|
859
|
+
isResizing = true;
|
|
860
|
+
resizeColumn = columnIndex;
|
|
861
|
+
startX = e.clientX;
|
|
862
|
+
|
|
863
|
+
if (!currentTable) return;
|
|
864
|
+
|
|
865
|
+
const headerRow = currentTable.querySelector('thead tr, tbody tr:first-child') as HTMLTableRowElement;
|
|
866
|
+
if (headerRow && headerRow.cells[columnIndex]) {
|
|
867
|
+
startWidth = (headerRow.cells[columnIndex] as HTMLElement).offsetWidth;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
document.body.style.cursor = 'col-resize';
|
|
871
|
+
document.body.style.userSelect = 'none';
|
|
872
|
+
|
|
873
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
874
|
+
if (!isResizing || resizeColumn === null || !currentTable) return;
|
|
875
|
+
|
|
876
|
+
const deltaX = e.clientX - startX;
|
|
877
|
+
const newWidth = Math.max(50, startWidth + deltaX);
|
|
878
|
+
|
|
879
|
+
// Set width for all cells in this column
|
|
880
|
+
const allRows = currentTable.querySelectorAll('tr') as NodeListOf<HTMLTableRowElement>;
|
|
881
|
+
allRows.forEach(row => {
|
|
882
|
+
if (row.cells[resizeColumn!]) {
|
|
883
|
+
(row.cells[resizeColumn!] as HTMLElement).style.width = newWidth + 'px';
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const handleMouseUp = () => {
|
|
889
|
+
isResizing = false;
|
|
890
|
+
resizeColumn = null;
|
|
891
|
+
document.body.style.cursor = '';
|
|
892
|
+
document.body.style.userSelect = '';
|
|
893
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
894
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
898
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function startTableResize(e: MouseEvent): void {
|
|
902
|
+
if (!currentTable) return;
|
|
903
|
+
|
|
904
|
+
isTableResizing = true;
|
|
905
|
+
tableStartX = e.clientX;
|
|
906
|
+
tableStartY = e.clientY;
|
|
907
|
+
tableStartWidth = currentTable.offsetWidth;
|
|
908
|
+
tableStartHeight = currentTable.offsetHeight;
|
|
909
|
+
|
|
910
|
+
document.body.style.cursor = 'nwse-resize';
|
|
911
|
+
document.body.style.userSelect = 'none';
|
|
912
|
+
|
|
913
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
914
|
+
if (!isTableResizing || !currentTable) return;
|
|
915
|
+
|
|
916
|
+
const deltaX = e.clientX - tableStartX;
|
|
917
|
+
const deltaY = e.clientY - tableStartY;
|
|
918
|
+
const newWidth = Math.max(200, tableStartWidth + deltaX);
|
|
919
|
+
const newHeight = Math.max(100, tableStartHeight + deltaY);
|
|
920
|
+
|
|
921
|
+
currentTable.style.width = newWidth + 'px';
|
|
922
|
+
currentTable.style.height = newHeight + 'px';
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const handleMouseUp = () => {
|
|
926
|
+
isTableResizing = false;
|
|
927
|
+
document.body.style.cursor = '';
|
|
928
|
+
document.body.style.userSelect = '';
|
|
929
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
930
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
934
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// ============================================
|
|
938
|
+
// SVG ICONS (matching React version exactly)
|
|
939
|
+
// ============================================
|
|
940
|
+
|
|
941
|
+
function getIconAddRowAbove(): string {
|
|
942
|
+
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
943
|
+
<path d="M2 7h12V5H2v2zm0 4h12V9H2v2zM8 1v3H5v2h3v3h2V6h3V4h-3V1H8z"/>
|
|
944
|
+
</svg>`;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function getIconAddRowBelow(): string {
|
|
948
|
+
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
949
|
+
<path d="M2 3h12V1H2v2zm0 4h12V5H2v2zm6 4v3h3v-2h2v-2h-2v-3h-2v3H5v2h3z"/>
|
|
950
|
+
</svg>`;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function getIconDeleteRow(): string {
|
|
954
|
+
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
955
|
+
<path d="M2 5h12v2H2V5zm0 4h12v2H2V9zm4-6v2H4v2h2v2h2V7h2V5H8V3H6z"/>
|
|
956
|
+
</svg>`;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function getIconAddColumnLeft(): string {
|
|
960
|
+
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
961
|
+
<path d="M7 2v12h2V2H7zm4 0v12h2V2h-2zM1 8h3v-3H1v3zm3 2H1v3h3v-3z"/>
|
|
962
|
+
</svg>`;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function getIconAddColumnRight(): string {
|
|
966
|
+
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
967
|
+
<path d="M2 2v12h2V2H2zm4 0v12h2V2H6zM12 8h3v-3h-3v3zm0 2h3v3h-3v-3z"/>
|
|
968
|
+
</svg>`;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function getIconDeleteColumn(): string {
|
|
972
|
+
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
973
|
+
<path d="M5 2v12h2V2H5zm4 0v12h2V2H9zm3 2h3V1h-3v3zm3 2h-3v3h3V6zm0 4h-3v3h3v-3z"/>
|
|
974
|
+
</svg>`;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function getIconToggleHeaderRow(): string {
|
|
978
|
+
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
979
|
+
<path d="M2 2h12v3H2V2zm0 5h12v8H2V7zm2 2v4h2V9H4zm4 0v4h2V9H8zm4 0v4h2V9h-2z"/>
|
|
980
|
+
</svg>`;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function getIconToggleHeaderColumn(): string {
|
|
984
|
+
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
985
|
+
<path d="M2 2v12h3V2H2zm5 0v12h8V2H7zm2 2h4v2H9V4zm0 4h4v2H9V8zm0 4h4v2H9v-2z"/>
|
|
986
|
+
</svg>`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function getIconDeleteTable(): string {
|
|
990
|
+
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
991
|
+
<path d="M3 1h10v1H3V1zm1 2v11h8V3H4zM6 5h1v6H6V5zm3 0h1v6H9V5z"/>
|
|
992
|
+
</svg>`;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function getIconMergeCells(): string {
|
|
996
|
+
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
997
|
+
<path d="M2 2h4v3H2V2zm5 0h4v3H7V2zm5 0h2v3h-2V2zm-10 4h4v3H2V6zm5 0h4v3H7V6zm5 0h2v3h-2V6zm-10 4h4v3H2v-3zm5 0h4v3H7v-3zm5 0h2v3h-2v-3z"/>
|
|
998
|
+
</svg>`;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ============================================
|
|
1002
|
+
// MODULE-LEVEL INITIALIZATION
|
|
1003
|
+
// ============================================
|
|
1004
|
+
|
|
1005
|
+
// Initialize table toolbar monitoring
|
|
1006
|
+
if (typeof window !== 'undefined' && !window.__tablePluginInitialized) {
|
|
1007
|
+
window.__tablePluginInitialized = true;
|
|
1008
|
+
|
|
1009
|
+
const initTablePlugin = () => {
|
|
1010
|
+
initTableToolbar();
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
if (document.readyState === 'loading') {
|
|
1014
|
+
document.addEventListener('DOMContentLoaded', initTablePlugin);
|
|
1015
|
+
} else {
|
|
1016
|
+
// If DOM is already ready, init immediately
|
|
1017
|
+
setTimeout(initTablePlugin, 100);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// ============================================
|
|
1022
|
+
// PLUGIN DEFINITION
|
|
1023
|
+
// ============================================
|
|
1024
|
+
|
|
1025
|
+
export const TablePlugin = (): Plugin => ({
|
|
1026
|
+
name: "table",
|
|
1027
|
+
|
|
1028
|
+
toolbar: [
|
|
1029
|
+
{
|
|
1030
|
+
label: "Insert Table",
|
|
1031
|
+
command: "insertTable",
|
|
1032
|
+
icon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" focusable="false" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M3 10h18M3 15h18M9 4v16M15 4v16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
1033
|
+
},
|
|
1034
|
+
],
|
|
1035
|
+
|
|
1036
|
+
commands: {
|
|
1037
|
+
insertTable: () => {
|
|
1038
|
+
insertTableCommand();
|
|
1039
|
+
return true;
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
|
|
1043
|
+
keymap: {
|
|
1044
|
+
"Mod-Shift-r": () => {
|
|
1045
|
+
addRowBelowCommand();
|
|
1046
|
+
return true;
|
|
1047
|
+
},
|
|
1048
|
+
"Mod-Shift-c": () => {
|
|
1049
|
+
addColumnRightCommand();
|
|
1050
|
+
return true;
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
});
|