autumnnote 1.6.1 → 1.6.2
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 +123 -213
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +123 -213
- 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/ImageDialog.js +12 -82
- package/src/js/module/LinkDialog.js +12 -78
- 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.2",
|
|
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.2',
|
|
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,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
|
}
|
|
@@ -3,41 +3,12 @@
|
|
|
3
3
|
* Inspired by Summernote's LinkDialog — rewritten without jQuery
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createElement, on
|
|
7
|
-
import {
|
|
6
|
+
import { createElement, on } from '../core/dom.js';
|
|
7
|
+
import { BaseDialog } from './BaseDialog.js';
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
}
|
|
21
|
-
|
|
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"><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>`;
|
|
40
10
|
|
|
11
|
+
export class LinkDialog extends BaseDialog {
|
|
41
12
|
// ---------------------------------------------------------------------------
|
|
42
13
|
// Public API
|
|
43
14
|
// ---------------------------------------------------------------------------
|
|
@@ -47,9 +18,7 @@ export class LinkDialog {
|
|
|
47
18
|
* Pre-fills with the currently selected link if present.
|
|
48
19
|
*/
|
|
49
20
|
show() {
|
|
50
|
-
|
|
51
|
-
this._savedRange = range;
|
|
52
|
-
});
|
|
21
|
+
this._saveRange();
|
|
53
22
|
this._prefill();
|
|
54
23
|
this._open();
|
|
55
24
|
}
|
|
@@ -60,16 +29,7 @@ export class LinkDialog {
|
|
|
60
29
|
|
|
61
30
|
_buildDialog() {
|
|
62
31
|
const L = this.context.locale.linkDialog;
|
|
63
|
-
const
|
|
64
|
-
const box = createElement('div', { class: 'an-dialog-box' });
|
|
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>`;
|
|
69
|
-
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
70
|
-
title.textContent = L.title;
|
|
71
|
-
header.appendChild(iconEl);
|
|
72
|
-
header.appendChild(title);
|
|
32
|
+
const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
|
|
73
33
|
|
|
74
34
|
// URL field
|
|
75
35
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
@@ -83,6 +43,7 @@ export class LinkDialog {
|
|
|
83
43
|
autocomplete: 'off',
|
|
84
44
|
}));
|
|
85
45
|
this._urlInput = urlInput;
|
|
46
|
+
this._firstInput = urlInput;
|
|
86
47
|
|
|
87
48
|
// Text field
|
|
88
49
|
const textLabel = createElement('label', { class: 'an-label' });
|
|
@@ -108,26 +69,13 @@ export class LinkDialog {
|
|
|
108
69
|
tabLabel.appendChild(tabCheckbox);
|
|
109
70
|
tabLabel.appendChild(document.createTextNode(' ' + L.openInNewTab));
|
|
110
71
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
116
|
-
cancelBtn.textContent = L.cancelBtn;
|
|
117
|
-
btnRow.appendChild(insertBtn);
|
|
118
|
-
btnRow.appendChild(cancelBtn);
|
|
119
|
-
|
|
120
|
-
box.append(header, urlLabel, urlInput, textLabel, textInput, tabLabel, btnRow);
|
|
121
|
-
overlay.appendChild(box);
|
|
122
|
-
makeDraggable(header, box);
|
|
123
|
-
|
|
124
|
-
// Events
|
|
125
|
-
const d1 = on(insertBtn, 'click', () => this._onInsert());
|
|
126
|
-
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
127
|
-
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
72
|
+
const btnRow = this._buildButtonRow(L.insertBtn, L.cancelBtn, () => this._onInsert());
|
|
73
|
+
|
|
74
|
+
box.append(urlLabel, urlInput, textLabel, textInput, tabLabel, btnRow);
|
|
75
|
+
|
|
128
76
|
const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
129
77
|
const d5 = on(textInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
130
|
-
this._disposers.push(
|
|
78
|
+
this._disposers.push(d4, d5);
|
|
131
79
|
|
|
132
80
|
return overlay;
|
|
133
81
|
}
|
|
@@ -194,18 +142,4 @@ export class LinkDialog {
|
|
|
194
142
|
this.context.invoke('editor.insertLink', url, text, newTab);
|
|
195
143
|
this._close();
|
|
196
144
|
}
|
|
197
|
-
|
|
198
|
-
_open() {
|
|
199
|
-
if (this._dialog) {
|
|
200
|
-
this._dialog.style.display = 'flex';
|
|
201
|
-
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
202
|
-
setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
_close() {
|
|
207
|
-
if (this._dialog) this._dialog.style.display = 'none';
|
|
208
|
-
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
209
|
-
this._savedRange = null;
|
|
210
|
-
}
|
|
211
145
|
}
|
|
@@ -7,47 +7,18 @@
|
|
|
7
7
|
* • Direct video URLs → <video> element (.mp4 / .webm / .ogg)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createElement, on
|
|
11
|
-
import {
|
|
10
|
+
import { createElement, on } from '../core/dom.js';
|
|
11
|
+
import { BaseDialog } from './BaseDialog.js';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
/** @param {import('../Context.js').Context} context */
|
|
15
|
-
constructor(context) {
|
|
16
|
-
this.context = context;
|
|
17
|
-
this.options = context.options;
|
|
18
|
-
/** @type {HTMLElement|null} */
|
|
19
|
-
this._dialog = null;
|
|
20
|
-
this._disposers = [];
|
|
21
|
-
this._savedRange = null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Lifecycle
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
initialize() {
|
|
29
|
-
this._dialog = this._buildDialog();
|
|
30
|
-
document.body.appendChild(this._dialog);
|
|
31
|
-
return this;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
destroy() {
|
|
35
|
-
this._disposers.forEach((d) => d());
|
|
36
|
-
this._disposers = [];
|
|
37
|
-
if (this._dialog && this._dialog.parentNode) {
|
|
38
|
-
this._dialog.remove();
|
|
39
|
-
}
|
|
40
|
-
this._dialog = null;
|
|
41
|
-
}
|
|
13
|
+
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"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`;
|
|
42
14
|
|
|
15
|
+
export class VideoDialog extends BaseDialog {
|
|
43
16
|
// ---------------------------------------------------------------------------
|
|
44
17
|
// Public API
|
|
45
18
|
// ---------------------------------------------------------------------------
|
|
46
19
|
|
|
47
20
|
show() {
|
|
48
|
-
|
|
49
|
-
this._savedRange = range;
|
|
50
|
-
});
|
|
21
|
+
this._saveRange();
|
|
51
22
|
this._urlInput.value = '';
|
|
52
23
|
this._widthInput.value = '560';
|
|
53
24
|
this._hintEl.textContent = '';
|
|
@@ -60,21 +31,7 @@ export class VideoDialog {
|
|
|
60
31
|
|
|
61
32
|
_buildDialog() {
|
|
62
33
|
const L = this.context.locale.videoDialog;
|
|
63
|
-
const overlay =
|
|
64
|
-
class: 'an-dialog-overlay',
|
|
65
|
-
role: 'dialog',
|
|
66
|
-
'aria-modal': 'true',
|
|
67
|
-
'aria-label': L.ariaLabel,
|
|
68
|
-
});
|
|
69
|
-
const box = createElement('div', { class: 'an-dialog-box' });
|
|
70
|
-
|
|
71
|
-
const header = createElement('div', { class: 'an-dialog-header' });
|
|
72
|
-
const iconEl = createElement('span', { class: 'an-dialog-icon' });
|
|
73
|
-
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"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`;
|
|
74
|
-
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
75
|
-
title.textContent = L.title;
|
|
76
|
-
header.appendChild(iconEl);
|
|
77
|
-
header.appendChild(title);
|
|
34
|
+
const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
|
|
78
35
|
|
|
79
36
|
// URL input
|
|
80
37
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
@@ -86,6 +43,7 @@ export class VideoDialog {
|
|
|
86
43
|
autocomplete: 'off',
|
|
87
44
|
}));
|
|
88
45
|
this._urlInput = urlInput;
|
|
46
|
+
this._firstInput = urlInput;
|
|
89
47
|
|
|
90
48
|
// Hint (detected source)
|
|
91
49
|
const hintEl = createElement('p', { class: 'an-dialog-hint' });
|
|
@@ -104,30 +62,16 @@ export class VideoDialog {
|
|
|
104
62
|
}));
|
|
105
63
|
this._widthInput = widthInput;
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
110
|
-
insertBtn.textContent = L.insertBtn;
|
|
111
|
-
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
112
|
-
cancelBtn.textContent = L.cancelBtn;
|
|
113
|
-
btnRow.appendChild(insertBtn);
|
|
114
|
-
btnRow.appendChild(cancelBtn);
|
|
115
|
-
|
|
116
|
-
box.append(header, urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
|
|
117
|
-
overlay.appendChild(box);
|
|
118
|
-
makeDraggable(header, box);
|
|
65
|
+
const btnRow = this._buildButtonRow(L.insertBtn, L.cancelBtn, () => this._onInsert());
|
|
66
|
+
box.append(urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
|
|
119
67
|
|
|
120
68
|
// Live URL hint
|
|
121
69
|
const d0 = on(urlInput, 'input', () => {
|
|
122
70
|
const info = this._parseVideoUrl(urlInput.value.trim());
|
|
123
71
|
hintEl.textContent = info ? this.context.locale.videoDialog.detected(info.type) : (urlInput.value ? this.context.locale.videoDialog.unknownFormat : '');
|
|
124
72
|
});
|
|
125
|
-
|
|
126
|
-
const d1 = on(insertBtn, 'click', () => this._onInsert());
|
|
127
|
-
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
128
|
-
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
129
73
|
const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
130
|
-
this._disposers.push(d0,
|
|
74
|
+
this._disposers.push(d0, d4);
|
|
131
75
|
|
|
132
76
|
return overlay;
|
|
133
77
|
}
|
|
@@ -157,20 +101,6 @@ export class VideoDialog {
|
|
|
157
101
|
this._close();
|
|
158
102
|
}
|
|
159
103
|
|
|
160
|
-
_open() {
|
|
161
|
-
if (this._dialog) {
|
|
162
|
-
this._dialog.style.display = 'flex';
|
|
163
|
-
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
164
|
-
setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
_close() {
|
|
169
|
-
if (this._dialog) this._dialog.style.display = 'none';
|
|
170
|
-
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
171
|
-
this._savedRange = null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
104
|
// ---------------------------------------------------------------------------
|
|
175
105
|
// URL parsing & HTML building
|
|
176
106
|
// ---------------------------------------------------------------------------
|