autumnnote 1.6.7 → 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.6.7",
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
  /**
@@ -385,6 +388,22 @@ export class Context {
385
388
  this.invoke('editor.clearHistory');
386
389
  }
387
390
 
391
+ /**
392
+ * Returns the number of available undo steps.
393
+ * @returns {number}
394
+ */
395
+ getUndoCount() {
396
+ return this.invoke('editor.getUndoCount') ?? 0;
397
+ }
398
+
399
+ /**
400
+ * Returns the number of available redo steps.
401
+ * @returns {number}
402
+ */
403
+ getRedoCount() {
404
+ return this.invoke('editor.getRedoCount') ?? 0;
405
+ }
406
+
388
407
  /**
389
408
  * Returns true when the editor has no meaningful content.
390
409
  * @returns {boolean}
@@ -513,6 +532,44 @@ export class Context {
513
532
  });
514
533
  }
515
534
 
535
+ /**
536
+ * Returns an array of heading objects representing the table of contents.
537
+ * Each entry has: level (1-6), text (heading text), element (DOM element).
538
+ * @returns {{ level: number, text: string, element: HTMLElement }[]}
539
+ */
540
+ getTableOfContents() {
541
+ const headings = Array.from(
542
+ this.layoutInfo.editable.querySelectorAll('h1,h2,h3,h4,h5,h6')
543
+ );
544
+ return headings.map((el) => ({
545
+ level: parseInt(el.tagName[1], 10),
546
+ text: el.textContent?.trim() ?? '',
547
+ element: /** @type {HTMLElement} */ (el),
548
+ }));
549
+ }
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
+
516
573
  /**
517
574
  * Sets whether the editor is disabled (readonly).
518
575
  * @param {boolean} disabled
@@ -561,15 +618,19 @@ export class Context {
561
618
 
562
619
  const container = this.layoutInfo.container;
563
620
  const wasDark = container?.classList.contains('an-theme-dark');
621
+ const wasAuto = container?.classList.contains('an-theme-auto');
564
622
  if (container?.parentNode) {
565
623
  // Restore original element
566
624
  this.targetEl.style.display = '';
567
625
  container.remove();
568
626
  }
569
- // If this was a dark editor and no other dark containers remain, clean up body
627
+ // Clean up body theme classes if no other editors of that type remain
570
628
  if (wasDark && !document.querySelector('.an-container.an-theme-dark')) {
571
629
  document.body.classList.remove('an-theme-dark');
572
630
  }
631
+ if (wasAuto && !document.querySelector('.an-container.an-theme-auto')) {
632
+ document.body.classList.remove('an-theme-auto');
633
+ }
573
634
 
574
635
  if (typeof this.options.onDestroy === 'function') {
575
636
  this.options.onDestroy(this);
@@ -221,4 +221,14 @@ export class History {
221
221
  canRedo() {
222
222
  return this.stackOffset < this.stack.length - 1;
223
223
  }
224
+
225
+ /** @returns {number} */
226
+ getUndoCount() {
227
+ return Math.max(0, this.stackOffset);
228
+ }
229
+
230
+ /** @returns {number} */
231
+ getRedoCount() {
232
+ return Math.max(0, this.stack.length - 1 - this.stackOffset);
233
+ }
224
234
  }
package/src/js/i18n/de.js CHANGED
@@ -157,6 +157,7 @@ export const de = {
157
157
  replaceAriaLabel: 'Ersetzen durch',
158
158
  replaceBtn: 'Ersetzen',
159
159
  noResults: 'Keine Ergebnisse',
160
+ useRegex: 'Regulären Ausdruck verwenden',
160
161
  replaceAllBtn: 'Alle ersetzen',
161
162
  close: '×',
162
163
  },
@@ -275,6 +276,7 @@ export const de = {
275
276
  disableWordWrap: 'Zeilenumbruch deaktivieren',
276
277
  convertToParagraph: 'In Absatz umwandeln',
277
278
  deleteCodeBlock: 'Codeblock löschen',
279
+ lineNumbers: 'Zeilennummern umschalten',
278
280
  },
