autumnnote 1.15.0 → 1.16.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 +22 -0
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +543 -62
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +61 -0
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +5 -5
- package/src/js/Context.js +154 -15
- package/src/js/editing/History.js +38 -6
- package/src/js/i18n/de.js +15 -0
- package/src/js/i18n/en.js +15 -0
- package/src/js/i18n/es.js +15 -0
- package/src/js/i18n/fr.js +15 -0
- package/src/js/i18n/ja.js +15 -0
- package/src/js/i18n/ko.js +15 -0
- package/src/js/i18n/vi.js +15 -0
- package/src/js/i18n/zh.js +15 -0
- package/src/js/index.js +13 -1
- package/src/js/module/Clipboard.js +20 -6
- package/src/js/module/Editor.js +15 -1
- package/src/js/module/ImageDialog.js +34 -6
- package/src/js/module/MarkdownShortcuts.js +2 -2
- package/src/js/module/SlashMenu.js +376 -0
- package/src/js/module/Statusbar.js +3 -9
- package/src/js/settings.js +27 -0
- package/src/styles/autumnnote.scss +49 -0
- package/types/index.d.ts +84 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autumnnote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "WYSIWYG rich-text editor built with vanilla JavaScript — zero dependencies, no jQuery. Dark mode, @mention, markdown shortcuts, bubble toolbar. React and Vue 3 wrappers included.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/autumnnote.cjs",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
24
|
"dev": "vite",
|
|
25
|
-
"build": "vite build && vite build --config vite.umd.config.js && node scripts/create-cjs-alias.mjs",
|
|
25
|
+
"build": "vite build && vite build --config vite.umd.config.js && vite build --config vite.cdn.config.js && node scripts/create-cjs-alias.mjs",
|
|
26
26
|
"build:demo": "vite build --config vite.demo.config.js",
|
|
27
27
|
"preview": "vite preview",
|
|
28
28
|
"prepublishOnly": "npm run build",
|
|
@@ -100,13 +100,13 @@
|
|
|
100
100
|
"@vitest/browser-playwright": "^4.1.10",
|
|
101
101
|
"@vitest/coverage-v8": "^4.1.8",
|
|
102
102
|
"cross-env": "^10.1.0",
|
|
103
|
-
"eslint": "10.
|
|
103
|
+
"eslint": "10.7.0",
|
|
104
104
|
"jsdom": "^25.0.1",
|
|
105
105
|
"playwright": "^1.61.1",
|
|
106
106
|
"rollup-plugin-visualizer": "^7.0.1",
|
|
107
|
-
"sass": "^1.
|
|
107
|
+
"sass": "^1.101.0",
|
|
108
108
|
"typescript": "^6.0.2",
|
|
109
|
-
"vite": "^8.
|
|
109
|
+
"vite": "^8.1.5",
|
|
110
110
|
"vitest": "^4.1.8"
|
|
111
111
|
},
|
|
112
112
|
"packageManager": "pnpm@11.1.3",
|
package/src/js/Context.js
CHANGED
|
@@ -39,6 +39,7 @@ import { AutoSaveRestore } from './module/AutoSaveRestore.js';
|
|
|
39
39
|
import { MarkdownShortcuts } from './module/MarkdownShortcuts.js';
|
|
40
40
|
import { BubbleToolbar } from './module/BubbleToolbar.js';
|
|
41
41
|
import { Mention } from './module/Mention.js';
|
|
42
|
+
import { SlashMenu } from './module/SlashMenu.js';
|
|
42
43
|
|
|
43
44
|
/** Module registry shared across all Context instances (populated via AutumnNote.registerModule). */
|
|
44
45
|
export const _customModules = new Map();
|
|
@@ -72,6 +73,9 @@ export class Context {
|
|
|
72
73
|
|
|
73
74
|
this._disposers = [];
|
|
74
75
|
this._alive = false;
|
|
76
|
+
this._autoSaveTimer = null;
|
|
77
|
+
this._pendingAutoSave = null;
|
|
78
|
+
this._suppressedRemoteHTML = null;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
// ---------------------------------------------------------------------------
|
|
@@ -153,10 +157,11 @@ export class Context {
|
|
|
153
157
|
register('shortcutsDialog', ShortcutsDialog);
|
|
154
158
|
register('findReplace', FindReplace);
|
|
155
159
|
register('imageCropOverlay', ImageCropOverlay);
|
|
156
|
-
register('autoSaveRestore', AutoSaveRestore);
|
|
157
|
-
register('markdownShortcuts', MarkdownShortcuts);
|
|
158
|
-
register('bubbleToolbar', BubbleToolbar);
|
|
159
|
-
register('mention', Mention);
|
|
160
|
+
if (this.options.autoSaveRestore) register('autoSaveRestore', AutoSaveRestore);
|
|
161
|
+
if (this.options.markdownShortcuts !== false) register('markdownShortcuts', MarkdownShortcuts);
|
|
162
|
+
if (this.options.bubbleToolbar) register('bubbleToolbar', BubbleToolbar);
|
|
163
|
+
if (this.options.mention) register('mention', Mention);
|
|
164
|
+
if (this.options.slashMenu !== false) register('slashMenu', SlashMenu);
|
|
160
165
|
|
|
161
166
|
// Custom modules registered via AutumnNote.registerModule()
|
|
162
167
|
if (_customModules.size > 0) {
|
|
@@ -180,6 +185,18 @@ export class Context {
|
|
|
180
185
|
return this;
|
|
181
186
|
}
|
|
182
187
|
|
|
188
|
+
registerSlashCommand(command) {
|
|
189
|
+
if (!command?.id || typeof command.run !== 'function') {
|
|
190
|
+
throw new TypeError('[AutumnNote] Slash command requires an id and run(context) function.');
|
|
191
|
+
}
|
|
192
|
+
const commands = this.options.slashCommands || (this.options.slashCommands = []);
|
|
193
|
+
const index = commands.findIndex((item) => item.id === command.id);
|
|
194
|
+
if (index >= 0) commands[index] = command;
|
|
195
|
+
else commands.push(command);
|
|
196
|
+
this.invoke('slashMenu.refresh');
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
|
|
183
200
|
/**
|
|
184
201
|
* Installs a plugin on this editor instance.
|
|
185
202
|
* If called after create(), buttons are registered immediately but the toolbar
|
|
@@ -246,22 +263,57 @@ export class Context {
|
|
|
246
263
|
}
|
|
247
264
|
});
|
|
248
265
|
// Sync textarea/input value on every change so form.submit() always gets fresh content
|
|
249
|
-
const d3 = this.on('change', () => this._syncToTarget());
|
|
250
|
-
this.
|
|
266
|
+
const d3 = this.on('change', (html) => this._syncToTarget(html));
|
|
267
|
+
const dRemote = this.on('change', (html) => {
|
|
268
|
+
if (this.options.blockIds) { this.ensureBlockIds(); html = this.getHTML(); }
|
|
269
|
+
if (html === this._suppressedRemoteHTML) { this._suppressedRemoteHTML = null; return; }
|
|
270
|
+
this.options.collaborationAdapter?.onLocalChange?.(html, this);
|
|
271
|
+
});
|
|
272
|
+
this._disposers.push(d0, d1, d2, d3, dRemote);
|
|
251
273
|
|
|
252
274
|
// Auto-save to localStorage on every change (also writes :asrmeta for restore banner)
|
|
253
275
|
if (this.options.autoSave && this.options.autoSaveKey) {
|
|
254
|
-
const d4 = this.on('change', () =>
|
|
255
|
-
try {
|
|
256
|
-
const key = this.options.autoSaveKey;
|
|
257
|
-
localStorage.setItem(key, this.getHTML());
|
|
258
|
-
localStorage.setItem(key + ':asrmeta', JSON.stringify({ savedAt: Date.now() }));
|
|
259
|
-
} catch (_) { void _; }
|
|
260
|
-
});
|
|
276
|
+
const d4 = this.on('change', (html) => this._scheduleAutoSave(html));
|
|
261
277
|
this._disposers.push(d4);
|
|
262
278
|
}
|
|
263
279
|
}
|
|
264
280
|
|
|
281
|
+
_scheduleAutoSave(html) {
|
|
282
|
+
this._pendingAutoSave = html;
|
|
283
|
+
clearTimeout(this._autoSaveTimer);
|
|
284
|
+
this._autoSaveTimer = setTimeout(() => this.flushAutoSave(), this.options.autoSaveDelay ?? 400);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async flushAutoSave() {
|
|
288
|
+
clearTimeout(this._autoSaveTimer);
|
|
289
|
+
this._autoSaveTimer = null;
|
|
290
|
+
if (this._pendingAutoSave == null) return;
|
|
291
|
+
const html = this._pendingAutoSave;
|
|
292
|
+
this._pendingAutoSave = null;
|
|
293
|
+
const key = this.options.autoSaveKey;
|
|
294
|
+
const savedAt = Date.now();
|
|
295
|
+
try {
|
|
296
|
+
const adapter = this.options.autoSaveAdapter;
|
|
297
|
+
if (typeof adapter?.save === 'function') {
|
|
298
|
+
await adapter.save({ key, html, savedAt, context: this });
|
|
299
|
+
} else {
|
|
300
|
+
localStorage.setItem(key, html);
|
|
301
|
+
localStorage.setItem(key + ':asrmeta', JSON.stringify({ savedAt }));
|
|
302
|
+
}
|
|
303
|
+
this.triggerEvent('autoSave', { key, html, savedAt });
|
|
304
|
+
} catch (error) {
|
|
305
|
+
this.triggerEvent('autoSaveError', { key, error });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async loadAutoSave() {
|
|
310
|
+
const adapter = this.options.autoSaveAdapter;
|
|
311
|
+
if (typeof adapter?.load === 'function') {
|
|
312
|
+
return adapter.load({ key: this.options.autoSaveKey, context: this });
|
|
313
|
+
}
|
|
314
|
+
try { return localStorage.getItem(this.options.autoSaveKey); } catch (_) { return null; }
|
|
315
|
+
}
|
|
316
|
+
|
|
265
317
|
// ---------------------------------------------------------------------------
|
|
266
318
|
// Module invocation
|
|
267
319
|
// ---------------------------------------------------------------------------
|
|
@@ -337,6 +389,34 @@ export class Context {
|
|
|
337
389
|
// Public editor API
|
|
338
390
|
// ---------------------------------------------------------------------------
|
|
339
391
|
|
|
392
|
+
/** Updates runtime-safe options without recreating the editor. */
|
|
393
|
+
updateOptions(overrides = {}) {
|
|
394
|
+
const next = mergeDeep(this.options, overrides);
|
|
395
|
+
Object.keys(this.options).forEach((key) => delete this.options[key]);
|
|
396
|
+
Object.assign(this.options, next);
|
|
397
|
+
|
|
398
|
+
const { container, editable } = this.layoutInfo;
|
|
399
|
+
if (Object.hasOwn(overrides, 'readOnly')) this.setDisabled(Boolean(this.options.readOnly));
|
|
400
|
+
if (Object.hasOwn(overrides, 'spellcheck')) editable.spellcheck = this.options.spellcheck !== false;
|
|
401
|
+
if (Object.hasOwn(overrides, 'placeholder')) editable.dataset.placeholder = this.options.placeholder || '';
|
|
402
|
+
if (Object.hasOwn(overrides, 'direction')) {
|
|
403
|
+
const rtl = this.options.direction === 'rtl';
|
|
404
|
+
editable.setAttribute('dir', rtl ? 'rtl' : 'ltr');
|
|
405
|
+
container.classList.toggle('an-dir-rtl', rtl);
|
|
406
|
+
}
|
|
407
|
+
if (Object.hasOwn(overrides, 'height') || Object.hasOwn(overrides, 'minHeight')) {
|
|
408
|
+
const height = this.options.height || this.options.minHeight || 0;
|
|
409
|
+
editable.style.minHeight = height ? `${height}px` : '';
|
|
410
|
+
}
|
|
411
|
+
if (Object.hasOwn(overrides, 'maxHeight')) {
|
|
412
|
+
editable.style.maxHeight = this.options.maxHeight ? `${this.options.maxHeight}px` : '';
|
|
413
|
+
}
|
|
414
|
+
if (Object.hasOwn(overrides, 'toolbar')) this.invoke('toolbar.rebuild');
|
|
415
|
+
this.invoke('statusbar.update');
|
|
416
|
+
this.triggerEvent('optionsChange', { ...overrides });
|
|
417
|
+
return this;
|
|
418
|
+
}
|
|
419
|
+
|
|
340
420
|
/**
|
|
341
421
|
* Returns the current HTML content of the editor.
|
|
342
422
|
* Zero-width spaces (U+200B) inserted by inline editing helpers are stripped
|
|
@@ -444,6 +524,64 @@ export class Context {
|
|
|
444
524
|
return this.invoke('editor.getMarkdown');
|
|
445
525
|
}
|
|
446
526
|
|
|
527
|
+
getSelectionBookmark() {
|
|
528
|
+
return this.invoke('editor.getSelectionBookmark') ?? null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
restoreSelectionBookmark(bookmark) {
|
|
532
|
+
return this.invoke('editor.restoreSelectionBookmark', bookmark);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async importDocument(format, data) {
|
|
536
|
+
const adapter = this.options.documentAdapters?.[format];
|
|
537
|
+
let html;
|
|
538
|
+
if (typeof adapter?.import === 'function') html = await adapter.import(data, this);
|
|
539
|
+
else if (format === 'html') html = String(data ?? '');
|
|
540
|
+
else if (format === 'markdown') { this.setMarkdown(String(data ?? '')); return this; }
|
|
541
|
+
else if (format === 'text') { this.setText(String(data ?? '')); return this; }
|
|
542
|
+
else throw new Error(`[AutumnNote] No importer registered for "${format}".`);
|
|
543
|
+
this.setHTML(html);
|
|
544
|
+
return this;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async exportDocument(format) {
|
|
548
|
+
const adapter = this.options.documentAdapters?.[format];
|
|
549
|
+
if (typeof adapter?.export === 'function') return adapter.export(this, this.getHTML());
|
|
550
|
+
if (format === 'html') return this.getHTML();
|
|
551
|
+
if (format === 'markdown') return this.getMarkdown();
|
|
552
|
+
if (format === 'text') return this.getText();
|
|
553
|
+
throw new Error(`[AutumnNote] No exporter registered for "${format}".`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
ensureBlockIds() {
|
|
557
|
+
const blocks = this.layoutInfo.editable.children;
|
|
558
|
+
for (const block of blocks) {
|
|
559
|
+
if (!block.hasAttribute('data-an-block-id')) {
|
|
560
|
+
const id = globalThis.crypto?.randomUUID?.() || `an-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
561
|
+
block.setAttribute('data-an-block-id', id);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return this;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
getDocument() {
|
|
568
|
+
if (this.options.blockIds) this.ensureBlockIds();
|
|
569
|
+
return { version: 1, html: this.getHTML(), markdown: this.getMarkdown() };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
loadDocument(documentData) {
|
|
573
|
+
this.setHTML(documentData?.html || '');
|
|
574
|
+
this.clearHistory();
|
|
575
|
+
return this;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
applyRemoteHTML(html) {
|
|
579
|
+
this.setHTML(html);
|
|
580
|
+
this._suppressedRemoteHTML = this.getHTML();
|
|
581
|
+
this.clearHistory();
|
|
582
|
+
return this;
|
|
583
|
+
}
|
|
584
|
+
|
|
447
585
|
/**
|
|
448
586
|
* Returns the current word count of the editor content.
|
|
449
587
|
* @returns {number}
|
|
@@ -601,6 +739,7 @@ export class Context {
|
|
|
601
739
|
destroy() {
|
|
602
740
|
if (!this._alive) return;
|
|
603
741
|
|
|
742
|
+
if (this._pendingAutoSave != null) void this.flushAutoSave();
|
|
604
743
|
this._modules.forEach((module) => {
|
|
605
744
|
if (typeof module.destroy === 'function') module.destroy();
|
|
606
745
|
});
|
|
@@ -647,9 +786,9 @@ export class Context {
|
|
|
647
786
|
/**
|
|
648
787
|
* Syncs editor HTML back into the original textarea/input for form submission.
|
|
649
788
|
*/
|
|
650
|
-
_syncToTarget() {
|
|
789
|
+
_syncToTarget(html) {
|
|
651
790
|
if (this.targetEl.tagName === 'TEXTAREA' || this.targetEl.tagName === 'INPUT') {
|
|
652
|
-
/** @type {HTMLInputElement} */ (this.targetEl).value = this.getHTML();
|
|
791
|
+
/** @type {HTMLInputElement} */ (this.targetEl).value = typeof html === 'string' ? html : this.getHTML();
|
|
653
792
|
}
|
|
654
793
|
}
|
|
655
794
|
}
|
|
@@ -7,16 +7,37 @@ export class History {
|
|
|
7
7
|
/**
|
|
8
8
|
* @param {HTMLElement} editable - the contenteditable element
|
|
9
9
|
* @param {number} [limit=100] - maximum number of undo/redo states
|
|
10
|
+
* @param {number} [maxBytes=10485760] - maximum combined size (chars) of all
|
|
11
|
+
* stacked snapshots (html + tokenized image data). Oldest states are
|
|
12
|
+
* evicted first when exceeded, even if `limit` hasn't been reached —
|
|
13
|
+
* documents with many large embedded images can otherwise hold dozens of
|
|
14
|
+
* full-size copies in memory despite the step-count limit.
|
|
10
15
|
*/
|
|
11
|
-
constructor(editable, limit = 100) {
|
|
16
|
+
constructor(editable, limit = 100, maxBytes = 10 * 1024 * 1024) {
|
|
12
17
|
this.editable = editable;
|
|
13
18
|
this._limit = limit;
|
|
19
|
+
this._maxBytes = maxBytes;
|
|
20
|
+
this._bytes = 0;
|
|
14
21
|
/** @type {Array<{html: string, images?: Record<string,string>, sel: {start: number, end: number}|null}>} */
|
|
15
22
|
this.stack = [];
|
|
16
23
|
this.stackOffset = -1;
|
|
17
24
|
this._savePoint();
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Approximate in-memory size (chars) of one stacked snapshot: the tokenized
|
|
29
|
+
* HTML string plus every image data URL it references.
|
|
30
|
+
* @param {{html: string, images?: Record<string,string>}} entry
|
|
31
|
+
* @returns {number}
|
|
32
|
+
*/
|
|
33
|
+
_entrySize(entry) {
|
|
34
|
+
let size = entry.html.length;
|
|
35
|
+
if (entry.images) {
|
|
36
|
+
for (const key in entry.images) size += entry.images[key].length;
|
|
37
|
+
}
|
|
38
|
+
return size;
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
// ---------------------------------------------------------------------------
|
|
21
42
|
// Private helpers
|
|
22
43
|
// ---------------------------------------------------------------------------
|
|
@@ -115,16 +136,26 @@ export class History {
|
|
|
115
136
|
_savePoint() {
|
|
116
137
|
// Trim future history if we're mid-stack
|
|
117
138
|
if (this.stackOffset < this.stack.length - 1) {
|
|
139
|
+
for (const entry of this.stack.slice(this.stackOffset + 1)) {
|
|
140
|
+
this._bytes -= this._entrySize(entry);
|
|
141
|
+
}
|
|
118
142
|
this.stack = this.stack.slice(0, this.stackOffset + 1);
|
|
119
143
|
}
|
|
120
144
|
const raw = this._serialize();
|
|
121
145
|
const { html, images } = this._tokenizeImages(raw);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
const entry = { html, images, sel: this._serializeSelection() };
|
|
147
|
+
this.stack.push(entry);
|
|
148
|
+
this._bytes += this._entrySize(entry);
|
|
149
|
+
|
|
150
|
+
// Evict oldest states first, whichever budget (step count or byte size)
|
|
151
|
+
// is exceeded — always keep at least the just-pushed current state.
|
|
152
|
+
while (this.stack.length > 1 && (this.stack.length > this._limit || this._bytes > this._maxBytes)) {
|
|
153
|
+
this._bytes -= this._entrySize(this.stack.shift());
|
|
127
154
|
}
|
|
155
|
+
// The newly-pushed current state is always the final entry. Recompute the
|
|
156
|
+
// offset from the resulting stack instead of incrementing conditionally:
|
|
157
|
+
// a single oversized snapshot may evict several older entries at once.
|
|
158
|
+
this.stackOffset = this.stack.length - 1;
|
|
128
159
|
}
|
|
129
160
|
|
|
130
161
|
_restore(point) {
|
|
@@ -209,6 +240,7 @@ export class History {
|
|
|
209
240
|
reset() {
|
|
210
241
|
this.stack = [];
|
|
211
242
|
this.stackOffset = -1;
|
|
243
|
+
this._bytes = 0;
|
|
212
244
|
this._savePoint();
|
|
213
245
|
}
|
|
214
246
|
|
package/src/js/i18n/de.js
CHANGED
|
@@ -342,4 +342,19 @@ export const de = {
|
|
|
342
342
|
restore: 'Wiederherstellen',
|
|
343
343
|
discard: 'Verwerfen',
|
|
344
344
|
},
|
|
345
|
+
|
|
346
|
+
slashMenu: {
|
|
347
|
+
noResults: 'Keine passenden Befehle',
|
|
348
|
+
heading1: 'Überschrift 1',
|
|
349
|
+
heading2: 'Überschrift 2',
|
|
350
|
+
heading3: 'Überschrift 3',
|
|
351
|
+
bulletList: 'Aufzählungsliste',
|
|
352
|
+
numberedList: 'Nummerierte Liste',
|
|
353
|
+
checklist: 'Checkliste',
|
|
354
|
+
blockquote: 'Zitat',
|
|
355
|
+
codeBlock: 'Codeblock',
|
|
356
|
+
horizontalRule: 'Horizontale Linie',
|
|
357
|
+
table: 'Tabelle',
|
|
358
|
+
image: 'Bild',
|
|
359
|
+
},
|
|
345
360
|
};
|
package/src/js/i18n/en.js
CHANGED
|
@@ -350,4 +350,19 @@ export const en = {
|
|
|
350
350
|
imageSize: (maxSize) =>
|
|
351
351
|
`Image file is too large. Maximum allowed size is ${maxSize} MB.`,
|
|
352
352
|
},
|
|
353
|
+
|
|
354
|
+
slashMenu: {
|
|
355
|
+
noResults: 'No matching commands',
|
|
356
|
+
heading1: 'Heading 1',
|
|
357
|
+
heading2: 'Heading 2',
|
|
358
|
+
heading3: 'Heading 3',
|
|
359
|
+
bulletList: 'Bullet List',
|
|
360
|
+
numberedList: 'Numbered List',
|
|
361
|
+
checklist: 'Checklist',
|
|
362
|
+
blockquote: 'Blockquote',
|
|
363
|
+
codeBlock: 'Code Block',
|
|
364
|
+
horizontalRule: 'Horizontal Rule',
|
|
365
|
+
table: 'Table',
|
|
366
|
+
image: 'Image',
|
|
367
|
+
},
|
|
353
368
|
};
|
package/src/js/i18n/es.js
CHANGED
|
@@ -342,4 +342,19 @@ export const es = {
|
|
|
342
342
|
restore: 'Restaurar',
|
|
343
343
|
discard: 'Descartar',
|
|
344
344
|
},
|
|
345
|
+
|
|
346
|
+
slashMenu: {
|
|
347
|
+
noResults: 'No hay comandos coincidentes',
|
|
348
|
+
heading1: 'Encabezado 1',
|
|
349
|
+
heading2: 'Encabezado 2',
|
|
350
|
+
heading3: 'Encabezado 3',
|
|
351
|
+
bulletList: 'Lista con viñetas',
|
|
352
|
+
numberedList: 'Lista numerada',
|
|
353
|
+
checklist: 'Lista de verificación',
|
|
354
|
+
blockquote: 'Cita',
|
|
355
|
+
codeBlock: 'Bloque de código',
|
|
356
|
+
horizontalRule: 'Línea horizontal',
|
|
357
|
+
table: 'Tabla',
|
|
358
|
+
image: 'Imagen',
|
|
359
|
+
},
|
|
345
360
|
};
|
package/src/js/i18n/fr.js
CHANGED
|
@@ -343,4 +343,19 @@ export const fr = {
|
|
|
343
343
|
restore: 'Restaurer',
|
|
344
344
|
discard: 'Ignorer',
|
|
345
345
|
},
|
|
346
|
+
|
|
347
|
+
slashMenu: {
|
|
348
|
+
noResults: 'Aucune commande correspondante',
|
|
349
|
+
heading1: 'Titre 1',
|
|
350
|
+
heading2: 'Titre 2',
|
|
351
|
+
heading3: 'Titre 3',
|
|
352
|
+
bulletList: 'Liste à puces',
|
|
353
|
+
numberedList: 'Liste numérotée',
|
|
354
|
+
checklist: 'Liste de contrôle',
|
|
355
|
+
blockquote: 'Citation',
|
|
356
|
+
codeBlock: 'Bloc de code',
|
|
357
|
+
horizontalRule: 'Ligne horizontale',
|
|
358
|
+
table: 'Tableau',
|
|
359
|
+
image: 'Image',
|
|
360
|
+
},
|
|
346
361
|
};
|
package/src/js/i18n/ja.js
CHANGED
|
@@ -343,4 +343,19 @@ export const ja = {
|
|
|
343
343
|
restore: '復元',
|
|
344
344
|
discard: '破棄',
|
|
345
345
|
},
|
|
346
|
+
|
|
347
|
+
slashMenu: {
|
|
348
|
+
noResults: '一致するコマンドがありません',
|
|
349
|
+
heading1: '見出し1',
|
|
350
|
+
heading2: '見出し2',
|
|
351
|
+
heading3: '見出し3',
|
|
352
|
+
bulletList: '箇条書きリスト',
|
|
353
|
+
numberedList: '番号付きリスト',
|
|
354
|
+
checklist: 'チェックリスト',
|
|
355
|
+
blockquote: '引用',
|
|
356
|
+
codeBlock: 'コードブロック',
|
|
357
|
+
horizontalRule: '水平線',
|
|
358
|
+
table: '表',
|
|
359
|
+
image: '画像',
|
|
360
|
+
},
|
|
346
361
|
};
|
package/src/js/i18n/ko.js
CHANGED
|
@@ -342,4 +342,19 @@ export const ko = {
|
|
|
342
342
|
restore: '복원',
|
|
343
343
|
discard: '삭제',
|
|
344
344
|
},
|
|
345
|
+
|
|
346
|
+
slashMenu: {
|
|
347
|
+
noResults: '일치하는 명령이 없습니다',
|
|
348
|
+
heading1: '제목 1',
|
|
349
|
+
heading2: '제목 2',
|
|
350
|
+
heading3: '제목 3',
|
|
351
|
+
bulletList: '글머리 기호 목록',
|
|
352
|
+
numberedList: '번호 매기기 목록',
|
|
353
|
+
checklist: '체크리스트',
|
|
354
|
+
blockquote: '인용문',
|
|
355
|
+
codeBlock: '코드 블록',
|
|
356
|
+
horizontalRule: '가로선',
|
|
357
|
+
table: '표',
|
|
358
|
+
image: '이미지',
|
|
359
|
+
},
|
|
345
360
|
};
|
package/src/js/i18n/vi.js
CHANGED
|
@@ -343,4 +343,19 @@ export const vi = {
|
|
|
343
343
|
restore: 'Khôi phục',
|
|
344
344
|
discard: 'Bỏ qua',
|
|
345
345
|
},
|
|
346
|
+
|
|
347
|
+
slashMenu: {
|
|
348
|
+
noResults: 'Không có lệnh phù hợp',
|
|
349
|
+
heading1: 'Tiêu đề 1',
|
|
350
|
+
heading2: 'Tiêu đề 2',
|
|
351
|
+
heading3: 'Tiêu đề 3',
|
|
352
|
+
bulletList: 'Danh sách không thứ tự',
|
|
353
|
+
numberedList: 'Danh sách có thứ tự',
|
|
354
|
+
checklist: 'Danh sách công việc',
|
|
355
|
+
blockquote: 'Trích dẫn',
|
|
356
|
+
codeBlock: 'Khối mã',
|
|
357
|
+
horizontalRule: 'Đường kẻ ngang',
|
|
358
|
+
table: 'Bảng',
|
|
359
|
+
image: 'Hình ảnh',
|
|
360
|
+
},
|
|
346
361
|
};
|
package/src/js/i18n/zh.js
CHANGED
|
@@ -343,4 +343,19 @@ export const zh = {
|
|
|
343
343
|
restore: '恢复',
|
|
344
344
|
discard: '放弃',
|
|
345
345
|
},
|
|
346
|
+
|
|
347
|
+
slashMenu: {
|
|
348
|
+
noResults: '没有匹配的命令',
|
|
349
|
+
heading1: '标题 1',
|
|
350
|
+
heading2: '标题 2',
|
|
351
|
+
heading3: '标题 3',
|
|
352
|
+
bulletList: '项目符号列表',
|
|
353
|
+
numberedList: '编号列表',
|
|
354
|
+
checklist: '检查清单',
|
|
355
|
+
blockquote: '引用',
|
|
356
|
+
codeBlock: '代码块',
|
|
357
|
+
horizontalRule: '水平线',
|
|
358
|
+
table: '表格',
|
|
359
|
+
image: '图片',
|
|
360
|
+
},
|
|
346
361
|
};
|
package/src/js/index.js
CHANGED
|
@@ -140,11 +140,23 @@ const AutumnNote = {
|
|
|
140
140
|
*/
|
|
141
141
|
registerButton(btnDef) { registerButton(btnDef); return this; },
|
|
142
142
|
|
|
143
|
+
/** Registers a slash-menu command for future editor instances. */
|
|
144
|
+
registerSlashCommand(command) {
|
|
145
|
+
if (!command?.id || typeof command.run !== 'function') {
|
|
146
|
+
throw new TypeError('[AutumnNote] Slash command requires an id and run(context) function.');
|
|
147
|
+
}
|
|
148
|
+
const commands = defaultOptions.slashCommands;
|
|
149
|
+
const index = commands.findIndex((item) => item.id === command.id);
|
|
150
|
+
if (index >= 0) commands[index] = command;
|
|
151
|
+
else commands.push(command);
|
|
152
|
+
return this;
|
|
153
|
+
},
|
|
154
|
+
|
|
143
155
|
/** All pre-built button definitions — accessible in every module format including UMD/CJS. */
|
|
144
156
|
buttons,
|
|
145
157
|
|
|
146
158
|
/** Library version */
|
|
147
|
-
version: '1.
|
|
159
|
+
version: '1.16.0',
|
|
148
160
|
};
|
|
149
161
|
|
|
150
162
|
// ---------------------------------------------------------------------------
|
|
@@ -416,12 +416,7 @@ export class Clipboard {
|
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
const alt = file.name.replace(/\.[^.]+$/, '');
|
|
419
|
-
this.
|
|
420
|
-
// Keep the large data URL in a JS Map; insert a lightweight blob: URL
|
|
421
|
-
// into the DOM so editable.innerHTML never contains the big base64 string.
|
|
422
|
-
const blob = this._dataUrlToBlob(dataUrl);
|
|
423
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
424
|
-
this._blobRegistry.set(blobUrl, dataUrl);
|
|
419
|
+
this.compressAndRegister(file).then((blobUrl) => {
|
|
425
420
|
this.context.invoke('editor.insertImage', blobUrl, alt);
|
|
426
421
|
}).catch((err) => {
|
|
427
422
|
const message = `Image "${file.name}" could not be processed.`;
|
|
@@ -431,6 +426,25 @@ export class Clipboard {
|
|
|
431
426
|
});
|
|
432
427
|
}
|
|
433
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Compresses an image File via canvas and registers the result behind a
|
|
431
|
+
* lightweight blob: URL (see `resolveImages`), so callers never have to hold
|
|
432
|
+
* the full base64 string in the DOM. Shared by paste/drop and ImageDialog's
|
|
433
|
+
* file picker so every image-insertion path gets the same compression.
|
|
434
|
+
* @param {File} file
|
|
435
|
+
* @returns {Promise<string>} blob: URL usable as an <img src>
|
|
436
|
+
*/
|
|
437
|
+
async compressAndRegister(file) {
|
|
438
|
+
const processor = this.options.imageProcessor;
|
|
439
|
+
const dataUrl = typeof processor === 'function'
|
|
440
|
+
? await processor(file, { context: this.context })
|
|
441
|
+
: await this._compressImage(file);
|
|
442
|
+
const blob = this._dataUrlToBlob(dataUrl);
|
|
443
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
444
|
+
this._blobRegistry.set(blobUrl, dataUrl);
|
|
445
|
+
return blobUrl;
|
|
446
|
+
}
|
|
447
|
+
|
|
434
448
|
/**
|
|
435
449
|
* Replaces any blob: URLs created by this module with their original data URLs.
|
|
436
450
|
* Called by Editor.getHTML() so the returned HTML is fully self-contained.
|
package/src/js/module/Editor.js
CHANGED
|
@@ -34,7 +34,11 @@ export class Editor {
|
|
|
34
34
|
|
|
35
35
|
initialize() {
|
|
36
36
|
const editable = this.context.layoutInfo.editable;
|
|
37
|
-
this._history = new History(
|
|
37
|
+
this._history = new History(
|
|
38
|
+
editable,
|
|
39
|
+
this.options.historyLimit || 100,
|
|
40
|
+
this.options.historyMaxBytes || 10 * 1024 * 1024,
|
|
41
|
+
);
|
|
38
42
|
this._bindEvents(editable);
|
|
39
43
|
return this;
|
|
40
44
|
}
|
|
@@ -539,6 +543,16 @@ export class Editor {
|
|
|
539
543
|
return this._history ? this._history.getRedoCount() : 0;
|
|
540
544
|
}
|
|
541
545
|
|
|
546
|
+
getSelectionBookmark() {
|
|
547
|
+
return this._history?._serializeSelection() ?? null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
restoreSelectionBookmark(bookmark) {
|
|
551
|
+
if (!bookmark || !this._history) return false;
|
|
552
|
+
this._history._restoreSelection(bookmark);
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
542
556
|
// ---------------------------------------------------------------------------
|
|
543
557
|
// Style commands (delegated to Style module)
|
|
544
558
|
// ---------------------------------------------------------------------------
|
|
@@ -13,7 +13,11 @@ export class ImageDialog extends BaseDialog {
|
|
|
13
13
|
// Public API
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* @param {{ beforeInsert?: () => void }} [hooks]
|
|
18
|
+
*/
|
|
19
|
+
show({ beforeInsert } = {}) {
|
|
20
|
+
this._beforeInsert = typeof beforeInsert === 'function' ? beforeInsert : null;
|
|
17
21
|
this._saveRange();
|
|
18
22
|
this._urlInput.value = '';
|
|
19
23
|
this._altInput.value = '';
|
|
@@ -137,11 +141,23 @@ export class ImageDialog extends BaseDialog {
|
|
|
137
141
|
return;
|
|
138
142
|
}
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
// Resize/compress via Clipboard's shared canvas pipeline so a file chosen
|
|
145
|
+
// through this dialog gets the same treatment as a pasted/dropped image
|
|
146
|
+
// instead of embedding the original (potentially multi-MB) file untouched.
|
|
147
|
+
this.context.invoke('clipboard.compressAndRegister', file).then((blobUrl) => {
|
|
148
|
+
if (this.context._alive === false) return;
|
|
149
|
+
this._urlInput.value = blobUrl;
|
|
150
|
+
}).catch((err) => {
|
|
151
|
+
if (this.context._alive === false) return;
|
|
152
|
+
// Compression failed (e.g. canvas unavailable) — fall back to embedding
|
|
153
|
+
// the original file directly.
|
|
154
|
+
console.warn('[AutumnNote] ImageDialog: image compression failed, using original file.', err);
|
|
155
|
+
const reader = new FileReader();
|
|
156
|
+
reader.onload = (e) => {
|
|
157
|
+
this._urlInput.value = /** @type {string} */ (e.target.result);
|
|
158
|
+
};
|
|
159
|
+
reader.readAsDataURL(file);
|
|
160
|
+
});
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
_onInsert() {
|
|
@@ -159,7 +175,19 @@ export class ImageDialog extends BaseDialog {
|
|
|
159
175
|
this._savedRange.select();
|
|
160
176
|
}
|
|
161
177
|
|
|
178
|
+
// Let callers make a deferred DOM change only after the user commits the
|
|
179
|
+
// dialog. SlashMenu uses this to keep "/image" intact when the dialog is
|
|
180
|
+
// cancelled and remove it atomically immediately before image insertion.
|
|
181
|
+
const beforeInsert = this._beforeInsert;
|
|
182
|
+
this._beforeInsert = null;
|
|
183
|
+
beforeInsert?.();
|
|
184
|
+
|
|
162
185
|
this.context.invoke('editor.insertImage', src, alt, align);
|
|
163
186
|
this._close();
|
|
164
187
|
}
|
|
188
|
+
|
|
189
|
+
_close() {
|
|
190
|
+
this._beforeInsert = null;
|
|
191
|
+
super._close();
|
|
192
|
+
}
|
|
165
193
|
}
|