autumnnote 1.6.1 → 1.6.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/dist/autumnnote.es.js +147 -348
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +147 -348
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/js/Context.js +5 -2
- package/src/js/index.js +1 -1
- package/src/js/module/BaseDialog.js +126 -0
- package/src/js/module/EmojiDialog.js +7 -44
- package/src/js/module/FindReplace.js +4 -18
- package/src/js/module/IconDialog.js +9 -41
- package/src/js/module/ImageDialog.js +12 -82
- package/src/js/module/LinkDialog.js +12 -78
- package/src/js/module/ShortcutsDialog.js +20 -49
- package/src/js/module/VideoDialog.js +10 -80
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autumnnote",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "WYSIWYG rich-text editor built with vanilla JavaScript — zero dependencies, no jQuery. Dark mode, @mention, markdown shortcuts, bubble toolbar. React and Vue 3 wrappers included.",
|
|
5
5
|
"main": "dist/autumnnote.umd.js",
|
|
6
6
|
"module": "dist/autumnnote.es.js",
|
package/src/js/Context.js
CHANGED
|
@@ -159,8 +159,10 @@ export class Context {
|
|
|
159
159
|
register('mention', Mention);
|
|
160
160
|
|
|
161
161
|
// Custom modules registered via AutumnNote.registerModule()
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
if (_customModules.size > 0) {
|
|
163
|
+
for (const [name, ModuleClass] of _customModules) {
|
|
164
|
+
register(name, ModuleClass);
|
|
165
|
+
}
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
|
|
@@ -220,6 +222,7 @@ export class Context {
|
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
_applyGlobalPlugins() {
|
|
225
|
+
if (_globalPlugins.size === 0) return;
|
|
223
226
|
for (const { plugin, options } of _globalPlugins.values()) {
|
|
224
227
|
this._installPlugin(plugin, options);
|
|
225
228
|
}
|
package/src/js/index.js
CHANGED
|
@@ -141,7 +141,7 @@ const AutumnNote = {
|
|
|
141
141
|
registerButton(btnDef) { registerButton(btnDef); return this; },
|
|
142
142
|
|
|
143
143
|
/** Library version */
|
|
144
|
-
version: '1.
|
|
144
|
+
version: '1.6.3',
|
|
145
145
|
};
|
|
146
146
|
|
|
147
147
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
2
|
+
import { withSavedRange } from '../core/range.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared lifecycle and shell-building logic for all modal dialogs.
|
|
6
|
+
* Subclasses implement _buildDialog() for their specific form fields.
|
|
7
|
+
*/
|
|
8
|
+
export class BaseDialog {
|
|
9
|
+
/** @param {import('../Context.js').Context} context */
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
this.options = context.options;
|
|
13
|
+
/** @type {HTMLElement|null} */
|
|
14
|
+
this._dialog = null;
|
|
15
|
+
this._disposers = [];
|
|
16
|
+
this._savedRange = null;
|
|
17
|
+
/** @type {HTMLElement|null} First focusable input; set by subclass in _buildDialog(). */
|
|
18
|
+
this._firstInput = null;
|
|
19
|
+
this._removeTrap = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Lifecycle (shared)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
initialize() {
|
|
27
|
+
this._dialog = this._buildDialog();
|
|
28
|
+
document.body.appendChild(this._dialog);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
destroy() {
|
|
33
|
+
this._disposers.forEach((d) => d());
|
|
34
|
+
this._disposers = [];
|
|
35
|
+
if (this._dialog?.parentNode) {
|
|
36
|
+
this._dialog.remove();
|
|
37
|
+
}
|
|
38
|
+
this._dialog = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Shared show helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/** Saves the current selection range before opening the dialog. */
|
|
46
|
+
_saveRange() {
|
|
47
|
+
withSavedRange((range) => { this._savedRange = range; });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_open() {
|
|
51
|
+
if (this._dialog) {
|
|
52
|
+
this._dialog.style.display = 'flex';
|
|
53
|
+
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
54
|
+
setTimeout(() => this._firstInput && this._firstInput.focus(), 50);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_close() {
|
|
59
|
+
if (this._dialog) this._dialog.style.display = 'none';
|
|
60
|
+
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
61
|
+
this._savedRange = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Shell builders (used by subclass _buildDialog())
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Builds the overlay + box + header shell common to all dialogs.
|
|
70
|
+
* Also wires up draggable and overlay-click-to-close.
|
|
71
|
+
* @param {string} ariaLabel
|
|
72
|
+
* @param {string} iconHtml Raw SVG string for the dialog icon
|
|
73
|
+
* @param {string} titleText
|
|
74
|
+
* @returns {{ overlay: HTMLElement, box: HTMLElement }}
|
|
75
|
+
*/
|
|
76
|
+
_buildDialogShell(ariaLabel, iconHtml, titleText) {
|
|
77
|
+
const overlay = createElement('div', {
|
|
78
|
+
class: 'an-dialog-overlay',
|
|
79
|
+
role: 'dialog',
|
|
80
|
+
'aria-modal': 'true',
|
|
81
|
+
'aria-label': ariaLabel,
|
|
82
|
+
});
|
|
83
|
+
const box = createElement('div', { class: 'an-dialog-box' });
|
|
84
|
+
|
|
85
|
+
const header = createElement('div', { class: 'an-dialog-header' });
|
|
86
|
+
const iconEl = createElement('span', { class: 'an-dialog-icon' });
|
|
87
|
+
iconEl.innerHTML = iconHtml;
|
|
88
|
+
const titleEl = createElement('h3', { class: 'an-dialog-title' });
|
|
89
|
+
titleEl.textContent = titleText;
|
|
90
|
+
header.appendChild(iconEl);
|
|
91
|
+
header.appendChild(titleEl);
|
|
92
|
+
|
|
93
|
+
box.appendChild(header);
|
|
94
|
+
overlay.appendChild(box);
|
|
95
|
+
makeDraggable(header, box);
|
|
96
|
+
|
|
97
|
+
const d = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
98
|
+
this._disposers.push(d);
|
|
99
|
+
|
|
100
|
+
return { overlay, box };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Builds an action button row with a primary insert button and a cancel button.
|
|
105
|
+
* Disposers are registered automatically.
|
|
106
|
+
* @param {string} insertLabel
|
|
107
|
+
* @param {string} cancelLabel
|
|
108
|
+
* @param {() => void} onInsert
|
|
109
|
+
* @returns {HTMLElement}
|
|
110
|
+
*/
|
|
111
|
+
_buildButtonRow(insertLabel, cancelLabel, onInsert) {
|
|
112
|
+
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
113
|
+
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
114
|
+
insertBtn.textContent = insertLabel;
|
|
115
|
+
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
116
|
+
cancelBtn.textContent = cancelLabel;
|
|
117
|
+
btnRow.appendChild(insertBtn);
|
|
118
|
+
btnRow.appendChild(cancelBtn);
|
|
119
|
+
|
|
120
|
+
const d1 = on(insertBtn, 'click', onInsert);
|
|
121
|
+
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
122
|
+
this._disposers.push(d1, d2);
|
|
123
|
+
|
|
124
|
+
return btnRow;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Click an emoji to insert it directly at the caret — no extra "Insert" step.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createElement, on,
|
|
7
|
-
import {
|
|
6
|
+
import { createElement, on, makeDraggable } from '../core/dom.js';
|
|
7
|
+
import { BaseDialog } from './BaseDialog.js';
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Emoji catalogue
|
|
@@ -500,37 +500,18 @@ const EMOJI_LIST = [
|
|
|
500
500
|
// Dialog class
|
|
501
501
|
// ---------------------------------------------------------------------------
|
|
502
502
|
|
|
503
|
-
export class EmojiDialog {
|
|
504
|
-
|
|
505
|
-
* @param {import('../Context.js').Context} context
|
|
506
|
-
*/
|
|
507
|
-
constructor(context) {
|
|
508
|
-
this.context = context;
|
|
509
|
-
/** @type {HTMLElement|null} */
|
|
510
|
-
this._dialog = null;
|
|
511
|
-
this._disposers = [];
|
|
512
|
-
this._savedRange = null;
|
|
513
|
-
this._activeCat = 'all';
|
|
514
|
-
}
|
|
503
|
+
export class EmojiDialog extends BaseDialog {
|
|
504
|
+
_activeCat = 'all';
|
|
515
505
|
|
|
516
506
|
// ---------------------------------------------------------------------------
|
|
517
507
|
// Lifecycle
|
|
518
508
|
// ---------------------------------------------------------------------------
|
|
519
509
|
|
|
520
510
|
initialize() {
|
|
521
|
-
//
|
|
511
|
+
// Grid is built lazily in show() to avoid ~500 DOM nodes at load time.
|
|
522
512
|
return this;
|
|
523
513
|
}
|
|
524
514
|
|
|
525
|
-
destroy() {
|
|
526
|
-
this._disposers.forEach((d) => d());
|
|
527
|
-
this._disposers = [];
|
|
528
|
-
if (this._dialog && this._dialog.parentNode) {
|
|
529
|
-
this._dialog.remove();
|
|
530
|
-
}
|
|
531
|
-
this._dialog = null;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
515
|
// ---------------------------------------------------------------------------
|
|
535
516
|
// Public API
|
|
536
517
|
// ---------------------------------------------------------------------------
|
|
@@ -540,9 +521,7 @@ export class EmojiDialog {
|
|
|
540
521
|
this._dialog = this._buildDialog();
|
|
541
522
|
document.body.appendChild(this._dialog);
|
|
542
523
|
}
|
|
543
|
-
|
|
544
|
-
this._savedRange = range;
|
|
545
|
-
});
|
|
524
|
+
this._saveRange();
|
|
546
525
|
this._activeCat = 'all';
|
|
547
526
|
this._searchInput.value = '';
|
|
548
527
|
this._updateCatTabs();
|
|
@@ -612,6 +591,7 @@ export class EmojiDialog {
|
|
|
612
591
|
grid.appendChild(cell);
|
|
613
592
|
});
|
|
614
593
|
this._grid = grid;
|
|
594
|
+
this._firstInput = searchInput;
|
|
615
595
|
|
|
616
596
|
// Cancel only — clicking an emoji inserts immediately
|
|
617
597
|
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
@@ -724,21 +704,4 @@ export class EmojiDialog {
|
|
|
724
704
|
this.context.invoke('editor.afterCommand');
|
|
725
705
|
}
|
|
726
706
|
|
|
727
|
-
// ---------------------------------------------------------------------------
|
|
728
|
-
// Open / Close
|
|
729
|
-
// ---------------------------------------------------------------------------
|
|
730
|
-
|
|
731
|
-
_open() {
|
|
732
|
-
if (this._dialog) {
|
|
733
|
-
this._dialog.style.display = 'flex';
|
|
734
|
-
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
735
|
-
setTimeout(() => this._searchInput?.focus(), 50);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
_close() {
|
|
740
|
-
if (this._dialog) this._dialog.style.display = 'none';
|
|
741
|
-
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
742
|
-
this._savedRange = null;
|
|
743
|
-
}
|
|
744
707
|
}
|
|
@@ -8,14 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
11
|
+
import { BaseDialog } from './BaseDialog.js';
|
|
11
12
|
|
|
12
|
-
export class FindReplace {
|
|
13
|
+
export class FindReplace extends BaseDialog {
|
|
13
14
|
/** @param {import('../Context.js').Context} context */
|
|
14
15
|
constructor(context) {
|
|
15
|
-
|
|
16
|
+
super(context);
|
|
16
17
|
|
|
17
|
-
/** @type {HTMLElement|null} */
|
|
18
|
-
this._dialog = null;
|
|
19
18
|
/** @type {HTMLInputElement|null} */
|
|
20
19
|
this._findInput = null;
|
|
21
20
|
/** @type {HTMLInputElement|null} */
|
|
@@ -39,8 +38,6 @@ export class FindReplace {
|
|
|
39
38
|
this._lastQuery = null;
|
|
40
39
|
this._lastCaseSensitive = null;
|
|
41
40
|
|
|
42
|
-
this._disposers = [];
|
|
43
|
-
this._removeTrap = null;
|
|
44
41
|
this._focusTimer = null;
|
|
45
42
|
}
|
|
46
43
|
|
|
@@ -48,22 +45,11 @@ export class FindReplace {
|
|
|
48
45
|
// Lifecycle
|
|
49
46
|
// ---------------------------------------------------------------------------
|
|
50
47
|
|
|
51
|
-
initialize() {
|
|
52
|
-
this._dialog = this._buildDialog();
|
|
53
|
-
document.body.appendChild(this._dialog);
|
|
54
|
-
return this;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
48
|
destroy() {
|
|
58
49
|
clearTimeout(this._focusTimer);
|
|
59
50
|
this._focusTimer = null;
|
|
60
51
|
this._clearHighlights();
|
|
61
|
-
|
|
62
|
-
this._disposers = [];
|
|
63
|
-
if (this._dialog && this._dialog.parentNode) {
|
|
64
|
-
this._dialog.remove();
|
|
65
|
-
}
|
|
66
|
-
this._dialog = null;
|
|
52
|
+
super.destroy();
|
|
67
53
|
}
|
|
68
54
|
|
|
69
55
|
// ---------------------------------------------------------------------------
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* IconDialog.js - Browse and insert FontAwesome Free icons
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { createElement, on,
|
|
6
|
-
import {
|
|
5
|
+
import { createElement, on, makeDraggable } from '../core/dom.js';
|
|
6
|
+
import { BaseDialog } from './BaseDialog.js';
|
|
7
7
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
// Icon catalogue — FA 6 Free Solid slug names, grouped by category
|
|
@@ -247,19 +247,9 @@ const ICON_LIST = [
|
|
|
247
247
|
['puzzle-piece', 'objects'],
|
|
248
248
|
];
|
|
249
249
|
|
|
250
|
-
export class IconDialog {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
*/
|
|
254
|
-
constructor(context) {
|
|
255
|
-
this.context = context;
|
|
256
|
-
/** @type {HTMLElement|null} */
|
|
257
|
-
this._dialog = null;
|
|
258
|
-
this._disposers = [];
|
|
259
|
-
this._savedRange = null;
|
|
260
|
-
this._selectedIcon = null;
|
|
261
|
-
this._activeCat = 'all';
|
|
262
|
-
}
|
|
250
|
+
export class IconDialog extends BaseDialog {
|
|
251
|
+
_selectedIcon = null;
|
|
252
|
+
_activeCat = 'all';
|
|
263
253
|
|
|
264
254
|
// ---------------------------------------------------------------------------
|
|
265
255
|
// Lifecycle
|
|
@@ -267,7 +257,7 @@ export class IconDialog {
|
|
|
267
257
|
|
|
268
258
|
initialize() {
|
|
269
259
|
this._ensureFontAwesome();
|
|
270
|
-
//
|
|
260
|
+
// Grid is built lazily in show() to avoid ~250 icon cells at load time.
|
|
271
261
|
return this;
|
|
272
262
|
}
|
|
273
263
|
|
|
@@ -294,13 +284,6 @@ export class IconDialog {
|
|
|
294
284
|
document.head.appendChild(link);
|
|
295
285
|
}
|
|
296
286
|
|
|
297
|
-
destroy() {
|
|
298
|
-
this._disposers.forEach((d) => d());
|
|
299
|
-
this._disposers = [];
|
|
300
|
-
this._dialog?.remove();
|
|
301
|
-
this._dialog = null;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
287
|
// ---------------------------------------------------------------------------
|
|
305
288
|
// Public API
|
|
306
289
|
// ---------------------------------------------------------------------------
|
|
@@ -310,9 +293,7 @@ export class IconDialog {
|
|
|
310
293
|
this._dialog = this._buildDialog();
|
|
311
294
|
document.body.appendChild(this._dialog);
|
|
312
295
|
}
|
|
313
|
-
|
|
314
|
-
this._savedRange = range;
|
|
315
|
-
});
|
|
296
|
+
this._saveRange();
|
|
316
297
|
this._selectedIcon = null;
|
|
317
298
|
this._activeCat = 'all';
|
|
318
299
|
this._searchInput.value = '';
|
|
@@ -360,6 +341,7 @@ export class IconDialog {
|
|
|
360
341
|
autocomplete: 'off',
|
|
361
342
|
}));
|
|
362
343
|
this._searchInput = searchInput;
|
|
344
|
+
this._firstInput = searchInput;
|
|
363
345
|
|
|
364
346
|
// Category tabs
|
|
365
347
|
const catBar = createElement('div', { class: 'an-icon-cats' });
|
|
@@ -629,22 +611,8 @@ export class IconDialog {
|
|
|
629
611
|
this.context.invoke('editor.afterCommand');
|
|
630
612
|
}
|
|
631
613
|
|
|
632
|
-
// ---------------------------------------------------------------------------
|
|
633
|
-
// Open / Close
|
|
634
|
-
// ---------------------------------------------------------------------------
|
|
635
|
-
|
|
636
|
-
_open() {
|
|
637
|
-
if (this._dialog) {
|
|
638
|
-
this._dialog.style.display = 'flex';
|
|
639
|
-
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
640
|
-
setTimeout(() => this._searchInput?.focus(), 50);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
614
|
_close() {
|
|
645
|
-
if (this._dialog) this._dialog.style.display = 'none';
|
|
646
|
-
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
647
|
-
this._savedRange = null;
|
|
648
615
|
this._selectedIcon = null;
|
|
616
|
+
super._close();
|
|
649
617
|
}
|
|
650
618
|
}
|
|
@@ -3,49 +3,18 @@
|
|
|
3
3
|
* Inspired by Summernote's ImageDialog — rewritten without jQuery
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createElement, on
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
export class ImageDialog {
|
|
10
|
-
/**
|
|
11
|
-
* @param {import('../Context.js').Context} context
|
|
12
|
-
*/
|
|
13
|
-
constructor(context) {
|
|
14
|
-
this.context = context;
|
|
15
|
-
this.options = context.options;
|
|
16
|
-
/** @type {HTMLElement|null} */
|
|
17
|
-
this._dialog = null;
|
|
18
|
-
this._disposers = [];
|
|
19
|
-
this._savedRange = null;
|
|
20
|
-
}
|
|
6
|
+
import { createElement, on } from '../core/dom.js';
|
|
7
|
+
import { BaseDialog } from './BaseDialog.js';
|
|
21
8
|
|
|
22
|
-
|
|
23
|
-
// Lifecycle
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
initialize() {
|
|
27
|
-
this._dialog = this._buildDialog();
|
|
28
|
-
document.body.appendChild(this._dialog);
|
|
29
|
-
return this;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
destroy() {
|
|
33
|
-
this._disposers.forEach((d) => d());
|
|
34
|
-
this._disposers = [];
|
|
35
|
-
if (this._dialog && this._dialog.parentNode) {
|
|
36
|
-
this._dialog.remove();
|
|
37
|
-
}
|
|
38
|
-
this._dialog = null;
|
|
39
|
-
}
|
|
9
|
+
const ICON_SVG = `<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>`;
|
|
40
10
|
|
|
11
|
+
export class ImageDialog extends BaseDialog {
|
|
41
12
|
// ---------------------------------------------------------------------------
|
|
42
13
|
// Public API
|
|
43
14
|
// ---------------------------------------------------------------------------
|
|
44
15
|
|
|
45
16
|
show() {
|
|
46
|
-
|
|
47
|
-
this._savedRange = range;
|
|
48
|
-
});
|
|
17
|
+
this._saveRange();
|
|
49
18
|
this._urlInput.value = '';
|
|
50
19
|
this._altInput.value = '';
|
|
51
20
|
if (this._fileInput) this._fileInput.value = '';
|
|
@@ -58,23 +27,9 @@ export class ImageDialog {
|
|
|
58
27
|
|
|
59
28
|
_buildDialog() {
|
|
60
29
|
const L = this.context.locale.imageDialog;
|
|
61
|
-
const overlay =
|
|
62
|
-
class: 'an-dialog-overlay',
|
|
63
|
-
role: 'dialog',
|
|
64
|
-
'aria-modal': 'true',
|
|
65
|
-
'aria-label': L.ariaLabel,
|
|
66
|
-
});
|
|
67
|
-
const box = createElement('div', { class: 'an-dialog-box' });
|
|
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>`;
|
|
72
|
-
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
73
|
-
title.textContent = L.title;
|
|
74
|
-
header.appendChild(iconEl);
|
|
75
|
-
header.appendChild(title);
|
|
30
|
+
const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
|
|
76
31
|
|
|
77
|
-
// URL
|
|
32
|
+
// URL field
|
|
78
33
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
79
34
|
urlLabel.textContent = L.imageUrl;
|
|
80
35
|
const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
@@ -84,6 +39,7 @@ export class ImageDialog {
|
|
|
84
39
|
autocomplete: 'off',
|
|
85
40
|
}));
|
|
86
41
|
this._urlInput = urlInput;
|
|
42
|
+
this._firstInput = urlInput;
|
|
87
43
|
|
|
88
44
|
// Alt text
|
|
89
45
|
const altLabel = createElement('label', { class: 'an-label' });
|
|
@@ -96,7 +52,7 @@ export class ImageDialog {
|
|
|
96
52
|
}));
|
|
97
53
|
this._altInput = altInput;
|
|
98
54
|
|
|
99
|
-
box.append(
|
|
55
|
+
box.append(urlLabel, urlInput, altLabel, altInput);
|
|
100
56
|
|
|
101
57
|
// Alignment
|
|
102
58
|
const alignLabel = createElement('label', { class: 'an-label' });
|
|
@@ -118,6 +74,7 @@ export class ImageDialog {
|
|
|
118
74
|
});
|
|
119
75
|
this._alignRow = alignRow;
|
|
120
76
|
box.append(alignLabel, alignRow);
|
|
77
|
+
|
|
121
78
|
if (this.options.allowImageUpload !== false) {
|
|
122
79
|
const fileLabel = createElement('label', { class: 'an-label' });
|
|
123
80
|
fileLabel.textContent = L.uploadLabel;
|
|
@@ -135,25 +92,12 @@ export class ImageDialog {
|
|
|
135
92
|
box.append(fileLabel, fileInput, fileHint);
|
|
136
93
|
}
|
|
137
94
|
|
|
138
|
-
|
|
139
|
-
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
140
|
-
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
141
|
-
insertBtn.textContent = L.insertBtn;
|
|
142
|
-
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
143
|
-
cancelBtn.textContent = L.cancelBtn;
|
|
144
|
-
btnRow.appendChild(insertBtn);
|
|
145
|
-
btnRow.appendChild(cancelBtn);
|
|
146
|
-
|
|
95
|
+
const btnRow = this._buildButtonRow(L.insertBtn, L.cancelBtn, () => this._onInsert());
|
|
147
96
|
box.append(btnRow);
|
|
148
|
-
overlay.appendChild(box);
|
|
149
|
-
makeDraggable(header, box);
|
|
150
97
|
|
|
151
|
-
const d1 = on(insertBtn, 'click', () => this._onInsert());
|
|
152
|
-
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
153
|
-
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
154
98
|
const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
155
99
|
const d5 = on(altInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
156
|
-
this._disposers.push(
|
|
100
|
+
this._disposers.push(d4, d5);
|
|
157
101
|
|
|
158
102
|
return overlay;
|
|
159
103
|
}
|
|
@@ -217,18 +161,4 @@ export class ImageDialog {
|
|
|
217
161
|
this.context.invoke('editor.insertImage', src, alt, align);
|
|
218
162
|
this._close();
|
|
219
163
|
}
|
|
220
|
-
|
|
221
|
-
_open() {
|
|
222
|
-
if (this._dialog) {
|
|
223
|
-
this._dialog.style.display = 'flex';
|
|
224
|
-
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
225
|
-
setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
_close() {
|
|
230
|
-
if (this._dialog) this._dialog.style.display = 'none';
|
|
231
|
-
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
232
|
-
this._savedRange = null;
|
|
233
|
-
}
|
|
234
164
|
}
|