autumnnote 1.7.0 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autumnnote",
3
- "version": "1.7.0",
3
+ "version": "1.8.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
  "main": "dist/autumnnote.umd.js",
6
6
  "module": "dist/autumnnote.es.js",
package/src/js/Context.js CHANGED
@@ -339,10 +339,13 @@ export class Context {
339
339
 
340
340
  /**
341
341
  * Returns the current HTML content of the editor.
342
+ * Zero-width spaces (U+200B) inserted by inline editing helpers are stripped
343
+ * from the output so they don't leak into the consumer's HTML.
342
344
  * @returns {string}
343
345
  */
344
346
  getHTML() {
345
- return this.invoke('editor.getHTML');
347
+ const html = this.invoke('editor.getHTML');
348
+ return typeof html === 'string' ? html.replace(/​/g, '') : html;
346
349
  }
347
350
 
348
351
  /**
@@ -545,6 +548,28 @@ export class Context {
545
548
  }));
546
549
  }
547
550
 
551
+ /**
552
+ * Moves focus into the editable area.
553
+ */
554
+ focus() {
555
+ this.layoutInfo.editable.focus();
556
+ }
557
+
558
+ /**
559
+ * Removes focus from the editable area.
560
+ */
561
+ blur() {
562
+ this.layoutInfo.editable.blur();
563
+ }
564
+
565
+ /**
566
+ * Returns true when the editor is currently in fullscreen mode.
567
+ * @returns {boolean}
568
+ */
569
+ isFullscreen() {
570
+ return this.invoke('fullscreen.isActive') === true;
571
+ }
572
+
548
573
  /**
549
574
  * Sets whether the editor is disabled (readonly).
550
575
  * @param {boolean} disabled
package/src/js/i18n/de.js CHANGED
@@ -325,4 +325,11 @@ export const de = {
325
325
  imageSize: (maxSize) =>
326
326
  `Die Bilddatei ist zu groß. Die maximal zulässige Größe beträgt ${maxSize} MB.`,
327
327
  },
328
+
329
+ autoSaveRestore: {
330
+ found: 'Entwurf gefunden. Wiederherstellen?',
331
+ foundAt: 'Entwurf vom {date}. Wiederherstellen?',
332
+ restore: 'Wiederherstellen',
333
+ discard: 'Verwerfen',
334
+ },
328
335
  };
package/src/js/i18n/en.js CHANGED
@@ -153,6 +153,7 @@ export const en = {
153
153
  findPlaceholder: 'Find\u2026',
154
154
  searchAriaLabel: 'Search text',
155
155
  caseSensitive: '\u00a0Case sensitive',
156
+ wholeWord: 'Whole Word',
156
157
  prevBtn: '\u2190 Prev',
157
158
  nextBtn: 'Next \u2192',
158
159
  replacePlaceholder: 'Replace with\u2026',
@@ -164,6 +165,13 @@ export const en = {
164
165
  close: '\u00d7',
165
166
  },
166
167
 
168
+ autoSaveRestore: {
169
+ found: 'Draft found. Restore?',
170
+ foundAt: 'Draft from {date}. Restore?',
171
+ restore: 'Restore',
172
+ discard: 'Discard',
173
+ },
174
+
167
175
  shortcutsDialog: {
168
176
  title: 'Keyboard Shortcuts',
169
177
  ariaLabel: 'Keyboard Shortcuts',
package/src/js/i18n/es.js CHANGED
@@ -325,4 +325,11 @@ export const es = {
325
325
  imageSize: (maxSize) =>
326
326
  `El archivo de imagen es demasiado grande. El tamaño máximo permitido es ${maxSize} MB.`,
327
327
  },
328
+
329
+ autoSaveRestore: {
330
+ found: 'Borrador encontrado. ¿Restaurar?',
331
+ foundAt: 'Borrador del {date}. ¿Restaurar?',
332
+ restore: 'Restaurar',
333
+ discard: 'Descartar',
334
+ },
328
335
  };
package/src/js/i18n/fr.js CHANGED
@@ -326,4 +326,11 @@ export const fr = {
326
326
  imageSize: (maxSize) =>
327
327
  `Le fichier image est trop volumineux. La taille maximale autorisée est de ${maxSize}\u00a0Mo.`,
328
328
  },
329
- };
329
+
330
+ autoSaveRestore: {
331
+ found: 'Brouillon trouvé. Restaurer ?',
332
+ foundAt: 'Brouillon du {date}. Restaurer ?',
333
+ restore: 'Restaurer',
334
+ discard: 'Ignorer',
335
+ },
336
+ };
package/src/js/i18n/ja.js CHANGED
@@ -326,4 +326,11 @@ export const ja = {
326
326
  imageSize: (maxSize) =>
327
327
  `画像ファイルが大きすぎます。最大許容サイズは ${maxSize} MB です。`,
328
328
  },
329
+
330
+ autoSaveRestore: {
331
+ found: '下書きが見つかりました。復元しますか?',
332
+ foundAt: '{date} の下書きが見つかりました。復元しますか?',
333
+ restore: '復元',
334
+ discard: '破棄',
335
+ },
329
336
  };
package/src/js/i18n/ko.js CHANGED
@@ -325,4 +325,11 @@ export const ko = {
325
325
  imageSize: (maxSize) =>
326
326
  `이미지 파일이 너무 큽니다. 최대 허용 크기는 ${maxSize} MB입니다.`,
327
327
  },
328
+
329
+ autoSaveRestore: {
330
+ found: '초안을 찾았습니다. 복원하시겠습니까?',
331
+ foundAt: '{date}의 초안을 찾았습니다. 복원하시겠습니까?',
332
+ restore: '복원',
333
+ discard: '삭제',
334
+ },
328
335
  };
package/src/js/i18n/vi.js CHANGED
@@ -328,4 +328,11 @@ export const vi = {
328
328
  imageSize: (maxSize) =>
329
329
  `Tệp hình ảnh quá lớn. Kích thước tối đa cho phép là ${maxSize} MB.`,
330
330
  },
331
+
332
+ autoSaveRestore: {
333
+ found: 'Tìm thấy bản nháp. Khôi phục?',
334
+ foundAt: 'Bản nháp từ {date}. Khôi phục?',
335
+ restore: 'Khôi phục',
336
+ discard: 'Bỏ qua',
337
+ },
331
338
  };
package/src/js/i18n/zh.js CHANGED
@@ -326,4 +326,11 @@ export const zh = {
326
326
  imageSize: (maxSize) =>
327
327
  `图片文件过大。最大允许大小为 ${maxSize} MB。`,
328
328
  },
329
+
330
+ autoSaveRestore: {
331
+ found: '找到草稿。是否恢复?',
332
+ foundAt: '发现 {date} 的草稿。是否恢复?',
333
+ restore: '恢复',
334
+ discard: '放弃',
335
+ },
329
336
  };
package/src/js/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // @ts-ignore
13
13
  import '../styles/autumnnote.scss';
14
14
  import { Context, _customModules, _globalPlugins } from './Context.js';
15
- import { registerButton } from './module/Buttons.js';
15
+ import { registerButton, buttons } from './module/Buttons.js';
16
16
  import { defaultOptions } from './settings.js';
17
17
 
18
18
  // Snapshot of factory defaults taken at module-load time (before any setDefaults() calls)
@@ -140,8 +140,11 @@ const AutumnNote = {
140
140
  */
141
141
  registerButton(btnDef) { registerButton(btnDef); return this; },
142
142
 
143
+ /** All pre-built button definitions — accessible in every module format including UMD/CJS. */
144
+ buttons,
145
+
143
146
  /** Library version */
144
- version: '1.7.0',
147
+ version: '1.8.0',
145
148
  };
