autumnnote 1.0.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/LICENSE +21 -0
- package/README.md +874 -0
- package/dist/autumnnote.css +1 -0
- package/dist/autumnnote.es.js +5888 -0
- package/dist/autumnnote.es.js.map +1 -0
- package/dist/autumnnote.umd.js +74 -0
- package/dist/autumnnote.umd.js.map +1 -0
- package/package.json +55 -0
- package/src/js/Context.js +497 -0
- package/src/js/core/dom.js +315 -0
- package/src/js/core/env.js +25 -0
- package/src/js/core/func.js +153 -0
- package/src/js/core/key.js +66 -0
- package/src/js/core/lists.js +121 -0
- package/src/js/core/markdown.js +294 -0
- package/src/js/core/range.js +194 -0
- package/src/js/core/sanitise.js +78 -0
- package/src/js/editing/History.js +205 -0
- package/src/js/editing/Style.js +329 -0
- package/src/js/editing/Table.js +59 -0
- package/src/js/editing/Typing.js +142 -0
- package/src/js/index.js +126 -0
- package/src/js/module/Buttons.js +300 -0
- package/src/js/module/Clipboard.js +460 -0
- package/src/js/module/CodeTooltip.js +428 -0
- package/src/js/module/Codeview.js +122 -0
- package/src/js/module/ContextMenu.js +470 -0
- package/src/js/module/Editor.js +528 -0
- package/src/js/module/EmojiDialog.js +726 -0
- package/src/js/module/FindReplace.js +440 -0
- package/src/js/module/Fullscreen.js +80 -0
- package/src/js/module/IconDialog.js +620 -0
- package/src/js/module/ImageDialog.js +208 -0
- package/src/js/module/ImageResizer.js +216 -0
- package/src/js/module/ImageTooltip.js +286 -0
- package/src/js/module/LinkDialog.js +204 -0
- package/src/js/module/LinkTooltip.js +242 -0
- package/src/js/module/Placeholder.js +44 -0
- package/src/js/module/ShortcutsDialog.js +141 -0
- package/src/js/module/Statusbar.js +238 -0
- package/src/js/module/TableTooltip.js +568 -0
- package/src/js/module/Toolbar.js +562 -0
- package/src/js/module/VideoDialog.js +263 -0
- package/src/js/module/VideoResizer.js +227 -0
- package/src/js/module/VideoTooltip.js +252 -0
- package/src/js/renderer.js +107 -0
- package/src/js/settings.js +134 -0
- package/src/styles/_variables.scss +48 -0
- package/src/styles/autumnnote.scss +1740 -0
- package/types/index.d.ts +324 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "autumnnote",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A modern, lightweight WYSIWYG editor — built with vanilla JavaScript, no jQuery required.",
|
|
5
|
+
"main": "dist/autumnnote.umd.js",
|
|
6
|
+
"module": "dist/autumnnote.es.js",
|
|
7
|
+
"types": "types/index.d.ts",
|
|
8
|
+
"style": "dist/autumnnote.css",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src",
|
|
12
|
+
"types"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "vite",
|
|
16
|
+
"build": "vite build",
|
|
17
|
+
"build:demo": "vite build --config vite.demo.config.js",
|
|
18
|
+
"preview": "vite preview",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"lint": "eslint src --ext .js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"wysiwyg",
|
|
26
|
+
"editor",
|
|
27
|
+
"rich-text",
|
|
28
|
+
"rich-text-editor",
|
|
29
|
+
"vanilla-js",
|
|
30
|
+
"vanilla-javascript",
|
|
31
|
+
"contenteditable",
|
|
32
|
+
"summernote",
|
|
33
|
+
"text-editor",
|
|
34
|
+
"html-editor",
|
|
35
|
+
"no-jquery",
|
|
36
|
+
"lightweight",
|
|
37
|
+
"toolbar",
|
|
38
|
+
"javascript-editor"
|
|
39
|
+
],
|
|
40
|
+
"author": "Minh Pham",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@vitest/browser": "^1.0.0",
|
|
44
|
+
"eslint": "^8.57.0",
|
|
45
|
+
"jsdom": "^29.0.1",
|
|
46
|
+
"sass": "^1.77.0",
|
|
47
|
+
"vite": "^5.2.0",
|
|
48
|
+
"vitest": "^1.6.0"
|
|
49
|
+
},
|
|
50
|
+
"browserslist": [
|
|
51
|
+
"last 2 versions",
|
|
52
|
+
"not dead",
|
|
53
|
+
"> 0.5%"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
@@ -0,0 +1,497 @@
|
|
|
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 { defaultOptions } from './settings.js';
|
|
9
|
+
import { renderLayout } from './renderer.js';
|
|
10
|
+
import { on } from './core/dom.js';
|
|
11
|
+
|
|
12
|
+
// Modules
|
|
13
|
+
import { Editor } from './module/Editor.js';
|
|
14
|
+
import { Toolbar } from './module/Toolbar.js';
|
|
15
|
+
import { Statusbar } from './module/Statusbar.js';
|
|
16
|
+
import { Clipboard } from './module/Clipboard.js';
|
|
17
|
+
import { Placeholder } from './module/Placeholder.js';
|
|
18
|
+
import { Codeview } from './module/Codeview.js';
|
|
19
|
+
import { Fullscreen } from './module/Fullscreen.js';
|
|
20
|
+
import { LinkDialog } from './module/LinkDialog.js';
|
|
21
|
+
import { ImageDialog } from './module/ImageDialog.js';
|
|
22
|
+
import { VideoDialog } from './module/VideoDialog.js';
|
|
23
|
+
import { ImageResizer } from './module/ImageResizer.js';
|
|
24
|
+
import { VideoResizer } from './module/VideoResizer.js';
|
|
25
|
+
import { LinkTooltip } from './module/LinkTooltip.js';
|
|
26
|
+
import { ImageTooltip } from './module/ImageTooltip.js';
|
|
27
|
+
import { VideoTooltip } from './module/VideoTooltip.js';
|
|
28
|
+
import { TableTooltip } from './module/TableTooltip.js';
|
|
29
|
+
import { CodeTooltip } from './module/CodeTooltip.js';
|
|
30
|
+
import { EmojiDialog } from './module/EmojiDialog.js';
|
|
31
|
+
import { IconDialog } from './module/IconDialog.js';
|
|
32
|
+
import { ContextMenu } from './module/ContextMenu.js';
|
|
33
|
+
import { ShortcutsDialog } from './module/ShortcutsDialog.js';
|
|
34
|
+
import { FindReplace } from './module/FindReplace.js';
|
|
35
|
+
|
|
36
|
+
/** Module registry shared across all Context instances (populated via AutumnNote.registerModule). */
|
|
37
|
+
export const _customModules = new Map();
|
|
38
|
+
|
|
39
|
+
export class Context {
|
|
40
|
+
/**
|
|
41
|
+
* @param {HTMLElement} targetEl - The element to replace with the editor
|
|
42
|
+
* @param {import('./settings.js').AsnOptions} [userOptions]
|
|
43
|
+
*/
|
|
44
|
+
constructor(targetEl, userOptions = {}) {
|
|
45
|
+
this.targetEl = targetEl;
|
|
46
|
+
this.options = mergeDeep(defaultOptions, userOptions);
|
|
47
|
+
|
|
48
|
+
/** @type {{ container: HTMLElement, editable: HTMLElement, toolbar?: HTMLElement, statusbar?: HTMLElement }} */
|
|
49
|
+
this.layoutInfo = {};
|
|
50
|
+
|
|
51
|
+
/** @type {Map<string, Function[]>} */
|
|
52
|
+
this._listeners = new Map();
|
|
53
|
+
|
|
54
|
+
/** @type {Map<string, object>} */
|
|
55
|
+
this._modules = new Map();
|
|
56
|
+
|
|
57
|
+
this._disposers = [];
|
|
58
|
+
this._alive = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Initialisation
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
initialize() {
|
|
66
|
+
// 1. Render the DOM skeleton
|
|
67
|
+
const { container, editable } = renderLayout(this.targetEl, this.options);
|
|
68
|
+
this.layoutInfo.container = container;
|
|
69
|
+
this.layoutInfo.editable = editable;
|
|
70
|
+
|
|
71
|
+
// 2. Register core modules
|
|
72
|
+
this._registerModules();
|
|
73
|
+
|
|
74
|
+
// 3. Attach toolbar/statusbar to container
|
|
75
|
+
const toolbar = this._modules.get('toolbar');
|
|
76
|
+
if (toolbar && toolbar.el) {
|
|
77
|
+
container.insertBefore(toolbar.el, editable);
|
|
78
|
+
this.layoutInfo.toolbar = toolbar.el;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const statusbar = this._modules.get('statusbar');
|
|
82
|
+
if (statusbar && statusbar.el) {
|
|
83
|
+
container.appendChild(statusbar.el);
|
|
84
|
+
this.layoutInfo.statusbar = statusbar.el;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 4. Bind editor-level events
|
|
88
|
+
this._bindEditorEvents(editable);
|
|
89
|
+
|
|
90
|
+
// 5. Auto-focus if requested
|
|
91
|
+
if (this.options.focus) {
|
|
92
|
+
editable.focus();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this._alive = true;
|
|
96
|
+
|
|
97
|
+
// Initial toolbar sync so dropdowns show the correct font on load
|
|
98
|
+
this.invoke('toolbar.refresh');
|
|
99
|
+
|
|
100
|
+
if (typeof this.options.onInit === 'function') {
|
|
101
|
+
this.options.onInit(this);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_registerModules() {
|
|
108
|
+
const register = (name, ModuleClass) => {
|
|
109
|
+
const instance = new ModuleClass(this);
|
|
110
|
+
instance.initialize();
|
|
111
|
+
this._modules.set(name, instance);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
register('editor', Editor);
|
|
115
|
+
register('toolbar', Toolbar);
|
|
116
|
+
register('statusbar', Statusbar);
|
|
117
|
+
register('clipboard', Clipboard);
|
|
118
|
+
register('contextMenu', ContextMenu);
|
|
119
|
+
register('placeholder', Placeholder);
|
|
120
|
+
register('codeview', Codeview);
|
|
121
|
+
register('fullscreen', Fullscreen);
|
|
122
|
+
register('linkDialog', LinkDialog);
|
|
123
|
+
register('imageDialog', ImageDialog);
|
|
124
|
+
register('videoDialog', VideoDialog);
|
|
125
|
+
register('imageResizer', ImageResizer);
|
|
126
|
+
register('videoResizer', VideoResizer);
|
|
127
|
+
register('linkTooltip', LinkTooltip);
|
|
128
|
+
register('imageTooltip', ImageTooltip);
|
|
129
|
+
register('videoTooltip', VideoTooltip);
|
|
130
|
+
register('tableTooltip', TableTooltip);
|
|
131
|
+
register('codeTooltip', CodeTooltip);
|
|
132
|
+
register('emojiDialog', EmojiDialog);
|
|
133
|
+
register('iconDialog', IconDialog);
|
|
134
|
+
register('shortcutsDialog', ShortcutsDialog);
|
|
135
|
+
register('findReplace', FindReplace);
|
|
136
|
+
|
|
137
|
+
// Custom modules registered via AutumnNote.registerModule()
|
|
138
|
+
for (const [name, ModuleClass] of _customModules) {
|
|
139
|
+
register(name, ModuleClass);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Registers and initialises a custom module on this instance.
|
|
145
|
+
* @param {string} name
|
|
146
|
+
* @param {Function} ModuleClass
|
|
147
|
+
* @returns {this}
|
|
148
|
+
*/
|
|
149
|
+
registerModule(name, ModuleClass) {
|
|
150
|
+
if (this._modules.has(name)) return this;
|
|
151
|
+
const instance = new ModuleClass(this);
|
|
152
|
+
instance.initialize();
|
|
153
|
+
this._modules.set(name, instance);
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_bindEditorEvents(editable) {
|
|
158
|
+
const d1 = on(editable, 'focus', () => {
|
|
159
|
+
this.layoutInfo.container.classList.add('an-focused');
|
|
160
|
+
if (typeof this.options.onFocus === 'function') {
|
|
161
|
+
this.options.onFocus(this);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
const d2 = on(editable, 'blur', () => {
|
|
165
|
+
this.layoutInfo.container.classList.remove('an-focused');
|
|
166
|
+
this._syncToTarget();
|
|
167
|
+
if (typeof this.options.onBlur === 'function') {
|
|
168
|
+
this.options.onBlur(this);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// Sync textarea/input value on every change so form.submit() always gets fresh content
|
|
172
|
+
const d3 = this.on('change', () => this._syncToTarget());
|
|
173
|
+
this._disposers.push(d1, d2, d3);
|
|
174
|
+
|
|
175
|
+
// Auto-save to localStorage on every change
|
|
176
|
+
if (this.options.autoSave && this.options.autoSaveKey) {
|
|
177
|
+
const d4 = this.on('change', () => {
|
|
178
|
+
try { localStorage.setItem(this.options.autoSaveKey, this.getHTML()); } catch (_) {}
|
|
179
|
+
});
|
|
180
|
+
this._disposers.push(d4);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Module invocation
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Invokes a method on a registered module.
|
|
190
|
+
* Format: 'moduleName.methodName'
|
|
191
|
+
* @param {string} path - e.g. 'editor.bold'
|
|
192
|
+
* @param {...*} args
|
|
193
|
+
* @returns {*}
|
|
194
|
+
*/
|
|
195
|
+
invoke(path, ...args) {
|
|
196
|
+
const [moduleName, methodName] = path.split('.');
|
|
197
|
+
const module = this._modules.get(moduleName);
|
|
198
|
+
if (!module) {
|
|
199
|
+
if (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') {
|
|
200
|
+
console.warn(`[AutumnNote] invoke: module "${moduleName}" not found (path: "${path}")`);
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
if (typeof module[methodName] !== 'function') {
|
|
205
|
+
if (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') {
|
|
206
|
+
console.warn(`[AutumnNote] invoke: method "${methodName}" not found on module "${moduleName}" (path: "${path}")`);
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return module[methodName](...args);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Event system
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Subscribes to an editor event.
|
|
219
|
+
* @param {string} eventName
|
|
220
|
+
* @param {Function} handler
|
|
221
|
+
* @returns {() => void} unsubscribe
|
|
222
|
+
*/
|
|
223
|
+
on(eventName, handler) {
|
|
224
|
+
if (!this._listeners.has(eventName)) {
|
|
225
|
+
this._listeners.set(eventName, []);
|
|
226
|
+
}
|
|
227
|
+
this._listeners.get(eventName).push(handler);
|
|
228
|
+
return () => this.off(eventName, handler);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Unsubscribes from an editor event.
|
|
233
|
+
* @param {string} eventName
|
|
234
|
+
* @param {Function} handler
|
|
235
|
+
*/
|
|
236
|
+
off(eventName, handler) {
|
|
237
|
+
const handlers = this._listeners.get(eventName);
|
|
238
|
+
if (!handlers) return;
|
|
239
|
+
const idx = handlers.indexOf(handler);
|
|
240
|
+
if (idx !== -1) handlers.splice(idx, 1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Triggers an editor event.
|
|
245
|
+
* @param {string} eventName
|
|
246
|
+
* @param {...*} args
|
|
247
|
+
*/
|
|
248
|
+
triggerEvent(eventName, ...args) {
|
|
249
|
+
const handlers = this._listeners.get(eventName) || [];
|
|
250
|
+
handlers.forEach((h) => h(...args));
|
|
251
|
+
|
|
252
|
+
// Also call options callback if present (e.g. onChange)
|
|
253
|
+
const cbName = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
|
|
254
|
+
if (typeof this.options[cbName] === 'function') {
|
|
255
|
+
this.options[cbName](...args);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Public editor API
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Returns the current HTML content of the editor.
|
|
265
|
+
* @returns {string}
|
|
266
|
+
*/
|
|
267
|
+
getHTML() {
|
|
268
|
+
return this.invoke('editor.getHTML');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Sets the HTML content of the editor.
|
|
273
|
+
* @param {string} html
|
|
274
|
+
*/
|
|
275
|
+
setHTML(html) {
|
|
276
|
+
this.invoke('editor.setHTML', html);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Returns the plain text content of the editor.
|
|
281
|
+
* @returns {string}
|
|
282
|
+
*/
|
|
283
|
+
getText() {
|
|
284
|
+
return this.invoke('editor.getText');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Sets the editor content as plain text (HTML-escaped).
|
|
289
|
+
* @param {string} text
|
|
290
|
+
*/
|
|
291
|
+
setText(text) {
|
|
292
|
+
this.invoke('editor.setText', text);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Clears the editor content.
|
|
297
|
+
*/
|
|
298
|
+
clear() {
|
|
299
|
+
this.invoke('editor.clear');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Resets the undo/redo history stack.
|
|
304
|
+
* Useful after programmatically loading a new document via setHTML() / setMarkdown()
|
|
305
|
+
* so that Ctrl+Z cannot undo back to the previous document.
|
|
306
|
+
*/
|
|
307
|
+
clearHistory() {
|
|
308
|
+
this.invoke('editor.clearHistory');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Returns true when the editor has no meaningful content.
|
|
313
|
+
* @returns {boolean}
|
|
314
|
+
*/
|
|
315
|
+
isEmpty() {
|
|
316
|
+
return this.invoke('editor.isEmpty');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Inserts HTML at the current cursor position.
|
|
321
|
+
* @param {string} html
|
|
322
|
+
*/
|
|
323
|
+
insertHTML(html) {
|
|
324
|
+
this.invoke('editor.insertHTML', html);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Inserts plain text at the current cursor position.
|
|
329
|
+
* @param {string} text
|
|
330
|
+
*/
|
|
331
|
+
insertText(text) {
|
|
332
|
+
this.invoke('editor.insertText', text);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Sets editor content from a Markdown string.
|
|
337
|
+
* @param {string} md
|
|
338
|
+
*/
|
|
339
|
+
setMarkdown(md) {
|
|
340
|
+
this.invoke('editor.setMarkdown', md);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Returns the editor content as Markdown.
|
|
345
|
+
* @returns {string}
|
|
346
|
+
*/
|
|
347
|
+
getMarkdown() {
|
|
348
|
+
return this.invoke('editor.getMarkdown');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Returns the current word count of the editor content.
|
|
353
|
+
* @returns {number}
|
|
354
|
+
*/
|
|
355
|
+
getWordCount() {
|
|
356
|
+
return this.invoke('statusbar.getWordCount') ?? 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Returns the current character count of the editor content.
|
|
361
|
+
* @returns {number}
|
|
362
|
+
*/
|
|
363
|
+
getCharCount() {
|
|
364
|
+
return this.invoke('statusbar.getCharCount') ?? 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Downloads the editor content as an HTML file.
|
|
369
|
+
* @param {string} [filename='document.html']
|
|
370
|
+
*/
|
|
371
|
+
downloadHTML(filename = 'document.html') {
|
|
372
|
+
this._download(this.getHTML(), filename, 'text/html');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Downloads the editor content as a plain-text file.
|
|
377
|
+
* @param {string} [filename='document.txt']
|
|
378
|
+
*/
|
|
379
|
+
downloadText(filename = 'document.txt') {
|
|
380
|
+
this._download(this.getText(), filename, 'text/plain');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Downloads the editor content as a Markdown file.
|
|
385
|
+
* @param {string} [filename='document.md']
|
|
386
|
+
*/
|
|
387
|
+
downloadMarkdown(filename = 'document.md') {
|
|
388
|
+
this._download(this.getMarkdown(), filename, 'text/markdown');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Creates a temporary Blob URL and triggers a browser file download.
|
|
393
|
+
* @param {string} content
|
|
394
|
+
* @param {string} filename
|
|
395
|
+
* @param {string} mimeType
|
|
396
|
+
*/
|
|
397
|
+
_download(content, filename, mimeType) {
|
|
398
|
+
const blob = new Blob([content], { type: mimeType });
|
|
399
|
+
const url = URL.createObjectURL(blob);
|
|
400
|
+
const a = document.createElement('a');
|
|
401
|
+
a.href = url;
|
|
402
|
+
a.download = filename;
|
|
403
|
+
a.style.display = 'none';
|
|
404
|
+
document.body.appendChild(a);
|
|
405
|
+
a.click();
|
|
406
|
+
document.body.removeChild(a);
|
|
407
|
+
URL.revokeObjectURL(url);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Opens the editor content in a new window and triggers the browser print dialog.
|
|
412
|
+
* @param {string} [title='']
|
|
413
|
+
*/
|
|
414
|
+
print(title = '') {
|
|
415
|
+
const content = this.getHTML();
|
|
416
|
+
const safeTitle = (title || '').replace(/</g, '<').replace(/>/g, '>');
|
|
417
|
+
const w = window.open('', '_blank');
|
|
418
|
+
if (!w) return; // popup blocked by browser
|
|
419
|
+
w.document.write(
|
|
420
|
+
'<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">' +
|
|
421
|
+
`<title>${safeTitle}</title>` +
|
|
422
|
+
'<style>' +
|
|
423
|
+
'body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif;font-size:14px;line-height:1.6;padding:20mm;color:#111827;}' +
|
|
424
|
+
'ul.an-checklist{list-style:none;padding-left:0;}' +
|
|
425
|
+
'ul.an-checklist li{padding-left:24px;position:relative;margin:2px 0;}' +
|
|
426
|
+
'ul.an-checklist li input[type="checkbox"]{position:absolute;left:0;top:3px;}' +
|
|
427
|
+
'code{background:#f3f4f6;border-radius:3px;padding:.1em .35em;font-family:monospace;}' +
|
|
428
|
+
'pre{background:#f3f4f6;padding:.75em 1em;border-radius:4px;overflow-x:auto;}' +
|
|
429
|
+
'table{border-collapse:collapse;}td,th{border:1px solid #d1d5db;padding:4px 8px;}' +
|
|
430
|
+
'</style>' +
|
|
431
|
+
`</head><body>${content}</body></html>`,
|
|
432
|
+
);
|
|
433
|
+
w.document.close();
|
|
434
|
+
w.onload = () => { w.print(); };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Sets whether the editor is disabled (readonly).
|
|
439
|
+
* @param {boolean} disabled
|
|
440
|
+
*/
|
|
441
|
+
setDisabled(disabled) {
|
|
442
|
+
const editable = this.layoutInfo.editable;
|
|
443
|
+
if (disabled) {
|
|
444
|
+
editable.setAttribute('contenteditable', 'false');
|
|
445
|
+
this.layoutInfo.container.classList.add('an-disabled');
|
|
446
|
+
} else {
|
|
447
|
+
editable.setAttribute('contenteditable', 'true');
|
|
448
|
+
this.layoutInfo.container.classList.remove('an-disabled');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
// Destroy
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Completely removes the editor and restores the original element.
|
|
458
|
+
*/
|
|
459
|
+
destroy() {
|
|
460
|
+
if (!this._alive) return;
|
|
461
|
+
|
|
462
|
+
this._modules.forEach((module) => {
|
|
463
|
+
if (typeof module.destroy === 'function') module.destroy();
|
|
464
|
+
});
|
|
465
|
+
this._modules.clear();
|
|
466
|
+
|
|
467
|
+
this._disposers.forEach((d) => d());
|
|
468
|
+
this._disposers = [];
|
|
469
|
+
|
|
470
|
+
const container = this.layoutInfo.container;
|
|
471
|
+
if (container && container.parentNode) {
|
|
472
|
+
// Restore original element
|
|
473
|
+
this.targetEl.style.display = '';
|
|
474
|
+
container.parentNode.removeChild(container);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (typeof this.options.onDestroy === 'function') {
|
|
478
|
+
this.options.onDestroy(this);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this._alive = false;
|
|
482
|
+
this._listeners.clear();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// Helpers
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Syncs editor HTML back into the original textarea/input for form submission.
|
|
491
|
+
*/
|
|
492
|
+
_syncToTarget() {
|
|
493
|
+
if (this.targetEl.tagName === 'TEXTAREA' || this.targetEl.tagName === 'INPUT') {
|
|
494
|
+
this.targetEl.value = this.getHTML();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|