autumnnote 2.1.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.
Files changed (65) hide show
  1. package/README.md +72 -5
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.es.js +385 -619
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.min.js +20 -20
  6. package/dist/autumnnote.umd.js +20 -20
  7. package/dist/autumnnote.umd.js.map +1 -1
  8. package/dist/icon-data-V0Xqv-wX.js +255 -0
  9. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  10. package/package.json +12 -3
  11. package/types/index.d.ts +8 -0
  12. package/src/js/Context.js +0 -854
  13. package/src/js/core/detectLang.js +0 -98
  14. package/src/js/core/dom.js +0 -372
  15. package/src/js/core/env.js +0 -38
  16. package/src/js/core/key.js +0 -66
  17. package/src/js/core/lists.js +0 -121
  18. package/src/js/core/markdown.js +0 -695
  19. package/src/js/core/range.js +0 -194
  20. package/src/js/core/sanitise.js +0 -304
  21. package/src/js/editing/History.js +0 -266
  22. package/src/js/editing/Style.js +0 -812
  23. package/src/js/editing/Table.js +0 -105
  24. package/src/js/editing/Typing.js +0 -397
  25. package/src/js/index.js +0 -193
  26. package/src/js/index.umd.js +0 -17
  27. package/src/js/module/AutoSaveRestore.js +0 -125
  28. package/src/js/module/BaseDialog.js +0 -133
  29. package/src/js/module/BaseMediaTooltip.js +0 -142
  30. package/src/js/module/BaseResizer.js +0 -322
  31. package/src/js/module/BubbleToolbar.js +0 -483
  32. package/src/js/module/Buttons.js +0 -399
  33. package/src/js/module/Clipboard.js +0 -579
  34. package/src/js/module/CodeTooltip.js +0 -493
  35. package/src/js/module/Codeview.js +0 -125
  36. package/src/js/module/ContextMenu.js +0 -621
  37. package/src/js/module/Editor.js +0 -747
  38. package/src/js/module/EmojiDialog.js +0 -254
  39. package/src/js/module/FindReplace.js +0 -512
  40. package/src/js/module/Fullscreen.js +0 -80
  41. package/src/js/module/IconDialog.js +0 -618
  42. package/src/js/module/ImageCropOverlay.js +0 -586
  43. package/src/js/module/ImageDialog.js +0 -193
  44. package/src/js/module/ImageResizer.js +0 -42
  45. package/src/js/module/ImageTooltip.js +0 -285
  46. package/src/js/module/LinkDialog.js +0 -145
  47. package/src/js/module/LinkTooltip.js +0 -250
  48. package/src/js/module/MarkdownShortcuts.js +0 -250
  49. package/src/js/module/Mention.js +0 -365
  50. package/src/js/module/Placeholder.js +0 -51
  51. package/src/js/module/ShortcutsDialog.js +0 -111
  52. package/src/js/module/SlashMenu.js +0 -376
  53. package/src/js/module/Statusbar.js +0 -246
  54. package/src/js/module/TableTooltip.js +0 -1392
  55. package/src/js/module/Toolbar.js +0 -855
  56. package/src/js/module/VideoDialog.js +0 -193
  57. package/src/js/module/VideoResizer.js +0 -66
  58. package/src/js/module/VideoTooltip.js +0 -248
  59. package/src/js/module/emoji-data.js +0 -496
  60. package/src/js/module/table-grid.js +0 -102
  61. package/src/js/module/table-icons.js +0 -33
  62. package/src/js/renderer.js +0 -120
  63. package/src/js/settings.js +0 -214
  64. package/src/styles/_variables.scss +0 -48
  65. package/src/styles/autumnnote.scss +0 -2877
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
- }