279
281
  table: {
280
282
  ariaLabel: 'Tabellen-Aktionen',
@@ -297,6 +299,11 @@ export const de = {
297
299
  tableBorderWidthPx: 'Tabellenrahmenbreite (px)',
298
300
  cancelBtn: 'Abbrechen',
299
301
  applyBtn: 'Anwenden',
302
+ sortAsc: 'Aufsteigend sortieren',
303
+ sortDesc: 'Absteigend sortieren',
304
+ exportCSV: 'Als CSV exportieren',
305
+ cellPadding: 'Zellauffüllung',
306
+ cellPaddingPx: 'Zellauffüllung (px)',
300
307
  },
301
308
  video: {
302
309
  ariaLabel: 'Video-Aktionen',
@@ -318,4 +325,11 @@ export const de = {
318
325
  imageSize: (maxSize) =>
319
326
  `Die Bilddatei ist zu groß. Die maximal zulässige Größe beträgt ${maxSize} MB.`,
320
327
  },
328
+
329
+ autoSaveRestore: {
330
+ found: 'Entwurf gefunden. Wiederherstellen?',
331
+ foundAt: 'Entwurf vom {date}. Wiederherstellen?',
332
+ restore: 'Wiederherstellen',
333
+ discard: 'Verwerfen',
334
+ },
321
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',
@@ -160,9 +161,17 @@ export const en = {
160
161
  replaceBtn: 'Replace',
161
162
  replaceAllBtn: 'Replace All',
162
163
  noResults: 'No results',
164
+ useRegex: 'Use Regular Expression',
163
165
  close: '\u00d7',
164
166
  },
165
167
 
168
+ autoSaveRestore: {
169
+ found: 'Draft found. Restore?',
170
+ foundAt: 'Draft from {date}. Restore?',
171
+ restore: 'Restore',
172
+ discard: 'Discard',
173
+ },
174
+
166
175
  shortcutsDialog: {
167
176
  title: 'Keyboard Shortcuts',
168
177
  ariaLabel: 'Keyboard Shortcuts',
@@ -281,6 +290,7 @@ export const en = {
281
290
  disableWordWrap: 'Disable Word Wrap',
282
291
  convertToParagraph: 'Convert to Paragraph',
283
292
  deleteCodeBlock: 'Delete Code Block',
293
+ lineNumbers: 'Toggle Line Numbers',
284
294
  },
285
295
  table: {
286
296
  ariaLabel: 'Table actions',
@@ -297,14 +307,26 @@ export const en = {
297
307
  columnWidth: 'Column Width',
298
308
  rowHeight: 'Row Height',
299
309
  tableBorderWidth: 'Table Border Width',
310
+ tableBorderColor: 'Table Border Color',
300
311
  deleteTable: 'Delete Table',
312
+ cellAlignLeft: 'Align Left',
313
+ cellAlignCenter: 'Align Center',
314
+ cellAlignRight: 'Align Right',
315
+ cellAlignJustify: 'Align Justify',
316
+ toggleHeaderRow: 'Toggle Header Row',
301
317
  cellBackground: 'Cell Background',
302
318
  noShading: 'No Shading',
319
+ noBorderColor: 'No Border Color',
303
320
  columnWidthPx: 'Column Width (px)',
304
321
  rowHeightPx: 'Row Height (px)',
305
322
  tableBorderWidthPx: 'Table Border Width (px)',
306
323
  cancelBtn: 'Cancel',
307
324
  applyBtn: 'Apply',
325
+ sortAsc: 'Sort Ascending',
326
+ sortDesc: 'Sort Descending',
327
+ exportCSV: 'Export as CSV',
328
+ cellPadding: 'Cell Padding',
329
+ cellPaddingPx: 'Cell Padding (px)',
308
330
  },
309
331
  video: {
310
332
  ariaLabel: 'Video actions',
package/src/js/i18n/es.js CHANGED
@@ -157,6 +157,7 @@ export const es = {
157
157
  replaceAriaLabel: 'Reemplazar con',
158
158
  replaceBtn: 'Reemplazar',
159
159
  noResults: 'Sin resultados',
160
+ useRegex: 'Usar expresión regular',
160
161
  replaceAllBtn: 'Reemplazar todo',
161
162
  close: '×',
162
163
  },
@@ -275,6 +276,7 @@ export const es = {
275
276
  disableWordWrap: 'Desactivar ajuste de línea',
276
277
  convertToParagraph: 'Convertir en párrafo',
277
278
  deleteCodeBlock: 'Eliminar bloque de código',
279
+ lineNumbers: 'Alternar números de línea',
278
280
  },
279
281
  table: {
280
282
  ariaLabel: 'Acciones de tabla',
@@ -297,6 +299,11 @@ export const es = {
297
299
  tableBorderWidthPx: 'Grosor del borde (px)',
298
300
  cancelBtn: 'Cancelar',
299
301
  applyBtn: 'Aplicar',
302
+ sortAsc: 'Ordenar ascendente',
303
+ sortDesc: 'Ordenar descendente',
304
+ exportCSV: 'Exportar como CSV',
305
+ cellPadding: 'Relleno de celda',
306
+ cellPaddingPx: 'Relleno de celda (px)',
300
307
  },
301
308
  video: {
302
309
  ariaLabel: 'Acciones de vídeo',
@@ -318,4 +325,11 @@ export const es = {
318
325
  imageSize: (maxSize) =>
319
326
  `El archivo de imagen es demasiado grande. El tamaño máximo permitido es ${maxSize} MB.`,
320
327
  },
328
+
329
+ autoSaveRestore: {
330
+ found: 'Borrador encontrado. ¿Restaurar?',
331
+ foundAt: 'Borrador del {date}. ¿Restaurar?',
332
+ restore: 'Restaurar',
333
+ discard: 'Descartar',
334
+ },
321
335
  };
package/src/js/i18n/fr.js CHANGED
@@ -158,6 +158,7 @@ export const fr = {
158
158
  replaceAriaLabel: 'Remplacer par',
159
159
  replaceBtn: 'Remplacer',
160
160
  noResults: 'Aucun résultat',
161
+ useRegex: 'Utiliser une expression régulière',
161
162
  replaceAllBtn: 'Tout remplacer',
162
163
  close: '\u00d7',
163
164
  },
@@ -276,6 +277,7 @@ export const fr = {
276
277
  disableWordWrap: 'Désactiver le retour à la ligne',
277
278
  convertToParagraph: 'Convertir en paragraphe',
278
279
  deleteCodeBlock: 'Supprimer le bloc de code',
280
+ lineNumbers: 'Activer/désactiver les numéros de ligne',
279
281
  },
280
282
  table: {
281
283
  ariaLabel: 'Actions du tableau',
@@ -298,6 +300,11 @@ export const fr = {
298
300
  tableBorderWidthPx: 'Épaisseur des bordures (px)',
299
301
  cancelBtn: 'Annuler',
300
302
  applyBtn: 'Appliquer',
303
+ sortAsc: 'Trier par ordre croissant',
304
+ sortDesc: 'Trier par ordre décroissant',
305
+ exportCSV: 'Exporter en CSV',
306
+ cellPadding: 'Rembourrage des cellules',
307
+ cellPaddingPx: 'Rembourrage des cellules (px)',
301
308
  },
302
309
  video: {
303
310
  ariaLabel: 'Actions de la vidéo',
@@ -319,4 +326,11 @@ export const fr = {
319
326
  imageSize: (maxSize) =>
320
327
  `Le fichier image est trop volumineux. La taille maximale autorisée est de ${maxSize}\u00a0Mo.`,
321
328
  },
322
- };
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
@@ -158,6 +158,7 @@ export const ja = {
158
158
  replaceAriaLabel: '置換後のテキスト',
159
159
  replaceBtn: '置換',
160
160
  noResults: '結果なし',
161
+ useRegex: '正規表現を使用',
161
162
  replaceAllBtn: 'すべて置換',
162
163
  close: '\u00d7',
163
164
  },
@@ -276,6 +277,7 @@ export const ja = {
276
277
  disableWordWrap: '折り返しを無効にする',
277
278
  convertToParagraph: '段落に変換',
278
279
  deleteCodeBlock: 'コードブロックを削除',
280
+ lineNumbers: '行番号を切り替え',
279
281
  },
280
282
  table: {
281
283
  ariaLabel: 'テーブル操作',
@@ -298,6 +300,11 @@ export const ja = {
298
300
  tableBorderWidthPx: 'テーブルの枠幅 (px)',
299
301
  cancelBtn: 'キャンセル',
300
302
  applyBtn: '適用',
303
+ sortAsc: '昇順で並べ替え',
304
+ sortDesc: '降順で並べ替え',
305
+ exportCSV: 'CSVとしてエクスポート',
306
+ cellPadding: 'セルの余白',
307
+ cellPaddingPx: 'セルの余白 (px)',
301
308
  },
302
309
  video: {
303
310
  ariaLabel: '動画操作',
@@ -319,4 +326,11 @@ export const ja = {
319
326
  imageSize: (maxSize) =>
320
327
  `画像ファイルが大きすぎます。最大許容サイズは ${maxSize} MB です。`,
321
328
  },
329
+
330
+ autoSaveRestore: {
331
+ found: '下書きが見つかりました。復元しますか?',
332
+ foundAt: '{date} の下書きが見つかりました。復元しますか?',
333
+ restore: '復元',
334
+ discard: '破棄',
335
+ },
322
336
  };
package/src/js/i18n/ko.js CHANGED
@@ -157,6 +157,7 @@ export const ko = {
157
157
  replaceAriaLabel: '바꿀 내용',
158
158
  replaceBtn: '바꾸기',
159
159
  noResults: '결과 없음',
160
+ useRegex: '정규식 사용',
160
161
  replaceAllBtn: '모두 바꾸기',
161
162
  close: '×',
162
163
  },
@@ -275,6 +276,7 @@ export const ko = {
275
276
  disableWordWrap: '줄 바꿈 해제',
276
277
  convertToParagraph: '단락으로 변환',
277
278
  deleteCodeBlock: '코드 블록 삭제',
279
+ lineNumbers: '줄 번호 전환',
278
280
  },
279
281
  table: {
280
282
  ariaLabel: '표 작업',
@@ -297,6 +299,11 @@ export const ko = {
297
299
  tableBorderWidthPx: '표 테두리 너비 (px)',
298
300
  cancelBtn: '취소',
299
301
  applyBtn: '적용',
302
+ sortAsc: '오름차순 정렬',
303
+ sortDesc: '내림차순 정렬',
304
+ exportCSV: 'CSV로 내보내기',
305
+ cellPadding: '셀 패딩',
306
+ cellPaddingPx: '셀 패딩 (px)',
300
307
  },
301
308
  video: {
302
309
  ariaLabel: '동영상 작업',
@@ -318,4 +325,11 @@ export const ko = {
318
325
  imageSize: (maxSize) =>
319
326
  `이미지 파일이 너무 큽니다. 최대 허용 크기는 ${maxSize} MB입니다.`,
320
327
  },
328
+
329
+ autoSaveRestore: {
330
+ found: '초안을 찾았습니다. 복원하시겠습니까?',
331
+ foundAt: '{date}의 초안을 찾았습니다. 복원하시겠습니까?',
332
+ restore: '복원',
333
+ discard: '삭제',
334
+ },
321
335
  };
package/src/js/i18n/vi.js CHANGED
@@ -159,6 +159,7 @@ export const vi = {
159
159
  replaceBtn: 'Thay thế',
160
160
  replaceAllBtn: 'Thay thế tất cả',
161
161
  noResults: 'Không có kết quả',
162
+ useRegex: 'Dùng biểu thức chính quy',
162
163
  close: '\u00d7',
163
164
  },
164
165
 
@@ -276,6 +277,7 @@ export const vi = {
276
277
  disableWordWrap: 'Tắt xuống dòng',
277
278
  convertToParagraph: 'Chuyển thành đoạn văn',
278
279
  deleteCodeBlock: 'Xóa khối mã',
280
+ lineNumbers: 'Bật/Tắt số dòng',
279
281
  },
280
282
  table: {
281
283
  ariaLabel: 'Hành động bảng',
@@ -300,6 +302,11 @@ export const vi = {
300
302
  tableBorderWidthPx: 'Độ rộng viền bảng (px)',
301
303
  cancelBtn: 'Hủy',
302
304
  applyBtn: 'Áp dụng',
305
+ sortAsc: 'Sắp xếp tăng dần',
306
+ sortDesc: 'Sắp xếp giảm dần',
307
+ exportCSV: 'Xuất CSV',
308
+ cellPadding: 'Khoảng đệm ô',
309
+ cellPaddingPx: 'Khoảng đệm ô (px)',
303
310
  },
304
311
  video: {
305
312
  ariaLabel: 'Hành động video',
@@ -321,4 +328,11 @@ export const vi = {
321
328
  imageSize: (maxSize) =>
322
329
  `Tệp hình ảnh quá lớn. Kích thước tối đa cho phép là ${maxSize} MB.`,
323
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
+ },
324
338
  };
package/src/js/i18n/zh.js CHANGED
@@ -158,6 +158,7 @@ export const zh = {
158
158
  replaceAriaLabel: '替换为',
159
159
  replaceBtn: '替换',
160
160
  noResults: '没有结果',
161
+ useRegex: '使用正则表达式',
161
162
  replaceAllBtn: '全部替换',
162
163
  close: '\u00d7',
163
164
  },
@@ -276,6 +277,7 @@ export const zh = {
276
277
  disableWordWrap: '禁用自动换行',
277
278
  convertToParagraph: '转换为段落',
278
279
  deleteCodeBlock: '删除代码块',
280
+ lineNumbers: '切换行号',
279
281
  },
280
282
  table: {
281
283
  ariaLabel: '表格操作',
@@ -298,6 +300,11 @@ export const zh = {
298
300
  tableBorderWidthPx: '表格边框宽度 (px)',
299
301
  cancelBtn: '取消',
300
302
  applyBtn: '应用',
303
+ sortAsc: '升序排列',
304
+ sortDesc: '降序排列',
305
+ exportCSV: '导出为 CSV',
306
+ cellPadding: '单元格内边距',
307
+ cellPaddingPx: '单元格内边距 (px)',
301
308
  },
302
309
  video: {
303
310
  ariaLabel: '视频操作',
@@ -319,4 +326,11 @@ export const zh = {
319
326
  imageSize: (maxSize) =>
320
327
  `图片文件过大。最大允许大小为 ${maxSize} MB。`,
321
328
  },
329
+
330
+ autoSaveRestore: {
331
+ found: '找到草稿。是否恢复?',
332
+ foundAt: '发现 {date} 的草稿。是否恢复?',
333
+ restore: '恢复',
334
+ discard: '放弃',
335
+ },
322
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.6.7',
147
+ version: '1.8.0',
145
148
  };
146
149
 
147
150
  // ---------------------------------------------------------------------------
@@ -19,6 +19,13 @@ export class BaseDialog {
19
19
  this._removeTrap = null;
20
20
  }
21
21
 
22
+ // ---------------------------------------------------------------------------
23
+ // Abstract — subclasses must override
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** @returns {HTMLElement} */
27
+ _buildDialog() { throw new Error('_buildDialog() must be implemented by subclass'); }
28
+
22
29
  // ---------------------------------------------------------------------------
23
30
  // Lifecycle (shared)
24
31
  // ---------------------------------------------------------------------------
@@ -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(
@@ -14,6 +14,7 @@ const ICONS = {
14
14
  wrapOn: `<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"><line x1="3" y1="6" x2="21" y2="6"/><path d="M3 12h15a3 3 0 0 1 0 6H3"/><polyline points="6 15 3 18 6 21"/></svg>`,
15
15
  toParagraph:`<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"><path d="M13 4v16"/><path d="M17 4v16"/><path d="M9 4h8a4 4 0 0 1 0 8H9V4z"/></svg>`,
16
16
  deleteCode: `<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"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
17
+ lineNumbers: `<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"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-2-2-2"/></svg>`,
17
18
  };
18
19
 
19
20
  export class CodeTooltip {
@@ -26,6 +27,8 @@ export class CodeTooltip {
26
27
  this._hideTimer = null;
27
28
  this._disposers = [];
28
29
  this._copyBtn = null;
30
+ this._wrapBtn = null;
31
+ this._lineNumbersBtn = null;
29
32
  this._langSelect = null;
30
33
  this._prismScript = null; // script element while Prism is loading
31
34
  }
@@ -127,6 +130,10 @@ export class CodeTooltip {
127
130
  this._wrapBtn = this._makeBtn(ICONS.wrapOn, L.toggleWordWrap, () => this._toggleWrap());
128
131
  el.appendChild(this._wrapBtn);
129
132
 
133
+ // Toggle line numbers
134
+ this._lineNumbersBtn = this._makeBtn(ICONS.lineNumbers, L.lineNumbers, () => this._toggleLineNumbers());
135
+ el.appendChild(this._lineNumbersBtn);
136
+
130
137
  el.appendChild(this._sep());
131
138
 
132
139
  // Convert to normal paragraph
@@ -183,6 +190,7 @@ export class CodeTooltip {
183
190
  this._showTimer = setTimeout(() => {
184
191
  this._activePre = pre;
185
192
  this._syncWrapBtn();
193
+ this._syncLineNumbersBtn();
186
194
  this._syncLangSelect();
187
195
  this._show(pre);
188
196
  }, SHOW_DELAY);
@@ -248,6 +256,13 @@ export class CodeTooltip {
248
256
  : this.context.locale.tooltips.code.enableWordWrap;
249
257
  }
250
258
 
259
+ _syncLineNumbersBtn() {
260
+ if (!this._activePre || !this._lineNumbersBtn) return;
261
+ const on = this._activePre.classList.contains('an-code-line-numbers');
262
+ this._lineNumbersBtn.classList.toggle('active', on);
263
+ this._lineNumbersBtn.title = this.context.locale.tooltips.code.lineNumbers;
264
+ }
265
+
251
266
  _syncLangSelect() {
252
267
  if (!this._activePre || !this._langSelect) return;
253
268
  const codeEl = this._activePre.querySelector('code');
@@ -301,6 +316,14 @@ export class CodeTooltip {
301
316
  this._positionNear(pre);
302
317
  }
303
318
 
319
+ _toggleLineNumbers() {
320
+ const pre = this._activePre;
321
+ if (!pre) return;
322
+ pre.classList.toggle('an-code-line-numbers');
323
+ this._syncLineNumbersBtn();
324
+ this.context.invoke('editor.afterCommand');
325
+ }
326
+
304
327
  /**
305
328
  * Applies a language to a given <pre> element: sets classes, data-language,
306
329
  * and triggers Prism highlighting. Called by the auto-detect flow.
@@ -525,6 +525,14 @@ export class Editor {
525
525
  return this._history ? this._history.canRedo() : false;
526
526
  }
527
527
 
528
+ getUndoCount() {
529
+ return this._history ? this._history.getUndoCount() : 0;
530
+ }
531
+
532
+ getRedoCount() {
533
+ return this._history ? this._history.getRedoCount() : 0;
534
+ }
535
+
528
536
  // ---------------------------------------------------------------------------
529
537
  // Style commands (delegated to Style module)
530
538
  // ---------------------------------------------------------------------------