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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autumnnote",
3
- "version": "1.6.1",
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
- for (const [name, ModuleClass] of _customModules) {
163
- register(name, ModuleClass);
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.5.0',
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, trapFocus, makeDraggable } from '../core/dom.js';
7
- import { withSavedRange } from '../core/range.js';
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
- withSavedRange((range) => {
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 = createElement('div', {
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 tab
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(header, urlLabel, urlInput, altLabel, altInput);
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
- // Buttons
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(d1, d2, d3, d4, d5);
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, trapFocus, makeDraggable } from '../core/dom.js';
7
- import { withSavedRange } from '../core/range.js';
6
+ import { createElement, on } from '../core/dom.js';
7
+ import { BaseDialog } from './BaseDialog.js';
8
8
 
9
- export class LinkDialog {
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
- withSavedRange((range) => {
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 overlay = createElement('div', { class: 'an-dialog-overlay', role: 'dialog', 'aria-modal': 'true', 'aria-label': L.ariaLabel });
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
- // Buttons
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 = L.insertBtn;
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(d1, d2, d3, d4, d5);
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, trapFocus, makeDraggable } from '../core/dom.js';
11
- import { withSavedRange } from '../core/range.js';
10
+ import { createElement, on } from '../core/dom.js';
11
+ import { BaseDialog } from './BaseDialog.js';
12
12
 
13
- export class VideoDialog {
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
- withSavedRange((range) => {
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 = createElement('div', {
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
- // Buttons
108
- const btnRow = createElement('div', { class: 'an-dialog-actions' });
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, d1, d2, d3, d4);
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
  // ---------------------------------------------------------------------------