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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autumnnote",
3
- "version": "1.6.1",
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
- 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.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, trapFocus, makeDraggable } from '../core/dom.js';
7
- import { withSavedRange } from '../core/range.js';
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
- // Dialog grid is built lazily on first show() to avoid rendering ~500 DOM nodes at load time.
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
- withSavedRange((range) => {
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
- this.context = context;
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
- this._disposers.forEach((d) => d());
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, trapFocus, makeDraggable } from '../core/dom.js';
6
- import { withSavedRange } from '../core/range.js';
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
- * @param {import('../Context.js').Context} context
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
- // Dialog grid is built lazily on first show() to avoid rendering ~250 icon cells at load time.
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
- withSavedRange((range) => {
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, 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
  }