autumnnote 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +332 -638
  2. package/dist/autumnnote.css +40 -4
  3. package/dist/autumnnote.es.js +3375 -262
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.umd.js +3374 -261
  6. package/dist/autumnnote.umd.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/js/Context.js +4 -0
  9. package/src/js/editing/Style.js +195 -44
  10. package/src/js/editing/Typing.js +2 -2
  11. package/src/js/i18n/de.js +320 -0
  12. package/src/js/i18n/en.js +328 -0
  13. package/src/js/i18n/es.js +320 -0
  14. package/src/js/i18n/fr.js +321 -0
  15. package/src/js/i18n/index.js +59 -0
  16. package/src/js/i18n/ja.js +321 -0
  17. package/src/js/i18n/ko.js +320 -0
  18. package/src/js/i18n/vi.js +321 -0
  19. package/src/js/i18n/zh.js +321 -0
  20. package/src/js/index.js +2 -1
  21. package/src/js/module/CodeTooltip.js +12 -9
  22. package/src/js/module/ContextMenu.js +13 -11
  23. package/src/js/module/Editor.js +14 -3
  24. package/src/js/module/EmojiDialog.js +19 -7
  25. package/src/js/module/FindReplace.js +14 -13
  26. package/src/js/module/IconDialog.js +25 -13
  27. package/src/js/module/ImageDialog.js +25 -20
  28. package/src/js/module/ImageTooltip.js +13 -12
  29. package/src/js/module/LinkDialog.js +10 -9
  30. package/src/js/module/LinkTooltip.js +6 -5
  31. package/src/js/module/Placeholder.js +6 -1
  32. package/src/js/module/ShortcutsDialog.js +5 -4
  33. package/src/js/module/Statusbar.js +6 -5
  34. package/src/js/module/TableTooltip.js +412 -102
  35. package/src/js/module/Toolbar.js +21 -15
  36. package/src/js/module/VideoDialog.js +10 -9
  37. package/src/js/module/VideoTooltip.js +12 -11
  38. package/src/js/settings.js +4 -0
  39. package/src/styles/autumnnote.scss +50 -5
  40. package/types/index.d.ts +58 -1
@@ -64,6 +64,43 @@ function getCellAfterVisualCol(row, visualIdx) {
64
64
  return null;
65
65
  }
66
66
 
