autumnnote 1.4.2 → 1.6.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.
Files changed (46) hide show
  1. package/README.md +102 -21
  2. package/dist/autumnnote.css +324 -2
  3. package/dist/autumnnote.es.js +700 -228
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.umd.js +691 -235
  6. package/dist/autumnnote.umd.js.map +1 -1
  7. package/package.json +8 -1
  8. package/src/js/Context.js +8 -3
  9. package/src/js/core/detectLang.js +98 -0
  10. package/src/js/core/dom.js +62 -6
  11. package/src/js/core/markdown.js +14 -13
  12. package/src/js/core/range.js +2 -2
  13. package/src/js/editing/History.js +6 -6
  14. package/src/js/editing/Style.js +19 -19
  15. package/src/js/editing/Table.js +4 -6
  16. package/src/js/editing/Typing.js +6 -6
  17. package/src/js/i18n/en.js +2 -0
  18. package/src/js/i18n/vi.js +2 -0
  19. package/src/js/index.js +4 -3
  20. package/src/js/module/BubbleToolbar.js +30 -13
  21. package/src/js/module/Buttons.js +16 -14
  22. package/src/js/module/Clipboard.js +5 -5
  23. package/src/js/module/CodeTooltip.js +42 -16
  24. package/src/js/module/Codeview.js +2 -2
  25. package/src/js/module/ContextMenu.js +12 -12
  26. package/src/js/module/Editor.js +64 -12
  27. package/src/js/module/EmojiDialog.js +19 -13
  28. package/src/js/module/FindReplace.js +85 -59
  29. package/src/js/module/Fullscreen.js +1 -1
  30. package/src/js/module/IconDialog.js +26 -20
  31. package/src/js/module/ImageCropOverlay.js +6 -6
  32. package/src/js/module/ImageDialog.js +18 -12
  33. package/src/js/module/ImageResizer.js +1 -1
  34. package/src/js/module/ImageTooltip.js +8 -4
  35. package/src/js/module/LinkDialog.js +18 -12
  36. package/src/js/module/LinkTooltip.js +5 -2
  37. package/src/js/module/Mention.js +3 -3
  38. package/src/js/module/Statusbar.js +2 -2
  39. package/src/js/module/TableTooltip.js +180 -18
  40. package/src/js/module/Toolbar.js +16 -15
  41. package/src/js/module/VideoDialog.js +8 -2
  42. package/src/js/module/VideoResizer.js +3 -3
  43. package/src/js/module/VideoTooltip.js +11 -6
  44. package/src/js/renderer.js +4 -2
  45. package/src/js/settings.js +53 -36
  46. package/src/styles/autumnnote.scss +332 -4
