autumnnote 1.5.0 → 1.6.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.
- package/README.md +10 -8
- package/dist/autumnnote.css +324 -4
- package/dist/autumnnote.es.js +943 -526
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +935 -519
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +21 -3
- package/src/js/Context.js +23 -16
- package/src/js/core/detectLang.js +98 -0
- package/src/js/core/dom.js +67 -11
- package/src/js/core/env.js +1 -1
- package/src/js/core/func.js +1 -1
- package/src/js/core/lists.js +1 -1
- package/src/js/core/markdown.js +32 -31
- package/src/js/core/range.js +8 -8
- package/src/js/editing/History.js +10 -10
- package/src/js/editing/Style.js +44 -44
- package/src/js/editing/Table.js +5 -7
- package/src/js/editing/Typing.js +19 -19
- 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/AutoSaveRestore.js +2 -4
- package/src/js/module/BubbleToolbar.js +51 -34
- package/src/js/module/Buttons.js +20 -18
- package/src/js/module/Clipboard.js +16 -17
- package/src/js/module/CodeTooltip.js +48 -24
- package/src/js/module/Codeview.js +5 -7
- package/src/js/module/ContextMenu.js +37 -37
- package/src/js/module/Editor.js +77 -26
- package/src/js/module/EmojiDialog.js +24 -18
- package/src/js/module/FindReplace.js +93 -66
- package/src/js/module/Fullscreen.js +1 -1
- package/src/js/module/IconDialog.js +39 -35
- package/src/js/module/ImageCropOverlay.js +13 -13
- package/src/js/module/ImageDialog.js +19 -13
- package/src/js/module/ImageResizer.js +5 -5
- package/src/js/module/ImageTooltip.js +20 -16
- package/src/js/module/LinkDialog.js +20 -14
- package/src/js/module/LinkTooltip.js +15 -12
- package/src/js/module/MarkdownShortcuts.js +11 -14
- package/src/js/module/Mention.js +11 -13
- package/src/js/module/Placeholder.js +1 -1
- package/src/js/module/ShortcutsDialog.js +2 -4
- package/src/js/module/Statusbar.js +5 -7
- package/src/js/module/TableTooltip.js +196 -36
- package/src/js/module/Toolbar.js +35 -34
- package/src/js/module/VideoDialog.js +16 -10
- package/src/js/module/VideoResizer.js +7 -9
- package/src/js/module/VideoTooltip.js +15 -10
- package/src/js/renderer.js +5 -3
- package/src/js/settings.js +53 -36
- package/src/styles/autumnnote.scss +332 -7
|
@@ -44,16 +44,16 @@ const _ACTIONS = {
|
|
|
44
44
|
strikethrough: (ctx) => ctx.invoke('editor.strikethrough'),
|
|
45
45
|
link: (ctx) => ctx.invoke('linkDialog.show'),
|
|
46
46
|
removeFormat: (ctx) => {
|
|
47
|
-
const editable = ctx.layoutInfo
|
|
47
|
+
const editable = ctx.layoutInfo?.editable;
|
|
48
48
|
if (!editable) return;
|
|
49
49
|
editable.focus();
|
|
50
50
|
document.execCommand('removeFormat');
|
|
51
51
|
// Also strip inline style attributes which execCommand('removeFormat') misses
|
|
52
|
-
const sel =
|
|
53
|
-
if (sel
|
|
52
|
+
const sel = globalThis.getSelection();
|
|
53
|
+
if (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,14 +115,16 @@ export class BubbleToolbar {
|
|
|
115
115
|
const d6 = this.context.on('contextMenu:hide', () => {
|
|
116
116
|
this._contextMenuOpen = false;
|
|
117
117
|
});
|
|
118
|
-
|
|
118
|
+
const d7 = on(globalThis, 'scroll', () => this._hide(), { passive: true });
|
|
119
|
+
const d8 = on(globalThis, '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
|
|
|
122
124
|
destroy() {
|
|
123
|
-
|
|
125
|
+
this._el?.remove();
|
|
124
126
|
this._el = null;
|
|
125
|
-
|
|
127
|
+
this._picker?.remove();
|
|
126
128
|
this._picker = null;
|
|
127
129
|
this._disposers.forEach((d) => d());
|
|
128
130
|
this._disposers = [];
|
|
@@ -249,31 +251,33 @@ 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) {
|
|
258
261
|
// Save current selection before the picker might shift focus
|
|
259
|
-
const sel =
|
|
260
|
-
if (sel
|
|
262
|
+
const sel = globalThis.getSelection();
|
|
263
|
+
if (sel?.rangeCount > 0) {
|
|
261
264
|
this._savedRange = sel.getRangeAt(0).cloneRange();
|
|
262
265
|
}
|
|
263
266
|
|
|
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
|
-
} else {
|
|
272
|
-
|
|
275
|
+
} else if (palette.contains(noColorBtn)) {
|
|
276
|
+
noColorBtn.remove();
|
|
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.
|
|
@@ -288,7 +292,7 @@ export class BubbleToolbar {
|
|
|
288
292
|
// Align horizontally with the clicked button, clamped to viewport
|
|
289
293
|
const btnRect = anchorBtn.getBoundingClientRect();
|
|
290
294
|
let left = btnRect.left;
|
|
291
|
-
left = Math.max(8, Math.min(left,
|
|
295
|
+
left = Math.max(8, Math.min(left, globalThis.innerWidth - pw - 8));
|
|
292
296
|
|
|
293
297
|
this._picker.style.left = `${left}px`;
|
|
294
298
|
this._picker.style.top = `${top}px`;
|
|
@@ -301,11 +305,11 @@ export class BubbleToolbar {
|
|
|
301
305
|
|
|
302
306
|
/** Restore the saved selection, apply execCommand, update the color strip, then close the picker. */
|
|
303
307
|
_applyColor(type, color) {
|
|
304
|
-
const editable = this.context.layoutInfo
|
|
308
|
+
const editable = this.context.layoutInfo?.editable;
|
|
305
309
|
if (!editable || !this._savedRange) return;
|
|
306
310
|
|
|
307
311
|
editable.focus();
|
|
308
|
-
const sel =
|
|
312
|
+
const sel = globalThis.getSelection();
|
|
309
313
|
sel.removeAllRanges();
|
|
310
314
|
try { sel.addRange(this._savedRange.cloneRange()); } catch (_) { return; }
|
|
311
315
|
|
|
@@ -318,9 +322,9 @@ export class BubbleToolbar {
|
|
|
318
322
|
|
|
319
323
|
// Update the color strip on the corresponding button
|
|
320
324
|
const name = type === 'hiliteColor' ? 'hiliteColor' : 'foreColor';
|
|
321
|
-
const btn = this._el
|
|
322
|
-
const strip = btn
|
|
323
|
-
if (strip) strip.style.background = color === 'transparent' ? 'transparent' : color;
|
|
325
|
+
const btn = this._el?.querySelector(`[data-name="${name}"]`);
|
|
326
|
+
const strip = btn?.querySelector('.an-bubble-color-strip');
|
|
327
|
+
if (strip) /** @type {HTMLElement} */ (strip).style.background = color === 'transparent' ? 'transparent' : color;
|
|
324
328
|
|
|
325
329
|
this._closeColorPicker();
|
|
326
330
|
this._syncActive();
|
|
@@ -346,12 +350,25 @@ export class BubbleToolbar {
|
|
|
346
350
|
let left = rect.left + rect.width / 2 - bw / 2;
|
|
347
351
|
let top = rect.top - bh - gap;
|
|
348
352
|
|
|
349
|
-
left = Math.max(8, Math.min(left,
|
|
353
|
+
left = Math.max(8, Math.min(left, globalThis.innerWidth - bw - 8));
|
|
350
354
|
|
|
351
355
|
if (top < 8) {
|
|
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 > globalThis.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
|
}
|
|
@@ -379,22 +396,22 @@ export class BubbleToolbar {
|
|
|
379
396
|
/** Read the current selection's color and update the color-strip indicators. */
|
|
380
397
|
_syncColorStrips() {
|
|
381
398
|
if (!this._el) return;
|
|
382
|
-
const sel =
|
|
399
|
+
const sel = globalThis.getSelection();
|
|
383
400
|
if (!sel || !sel.rangeCount) return;
|
|
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 =
|
|
404
|
+
const cs = globalThis.getComputedStyle(/** @type {Element} */ (node));
|
|
388
405
|
|
|
389
406
|
const foreBtn = this._el.querySelector('[data-name="foreColor"]');
|
|
390
|
-
const foreStrip = foreBtn
|
|
391
|
-
if (foreStrip) foreStrip.style.background = cs.color || '#000000';
|
|
407
|
+
const foreStrip = foreBtn?.querySelector('.an-bubble-color-strip');
|
|
408
|
+
if (foreStrip) /** @type {HTMLElement} */ (foreStrip).style.background = cs.color || '#000000';
|
|
392
409
|
|
|
393
410
|
const hiliteBtn = this._el.querySelector('[data-name="hiliteColor"]');
|
|
394
|
-
const hiliteStrip = hiliteBtn
|
|
411
|
+
const hiliteStrip = 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
|
|
|
@@ -409,7 +426,7 @@ export class BubbleToolbar {
|
|
|
409
426
|
// Keep toolbar visible while color picker is open
|
|
410
427
|
if (this._picker && this._picker.style.display !== 'none') return;
|
|
411
428
|
|
|
412
|
-
const sel =
|
|
429
|
+
const sel = globalThis.getSelection();
|
|
413
430
|
if (!sel || sel.isCollapsed || !sel.rangeCount) {
|
|
414
431
|
this._hide();
|
|
415
432
|
return;
|
|
@@ -441,8 +458,8 @@ export class BubbleToolbar {
|
|
|
441
458
|
_onMousedown(e) {
|
|
442
459
|
// Hide when clicking outside both the editable, the bubble toolbar, and the color picker
|
|
443
460
|
if (!this._visible) return;
|
|
444
|
-
if (this._el
|
|
445
|
-
if (this._picker
|
|
461
|
+
if (this._el?.contains(e.target)) return;
|
|
462
|
+
if (this._picker?.contains(e.target)) return;
|
|
446
463
|
const editable = this.context.layoutInfo.editable;
|
|
447
464
|
if (editable.contains(e.target)) return;
|
|
448
465
|
this._hide();
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -99,11 +101,11 @@ export const underlineBtn = btn('underline', 'underline', 'Underline (Ctrl+U)',
|
|
|
99
101
|
// also check for a <u> ancestor in the DOM using startContainer for
|
|
100
102
|
// consistent behaviour across both collapsed and range selections.
|
|
101
103
|
if (document.queryCommandState('underline')) return true;
|
|
102
|
-
const sel =
|
|
104
|
+
const sel = globalThis.getSelection();
|
|
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'));
|
|
@@ -177,16 +179,16 @@ export const fontSizeBtn = {
|
|
|
177
179
|
action: (ctx, value) => Style.fontSize(value, ctx.layoutInfo.editable),
|
|
178
180
|
getValue: (ctx) => {
|
|
179
181
|
try {
|
|
180
|
-
const sel =
|
|
182
|
+
const sel = globalThis.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
|
|
189
|
-
const editable = ctx
|
|
191
|
+
const editable = ctx?.layoutInfo?.editable;
|
|
190
192
|
if (editable) return editable.style.fontSize || '';
|
|
191
193
|
return '';
|
|
192
194
|
} catch { return ''; }
|
|
@@ -278,14 +280,14 @@ export const lineHeightBtn = {
|
|
|
278
280
|
action: (_ctx, value) => Style.lineHeight(value),
|
|
279
281
|
getValue: () => {
|
|
280
282
|
try {
|
|
281
|
-
const sel =
|
|
283
|
+
const sel = globalThis.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') || '';
|
|
@@ -139,7 +139,7 @@ export class Clipboard {
|
|
|
139
139
|
// Unwrap — replace el with its children
|
|
140
140
|
const parent = el.parentNode;
|
|
141
141
|
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
142
|
-
|
|
142
|
+
el.remove();
|
|
143
143
|
}
|
|
144
144
|
// Strip class and all data-* attributes from every remaining element
|
|
145
145
|
doc.querySelectorAll('*').forEach((el) => {
|
|
@@ -180,7 +180,7 @@ export class Clipboard {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
_onPaste(event) {
|
|
183
|
-
const clipboardData = event.clipboardData ||
|
|
183
|
+
const clipboardData = event.clipboardData || /** @type {any} */ (globalThis).clipboardData;
|
|
184
184
|
if (!clipboardData) return;
|
|
185
185
|
|
|
186
186
|
// Consume and reset the one-shot plain-paste flag
|
|
@@ -247,7 +247,6 @@ export class Clipboard {
|
|
|
247
247
|
if (this.options.pasteStripAttributes) html = this._stripAttributes(html);
|
|
248
248
|
execCommand('insertHTML', html);
|
|
249
249
|
this.context.invoke('editor.afterCommand');
|
|
250
|
-
return;
|
|
251
250
|
}
|
|
252
251
|
|
|
253
252
|
// Otherwise let the browser handle paste natively
|
|
@@ -300,11 +299,11 @@ export class Clipboard {
|
|
|
300
299
|
}
|
|
301
300
|
|
|
302
301
|
// C2: Reject image formats that browsers cannot decode/display.
|
|
303
|
-
const UNSUPPORTED = ['image/tiff', 'image/x-tiff', 'image/bmp', 'image/x-bmp', 'image/x-ms-bmp'];
|
|
302
|
+
const UNSUPPORTED = new Set(['image/tiff', 'image/x-tiff', 'image/bmp', 'image/x-bmp', 'image/x-ms-bmp']);
|
|
304
303
|
const maxBytes = (this.options.maxImageSize || 5) * 1024 * 1024;
|
|
305
304
|
files.forEach((file) => {
|
|
306
305
|
if (!file || !file.type.startsWith('image/')) return;
|
|
307
|
-
if (UNSUPPORTED.
|
|
306
|
+
if (UNSUPPORTED.has(file.type)) {
|
|
308
307
|
const message = `Image format "${file.type}" is not supported for display in web browsers. Please convert to PNG, JPEG, or WebP first.`;
|
|
309
308
|
this.context.triggerEvent('imageError', { file, message });
|
|
310
309
|
console.warn('[AutumnNote]', message);
|
|
@@ -351,10 +350,10 @@ export class Clipboard {
|
|
|
351
350
|
*/
|
|
352
351
|
_dataUrlToBlob(dataUrl) {
|
|
353
352
|
const [header, b64] = dataUrl.split(',');
|
|
354
|
-
const mime =
|
|
353
|
+
const mime = /:(.*?);/.exec(header)?.[1] ?? 'image/png';
|
|
355
354
|
const binary = atob(b64);
|
|
356
355
|
const arr = new Uint8Array(binary.length);
|
|
357
|
-
for (let i = 0; i < binary.length; i++) arr[i] = binary.
|
|
356
|
+
for (let i = 0; i < binary.length; i++) arr[i] = binary.codePointAt(i);
|
|
358
357
|
return new Blob([arr], { type: mime });
|
|
359
358
|
}
|
|
360
359
|
|
|
@@ -438,7 +437,7 @@ export class Clipboard {
|
|
|
438
437
|
}
|
|
439
438
|
}
|
|
440
439
|
if (!range) return;
|
|
441
|
-
const sel =
|
|
440
|
+
const sel = globalThis.getSelection();
|
|
442
441
|
if (sel) {
|
|
443
442
|
sel.removeAllRanges();
|
|
444
443
|
sel.addRange(range);
|
|
@@ -456,10 +455,10 @@ export class Clipboard {
|
|
|
456
455
|
*/
|
|
457
456
|
_escapeHTML(str) {
|
|
458
457
|
return str
|
|
459
|
-
.
|
|
460
|
-
.
|
|
461
|
-
.
|
|
462
|
-
.
|
|
463
|
-
.
|
|
458
|
+
.replaceAll('&', '&')
|
|
459
|
+
.replaceAll('<', '<')
|
|
460
|
+
.replaceAll('>', '>')
|
|
461
|
+
.replaceAll('"', '"')
|
|
462
|
+
.replaceAll("'", ''');
|
|
464
463
|
}
|
|
465
464
|
}
|
|
@@ -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
|
}),
|
|
@@ -67,7 +68,7 @@ export class CodeTooltip {
|
|
|
67
68
|
this._clearTimers();
|
|
68
69
|
this._disposers.forEach((d) => d());
|
|
69
70
|
this._disposers = [];
|
|
70
|
-
|
|
71
|
+
this._el?.remove();
|
|
71
72
|
this._el = null;
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -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'],
|
|
@@ -225,7 +226,7 @@ export class CodeTooltip {
|
|
|
225
226
|
let left = rect.left + (rect.width - tipW) / 2;
|
|
226
227
|
|
|
227
228
|
if (top < margin) top = rect.bottom + margin;
|
|
228
|
-
if (left + tipW >
|
|
229
|
+
if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
|
|
229
230
|
if (left < margin) left = margin;
|
|
230
231
|
|
|
231
232
|
// Tooltip uses position:fixed, so viewport coordinates are used directly.
|
|
@@ -240,7 +241,7 @@ export class CodeTooltip {
|
|
|
240
241
|
_syncWrapBtn() {
|
|
241
242
|
if (!this._activePre || !this._wrapBtn) return;
|
|
242
243
|
const wrapped = (this._activePre.style.whiteSpace || '').includes('pre-wrap')
|
|
243
|
-
||
|
|
244
|
+
|| globalThis.getComputedStyle(this._activePre).whiteSpace === 'pre-wrap';
|
|
244
245
|
this._wrapBtn.classList.toggle('active', wrapped);
|
|
245
246
|
this._wrapBtn.title = wrapped
|
|
246
247
|
? this.context.locale.tooltips.code.disableWordWrap
|
|
@@ -250,7 +251,7 @@ export class CodeTooltip {
|
|
|
250
251
|
_syncLangSelect() {
|
|
251
252
|
if (!this._activePre || !this._langSelect) return;
|
|
252
253
|
const codeEl = this._activePre.querySelector('code');
|
|
253
|
-
const fromAttr = this._activePre.
|
|
254
|
+
const fromAttr = this._activePre.dataset.language || '';
|
|
254
255
|
const fromClass = codeEl ? (_LANG_CLASS_RE.exec(codeEl.className) || [])[1] || '' : '';
|
|
255
256
|
this._langSelect.value = fromAttr || fromClass || '';
|
|
256
257
|
}
|
|
@@ -273,7 +274,7 @@ export class CodeTooltip {
|
|
|
273
274
|
document.body.appendChild(ta);
|
|
274
275
|
ta.select();
|
|
275
276
|
try { document.execCommand('copy'); this._flashCopied(); } catch (_) {}
|
|
276
|
-
|
|
277
|
+
ta.remove();
|
|
277
278
|
}
|
|
278
279
|
}
|
|
279
280
|
|
|
@@ -300,10 +301,31 @@ 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
|
+
this._activePre = pre;
|
|
315
|
+
if (this._langSelect) this._langSelect.value = lang;
|
|
316
|
+
this._onLangChange();
|
|
317
|
+
// Update the select to reflect the detected language when tooltip is shown
|
|
318
|
+
if (this._langSelect) this._langSelect.value = lang;
|
|
319
|
+
this._activePre = savedPre || pre;
|
|
320
|
+
// Don't restore savedPre if it was null — keep `pre` as activePre so that
|
|
321
|
+
// the tooltip select is correct the first time the user hovers over it.
|
|
322
|
+
}
|
|
323
|
+
|
|
303
324
|
_onLangChange() {
|
|
304
325
|
const pre = this._activePre;
|
|
305
326
|
if (!pre) return;
|
|
306
327
|
const lang = this._langSelect.value;
|
|
328
|
+
const _w = /** @type {any} */ (globalThis);
|
|
307
329
|
|
|
308
330
|
// Ensure a <code> child exists (Prism targets <pre><code class="language-xxx">)
|
|
309
331
|
let codeEl = pre.querySelector('code');
|
|
@@ -318,9 +340,9 @@ export class CodeTooltip {
|
|
|
318
340
|
// Mirror language class on <pre> so Prism CSS theme targets it (pre[class*='language-'])
|
|
319
341
|
pre.className = lang ? `language-${lang}` : '';
|
|
320
342
|
if (lang) {
|
|
321
|
-
pre.
|
|
343
|
+
pre.dataset.language = lang;
|
|
322
344
|
} else {
|
|
323
|
-
pre.
|
|
345
|
+
delete pre.dataset.language;
|
|
324
346
|
}
|
|
325
347
|
|
|
326
348
|
// Trigger Prism if available.
|
|
@@ -328,14 +350,14 @@ export class CodeTooltip {
|
|
|
328
350
|
// which drops <br> entirely, collapsing all lines into one. Convert first.
|
|
329
351
|
const applyPrism = () => {
|
|
330
352
|
codeEl.querySelectorAll('br').forEach((br) => br.replaceWith('\n'));
|
|
331
|
-
|
|
353
|
+
_w.Prism.highlightElement(codeEl);
|
|
332
354
|
this.context.invoke('editor.afterCommand');
|
|
333
355
|
};
|
|
334
356
|
|
|
335
357
|
if (lang) {
|
|
336
|
-
if (
|
|
358
|
+
if (_w.Prism !== undefined) {
|
|
337
359
|
// Grammar already loaded — highlight immediately
|
|
338
|
-
if (
|
|
360
|
+
if (_w.Prism.languages[lang]) {
|
|
339
361
|
applyPrism();
|
|
340
362
|
return;
|
|
341
363
|
}
|
|
@@ -345,7 +367,7 @@ export class CodeTooltip {
|
|
|
345
367
|
} else if (this._prismScript) {
|
|
346
368
|
// Prism core is still loading — highlight once it arrives, then load grammar if needed
|
|
347
369
|
this._prismScript.addEventListener('load', () => {
|
|
348
|
-
if (
|
|
370
|
+
if (_w.Prism.languages[lang]) {
|
|
349
371
|
applyPrism();
|
|
350
372
|
} else {
|
|
351
373
|
this._loadPrismComponent(lang, applyPrism);
|
|
@@ -363,7 +385,8 @@ export class CodeTooltip {
|
|
|
363
385
|
* Called once at initialize time. Fire-and-forget; errors are silent.
|
|
364
386
|
*/
|
|
365
387
|
_ensurePrism() {
|
|
366
|
-
|
|
388
|
+
const _w = /** @type {any} */ (globalThis);
|
|
389
|
+
if (!this.context.options.codeHighlight || _w.Prism) return;
|
|
367
390
|
const cdn = this.context.options.codeHighlightCDN;
|
|
368
391
|
const themeHref = `${cdn}/themes/prism-tomorrow.min.css`;
|
|
369
392
|
const scriptSrc = `${cdn}/prism.min.js`;
|
|
@@ -377,7 +400,7 @@ export class CodeTooltip {
|
|
|
377
400
|
|
|
378
401
|
const existingScript = document.querySelector(`script[src="${scriptSrc}"]`);
|
|
379
402
|
if (existingScript) {
|
|
380
|
-
this._prismScript =
|
|
403
|
+
this._prismScript = _w.Prism ? null : existingScript;
|
|
381
404
|
return;
|
|
382
405
|
}
|
|
383
406
|
|
|
@@ -397,13 +420,14 @@ export class CodeTooltip {
|
|
|
397
420
|
* @param {Function} cb – called once the grammar is ready
|
|
398
421
|
*/
|
|
399
422
|
_loadPrismComponent(lang, cb) {
|
|
423
|
+
const _w = /** @type {any} */ (globalThis);
|
|
400
424
|
const cdn = this.context.options.codeHighlightCDN;
|
|
401
425
|
const src = `${cdn}/components/prism-${lang}.min.js`;
|
|
402
426
|
// Avoid loading the same component twice
|
|
403
427
|
if (document.querySelector(`script[src="${src}"]`)) {
|
|
404
428
|
// Already in DOM — might still be loading; poll briefly then call cb
|
|
405
429
|
const poll = setInterval(() => {
|
|
406
|
-
if (
|
|
430
|
+
if (_w.Prism?.languages[lang]) {
|
|
407
431
|
clearInterval(poll);
|
|
408
432
|
cb();
|
|
409
433
|
}
|
|
@@ -413,7 +437,7 @@ export class CodeTooltip {
|
|
|
413
437
|
}
|
|
414
438
|
const s = document.createElement('script');
|
|
415
439
|
s.src = src;
|
|
416
|
-
s.addEventListener('load', cb, { once: true });
|
|
440
|
+
s.addEventListener('load', /** @type {EventListener} */ (cb), { once: true });
|
|
417
441
|
document.head.appendChild(s);
|
|
418
442
|
}
|
|
419
443
|
|
|
@@ -440,7 +464,7 @@ export class CodeTooltip {
|
|
|
440
464
|
const pre = this._activePre;
|
|
441
465
|
if (!pre) return;
|
|
442
466
|
this._hide();
|
|
443
|
-
|
|
467
|
+
pre.remove();
|
|
444
468
|
this.context.invoke('editor.afterCommand');
|
|
445
469
|
}
|
|
446
470
|
}
|
|
@@ -26,9 +26,7 @@ export class Codeview {
|
|
|
26
26
|
destroy() {
|
|
27
27
|
this._disposers.forEach((d) => d());
|
|
28
28
|
this._disposers = [];
|
|
29
|
-
|
|
30
|
-
this._textarea.parentNode.removeChild(this._textarea);
|
|
31
|
-
}
|
|
29
|
+
this._textarea?.remove();
|
|
32
30
|
this._textarea = null;
|
|
33
31
|
}
|
|
34
32
|
|
|
@@ -53,13 +51,13 @@ export class Codeview {
|
|
|
53
51
|
const { editable } = this.context.layoutInfo;
|
|
54
52
|
const html = editable.innerHTML;
|
|
55
53
|
|
|
56
|
-
this._textarea = createElement('textarea', {
|
|
54
|
+
this._textarea = /** @type {HTMLTextAreaElement} */ (createElement('textarea', {
|
|
57
55
|
class: 'an-codeview',
|
|
58
56
|
spellcheck: 'false',
|
|
59
57
|
autocomplete: 'off',
|
|
60
58
|
autocorrect: 'off',
|
|
61
59
|
autocapitalize: 'off',
|
|
62
|
-
});
|
|
60
|
+
}));
|
|
63
61
|
this._textarea.value = this._prettyPrint(html);
|
|
64
62
|
|
|
65
63
|
editable.style.display = 'none';
|
|
@@ -74,7 +72,7 @@ export class Codeview {
|
|
|
74
72
|
const { editable } = this.context.layoutInfo;
|
|
75
73
|
// Sanitise the HTML typed in the textarea before applying (allow iframes for video embeds)
|
|
76
74
|
editable.innerHTML = sanitiseHTML(this._textarea.value, { allowIframes: true });
|
|
77
|
-
this._textarea.
|
|
75
|
+
this._textarea.remove();
|
|
78
76
|
this._textarea = null;
|
|
79
77
|
editable.style.display = '';
|
|
80
78
|
this._active = false;
|
|
@@ -108,7 +106,7 @@ export class Codeview {
|
|
|
108
106
|
.map((line) => {
|
|
109
107
|
const stripped = line.trim();
|
|
110
108
|
if (!stripped) return '';
|
|
111
|
-
if (
|
|
109
|
+
if (stripped.startsWith('</')) indent = Math.max(0, indent - 1);
|
|
112
110
|
const out = ' '.repeat(indent) + stripped;
|
|
113
111
|
if (
|
|
114
112
|
/^<[^/!][^>]*[^/]>/.test(stripped) &&
|