67
+ /**
68
+ * Build a 2D grid map of the table, accounting for both rowspan and colspan.
69
+ *
70
+ * gridMap[r][c] = the DOM cell occupying visual grid position (r, c).
71
+ * cellPos = WeakMap: cell → { r, c, rs, cs } (top-left grid origin + span).
72
+ *
73
+ * Uses HTMLTableElement.rows which is scoped to the table itself and never
74
+ * includes rows from nested tables.
75
+ *
76
+ * @param {HTMLTableElement} table
77
+ * @returns {{ gridMap: Object, cellPos: WeakMap }}
78
+ */
79
+ function buildGridMap(table) {
80
+ const rows = Array.from(table.rows);
81
+ const gridMap = {};
82
+ const cellPos = new WeakMap();
83
+ rows.forEach((row, r) => {
84
+ if (!gridMap[r]) gridMap[r] = {};
85
+ let c = 0;
86
+ for (const cell of row.cells) {
87
+ // Skip positions already occupied by a rowspan from a previous row
88
+ while (gridMap[r][c]) c++;
89
+ const rs = cell.rowSpan || 1;
90
+ const cs = cell.colSpan || 1;
91
+ cellPos.set(cell, { r, c, rs, cs });
92
+ for (let dr = 0; dr < rs; dr++) {
93
+ if (!gridMap[r + dr]) gridMap[r + dr] = {};
94
+ for (let dc = 0; dc < cs; dc++) {
95
+ gridMap[r + dr][c + dc] = cell;
96
+ }
97
+ }
98
+ c += cs;
99
+ }
100
+ });
101
+ return { gridMap, cellPos };
102
+ }
103
+
67
104
  const ICONS = {
68
105
  rowAbove: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="12" x2="21" y2="12"/><path d="M12 3v7"/><path d="M9 7l3-4 3 4"/></svg>`,
69
106
  rowBelow: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="12" x2="21" y2="12"/><path d="M12 12v7"/><path d="M9 17l3 4 3-4"/></svg>`,
@@ -71,11 +108,13 @@ const ICONS = {
71
108
  colLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="12" y1="3" x2="12" y2="21"/><path d="M3 12h7"/><path d="M7 8l-4 4 4 4"/></svg>`,
72
109
  colRight: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="12" y1="3" x2="12" y2="21"/><path d="M12 12h9"/><path d="M17 8l4 4-4 4"/></svg>`,
73
110
  deleteCol: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="15" y1="6" x2="21" y2="12"/><line x1="21" y1="6" x2="15" y2="12"/></svg>`,
74
- mergeCells: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="8" height="10" rx="1"/><rect x="14" y="7" width="8" height="10" rx="1"/><path d="M10 12h4"/><path d="M12 10l2 2-2 2"/></svg>`,
111
+ mergeCells: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="8" height="10" rx="1"/><rect x="14" y="7" width="8" height="10" rx="1"/><path d="M10 12h4"/><path d="M12 10l2 2-2 2"/></svg>`,
112
+ unmergeCells: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="5" width="20" height="14" rx="1"/><line x1="12" y1="5" x2="12" y2="19" stroke-dasharray="2.5 2"/><line x1="2" y1="12" x2="22" y2="12" stroke-dasharray="2.5 2"/><path d="M9 9 L6 12 L9 15"/><path d="M15 9 L18 12 L15 15"/></svg>`,
75
113
  colWidth: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="4" x2="7" y2="20"/><line x1="17" y1="4" x2="17" y2="20"/><line x1="7" y1="12" x2="17" y2="12"/><path d="M10 9l-3 3 3 3"/><path d="M14 9l3 3-3 3"/></svg>`,
76
114
  rowHeight: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="7" x2="20" y2="7"/><line x1="4" y1="17" x2="20" y2="17"/><line x1="12" y1="7" x2="12" y2="17"/><path d="M9 10l3-3 3 3"/><path d="M9 14l3 3 3-3"/></svg>`,
77
115
  tableBorder: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6" stroke-width="1"/><line x1="3" y1="13" x2="21" y2="13" stroke-width="2"/><line x1="3" y1="20" x2="21" y2="20" stroke-width="3"/></svg>`,
78
116
  deleteTable: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/><line x1="16" y1="16" x2="22" y2="22" stroke="#ef4444"/><line x1="22" y1="16" x2="16" y2="22" stroke="#ef4444"/></svg>`,
117
+ selectCells: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4 L4 20 L9 15 L12 21 L14 20 L11 14 L17 14 Z" fill="currentColor" opacity="0.15"/><path d="M4 4 L4 20 L9 15 L12 21 L14 20 L11 14 L17 14 Z"/></svg>`,
79
118
  };
80
119
 
81
120
  export class TableTooltip {
@@ -92,6 +131,13 @@ export class TableTooltip {
92
131
  this._sizeApply = null;
93
132
  this._sizeTitleEl = null;
94
133
  this._sizeInputEl = null;
134
+ // Cell selection
135
+ this._selectMode = false;
136
+ this._selectedCells = [];
137
+ this._selectStart = null;
138
+ this._selectDragging = false;
139
+ this._selectBtn = null;
140
+ this._editable = null;
95
141
  }
96
142
 
97
143
  initialize() {
@@ -102,6 +148,36 @@ export class TableTooltip {
102
148
  document.body.appendChild(this._sizePopover);
103
149
 
104
150
  const editable = this.context.layoutInfo.editable;
151
+ this._editable = editable;
152
+
153
+ // ── Cell selection drag ──────────────────────────────────────────────────
154
+ const onSelMousedown = (e) => {
155
+ if (!this._selectMode) return;
156
+ const cell = e.target.closest('td, th');
157
+ if (!cell || !editable.contains(cell)) return;
158
+ // Don't interfere with col/row resize (inline cursor is set by resize logic)
159
+ if (cell.style.cursor === 'col-resize' || cell.style.cursor === 'row-resize') return;
160
+ e.preventDefault(); // suppress text-cursor placement while selecting cells
161
+ this._activeTable = cell.closest('table');
162
+ this._selectStart = cell;
163
+ this._selectDragging = true;
164
+ this._setSelection([cell]);
165
+ };
166
+ const onSelMousemove = (e) => {
167
+ if (!this._selectMode || !this._selectDragging || !this._selectStart) return;
168
+ const cell = e.target.closest('td, th');
169
+ if (!cell || !editable.contains(cell)) return;
170
+ if (cell.closest('table') !== this._activeTable) return;
171
+ this._setSelection(this._getRectCells(this._selectStart, cell));
172
+ };
173
+ const onSelMouseup = () => { this._selectDragging = false; };
174
+
175
+ this._disposers.push(
176
+ on(editable, 'mousedown', onSelMousedown),
177
+ on(editable, 'mousemove', onSelMousemove),
178
+ on(document, 'mouseup', onSelMouseup),
179
+ );
180
+ // ────────────────────────────────────────────────────────────────────────
105
181
 
106
182
  this._disposers.push(
107
183
  on(editable, 'mouseover', (e) => {
@@ -114,6 +190,7 @@ export class TableTooltip {
114
190
  }
115
191
  }, { passive: true }),
116
192
  on(editable, 'mouseout', (e) => {
193
+ if (this._selectMode) return; // keep tooltip alive during cell selection
117
194
  const to = e.relatedTarget;
118
195
  if (!to || (
119
196
  !editable.contains(to) &&
@@ -124,6 +201,7 @@ export class TableTooltip {
124
201
  }
125
202
  }, { passive: true }),
126
203
  on(document, 'click', (e) => {
204
+ if (this._selectMode && this._activeTable && this._activeTable.contains(e.target)) return;
127
205
  if (this._activeTable &&
128
206
  !this._activeTable.contains(e.target) &&
129
207
  !this._el.contains(e.target) &&
@@ -210,11 +288,16 @@ export class TableTooltip {
210
288
  if (_edge === 'col') {
211
289
  _startW = _nearCell.offsetWidth;
212
290
  _colIdx = getVisualColIndex(_nearCell);
213
- // Cache column cells once so onDocMove never runs querySelectorAll per frame
291
+ // Cache column cells once so onDocMove never runs querySelectorAll per frame.
292
+ // F-1: skip merged cells (colSpan > 1) — setting width on a merged cell
293
+ // distributes it equally across all spanned columns instead of resizing
294
+ // only the target column. Rows that have an individual cell at this
295
+ // visual column index are resized independently as expected.
214
296
  _colCells = _colIdx >= 0
215
297
  ? Array.from(_table.querySelectorAll('tr'))
216
298
  .map(r => getCellAtVisualCol(r, _colIdx))
217
299
  .filter(Boolean)
300
+ .filter(c => (c.colSpan || 1) === 1)
218
301
  : [];
219
302
  document.body.style.cursor = 'col-resize';
220
303
  } else {
@@ -293,48 +376,56 @@ export class TableTooltip {
293
376
  // ---------------------------------------------------------------------------
294
377
 
295
378
  _buildTooltip() {
379
+ const L = this.context.locale.tooltips.table;
296
380
  const el = createElement('div', {
297
381
  class: 'an-link-tooltip an-table-tooltip',
298
382
  role: 'toolbar',
299
- 'aria-label': 'Table actions',
383
+ 'aria-label': L.ariaLabel,
300
384
  });
301
385
  el.style.display = 'none';
302
386
 
303
387
  // Label
304
388
  this._label = createElement('span', { class: 'an-link-tooltip-url' });
305
- this._label.textContent = 'Table';
389
+ this._label.textContent = L.label;
306
390
  el.appendChild(this._label);
307
391
 
308
392
  el.appendChild(this._sep());
309
393
 
394
+ // Select-cells toggle
395
+ this._selectBtn = this._makeBtn(ICONS.selectCells, L.selectCells, () => this._toggleSelectMode());
396
+ el.appendChild(this._selectBtn);
397
+
398
+ el.appendChild(this._sep());
399
+
310
400
  // Row operations
311
- el.appendChild(this._makeBtn(ICONS.rowAbove, 'Add Row Above', () => this._addRow('above')));
312
- el.appendChild(this._makeBtn(ICONS.rowBelow, 'Add Row Below', () => this._addRow('below')));
313
- el.appendChild(this._makeBtn(ICONS.deleteRow, 'Delete Row', () => this._deleteRow()));
401
+ el.appendChild(this._makeBtn(ICONS.rowAbove, L.addRowAbove, () => this._addRow('above')));
402
+ el.appendChild(this._makeBtn(ICONS.rowBelow, L.addRowBelow, () => this._addRow('below')));
403
+ el.appendChild(this._makeBtn(ICONS.deleteRow, L.deleteRow, () => this._deleteRow()));
314
404
 
315
405
  el.appendChild(this._sep());
316
406
 
317
407
  // Column operations
318
- el.appendChild(this._makeBtn(ICONS.colLeft, 'Add Column Left', () => this._addColumn('left')));
319
- el.appendChild(this._makeBtn(ICONS.colRight, 'Add Column Right', () => this._addColumn('right')));
320
- el.appendChild(this._makeBtn(ICONS.deleteCol, 'Delete Column', () => this._deleteColumn()));
408
+ el.appendChild(this._makeBtn(ICONS.colLeft, L.addColumnLeft, () => this._addColumn('left')));
409
+ el.appendChild(this._makeBtn(ICONS.colRight, L.addColumnRight, () => this._addColumn('right')));
410
+ el.appendChild(this._makeBtn(ICONS.deleteCol, L.deleteColumn, () => this._deleteColumn()));
321
411
 
322
412
  el.appendChild(this._sep());
323
413
 
324
414
  // Merge cells
325
- el.appendChild(this._makeBtn(ICONS.mergeCells, 'Merge Cells', () => this._mergeCells()));
415
+ el.appendChild(this._makeBtn(ICONS.mergeCells, L.mergeCells, () => this._mergeCells()));
416
+ el.appendChild(this._makeBtn(ICONS.unmergeCells, L.unmergeCells, () => this._unmergeCells()));
326
417
 
327
418
  el.appendChild(this._sep());
328
419
 
329
420
  // Resize
330
- el.appendChild(this._makeBtn(ICONS.colWidth, 'Column Width', () => this._openSizePopover('col')));
331
- el.appendChild(this._makeBtn(ICONS.rowHeight, 'Row Height', () => this._openSizePopover('row')));
332
- el.appendChild(this._makeBtn(ICONS.tableBorder,'Table Border Width',() => this._openSizePopover('border')));
421
+ el.appendChild(this._makeBtn(ICONS.colWidth, L.columnWidth, () => this._openSizePopover('col')));
422
+ el.appendChild(this._makeBtn(ICONS.rowHeight, L.rowHeight, () => this._openSizePopover('row')));
423
+ el.appendChild(this._makeBtn(ICONS.tableBorder,L.tableBorderWidth, () => this._openSizePopover('border')));
333
424
 
334
425
  el.appendChild(this._sep());
335
426
 
336
427
  // Delete table (danger)
337
- el.appendChild(this._makeBtn(ICONS.deleteTable, 'Delete Table', () => this._deleteTable(), true));
428
+ el.appendChild(this._makeBtn(ICONS.deleteTable, L.deleteTable, () => this._deleteTable(), true));
338
429
 
339
430
  // Keep tooltip alive while hovering.
340
431
  // Don't schedule hide on mouseleave when the size popover is open —
@@ -342,6 +433,7 @@ export class TableTooltip {
342
433
  this._disposers.push(
343
434
  on(el, 'mouseenter', () => this._clearTimers()),
344
435
  on(el, 'mouseleave', () => {
436
+ if (this._selectMode) return; // keep tooltip alive during cell selection
345
437
  if (this._sizePopover && this._sizePopover.style.display !== 'none') return;
346
438
  this._scheduleHide();
347
439
  }),
@@ -412,6 +504,13 @@ export class TableTooltip {
412
504
  this._el.style.display = 'none';
413
505
  this._activeTable = null;
414
506
  this._activeCell = null;
507
+ // Reset select mode
508
+ if (this._selectMode) {
509
+ this._selectMode = false;
510
+ if (this._selectBtn) this._selectBtn.classList.remove('an-link-tooltip-btn--active');
511
+ if (this._editable) this._editable.classList.remove('an-table-select-mode');
512
+ }
513
+ this._clearSelection();
415
514
  this._clearTimers();
416
515
  this._hideSizePopover();
417
516
  }
@@ -461,18 +560,129 @@ export class TableTooltip {
461
560
  || (this._activeTable && this._activeTable.querySelector('td, th'));
462
561
  }
463
562
 
563
+ // ---------------------------------------------------------------------------
564
+ // Cell selection helpers
565
+ // ---------------------------------------------------------------------------
566
+
567
+ _toggleSelectMode() {
568
+ this._selectMode = !this._selectMode;
569
+ if (this._selectBtn) {
570
+ this._selectBtn.classList.toggle('an-link-tooltip-btn--active', this._selectMode);
571
+ }
572
+ if (this._editable) {
573
+ this._editable.classList.toggle('an-table-select-mode', this._selectMode);
574
+ }
575
+ if (!this._selectMode) {
576
+ this._clearSelection();
577
+ }
578
+ }
579
+
580
+ _clearSelection() {
581
+ this._selectedCells.forEach((c) => c.classList.remove('an-cell-selected'));
582
+ this._selectedCells = [];
583
+ this._selectStart = null;
584
+ }
585
+
586
+ _setSelection(cells) {
587
+ // Remove highlight from cells no longer in selection
588
+ this._selectedCells.forEach((c) => {
589
+ if (!cells.includes(c)) c.classList.remove('an-cell-selected');
590
+ });
591
+ this._selectedCells = cells;
592
+ cells.forEach((c) => c.classList.add('an-cell-selected'));
593
+ }
594
+
595
+ /**
596
+ * Returns all cells in the rectangular area between startCell and endCell,
597
+ * correctly handling rowspan/colspan by using the grid map.
598
+ * The rect is expanded iteratively until it is stable — this ensures any
599
+ * merged cell that starts outside the initial rect but spans into it is
600
+ * fully included.
601
+ */
602
+ _getRectCells(startCell, endCell) {
603
+ if (!startCell) return [];
604
+ if (!endCell || startCell === endCell) return [startCell];
605
+ const table = startCell.closest('table');
606
+ if (!table || !table.contains(endCell)) return [startCell];
607
+
608
+ const { gridMap, cellPos } = buildGridMap(table);
609
+ const sp = cellPos.get(startCell);
610
+ const ep = cellPos.get(endCell);
611
+ if (!sp || !ep) return [startCell];
612
+
613
+ let minR = Math.min(sp.r, ep.r);
614
+ let maxR = Math.max(sp.r + sp.rs - 1, ep.r + ep.rs - 1);
615
+ let minC = Math.min(sp.c, ep.c);
616
+ let maxC = Math.max(sp.c + sp.cs - 1, ep.c + ep.cs - 1);
617
+
618
+ // Iteratively expand until stable — handles spans that exceed the current rect
619
+ let changed = true;
620
+ while (changed) {
621
+ changed = false;
622
+ for (let r = minR; r <= maxR; r++) {
623
+ const rowMap = gridMap[r];
624
+ if (!rowMap) continue;
625
+ for (let c = minC; c <= maxC; c++) {
626
+ const cell = rowMap[c];
627
+ if (!cell) continue;
628
+ const pos = cellPos.get(cell);
629
+ if (!pos) continue;
630
+ if (pos.r < minR) { minR = pos.r; changed = true; }
631
+ if (pos.r + pos.rs - 1 > maxR) { maxR = pos.r + pos.rs - 1; changed = true; }
632
+ if (pos.c < minC) { minC = pos.c; changed = true; }
633
+ if (pos.c + pos.cs - 1 > maxC) { maxC = pos.c + pos.cs - 1; changed = true; }
634
+ }
635
+ }
636
+ }
637
+
638
+ // Collect unique cells in document order
639
+ const seen = new Set();
640
+ const result = [];
641
+ for (let r = minR; r <= maxR; r++) {
642
+ const rowMap = gridMap[r];
643
+ if (!rowMap) continue;
644
+ for (let c = minC; c <= maxC; c++) {
645
+ const cell = rowMap[c];
646
+ if (cell && !seen.has(cell)) {
647
+ seen.add(cell);
648
+ result.push(cell);
649
+ }
650
+ }
651
+ }
652
+ return result.length > 0 ? result : [startCell];
653
+ }
654
+
655
+ /**
656
+ * Returns the active cell set: user-selected cells when available,
657
+ * otherwise the single active/cursor cell.
658
+ * @returns {HTMLTableCellElement[]}
659
+ */
660
+ _getSelectedCells() {
661
+ return this._selectedCells.length > 0
662
+ ? this._selectedCells
663
+ : [this._getCell()].filter(Boolean);
664
+ }
665
+
464
666
  // ---------------------------------------------------------------------------
465
667
  // Table operations
466
668
  // ---------------------------------------------------------------------------
467
669
 
468
670
  _addRow(position) {
469
- const cell = this._getCell();
470
- if (!cell) return;
471
- const row = cell.closest('tr');
472
- if (!row) return;
473
- const colCount = Array.from(row.cells).reduce((sum, c) => sum + (c.colSpan || 1), 0);
671
+ const cells = this._getSelectedCells();
672
+ if (!cells.length) return;
673
+ const table = cells[0].closest('table');
674
+ if (!table) return;
675
+ const allRows = Array.from(table.querySelectorAll('tr'));
676
+ const selectedRows = [...new Set(cells.map((c) => c.closest('tr')).filter(Boolean))];
677
+ // Reference row: topmost for 'above', bottommost for 'below'
678
+ const refRow = selectedRows.reduce((best, r) => {
679
+ const bi = allRows.indexOf(best);
680
+ const ri = allRows.indexOf(r);
681
+ return position === 'above' ? (ri < bi ? r : best) : (ri > bi ? r : best);
682
+ });
683
+ const colCount = Array.from(refRow.cells).reduce((sum, c) => sum + (c.colSpan || 1), 0);
474
684
  const newRow = document.createElement('tr');
475
- const refCells = Array.from(row.cells);
685
+ const refCells = Array.from(refRow.cells);
476
686
  for (let i = 0; i < colCount; i++) {
477
687
  const td = createElement('td', {}, ['\u00a0']);
478
688
  const ref = refCells[i];
@@ -480,25 +690,24 @@ export class TableTooltip {
480
690
  if (ref && ref.style.minWidth) td.style.minWidth = ref.style.minWidth;
481
691
  newRow.appendChild(td);
482
692
  }
483
- if (position === 'above') row.parentElement?.insertBefore(newRow, row);
484
- else row.insertAdjacentElement('afterend', newRow);
485
- // Defer positioning until the browser has painted the new layout
693
+ if (position === 'above') refRow.parentElement?.insertBefore(newRow, refRow);
694
+ else refRow.insertAdjacentElement('afterend', newRow);
486
695
  requestAnimationFrame(() => this._positionNear(this._activeTable));
487
696
  this.context.invoke('editor.afterCommand');
488
697
  }
489
698
 
490
699
  _addColumn(position) {
491
- const cell = this._getCell();
492
- if (!cell) return;
493
- const table = cell.closest('table');
700
+ const cells = this._getSelectedCells();
701
+ if (!cells.length) return;
702
+ const table = cells[0].closest('table');
494
703
  if (!table) return;
495
- const visualColIdx = getVisualColIndex(cell);
704
+ const colIndices = cells.map((c) => getVisualColIndex(c));
705
+ const targetColIdx = position === 'left' ? Math.min(...colIndices) : Math.max(...colIndices);
496
706
  const rows = Array.from(table.querySelectorAll('tr'));
497
- // Batch reads first, then writes — prevents layout thrashing inside the loop
498
- const refs = rows.map(r => position === 'left'
499
- ? getCellAtVisualCol(r, visualColIdx)
500
- : getCellAfterVisualCol(r, visualColIdx));
501
- const isHeaders = rows.map(r => r.closest('thead') !== null);
707
+ const refs = rows.map((r) => position === 'left'
708
+ ? getCellAtVisualCol(r, targetColIdx)
709
+ : getCellAfterVisualCol(r, targetColIdx));
710
+ const isHeaders = rows.map((r) => r.closest('thead') !== null);
502
711
  rows.forEach((r, i) => {
503
712
  r.insertBefore(createElement(isHeaders[i] ? 'th' : 'td', {}, ['\u00a0']), refs[i]);
504
713
  });
@@ -507,34 +716,45 @@ export class TableTooltip {
507
716
  }
508
717
 
509
718
  _deleteRow() {
510
- const cell = this._getCell();
511
- if (!cell) return;
512
- const row = cell.closest('tr');
513
- const table = cell.closest('table');
514
- if (!row || !table) return;
515
- // Guard: do not delete the last body row (thead rows are not counted)
719
+ const cells = this._getSelectedCells();
720
+ if (!cells.length) return;
721
+ const table = cells[0].closest('table');
722
+ if (!table) return;
516
723
  const tbody = table.querySelector('tbody');
517
- const bodyRows = tbody ? tbody.querySelectorAll('tr').length : table.querySelectorAll('tr').length;
518
- if (bodyRows <= 1 && row.closest('tbody')) return;
724
+ const totalBodyRows = tbody
725
+ ? tbody.querySelectorAll('tr').length
726
+ : table.querySelectorAll('tr').length;
727
+ const selectedRows = [...new Set(cells.map((c) => c.closest('tr')).filter(Boolean))];
728
+ const bodyRowsToDelete = selectedRows.filter((r) => r.closest('tbody'));
729
+ // Guard: keep at least one body row
730
+ if (bodyRowsToDelete.length >= totalBodyRows) return;
519
731
  this._activeCell = null;
520
- row.parentElement?.removeChild(row);
732
+ this._clearSelection();
733
+ selectedRows.forEach((r) => r.parentElement?.removeChild(r));
521
734
  requestAnimationFrame(() => this._positionNear(this._activeTable));
522
735
  this.context.invoke('editor.afterCommand');
523
736
  }
524
737
 
525
738
  _deleteColumn() {
526
- const cell = this._getCell();
527
- if (!cell) return;
528
- const table = cell.closest('table');
739
+ const cells = this._getSelectedCells();
740
+ if (!cells.length) return;
741
+ const table = cells[0].closest('table');
529
742
  if (!table) return;
530
- const row = cell.closest('tr');
531
- if (row && row.cells.length <= 1) return;
532
- const visualColIdx = getVisualColIndex(cell);
743
+ const tableRows = Array.from(table.querySelectorAll('tr'));
744
+ if (tableRows[0] && tableRows[0].cells.length <= 1) return; // guard: keep ≥1 col
745
+ const colIndices = [...new Set(cells.map((c) => getVisualColIndex(c)))];
746
+ if (colIndices.length >= (tableRows[0]?.cells.length ?? 1)) return;
747
+ // Collect all cell references upfront before any removal (avoids stale visual indices)
748
+ const cellsToDelete = [];
749
+ colIndices.forEach((colIdx) => {
750
+ tableRows.forEach((r) => {
751
+ const c = getCellAtVisualCol(r, colIdx);
752
+ if (c) cellsToDelete.push(c);
753
+ });
754
+ });
533
755
  this._activeCell = null;
534
- const rows = Array.from(table.querySelectorAll('tr'));
535
- // Batch reads before writes to avoid forced reflows inside the loop
536
- const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
537
- cells.forEach((c, i) => { if (c) rows[i].removeChild(c); });
756
+ this._clearSelection();
757
+ cellsToDelete.forEach((c) => c.parentElement?.removeChild(c));
538
758
  requestAnimationFrame(() => this._positionNear(this._activeTable));
539
759
  this.context.invoke('editor.afterCommand');
540
760
  }
@@ -542,41 +762,57 @@ export class TableTooltip {
542
762
  _mergeCells() {
543
763
  const cell = this._getCell();
544
764
  if (!cell) return;
545
- const sel = window.getSelection();
546
- if (!sel || sel.rangeCount === 0) return;
547
- const range = sel.getRangeAt(0);
548
765
  const table = cell.closest('table');
549
766
  if (!table) return;
550
767
 
551
- // Collect all cells (in any row) that intersect the selection
552
- const allCells = Array.from(table.querySelectorAll('td, th'));
553
- const selected = allCells.filter((c) => {
554
- try { return range.intersectsNode(c); } catch { return false; }
555
- });
556
- if (selected.length < 2) return;
557
-
558
- // Determine if all selected cells are in the same row (horizontal merge)
559
- const rows = [...new Set(selected.map((c) => c.closest('tr')))];
560
- if (rows.length === 1) {
561
- // Horizontal merge within a single row
562
- const row = rows[0];
563
- const rowSelected = Array.from(row.cells).filter((c) => selected.includes(c));
564
- if (rowSelected.length < 2) return;
565
- const first = rowSelected[0];
566
- first.colSpan = rowSelected.reduce((sum, c) => sum + (c.colSpan || 1), 0);
567
- first.innerHTML = rowSelected.map((c) => c.innerHTML).join('');
568
- rowSelected.slice(1).forEach((c) => row.removeChild(c));
569
- } else {
570
- // Vertical merge across rows — merge into first selected cell (rowspan)
571
- const visualCols = [...new Set(selected.map((c) => getVisualColIndex(c)))];
572
- if (visualCols.length !== 1) return; // only support single-column vertical merge
573
- const first = selected[0];
574
- first.rowSpan = selected.reduce((sum, c) => sum + (c.rowSpan || 1), 0);
575
- first.innerHTML = selected.map((c) => c.innerHTML).join('');
576
- selected.slice(1).forEach((c) => {
577
- if (c.closest('tr')) c.closest('tr').removeChild(c);
768
+ // Prefer user panel-selected cells; fall back to text-selection range
769
+ let selected = this._getSelectedCells().filter((c) => table.contains(c));
770
+ if (selected.length < 2) {
771
+ const sel = window.getSelection();
772
+ if (!sel || sel.rangeCount === 0) return;
773
+ const range = sel.getRangeAt(0);
774
+ const allCells = Array.from(table.querySelectorAll('td, th'));
775
+ selected = allCells.filter((c) => {
776
+ try { return range.intersectsNode(c); } catch { return false; }
578
777
  });
778
+ if (selected.length < 2) return;
779
+ }
780
+
781
+ const { gridMap, cellPos } = buildGridMap(table);
782
+
783
+ // Bounding rect that encompasses every selected cell's full span
784
+ let minR = Infinity, maxR = -Infinity, minC = Infinity, maxC = -Infinity;
785
+ selected.forEach((c) => {
786
+ const pos = cellPos.get(c);
787
+ if (!pos) return;
788
+ if (pos.r < minR) minR = pos.r;
789
+ if (pos.r + pos.rs - 1 > maxR) maxR = pos.r + pos.rs - 1;
790
+ if (pos.c < minC) minC = pos.c;
791
+ if (pos.c + pos.cs - 1 > maxC) maxC = pos.c + pos.cs - 1;
792
+ });
793
+ if (minR === Infinity) return;
794
+
795
+ // Collect every unique cell in the bounding rect
796
+ const seen = new Set();
797
+ const rectCells = [];
798
+ for (let r = minR; r <= maxR; r++) {
799
+ const rowMap = gridMap[r];
800
+ if (!rowMap) continue;
801
+ for (let c = minC; c <= maxC; c++) {
802
+ const tc = rowMap[c];
803
+ if (tc && !seen.has(tc)) { seen.add(tc); rectCells.push(tc); }
804
+ }
579
805
  }
806
+ if (rectCells.length < 2) return;
807
+
808
+ const first = rectCells[0];
809
+ first.colSpan = maxC - minC + 1;
810
+ first.rowSpan = maxR - minR + 1;
811
+ first.style.verticalAlign = 'middle';
812
+ first.innerHTML = rectCells.map((c) => c.innerHTML).join('');
813
+ rectCells.slice(1).forEach((c) => c.parentElement?.removeChild(c));
814
+
815
+ this._clearSelection();
580
816
  this.context.invoke('editor.afterCommand');
581
817
  }
582
818
 
@@ -588,6 +824,73 @@ export class TableTooltip {
588
824
  this.context.invoke('editor.afterCommand');
589
825
  }
590
826
 
827
+ _unmergeCells() {
828
+ const cells = this._getSelectedCells();
829
+ if (!cells.length) return;
830
+ const table = cells[0].closest('table');
831
+ if (!table) return;
832
+ const mergedCells = cells.filter(
833
+ (c) => table.contains(c) && ((c.colSpan || 1) > 1 || (c.rowSpan || 1) > 1)
834
+ );
835
+ if (!mergedCells.length) return;
836
+ mergedCells.forEach((cell) => {
837
+ if (table.contains(cell)) this._unmergeOne(cell, table);
838
+ });
839
+ this._clearSelection();
840
+ requestAnimationFrame(() => this._positionNear(this._activeTable));
841
+ this.context.invoke('editor.afterCommand');
842
+ }
843
+
844
+ /**
845
+ * Split a single merged cell (colspan/rowspan > 1) back into individual cells.
846
+ * New cells are empty (&nbsp;); the original cell retains its content.
847
+ * @param {HTMLTableCellElement} cell
848
+ * @param {HTMLTableElement} table
849
+ */
850
+ _unmergeOne(cell, table) {
851
+ const cs = cell.colSpan || 1;
852
+ const rs = cell.rowSpan || 1;
853
+ if (cs === 1 && rs === 1) return;
854
+
855
+ // Build grid map from current DOM state (before any mutation)
856
+ const { cellPos } = buildGridMap(table);
857
+ const pos = cellPos.get(cell);
858
+ if (!pos) return;
859
+
860
+ const { r, c } = pos;
861
+ const tableRows = Array.from(table.rows);
862
+ const tag = cell.tagName.toLowerCase(); // preserve td / th
863
+
864
+ // Reset the original cell
865
+ cell.rowSpan = 1;
866
+ cell.colSpan = 1;
867
+ cell.style.verticalAlign = '';
868
+
869
+ // Same row: insert (cs - 1) sibling cells after the original cell
870
+ if (cs > 1) {
871
+ const insertRef = cell.nextElementSibling;
872
+ for (let dc = 1; dc < cs; dc++) {
873
+ tableRows[r].insertBefore(createElement(tag, {}, ['\u00a0']), insertRef);
874
+ }
875
+ }
876
+
877
+ // Rows below: insert cs cells at the correct visual column position (for rowspan)
878
+ for (let dr = 1; dr < rs; dr++) {
879
+ const targetRow = tableRows[r + dr];
880
+ if (!targetRow) continue;
881
+ // Find the first cell in this row whose visual-col origin is beyond c
882
+ // (using cellPos built before mutations — valid for these untouched rows)
883
+ let ref = null;
884
+ for (const tc of targetRow.cells) {
885
+ const tp = cellPos.get(tc);
886
+ if (tp && tp.c > c) { ref = tc; break; }
887
+ }
888
+ for (let dc = 0; dc < cs; dc++) {
889
+ targetRow.insertBefore(createElement(tag, {}, ['\u00a0']), ref);
890
+ }
891
+ }
892
+ }
893
+
591
894
  // ---------------------------------------------------------------------------
592
895
  // Size popover (column width / row height)
593
896
  // ---------------------------------------------------------------------------
@@ -607,9 +910,9 @@ export class TableTooltip {
607
910
 
608
911
  const actionsEl = createElement('div', { class: 'an-size-popover-actions' });
609
912
  const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
610
- cancelBtn.textContent = 'Cancel';
913
+ cancelBtn.textContent = this.context.locale.tooltips.table.cancelBtn;
611
914
  const applyBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
612
- applyBtn.textContent = 'Apply';
915
+ applyBtn.textContent = this.context.locale.tooltips.table.applyBtn;
613
916
  actionsEl.appendChild(cancelBtn);
614
917
  actionsEl.appendChild(applyBtn);
615
918
 
@@ -653,51 +956,58 @@ export class TableTooltip {
653
956
  if (type === 'border') {
654
957
  const table = cell.closest('table');
655
958
  if (!table) return;
656
- // Read current inline border-width, fall back to computed value, then default 1px
657
959
  const firstCell = table.querySelector('td, th');
658
960
  const currentPx = firstCell
659
961
  ? (parseInt(firstCell.style.borderWidth, 10) ||
660
962
  parseInt(window.getComputedStyle(firstCell).borderWidth, 10) || 1)
661
963
  : 1;
662
- this._sizeTitleEl.textContent = 'Table Border Width (px)';
964
+ this._sizeTitleEl.textContent = this.context.locale.tooltips.table.tableBorderWidthPx;
663
965
  this._sizeInputEl.min = '0';
664
966
  this._sizeInputEl.max = '10';
665
967
  this._sizeInputEl.value = currentPx;
666
968
  this._sizeApply = (val) => {
667
- // Apply border-width to every cell; with border-collapse:collapse this
668
- // controls all grid lines including the outer table border.
669
- // Setting only borderWidth preserves the existing border-color from CSS.
670
969
  const cells = Array.from(table.querySelectorAll('td, th'));
671
970
  if (val === 0) {
672
- cells.forEach(c => { c.style.borderWidth = '0'; c.style.borderStyle = 'none'; });
971
+ cells.forEach((c) => { c.style.borderWidth = '0'; c.style.borderStyle = 'none'; });
673
972
  } else {
674
- cells.forEach(c => { c.style.borderWidth = `${val}px`; c.style.borderStyle = 'solid'; });
973
+ cells.forEach((c) => { c.style.borderWidth = `${val}px`; c.style.borderStyle = 'solid'; });
675
974
  }
676
975
  this.context.invoke('editor.afterCommand');
677
976
  };
678
977
  } else {
679
978
  const isCol = type === 'col';
680
- this._sizeTitleEl.textContent = isCol ? 'Column Width (px)' : 'Row Height (px)';
979
+ const activeCells = this._getSelectedCells().filter((c) => {
980
+ const t = c.closest('table');
981
+ return t && t === cell.closest('table');
982
+ });
983
+ this._sizeTitleEl.textContent = isCol
984
+ ? this.context.locale.tooltips.table.columnWidthPx
985
+ : this.context.locale.tooltips.table.rowHeightPx;
681
986
  this._sizeInputEl.min = '1';
682
987
  this._sizeInputEl.max = '2000';
683
988
  this._sizeInputEl.value = isCol
684
989
  ? (cell.offsetWidth || 120)
685
990
  : (cell.closest('tr') ? (cell.closest('tr').offsetHeight || 40) : 40);
686
991
  this._sizeApply = (val) => {
992
+ const table = cell.closest('table');
993
+ if (!table) return;
687
994
  if (isCol) {
688
- const table = cell.closest('table');
689
- const visualColIdx = getVisualColIndex(cell);
690
- const rows = Array.from(table.querySelectorAll('tr'));
691
- // Batch reads, then writes
692
- const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
693
- cells.forEach(c => {
694
- if (c) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
995
+ // Apply to all selected columns (or just the current column)
996
+ const colIndices = [...new Set(activeCells.map((c) => getVisualColIndex(c)))];
997
+ const tableRows = Array.from(table.querySelectorAll('tr'));
998
+ colIndices.forEach((colIdx) => {
999
+ tableRows.forEach((r) => {
1000
+ const c = getCellAtVisualCol(r, colIdx);
1001
+ // F-1: skip merged cells same reason as drag-resize _colCells.
1002
+ if (c && (c.colSpan || 1) === 1) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
1003
+ });
695
1004
  });
696
1005
  } else {
697
- const row = cell.closest('tr');
698
- if (row) {
1006
+ // Apply to all selected rows (or just the current row)
1007
+ const selectedRows = [...new Set(activeCells.map((c) => c.closest('tr')).filter(Boolean))];
1008
+ selectedRows.forEach((row) => {
699
1009
  for (const c of row.cells) { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; }
700
- }
1010
+ });
701
1011
  }
702
1012
  this.context.invoke('editor.afterCommand');
703
1013
  };