package/src/js/i18n/en.js CHANGED
@@ -298,6 +298,8 @@ export const en = {
298
298
  rowHeight: 'Row Height',
299
299
  tableBorderWidth: 'Table Border Width',
300
300
  deleteTable: 'Delete Table',
301
+ cellBackground: 'Cell Background',
302
+ noShading: 'No Shading',
301
303
  columnWidthPx: 'Column Width (px)',
302
304
  rowHeightPx: 'Row Height (px)',
303
305
  tableBorderWidthPx: 'Table Border Width (px)',
package/src/js/i18n/vi.js CHANGED
@@ -293,6 +293,8 @@ export const vi = {
293
293
  rowHeight: 'Chiều cao hàng',
294
294
  tableBorderWidth: 'Độ rộng viền bảng',
295
295
  deleteTable: 'Xóa bảng',
296
+ cellBackground: 'Màu Nền Ô',
297
+ noShading: 'Xóa Màu Nền',
296
298
  columnWidthPx: 'Chiều rộng cột (px)',
297
299
  rowHeightPx: 'Chiều cao hàng (px)',
298
300
  tableBorderWidthPx: 'Độ rộng viền bảng (px)',
package/src/js/index.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * const editor = AutumnNote.create('#my-editor');
10
10
  */
11
11
 
12
+ // @ts-ignore
12
13
  import '../styles/autumnnote.scss';
13
14
  import { Context, _customModules, _globalPlugins } from './Context.js';
14
15
  import { registerButton } from './module/Buttons.js';
@@ -49,7 +50,7 @@ const AutumnNote = {
49
50
  const elements = resolveElements(selector);
50
51
  const ctxs = elements.map((el) => {
51
52
  if (instances.has(el)) return instances.get(el);
52
- const ctx = new Context(el, options);
53
+ const ctx = new Context(/** @type {HTMLElement} */ (el), options);
53
54
  ctx.initialize();
54
55
  instances.set(el, ctx);
55
56
  return ctx;
@@ -140,7 +141,7 @@ const AutumnNote = {
140
141
  registerButton(btnDef) { registerButton(btnDef); return this; },
141
142
 
142
143
  /** Library version */
143
- version: '1.4.2',
144
+ version: '1.5.0',
144
145
  };
145
146
 
146
147
  // ---------------------------------------------------------------------------
@@ -159,7 +160,7 @@ function resolveElements(selector) {
159
160
  return [selector];
160
161
  }
161
162
  if (selector instanceof NodeList || Array.isArray(selector)) {
162
- return Array.from(selector);
163
+ return /** @type {Element[]} */ (Array.from(selector));
163
164
  }
164
165
  return [];
165
166
  }
@@ -53,7 +53,7 @@ const _ACTIONS = {
53
53
  if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
54
54
  const range = sel.getRangeAt(0);
55
55
  const ancestor = range.commonAncestorContainer;
56
- const root = ancestor.nodeType === 1 ? ancestor : ancestor.parentElement;
56
+ const root = /** @type {Element|null} */ (ancestor.nodeType === 1 ? ancestor : ancestor.parentElement);
57
57
  if (root) {
58
58
  const candidates = [root, ...root.querySelectorAll('[style]')];
59
59
  for (const el of candidates) {
@@ -115,7 +115,9 @@ export class BubbleToolbar {
115
115
  const d6 = this.context.on('contextMenu:hide', () => {
116
116
  this._contextMenuOpen = false;
117
117
  });
118
- this._disposers.push(d1, d2, d3, d4, d5, d6);
118
+ const d7 = on(window, 'scroll', () => this._hide(), { passive: true });
119
+ const d8 = on(window, 'resize', () => this._hide(), { passive: true });
120
+ this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8);
119
121
  return this;
120
122
  }
121
123
 
@@ -249,9 +251,10 @@ export class BubbleToolbar {
249
251
  document.body.appendChild(picker);
250
252
 
251
253
  this._picker = picker;
252
- this._picker._paletteEl = palette;
253
- this._picker._noColorBtn = noColorBtn;
254
- this._picker._colorInput = colorInput;
254
+ const pickerAny = /** @type {any} */ (picker);
255
+ pickerAny._paletteEl = palette;
256
+ pickerAny._noColorBtn = noColorBtn;
257
+ pickerAny._colorInput = colorInput;
255
258
  }
256
259
 
257
260
  _openColorPicker(type, anchorBtn) {
@@ -264,8 +267,9 @@ export class BubbleToolbar {
264
267
  this._pickerType = type;
265
268
 
266
269
  // Toggle "No highlight" swatch based on type
267
- const palette = this._picker._paletteEl;
268
- const noColorBtn = this._picker._noColorBtn;
270
+ const pickerAny = /** @type {any} */ (this._picker);
271
+ const palette = pickerAny._paletteEl;
272
+ const noColorBtn = pickerAny._noColorBtn;
269
273
  if (type === 'hiliteColor') {
270
274
  if (!palette.contains(noColorBtn)) palette.appendChild(noColorBtn);
271
275
  } else {
@@ -273,7 +277,7 @@ export class BubbleToolbar {
273
277
  }
274
278
 
275
279
  // Seed the custom color input
276
- this._picker._colorInput.value = type === 'foreColor' ? '#000000' : '#ffff00';
280
+ /** @type {any} */ (this._picker)._colorInput.value = type === 'foreColor' ? '#000000' : '#ffff00';
277
281
 
278
282
  // Position the picker above the bubble toolbar (not blocking the content below it).
279
283
  // Fall back to below the toolbar if there's not enough room above.
@@ -320,7 +324,7 @@ export class BubbleToolbar {
320
324
  const name = type === 'hiliteColor' ? 'hiliteColor' : 'foreColor';
321
325
  const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
322
326
  const strip = btn && btn.querySelector('.an-bubble-color-strip');
323
- if (strip) strip.style.background = color === 'transparent' ? 'transparent' : color;
327
+ if (strip) /** @type {HTMLElement} */ (strip).style.background = color === 'transparent' ? 'transparent' : color;
324
328
 
325
329
  this._closeColorPicker();
326
330
  this._syncActive();
@@ -352,6 +356,19 @@ export class BubbleToolbar {
352
356
  top = rect.bottom + gap;
353
357
  }
354
358
 
359
+ // If the Table Tooltip is visible, avoid overlapping it by flipping below the selection.
360
+ // The Table Tooltip sits above the table, which is the same vertical zone the bubble
361
+ // toolbar would normally occupy when text is selected inside a table cell.
362
+ const tableTooltipEl = document.querySelector('.an-table-tooltip');
363
+ if (tableTooltipEl && /** @type {HTMLElement} */ (tableTooltipEl).style.display !== 'none') {
364
+ const ttRect = tableTooltipEl.getBoundingClientRect();
365
+ const overlapsVertically = top < ttRect.bottom + gap && top + bh > ttRect.top - gap;
366
+ if (overlapsVertically) {
367
+ top = rect.bottom + gap;
368
+ if (top + bh > window.innerHeight - 8) top = ttRect.bottom + gap;
369
+ }
370
+ }
371
+
355
372
  el.style.top = `${top}px`;
356
373
  el.style.left = `${left}px`;
357
374
  el.style.visibility = '';
@@ -371,7 +388,7 @@ export class BubbleToolbar {
371
388
  _syncActive() {
372
389
  if (!this._btnCache) return;
373
390
  this._btnCache.forEach((btn) => {
374
- const activeFn = _ACTIVE[btn.dataset.name];
391
+ const activeFn = _ACTIVE[/** @type {HTMLElement} */ (btn).dataset.name];
375
392
  btn.classList.toggle('an-active', !!(activeFn && activeFn()));
376
393
  });
377
394
  }
@@ -384,17 +401,17 @@ export class BubbleToolbar {
384
401
  let node = sel.getRangeAt(0).startContainer;
385
402
  if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
386
403
  if (!node) return;
387
- const cs = window.getComputedStyle(node);
404
+ const cs = window.getComputedStyle(/** @type {Element} */ (node));
388
405
 
389
406
  const foreBtn = this._el.querySelector('[data-name="foreColor"]');
390
407
  const foreStrip = foreBtn && foreBtn.querySelector('.an-bubble-color-strip');
391
- if (foreStrip) foreStrip.style.background = cs.color || '#000000';
408
+ if (foreStrip) /** @type {HTMLElement} */ (foreStrip).style.background = cs.color || '#000000';
392
409
 
393
410
  const hiliteBtn = this._el.querySelector('[data-name="hiliteColor"]');
394
411
  const hiliteStrip = hiliteBtn && hiliteBtn.querySelector('.an-bubble-color-strip');
395
412
  if (hiliteStrip) {
396
413
  const bg = cs.backgroundColor;
397
- hiliteStrip.style.background = (!bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent') ? 'transparent' : bg;
414
+ /** @type {HTMLElement} */ (hiliteStrip).style.background = (!bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent') ? 'transparent' : bg;
398
415
  }
399
416
  }
400
417
 
@@ -12,12 +12,14 @@ import * as Style from '../editing/Style.js';
12
12
 
13
13
  /**
14
14
  * @typedef {object} DropdownDef
15
- * @property {string} name - unique identifier
16
- * @property {'select'} type - discriminator for Toolbar renderer
15
+ * @property {string} name - unique identifier
16
+ * @property {'select'} type - discriminator for Toolbar renderer
17
17
  * @property {string} tooltip
18
- * @property {string[]} [items] - overridden at render time from options
19
- * @property {Function} action - called with (context, value)
20
- * @property {Function} [getValue] - called with (context) to get current value
18
+ * @property {Array<string|{value:string,label:string,disabled?:boolean}>} [items] - overridden at render time from options
19
+ * @property {Function} action - called with (context, value)
20
+ * @property {Function} [getValue] - called with (context) to get current value
21
+ * @property {string} [selectClass] - extra CSS class(es) for the <select>
22
+ * @property {string} [placeholder] - placeholder text for the empty option
21
23
  */
22
24
 
23
25
  // ---------------------------------------------------------------------------
@@ -103,7 +105,7 @@ export const underlineBtn = btn('underline', 'underline', 'Underline (Ctrl+U)',
103
105
  if (!sel || !sel.rangeCount) return false;
104
106
  let sc = sel.getRangeAt(0).startContainer;
105
107
  if (sc.nodeType === 3) sc = sc.parentElement;
106
- return !!(sc && sc.closest && sc.closest('u'));
108
+ return !!(sc && /** @type {Element} */ (sc).closest('u'));
107
109
  });
108
110
  export const strikeBtn = btn('strikethrough', 'strikethrough', 'Strikethrough', () => Style.strikethrough(), () => document.queryCommandState('strikeThrough'));
109
111
  export const superscriptBtn = btn('superscript', 'superscript', 'Superscript', () => Style.superscript(), () => document.queryCommandState('superscript'));
@@ -179,10 +181,10 @@ export const fontSizeBtn = {
179
181
  try {
180
182
  const sel = window.getSelection();
181
183
  if (sel && sel.rangeCount) {
182
- let el = sel.getRangeAt(0).startContainer;
183
- if (el.nodeType === 3) el = el.parentElement;
184
- while (el && el.nodeType === 1 && !el.style.fontSize) el = el.parentElement;
185
- const size = (el && el.style && el.style.fontSize) ? el.style.fontSize : '';
184
+ let el = /** @type {Element|null} */ (sel.getRangeAt(0).startContainer);
185
+ if (el && el.nodeType === 3) el = el.parentElement;
186
+ while (el && el.nodeType === 1 && !/** @type {HTMLElement} */ (el).style.fontSize) el = el.parentElement;
187
+ const size = (el && /** @type {HTMLElement} */ (el).style.fontSize) ? /** @type {HTMLElement} */ (el).style.fontSize : '';
186
188
  if (size) return size;
187
189
  }
188
190
  // Fallback: read the base font size from the editable element itself
@@ -281,11 +283,11 @@ export const lineHeightBtn = {
281
283
  const sel = window.getSelection();
282
284
  if (!sel || !sel.rangeCount) return '';
283
285
  const BLOCKS = new Set(['P','DIV','H1','H2','H3','H4','H5','H6','LI','BLOCKQUOTE','PRE','TD','TH']);
284
- let el = sel.getRangeAt(0).startContainer;
285
- if (el.nodeType === 3) el = el.parentElement;
286
- while (el && !BLOCKS.has(el.tagName)) el = el.parentElement;
286
+ let el = /** @type {Element|null} */ (sel.getRangeAt(0).startContainer);
287
+ if (el && el.nodeType === 3) el = el.parentElement;
288
+ while (el && !BLOCKS.has(/** @type {Element} */ (el).tagName)) el = el.parentElement;
287
289
  if (!el) return '';
288
- return el.style.lineHeight || getComputedStyle(el).lineHeight || '';
290
+ return /** @type {HTMLElement} */ (el).style.lineHeight || getComputedStyle(/** @type {Element} */ (el)).lineHeight || '';
289
291
  } catch { return ''; }
290
292
  },
291
293
  };
@@ -65,11 +65,11 @@ export class Clipboard {
65
65
  */
66
66
  _revokeRemovedBlobs(node) {
67
67
  if (!this._blobRegistry || !this._blobRegistry.size) return;
68
- const imgs = [];
68
+ const imgs = /** @type {Element[]} */ ([]);
69
69
  if (node.nodeName === 'IMG') {
70
- imgs.push(node);
71
- } else if (node.querySelectorAll) {
72
- imgs.push(...node.querySelectorAll('img'));
70
+ imgs.push(/** @type {Element} */ (node));
71
+ } else if (/** @type {Element} */ (node).querySelectorAll) {
72
+ imgs.push(.../** @type {Element} */ (node).querySelectorAll('img'));
73
73
  }
74
74
  imgs.forEach((img) => {
75
75
  const src = img.getAttribute('src') || '';
@@ -180,7 +180,7 @@ export class Clipboard {
180
180
  }
181
181
 
182
182
  _onPaste(event) {
183
- const clipboardData = event.clipboardData || window.clipboardData;
183
+ const clipboardData = event.clipboardData || /** @type {any} */ (window).clipboardData;
184
184
  if (!clipboardData) return;
185
185
 
186
186
  // Consume and reset the one-shot plain-paste flag
@@ -40,21 +40,22 @@ export class CodeTooltip {
40
40
  this._disposers.push(
41
41
  on(editable, 'mouseover', (e) => {
42
42
  if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
43
- const pre = e.target.closest('pre');
43
+ const pre = /** @type {Element} */ (e.target)?.closest('pre');
44
44
  if (pre && editable.contains(pre)) {
45
45
  this._scheduleShow(pre);
46
46
  }
47
47
  }),
48
48
  on(editable, 'mouseout', (e) => {
49
- const to = e.relatedTarget;
49
+ const to = /** @type {Node|null} */ (/** @type {MouseEvent} */ (e).relatedTarget);
50
50
  if (!to || (!editable.contains(to) && !this._el.contains(to))) {
51
51
  this._scheduleHide();
52
52
  }
53
53
  }),
54
54
  on(document, 'click', (e) => {
55
+ const et = /** @type {Node} */ (e.target);
55
56
  if (this._activePre &&
56
- !this._activePre.contains(e.target) &&
57
- !this._el.contains(e.target)) {
57
+ !this._activePre.contains(et) &&
58
+ !this._el.contains(et)) {
58
59
  this._hide();
59
60
  }
60
61
  }),
@@ -92,15 +93,15 @@ export class CodeTooltip {
92
93
  el.appendChild(this._sep());
93
94
 
94
95
  // Language selector
95
- this._langSelect = createElement('select', {
96
+ this._langSelect = /** @type {HTMLSelectElement} */ (createElement('select', {
96
97
  class: 'an-code-lang-select',
97
98
  title: L.syntaxLanguage,
98
99
  'aria-label': L.syntaxAriaLabel,
99
- });
100
+ }));
100
101
  const LANGUAGES = [
101
102
  ['', 'Plain text'], ['javascript', 'JavaScript'], ['typescript', 'TypeScript'],
102
- ['python', 'Python'], ['html', 'HTML'], ['css', 'CSS'], ['json', 'JSON'],
103
- ['xml', 'XML'], ['bash', 'Bash / Shell'], ['sql', 'SQL'],
103
+ ['python', 'Python'], ['html', 'HTML'], ['css', 'CSS'], ['scss', 'SCSS'],
104
+ ['json', 'JSON'], ['xml', 'XML'], ['bash', 'Bash / Shell'], ['sql', 'SQL'],
104
105
  ['java', 'Java'], ['csharp', 'C#'], ['php', 'PHP'], ['ruby', 'Ruby'],
105
106
  ['go', 'Go'], ['rust', 'Rust'], ['cpp', 'C++'], ['c', 'C'],
106
107
  ['kotlin', 'Kotlin'], ['swift', 'Swift'],
@@ -300,10 +301,33 @@ export class CodeTooltip {
300
301
  this._positionNear(pre);
301
302
  }
302
303
 
304
+ /**
305
+ * Applies a language to a given <pre> element: sets classes, data-language,
306
+ * and triggers Prism highlighting. Called by the auto-detect flow.
307
+ * @param {HTMLElement} pre
308
+ * @param {string} lang - Prism language identifier, e.g. 'javascript'
309
+ */
310
+ applyLanguage(pre, lang) {
311
+ if (!pre || !lang) return;
312
+ // Temporarily set activePre so _onLangChange can target it
313
+ const savedPre = this._activePre;
314
+ const savedSelect = this._langSelect ? this._langSelect.value : '';
315
+ this._activePre = pre;
316
+ if (this._langSelect) this._langSelect.value = lang;
317
+ this._onLangChange();
318
+ // Update the select to reflect the detected language when tooltip is shown
319
+ if (this._langSelect) this._langSelect.value = lang;
320
+ this._activePre = savedPre || pre;
321
+ // Don't restore savedPre if it was null — keep `pre` as activePre so that
322
+ // the tooltip select is correct the first time the user hovers over it.
323
+ void savedSelect;
324
+ }
325
+
303
326
  _onLangChange() {
304
327
  const pre = this._activePre;
305
328
  if (!pre) return;
306
329
  const lang = this._langSelect.value;
330
+ const _w = /** @type {any} */ (window);
307
331
 
308
332
  // Ensure a <code> child exists (Prism targets <pre><code class="language-xxx">)
309
333
  let codeEl = pre.querySelector('code');
@@ -328,14 +352,14 @@ export class CodeTooltip {
328
352
  // which drops <br> entirely, collapsing all lines into one. Convert first.
329
353
  const applyPrism = () => {
330
354
  codeEl.querySelectorAll('br').forEach((br) => br.replaceWith('\n'));
331
- window.Prism.highlightElement(codeEl);
355
+ _w.Prism.highlightElement(codeEl);
332
356
  this.context.invoke('editor.afterCommand');
333
357
  };
334
358
 
335
359
  if (lang) {
336
- if (typeof window.Prism !== 'undefined') {
360
+ if (typeof _w.Prism !== 'undefined') {
337
361
  // Grammar already loaded — highlight immediately
338
- if (window.Prism.languages[lang]) {
362
+ if (_w.Prism.languages[lang]) {
339
363
  applyPrism();
340
364
  return;
341
365
  }
@@ -345,7 +369,7 @@ export class CodeTooltip {
345
369
  } else if (this._prismScript) {
346
370
  // Prism core is still loading — highlight once it arrives, then load grammar if needed
347
371
  this._prismScript.addEventListener('load', () => {
348
- if (window.Prism.languages[lang]) {
372
+ if (_w.Prism.languages[lang]) {
349
373
  applyPrism();
350
374
  } else {
351
375
  this._loadPrismComponent(lang, applyPrism);
@@ -363,7 +387,8 @@ export class CodeTooltip {
363
387
  * Called once at initialize time. Fire-and-forget; errors are silent.
364
388
  */
365
389
  _ensurePrism() {
366
- if (!this.context.options.codeHighlight || window.Prism) return;
390
+ const _w = /** @type {any} */ (window);
391
+ if (!this.context.options.codeHighlight || _w.Prism) return;
367
392
  const cdn = this.context.options.codeHighlightCDN;
368
393
  const themeHref = `${cdn}/themes/prism-tomorrow.min.css`;
369
394
  const scriptSrc = `${cdn}/prism.min.js`;
@@ -377,7 +402,7 @@ export class CodeTooltip {
377
402
 
378
403
  const existingScript = document.querySelector(`script[src="${scriptSrc}"]`);
379
404
  if (existingScript) {
380
- this._prismScript = window.Prism ? null : existingScript;
405
+ this._prismScript = _w.Prism ? null : existingScript;
381
406
  return;
382
407
  }
383
408
 
@@ -397,13 +422,14 @@ export class CodeTooltip {
397
422
  * @param {Function} cb – called once the grammar is ready
398
423
  */
399
424
  _loadPrismComponent(lang, cb) {
425
+ const _w = /** @type {any} */ (window);
400
426
  const cdn = this.context.options.codeHighlightCDN;
401
427
  const src = `${cdn}/components/prism-${lang}.min.js`;
402
428
  // Avoid loading the same component twice
403
429
  if (document.querySelector(`script[src="${src}"]`)) {
404
430
  // Already in DOM — might still be loading; poll briefly then call cb
405
431
  const poll = setInterval(() => {
406
- if (window.Prism && window.Prism.languages[lang]) {
432
+ if (_w.Prism && _w.Prism.languages[lang]) {
407
433
  clearInterval(poll);
408
434
  cb();
409
435
  }
@@ -413,7 +439,7 @@ export class CodeTooltip {
413
439
  }
414
440
  const s = document.createElement('script');
415
441
  s.src = src;
416
- s.addEventListener('load', cb, { once: true });
442
+ s.addEventListener('load', /** @type {EventListener} */ (cb), { once: true });
417
443
  document.head.appendChild(s);
418
444
  }
419
445
 
@@ -53,13 +53,13 @@ export class Codeview {
53
53
  const { editable } = this.context.layoutInfo;
54
54
  const html = editable.innerHTML;
55
55
 
56
- this._textarea = createElement('textarea', {
56
+ this._textarea = /** @type {HTMLTextAreaElement} */ (createElement('textarea', {
57
57
  class: 'an-codeview',
58
58
  spellcheck: 'false',
59
59
  autocomplete: 'off',
60
60
  autocorrect: 'off',
61
61
  autocapitalize: 'off',
62
- });
62
+ }));
63
63
  this._textarea.value = this._prettyPrint(html);
64
64
 
65
65
  editable.style.display = 'none';
@@ -137,16 +137,16 @@ export class ContextMenu {
137
137
  }
138
138
 
139
139
  this._disposers.push(on(document, 'click', (e) => this._maybeHide(e)));
140
- this._disposers.push(on(document, 'keydown', (e) => { if (e.key === 'Escape') this.hide(); }));
140
+ this._disposers.push(on(document, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Escape') this.hide(); }));
141
141
  this._disposers.push(on(window, 'scroll', () => this.hide(), { passive: true }));
142
142
 
143
143
  return this;
144
144
  }
145
145
 
146
146
  destroy() {
147
- this._menuDisposers.forEach((d) => { try { d(); } catch (e) {} });
147
+ this._menuDisposers.forEach((d) => { try { d(); } catch (_e) {} });
148
148
  this._menuDisposers = [];
149
- this._disposers.forEach((d) => { try { d(); } catch (e) {} });
149
+ this._disposers.forEach((d) => { try { d(); } catch (_e) {} });
150
150
  this._disposers = [];
151
151
  if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
152
152
  this.el = null;
@@ -253,12 +253,12 @@ export class ContextMenu {
253
253
 
254
254
  // Custom color row
255
255
  const customRow = createElement('div', { class: 'an-context-color-custom' });
256
- const colorInput = createElement('input', {
256
+ const colorInput = /** @type {HTMLInputElement} */ (createElement('input', {
257
257
  type: 'color',
258
258
  value: it.colorType === 'foreColor' ? '#000000' : '#ffff00',
259
259
  title: this.context.locale.contextMenu.customColor || 'Custom color',
260
260
  'aria-label': this.context.locale.contextMenu.customColor || 'Custom color',
261
- });
261
+ }));
262
262
  const customLabel = createElement('span', {}, [this.context.locale.contextMenu.customColorLabel || 'Custom…']);
263
263
  const offCustom = on(colorInput, 'change', () => this._applyColor(it.colorType, colorInput.value));
264
264
  this._menuDisposers.push(offCustom);
@@ -324,13 +324,13 @@ export class ContextMenu {
324
324
  this._menuDisposers.push(offHeader);
325
325
 
326
326
  const offMove = on(gridEl, 'mousemove', (e) => {
327
- const cell = e.target.closest('[data-row]');
327
+ const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-row]'));
328
328
  if (!cell) return;
329
329
  setHighlight(+cell.dataset.row, +cell.dataset.col);
330
330
  });
331
331
  const offLeave = on(gridEl, 'mouseleave', () => setHighlight(0, 0));
332
332
  const offClick = on(gridEl, 'click', (e) => {
333
- const cell = e.target.closest('[data-row]');
333
+ const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-row]'));
334
334
  if (!cell) return;
335
335
  const rows = +cell.dataset.row;
336
336
  const cols = +cell.dataset.col;
@@ -354,7 +354,7 @@ export class ContextMenu {
354
354
 
355
355
  // Regular item
356
356
  const btn = createElement('button', { type: 'button', class: 'an-context-item', 'data-name': it.name || '' });
357
- if (typeof it.disabled === 'function' ? it.disabled(this.context) : !!it.disabled) btn.disabled = true;
357
+ if (typeof it.disabled === 'function' ? it.disabled(this.context) : !!it.disabled) /** @type {HTMLButtonElement} */ (btn).disabled = true;
358
358
  if (it.icon) {
359
359
  const iconSpan = createElement('span', { class: 'an-context-icon', 'aria-hidden': 'true' });
360
360
  iconSpan.innerHTML = it.icon;
@@ -386,7 +386,7 @@ export class ContextMenu {
386
386
  // Only apply when there is a real (non-collapsed) selection — a collapsed range
387
387
  // (cursor only) also has height > 0, which would misplace the menu relative
388
388
  // to the actual click point.
389
- let openX = event.clientX;
389
+ const openX = event.clientX;
390
390
  let openY = event.clientY;
391
391
  if (this._savedRange && !this._savedRange.collapsed) {
392
392
  try {
@@ -454,7 +454,7 @@ export class ContextMenu {
454
454
  let node = range.startContainer;
455
455
  if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
456
456
  if (!node) return type === 'foreColor' ? '#000000' : 'transparent';
457
- const cs = window.getComputedStyle(node);
457
+ const cs = window.getComputedStyle(/** @type {Element} */ (node));
458
458
  if (type === 'foreColor') {
459
459
  return cs.color || '#000000';
460
460
  }
@@ -488,7 +488,7 @@ export class ContextMenu {
488
488
  if (!node || !editable || !editable.contains(node)) return;
489
489
 
490
490
  // Walk up to collect explicitly-set inline properties from the nearest styled ancestor
491
- const cs = window.getComputedStyle(node);
491
+ const cs = window.getComputedStyle(/** @type {Element} */ (node));
492
492
 
493
493
  // Detect if an explicit font-family / font-size is set on an element (not just inherited)
494
494
  const explicitFontFamily = this._findExplicitStyle(node, editable, 'fontFamily');
@@ -609,7 +609,7 @@ export class ContextMenu {
609
609
  let el;
610
610
  while ((el = iter.nextNode())) {
611
611
  if (!editable.contains(el) || el === editable) continue;
612
- try { if (range.intersectsNode(el)) el.removeAttribute('style'); } catch { /* ignore */ }
612
+ try { if (range.intersectsNode(el)) /** @type {Element} */ (el).removeAttribute('style'); } catch { /* ignore */ }
613
613
  }
614
614
 
615
615
  this.context.invoke('editor.afterCommand');
@@ -12,6 +12,7 @@ import { handleKeydown } from '../editing/Typing.js';
12
12
  import { on } from '../core/dom.js';
13
13
  import { sanitiseHTML, sanitiseUrl } from '../core/sanitise.js';
14
14
  import { markdownToHTML, htmlToMarkdown } from '../core/markdown.js';
15
+ import { detectLang } from '../core/detectLang.js';
15
16
 
16
17
  export class Editor {
17
18
  /**
@@ -93,7 +94,8 @@ export class Editor {
93
94
  // Only act when cursor is at the <li> element node itself (not inside
94
95
  // a text node — the browser already placed it correctly in that case).
95
96
  if (sc.nodeType !== Node.ELEMENT_NODE) return;
96
- const li = sc.matches && sc.matches('.an-checklist li') ? sc : null;
97
+ const scEl = /** @type {Element} */ (sc);
98
+ const li = scEl.matches('.an-checklist li') ? scEl : null;
97
99
  if (!li) return;
98
100
  const cb = li.querySelector('input[type="checkbox"]');
99
101
  if (!cb) return;
@@ -160,9 +162,9 @@ export class Editor {
160
162
  // the mouse and moving outside the wrapper before releasing.
161
163
  on(editable, 'dragstart', (e) => {
162
164
  if (isReadOnly()) { e.preventDefault(); return; }
163
- const target = e.target;
165
+ const target = /** @type {Element} */ (e.target);
164
166
  if (target && (target.nodeName === 'IFRAME' ||
165
- (target.closest && target.closest('.an-video-wrapper')))) {
167
+ target.closest('.an-video-wrapper'))) {
166
168
  e.preventDefault();
167
169
  }
168
170
  }),
@@ -182,9 +184,10 @@ export class Editor {
182
184
  if (!sel || !sel.rangeCount) { _compositionSupSub = null; return; }
183
185
  let node = sel.getRangeAt(0).startContainer;
184
186
  if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
185
- if (node && node.closest) {
186
- if (node.closest('sup')) _compositionSupSub = 'superscript';
187
- else if (node.closest('sub')) _compositionSupSub = 'subscript';
187
+ if (node) {
188
+ const el = /** @type {Element} */ (node);
189
+ if (el.closest('sup')) _compositionSupSub = 'superscript';
190
+ else if (el.closest('sub')) _compositionSupSub = 'subscript';
188
191
  else _compositionSupSub = null;
189
192
  }
190
193
  };
@@ -196,8 +199,9 @@ export class Editor {
196
199
  if (!sel || !sel.rangeCount) return;
197
200
  let node = sel.getRangeAt(0).startContainer;
198
201
  if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
199
- const inContext = node && node.closest &&
200
- (tag === 'superscript' ? node.closest('sup') : node.closest('sub'));
202
+ const el = /** @type {Element} */ (node);
203
+ const inContext = el &&
204
+ (tag === 'superscript' ? el.closest('sup') : el.closest('sub'));
201
205
  if (!inContext) {
202
206
  // The composed character escaped the sup/sub — re-apply the format.
203
207
  document.execCommand(tag);
@@ -314,6 +318,9 @@ export class Editor {
314
318
  // C4: Remove figure.an-figure elements whose <img> has been deleted so
315
319
  // orphaned figcaptions do not accumulate in the DOM.
316
320
  this._cleanOrphanedFigures();
321
+ // Ensure the editable always ends with a paragraph so the user can click
322
+ // and type after block elements that trap the cursor (pre, table, etc.).
323
+ this._ensureTrailingParagraph();
317
324
  // Immediate: keep toolbar and statusbar in sync on every mutation.
318
325
  this.context.invoke('toolbar.refresh');
319
326
  this.context.invoke('statusbar.update');
@@ -351,6 +358,25 @@ export class Editor {
351
358
  });
352
359
  }
353
360
 
361
+ /**
362
+ * Ensures the editable always ends with a plain paragraph so the cursor can
363
+ * be placed after block elements that do not naturally allow it
364
+ * (pre, blockquote, table, figure, ul, ol, hr).
365
+ * Without this, clicking below the last such element does nothing.
366
+ */
367
+ _ensureTrailingParagraph() {
368
+ const editable = this.context.layoutInfo.editable;
369
+ if (!editable) return;
370
+ const last = editable.lastElementChild;
371
+ if (!last) return;
372
+ const TRAPPING = new Set(['PRE', 'BLOCKQUOTE', 'TABLE', 'FIGURE', 'UL', 'OL', 'HR']);
373
+ if (TRAPPING.has(last.nodeName)) {
374
+ const p = document.createElement('p');
375
+ p.innerHTML = '<br>';
376
+ editable.appendChild(p);
377
+ }
378
+ }
379
+
354
380
  // ---------------------------------------------------------------------------
355
381
  // Focus management
356
382
  // ---------------------------------------------------------------------------
@@ -523,9 +549,35 @@ export class Editor {
523
549
  print() { this.context.print(); }
524
550
 
525
551
  /**
526
- * @param {string} tagName - e.g. 'h1', 'p', 'blockquote'
552
+ * @param {string} tagName - e.g. 'h1', 'p', 'blockquote', 'pre'
527
553
  */
528
- formatBlock(tagName) { Style.formatBlock(tagName); this.afterCommand(); }
554
+ formatBlock(tagName) {
555
+ Style.formatBlock(tagName);
556
+
557
+ // Auto-detect the programming language when the user formats a code block.
558
+ // Only runs when converting TO <pre> and the block has no language yet.
559
+ if (tagName === 'pre') {
560
+ const sel = window.getSelection();
561
+ if (sel && sel.rangeCount > 0) {
562
+ const container = sel.getRangeAt(0).commonAncestorContainer;
563
+ const pre = /** @type {Element|null} */ (
564
+ container.nodeType === 1
565
+ ? /** @type {Element} */ (container).closest('pre')
566
+ : (/** @type {Element|null} */ (container.parentElement))?.closest('pre')
567
+ );
568
+ if (pre && !pre.getAttribute('data-language')) {
569
+ const code = pre.textContent || '';
570
+ const lang = detectLang(code);
571
+ if (lang) {
572
+ this.context.invoke('codeTooltip.applyLanguage', pre, lang);
573
+ return; // applyLanguage already calls afterCommand internally
574
+ }
575
+ }
576
+ }
577
+ }
578
+
579
+ this.afterCommand();
580
+ }
529
581
 
530
582
  /**
531
583
  * @param {string} color
@@ -580,8 +632,8 @@ export class Editor {
580
632
  if (openInNewTab) {
581
633
  const link = this._getClosestAnchor();
582
634
  if (link) {
583
- link.setAttribute('target', '_blank');
584
- link.setAttribute('rel', 'noopener noreferrer');
635
+ /** @type {Element} */ (link).setAttribute('target', '_blank');
636
+ /** @type {Element} */ (link).setAttribute('rel', 'noopener noreferrer');
585
637
  }
586
638
  }
587
639
  }