autumnnote 1.0.1 → 1.0.4
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 +2254 -1828
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +11 -11
- 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/index.umd.js +12 -12
- 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 +63 -29
- package/src/js/module/ImageTooltip.js +77 -16
- package/src/js/module/Placeholder.js +2 -1
- package/src/js/module/Statusbar.js +16 -2
- package/src/js/module/TableTooltip.js +197 -76
- package/src/js/module/Toolbar.js +103 -17
- package/src/js/module/VideoDialog.js +3 -3
- package/src/js/module/VideoResizer.js +67 -36
- package/src/js/module/VideoTooltip.js +82 -6
- 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,14 +27,25 @@ 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
|
|
|
41
|
+
// Debounce window resize — _updateOverlayPosition already has rAF gating but
|
|
42
|
+
// every call cancels + re-schedules it; a debounce reduces that churn.
|
|
43
|
+
let _resizeDebounce = null;
|
|
44
|
+
const onWindowResize = () => {
|
|
45
|
+
clearTimeout(_resizeDebounce);
|
|
46
|
+
_resizeDebounce = setTimeout(() => this._updateOverlayPosition(), 100);
|
|
47
|
+
};
|
|
48
|
+
|
|
38
49
|
this._disposers.push(
|
|
39
50
|
on(editable, 'click', (e) => this._onEditorClick(e)),
|
|
40
51
|
// Also select on right-click so the highlight shows before the context menu
|
|
@@ -44,7 +55,7 @@ export class ImageResizer {
|
|
|
44
55
|
}),
|
|
45
56
|
on(document, 'click', (e) => this._onDocClick(e)),
|
|
46
57
|
on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
47
|
-
on(window, 'resize',
|
|
58
|
+
on(window, 'resize', onWindowResize),
|
|
48
59
|
on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
49
60
|
);
|
|
50
61
|
|
|
@@ -58,6 +69,10 @@ export class ImageResizer {
|
|
|
58
69
|
this._dragDisposers.forEach((d) => d());
|
|
59
70
|
this._dragDisposers = null;
|
|
60
71
|
}
|
|
72
|
+
if (this._positionRaf) {
|
|
73
|
+
cancelAnimationFrame(this._positionRaf);
|
|
74
|
+
this._positionRaf = null;
|
|
75
|
+
}
|
|
61
76
|
this._deselect();
|
|
62
77
|
if (this._overlay && this._overlay.parentNode) {
|
|
63
78
|
this._overlay.parentNode.removeChild(this._overlay);
|
|
@@ -148,10 +163,22 @@ export class ImageResizer {
|
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
_updateOverlayPosition() {
|
|
166
|
+
if (this._positionRaf) cancelAnimationFrame(this._positionRaf);
|
|
167
|
+
this._positionRaf = requestAnimationFrame(() => {
|
|
168
|
+
this._positionRaf = null;
|
|
169
|
+
this._updateOverlayPositionNow();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_updateOverlayPositionNow() {
|
|
151
174
|
if (!this._activeImg || !this._overlay) return;
|
|
175
|
+
const offsetParent = this._overlay.offsetParent || this._container;
|
|
176
|
+
const containerRect = offsetParent.getBoundingClientRect();
|
|
152
177
|
const rect = this._activeImg.getBoundingClientRect();
|
|
153
|
-
|
|
154
|
-
|
|
178
|
+
const left = rect.left - containerRect.left + offsetParent.scrollLeft;
|
|
179
|
+
const top = rect.top - containerRect.top + offsetParent.scrollTop;
|
|
180
|
+
this._overlay.style.left = `${left}px`;
|
|
181
|
+
this._overlay.style.top = `${top}px`;
|
|
155
182
|
this._overlay.style.width = `${rect.width}px`;
|
|
156
183
|
this._overlay.style.height = `${rect.height}px`;
|
|
157
184
|
}
|
|
@@ -168,37 +195,44 @@ export class ImageResizer {
|
|
|
168
195
|
const isCorner = pos.length === 2; // 'nw','ne','se','sw'
|
|
169
196
|
|
|
170
197
|
const editable = this.context.layoutInfo.editable;
|
|
198
|
+
let _raf = null; // rAF handle — ensures at most one write per paint frame
|
|
199
|
+
|
|
171
200
|
const onMove = (me) => {
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
201
|
+
if (_raf !== null) return; // frame already pending, discard this event
|
|
202
|
+
const clientX = me.clientX;
|
|
203
|
+
const clientY = me.clientY;
|
|
204
|
+
_raf = requestAnimationFrame(() => {
|
|
205
|
+
_raf = null;
|
|
206
|
+
const dx = clientX - startX;
|
|
207
|
+
const dy = clientY - startY;
|
|
208
|
+
const maxW = editable.clientWidth || Infinity;
|
|
209
|
+
let newW = startW;
|
|
210
|
+
let newH = startH;
|
|
211
|
+
|
|
212
|
+
if (pos.includes('e')) newW = Math.max(20, startW + dx);
|
|
213
|
+
if (pos.includes('w')) newW = Math.max(20, startW - dx);
|
|
214
|
+
if (pos.includes('s')) newH = Math.max(20, startH + dy);
|
|
215
|
+
if (pos.includes('n')) newH = Math.max(20, startH - dy);
|
|
216
|
+
|
|
217
|
+
newW = Math.min(newW, maxW);
|
|
218
|
+
|
|
219
|
+
if (isCorner) {
|
|
220
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
221
|
+
newH = Math.max(20, Math.round(newW / aspectRatio));
|
|
222
|
+
} else {
|
|
223
|
+
newW = Math.min(Math.max(20, Math.round(newH * aspectRatio)), maxW);
|
|
224
|
+
newH = Math.max(20, Math.round(newW / aspectRatio));
|
|
225
|
+
}
|
|
193
226
|
}
|
|
194
|
-
}
|
|
195
227
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
228
|
+
img.style.width = `${newW}px`;
|
|
229
|
+
img.style.height = `${newH}px`;
|
|
230
|
+
this._updateOverlayPosition();
|
|
231
|
+
});
|
|
199
232
|
};
|
|
200
233
|
|
|
201
234
|
const onUp = () => {
|
|
235
|
+
if (_raf !== null) { cancelAnimationFrame(_raf); _raf = null; }
|
|
202
236
|
document.removeEventListener('mousemove', onMove);
|
|
203
237
|
document.removeEventListener('mouseup', onUp);
|
|
204
238
|
this._dragDisposers = null;
|
|
@@ -11,6 +11,8 @@ const ICONS = {
|
|
|
11
11
|
originalSize:`<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"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`,
|
|
12
12
|
deleteImg: `<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"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
|
|
13
13
|
caption: `<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="11" rx="2"/><line x1="6" y1="18" x2="18" y2="18"/><line x1="9" y1="21" x2="15" y2="21"/></svg>`,
|
|
14
|
+
rotateLeft: `<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"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><polyline points="3 3 3 8 8 8"/></svg>`,
|
|
15
|
+
rotateRight: `<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"><path d="M21 12a9 9 0 1 1-9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><polyline points="21 3 21 8 16 8"/></svg>`,
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
const SHOW_DELAY = 100;
|
|
@@ -36,16 +38,17 @@ export class ImageTooltip {
|
|
|
36
38
|
this._disposers.push(
|
|
37
39
|
on(editable, 'mouseover', (e) => {
|
|
38
40
|
const img = e.target.closest('img');
|
|
39
|
-
|
|
41
|
+
// Skip when the image is inside a link — LinkTooltip takes priority there
|
|
42
|
+
if (img && editable.contains(img) && !img.closest('a[href]')) {
|
|
40
43
|
this._scheduleShow(img);
|
|
41
44
|
}
|
|
42
|
-
}),
|
|
45
|
+
}, { passive: true }),
|
|
43
46
|
on(editable, 'mouseout', (e) => {
|
|
44
47
|
const to = e.relatedTarget;
|
|
45
48
|
if (!to || (!editable.contains(to) && !this._el.contains(to))) {
|
|
46
49
|
this._scheduleHide();
|
|
47
50
|
}
|
|
48
|
-
}),
|
|
51
|
+
}, { passive: true }),
|
|
49
52
|
// Hide when image is deselected by clicking elsewhere
|
|
50
53
|
on(document, 'click', (e) => {
|
|
51
54
|
if (this._activeImg && !this._activeImg.contains(e.target) && !this._el.contains(e.target)) {
|
|
@@ -102,6 +105,11 @@ export class ImageTooltip {
|
|
|
102
105
|
|
|
103
106
|
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
104
107
|
|
|
108
|
+
el.appendChild(this._makeBtn(ICONS.rotateLeft, 'Rotate Left', () => this._rotate(-90)));
|
|
109
|
+
el.appendChild(this._makeBtn(ICONS.rotateRight, 'Rotate Right', () => this._rotate(90)));
|
|
110
|
+
|
|
111
|
+
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
112
|
+
|
|
105
113
|
this._captionBtn = this._makeBtn(ICONS.caption, 'Add / Edit Caption', () => this._toggleCaption());
|
|
106
114
|
el.appendChild(this._captionBtn);
|
|
107
115
|
|
|
@@ -165,7 +173,10 @@ export class ImageTooltip {
|
|
|
165
173
|
|
|
166
174
|
_show(img) {
|
|
167
175
|
this._el.style.display = 'flex';
|
|
168
|
-
|
|
176
|
+
// Defer positioning: offsetWidth on a newly-visible element forces layout
|
|
177
|
+
requestAnimationFrame(() => {
|
|
178
|
+
if (this._activeImg) this._positionNear(this._activeImg);
|
|
179
|
+
});
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
_hide() {
|
|
@@ -205,25 +216,27 @@ export class ImageTooltip {
|
|
|
205
216
|
_setFloat(value) {
|
|
206
217
|
const img = this._activeImg;
|
|
207
218
|
if (!img) return;
|
|
208
|
-
img.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
const target = img.closest('figure.an-figure') || img;
|
|
220
|
+
target.style.float = value;
|
|
221
|
+
target.style.display = '';
|
|
222
|
+
target.style.marginLeft = value === 'right' ? '12px' : '';
|
|
223
|
+
target.style.marginRight = value === 'left' ? '12px' : '';
|
|
212
224
|
this.context.invoke('editor.afterCommand');
|
|
213
225
|
this.context.invoke('imageResizer.updateOverlay');
|
|
214
|
-
this._positionNear(
|
|
226
|
+
requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
|
|
215
227
|
}
|
|
216
228
|
|
|
217
229
|
_setCenter() {
|
|
218
230
|
const img = this._activeImg;
|
|
219
231
|
if (!img) return;
|
|
220
|
-
img.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
232
|
+
const target = img.closest('figure.an-figure') || img;
|
|
233
|
+
target.style.float = '';
|
|
234
|
+
target.style.display = 'block';
|
|
235
|
+
target.style.marginLeft = 'auto';
|
|
236
|
+
target.style.marginRight = 'auto';
|
|
224
237
|
this.context.invoke('editor.afterCommand');
|
|
225
238
|
this.context.invoke('imageResizer.updateOverlay');
|
|
226
|
-
this._positionNear(
|
|
239
|
+
requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
|
|
227
240
|
}
|
|
228
241
|
|
|
229
242
|
_resetSize() {
|
|
@@ -233,7 +246,31 @@ export class ImageTooltip {
|
|
|
233
246
|
img.style.height = '';
|
|
234
247
|
this.context.invoke('editor.afterCommand');
|
|
235
248
|
this.context.invoke('imageResizer.updateOverlay');
|
|
236
|
-
this._positionNear(
|
|
249
|
+
requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Rotate the active image by `delta` degrees (±90).
|
|
254
|
+
* Reads the existing rotate() value from the transform style so
|
|
255
|
+
* repeated clicks accumulate correctly.
|
|
256
|
+
* @param {number} delta
|
|
257
|
+
*/
|
|
258
|
+
_rotate(delta) {
|
|
259
|
+
const img = this._activeImg;
|
|
260
|
+
if (!img) return;
|
|
261
|
+
// Parse current rotation angle from inline transform
|
|
262
|
+
const current = img.style.transform || '';
|
|
263
|
+
const match = current.match(/rotate\((-?[\d.]+)deg\)/);
|
|
264
|
+
const prev = match ? parseFloat(match[1]) : 0;
|
|
265
|
+
const next = (prev + delta + 360) % 360; // normalise to [0, 360)
|
|
266
|
+
// Preserve any other transform functions (e.g. scale), replace only rotate()
|
|
267
|
+
const cleaned = current.replace(/rotate\(-?[\d.]+deg\)/, '').trim();
|
|
268
|
+
img.style.transform = cleaned
|
|
269
|
+
? `${cleaned} rotate(${next}deg)`
|
|
270
|
+
: next === 0 ? '' : `rotate(${next}deg)`;
|
|
271
|
+
this.context.invoke('editor.afterCommand');
|
|
272
|
+
this.context.invoke('imageResizer.updateOverlay');
|
|
273
|
+
requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
|
|
237
274
|
}
|
|
238
275
|
|
|
239
276
|
_delete() {
|
|
@@ -241,7 +278,12 @@ export class ImageTooltip {
|
|
|
241
278
|
if (!img) return;
|
|
242
279
|
this._hide();
|
|
243
280
|
this.context.invoke('imageResizer.deselect');
|
|
244
|
-
|
|
281
|
+
const figure = img.closest('figure.an-figure');
|
|
282
|
+
if (figure && figure.parentNode) {
|
|
283
|
+
figure.parentNode.removeChild(figure);
|
|
284
|
+
} else if (img.parentNode) {
|
|
285
|
+
img.parentNode.removeChild(img);
|
|
286
|
+
}
|
|
245
287
|
this.context.invoke('editor.afterCommand');
|
|
246
288
|
}
|
|
247
289
|
|
|
@@ -266,6 +308,23 @@ export class ImageTooltip {
|
|
|
266
308
|
// Wrap image in <figure><figcaption>
|
|
267
309
|
const figure = document.createElement('figure');
|
|
268
310
|
figure.className = 'an-figure';
|
|
311
|
+
|
|
312
|
+
// Transfer existing alignment from <img> to <figure> (C4 fix)
|
|
313
|
+
if (img.style.float) {
|
|
314
|
+
figure.style.float = img.style.float;
|
|
315
|
+
figure.style.marginLeft = img.style.marginLeft;
|
|
316
|
+
figure.style.marginRight = img.style.marginRight;
|
|
317
|
+
img.style.float = '';
|
|
318
|
+
img.style.marginLeft = '';
|
|
319
|
+
img.style.marginRight = '';
|
|
320
|
+
} else if (img.style.display === 'block' && img.style.marginLeft === 'auto') {
|
|
321
|
+
figure.style.display = 'block';
|
|
322
|
+
figure.style.marginLeft = 'auto';
|
|
323
|
+
figure.style.marginRight = 'auto';
|
|
324
|
+
img.style.display = '';
|
|
325
|
+
img.style.marginLeft = '';
|
|
326
|
+
img.style.marginRight = '';
|
|
327
|
+
}
|
|
269
328
|
const figcaption = document.createElement('figcaption');
|
|
270
329
|
figcaption.className = 'an-figcaption';
|
|
271
330
|
figcaption.textContent = 'Caption';
|
|
@@ -281,6 +340,8 @@ export class ImageTooltip {
|
|
|
281
340
|
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
|
|
282
341
|
|
|
283
342
|
this.context.invoke('editor.afterCommand');
|
|
343
|
+
// Re-sync the resize overlay: wrapping img in <figure> changes its layout position.
|
|
344
|
+
this.context.invoke('imageResizer.updateOverlay');
|
|
284
345
|
this._hide();
|
|
285
346
|
}
|
|
286
347
|
}
|
|
@@ -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 = [
|