autumnnote 1.5.0 → 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.
- package/README.md +6 -4
- package/dist/autumnnote.css +324 -2
- package/dist/autumnnote.es.js +638 -227
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +637 -226
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/js/Context.js +8 -3
- package/src/js/core/detectLang.js +98 -0
- package/src/js/core/dom.js +62 -6
- package/src/js/core/markdown.js +14 -13
- package/src/js/core/range.js +2 -2
- package/src/js/editing/History.js +6 -6
- package/src/js/editing/Style.js +19 -19
- package/src/js/editing/Table.js +4 -6
- package/src/js/editing/Typing.js +6 -6
- package/src/js/i18n/en.js +2 -0
- package/src/js/i18n/vi.js +2 -0
- package/src/js/index.js +3 -2
- package/src/js/module/BubbleToolbar.js +30 -13
- package/src/js/module/Buttons.js +16 -14
- package/src/js/module/Clipboard.js +5 -5
- package/src/js/module/CodeTooltip.js +42 -16
- package/src/js/module/Codeview.js +2 -2
- package/src/js/module/ContextMenu.js +12 -12
- package/src/js/module/Editor.js +64 -12
- package/src/js/module/EmojiDialog.js +19 -13
- package/src/js/module/FindReplace.js +85 -59
- package/src/js/module/Fullscreen.js +1 -1
- package/src/js/module/IconDialog.js +26 -20
- package/src/js/module/ImageCropOverlay.js +6 -6
- package/src/js/module/ImageDialog.js +18 -12
- package/src/js/module/ImageResizer.js +1 -1
- package/src/js/module/ImageTooltip.js +8 -4
- package/src/js/module/LinkDialog.js +18 -12
- package/src/js/module/LinkTooltip.js +5 -2
- package/src/js/module/Mention.js +3 -3
- package/src/js/module/Statusbar.js +2 -2
- package/src/js/module/TableTooltip.js +180 -18
- package/src/js/module/Toolbar.js +16 -15
- package/src/js/module/VideoDialog.js +8 -2
- package/src/js/module/VideoResizer.js +3 -3
- package/src/js/module/VideoTooltip.js +11 -6
- package/src/js/renderer.js +4 -2
- package/src/js/settings.js +53 -36
- package/src/styles/autumnnote.scss +332 -4
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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
268
|
-
const
|
|
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
|
|
package/src/js/module/Buttons.js
CHANGED
|
@@ -12,12 +12,14 @@ import * as Style from '../editing/Style.js';
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @typedef {object} DropdownDef
|
|
15
|
-
* @property {string} name
|
|
16
|
-
* @property {'select'} type
|
|
15
|
+
* @property {string} name - unique identifier
|
|
16
|
+
* @property {'select'} type - discriminator for Toolbar renderer
|
|
17
17
|
* @property {string} tooltip
|
|
18
|
-
* @property {string
|
|
19
|
-
* @property {Function} action
|
|
20
|
-
* @property {Function} [getValue]
|
|
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 &&
|
|
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 &&
|
|
185
|
-
const size = (el &&
|
|
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(
|
|
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
|
|
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(
|
|
57
|
-
!this._el.contains(
|
|
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'], ['
|
|
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
|
-
|
|
355
|
+
_w.Prism.highlightElement(codeEl);
|
|
332
356
|
this.context.invoke('editor.afterCommand');
|
|
333
357
|
};
|
|
334
358
|
|
|
335
359
|
if (lang) {
|
|
336
|
-
if (typeof
|
|
360
|
+
if (typeof _w.Prism !== 'undefined') {
|
|
337
361
|
// Grammar already loaded — highlight immediately
|
|
338
|
-
if (
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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 (
|
|
147
|
+
this._menuDisposers.forEach((d) => { try { d(); } catch (_e) {} });
|
|
148
148
|
this._menuDisposers = [];
|
|
149
|
-
this._disposers.forEach((d) => { try { d(); } catch (
|
|
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
|
|
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
|
|
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
|
-
|
|
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');
|
package/src/js/module/Editor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
200
|
-
|
|
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) {
|
|
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
|
}
|