autumnnote 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -5
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +661 -798
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +20 -20
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +13 -4
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -25
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -231
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -312
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1521
- package/src/js/module/Toolbar.js +0 -750
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- package/src/styles/autumnnote.scss +0 -2866
package/src/js/Context.js
DELETED
|
@@ -1,854 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Context.js - Central hub for the editor instance
|
|
3
|
-
* Holds references to all sub-modules and manages inter-module communication.
|
|
4
|
-
* Inspired by Summernote's Context.js
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { mergeDeep } from './core/func.js';
|
|
8
|
-
import { registerButton } from './module/Buttons.js';
|
|
9
|
-
import { defaultOptions } from './settings.js';
|
|
10
|
-
import { resolveLocale } from './i18n/index.js';
|
|
11
|
-
import { renderLayout } from './renderer.js';
|
|
12
|
-
import { on } from './core/dom.js';
|
|
13
|
-
|
|
14
|
-
// Modules
|
|
15
|
-
import { Editor } from './module/Editor.js';
|
|
16
|
-
import { Toolbar } from './module/Toolbar.js';
|
|
17
|
-
import { Statusbar } from './module/Statusbar.js';
|
|
18
|
-
import { Clipboard } from './module/Clipboard.js';
|
|
19
|
-
import { Placeholder } from './module/Placeholder.js';
|
|
20
|
-
import { Codeview } from './module/Codeview.js';
|
|
21
|
-
import { Fullscreen } from './module/Fullscreen.js';
|
|
22
|
-
import { LinkDialog } from './module/LinkDialog.js';
|
|
23
|
-
import { ImageDialog } from './module/ImageDialog.js';
|
|
24
|
-
import { VideoDialog } from './module/VideoDialog.js';
|
|
25
|
-
import { ImageResizer } from './module/ImageResizer.js';
|
|
26
|
-
import { VideoResizer } from './module/VideoResizer.js';
|
|
27
|
-
import { LinkTooltip } from './module/LinkTooltip.js';
|
|
28
|
-
import { ImageTooltip } from './module/ImageTooltip.js';
|
|
29
|
-
import { VideoTooltip } from './module/VideoTooltip.js';
|
|
30
|
-
import { TableTooltip } from './module/TableTooltip.js';
|
|
31
|
-
import { CodeTooltip } from './module/CodeTooltip.js';
|
|
32
|
-
import { EmojiDialog } from './module/EmojiDialog.js';
|
|
33
|
-
import { IconDialog } from './module/IconDialog.js';
|
|
34
|
-
import { ContextMenu } from './module/ContextMenu.js';
|
|
35
|
-
import { ShortcutsDialog } from './module/ShortcutsDialog.js';
|
|
36
|
-
import { FindReplace } from './module/FindReplace.js';
|
|
37
|
-
import { ImageCropOverlay } from './module/ImageCropOverlay.js';
|
|
38
|
-
import { AutoSaveRestore } from './module/AutoSaveRestore.js';
|
|
39
|
-
import { MarkdownShortcuts } from './module/MarkdownShortcuts.js';
|
|
40
|
-
import { BubbleToolbar } from './module/BubbleToolbar.js';
|
|
41
|
-
import { Mention } from './module/Mention.js';
|
|
42
|
-
import { SlashMenu } from './module/SlashMenu.js';
|
|
43
|
-
|
|
44
|
-
/** Module registry shared across all Context instances (populated via AutumnNote.registerModule). */
|
|
45
|
-
export const _customModules = new Map();
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Modules whose registration depends on an option value. Declared once so the
|
|
49
|
-
* initial mount (`_registerModules`) and runtime toggles (`updateOptions`) can
|
|
50
|
-
* never drift apart — previously `updateOptions({ bubbleToolbar: true })`
|
|
51
|
-
* changed the option but silently left the module unregistered.
|
|
52
|
-
* @type {Array<{ name: string, Class: new (ctx: Context) => { initialize: () => any, destroy?: () => void }, enabled: (options: any) => boolean }>}
|
|
53
|
-
*/
|
|
54
|
-
const OPTIONAL_MODULES = [
|
|
55
|
-
{ name: 'autoSaveRestore', Class: AutoSaveRestore, enabled: (o) => !!o.autoSaveRestore },
|
|
56
|
-
{ name: 'markdownShortcuts', Class: MarkdownShortcuts, enabled: (o) => o.markdownShortcuts !== false },
|
|
57
|
-
{ name: 'bubbleToolbar', Class: BubbleToolbar, enabled: (o) => !!o.bubbleToolbar },
|
|
58
|
-
{ name: 'mention', Class: Mention, enabled: (o) => !!o.mention },
|
|
59
|
-
{ name: 'slashMenu', Class: SlashMenu, enabled: (o) => o.slashMenu !== false },
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
/** Global plugin registry (populated via AutumnNote.use()). Applied to every new Context. */
|
|
63
|
-
export const _globalPlugins = new Map();
|
|
64
|
-
|
|
65
|
-
export class Context {
|
|
66
|
-
/**
|
|
67
|
-
* @param {HTMLElement} targetEl - The element to replace with the editor
|
|
68
|
-
* @param {import('./settings.js').AsnOptions} [userOptions]
|
|
69
|
-
*/
|
|
70
|
-
constructor(targetEl, userOptions = {}) {
|
|
71
|
-
this.targetEl = targetEl;
|
|
72
|
-
this.options = mergeDeep(defaultOptions, userOptions);
|
|
73
|
-
|
|
74
|
-
/** @type {import('./i18n/index.js').AsnLocale} */
|
|
75
|
-
this.locale = resolveLocale(this.options.lang);
|
|
76
|
-
|
|
77
|
-
/** @type {{ container: HTMLElement, editable: HTMLElement, toolbar?: HTMLElement, statusbar?: HTMLElement }} */
|
|
78
|
-
this.layoutInfo = /** @type {any} */ ({});
|
|
79
|
-
|
|
80
|
-
/** @type {Map<string, Function[]>} */
|
|
81
|
-
this._listeners = new Map();
|
|
82
|
-
|
|
83
|
-
/** @type {Map<string, object>} */
|
|
84
|
-
this._modules = new Map();
|
|
85
|
-
|
|
86
|
-
/** @type {Map<string, { plugin: object, publicApi: * }>} */
|
|
87
|
-
this._plugins = new Map();
|
|
88
|
-
|
|
89
|
-
this._disposers = [];
|
|
90
|
-
this._alive = false;
|
|
91
|
-
this._autoSaveTimer = null;
|
|
92
|
-
this._pendingAutoSave = null;
|
|
93
|
-
this._suppressedRemoteHTML = null;
|
|
94
|
-
/** @type {Promise<void>|null} Settles when destroy()'s closing auto-save finishes */
|
|
95
|
-
this._destroyPromise = null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Initialisation
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
initialize() {
|
|
103
|
-
// 1. Render the DOM skeleton
|
|
104
|
-
const { container, editable } = renderLayout(this.targetEl, this.options);
|
|
105
|
-
this.layoutInfo.container = container;
|
|
106
|
-
this.layoutInfo.editable = editable;
|
|
107
|
-
|
|
108
|
-
// 2. Register core modules
|
|
109
|
-
this._registerModules();
|
|
110
|
-
|
|
111
|
-
// 3. Attach toolbar/statusbar to container
|
|
112
|
-
const toolbar = this._modules.get('toolbar');
|
|
113
|
-
if (toolbar?.el) {
|
|
114
|
-
container.insertBefore(toolbar.el, editable);
|
|
115
|
-
this.layoutInfo.toolbar = toolbar.el;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const statusbar = this._modules.get('statusbar');
|
|
119
|
-
if (statusbar?.el) {
|
|
120
|
-
container.appendChild(statusbar.el);
|
|
121
|
-
this.layoutInfo.statusbar = statusbar.el;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 4. Bind editor-level events
|
|
125
|
-
this._bindEditorEvents(editable);
|
|
126
|
-
|
|
127
|
-
// 5. Auto-focus if requested
|
|
128
|
-
if (this.options.focus) {
|
|
129
|
-
editable.focus();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
this._alive = true;
|
|
133
|
-
|
|
134
|
-
// Initial toolbar sync so dropdowns show the correct font on load
|
|
135
|
-
this.invoke('toolbar.refresh');
|
|
136
|
-
|
|
137
|
-
// Apply plugins registered globally via AutumnNote.use()
|
|
138
|
-
this._applyGlobalPlugins();
|
|
139
|
-
|
|
140
|
-
if (typeof this.options.onInit === 'function') {
|
|
141
|
-
this.options.onInit(this);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return this;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
_registerModules() {
|
|
148
|
-
const register = (name, ModuleClass) => {
|
|
149
|
-
const instance = new ModuleClass(this);
|
|
150
|
-
this._modules.set(name, instance);
|
|
151
|
-
instance.initialize();
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
register('editor', Editor);
|
|
155
|
-
register('toolbar', Toolbar);
|
|
156
|
-
register('statusbar', Statusbar);
|
|
157
|
-
register('clipboard', Clipboard);
|
|
158
|
-
register('contextMenu', ContextMenu);
|
|
159
|
-
register('placeholder', Placeholder);
|
|
160
|
-
register('codeview', Codeview);
|
|
161
|
-
register('fullscreen', Fullscreen);
|
|
162
|
-
register('linkDialog', LinkDialog);
|
|
163
|
-
register('imageDialog', ImageDialog);
|
|
164
|
-
register('videoDialog', VideoDialog);
|
|
165
|
-
register('imageResizer', ImageResizer);
|
|
166
|
-
register('videoResizer', VideoResizer);
|
|
167
|
-
register('linkTooltip', LinkTooltip);
|
|
168
|
-
register('imageTooltip', ImageTooltip);
|
|
169
|
-
register('videoTooltip', VideoTooltip);
|
|
170
|
-
register('tableTooltip', TableTooltip);
|
|
171
|
-
register('codeTooltip', CodeTooltip);
|
|
172
|
-
register('emojiDialog', EmojiDialog);
|
|
173
|
-
register('iconDialog', IconDialog);
|
|
174
|
-
register('shortcutsDialog', ShortcutsDialog);
|
|
175
|
-
register('findReplace', FindReplace);
|
|
176
|
-
register('imageCropOverlay', ImageCropOverlay);
|
|
177
|
-
for (const { name, Class, enabled } of OPTIONAL_MODULES) {
|
|
178
|
-
if (enabled(this.options)) register(name, Class);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Custom modules registered via AutumnNote.registerModule()
|
|
182
|
-
if (_customModules.size > 0) {
|
|
183
|
-
for (const [name, ModuleClass] of _customModules) {
|
|
184
|
-
register(name, ModuleClass);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Registers or tears down option-gated modules so they match the current
|
|
191
|
-
* option values. Called after `updateOptions()` so toggling e.g.
|
|
192
|
-
* `bubbleToolbar` at runtime actually takes effect.
|
|
193
|
-
*/
|
|
194
|
-
_syncOptionalModules() {
|
|
195
|
-
for (const { name, Class, enabled } of OPTIONAL_MODULES) {
|
|
196
|
-
const shouldRun = enabled(this.options);
|
|
197
|
-
const isRunning = this._modules.has(name);
|
|
198
|
-
if (shouldRun === isRunning) continue;
|
|
199
|
-
|
|
200
|
-
if (shouldRun) {
|
|
201
|
-
const instance = new Class(this);
|
|
202
|
-
this._modules.set(name, instance);
|
|
203
|
-
instance.initialize();
|
|
204
|
-
} else {
|
|
205
|
-
const instance = this._modules.get(name);
|
|
206
|
-
if (typeof instance?.destroy === 'function') instance.destroy();
|
|
207
|
-
this._modules.delete(name);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Registers and initialises a custom module on this instance.
|
|
214
|
-
* @param {string} name
|
|
215
|
-
* @param {new (ctx: this) => any} ModuleClass
|
|
216
|
-
* @returns {this}
|
|
217
|
-
*/
|
|
218
|
-
registerModule(name, ModuleClass) {
|
|
219
|
-
if (this._modules.has(name)) return this;
|
|
220
|
-
const instance = new ModuleClass(this);
|
|
221
|
-
instance.initialize();
|
|
222
|
-
this._modules.set(name, instance);
|
|
223
|
-
return this;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
registerSlashCommand(command) {
|
|
227
|
-
if (!command?.id || typeof command.run !== 'function') {
|
|
228
|
-
throw new TypeError('[AutumnNote] Slash command requires an id and run(context) function.');
|
|
229
|
-
}
|
|
230
|
-
const commands = this.options.slashCommands || (this.options.slashCommands = []);
|
|
231
|
-
const index = commands.findIndex((item) => item.id === command.id);
|
|
232
|
-
if (index >= 0) commands[index] = command;
|
|
233
|
-
else commands.push(command);
|
|
234
|
-
this.invoke('slashMenu.refresh');
|
|
235
|
-
return this;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Installs a plugin on this editor instance.
|
|
240
|
-
* If called after create(), buttons are registered immediately but the toolbar
|
|
241
|
-
* must be rebuilt via ctx.invoke('toolbar.rebuild') to render new buttons.
|
|
242
|
-
* @param {object} plugin - { name, version?, buttons?, install?, uninstall? }
|
|
243
|
-
* @param {object} [options] - Forwarded to plugin.install(context, options)
|
|
244
|
-
* @returns {this}
|
|
245
|
-
*/
|
|
246
|
-
use(plugin, options = {}) {
|
|
247
|
-
if (Array.isArray(plugin.buttons)) {
|
|
248
|
-
plugin.buttons.forEach((b) => registerButton(b));
|
|
249
|
-
}
|
|
250
|
-
this._installPlugin(plugin, options);
|
|
251
|
-
return this;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Returns the public API returned by plugin.install(), or null.
|
|
256
|
-
* @param {string} name
|
|
257
|
-
* @returns {*}
|
|
258
|
-
*/
|
|
259
|
-
getPlugin(name) {
|
|
260
|
-
return this._plugins.get(name)?.publicApi ?? null;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
_installPlugin(plugin, pluginOptions = {}) {
|
|
264
|
-
const { name } = plugin;
|
|
265
|
-
if (!name || typeof name !== 'string') {
|
|
266
|
-
console.warn('[AutumnNote] Plugin must have a string `name` property.');
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
if (this._plugins.has(name)) {
|
|
270
|
-
console.warn(`[AutumnNote] Plugin "${name}" already installed on this instance. Skipping.`);
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
const publicApi = (typeof plugin.install === 'function')
|
|
274
|
-
? plugin.install(this, pluginOptions) ?? null
|
|
275
|
-
: null;
|
|
276
|
-
this._plugins.set(name, { plugin, publicApi });
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
_applyGlobalPlugins() {
|
|
280
|
-
if (_globalPlugins.size === 0) return;
|
|
281
|
-
for (const { plugin, options } of _globalPlugins.values()) {
|
|
282
|
-
this._installPlugin(plugin, options);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
_bindEditorEvents(editable) {
|
|
287
|
-
// Keep the original textarea/input value in sync immediately on every input.
|
|
288
|
-
// This guarantees form.submit() sees fresh data even before debounced change.
|
|
289
|
-
const d0 = on(editable, 'input', () => this._syncToTarget());
|
|
290
|
-
const d1 = on(editable, 'focus', () => {
|
|
291
|
-
this.layoutInfo.container.classList.add('an-focused');
|
|
292
|
-
if (typeof this.options.onFocus === 'function') {
|
|
293
|
-
this.options.onFocus(this);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
const d2 = on(editable, 'blur', () => {
|
|
297
|
-
this.layoutInfo.container.classList.remove('an-focused');
|
|
298
|
-
this._syncToTarget();
|
|
299
|
-
if (typeof this.options.onBlur === 'function') {
|
|
300
|
-
this.options.onBlur(this);
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
// Sync textarea/input value on every change so form.submit() always gets fresh content
|
|
304
|
-
const d3 = this.on('change', (html) => this._syncToTarget(html));
|
|
305
|
-
const dRemote = this.on('change', (html) => {
|
|
306
|
-
if (this.options.blockIds) { this.ensureBlockIds(); html = this.getHTML(); }
|
|
307
|
-
if (html === this._suppressedRemoteHTML) { this._suppressedRemoteHTML = null; return; }
|
|
308
|
-
this.options.collaborationAdapter?.onLocalChange?.(html, this);
|
|
309
|
-
});
|
|
310
|
-
this._disposers.push(d0, d1, d2, d3, dRemote);
|
|
311
|
-
|
|
312
|
-
// Auto-save to localStorage on every change (also writes :asrmeta for restore banner)
|
|
313
|
-
if (this.options.autoSave && this.options.autoSaveKey) {
|
|
314
|
-
const d4 = this.on('change', (html) => this._scheduleAutoSave(html));
|
|
315
|
-
this._disposers.push(d4);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
_scheduleAutoSave(html) {
|
|
320
|
-
this._pendingAutoSave = html;
|
|
321
|
-
clearTimeout(this._autoSaveTimer);
|
|
322
|
-
this._autoSaveTimer = setTimeout(() => this.flushAutoSave(), this.options.autoSaveDelay ?? 400);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async flushAutoSave() {
|
|
326
|
-
clearTimeout(this._autoSaveTimer);
|
|
327
|
-
this._autoSaveTimer = null;
|
|
328
|
-
if (this._pendingAutoSave == null) return;
|
|
329
|
-
const html = this._pendingAutoSave;
|
|
330
|
-
this._pendingAutoSave = null;
|
|
331
|
-
const key = this.options.autoSaveKey;
|
|
332
|
-
const savedAt = Date.now();
|
|
333
|
-
try {
|
|
334
|
-
const adapter = this.options.autoSaveAdapter;
|
|
335
|
-
if (typeof adapter?.save === 'function') {
|
|
336
|
-
await adapter.save({ key, html, savedAt, context: this });
|
|
337
|
-
} else {
|
|
338
|
-
localStorage.setItem(key, html);
|
|
339
|
-
localStorage.setItem(key + ':asrmeta', JSON.stringify({ savedAt }));
|
|
340
|
-
}
|
|
341
|
-
this.triggerEvent('autoSave', { key, html, savedAt });
|
|
342
|
-
} catch (error) {
|
|
343
|
-
this.triggerEvent('autoSaveError', { key, error });
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async loadAutoSave() {
|
|
348
|
-
const adapter = this.options.autoSaveAdapter;
|
|
349
|
-
if (typeof adapter?.load === 'function') {
|
|
350
|
-
return adapter.load({ key: this.options.autoSaveKey, context: this });
|
|
351
|
-
}
|
|
352
|
-
try { return localStorage.getItem(this.options.autoSaveKey); } catch (_) { return null; }
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// ---------------------------------------------------------------------------
|
|
356
|
-
// Module invocation
|
|
357
|
-
// ---------------------------------------------------------------------------
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Invokes a method on a registered module.
|
|
361
|
-
* Format: 'moduleName.methodName'
|
|
362
|
-
* @param {string} path - e.g. 'editor.bold'
|
|
363
|
-
* @param {...*} args
|
|
364
|
-
* @returns {*}
|
|
365
|
-
*/
|
|
366
|
-
invoke(path, ...args) {
|
|
367
|
-
const [moduleName, methodName] = path.split('.');
|
|
368
|
-
const module = this._modules.get(moduleName);
|
|
369
|
-
if (!module) {
|
|
370
|
-
console.warn(`[AutumnNote] invoke: module "${moduleName}" not found (path: "${path}")`);
|
|
371
|
-
return undefined;
|
|
372
|
-
}
|
|
373
|
-
if (typeof module[methodName] !== 'function') {
|
|
374
|
-
console.warn(`[AutumnNote] invoke: method "${methodName}" not found on module "${moduleName}" (path: "${path}")`);
|
|
375
|
-
return undefined;
|
|
376
|
-
}
|
|
377
|
-
return module[methodName](...args);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// ---------------------------------------------------------------------------
|
|
381
|
-
// Event system
|
|
382
|
-
// ---------------------------------------------------------------------------
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Subscribes to an editor event.
|
|
386
|
-
* @param {string} eventName
|
|
387
|
-
* @param {Function} handler
|
|
388
|
-
* @returns {() => void} unsubscribe
|
|
389
|
-
*/
|
|
390
|
-
on(eventName, handler) {
|
|
391
|
-
if (!this._listeners.has(eventName)) {
|
|
392
|
-
this._listeners.set(eventName, []);
|
|
393
|
-
}
|
|
394
|
-
this._listeners.get(eventName).push(handler);
|
|
395
|
-
return () => this.off(eventName, handler);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Unsubscribes from an editor event.
|
|
400
|
-
* @param {string} eventName
|
|
401
|
-
* @param {Function} handler
|
|
402
|
-
*/
|
|
403
|
-
off(eventName, handler) {
|
|
404
|
-
const handlers = this._listeners.get(eventName);
|
|
405
|
-
if (!handlers) return;
|
|
406
|
-
const idx = handlers.indexOf(handler);
|
|
407
|
-
if (idx !== -1) handlers.splice(idx, 1);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Triggers an editor event.
|
|
412
|
-
* @param {string} eventName
|
|
413
|
-
* @param {...*} args
|
|
414
|
-
*/
|
|
415
|
-
triggerEvent(eventName, ...args) {
|
|
416
|
-
const handlers = this._listeners.get(eventName) || [];
|
|
417
|
-
handlers.forEach((h) => h(...args));
|
|
418
|
-
|
|
419
|
-
// Also call options callback if present (e.g. onChange)
|
|
420
|
-
const cbName = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
|
|
421
|
-
if (typeof this.options[cbName] === 'function') {
|
|
422
|
-
this.options[cbName](...args);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// ---------------------------------------------------------------------------
|
|
427
|
-
// Public editor API
|
|
428
|
-
// ---------------------------------------------------------------------------
|
|
429
|
-
|
|
430
|
-
/** Updates runtime-safe options without recreating the editor. */
|
|
431
|
-
updateOptions(overrides = {}) {
|
|
432
|
-
const next = mergeDeep(this.options, overrides);
|
|
433
|
-
Object.keys(this.options).forEach((key) => delete this.options[key]);
|
|
434
|
-
Object.assign(this.options, next);
|
|
435
|
-
|
|
436
|
-
const { container, editable } = this.layoutInfo;
|
|
437
|
-
if (Object.hasOwn(overrides, 'readOnly')) this.setDisabled(Boolean(this.options.readOnly));
|
|
438
|
-
if (Object.hasOwn(overrides, 'spellcheck')) editable.spellcheck = this.options.spellcheck !== false;
|
|
439
|
-
if (Object.hasOwn(overrides, 'placeholder')) editable.dataset.placeholder = this.options.placeholder || '';
|
|
440
|
-
if (Object.hasOwn(overrides, 'direction')) {
|
|
441
|
-
const rtl = this.options.direction === 'rtl';
|
|
442
|
-
editable.setAttribute('dir', rtl ? 'rtl' : 'ltr');
|
|
443
|
-
container.classList.toggle('an-dir-rtl', rtl);
|
|
444
|
-
}
|
|
445
|
-
if (Object.hasOwn(overrides, 'height') || Object.hasOwn(overrides, 'minHeight')) {
|
|
446
|
-
const height = this.options.height || this.options.minHeight || 0;
|
|
447
|
-
editable.style.minHeight = height ? `${height}px` : '';
|
|
448
|
-
}
|
|
449
|
-
if (Object.hasOwn(overrides, 'maxHeight')) {
|
|
450
|
-
editable.style.maxHeight = this.options.maxHeight ? `${this.options.maxHeight}px` : '';
|
|
451
|
-
}
|
|
452
|
-
if (Object.hasOwn(overrides, 'toolbar')) this.invoke('toolbar.rebuild');
|
|
453
|
-
// Start/stop option-gated modules (bubbleToolbar, mention, slashMenu, ...)
|
|
454
|
-
// so toggling them here behaves the same as passing them to create().
|
|
455
|
-
this._syncOptionalModules();
|
|
456
|
-
this.invoke('statusbar.update');
|
|
457
|
-
this.triggerEvent('optionsChange', { ...overrides });
|
|
458
|
-
return this;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Returns the current HTML content of the editor.
|
|
463
|
-
* Zero-width spaces (U+200B) inserted by inline editing helpers are stripped
|
|
464
|
-
* from the output so they don't leak into the consumer's HTML.
|
|
465
|
-
* @returns {string}
|
|
466
|
-
*/
|
|
467
|
-
getHTML() {
|
|
468
|
-
const html = this.invoke('editor.getHTML');
|
|
469
|
-
return typeof html === 'string' ? html.replace(//g, '') : html;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Sets the HTML content of the editor.
|
|
474
|
-
* @param {string} html
|
|
475
|
-
*/
|
|
476
|
-
setHTML(html) {
|
|
477
|
-
this.invoke('editor.setHTML', html);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Returns the plain text content of the editor.
|
|
482
|
-
* @returns {string}
|
|
483
|
-
*/
|
|
484
|
-
getText() {
|
|
485
|
-
return this.invoke('editor.getText');
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Sets the editor content as plain text (HTML-escaped).
|
|
490
|
-
* @param {string} text
|
|
491
|
-
*/
|
|
492
|
-
setText(text) {
|
|
493
|
-
this.invoke('editor.setText', text);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Clears the editor content.
|
|
498
|
-
*/
|
|
499
|
-
clear() {
|
|
500
|
-
this.invoke('editor.clear');
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Resets the undo/redo history stack.
|
|
505
|
-
* Useful after programmatically loading a new document via setHTML() / setMarkdown()
|
|
506
|
-
* so that Ctrl+Z cannot undo back to the previous document.
|
|
507
|
-
*/
|
|
508
|
-
clearHistory() {
|
|
509
|
-
this.invoke('editor.clearHistory');
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Returns the number of available undo steps.
|
|
514
|
-
* @returns {number}
|
|
515
|
-
*/
|
|
516
|
-
getUndoCount() {
|
|
517
|
-
return this.invoke('editor.getUndoCount') ?? 0;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Returns the number of available redo steps.
|
|
522
|
-
* @returns {number}
|
|
523
|
-
*/
|
|
524
|
-
getRedoCount() {
|
|
525
|
-
return this.invoke('editor.getRedoCount') ?? 0;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Returns true when the editor has no meaningful content.
|
|
530
|
-
* @returns {boolean}
|
|
531
|
-
*/
|
|
532
|
-
isEmpty() {
|
|
533
|
-
return this.invoke('editor.isEmpty');
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Inserts HTML at the current cursor position.
|
|
538
|
-
* @param {string} html
|
|
539
|
-
*/
|
|
540
|
-
insertHTML(html) {
|
|
541
|
-
this.invoke('editor.insertHTML', html);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Inserts plain text at the current cursor position.
|
|
546
|
-
* @param {string} text
|
|
547
|
-
*/
|
|
548
|
-
insertText(text) {
|
|
549
|
-
this.invoke('editor.insertText', text);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* Sets editor content from a Markdown string.
|
|
554
|
-
* @param {string} md
|
|
555
|
-
*/
|
|
556
|
-
setMarkdown(md) {
|
|
557
|
-
this.invoke('editor.setMarkdown', md);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Returns the editor content as Markdown.
|
|
562
|
-
* @returns {string}
|
|
563
|
-
*/
|
|
564
|
-
getMarkdown() {
|
|
565
|
-
return this.invoke('editor.getMarkdown');
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
getSelectionBookmark() {
|
|
569
|
-
return this.invoke('editor.getSelectionBookmark') ?? null;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
restoreSelectionBookmark(bookmark) {
|
|
573
|
-
return this.invoke('editor.restoreSelectionBookmark', bookmark);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
async importDocument(format, data) {
|
|
577
|
-
const adapter = this.options.documentAdapters?.[format];
|
|
578
|
-
let html;
|
|
579
|
-
if (typeof adapter?.import === 'function') html = await adapter.import(data, this);
|
|
580
|
-
else if (format === 'html') html = String(data ?? '');
|
|
581
|
-
else if (format === 'markdown') { this.setMarkdown(String(data ?? '')); return this; }
|
|
582
|
-
else if (format === 'text') { this.setText(String(data ?? '')); return this; }
|
|
583
|
-
else throw new Error(`[AutumnNote] No importer registered for "${format}".`);
|
|
584
|
-
this.setHTML(html);
|
|
585
|
-
return this;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
async exportDocument(format) {
|
|
589
|
-
const adapter = this.options.documentAdapters?.[format];
|
|
590
|
-
if (typeof adapter?.export === 'function') return adapter.export(this, this.getHTML());
|
|
591
|
-
if (format === 'html') return this.getHTML();
|
|
592
|
-
if (format === 'markdown') return this.getMarkdown();
|
|
593
|
-
if (format === 'text') return this.getText();
|
|
594
|
-
throw new Error(`[AutumnNote] No exporter registered for "${format}".`);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
ensureBlockIds() {
|
|
598
|
-
const blocks = this.layoutInfo.editable.children;
|
|
599
|
-
for (const block of blocks) {
|
|
600
|
-
if (!block.hasAttribute('data-an-block-id')) {
|
|
601
|
-
const id = globalThis.crypto?.randomUUID?.() || `an-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
602
|
-
block.setAttribute('data-an-block-id', id);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
return this;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
getDocument() {
|
|
609
|
-
if (this.options.blockIds) this.ensureBlockIds();
|
|
610
|
-
return { version: 1, html: this.getHTML(), markdown: this.getMarkdown() };
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
loadDocument(documentData) {
|
|
614
|
-
this.setHTML(documentData?.html || '');
|
|
615
|
-
this.clearHistory();
|
|
616
|
-
return this;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
applyRemoteHTML(html) {
|
|
620
|
-
this.setHTML(html);
|
|
621
|
-
this._suppressedRemoteHTML = this.getHTML();
|
|
622
|
-
this.clearHistory();
|
|
623
|
-
return this;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Returns the current word count of the editor content.
|
|
628
|
-
* @returns {number}
|
|
629
|
-
*/
|
|
630
|
-
getWordCount() {
|
|
631
|
-
return this.invoke('statusbar.getWordCount') ?? 0;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Returns the current character count of the editor content.
|
|
636
|
-
* @returns {number}
|
|
637
|
-
*/
|
|
638
|
-
getCharCount() {
|
|
639
|
-
return this.invoke('statusbar.getCharCount') ?? 0;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Downloads the editor content as an HTML file.
|
|
644
|
-
* @param {string} [filename='document.html']
|
|
645
|
-
*/
|
|
646
|
-
downloadHTML(filename = 'document.html') {
|
|
647
|
-
this._download(this.getHTML(), filename, 'text/html');
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Downloads the editor content as a plain-text file.
|
|
652
|
-
* @param {string} [filename='document.txt']
|
|
653
|
-
*/
|
|
654
|
-
downloadText(filename = 'document.txt') {
|
|
655
|
-
this._download(this.getText(), filename, 'text/plain');
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Downloads the editor content as a Markdown file.
|
|
660
|
-
* @param {string} [filename='document.md']
|
|
661
|
-
*/
|
|
662
|
-
downloadMarkdown(filename = 'document.md') {
|
|
663
|
-
this._download(this.getMarkdown(), filename, 'text/markdown');
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* Creates a temporary Blob URL and triggers a browser file download.
|
|
668
|
-
* @param {string} content
|
|
669
|
-
* @param {string} filename
|
|
670
|
-
* @param {string} mimeType
|
|
671
|
-
*/
|
|
672
|
-
_download(content, filename, mimeType) {
|
|
673
|
-
const blob = new Blob([content], { type: mimeType });
|
|
674
|
-
const url = URL.createObjectURL(blob);
|
|
675
|
-
const a = document.createElement('a');
|
|
676
|
-
a.href = url;
|
|
677
|
-
a.download = filename;
|
|
678
|
-
a.style.display = 'none';
|
|
679
|
-
document.body.appendChild(a);
|
|
680
|
-
a.click();
|
|
681
|
-
a.remove();
|
|
682
|
-
URL.revokeObjectURL(url);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Opens the editor content in a new globalThis and triggers the browser print dialog.
|
|
687
|
-
* @param {string} [title='']
|
|
688
|
-
*/
|
|
689
|
-
print(title = '') {
|
|
690
|
-
const content = this.getHTML();
|
|
691
|
-
const safeTitle = (title || '').replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
692
|
-
const markup = '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">' +
|
|
693
|
-
`<title>${safeTitle}</title>` +
|
|
694
|
-
'<style>' +
|
|
695
|
-
'body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif;font-size:14px;line-height:1.6;padding:20mm;color:#111827;}' +
|
|
696
|
-
'ul.an-checklist{list-style:none;padding-left:0;}' +
|
|
697
|
-
'ul.an-checklist li{padding-left:24px;position:relative;margin:2px 0;}' +
|
|
698
|
-
'ul.an-checklist li input[type="checkbox"]{position:absolute;left:0;top:3px;}' +
|
|
699
|
-
'code{background:#f3f4f6;border-radius:3px;padding:.1em .35em;font-family:monospace;}' +
|
|
700
|
-
'pre{background:#f3f4f6;padding:.75em 1em;border-radius:4px;overflow-x:auto;}' +
|
|
701
|
-
'table{border-collapse:collapse;}td,th{border:1px solid #d1d5db;padding:4px 8px;}' +
|
|
702
|
-
'</style>' +
|
|
703
|
-
`</head><body>${content}</body></html>`;
|
|
704
|
-
const blob = new Blob([markup], { type: 'text/html' });
|
|
705
|
-
const url = URL.createObjectURL(blob);
|
|
706
|
-
const w = globalThis.open(url, '_blank');
|
|
707
|
-
if (!w) { URL.revokeObjectURL(url); return; } // popup blocked by browser
|
|
708
|
-
w.addEventListener('load', () => {
|
|
709
|
-
w.print();
|
|
710
|
-
URL.revokeObjectURL(url);
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* Returns an array of heading objects representing the table of contents.
|
|
716
|
-
* Each entry has: level (1-6), text (heading text), element (DOM element).
|
|
717
|
-
* @returns {{ level: number, text: string, element: HTMLElement }[]}
|
|
718
|
-
*/
|
|
719
|
-
getTableOfContents() {
|
|
720
|
-
const headings = Array.from(
|
|
721
|
-
this.layoutInfo.editable.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
|
722
|
-
);
|
|
723
|
-
return headings.map((el) => ({
|
|
724
|
-
level: parseInt(el.tagName[1], 10),
|
|
725
|
-
text: el.textContent?.trim() ?? '',
|
|
726
|
-
element: /** @type {HTMLElement} */ (el),
|
|
727
|
-
}));
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Moves focus into the editable area.
|
|
732
|
-
*/
|
|
733
|
-
focus() {
|
|
734
|
-
this.layoutInfo.editable.focus();
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* Removes focus from the editable area.
|
|
739
|
-
*/
|
|
740
|
-
blur() {
|
|
741
|
-
this.layoutInfo.editable.blur();
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
* Returns true when the editor is currently in fullscreen mode.
|
|
746
|
-
* @returns {boolean}
|
|
747
|
-
*/
|
|
748
|
-
isFullscreen() {
|
|
749
|
-
return this.invoke('fullscreen.isActive') === true;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* Sets whether the editor is disabled (readonly).
|
|
754
|
-
* @param {boolean} disabled
|
|
755
|
-
*/
|
|
756
|
-
setDisabled(disabled) {
|
|
757
|
-
const editable = this.layoutInfo.editable;
|
|
758
|
-
if (disabled) {
|
|
759
|
-
editable.setAttribute('contenteditable', 'false');
|
|
760
|
-
this.layoutInfo.container.classList.add('an-disabled');
|
|
761
|
-
editable.querySelectorAll('ul.an-checklist input[type="checkbox"]').forEach((cb) => {
|
|
762
|
-
cb.setAttribute('disabled', '');
|
|
763
|
-
});
|
|
764
|
-
} else {
|
|
765
|
-
editable.setAttribute('contenteditable', 'true');
|
|
766
|
-
this.layoutInfo.container.classList.remove('an-disabled');
|
|
767
|
-
editable.querySelectorAll('ul.an-checklist input[type="checkbox"]').forEach((cb) => {
|
|
768
|
-
cb.removeAttribute('disabled');
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// ---------------------------------------------------------------------------
|
|
774
|
-
// Destroy
|
|
775
|
-
// ---------------------------------------------------------------------------
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* Completely removes the editor and restores the original element.
|
|
779
|
-
*
|
|
780
|
-
* Teardown itself is synchronous; the returned promise only settles once the
|
|
781
|
-
* closing auto-save has finished, so `await editor.destroy()` is worth doing
|
|
782
|
-
* when using an async `autoSaveAdapter`. Ignoring the return value is safe.
|
|
783
|
-
* @returns {Promise<void>}
|
|
784
|
-
*/
|
|
785
|
-
destroy() {
|
|
786
|
-
if (!this._alive) return this._destroyPromise ?? Promise.resolve();
|
|
787
|
-
|
|
788
|
-
// Start the final auto-save before tearing anything down. `_listeners` is
|
|
789
|
-
// deliberately kept alive until this settles, otherwise the closing
|
|
790
|
-
// autoSave/autoSaveError event fires into an already-cleared listener map
|
|
791
|
-
// and an async adapter's last write completes silently.
|
|
792
|
-
const pendingFlush = this._pendingAutoSave != null
|
|
793
|
-
? this.flushAutoSave().catch(() => {})
|
|
794
|
-
: null;
|
|
795
|
-
|
|
796
|
-
this._modules.forEach((module) => {
|
|
797
|
-
if (typeof module.destroy === 'function') module.destroy();
|
|
798
|
-
});
|
|
799
|
-
this._modules.clear();
|
|
800
|
-
|
|
801
|
-
for (const { plugin } of this._plugins.values()) {
|
|
802
|
-
if (typeof plugin.uninstall === 'function') {
|
|
803
|
-
try { plugin.uninstall(this); } catch (_) { void _; }
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
this._plugins.clear();
|
|
807
|
-
|
|
808
|
-
this._disposers.forEach((d) => d());
|
|
809
|
-
this._disposers = [];
|
|
810
|
-
|
|
811
|
-
const container = this.layoutInfo.container;
|
|
812
|
-
const wasDark = container?.classList.contains('an-theme-dark');
|
|
813
|
-
const wasAuto = container?.classList.contains('an-theme-auto');
|
|
814
|
-
if (container?.parentNode) {
|
|
815
|
-
// Restore original element
|
|
816
|
-
this.targetEl.style.display = '';
|
|
817
|
-
container.remove();
|
|
818
|
-
}
|
|
819
|
-
// Clean up body theme classes if no other editors of that type remain
|
|
820
|
-
if (wasDark && !document.querySelector('.an-container.an-theme-dark')) {
|
|
821
|
-
document.body.classList.remove('an-theme-dark');
|
|
822
|
-
}
|
|
823
|
-
if (wasAuto && !document.querySelector('.an-container.an-theme-auto')) {
|
|
824
|
-
document.body.classList.remove('an-theme-auto');
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
if (typeof this.options.onDestroy === 'function') {
|
|
828
|
-
this.options.onDestroy(this);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
this._alive = false;
|
|
832
|
-
|
|
833
|
-
// Resolves once the closing auto-save (if any) has finished. Callers using
|
|
834
|
-
// an async autoSaveAdapter can `await editor.destroy()` to be sure the last
|
|
835
|
-
// write landed before unloading.
|
|
836
|
-
this._destroyPromise = Promise.resolve(pendingFlush).then(() => {
|
|
837
|
-
this._listeners.clear();
|
|
838
|
-
});
|
|
839
|
-
return this._destroyPromise;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// ---------------------------------------------------------------------------
|
|
843
|
-
// Helpers
|
|
844
|
-
// ---------------------------------------------------------------------------
|
|
845
|
-
|
|
846
|
-
/**
|
|
847
|
-
* Syncs editor HTML back into the original textarea/input for form submission.
|
|
848
|
-
*/
|
|
849
|
-
_syncToTarget(html) {
|
|
850
|
-
if (this.targetEl.tagName === 'TEXTAREA' || this.targetEl.tagName === 'INPUT') {
|
|
851
|
-
/** @type {HTMLInputElement} */ (this.targetEl).value = typeof html === 'string' ? html : this.getHTML();
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|