146
149
 
147
150
  // ---------------------------------------------------------------------------
@@ -348,3 +348,52 @@ export const defaultToolbar = [
348
348
  [hrBtn, linkBtn, imageBtn, videoBtn, tableBtn, emojiBtn, iconBtn],
349
349
  [removeFormatBtn, codeviewBtn, fullscreenBtn, findBtn, printBtn, shortcutsBtn],
350
350
  ];
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // Buttons namespace — all pre-built button definitions in a single object.
354
+ // Exposed as AutumnNote.buttons so UMD / CJS consumers can access them
355
+ // without named imports: AutumnNote.buttons.boldBtn, etc.
356
+ // ---------------------------------------------------------------------------
357
+
358
+ export const buttons = {
359
+ boldBtn,
360
+ italicBtn,
361
+ underlineBtn,
362
+ strikeBtn,
363
+ superscriptBtn,
364
+ subscriptBtn,
365
+ alignLeftBtn,
366
+ alignCenterBtn,
367
+ alignRightBtn,
368
+ alignJustifyBtn,
369
+ ulBtn,
370
+ olBtn,
371
+ indentBtn,
372
+ outdentBtn,
373
+ undoBtn,
374
+ redoBtn,
375
+ hrBtn,
376
+ linkBtn,
377
+ imageBtn,
378
+ videoBtn,
379
+ emojiBtn,
380
+ iconBtn,
381
+ tableBtn,
382
+ fontSizeBtn,
383
+ removeFormatBtn,
384
+ directionBtn,
385
+ fontFamilyBtn,
386
+ paragraphStyleBtn,
387
+ lineHeightBtn,
388
+ codeviewBtn,
389
+ fullscreenBtn,
390
+ shortcutsBtn,
391
+ findBtn,
392
+ findReplaceBtn,
393
+ inlineCodeBtn,
394
+ checklistBtn,
395
+ printBtn,
396
+ foreColorBtn,
397
+ backColorBtn,
398
+ defaultToolbar,
399
+ };
@@ -187,6 +187,18 @@ export class Clipboard {
187
187
  const forcePlain = this._forcePlain;
188
188
  this._forcePlain = false;
189
189
 
190
+ // Enforce maxPasteSize limit (default 5 MB)
191
+ const maxBytes = (this.options.maxPasteSize ?? 5) * 1024 * 1024;
192
+ if (maxBytes > 0) {
193
+ const text = clipboardData.getData('text/plain') || '';
194
+ const html = clipboardData.getData('text/html') || '';
195
+ const size = Math.max(text.length, html.length);
196
+ if (size > maxBytes) {
197
+ event.preventDefault();
198
+ return;
199
+ }
200
+ }
201
+
190
202
  // 1. Image file in clipboard (screenshot, copy-image-from-browser, etc.)
191
203
  if (clipboardData.items) {
192
204
  const imageItems = Array.from(clipboardData.items).filter(
@@ -31,6 +31,7 @@ export class FindReplace extends BaseDialog {
31
31
  this._currentIndex = -1;
32
32
  this._caseSensitive = false;
33
33
  this._useRegex = false;
34
+ this._wholeWord = false;
34
35
  /** @type {'find'|'replace'} */
35
36
  this._mode = 'find';
36
37
 
@@ -39,6 +40,7 @@ export class FindReplace extends BaseDialog {
39
40
  this._lastQuery = null;
40
41
  this._lastCaseSensitive = null;
41
42
  this._lastUseRegex = null;
43
+ this._lastWholeWord = null;
42
44
 
43
45
  this._focusTimer = null;
44
46
  }
@@ -179,6 +181,14 @@ export class FindReplace extends BaseDialog {
179
181
  });
180
182
  regexBtn.textContent = '.*';
181
183
 
184
+ const wholeWordBtn = createElement('button', {
185
+ type: 'button',
186
+ class: 'an-fr-icon-btn',
187
+ title: L.wholeWord,
188
+ 'aria-label': L.wholeWord,
189
+ });
190
+ wholeWordBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="18" height="10" rx="2"/><line x1="7" y1="21" x2="7" y2="17"/><line x1="17" y1="21" x2="17" y2="17"/></svg>`;
191
+
182
192
  const prevBtn = createElement('button', {
183
193
  type: 'button',
184
194
  class: 'an-fr-icon-btn',
@@ -198,7 +208,7 @@ export class FindReplace extends BaseDialog {
198
208
  const counter = createElement('span', { class: 'an-fr-counter' });
199
209
  this._counterEl = counter;
200
210
 
201
- searchBar.append(findInput, caseCheckbox, caseBtn, regexBtn, prevBtn, nextBtn, counter);
211
+ searchBar.append(findInput, caseCheckbox, caseBtn, regexBtn, wholeWordBtn, prevBtn, nextBtn, counter);
202
212
  box.appendChild(searchBar);
203
213
 
204
214
  // ---- Replace row: [input] [Replace] [All] (hidden by default) ----
@@ -266,7 +276,14 @@ export class FindReplace extends BaseDialog {
266
276
  this._lastQuery = null;
267
277
  this._onSearch();
268
278
  });
269
- this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, dRegex);
279
+ const dWholeWord = on(wholeWordBtn, 'click', () => {
280
+ this._wholeWord = !this._wholeWord;
281
+ wholeWordBtn.classList.toggle('an-fr-icon-btn--active', this._wholeWord);
282
+ this._queryRegex = null;
283
+ this._lastQuery = null;
284
+ this._onSearch();
285
+ });
286
+ this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, dRegex, dWholeWord);
270
287
 
271
288
  return overlay;
272
289
  }
@@ -341,13 +358,14 @@ export class FindReplace extends BaseDialog {
341
358
  */
342
359
  _findRawMatches(query, root) {
343
360
  const results = [];
344
- // Reuse compiled regex when query, case-sensitivity, and regex mode haven't changed
345
- if (this._lastQuery !== query || this._lastCaseSensitive !== this._caseSensitive || this._lastUseRegex !== this._useRegex) {
361
+ // Reuse compiled regex when query, case-sensitivity, regex mode, and whole-word haven't changed
362
+ if (this._lastQuery !== query || this._lastCaseSensitive !== this._caseSensitive || this._lastUseRegex !== this._useRegex || this._lastWholeWord !== this._wholeWord) {
346
363
  const flags = this._caseSensitive ? 'g' : 'gi';
347
364
  try {
348
- const pattern = this._useRegex
365
+ let pattern = this._useRegex
349
366
  ? query
350
367
  : query.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
368
+ if (this._wholeWord) pattern = `\\b${pattern}\\b`;
351
369
  this._queryRegex = new RegExp(pattern, flags);
352
370
  } catch (_) {
353
371
  this._queryRegex = null;
@@ -355,6 +373,7 @@ export class FindReplace extends BaseDialog {
355
373
  this._lastQuery = query;
356
374
  this._lastCaseSensitive = this._caseSensitive;
357
375
  this._lastUseRegex = this._useRegex;
376
+ this._lastWholeWord = this._wholeWord;
358
377
  }
359
378
  if (!this._queryRegex) return results;
360
379
  const re = this._queryRegex;
@@ -204,6 +204,7 @@ export class ImageResizer {
204
204
  const isCorner = pos.length === 2; // 'nw','ne','se','sw'
205
205
 
206
206
  const editable = this.context.layoutInfo.editable;
207
+ const minSz = this.context.options?.minImageSize ?? 20;
207
208
  let _raf = null; // rAF handle — ensures at most one write per paint frame
208
209
 
209
210
  const onMove = (me) => {
@@ -218,19 +219,19 @@ export class ImageResizer {
218
219
  let newW = startW;
219
220
  let newH = startH;
220
221
 
221
- if (pos.includes('e')) newW = Math.max(20, startW + dx);
222
- if (pos.includes('w')) newW = Math.max(20, startW - dx);
223
- if (pos.includes('s')) newH = Math.max(20, startH + dy);
224
- if (pos.includes('n')) newH = Math.max(20, startH - dy);
222
+ if (pos.includes('e')) newW = Math.max(minSz, startW + dx);
223
+ if (pos.includes('w')) newW = Math.max(minSz, startW - dx);
224
+ if (pos.includes('s')) newH = Math.max(minSz, startH + dy);
225
+ if (pos.includes('n')) newH = Math.max(minSz, startH - dy);
225
226
 
226
227
  newW = Math.min(newW, maxW);
227
228
 
228
229
  if (isCorner) {
229
230
  if (Math.abs(dx) >= Math.abs(dy)) {
230
- newH = Math.max(20, Math.round(newW / aspectRatio));
231
+ newH = Math.max(minSz, Math.round(newW / aspectRatio));
231
232
  } else {
232
- newW = Math.min(Math.max(20, Math.round(newH * aspectRatio)), maxW);
233
- newH = Math.max(20, Math.round(newW / aspectRatio));
233
+ newW = Math.min(Math.max(minSz, Math.round(newH * aspectRatio)), maxW);
234
+ newH = Math.max(minSz, Math.round(newW / aspectRatio));
234
235
  }
235
236
  }
236
237
 
@@ -279,7 +279,10 @@ export class Mention {
279
279
  };
280
280
  const result = this._cfg.onSearch(this._query, cb);
281
281
  if (result && typeof result.then === 'function') {
282
- result.then(cb).catch(() => this._hideDropdown());
282
+ result.then(cb).catch((err) => {
283
+ this._hideDropdown();
284
+ if (typeof this._cfg.onError === 'function') this._cfg.onError(err);
285
+ });
283
286
  }
284
287
  }, this._cfg.debounce);
285
288
  }
package/types/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * AutumnNote – TypeScript declarations
3
- * @version 1.7.0
3
+ * @version 1.8.0
4
4
  */
5
5
 
6
6
  // ---------------------------------------------------------------------------
@@ -119,6 +119,10 @@ export interface AsnOptions {
119
119
  autoSaveRestoreTimeout?: number;
120
120
  /** Callback fired after the user chooses to restore a draft. */
121
121
  onAutoSaveRestore?: (html: string, context: Context) => void;
122
+ /** Maximum paste size in MB before paste is silently dropped (0 = unlimited). Default: 5. */
123
+ maxPasteSize?: number;
124
+ /** Minimum image dimension in px during resize (width and height). Default: 20. */
125
+ minImageSize?: number;
122
126
 
123
127
  // ---- Markdown input shortcuts (#3) ----------------------------------------
124
128
 
@@ -165,6 +169,8 @@ export interface MentionOptions {
165
169
  * - Promise: `async onSearch(query) { return items; }`
166
170
  */
167
171
  onSearch: (query: string, callback: (items: MentionItem[]) => void) => void | Promise<MentionItem[]>;
172
+ /** Called when onSearch rejects with an error (Promise style only). */
173
+ onError?: (error: unknown) => void;
168
174
  /** Optional. Return custom HTML string for the inserted chip, or null to use the default. */
169
175
  onInsert?: (item: MentionItem) => string | null;
170
176
  /** CSS class added to the inserted mention chip. Default: 'an-mention'. */
@@ -309,6 +315,12 @@ export interface AsnLocale {
309
315
  imageFormat: (type: string) => string;
310
316
  imageSize: (maxMb: number) => string;
311
317
  };
318
+ autoSaveRestore: {
319
+ found: string;
320
+ foundAt: string;
321
+ restore: string;
322
+ discard: string;
323
+ };
312
324
  }
313
325
 
314
326
  /** Registry of all built-in locales (keys: 'en', 'vi', 'ja', 'zh', 'fr'). */
@@ -423,6 +435,18 @@ export declare class Context {
423
435
  /** Downloads the editor content as a Markdown file. */
424
436
  downloadMarkdown(filename?: string): void;
425
437
 
438
+ /** Moves keyboard focus into the editable area. */
439
+ focus(): void;
440
+
441
+ /** Removes keyboard focus from the editable area. */
442
+ blur(): void;
443
+
444
+ /** Returns true when the editor is currently in fullscreen mode. */
445
+ isFullscreen(): boolean;
446
+
447
+ /** Opens a print dialog with the editor content. */
448
+ print(title?: string): void;
449
+
426
450
  /** Enables or disables (read-only) mode on this instance. */
427
451
  setDisabled(disabled: boolean): void;
428
452
 
@@ -494,6 +518,56 @@ export declare const foreColorBtn: ColorPickerDef;
494
518
  export declare const backColorBtn: ColorPickerDef;
495
519
  export declare const defaultToolbar: Array<Array<ToolbarItemDef>>;
496
520
 
521
+ /**
522
+ * All pre-built button definitions grouped in a single namespace object.
523
+ * Use this to access individual buttons in UMD / CJS environments where
524
+ * named imports are not available: `AutumnNote.buttons.boldBtn`.
525
+ */
526
+ export interface ButtonsNamespace {
527
+ boldBtn: ButtonDef;
528
+ italicBtn: ButtonDef;
529
+ underlineBtn: ButtonDef;
530
+ strikeBtn: ButtonDef;
531
+ superscriptBtn: ButtonDef;
532
+ subscriptBtn: ButtonDef;
533
+ alignLeftBtn: ButtonDef;
534
+ alignCenterBtn: ButtonDef;
535
+ alignRightBtn: ButtonDef;
536
+ alignJustifyBtn: ButtonDef;
537
+ ulBtn: ButtonDef;
538
+ olBtn: ButtonDef;
539
+ indentBtn: ButtonDef;
540
+ outdentBtn: ButtonDef;
541
+ undoBtn: ButtonDef;
542
+ redoBtn: ButtonDef;
543
+ hrBtn: ButtonDef;
544
+ linkBtn: ButtonDef;
545
+ imageBtn: ButtonDef;
546
+ videoBtn: ButtonDef;
547
+ emojiBtn: ButtonDef;
548
+ iconBtn: ButtonDef;
549
+ tableBtn: GridButtonDef;
550
+ fontSizeBtn: DropdownDef;
551
+ removeFormatBtn: ButtonDef;
552
+ directionBtn: ButtonDef;
553
+ fontFamilyBtn: DropdownDef;
554
+ paragraphStyleBtn: DropdownDef;
555
+ lineHeightBtn: DropdownDef;
556
+ codeviewBtn: ButtonDef;
557
+ fullscreenBtn: ButtonDef;
558
+ shortcutsBtn: ButtonDef;
559
+ findBtn: ButtonDef;
560
+ findReplaceBtn: ButtonDef;
561
+ inlineCodeBtn: ButtonDef;
562
+ checklistBtn: ButtonDef;
563
+ printBtn: ButtonDef;
564
+ foreColorBtn: ColorPickerDef;
565
+ backColorBtn: ColorPickerDef;
566
+ defaultToolbar: Array<Array<ToolbarItemDef>>;
567
+ }
568
+
569
+ export declare const buttons: ButtonsNamespace;
570
+
497
571
  // ---------------------------------------------------------------------------
498
572
  // Main AutumnNote API
499
573
  // ---------------------------------------------------------------------------
@@ -543,6 +617,13 @@ export interface AutumnNoteStatic {
543
617
  */
544
618
  registerButton(btnDef: ToolbarItemDef): this;
545
619
 
620
+ /**
621
+ * All pre-built button definitions in a single namespace.
622
+ * Use in UMD / CJS builds where named imports are unavailable:
623
+ * `AutumnNote.buttons.boldBtn`, `AutumnNote.buttons.defaultToolbar`, etc.
624
+ */
625
+ readonly buttons: ButtonsNamespace;
626
+
546
627
  /** Library version string. */
547
628
  readonly version: string;
548
629
  }