autumnnote 1.7.0 → 1.8.1

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.
@@ -17,6 +17,25 @@ const _FA_PATTERN = /\bfa-/;
17
17
  const isFAIcon = (n) => !!(n?.nodeName === 'I' && _FA_PATTERN.test(n.className || ''));
18
18
  const isZwsAnchor = (n) => !!(n?.nodeType === Node.TEXT_NODE && (n.textContent === '\u200B' || n.textContent === ''));
19
19
 
20
+ /**
21
+ * Extracts the content from `startContainer:startOffset` to the end of `li`.
22
+ * Returns an empty fragment if the range is invalid (e.g. detached node).
23
+ * @param {Range} nativeRange
24
+ * @param {Element} li
25
+ * @returns {DocumentFragment}
26
+ */
27
+ function extractAfterContent(nativeRange, li) {
28
+ try {
29
+ const r = document.createRange();
30
+ r.setStart(nativeRange.startContainer, nativeRange.startOffset);
31
+ r.setEnd(li, li.childNodes.length);
32
+ return r.extractContents();
33
+ } catch (_) {
34
+ void _;
35
+ return document.createDocumentFragment();
36
+ }
37
+ }
38
+
20
39
  /**
21
40
  * Handles special keydown behaviour inside the editor.
22
41
  * @param {KeyboardEvent} event
@@ -312,16 +331,14 @@ export function handleKeydown(event, editable, options = {}) {
312
331
  if (!nativeRange.collapsed) {
313
332
  nativeRange.deleteContents();
314
333
  // nativeRange is now collapsed at the deletion point; re-read it
334
+ if (sel.rangeCount === 0 || !checkLi.isConnected) return true;
315
335
  nativeRange = sel.getRangeAt(0);
316
336
  }
317
337
 
318
338
  // 3. Extract everything from cursor to end of li into afterFrag.
319
339
  // Use startContainer/startOffset (cursor position after potential delete),
320
340
  // NOT endContainer/endOffset which is wrong for non-collapsed ranges.
321
- const afterRange = document.createRange();
322
- afterRange.setStart(nativeRange.startContainer, nativeRange.startOffset);
323
- afterRange.setEnd(checkLi, checkLi.childNodes.length);
324
- const afterFrag = afterRange.extractContents();
341
+ const afterFrag = extractAfterContent(nativeRange, checkLi);
325
342
 
326
343
  // 4. Build the new checklist item with the extracted "after" content.
327
344
  const newLi = document.createElement('li');
package/src/js/i18n/de.js CHANGED
@@ -151,6 +151,7 @@ export const de = {
151
151
  findPlaceholder: 'Suchen…',
152
152
  searchAriaLabel: 'Suchtext',
153
153
  caseSensitive: '\u00a0Groß-/Kleinschreibung',
154
+ wholeWord: 'Ganzes Wort',
154
155
  prevBtn: '← Zurück',
155
156
  nextBtn: 'Weiter →',
156
157
  replacePlaceholder: 'Ersetzen durch…',
@@ -293,7 +294,16 @@ export const de = {
293
294
  columnWidth: 'Spaltenbreite',
294
295
  rowHeight: 'Zeilenhöhe',
295
296
  tableBorderWidth: 'Tabellenrahmenbreite',
297
+ tableBorderColor: 'Tabellenrahmenfarbe',
296
298
  deleteTable: 'Tabelle löschen',
299
+ cellAlignLeft: 'Linksbündig',
300
+ cellAlignCenter: 'Zentrieren',
301
+ cellAlignRight: 'Rechtsbündig',
302
+ cellAlignJustify: 'Blocksatz',
303
+ toggleHeaderRow: 'Kopfzeile umschalten',
304
+ cellBackground: 'Zellhintergrund',
305
+ noShading: 'Kein Hintergrund',
306
+ noBorderColor: 'Keine Rahmenfarbe',
297
307
  columnWidthPx: 'Spaltenbreite (px)',
298
308
  rowHeightPx: 'Zeilenhöhe (px)',
299
309
  tableBorderWidthPx: 'Tabellenrahmenbreite (px)',
@@ -325,4 +335,11 @@ export const de = {
325
335
  imageSize: (maxSize) =>
326
336
  `Die Bilddatei ist zu groß. Die maximal zulässige Größe beträgt ${maxSize} MB.`,
327
337
  },
338
+
339
+ autoSaveRestore: {
340
+ found: 'Entwurf gefunden. Wiederherstellen?',
341
+ foundAt: 'Entwurf vom {date}. Wiederherstellen?',
342
+ restore: 'Wiederherstellen',
343
+ discard: 'Verwerfen',
344
+ },
328
345
  };
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
@@ -151,6 +151,7 @@ export const es = {
151
151
  findPlaceholder: 'Buscar…',
152
152
  searchAriaLabel: 'Texto de búsqueda',
153
153
  caseSensitive: '\u00a0Distinguir mayúsculas',
154
+ wholeWord: 'Palabra completa',
154
155
  prevBtn: '← Anterior',
155
156
  nextBtn: 'Siguiente →',
156
157
  replacePlaceholder: 'Reemplazar con…',
@@ -293,7 +294,16 @@ export const es = {
293
294
  columnWidth: 'Ancho de columna',
294
295
  rowHeight: 'Alto de fila',
295
296
  tableBorderWidth: 'Grosor del borde de la tabla',
297
+ tableBorderColor: 'Color del borde de la tabla',
296
298
  deleteTable: 'Eliminar tabla',
299
+ cellAlignLeft: 'Alinear a la izquierda',
300
+ cellAlignCenter: 'Centrar',
301
+ cellAlignRight: 'Alinear a la derecha',
302
+ cellAlignJustify: 'Justificar',
303
+ toggleHeaderRow: 'Activar fila de encabezado',
304
+ cellBackground: 'Fondo de celda',
305
+ noShading: 'Sin fondo',
306
+ noBorderColor: 'Sin color de borde',
297
307
  columnWidthPx: 'Ancho de columna (px)',
298
308
  rowHeightPx: 'Alto de fila (px)',
299
309
  tableBorderWidthPx: 'Grosor del borde (px)',
@@ -325,4 +335,11 @@ export const es = {
325
335
  imageSize: (maxSize) =>
326
336
  `El archivo de imagen es demasiado grande. El tamaño máximo permitido es ${maxSize} MB.`,
327
337
  },
338
+
339
+ autoSaveRestore: {
340
+ found: 'Borrador encontrado. ¿Restaurar?',
341
+ foundAt: 'Borrador del {date}. ¿Restaurar?',
342
+ restore: 'Restaurar',
343
+ discard: 'Descartar',
344
+ },
328
345
  };
package/src/js/i18n/fr.js CHANGED
@@ -152,6 +152,7 @@ export const fr = {
152
152
  findPlaceholder: 'Rechercher\u2026',
153
153
  searchAriaLabel: 'Texte à rechercher',
154
154
  caseSensitive: '\u00a0Respecter la casse',
155
+ wholeWord: 'Mot entier',
155
156
  prevBtn: '\u2190 Préc.',
156
157
  nextBtn: 'Suiv. \u2192',
157
158
  replacePlaceholder: 'Remplacer par\u2026',
@@ -294,7 +295,16 @@ export const fr = {
294
295
  columnWidth: 'Largeur de colonne',
295
296
  rowHeight: 'Hauteur de ligne',
296
297
  tableBorderWidth: 'Épaisseur des bordures',
298
+ tableBorderColor: 'Couleur de bordure',
297
299
  deleteTable: 'Supprimer le tableau',
300
+ cellAlignLeft: 'Aligner \u00e0 gauche',
301
+ cellAlignCenter: 'Centrer',
302
+ cellAlignRight: 'Aligner \u00e0 droite',
303
+ cellAlignJustify: 'Justifier',
304
+ toggleHeaderRow: 'Ligne d\'en-t\u00eate',
305
+ cellBackground: 'Couleur de fond',
306
+ noShading: 'Aucun fond',
307
+ noBorderColor: 'Aucune bordure',
298
308
  columnWidthPx: 'Largeur de colonne (px)',
299
309
  rowHeightPx: 'Hauteur de ligne (px)',
300
310
  tableBorderWidthPx: 'Épaisseur des bordures (px)',
@@ -326,4 +336,11 @@ export const fr = {
326
336
  imageSize: (maxSize) =>
327
337
  `Le fichier image est trop volumineux. La taille maximale autorisée est de ${maxSize}\u00a0Mo.`,
328
338
  },
329
- };
339
+
340
+ autoSaveRestore: {
341
+ found: 'Brouillon trouvé. Restaurer ?',
342
+ foundAt: 'Brouillon du {date}. Restaurer ?',
343
+ restore: 'Restaurer',
344
+ discard: 'Ignorer',
345
+ },
346
+ };
package/src/js/i18n/ja.js CHANGED
@@ -152,6 +152,7 @@ export const ja = {
152
152
  findPlaceholder: '検索\u2026',
153
153
  searchAriaLabel: '検索テキスト',
154
154
  caseSensitive: '\u00a0大文字/小文字を区別',
155
+ wholeWord: '単語単位',
155
156
  prevBtn: '\u2190 前へ',
156
157
  nextBtn: '次へ \u2192',
157
158
  replacePlaceholder: '置換後\u2026',
@@ -294,7 +295,16 @@ export const ja = {
294
295
  columnWidth: '列幅',
295
296
  rowHeight: '行の高さ',
296
297
  tableBorderWidth: 'テーブルの枠幅',
298
+ tableBorderColor: 'テーブルの枠線色',
297
299
  deleteTable: 'テーブルを削除',
300
+ cellAlignLeft: '左揃え',
301
+ cellAlignCenter: '中央揃え',
302
+ cellAlignRight: '右揃え',
303
+ cellAlignJustify: '両端揃え',
304
+ toggleHeaderRow: 'ヘッダー行の切り替え',
305
+ cellBackground: 'セル背景色',
306
+ noShading: '背景色なし',
307
+ noBorderColor: '枠線色なし',
298
308
  columnWidthPx: '列幅 (px)',
299
309
  rowHeightPx: '行の高さ (px)',
300
310
  tableBorderWidthPx: 'テーブルの枠幅 (px)',
@@ -326,4 +336,11 @@ export const ja = {
326
336
  imageSize: (maxSize) =>
327
337
  `画像ファイルが大きすぎます。最大許容サイズは ${maxSize} MB です。`,
328
338
  },
339
+
340
+ autoSaveRestore: {
341
+ found: '下書きが見つかりました。復元しますか?',
342
+ foundAt: '{date} の下書きが見つかりました。復元しますか?',
343
+ restore: '復元',
344
+ discard: '破棄',
345
+ },
329
346
  };
package/src/js/i18n/ko.js CHANGED
@@ -151,6 +151,7 @@ export const ko = {
151
151
  findPlaceholder: '찾기…',
152
152
  searchAriaLabel: '검색 텍스트',
153
153
  caseSensitive: '\u00a0대소문자 구분',
154
+ wholeWord: '전체 단어',
154
155
  prevBtn: '← 이전',
155
156
  nextBtn: '다음 →',
156
157
  replacePlaceholder: '바꿀 내용…',
@@ -293,7 +294,16 @@ export const ko = {
293
294
  columnWidth: '열 너비',
294
295
  rowHeight: '행 높이',
295
296
  tableBorderWidth: '표 테두리 너비',
297
+ tableBorderColor: '표 테두리 색상',
296
298
  deleteTable: '표 삭제',
299
+ cellAlignLeft: '왼쪽 정렬',
300
+ cellAlignCenter: '가운데 정렬',
301
+ cellAlignRight: '오른쪽 정렬',
302
+ cellAlignJustify: '양쪽 정렬',
303
+ toggleHeaderRow: '머리글 행 전환',
304
+ cellBackground: '셀 배경색',
305
+ noShading: '배경 없음',
306
+ noBorderColor: '테두리 색 없음',
297
307
  columnWidthPx: '열 너비 (px)',
298
308
  rowHeightPx: '행 높이 (px)',
299
309
  tableBorderWidthPx: '표 테두리 너비 (px)',
@@ -325,4 +335,11 @@ export const ko = {
325
335
  imageSize: (maxSize) =>
326
336
  `이미지 파일이 너무 큽니다. 최대 허용 크기는 ${maxSize} MB입니다.`,
327
337
  },
338
+
339
+ autoSaveRestore: {
340
+ found: '초안을 찾았습니다. 복원하시겠습니까?',
341
+ foundAt: '{date}의 초안을 찾았습니다. 복원하시겠습니까?',
342
+ restore: '복원',
343
+ discard: '삭제',
344
+ },
328
345
  };
package/src/js/i18n/vi.js CHANGED
@@ -152,6 +152,7 @@ export const vi = {
152
152
  findPlaceholder: 'Tìm\u2026',
153
153
  searchAriaLabel: 'Văn bản tìm kiếm',
154
154
  caseSensitive: '\u00a0Phân biệt hoa thường',
155
+ wholeWord: 'Toàn bộ từ',
155
156
  prevBtn: '\u2190 Trước',
156
157
  nextBtn: 'Tiếp \u2192',
157
158
  replacePlaceholder: 'Thay thế bằng\u2026',
@@ -294,9 +295,16 @@ export const vi = {
294
295
  columnWidth: 'Chiều rộng cột',
295
296
  rowHeight: 'Chiều cao hàng',
296
297
  tableBorderWidth: 'Độ rộng viền bảng',
298
+ tableBorderColor: 'Màu viền bảng',
297
299
  deleteTable: 'Xóa bảng',
300
+ cellAlignLeft: 'Căn trái',
301
+ cellAlignCenter: 'Căn giữa',
302
+ cellAlignRight: 'Căn phải',
303
+ cellAlignJustify: 'Căn đều',
304
+ toggleHeaderRow: 'Bật/tắt hàng tiêu đề',
298
305
  cellBackground: 'Màu Nền Ô',
299
306
  noShading: 'Xóa Màu Nền',
307
+ noBorderColor: 'Xóa màu viền',
300
308
  columnWidthPx: 'Chiều rộng cột (px)',
301
309
  rowHeightPx: 'Chiều cao hàng (px)',
302
310
  tableBorderWidthPx: 'Độ rộng viền bảng (px)',
@@ -328,4 +336,11 @@ export const vi = {
328
336
  imageSize: (maxSize) =>
329
337
  `Tệp hình ảnh quá lớn. Kích thước tối đa cho phép là ${maxSize} MB.`,
330
338
  },
339
+
340
+ autoSaveRestore: {
341
+ found: 'Tìm thấy bản nháp. Khôi phục?',
342
+ foundAt: 'Bản nháp từ {date}. Khôi phục?',
343
+ restore: 'Khôi phục',
344
+ discard: 'Bỏ qua',
345
+ },
331
346
  };
package/src/js/i18n/zh.js CHANGED
@@ -152,6 +152,7 @@ export const zh = {
152
152
  findPlaceholder: '查找\u2026',
153
153
  searchAriaLabel: '搜索文字',
154
154
  caseSensitive: '\u00a0区分大小写',
155
+ wholeWord: '全字匹配',
155
156
  prevBtn: '\u2190 上一个',
156
157
  nextBtn: '下一个 \u2192',
157
158
  replacePlaceholder: '替换为\u2026',
@@ -294,7 +295,16 @@ export const zh = {
294
295
  columnWidth: '列宽',
295
296
  rowHeight: '行高',
296
297
  tableBorderWidth: '表格边框宽度',
298
+ tableBorderColor: '表格边框颜色',
297
299
  deleteTable: '删除表格',
300
+ cellAlignLeft: '左对齐',
301
+ cellAlignCenter: '居中对齐',
302
+ cellAlignRight: '右对齐',
303
+ cellAlignJustify: '两端对齐',
304
+ toggleHeaderRow: '切换标题行',
305
+ cellBackground: '单元格背景',
306
+ noShading: '无底纹',
307
+ noBorderColor: '无边框颜色',
298
308
  columnWidthPx: '列宽 (px)',
299
309
  rowHeightPx: '行高 (px)',
300
310
  tableBorderWidthPx: '表格边框宽度 (px)',
@@ -326,4 +336,11 @@ export const zh = {
326
336
  imageSize: (maxSize) =>
327
337
  `图片文件过大。最大允许大小为 ${maxSize} MB。`,
328
338
  },
339
+
340
+ autoSaveRestore: {
341
+ found: '找到草稿。是否恢复?',
342
+ foundAt: '发现 {date} 的草稿。是否恢复?',
343
+ restore: '恢复',
344
+ discard: '放弃',
345
+ },
329
346
  };
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(
@@ -353,7 +365,7 @@ export class Clipboard {
353
365
  const mime = /:(.*?);/.exec(header)?.[1] ?? 'image/png';
354
366
  const binary = atob(b64);
355
367
  const arr = new Uint8Array(binary.length);
356
- for (let i = 0; i < binary.length; i++) arr[i] = binary.codePointAt(i);
368
+ for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
357
369
  return new Blob([arr], { type: mime });
358
370
  }
359
371
 
@@ -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;
@@ -107,6 +107,7 @@ export class ImageDialog extends BaseDialog {
107
107
  // ---------------------------------------------------------------------------
108
108
 
109
109
  _onFileChange() {
110
+ if (this.context._alive === false) return;
110
111
  const file = this._fileInput?.files?.[0];
111
112
  if (!file?.type?.startsWith('image/')) return;
112
113
 
@@ -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
 
@@ -17,6 +17,7 @@
17
17
  * debounce: 200,
18
18
  * onSearch: (query, callback) => void,
19
19
  * onInsert: (item) => string | null,
20
+ * onError: (err: Error) => void,
20
21
  * mentionClass: 'an-mention',
21
22
  * allowSpaces: false,
22
23
  * }
@@ -277,9 +278,19 @@ export class Mention {
277
278
  this._renderItems(items);
278
279
  this._showDropdown();
279
280
  };
280
- const result = this._cfg.onSearch(this._query, cb);
281
+ let result;
282
+ try {
283
+ result = this._cfg.onSearch(this._query, cb);
284
+ } catch (err) {
285
+ this._hideDropdown();
286
+ if (typeof this._cfg.onError === 'function') this._cfg.onError(err);
287
+ return;
288
+ }
281
289
  if (result && typeof result.then === 'function') {
282
- result.then(cb).catch(() => this._hideDropdown());
290
+ result.then(cb).catch((err) => {
291
+ this._hideDropdown();
292
+ if (typeof this._cfg.onError === 'function') this._cfg.onError(err);
293
+ });
283
294
  }
284
295
  }, this._cfg.debounce);
285
296
  }
@@ -1470,9 +1470,30 @@ export class TableTooltip {
1470
1470
  : bText.localeCompare(aText);
1471
1471
  });
1472
1472
  rows.forEach((row) => tbody.appendChild(row));
1473
+ this._markSortIndicator(table, colIdx, direction);
1473
1474
  this.context.invoke('editor.afterCommand');
1474
1475
  }
1475
1476
 
1477
+ /**
1478
+ * Marks the header cell of the sorted column with `an-sort-asc`/`an-sort-desc`
1479
+ * so the active sort column and direction are visible, and clears any previous
1480
+ * indicator. No-op for tables without a `<thead>` (no header row to mark).
1481
+ * @param {HTMLTableElement} table
1482
+ * @param {number} colIdx
1483
+ * @param {'asc'|'desc'} direction
1484
+ */
1485
+ _markSortIndicator(table, colIdx, direction) {
1486
+ const thead = table.querySelector('thead');
1487
+ if (!thead) return;
1488
+ thead.querySelectorAll('.an-sort-asc, .an-sort-desc').forEach((el) => {
1489
+ el.classList.remove('an-sort-asc', 'an-sort-desc');
1490
+ });
1491
+ const headerRow = thead.querySelector('tr');
1492
+ if (!headerRow) return;
1493
+ const headerCell = getCellAtVisualCol(headerRow, colIdx);
1494
+ if (headerCell) headerCell.classList.add(direction === 'asc' ? 'an-sort-asc' : 'an-sort-desc');
1495
+ }
1496
+
1476
1497
  _exportTableCSV() {
1477
1498
  const table = this._activeTable;
1478
1499
  if (!table) return;
@@ -31,9 +31,13 @@
31
31
 
32
32
  &.an-focused {
33
33
  border-color: var(--an-focus-color, #{$an-primary});
34
- // color-mix() blends the focus colour with transparent for the glow ring.
35
- // Falls back gracefully to the default blue glow in older browsers.
36
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--an-focus-color, #{$an-primary}) 18%, transparent);
34
+ // Fallback glow for browsers without color-mix() (pre-Chrome 111, Safari 15.3, Firefox 112).
35
+ // Uses the static $an-primary value so --an-focus-color overrides are ignored in those browsers.
36
+ box-shadow: 0 0 0 3px rgba($an-primary, 0.18);
37
+ // Modern browsers get a fully themeable glow that respects --an-focus-color.
38
+ @supports (color: color-mix(in srgb, red, blue)) {
39
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--an-focus-color, #{$an-primary}) 18%, transparent);
40
+ }
37
41
  }
38
42
 
39
43
  &.an-disabled {
@@ -431,6 +435,11 @@
431
435
  padding: $an-editable-pad;
432
436
  min-height: 0;
433
437
  outline: none;
438
+
439
+ &:focus-visible {
440
+ outline: 2px solid var(--an-focus-color, #{$an-primary});
441
+ outline-offset: -2px;
442
+ }
434
443
  line-height: $an-line-height;
435
444
  color: $an-text;
436
445
  overflow-y: auto;
@@ -565,6 +574,17 @@
565
574
  th {
566
575
  background: $an-bg-toolbar;
567
576
  font-weight: 600;
577
+
578
+ // Sort indicator — shows the active sorted column and direction
579
+ &.an-sort-asc::after,
580
+ &.an-sort-desc::after {
581
+ display: inline-block;
582
+ margin-left: 6px;
583
+ font-size: 0.7em;
584
+ opacity: 0.6;
585
+ }
586
+ &.an-sort-asc::after { content: '\25B2'; }
587
+ &.an-sort-desc::after { content: '\25BC'; }
568
588
  }
569
589
  }
570
590
  }