autumnnote 1.0.1 → 1.0.3
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/README.md +6 -2
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +2365 -1993
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +16 -16
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/js/core/sanitise.js +43 -0
- package/src/js/editing/History.js +10 -2
- package/src/js/editing/Style.js +65 -4
- package/src/js/editing/Typing.js +243 -37
- package/src/js/module/Buttons.js +15 -5
- package/src/js/module/CodeTooltip.js +18 -7
- package/src/js/module/Codeview.js +6 -1
- package/src/js/module/ContextMenu.js +13 -6
- package/src/js/module/Editor.js +70 -2
- package/src/js/module/IconDialog.js +27 -13
- package/src/js/module/ImageResizer.js +22 -3
- package/src/js/module/ImageTooltip.js +37 -10
- package/src/js/module/Placeholder.js +2 -1
- package/src/js/module/Statusbar.js +16 -2
- package/src/js/module/TableTooltip.js +119 -23
- package/src/js/module/Toolbar.js +103 -17
- package/src/js/module/VideoDialog.js +3 -3
- package/src/js/module/VideoResizer.js +22 -3
- package/src/js/module/VideoTooltip.js +73 -0
- package/src/js/renderer.js +5 -0
- package/src/js/settings.js +4 -0
- package/src/styles/autumnnote.scss +61 -31
- package/types/index.d.ts +4 -4
|
@@ -418,11 +418,23 @@ export class ContextMenu {
|
|
|
418
418
|
document.execCommand('hiliteColor', false, fmt.backgroundColor);
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
// Font family — apply BEFORE font-size DOM manipulation so the selection is still intact.
|
|
422
|
+
if (fmt.fontFamily) {
|
|
423
|
+
document.execCommand('fontName', false, fmt.fontFamily);
|
|
424
|
+
}
|
|
425
|
+
|
|
421
426
|
// Font size — use a unique data-marker to avoid touching pre-existing font[size="7"] nodes.
|
|
422
427
|
if (fmt.fontSize) {
|
|
423
428
|
const marker = `fs-${Date.now()}`;
|
|
429
|
+
// Snapshot pre-existing font[size="7"] BEFORE execCommand so we don't accidentally
|
|
430
|
+
// replace nodes that were already in the document (the comment below was aspirational
|
|
431
|
+
// but the original code never actually excluded them — this Set approach is the fix).
|
|
432
|
+
const preExisting = new Set(editable.querySelectorAll('font[size="7"]'));
|
|
424
433
|
document.execCommand('fontSize', false, '7');
|
|
425
|
-
|
|
434
|
+
// Mark only the NEWLY created font[size="7"] elements.
|
|
435
|
+
editable.querySelectorAll('font[size="7"]').forEach((el) => {
|
|
436
|
+
if (!preExisting.has(el)) el.setAttribute('data-an-tmp', marker);
|
|
437
|
+
});
|
|
426
438
|
editable.querySelectorAll(`[data-an-tmp="${marker}"]`).forEach((el) => {
|
|
427
439
|
const span = document.createElement('span');
|
|
428
440
|
span.style.fontSize = fmt.fontSize;
|
|
@@ -432,11 +444,6 @@ export class ContextMenu {
|
|
|
432
444
|
});
|
|
433
445
|
}
|
|
434
446
|
|
|
435
|
-
// Font family — only apply if it was explicitly set (not just inherited default).
|
|
436
|
-
if (fmt.fontFamily) {
|
|
437
|
-
document.execCommand('fontName', false, fmt.fontFamily);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
447
|
this.context.invoke('editor.afterCommand');
|
|
441
448
|
}
|
|
442
449
|
|
package/src/js/module/Editor.js
CHANGED
|
@@ -77,12 +77,80 @@ export class Editor {
|
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
// Guard: when cursor lands at the <li> element node of a checklist item
|
|
81
|
+
// (before the checkbox), nudge it to the correct text position.
|
|
82
|
+
//
|
|
83
|
+
// mouseup: use caretRangeFromPoint / caretPositionFromPoint so the cursor
|
|
84
|
+
// lands WHERE the user actually clicked (middle, end of text…).
|
|
85
|
+
// keyup : arrow-key navigation may land at <li>[0]; move to start-of-text.
|
|
86
|
+
const fixChecklistCursor = (event) => {
|
|
87
|
+
const sel = window.getSelection();
|
|
88
|
+
if (!sel || !sel.rangeCount) return;
|
|
89
|
+
const r = sel.getRangeAt(0);
|
|
90
|
+
if (!r.collapsed) return;
|
|
91
|
+
const sc = r.startContainer;
|
|
92
|
+
|
|
93
|
+
// Only act when cursor is at the <li> element node itself (not inside
|
|
94
|
+
// a text node — the browser already placed it correctly in that case).
|
|
95
|
+
if (sc.nodeType !== Node.ELEMENT_NODE) return;
|
|
96
|
+
const li = sc.matches && sc.matches('.an-checklist li') ? sc : null;
|
|
97
|
+
if (!li) return;
|
|
98
|
+
const cb = li.querySelector('input[type="checkbox"]');
|
|
99
|
+
if (!cb) return;
|
|
100
|
+
|
|
101
|
+
// For mouse events: ask the browser where the pointer landed so the
|
|
102
|
+
// cursor respects the actual click position inside the text.
|
|
103
|
+
if (event && event.type === 'mouseup') {
|
|
104
|
+
let caret = null;
|
|
105
|
+
if (document.caretRangeFromPoint) {
|
|
106
|
+
caret = document.caretRangeFromPoint(event.clientX, event.clientY);
|
|
107
|
+
} else if (document.caretPositionFromPoint) {
|
|
108
|
+
const cp = document.caretPositionFromPoint(event.clientX, event.clientY);
|
|
109
|
+
if (cp) {
|
|
110
|
+
caret = document.createRange();
|
|
111
|
+
caret.setStart(cp.offsetNode, cp.offset);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// If the caret from point landed inside a text node of this li, use it
|
|
115
|
+
if (caret && editable.contains(caret.startContainer) &&
|
|
116
|
+
caret.startContainer !== li) {
|
|
117
|
+
caret.collapse(true);
|
|
118
|
+
sel.removeAllRanges();
|
|
119
|
+
sel.addRange(caret);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fallback (keyboard nav, or caretRangeFromPoint not available / landed
|
|
125
|
+
// at li again): prefer the first text node after the checkbox so the
|
|
126
|
+
// cursor renders at the padding-left edge (after the visual checkbox)
|
|
127
|
+
// rather than at element-level where the browser may place it at x=0.
|
|
128
|
+
const nr = document.createRange();
|
|
129
|
+
let anchorNode = null;
|
|
130
|
+
for (const child of li.childNodes) {
|
|
131
|
+
if (child !== cb && child.nodeType === Node.TEXT_NODE) {
|
|
132
|
+
anchorNode = child;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (anchorNode) {
|
|
137
|
+
nr.setStart(anchorNode, 0);
|
|
138
|
+
} else {
|
|
139
|
+
nr.setStartAfter(cb);
|
|
140
|
+
}
|
|
141
|
+
nr.collapse(true);
|
|
142
|
+
sel.removeAllRanges();
|
|
143
|
+
sel.addRange(nr);
|
|
144
|
+
};
|
|
145
|
+
|
|
80
146
|
this._disposers.push(
|
|
81
147
|
on(editable, 'keydown', onKeydown),
|
|
82
148
|
on(editable, 'beforeinput', onBeforeInput),
|
|
83
149
|
on(editable, 'input', onInput),
|
|
84
150
|
on(document, 'selectionchange', onSelChange),
|
|
85
151
|
on(editable, 'click', onCheckboxClick),
|
|
152
|
+
on(editable, 'mouseup', fixChecklistCursor),
|
|
153
|
+
on(editable, 'keyup', fixChecklistCursor),
|
|
86
154
|
);
|
|
87
155
|
}
|
|
88
156
|
|
|
@@ -114,8 +182,8 @@ export class Editor {
|
|
|
114
182
|
return; // let the native paste event fire
|
|
115
183
|
}
|
|
116
184
|
|
|
117
|
-
// Show keyboard shortcuts dialog: Shift
|
|
118
|
-
if (event.key === '
|
|
185
|
+
// Show keyboard shortcuts dialog: Ctrl+Shift+/
|
|
186
|
+
if (event.key === '/' && event.shiftKey && event.ctrlKey && !event.metaKey) {
|
|
119
187
|
event.preventDefault();
|
|
120
188
|
this.context.invoke('shortcutsDialog.show');
|
|
121
189
|
return;
|
|
@@ -318,6 +318,9 @@ export class IconDialog {
|
|
|
318
318
|
this._selectedIcon = null;
|
|
319
319
|
this._activeCat = 'all';
|
|
320
320
|
this._searchInput.value = '';
|
|
321
|
+
// Clear previously selected icon highlight and disable Insert button
|
|
322
|
+
this._grid.querySelectorAll('.an-icon-cell.active').forEach((c) => c.classList.remove('active'));
|
|
323
|
+
this._insertBtn.setAttribute('disabled', '');
|
|
321
324
|
this._updateCatTabs();
|
|
322
325
|
this._filterIcons('', 'all');
|
|
323
326
|
this._updatePreview(null);
|
|
@@ -551,6 +554,7 @@ export class IconDialog {
|
|
|
551
554
|
const iconEl = document.createElement('i');
|
|
552
555
|
iconEl.className = `${cls} fa-${this._selectedIcon}`;
|
|
553
556
|
iconEl.setAttribute('aria-hidden', 'true');
|
|
557
|
+
iconEl.setAttribute('contenteditable', 'false');
|
|
554
558
|
if (styleParts.length) iconEl.setAttribute('style', styleParts.join(';'));
|
|
555
559
|
|
|
556
560
|
const savedRange = this._savedRange;
|
|
@@ -574,21 +578,26 @@ export class IconDialog {
|
|
|
574
578
|
range.deleteContents();
|
|
575
579
|
range.insertNode(iconEl);
|
|
576
580
|
|
|
577
|
-
// 4.
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
581
|
+
// 4. Place cursor after the icon.
|
|
582
|
+
// Keep a text anchor (\u200B) after the icon so ArrowLeft/ArrowRight and
|
|
583
|
+
// caret placement at end-of-line stay stable across browsers.
|
|
584
|
+
let caretTextNode = iconEl.nextSibling;
|
|
585
|
+
if (!caretTextNode || caretTextNode.nodeType !== Node.TEXT_NODE) {
|
|
586
|
+
caretTextNode = document.createTextNode('\u200B');
|
|
587
|
+
iconEl.parentNode.insertBefore(caretTextNode, iconEl.nextSibling);
|
|
588
|
+
} else if (!caretTextNode.textContent) {
|
|
589
|
+
// Split text-node insertions can leave an empty text node after icon.
|
|
590
|
+
// Fill it with ZWS so caret-after-icon remains stable at end-of-line.
|
|
591
|
+
caretTextNode.textContent = '\u200B';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const caretOffset = ((caretTextNode.textContent || '').startsWith('\u200B')) ? 1 : 0;
|
|
595
|
+
const caretRange = document.createRange();
|
|
596
|
+
caretRange.setStart(caretTextNode, Math.min(caretOffset, caretTextNode.textContent.length));
|
|
597
|
+
caretRange.collapse(true);
|
|
589
598
|
if (sel) {
|
|
590
599
|
sel.removeAllRanges();
|
|
591
|
-
sel.addRange(
|
|
600
|
+
sel.addRange(caretRange);
|
|
592
601
|
}
|
|
593
602
|
|
|
594
603
|
// 5. Close dialog last — same as ImageDialog
|
|
@@ -596,6 +605,11 @@ export class IconDialog {
|
|
|
596
605
|
|
|
597
606
|
// 6. Restore editor focus (needed for toolbar refresh via afterCommand)
|
|
598
607
|
editable.focus();
|
|
608
|
+
if (sel) {
|
|
609
|
+
// Some browsers reset selection when refocusing editable after closing dialog.
|
|
610
|
+
sel.removeAllRanges();
|
|
611
|
+
sel.addRange(caretRange);
|
|
612
|
+
}
|
|
599
613
|
this.context.invoke('editor.afterCommand');
|
|
600
614
|
}
|
|
601
615
|
|
|
@@ -27,11 +27,14 @@ export class ImageResizer {
|
|
|
27
27
|
this._activeImg = null;
|
|
28
28
|
this._overlay = null;
|
|
29
29
|
this._disposers = [];
|
|
30
|
+
this._positionRaf = null;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
initialize() {
|
|
33
34
|
this._overlay = this._buildOverlay();
|
|
34
|
-
|
|
35
|
+
const container = this.context.layoutInfo.editable.closest('.an-container') || document.body;
|
|
36
|
+
container.appendChild(this._overlay);
|
|
37
|
+
this._container = container;
|
|
35
38
|
|
|
36
39
|
const editable = this.context.layoutInfo.editable;
|
|
37
40
|
|
|
@@ -58,6 +61,10 @@ export class ImageResizer {
|
|
|
58
61
|
this._dragDisposers.forEach((d) => d());
|
|
59
62
|
this._dragDisposers = null;
|
|
60
63
|
}
|
|
64
|
+
if (this._positionRaf) {
|
|
65
|
+
cancelAnimationFrame(this._positionRaf);
|
|
66
|
+
this._positionRaf = null;
|
|
67
|
+
}
|
|
61
68
|
this._deselect();
|
|
62
69
|
if (this._overlay && this._overlay.parentNode) {
|
|
63
70
|
this._overlay.parentNode.removeChild(this._overlay);
|
|
@@ -148,10 +155,22 @@ export class ImageResizer {
|
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
_updateOverlayPosition() {
|
|
158
|
+
if (this._positionRaf) cancelAnimationFrame(this._positionRaf);
|
|
159
|
+
this._positionRaf = requestAnimationFrame(() => {
|
|
160
|
+
this._positionRaf = null;
|
|
161
|
+
this._updateOverlayPositionNow();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_updateOverlayPositionNow() {
|
|
151
166
|
if (!this._activeImg || !this._overlay) return;
|
|
167
|
+
const offsetParent = this._overlay.offsetParent || this._container;
|
|
168
|
+
const containerRect = offsetParent.getBoundingClientRect();
|
|
152
169
|
const rect = this._activeImg.getBoundingClientRect();
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
const left = rect.left - containerRect.left + offsetParent.scrollLeft;
|
|
171
|
+
const top = rect.top - containerRect.top + offsetParent.scrollTop;
|
|
172
|
+
this._overlay.style.left = `${left}px`;
|
|
173
|
+
this._overlay.style.top = `${top}px`;
|
|
155
174
|
this._overlay.style.width = `${rect.width}px`;
|
|
156
175
|
this._overlay.style.height = `${rect.height}px`;
|
|
157
176
|
}
|
|
@@ -36,7 +36,8 @@ export class ImageTooltip {
|
|
|
36
36
|
this._disposers.push(
|
|
37
37
|
on(editable, 'mouseover', (e) => {
|
|
38
38
|
const img = e.target.closest('img');
|
|
39
|
-
|
|
39
|
+
// Skip when the image is inside a link — LinkTooltip takes priority there
|
|
40
|
+
if (img && editable.contains(img) && !img.closest('a[href]')) {
|
|
40
41
|
this._scheduleShow(img);
|
|
41
42
|
}
|
|
42
43
|
}),
|
|
@@ -205,10 +206,11 @@ export class ImageTooltip {
|
|
|
205
206
|
_setFloat(value) {
|
|
206
207
|
const img = this._activeImg;
|
|
207
208
|
if (!img) return;
|
|
208
|
-
img.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
const target = img.closest('figure.an-figure') || img;
|
|
210
|
+
target.style.float = value;
|
|
211
|
+
target.style.display = '';
|
|
212
|
+
target.style.marginLeft = value === 'right' ? '12px' : '';
|
|
213
|
+
target.style.marginRight = value === 'left' ? '12px' : '';
|
|
212
214
|
this.context.invoke('editor.afterCommand');
|
|
213
215
|
this.context.invoke('imageResizer.updateOverlay');
|
|
214
216
|
this._positionNear(img);
|
|
@@ -217,10 +219,11 @@ export class ImageTooltip {
|
|
|
217
219
|
_setCenter() {
|
|
218
220
|
const img = this._activeImg;
|
|
219
221
|
if (!img) return;
|
|
220
|
-
img.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
const target = img.closest('figure.an-figure') || img;
|
|
223
|
+
target.style.float = '';
|
|
224
|
+
target.style.display = 'block';
|
|
225
|
+
target.style.marginLeft = 'auto';
|
|
226
|
+
target.style.marginRight = 'auto';
|
|
224
227
|
this.context.invoke('editor.afterCommand');
|
|
225
228
|
this.context.invoke('imageResizer.updateOverlay');
|
|
226
229
|
this._positionNear(img);
|
|
@@ -241,7 +244,12 @@ export class ImageTooltip {
|
|
|
241
244
|
if (!img) return;
|
|
242
245
|
this._hide();
|
|
243
246
|
this.context.invoke('imageResizer.deselect');
|
|
244
|
-
|
|
247
|
+
const figure = img.closest('figure.an-figure');
|
|
248
|
+
if (figure && figure.parentNode) {
|
|
249
|
+
figure.parentNode.removeChild(figure);
|
|
250
|
+
} else if (img.parentNode) {
|
|
251
|
+
img.parentNode.removeChild(img);
|
|
252
|
+
}
|
|
245
253
|
this.context.invoke('editor.afterCommand');
|
|
246
254
|
}
|
|
247
255
|
|
|
@@ -266,6 +274,23 @@ export class ImageTooltip {
|
|
|
266
274
|
// Wrap image in <figure><figcaption>
|
|
267
275
|
const figure = document.createElement('figure');
|
|
268
276
|
figure.className = 'an-figure';
|
|
277
|
+
|
|
278
|
+
// Transfer existing alignment from <img> to <figure> (C4 fix)
|
|
279
|
+
if (img.style.float) {
|
|
280
|
+
figure.style.float = img.style.float;
|
|
281
|
+
figure.style.marginLeft = img.style.marginLeft;
|
|
282
|
+
figure.style.marginRight = img.style.marginRight;
|
|
283
|
+
img.style.float = '';
|
|
284
|
+
img.style.marginLeft = '';
|
|
285
|
+
img.style.marginRight = '';
|
|
286
|
+
} else if (img.style.display === 'block' && img.style.marginLeft === 'auto') {
|
|
287
|
+
figure.style.display = 'block';
|
|
288
|
+
figure.style.marginLeft = 'auto';
|
|
289
|
+
figure.style.marginRight = 'auto';
|
|
290
|
+
img.style.display = '';
|
|
291
|
+
img.style.marginLeft = '';
|
|
292
|
+
img.style.marginRight = '';
|
|
293
|
+
}
|
|
269
294
|
const figcaption = document.createElement('figcaption');
|
|
270
295
|
figcaption.className = 'an-figcaption';
|
|
271
296
|
figcaption.textContent = 'Caption';
|
|
@@ -281,6 +306,8 @@ export class ImageTooltip {
|
|
|
281
306
|
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
|
|
282
307
|
|
|
283
308
|
this.context.invoke('editor.afterCommand');
|
|
309
|
+
// Re-sync the resize overlay: wrapping img in <figure> changes its layout position.
|
|
310
|
+
this.context.invoke('imageResizer.updateOverlay');
|
|
284
311
|
this._hide();
|
|
285
312
|
}
|
|
286
313
|
}
|
|
@@ -38,7 +38,8 @@ export class Placeholder {
|
|
|
38
38
|
|
|
39
39
|
_update() {
|
|
40
40
|
const editable = this.context.layoutInfo.editable;
|
|
41
|
+
const isFocused = document.activeElement === editable;
|
|
41
42
|
const isEmpty = !editable.textContent.trim() && !editable.querySelector('img, table, hr');
|
|
42
|
-
editable.classList.toggle('an-placeholder', isEmpty);
|
|
43
|
+
editable.classList.toggle('an-placeholder', isEmpty && !isFocused);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
@@ -130,8 +130,15 @@ export class Statusbar {
|
|
|
130
130
|
|
|
131
131
|
const applyDelta = (clientY) => {
|
|
132
132
|
const delta = clientY - startY;
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
// Compute the true minimum: fixed elements (toolbar + statusbar) must fit
|
|
134
|
+
// inside the container. Sum the offsetHeight of every child that is NOT
|
|
135
|
+
// the editable area, then add a small floor so the editable stays visible.
|
|
136
|
+
const MIN_EDITABLE = 40;
|
|
137
|
+
const fixedH = Array.from(containerEl.children)
|
|
138
|
+
.filter(child => !child.classList.contains('an-editable'))
|
|
139
|
+
.reduce((sum, child) => sum + child.offsetHeight, 0);
|
|
140
|
+
const trueMin = Math.max(this.options.minHeight || 100, fixedH + MIN_EDITABLE);
|
|
141
|
+
containerEl.style.height = `${Math.max(trueMin, startH + delta)}px`;
|
|
135
142
|
};
|
|
136
143
|
|
|
137
144
|
// Mouse drag
|
|
@@ -146,6 +153,11 @@ export class Statusbar {
|
|
|
146
153
|
const onMouseDown = (event) => {
|
|
147
154
|
startY = event.clientY;
|
|
148
155
|
startH = containerEl.offsetHeight;
|
|
156
|
+
// Clear the editable's inline min-height so the flex layout can compress
|
|
157
|
+
// it freely once the container has a fixed height. Without this, the
|
|
158
|
+
// editable's min-height (set from options.height) overflows the container
|
|
159
|
+
// when the user drags the handle to a size smaller than that value.
|
|
160
|
+
this.context.layoutInfo.editable.style.minHeight = '';
|
|
149
161
|
document.addEventListener('mousemove', onMouseMove);
|
|
150
162
|
document.addEventListener('mouseup', onMouseUp);
|
|
151
163
|
// Track drag-phase listeners so destroy() can remove them mid-drag
|
|
@@ -173,6 +185,8 @@ export class Statusbar {
|
|
|
173
185
|
if (!touch) return;
|
|
174
186
|
startY = touch.clientY;
|
|
175
187
|
startH = containerEl.offsetHeight;
|
|
188
|
+
// Same as onMouseDown: clear editable min-height so flex can compress it
|
|
189
|
+
this.context.layoutInfo.editable.style.minHeight = '';
|
|
176
190
|
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
177
191
|
document.addEventListener('touchend', onTouchEnd);
|
|
178
192
|
this._dragDisposers = [
|
|
@@ -6,6 +6,64 @@ import { createElement, on } from '../core/dom.js';
|
|
|
6
6
|
const SHOW_DELAY = 120;
|
|
7
7
|
const HIDE_DELAY = 200;
|
|
8
8
|
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Table helpers — visual column index (accounts for colspan)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the visual (logical) column index of a cell, taking colspan into
|
|
15
|
+
* account for all preceding cells in the same row.
|
|
16
|
+
* @param {HTMLTableCellElement} cell
|
|
17
|
+
* @returns {number} 0-based visual column index, or -1 on failure
|
|
18
|
+
*/
|
|
19
|
+
function getVisualColIndex(cell) {
|
|
20
|
+
const row = cell.closest('tr');
|
|
21
|
+
if (!row) return -1;
|
|
22
|
+
let visualIdx = 0;
|
|
23
|
+
for (const c of row.cells) {
|
|
24
|
+
if (c === cell) return visualIdx;
|
|
25
|
+
visualIdx += c.colSpan || 1;
|
|
26
|
+
}
|
|
27
|
+
return -1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Finds the first cell in a row whose visual start column equals visualIdx.
|
|
32
|
+
* Returns null if no exact match (e.g. the column is spanned by a merged cell).
|
|
33
|
+
* @param {HTMLTableRowElement} row
|
|
34
|
+
* @param {number} visualIdx
|
|
35
|
+
* @returns {HTMLTableCellElement|null}
|
|
36
|
+
*/
|
|
37
|
+
function getCellAtVisualCol(row, visualIdx) {
|
|
38
|
+
let vIdx = 0;
|
|
39
|
+
for (const c of row.cells) {
|
|
40
|
+
if (vIdx === visualIdx) return c;
|
|
41
|
+
if (vIdx > visualIdx) break;
|
|
42
|
+
vIdx += c.colSpan || 1;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Finds the first cell whose visual range ends after visualIdx
|
|
49
|
+
* (used for inserting a new column to the right of visualIdx).
|
|
50
|
+
* @param {HTMLTableRowElement} row
|
|
51
|
+
* @param {number} visualIdx
|
|
52
|
+
* @returns {HTMLTableCellElement|null} reference cell for insertBefore, or null = append
|
|
53
|
+
*/
|
|
54
|
+
function getCellAfterVisualCol(row, visualIdx) {
|
|
55
|
+
let vIdx = 0;
|
|
56
|
+
for (const c of row.cells) {
|
|
57
|
+
vIdx += c.colSpan || 1;
|
|
58
|
+
if (vIdx > visualIdx) {
|
|
59
|
+
// next cell after the one that starts at / spans visualIdx
|
|
60
|
+
const next = c.nextElementSibling;
|
|
61
|
+
return (next && next.tagName === 'TD' || next && next.tagName === 'TH') ? next : null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
9
67
|
const ICONS = {
|
|
10
68
|
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>`,
|
|
11
69
|
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>`,
|
|
@@ -109,6 +167,9 @@ export class TableTooltip {
|
|
|
109
167
|
if (_resizing) return;
|
|
110
168
|
const cell = e.target.closest('td, th');
|
|
111
169
|
if (!cell || !editable.contains(cell)) { clearHover(); return; }
|
|
170
|
+
if (_nearCell && _nearCell !== cell) {
|
|
171
|
+
_nearCell.style.cursor = '';
|
|
172
|
+
}
|
|
112
173
|
const rect = cell.getBoundingClientRect();
|
|
113
174
|
const onRight = Math.abs(e.clientX - rect.right) < HIT;
|
|
114
175
|
const onBottom = Math.abs(e.clientY - rect.bottom) < HIT;
|
|
@@ -136,7 +197,7 @@ export class TableTooltip {
|
|
|
136
197
|
_table = _nearCell.closest('table');
|
|
137
198
|
if (_edge === 'col') {
|
|
138
199
|
_startW = _nearCell.offsetWidth;
|
|
139
|
-
_colIdx =
|
|
200
|
+
_colIdx = getVisualColIndex(_nearCell);
|
|
140
201
|
document.body.style.cursor = 'col-resize';
|
|
141
202
|
} else {
|
|
142
203
|
_row = _nearCell.closest('tr');
|
|
@@ -154,7 +215,7 @@ export class TableTooltip {
|
|
|
154
215
|
const newW = Math.max(30, _startW + (e.clientX - _startX));
|
|
155
216
|
if (_table && _colIdx >= 0) {
|
|
156
217
|
Array.from(_table.querySelectorAll('tr')).forEach((r) => {
|
|
157
|
-
const c = r
|
|
218
|
+
const c = getCellAtVisualCol(r, _colIdx);
|
|
158
219
|
if (c) { c.style.width = `${newW}px`; c.style.minWidth = `${newW}px`; }
|
|
159
220
|
});
|
|
160
221
|
}
|
|
@@ -356,6 +417,16 @@ export class TableTooltip {
|
|
|
356
417
|
// ---------------------------------------------------------------------------
|
|
357
418
|
|
|
358
419
|
_getCell() {
|
|
420
|
+
// Prefer the cell under the current text cursor (most intuitive for operations)
|
|
421
|
+
const sel = window.getSelection();
|
|
422
|
+
if (sel && sel.rangeCount) {
|
|
423
|
+
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
424
|
+
if (container.nodeType === 3) container = container.parentElement;
|
|
425
|
+
const cellFromSel = container && container.closest && container.closest('td, th');
|
|
426
|
+
if (cellFromSel && this._activeTable && this._activeTable.contains(cellFromSel)) {
|
|
427
|
+
return cellFromSel;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
359
430
|
return this._activeCell
|
|
360
431
|
|| (this._activeTable && this._activeTable.querySelector('td, th'));
|
|
361
432
|
}
|
|
@@ -388,16 +459,19 @@ export class TableTooltip {
|
|
|
388
459
|
_addColumn(position) {
|
|
389
460
|
const cell = this._getCell();
|
|
390
461
|
if (!cell) return;
|
|
391
|
-
const row = cell.closest('tr');
|
|
392
462
|
const table = cell.closest('table');
|
|
393
|
-
if (!
|
|
394
|
-
const
|
|
463
|
+
if (!table) return;
|
|
464
|
+
const visualColIdx = getVisualColIndex(cell);
|
|
395
465
|
Array.from(table.querySelectorAll('tr')).forEach((r) => {
|
|
396
|
-
const cells = Array.from(r.cells);
|
|
397
466
|
const isHeader = r.closest('thead') !== null;
|
|
398
467
|
const newCell = createElement(isHeader ? 'th' : 'td', {}, ['\u00a0']);
|
|
399
|
-
|
|
400
|
-
|
|
468
|
+
if (position === 'left') {
|
|
469
|
+
const ref = getCellAtVisualCol(r, visualColIdx);
|
|
470
|
+
r.insertBefore(newCell, ref);
|
|
471
|
+
} else {
|
|
472
|
+
const ref = getCellAfterVisualCol(r, visualColIdx);
|
|
473
|
+
r.insertBefore(newCell, ref);
|
|
474
|
+
}
|
|
401
475
|
});
|
|
402
476
|
this._positionNear(this._activeTable);
|
|
403
477
|
this.context.invoke('editor.afterCommand');
|
|
@@ -422,14 +496,14 @@ export class TableTooltip {
|
|
|
422
496
|
_deleteColumn() {
|
|
423
497
|
const cell = this._getCell();
|
|
424
498
|
if (!cell) return;
|
|
425
|
-
const row = cell.closest('tr');
|
|
426
499
|
const table = cell.closest('table');
|
|
427
|
-
if (!
|
|
428
|
-
|
|
429
|
-
|
|
500
|
+
if (!table) return;
|
|
501
|
+
const row = cell.closest('tr');
|
|
502
|
+
if (row && row.cells.length <= 1) return;
|
|
503
|
+
const visualColIdx = getVisualColIndex(cell);
|
|
430
504
|
this._activeCell = null;
|
|
431
505
|
Array.from(table.querySelectorAll('tr')).forEach((r) => {
|
|
432
|
-
const c = r
|
|
506
|
+
const c = getCellAtVisualCol(r, visualColIdx);
|
|
433
507
|
if (c) r.removeChild(c);
|
|
434
508
|
});
|
|
435
509
|
this._positionNear(this._activeTable);
|
|
@@ -439,19 +513,41 @@ export class TableTooltip {
|
|
|
439
513
|
_mergeCells() {
|
|
440
514
|
const cell = this._getCell();
|
|
441
515
|
if (!cell) return;
|
|
442
|
-
const row = cell.closest('tr');
|
|
443
|
-
if (!row) return;
|
|
444
516
|
const sel = window.getSelection();
|
|
445
517
|
if (!sel || sel.rangeCount === 0) return;
|
|
446
518
|
const range = sel.getRangeAt(0);
|
|
447
|
-
const
|
|
519
|
+
const table = cell.closest('table');
|
|
520
|
+
if (!table) return;
|
|
521
|
+
|
|
522
|
+
// Collect all cells (in any row) that intersect the selection
|
|
523
|
+
const allCells = Array.from(table.querySelectorAll('td, th'));
|
|
524
|
+
const selected = allCells.filter((c) => {
|
|
448
525
|
try { return range.intersectsNode(c); } catch { return false; }
|
|
449
526
|
});
|
|
450
527
|
if (selected.length < 2) return;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
528
|
+
|
|
529
|
+
// Determine if all selected cells are in the same row (horizontal merge)
|
|
530
|
+
const rows = [...new Set(selected.map((c) => c.closest('tr')))];
|
|
531
|
+
if (rows.length === 1) {
|
|
532
|
+
// Horizontal merge within a single row
|
|
533
|
+
const row = rows[0];
|
|
534
|
+
const rowSelected = Array.from(row.cells).filter((c) => selected.includes(c));
|
|
535
|
+
if (rowSelected.length < 2) return;
|
|
536
|
+
const first = rowSelected[0];
|
|
537
|
+
first.colSpan = rowSelected.reduce((sum, c) => sum + (c.colSpan || 1), 0);
|
|
538
|
+
first.innerHTML = rowSelected.map((c) => c.innerHTML).join('');
|
|
539
|
+
rowSelected.slice(1).forEach((c) => row.removeChild(c));
|
|
540
|
+
} else {
|
|
541
|
+
// Vertical merge across rows — merge into first selected cell (rowspan)
|
|
542
|
+
const visualCols = [...new Set(selected.map((c) => getVisualColIndex(c)))];
|
|
543
|
+
if (visualCols.length !== 1) return; // only support single-column vertical merge
|
|
544
|
+
const first = selected[0];
|
|
545
|
+
first.rowSpan = selected.reduce((sum, c) => sum + (c.rowSpan || 1), 0);
|
|
546
|
+
first.innerHTML = selected.map((c) => c.innerHTML).join('');
|
|
547
|
+
selected.slice(1).forEach((c) => {
|
|
548
|
+
if (c.closest('tr')) c.closest('tr').removeChild(c);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
455
551
|
this.context.invoke('editor.afterCommand');
|
|
456
552
|
}
|
|
457
553
|
|
|
@@ -532,10 +628,10 @@ export class TableTooltip {
|
|
|
532
628
|
|
|
533
629
|
this._sizeApply = (val) => {
|
|
534
630
|
if (isCol) {
|
|
535
|
-
const table
|
|
536
|
-
const
|
|
631
|
+
const table = cell.closest('table');
|
|
632
|
+
const visualColIdx = getVisualColIndex(cell);
|
|
537
633
|
Array.from(table.querySelectorAll('tr')).forEach((r) => {
|
|
538
|
-
const c = r
|
|
634
|
+
const c = getCellAtVisualCol(r, visualColIdx);
|
|
539
635
|
if (c) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
|
|
540
636
|
});
|
|
541
637
|
} else {
|