autumnnote 1.5.0 → 1.6.1
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 +10 -8
- package/dist/autumnnote.css +324 -4
- package/dist/autumnnote.es.js +943 -526
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +935 -519
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +21 -3
- package/src/js/Context.js +23 -16
- package/src/js/core/detectLang.js +98 -0
- package/src/js/core/dom.js +67 -11
- package/src/js/core/env.js +1 -1
- package/src/js/core/func.js +1 -1
- package/src/js/core/lists.js +1 -1
- package/src/js/core/markdown.js +32 -31
- package/src/js/core/range.js +8 -8
- package/src/js/editing/History.js +10 -10
- package/src/js/editing/Style.js +44 -44
- package/src/js/editing/Table.js +5 -7
- package/src/js/editing/Typing.js +19 -19
- package/src/js/i18n/en.js +2 -0
- package/src/js/i18n/vi.js +2 -0
- package/src/js/index.js +3 -2
- package/src/js/module/AutoSaveRestore.js +2 -4
- package/src/js/module/BubbleToolbar.js +51 -34
- package/src/js/module/Buttons.js +20 -18
- package/src/js/module/Clipboard.js +16 -17
- package/src/js/module/CodeTooltip.js +48 -24
- package/src/js/module/Codeview.js +5 -7
- package/src/js/module/ContextMenu.js +37 -37
- package/src/js/module/Editor.js +77 -26
- package/src/js/module/EmojiDialog.js +24 -18
- package/src/js/module/FindReplace.js +93 -66
- package/src/js/module/Fullscreen.js +1 -1
- package/src/js/module/IconDialog.js +39 -35
- package/src/js/module/ImageCropOverlay.js +13 -13
- package/src/js/module/ImageDialog.js +19 -13
- package/src/js/module/ImageResizer.js +5 -5
- package/src/js/module/ImageTooltip.js +20 -16
- package/src/js/module/LinkDialog.js +20 -14
- package/src/js/module/LinkTooltip.js +15 -12
- package/src/js/module/MarkdownShortcuts.js +11 -14
- package/src/js/module/Mention.js +11 -13
- package/src/js/module/Placeholder.js +1 -1
- package/src/js/module/ShortcutsDialog.js +2 -4
- package/src/js/module/Statusbar.js +5 -7
- package/src/js/module/TableTooltip.js +196 -36
- package/src/js/module/Toolbar.js +35 -34
- package/src/js/module/VideoDialog.js +16 -10
- package/src/js/module/VideoResizer.js +7 -9
- package/src/js/module/VideoTooltip.js +15 -10
- package/src/js/renderer.js +5 -3
- package/src/js/settings.js +53 -36
- package/src/styles/autumnnote.scss +332 -7
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Inspired by Summernote's ImageDialog — rewritten without jQuery
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createElement, on, trapFocus } from '../core/dom.js';
|
|
6
|
+
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
7
7
|
import { withSavedRange } from '../core/range.js';
|
|
8
8
|
|
|
9
9
|
export class ImageDialog {
|
|
@@ -33,7 +33,7 @@ export class ImageDialog {
|
|
|
33
33
|
this._disposers.forEach((d) => d());
|
|
34
34
|
this._disposers = [];
|
|
35
35
|
if (this._dialog && this._dialog.parentNode) {
|
|
36
|
-
this._dialog.
|
|
36
|
+
this._dialog.remove();
|
|
37
37
|
}
|
|
38
38
|
this._dialog = null;
|
|
39
39
|
}
|
|
@@ -66,32 +66,37 @@ export class ImageDialog {
|
|
|
66
66
|
});
|
|
67
67
|
const box = createElement('div', { class: 'an-dialog-box' });
|
|
68
68
|
|
|
69
|
+
const header = createElement('div', { class: 'an-dialog-header' });
|
|
70
|
+
const iconEl = createElement('span', { class: 'an-dialog-icon' });
|
|
71
|
+
iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`;
|
|
69
72
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
70
73
|
title.textContent = L.title;
|
|
74
|
+
header.appendChild(iconEl);
|
|
75
|
+
header.appendChild(title);
|
|
71
76
|
|
|
72
77
|
// URL tab
|
|
73
78
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
74
79
|
urlLabel.textContent = L.imageUrl;
|
|
75
|
-
const urlInput = createElement('input', {
|
|
80
|
+
const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
76
81
|
type: 'url',
|
|
77
82
|
class: 'an-input',
|
|
78
83
|
placeholder: L.urlPlaceholder,
|
|
79
84
|
autocomplete: 'off',
|
|
80
|
-
});
|
|
85
|
+
}));
|
|
81
86
|
this._urlInput = urlInput;
|
|
82
87
|
|
|
83
88
|
// Alt text
|
|
84
89
|
const altLabel = createElement('label', { class: 'an-label' });
|
|
85
90
|
altLabel.textContent = L.altText;
|
|
86
|
-
const altInput = createElement('input', {
|
|
91
|
+
const altInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
87
92
|
type: 'text',
|
|
88
93
|
class: 'an-input',
|
|
89
94
|
placeholder: L.altPlaceholder,
|
|
90
95
|
autocomplete: 'off',
|
|
91
|
-
});
|
|
96
|
+
}));
|
|
92
97
|
this._altInput = altInput;
|
|
93
98
|
|
|
94
|
-
box.append(
|
|
99
|
+
box.append(header, urlLabel, urlInput, altLabel, altInput);
|
|
95
100
|
|
|
96
101
|
// Alignment
|
|
97
102
|
const alignLabel = createElement('label', { class: 'an-label' });
|
|
@@ -105,7 +110,7 @@ export class ImageDialog {
|
|
|
105
110
|
];
|
|
106
111
|
alignOptions.forEach(({ value, label }) => {
|
|
107
112
|
const radioId = `an-align-${value || 'none'}`;
|
|
108
|
-
const radio = createElement('input', { type: 'radio', name: 'an-img-align', id: radioId, value });
|
|
113
|
+
const radio = /** @type {HTMLInputElement} */ (createElement('input', { type: 'radio', name: 'an-img-align', id: radioId, value }));
|
|
109
114
|
if (value === '') radio.checked = true;
|
|
110
115
|
const lbl = createElement('label', { for: radioId, class: 'an-align-label' });
|
|
111
116
|
lbl.textContent = label;
|
|
@@ -116,11 +121,11 @@ export class ImageDialog {
|
|
|
116
121
|
if (this.options.allowImageUpload !== false) {
|
|
117
122
|
const fileLabel = createElement('label', { class: 'an-label' });
|
|
118
123
|
fileLabel.textContent = L.uploadLabel;
|
|
119
|
-
const fileInput = createElement('input', {
|
|
124
|
+
const fileInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
120
125
|
type: 'file',
|
|
121
126
|
class: 'an-input',
|
|
122
127
|
accept: 'image/jpeg,image/png,image/gif,image/webp,image/svg+xml,image/avif',
|
|
123
|
-
});
|
|
128
|
+
}));
|
|
124
129
|
this._fileInput = fileInput;
|
|
125
130
|
// Hint line shown below the file input for format errors
|
|
126
131
|
const fileHint = createElement('p', { class: 'an-dialog-hint' });
|
|
@@ -141,12 +146,13 @@ export class ImageDialog {
|
|
|
141
146
|
|
|
142
147
|
box.append(btnRow);
|
|
143
148
|
overlay.appendChild(box);
|
|
149
|
+
makeDraggable(header, box);
|
|
144
150
|
|
|
145
151
|
const d1 = on(insertBtn, 'click', () => this._onInsert());
|
|
146
152
|
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
147
153
|
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
148
|
-
const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
149
|
-
const d5 = on(altInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
154
|
+
const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
155
|
+
const d5 = on(altInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
150
156
|
this._disposers.push(d1, d2, d3, d4, d5);
|
|
151
157
|
|
|
152
158
|
return overlay;
|
|
@@ -197,7 +203,7 @@ export class ImageDialog {
|
|
|
197
203
|
const src = this._urlInput.value.trim();
|
|
198
204
|
const alt = this._altInput.value.trim();
|
|
199
205
|
const alignRadio = this._alignRow && this._alignRow.querySelector('input[name="an-img-align"]:checked');
|
|
200
|
-
const align = alignRadio ? alignRadio.value : '';
|
|
206
|
+
const align = alignRadio ? /** @type {HTMLInputElement} */ (alignRadio).value : '';
|
|
201
207
|
|
|
202
208
|
if (!src) {
|
|
203
209
|
this._urlInput.focus();
|
|
@@ -38,7 +38,7 @@ export class ImageResizer {
|
|
|
38
38
|
|
|
39
39
|
const editable = this.context.layoutInfo.editable;
|
|
40
40
|
|
|
41
|
-
// Debounce
|
|
41
|
+
// Debounce globalThis resize — _updateOverlayPosition already has rAF gating but
|
|
42
42
|
// every call cancels + re-schedules it; a debounce reduces that churn.
|
|
43
43
|
let _resizeDebounce = null;
|
|
44
44
|
const onWindowResize = () => {
|
|
@@ -51,12 +51,12 @@ export class ImageResizer {
|
|
|
51
51
|
// Also select on right-click so the highlight shows before the context menu
|
|
52
52
|
on(editable, 'contextmenu', (e) => {
|
|
53
53
|
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
54
|
-
const img = e.target
|
|
54
|
+
const img = /** @type {Element} */ (e.target)?.closest('img');
|
|
55
55
|
if (img) this._select(img);
|
|
56
56
|
}),
|
|
57
57
|
on(document, 'click', (e) => this._onDocClick(e)),
|
|
58
|
-
on(
|
|
59
|
-
on(
|
|
58
|
+
on(globalThis, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
59
|
+
on(globalThis, 'resize', onWindowResize, { passive: true }),
|
|
60
60
|
on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
61
61
|
);
|
|
62
62
|
|
|
@@ -76,7 +76,7 @@ export class ImageResizer {
|
|
|
76
76
|
}
|
|
77
77
|
this._deselect();
|
|
78
78
|
if (this._overlay && this._overlay.parentNode) {
|
|
79
|
-
this._overlay.
|
|
79
|
+
this._overlay.remove();
|
|
80
80
|
}
|
|
81
81
|
this._overlay = null;
|
|
82
82
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ImageTooltip.js - Hover tooltip for images inside the editor
|
|
1
|
+
// ImageTooltip.js - Hover tooltip for images inside the editor
|
|
2
2
|
// Displays a horizontal action bar below (or above) the selected image,
|
|
3
3
|
// similar in appearance and interaction to LinkTooltip.
|
|
4
4
|
import { createElement, on } from '../core/dom.js';
|
|
@@ -39,24 +39,28 @@ export class ImageTooltip {
|
|
|
39
39
|
this._disposers.push(
|
|
40
40
|
on(editable, 'mouseover', (e) => {
|
|
41
41
|
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
42
|
-
const img = e.target
|
|
42
|
+
const img = /** @type {Element} */ (e.target)?.closest('img');
|
|
43
43
|
// Skip when the image is inside a link — LinkTooltip takes priority there
|
|
44
44
|
if (img && editable.contains(img) && !img.closest('a[href]')) {
|
|
45
45
|
this._scheduleShow(img);
|
|
46
46
|
}
|
|
47
47
|
}, { passive: true }),
|
|
48
48
|
on(editable, 'mouseout', (e) => {
|
|
49
|
-
const to = e.relatedTarget;
|
|
49
|
+
const to = /** @type {Node|null} */ (/** @type {MouseEvent} */ (e).relatedTarget);
|
|
50
50
|
if (!to || (!editable.contains(to) && !this._el.contains(to))) {
|
|
51
51
|
this._scheduleHide();
|
|
52
52
|
}
|
|
53
53
|
}, { passive: true }),
|
|
54
54
|
// Hide when image is deselected by clicking elsewhere
|
|
55
55
|
on(document, 'click', (e) => {
|
|
56
|
-
|
|
56
|
+
const et = /** @type {Node} */ (e.target);
|
|
57
|
+
if (this._activeImg && !this._activeImg.contains(et) && !this._el.contains(et)) {
|
|
57
58
|
this._hide();
|
|
58
59
|
}
|
|
59
60
|
}),
|
|
61
|
+
// Hide when the page scrolls or resizes — the tooltip position becomes stale
|
|
62
|
+
on(globalThis, 'scroll', () => this._hide(), { passive: true }),
|
|
63
|
+
on(globalThis, 'resize', () => this._hide(), { passive: true }),
|
|
60
64
|
);
|
|
61
65
|
|
|
62
66
|
return this;
|
|
@@ -66,7 +70,7 @@ export class ImageTooltip {
|
|
|
66
70
|
this._clearTimers();
|
|
67
71
|
this._disposers.forEach((d) => d());
|
|
68
72
|
this._disposers = [];
|
|
69
|
-
|
|
73
|
+
this._el?.remove();
|
|
70
74
|
this._el = null;
|
|
71
75
|
}
|
|
72
76
|
|
|
@@ -181,7 +185,7 @@ export class ImageTooltip {
|
|
|
181
185
|
this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
|
|
182
186
|
}
|
|
183
187
|
|
|
184
|
-
_show(
|
|
188
|
+
_show(_img) {
|
|
185
189
|
this._el.style.display = 'flex';
|
|
186
190
|
// Defer positioning: offsetWidth on a newly-visible element forces layout
|
|
187
191
|
requestAnimationFrame(() => {
|
|
@@ -211,8 +215,8 @@ export class ImageTooltip {
|
|
|
211
215
|
let top = rect.bottom + margin;
|
|
212
216
|
let left = rect.left + (rect.width - tipW) / 2;
|
|
213
217
|
|
|
214
|
-
if (top + tipH >
|
|
215
|
-
if (left + tipW >
|
|
218
|
+
if (top + tipH > globalThis.innerHeight - margin) top = rect.top - tipH - margin;
|
|
219
|
+
if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
|
|
216
220
|
if (left < margin) left = margin;
|
|
217
221
|
|
|
218
222
|
this._el.style.top = `${top}px`;
|
|
@@ -275,8 +279,8 @@ export class ImageTooltip {
|
|
|
275
279
|
if (!img) return;
|
|
276
280
|
// Parse current rotation angle from inline transform
|
|
277
281
|
const current = img.style.transform || '';
|
|
278
|
-
const match =
|
|
279
|
-
const prev = match ? parseFloat(match[1]) : 0;
|
|
282
|
+
const match = /rotate\((-?[\d.]+)deg\)/.exec(current);
|
|
283
|
+
const prev = match ? Number.parseFloat(match[1]) : 0;
|
|
280
284
|
const next = (prev + delta + 360) % 360; // normalise to [0, 360)
|
|
281
285
|
// Preserve any other transform functions (e.g. scale), replace only rotate()
|
|
282
286
|
const cleaned = current.replace(/rotate\(-?[\d.]+deg\)/, '').trim();
|
|
@@ -294,10 +298,10 @@ export class ImageTooltip {
|
|
|
294
298
|
this._hide();
|
|
295
299
|
this.context.invoke('imageResizer.deselect');
|
|
296
300
|
const figure = img.closest('figure.an-figure');
|
|
297
|
-
if (figure
|
|
298
|
-
figure.
|
|
299
|
-
} else
|
|
300
|
-
img.
|
|
301
|
+
if (figure) {
|
|
302
|
+
figure.remove();
|
|
303
|
+
} else {
|
|
304
|
+
img.remove();
|
|
301
305
|
}
|
|
302
306
|
this.context.invoke('editor.afterCommand');
|
|
303
307
|
}
|
|
@@ -323,7 +327,7 @@ export class ImageTooltip {
|
|
|
323
327
|
this._hide();
|
|
324
328
|
const range = document.createRange();
|
|
325
329
|
range.selectNodeContents(cap);
|
|
326
|
-
const sel =
|
|
330
|
+
const sel = globalThis.getSelection();
|
|
327
331
|
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
|
|
328
332
|
}
|
|
329
333
|
return;
|
|
@@ -363,7 +367,7 @@ export class ImageTooltip {
|
|
|
363
367
|
// Select caption text immediately
|
|
364
368
|
const range = document.createRange();
|
|
365
369
|
range.selectNodeContents(figcaption);
|
|
366
|
-
const sel =
|
|
370
|
+
const sel = globalThis.getSelection();
|
|
367
371
|
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
|
|
368
372
|
|
|
369
373
|
this.context.invoke('editor.afterCommand');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Inspired by Summernote's LinkDialog — rewritten without jQuery
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createElement, on, trapFocus } from '../core/dom.js';
|
|
6
|
+
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
7
7
|
import { withSavedRange } from '../core/range.js';
|
|
8
8
|
|
|
9
9
|
export class LinkDialog {
|
|
@@ -33,7 +33,7 @@ export class LinkDialog {
|
|
|
33
33
|
this._disposers.forEach((d) => d());
|
|
34
34
|
this._disposers = [];
|
|
35
35
|
if (this._dialog && this._dialog.parentNode) {
|
|
36
|
-
this._dialog.
|
|
36
|
+
this._dialog.remove();
|
|
37
37
|
}
|
|
38
38
|
this._dialog = null;
|
|
39
39
|
}
|
|
@@ -63,42 +63,47 @@ export class LinkDialog {
|
|
|
63
63
|
const overlay = createElement('div', { class: 'an-dialog-overlay', role: 'dialog', 'aria-modal': 'true', 'aria-label': L.ariaLabel });
|
|
64
64
|
const box = createElement('div', { class: 'an-dialog-box' });
|
|
65
65
|
|
|
66
|
+
const header = createElement('div', { class: 'an-dialog-header' });
|
|
67
|
+
const iconEl = createElement('span', { class: 'an-dialog-icon' });
|
|
68
|
+
iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
|
|
66
69
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
67
70
|
title.textContent = L.title;
|
|
71
|
+
header.appendChild(iconEl);
|
|
72
|
+
header.appendChild(title);
|
|
68
73
|
|
|
69
74
|
// URL field
|
|
70
75
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
71
76
|
urlLabel.textContent = L.url;
|
|
72
|
-
const urlInput = createElement('input', {
|
|
77
|
+
const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
73
78
|
type: 'url',
|
|
74
79
|
class: 'an-input',
|
|
75
80
|
placeholder: L.urlPlaceholder,
|
|
76
81
|
id: 'an-link-url',
|
|
77
82
|
name: 'url',
|
|
78
83
|
autocomplete: 'off',
|
|
79
|
-
});
|
|
84
|
+
}));
|
|
80
85
|
this._urlInput = urlInput;
|
|
81
86
|
|
|
82
87
|
// Text field
|
|
83
88
|
const textLabel = createElement('label', { class: 'an-label' });
|
|
84
89
|
textLabel.textContent = L.displayText;
|
|
85
|
-
const textInput = createElement('input', {
|
|
90
|
+
const textInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
86
91
|
type: 'text',
|
|
87
92
|
class: 'an-input',
|
|
88
93
|
placeholder: L.textPlaceholder,
|
|
89
94
|
id: 'an-link-text',
|
|
90
95
|
name: 'linkText',
|
|
91
96
|
autocomplete: 'off',
|
|
92
|
-
});
|
|
97
|
+
}));
|
|
93
98
|
this._textInput = textInput;
|
|
94
99
|
|
|
95
100
|
// Open in new tab
|
|
96
101
|
const tabLabel = createElement('label', { class: 'an-label an-label-inline' });
|
|
97
|
-
const tabCheckbox = createElement('input', {
|
|
102
|
+
const tabCheckbox = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
98
103
|
type: 'checkbox',
|
|
99
104
|
id: 'an-link-newtab',
|
|
100
105
|
name: 'openInNewTab',
|
|
101
|
-
});
|
|
106
|
+
}));
|
|
102
107
|
this._tabCheckbox = tabCheckbox;
|
|
103
108
|
tabLabel.appendChild(tabCheckbox);
|
|
104
109
|
tabLabel.appendChild(document.createTextNode(' ' + L.openInNewTab));
|
|
@@ -112,15 +117,16 @@ export class LinkDialog {
|
|
|
112
117
|
btnRow.appendChild(insertBtn);
|
|
113
118
|
btnRow.appendChild(cancelBtn);
|
|
114
119
|
|
|
115
|
-
box.append(
|
|
120
|
+
box.append(header, urlLabel, urlInput, textLabel, textInput, tabLabel, btnRow);
|
|
116
121
|
overlay.appendChild(box);
|
|
122
|
+
makeDraggable(header, box);
|
|
117
123
|
|
|
118
124
|
// Events
|
|
119
125
|
const d1 = on(insertBtn, 'click', () => this._onInsert());
|
|
120
126
|
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
121
127
|
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
122
|
-
const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
123
|
-
const d5 = on(textInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
128
|
+
const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
129
|
+
const d5 = on(textInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
124
130
|
this._disposers.push(d1, d2, d3, d4, d5);
|
|
125
131
|
|
|
126
132
|
return overlay;
|
|
@@ -132,7 +138,7 @@ export class LinkDialog {
|
|
|
132
138
|
|
|
133
139
|
_prefill() {
|
|
134
140
|
// Try to find currently selected anchor
|
|
135
|
-
const sel =
|
|
141
|
+
const sel = globalThis.getSelection();
|
|
136
142
|
let anchor = null;
|
|
137
143
|
if (sel && sel.rangeCount > 0) {
|
|
138
144
|
let node = sel.getRangeAt(0).startContainer;
|
|
@@ -142,9 +148,9 @@ export class LinkDialog {
|
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
150
|
if (anchor) {
|
|
145
|
-
this._urlInput.value = anchor.getAttribute('href') || '';
|
|
151
|
+
this._urlInput.value = /** @type {Element} */ (anchor).getAttribute('href') || '';
|
|
146
152
|
this._textInput.value = anchor.textContent || '';
|
|
147
|
-
this._tabCheckbox.checked = anchor.getAttribute('target') === '_blank';
|
|
153
|
+
this._tabCheckbox.checked = /** @type {Element} */ (anchor).getAttribute('target') === '_blank';
|
|
148
154
|
} else {
|
|
149
155
|
this._urlInput.value = '';
|
|
150
156
|
this._textInput.value = sel ? sel.toString() : '';
|
|
@@ -33,18 +33,21 @@ export class LinkTooltip {
|
|
|
33
33
|
this._disposers.push(
|
|
34
34
|
// Detect when pointer enters a link
|
|
35
35
|
on(editable, 'mouseover', (e) => {
|
|
36
|
-
const anchor = e.target
|
|
36
|
+
const anchor = /** @type {Element} */ (e.target)?.closest('a[href]');
|
|
37
37
|
if (anchor && editable.contains(anchor)) {
|
|
38
38
|
this._scheduleShow(anchor);
|
|
39
39
|
}
|
|
40
40
|
}),
|
|
41
41
|
// Detect when pointer leaves the editable area entirely
|
|
42
42
|
on(editable, 'mouseout', (e) => {
|
|
43
|
-
const to = e.relatedTarget;
|
|
43
|
+
const to = /** @type {Node|null} */ (/** @type {MouseEvent} */ (e).relatedTarget);
|
|
44
44
|
if (!to || (!editable.contains(to) && !this._el.contains(to))) {
|
|
45
45
|
this._scheduleHide();
|
|
46
46
|
}
|
|
47
47
|
}),
|
|
48
|
+
// Hide when the page scrolls or resizes — the tooltip position becomes stale
|
|
49
|
+
on(globalThis, 'scroll', () => this._hide(), { passive: true }),
|
|
50
|
+
on(globalThis, 'resize', () => this._hide(), { passive: true }),
|
|
48
51
|
);
|
|
49
52
|
|
|
50
53
|
return this;
|
|
@@ -54,7 +57,7 @@ export class LinkTooltip {
|
|
|
54
57
|
this._clearTimers();
|
|
55
58
|
this._disposers.forEach((d) => d());
|
|
56
59
|
this._disposers = [];
|
|
57
|
-
if (this._el && this._el.parentNode) this._el.
|
|
60
|
+
if (this._el && this._el.parentNode) this._el.remove();
|
|
58
61
|
this._el = null;
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -160,11 +163,11 @@ export class LinkTooltip {
|
|
|
160
163
|
let top = rect.bottom + margin;
|
|
161
164
|
let left = rect.left;
|
|
162
165
|
|
|
163
|
-
if (top + tipH >
|
|
166
|
+
if (top + tipH > globalThis.innerHeight - margin) {
|
|
164
167
|
top = rect.top - tipH - margin;
|
|
165
168
|
}
|
|
166
|
-
if (left + tipW >
|
|
167
|
-
left =
|
|
169
|
+
if (left + tipW > globalThis.innerWidth - margin) {
|
|
170
|
+
left = globalThis.innerWidth - tipW - margin;
|
|
168
171
|
}
|
|
169
172
|
if (left < margin) left = margin;
|
|
170
173
|
|
|
@@ -187,13 +190,13 @@ export class LinkTooltip {
|
|
|
187
190
|
// ---------------------------------------------------------------------------
|
|
188
191
|
|
|
189
192
|
_openLink() {
|
|
190
|
-
const url = this._activeAnchor
|
|
191
|
-
if (url)
|
|
193
|
+
const url = this._activeAnchor?.getAttribute('href');
|
|
194
|
+
if (url) globalThis.open(url, '_blank', 'noopener,noreferrer');
|
|
192
195
|
this._hide();
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
_copyLink() {
|
|
196
|
-
const url = this._activeAnchor
|
|
199
|
+
const url = this._activeAnchor?.getAttribute('href');
|
|
197
200
|
if (url) {
|
|
198
201
|
navigator.clipboard.writeText(url).catch(() => {
|
|
199
202
|
// Fallback for environments where clipboard API is unavailable
|
|
@@ -204,7 +207,7 @@ export class LinkTooltip {
|
|
|
204
207
|
document.body.appendChild(ta);
|
|
205
208
|
ta.select();
|
|
206
209
|
document.execCommand('copy');
|
|
207
|
-
|
|
210
|
+
ta.remove();
|
|
208
211
|
});
|
|
209
212
|
}
|
|
210
213
|
// Brief visual feedback
|
|
@@ -220,7 +223,7 @@ export class LinkTooltip {
|
|
|
220
223
|
this._hide();
|
|
221
224
|
|
|
222
225
|
// Place cursor inside the anchor, then open the link dialog (which pre-fills from selection)
|
|
223
|
-
const sel =
|
|
226
|
+
const sel = globalThis.getSelection();
|
|
224
227
|
const range = document.createRange();
|
|
225
228
|
range.selectNodeContents(anchor);
|
|
226
229
|
sel.removeAllRanges();
|
|
@@ -235,7 +238,7 @@ export class LinkTooltip {
|
|
|
235
238
|
this._hide();
|
|
236
239
|
|
|
237
240
|
// Select the anchor text, then remove the link
|
|
238
|
-
const sel =
|
|
241
|
+
const sel = globalThis.getSelection();
|
|
239
242
|
const range = document.createRange();
|
|
240
243
|
range.selectNode(anchor);
|
|
241
244
|
sel.removeAllRanges();
|
|
@@ -61,8 +61,8 @@ export class MarkdownShortcuts {
|
|
|
61
61
|
* @returns {{ text: string, range: Range, lineNode: Node } | null}
|
|
62
62
|
*/
|
|
63
63
|
_getLineContext() {
|
|
64
|
-
const sel =
|
|
65
|
-
if (!sel
|
|
64
|
+
const sel = globalThis.getSelection();
|
|
65
|
+
if (!sel?.rangeCount) return null;
|
|
66
66
|
const range = sel.getRangeAt(0);
|
|
67
67
|
if (!range.collapsed) return null;
|
|
68
68
|
|
|
@@ -87,7 +87,7 @@ export class MarkdownShortcuts {
|
|
|
87
87
|
|
|
88
88
|
_isBlock(node) {
|
|
89
89
|
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
90
|
-
const display =
|
|
90
|
+
const display = globalThis.getComputedStyle(node).display;
|
|
91
91
|
return display === 'block' || display === 'list-item' || display === 'table-cell';
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -106,7 +106,7 @@ export class MarkdownShortcuts {
|
|
|
106
106
|
];
|
|
107
107
|
|
|
108
108
|
for (const { re, handler } of blockPatterns) {
|
|
109
|
-
const m =
|
|
109
|
+
const m = re.exec(text);
|
|
110
110
|
if (m) {
|
|
111
111
|
handler(m);
|
|
112
112
|
return true;
|
|
@@ -137,8 +137,8 @@ export class MarkdownShortcuts {
|
|
|
137
137
|
// ---------------------------------------------------------------------------
|
|
138
138
|
|
|
139
139
|
_selectLineAndDelete() {
|
|
140
|
-
const sel =
|
|
141
|
-
if (!sel
|
|
140
|
+
const sel = globalThis.getSelection();
|
|
141
|
+
if (!sel?.rangeCount) return;
|
|
142
142
|
const range = sel.getRangeAt(0);
|
|
143
143
|
// Select from the start of the block to the cursor and delete
|
|
144
144
|
const startRange = document.createRange();
|
|
@@ -188,8 +188,8 @@ export class MarkdownShortcuts {
|
|
|
188
188
|
// ---------------------------------------------------------------------------
|
|
189
189
|
|
|
190
190
|
_onInput() {
|
|
191
|
-
const sel =
|
|
192
|
-
if (!sel
|
|
191
|
+
const sel = globalThis.getSelection();
|
|
192
|
+
if (!sel?.rangeCount) return;
|
|
193
193
|
const range = sel.getRangeAt(0);
|
|
194
194
|
if (!range.collapsed) return;
|
|
195
195
|
|
|
@@ -215,7 +215,7 @@ export class MarkdownShortcuts {
|
|
|
215
215
|
|
|
216
216
|
const upToCursor = text.slice(0, offset);
|
|
217
217
|
for (const { re, tag } of inlineRules) {
|
|
218
|
-
const m =
|
|
218
|
+
const m = re.exec(upToCursor);
|
|
219
219
|
if (!m) continue;
|
|
220
220
|
|
|
221
221
|
const matchStart = upToCursor.length - m[0].length;
|
|
@@ -233,11 +233,8 @@ export class MarkdownShortcuts {
|
|
|
233
233
|
const beforeNode = document.createTextNode(before);
|
|
234
234
|
const afterNode = document.createTextNode('' + after);
|
|
235
235
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
parent.insertBefore(el, node);
|
|
239
|
-
parent.insertBefore(afterNode, node);
|
|
240
|
-
parent.removeChild(node);
|
|
236
|
+
/** @type {ChildNode} */ (node).before(beforeNode, el, afterNode);
|
|
237
|
+
/** @type {ChildNode} */ (node).remove();
|
|
241
238
|
|
|
242
239
|
// Place cursor after the element (after the ZWS)
|
|
243
240
|
const newRange = document.createRange();
|
package/src/js/module/Mention.js
CHANGED
|
@@ -76,9 +76,7 @@ export class Mention {
|
|
|
76
76
|
|
|
77
77
|
destroy() {
|
|
78
78
|
clearTimeout(this._debounceTimer);
|
|
79
|
-
|
|
80
|
-
this._dropdown.parentNode.removeChild(this._dropdown);
|
|
81
|
-
}
|
|
79
|
+
this._dropdown?.remove();
|
|
82
80
|
this._dropdown = null;
|
|
83
81
|
this._disposers.forEach((d) => d());
|
|
84
82
|
this._disposers = [];
|
|
@@ -96,11 +94,11 @@ export class Mention {
|
|
|
96
94
|
// Event delegation: single listeners on the container instead of per-item
|
|
97
95
|
el.addEventListener('mousedown', (e) => e.preventDefault());
|
|
98
96
|
el.addEventListener('click', (e) => {
|
|
99
|
-
const item = e.target
|
|
97
|
+
const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-mention-item'));
|
|
100
98
|
if (item) this._select(+item.dataset.index);
|
|
101
99
|
});
|
|
102
100
|
el.addEventListener('mousemove', (e) => {
|
|
103
|
-
const item = e.target
|
|
101
|
+
const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-mention-item'));
|
|
104
102
|
if (item) this._highlightItem(+item.dataset.index);
|
|
105
103
|
});
|
|
106
104
|
|
|
@@ -119,7 +117,7 @@ export class Mention {
|
|
|
119
117
|
const li = document.createElement('div');
|
|
120
118
|
li.className = 'an-mention-item';
|
|
121
119
|
li.setAttribute('role', 'option');
|
|
122
|
-
li.dataset.index = i;
|
|
120
|
+
li.dataset.index = String(i);
|
|
123
121
|
if (item.avatar) {
|
|
124
122
|
const img = document.createElement('img');
|
|
125
123
|
img.src = item.avatar;
|
|
@@ -163,10 +161,10 @@ export class Mention {
|
|
|
163
161
|
let top = rect.bottom + 4;
|
|
164
162
|
let left = rect.left;
|
|
165
163
|
|
|
166
|
-
if (rect.bottom + ddh + 8 >
|
|
164
|
+
if (rect.bottom + ddh + 8 > globalThis.innerHeight) {
|
|
167
165
|
top = rect.top - ddh - 4;
|
|
168
166
|
}
|
|
169
|
-
left = Math.max(8, Math.min(left,
|
|
167
|
+
left = Math.max(8, Math.min(left, globalThis.innerWidth - ddw - 8));
|
|
170
168
|
|
|
171
169
|
dd.style.top = `${top}px`;
|
|
172
170
|
dd.style.left = `${left}px`;
|
|
@@ -196,7 +194,7 @@ export class Mention {
|
|
|
196
194
|
_captureCaretRect() {
|
|
197
195
|
// Prefer a range over the trigger character — it has non-zero width and
|
|
198
196
|
// getBoundingClientRect() reliably returns a valid rect.
|
|
199
|
-
if (this._triggerNode
|
|
197
|
+
if (this._triggerNode?.isConnected) {
|
|
200
198
|
try {
|
|
201
199
|
const r = document.createRange();
|
|
202
200
|
const end = Math.min(this._triggerOffset + 1, this._triggerNode.textContent.length);
|
|
@@ -211,7 +209,7 @@ export class Mention {
|
|
|
211
209
|
}
|
|
212
210
|
|
|
213
211
|
// Fallback: use getClientRects() on the current collapsed selection
|
|
214
|
-
const sel =
|
|
212
|
+
const sel = globalThis.getSelection();
|
|
215
213
|
if (!sel || !sel.rangeCount) return;
|
|
216
214
|
const rects = sel.getRangeAt(0).getClientRects();
|
|
217
215
|
if (rects.length > 0) {
|
|
@@ -224,7 +222,7 @@ export class Mention {
|
|
|
224
222
|
// ---------------------------------------------------------------------------
|
|
225
223
|
|
|
226
224
|
_getQueryAtCursor() {
|
|
227
|
-
const sel =
|
|
225
|
+
const sel = globalThis.getSelection();
|
|
228
226
|
if (!sel || !sel.rangeCount) return null;
|
|
229
227
|
const range = sel.getRangeAt(0);
|
|
230
228
|
if (!range.collapsed) return null;
|
|
@@ -305,7 +303,7 @@ export class Mention {
|
|
|
305
303
|
|
|
306
304
|
_onDocClick(e) {
|
|
307
305
|
if (!this._open) return;
|
|
308
|
-
if (this._dropdown
|
|
306
|
+
if (this._dropdown?.contains(e.target)) return;
|
|
309
307
|
this._hideDropdown();
|
|
310
308
|
}
|
|
311
309
|
|
|
@@ -325,7 +323,7 @@ export class Mention {
|
|
|
325
323
|
node.textContent = before + after;
|
|
326
324
|
|
|
327
325
|
// Move cursor to where the trigger was
|
|
328
|
-
const sel =
|
|
326
|
+
const sel = globalThis.getSelection();
|
|
329
327
|
const range = document.createRange();
|
|
330
328
|
range.setStart(node, this._triggerOffset);
|
|
331
329
|
range.collapse(true);
|
|
@@ -43,7 +43,7 @@ export class Placeholder {
|
|
|
43
43
|
// addition to regular whitespace before deciding if the editor is empty.
|
|
44
44
|
// Without this, a freshly-created checklist item or icon leaves a ZWS in
|
|
45
45
|
// the DOM that causes the placeholder to overlap real content (A-1).
|
|
46
|
-
const hasText = editable.textContent.
|
|
46
|
+
const hasText = editable.textContent.replaceAll('\u200B', '').trim().length > 0;
|
|
47
47
|
const isEmpty = !hasText &&
|
|
48
48
|
!editable.querySelector('img, table, hr, .an-video-wrapper');
|
|
49
49
|
editable.classList.toggle('an-placeholder', isEmpty && !isFocused);
|
|
@@ -69,9 +69,7 @@ export class ShortcutsDialog {
|
|
|
69
69
|
destroy() {
|
|
70
70
|
this._disposers.forEach((d) => d());
|
|
71
71
|
this._disposers = [];
|
|
72
|
-
|
|
73
|
-
this._dialog.parentNode.removeChild(this._dialog);
|
|
74
|
-
}
|
|
72
|
+
this._dialog?.remove();
|
|
75
73
|
this._dialog = null;
|
|
76
74
|
}
|
|
77
75
|
|
|
@@ -79,7 +77,7 @@ export class ShortcutsDialog {
|
|
|
79
77
|
if (this._dialog) {
|
|
80
78
|
this._dialog.style.display = 'flex';
|
|
81
79
|
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
82
|
-
setTimeout(() => this._closeBtn
|
|
80
|
+
setTimeout(() => this._closeBtn?.focus(), 50);
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
|