autumnnote 2.0.0 → 2.2.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 +72 -5
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +661 -798
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +20 -20
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +13 -4
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -25
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -231
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -312
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1521
- package/src/js/module/Toolbar.js +0 -750
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- package/src/styles/autumnnote.scss +0 -2866
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
// CodeTooltip.js - Hover tooltip for <pre> code blocks inside the editor
|
|
2
|
-
// Shows a horizontal action bar above (or below) the hovered code block,
|
|
3
|
-
// consistent in appearance and interaction with ImageTooltip / TableTooltip.
|
|
4
|
-
import { createElement, on } from '../core/dom.js';
|
|
5
|
-
|
|
6
|
-
const SHOW_DELAY = 100;
|
|
7
|
-
const HIDE_DELAY = 180;
|
|
8
|
-
|
|
9
|
-
// Cached regex for extracting language class — defined once at module level.
|
|
10
|
-
const _LANG_CLASS_RE = /language-(\S+)/;
|
|
11
|
-
|
|
12
|
-
const ICONS = {
|
|
13
|
-
copy: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
|
|
14
|
-
wrapOn: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><path d="M3 12h15a3 3 0 0 1 0 6H3"/><polyline points="6 15 3 18 6 21"/></svg>`,
|
|
15
|
-
toParagraph:`<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 4v16"/><path d="M17 4v16"/><path d="M9 4h8a4 4 0 0 1 0 8H9V4z"/></svg>`,
|
|
16
|
-
deleteCode: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
|
|
17
|
-
lineNumbers: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-2-2-2"/></svg>`,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export class CodeTooltip {
|
|
21
|
-
/** @param {import('../Context.js').Context} context */
|
|
22
|
-
constructor(context) {
|
|
23
|
-
this.context = context;
|
|
24
|
-
this._el = null;
|
|
25
|
-
this._activePre = null;
|
|
26
|
-
this._showTimer = null;
|
|
27
|
-
this._hideTimer = null;
|
|
28
|
-
this._disposers = [];
|
|
29
|
-
this._copyBtn = null;
|
|
30
|
-
this._wrapBtn = null;
|
|
31
|
-
this._lineNumbersBtn = null;
|
|
32
|
-
this._langSelect = null;
|
|
33
|
-
this._prismScript = null; // script element while Prism is loading
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
initialize() {
|
|
37
|
-
this._el = this._buildTooltip();
|
|
38
|
-
document.body.appendChild(this._el);
|
|
39
|
-
this._ensurePrism();
|
|
40
|
-
|
|
41
|
-
const editable = this.context.layoutInfo.editable;
|
|
42
|
-
|
|
43
|
-
this._disposers.push(
|
|
44
|
-
on(editable, 'mouseover', (e) => {
|
|
45
|
-
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
46
|
-
const pre = /** @type {Element} */ (e.target)?.closest('pre');
|
|
47
|
-
if (pre && editable.contains(pre)) {
|
|
48
|
-
this._scheduleShow(pre);
|
|
49
|
-
}
|
|
50
|
-
}),
|
|
51
|
-
on(editable, 'mouseout', (e) => {
|
|
52
|
-
const to = /** @type {Node|null} */ (/** @type {MouseEvent} */ (e).relatedTarget);
|
|
53
|
-
if (!to || (!editable.contains(to) && !this._el.contains(to))) {
|
|
54
|
-
this._scheduleHide();
|
|
55
|
-
}
|
|
56
|
-
}),
|
|
57
|
-
on(document, 'click', (e) => {
|
|
58
|
-
const et = /** @type {Node} */ (e.target);
|
|
59
|
-
if (this._activePre &&
|
|
60
|
-
!this._activePre.contains(et) &&
|
|
61
|
-
!this._el.contains(et)) {
|
|
62
|
-
this._hide();
|
|
63
|
-
}
|
|
64
|
-
}),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
return this;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
destroy() {
|
|
71
|
-
this._clearTimers();
|
|
72
|
-
this._disposers.forEach((d) => d());
|
|
73
|
-
this._disposers = [];
|
|
74
|
-
this._el?.remove();
|
|
75
|
-
this._el = null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// Build
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
_buildTooltip() {
|
|
83
|
-
const L = this.context.locale.tooltips.code;
|
|
84
|
-
const el = createElement('div', {
|
|
85
|
-
class: 'an-link-tooltip an-code-tooltip',
|
|
86
|
-
role: 'toolbar',
|
|
87
|
-
'aria-label': L.ariaLabel,
|
|
88
|
-
});
|
|
89
|
-
el.style.display = 'none';
|
|
90
|
-
|
|
91
|
-
// Label
|
|
92
|
-
this._label = createElement('span', { class: 'an-link-tooltip-url' });
|
|
93
|
-
this._label.textContent = L.label;
|
|
94
|
-
el.appendChild(this._label);
|
|
95
|
-
|
|
96
|
-
el.appendChild(this._sep());
|
|
97
|
-
|
|
98
|
-
// Language selector
|
|
99
|
-
this._langSelect = /** @type {HTMLSelectElement} */ (createElement('select', {
|
|
100
|
-
class: 'an-code-lang-select',
|
|
101
|
-
title: L.syntaxLanguage,
|
|
102
|
-
'aria-label': L.syntaxAriaLabel,
|
|
103
|
-
}));
|
|
104
|
-
const LANGUAGES = [
|
|
105
|
-
['', 'Plain text'], ['javascript', 'JavaScript'], ['typescript', 'TypeScript'],
|
|
106
|
-
['python', 'Python'], ['html', 'HTML'], ['css', 'CSS'], ['scss', 'SCSS'],
|
|
107
|
-
['json', 'JSON'], ['xml', 'XML'], ['bash', 'Bash / Shell'], ['sql', 'SQL'],
|
|
108
|
-
['java', 'Java'], ['csharp', 'C#'], ['php', 'PHP'], ['ruby', 'Ruby'],
|
|
109
|
-
['go', 'Go'], ['rust', 'Rust'], ['cpp', 'C++'], ['c', 'C'],
|
|
110
|
-
['kotlin', 'Kotlin'], ['swift', 'Swift'],
|
|
111
|
-
];
|
|
112
|
-
LANGUAGES.forEach(([value, label]) => {
|
|
113
|
-
const opt = createElement('option', { value });
|
|
114
|
-
opt.textContent = label;
|
|
115
|
-
this._langSelect.appendChild(opt);
|
|
116
|
-
});
|
|
117
|
-
this._disposers.push(on(this._langSelect, 'change', () => this._onLangChange()));
|
|
118
|
-
el.appendChild(this._langSelect);
|
|
119
|
-
|
|
120
|
-
el.appendChild(this._sep());
|
|
121
|
-
|
|
122
|
-
// Copy code
|
|
123
|
-
this._copyBtn = this._makeBtn(ICONS.copy, L.copyCode, () => this._copyCode());
|
|
124
|
-
|
|
125
|
-
el.appendChild(this._copyBtn);
|
|
126
|
-
|
|
127
|
-
el.appendChild(this._sep());
|
|
128
|
-
|
|
129
|
-
// Toggle word-wrap
|
|
130
|
-
this._wrapBtn = this._makeBtn(ICONS.wrapOn, L.toggleWordWrap, () => this._toggleWrap());
|
|
131
|
-
el.appendChild(this._wrapBtn);
|
|
132
|
-
|
|
133
|
-
// Toggle line numbers
|
|
134
|
-
this._lineNumbersBtn = this._makeBtn(ICONS.lineNumbers, L.lineNumbers, () => this._toggleLineNumbers());
|
|
135
|
-
el.appendChild(this._lineNumbersBtn);
|
|
136
|
-
|
|
137
|
-
el.appendChild(this._sep());
|
|
138
|
-
|
|
139
|
-
// Convert to normal paragraph
|
|
140
|
-
el.appendChild(this._makeBtn(ICONS.toParagraph, L.convertToParagraph, () => this._toParagraph()));
|
|
141
|
-
|
|
142
|
-
el.appendChild(this._sep());
|
|
143
|
-
|
|
144
|
-
// Delete block
|
|
145
|
-
el.appendChild(this._makeBtn(ICONS.deleteCode, L.deleteCodeBlock, () => this._delete(), true));
|
|
146
|
-
|
|
147
|
-
// Keep tooltip alive while hovering it
|
|
148
|
-
this._disposers.push(
|
|
149
|
-
on(el, 'mouseenter', () => this._clearTimers()),
|
|
150
|
-
on(el, 'mouseleave', () => this._scheduleHide()),
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
return el;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
_sep() {
|
|
157
|
-
return createElement('div', { class: 'an-link-tooltip-sep' });
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* @param {string} icon
|
|
162
|
-
* @param {string} title
|
|
163
|
-
* @param {Function} handler
|
|
164
|
-
* @param {boolean} [isDanger]
|
|
165
|
-
*/
|
|
166
|
-
_makeBtn(icon, title, handler, isDanger = false) {
|
|
167
|
-
const btn = createElement('button', {
|
|
168
|
-
type: 'button',
|
|
169
|
-
class: isDanger ? 'an-link-tooltip-btn an-link-tooltip-btn--danger' : 'an-link-tooltip-btn',
|
|
170
|
-
title,
|
|
171
|
-
});
|
|
172
|
-
btn.innerHTML = icon;
|
|
173
|
-
this._disposers.push(on(btn, 'click', (e) => {
|
|
174
|
-
e.preventDefault();
|
|
175
|
-
e.stopPropagation();
|
|
176
|
-
handler();
|
|
177
|
-
}));
|
|
178
|
-
return btn;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
// Show / Hide
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
|
|
185
|
-
_scheduleShow(pre) {
|
|
186
|
-
if (this._activePre === pre && this._el.style.display !== 'none') return;
|
|
187
|
-
clearTimeout(this._hideTimer);
|
|
188
|
-
this._hideTimer = null;
|
|
189
|
-
clearTimeout(this._showTimer);
|
|
190
|
-
this._showTimer = setTimeout(() => {
|
|
191
|
-
this._activePre = pre;
|
|
192
|
-
this._syncWrapBtn();
|
|
193
|
-
this._syncLineNumbersBtn();
|
|
194
|
-
this._syncLangSelect();
|
|
195
|
-
this._show(pre);
|
|
196
|
-
}, SHOW_DELAY);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
_scheduleHide() {
|
|
200
|
-
clearTimeout(this._showTimer);
|
|
201
|
-
this._showTimer = null;
|
|
202
|
-
// Always reset the hide timer so rapid mouseout→mouseover sequences
|
|
203
|
-
// don't leave a stale timer that hides the tooltip prematurely.
|
|
204
|
-
clearTimeout(this._hideTimer);
|
|
205
|
-
this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
_show(pre) {
|
|
209
|
-
this._el.style.display = 'flex';
|
|
210
|
-
this._positionNear(pre);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
_hide() {
|
|
214
|
-
this._el.style.display = 'none';
|
|
215
|
-
this._activePre = null;
|
|
216
|
-
this._clearTimers();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
_clearTimers() {
|
|
220
|
-
clearTimeout(this._showTimer);
|
|
221
|
-
clearTimeout(this._hideTimer);
|
|
222
|
-
this._showTimer = null;
|
|
223
|
-
this._hideTimer = null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
_positionNear(pre) {
|
|
227
|
-
const rect = pre.getBoundingClientRect();
|
|
228
|
-
const tipW = this._el.offsetWidth || 260;
|
|
229
|
-
const tipH = this._el.offsetHeight || 32;
|
|
230
|
-
const margin = 6;
|
|
231
|
-
|
|
232
|
-
// Prefer above the block; fall back to below
|
|
233
|
-
let top = rect.top - tipH - margin;
|
|
234
|
-
let left = rect.left + (rect.width - tipW) / 2;
|
|
235
|
-
|
|
236
|
-
if (top < margin) top = rect.bottom + margin;
|
|
237
|
-
if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
|
|
238
|
-
if (left < margin) left = margin;
|
|
239
|
-
|
|
240
|
-
// Tooltip uses position:fixed, so viewport coordinates are used directly.
|
|
241
|
-
this._el.style.top = `${top}px`;
|
|
242
|
-
this._el.style.left = `${left}px`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ---------------------------------------------------------------------------
|
|
246
|
-
// Sync wrap-button active state
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
|
|
249
|
-
_syncWrapBtn() {
|
|
250
|
-
if (!this._activePre || !this._wrapBtn) return;
|
|
251
|
-
const wrapped = (this._activePre.style.whiteSpace || '').includes('pre-wrap')
|
|
252
|
-
|| globalThis.getComputedStyle(this._activePre).whiteSpace === 'pre-wrap';
|
|
253
|
-
this._wrapBtn.classList.toggle('active', wrapped);
|
|
254
|
-
this._wrapBtn.title = wrapped
|
|
255
|
-
? this.context.locale.tooltips.code.disableWordWrap
|
|
256
|
-
: this.context.locale.tooltips.code.enableWordWrap;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
_syncLineNumbersBtn() {
|
|
260
|
-
if (!this._activePre || !this._lineNumbersBtn) return;
|
|
261
|
-
const on = this._activePre.classList.contains('an-code-line-numbers');
|
|
262
|
-
this._lineNumbersBtn.classList.toggle('active', on);
|
|
263
|
-
this._lineNumbersBtn.title = this.context.locale.tooltips.code.lineNumbers;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
_syncLangSelect() {
|
|
267
|
-
if (!this._activePre || !this._langSelect) return;
|
|
268
|
-
const codeEl = this._activePre.querySelector('code');
|
|
269
|
-
const fromAttr = this._activePre.dataset.language || '';
|
|
270
|
-
const fromClass = codeEl ? (_LANG_CLASS_RE.exec(codeEl.className) || [])[1] || '' : '';
|
|
271
|
-
this._langSelect.value = fromAttr || fromClass || '';
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ---------------------------------------------------------------------------
|
|
275
|
-
// Actions
|
|
276
|
-
// ---------------------------------------------------------------------------
|
|
277
|
-
|
|
278
|
-
_copyCode() {
|
|
279
|
-
const pre = this._activePre;
|
|
280
|
-
if (!pre) return;
|
|
281
|
-
const text = pre.textContent || '';
|
|
282
|
-
if (navigator.clipboard) {
|
|
283
|
-
navigator.clipboard.writeText(text).then(() => this._flashCopied()).catch(() => {});
|
|
284
|
-
} else {
|
|
285
|
-
// Fallback for older browsers
|
|
286
|
-
const ta = document.createElement('textarea');
|
|
287
|
-
ta.value = text;
|
|
288
|
-
ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0';
|
|
289
|
-
document.body.appendChild(ta);
|
|
290
|
-
ta.select();
|
|
291
|
-
try { document.execCommand('copy'); this._flashCopied(); } catch (_) { void _; }
|
|
292
|
-
ta.remove();
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
_flashCopied() {
|
|
297
|
-
if (!this._copyBtn) return;
|
|
298
|
-
const original = this._copyBtn.innerHTML;
|
|
299
|
-
this._copyBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
300
|
-
this._copyBtn.classList.add('an-link-tooltip-btn--copied');
|
|
301
|
-
setTimeout(() => {
|
|
302
|
-
if (this._copyBtn) {
|
|
303
|
-
this._copyBtn.innerHTML = original;
|
|
304
|
-
this._copyBtn.classList.remove('an-link-tooltip-btn--copied');
|
|
305
|
-
}
|
|
306
|
-
}, 1400);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
_toggleWrap() {
|
|
310
|
-
const pre = this._activePre;
|
|
311
|
-
if (!pre) return;
|
|
312
|
-
const isWrapped = pre.style.whiteSpace === 'pre-wrap';
|
|
313
|
-
pre.style.whiteSpace = isWrapped ? 'pre' : 'pre-wrap';
|
|
314
|
-
this._syncWrapBtn();
|
|
315
|
-
this.context.invoke('editor.afterCommand');
|
|
316
|
-
this._positionNear(pre);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
_toggleLineNumbers() {
|
|
320
|
-
const pre = this._activePre;
|
|
321
|
-
if (!pre) return;
|
|
322
|
-
pre.classList.toggle('an-code-line-numbers');
|
|
323
|
-
this._syncLineNumbersBtn();
|
|
324
|
-
this.context.invoke('editor.afterCommand');
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Applies a language to a given <pre> element: sets classes, data-language,
|
|
329
|
-
* and triggers Prism highlighting. Called by the auto-detect flow.
|
|
330
|
-
* @param {HTMLElement} pre
|
|
331
|
-
* @param {string} lang - Prism language identifier, e.g. 'javascript'
|
|
332
|
-
*/
|
|
333
|
-
applyLanguage(pre, lang) {
|
|
334
|
-
if (!pre || !lang) return;
|
|
335
|
-
// Temporarily set activePre so _onLangChange can target it
|
|
336
|
-
const savedPre = this._activePre;
|
|
337
|
-
this._activePre = pre;
|
|
338
|
-
if (this._langSelect) this._langSelect.value = lang;
|
|
339
|
-
this._onLangChange();
|
|
340
|
-
// Update the select to reflect the detected language when tooltip is shown
|
|
341
|
-
if (this._langSelect) this._langSelect.value = lang;
|
|
342
|
-
this._activePre = savedPre || pre;
|
|
343
|
-
// Don't restore savedPre if it was null — keep `pre` as activePre so that
|
|
344
|
-
// the tooltip select is correct the first time the user hovers over it.
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
_onLangChange() {
|
|
348
|
-
const pre = this._activePre;
|
|
349
|
-
if (!pre) return;
|
|
350
|
-
const lang = this._langSelect.value;
|
|
351
|
-
const _w = /** @type {any} */ (globalThis);
|
|
352
|
-
|
|
353
|
-
// Ensure a <code> child exists (Prism targets <pre><code class="language-xxx">)
|
|
354
|
-
let codeEl = pre.querySelector('code');
|
|
355
|
-
if (!codeEl) {
|
|
356
|
-
codeEl = document.createElement('code');
|
|
357
|
-
codeEl.innerHTML = pre.innerHTML;
|
|
358
|
-
pre.innerHTML = '';
|
|
359
|
-
pre.appendChild(codeEl);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
codeEl.className = lang ? `language-${lang}` : '';
|
|
363
|
-
// Mirror language class on <pre> so Prism CSS theme targets it (pre[class*='language-'])
|
|
364
|
-
pre.className = lang ? `language-${lang}` : '';
|
|
365
|
-
if (lang) {
|
|
366
|
-
pre.dataset.language = lang;
|
|
367
|
-
} else {
|
|
368
|
-
delete pre.dataset.language;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Trigger Prism if available.
|
|
372
|
-
// contenteditable stores line breaks as <br> elements; Prism reads textContent
|
|
373
|
-
// which drops <br> entirely, collapsing all lines into one. Convert first.
|
|
374
|
-
const applyPrism = () => {
|
|
375
|
-
codeEl.querySelectorAll('br').forEach((br) => br.replaceWith('\n'));
|
|
376
|
-
_w.Prism.highlightElement(codeEl);
|
|
377
|
-
this.context.invoke('editor.afterCommand');
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
if (lang) {
|
|
381
|
-
if (_w.Prism !== undefined) {
|
|
382
|
-
// Grammar already loaded — highlight immediately
|
|
383
|
-
if (_w.Prism.languages[lang]) {
|
|
384
|
-
applyPrism();
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
// Grammar not in core bundle — load the component script first
|
|
388
|
-
this._loadPrismComponent(lang, applyPrism);
|
|
389
|
-
return;
|
|
390
|
-
} else if (this._prismScript) {
|
|
391
|
-
// Prism core is still loading — highlight once it arrives, then load grammar if needed
|
|
392
|
-
this._prismScript.addEventListener('load', () => {
|
|
393
|
-
if (_w.Prism.languages[lang]) {
|
|
394
|
-
applyPrism();
|
|
395
|
-
} else {
|
|
396
|
-
this._loadPrismComponent(lang, applyPrism);
|
|
397
|
-
}
|
|
398
|
-
}, { once: true });
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
this.context.invoke('editor.afterCommand');
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Loads Prism.js + CSS from CDN when options.codeHighlight is true.
|
|
408
|
-
* Called once at initialize time. Fire-and-forget; errors are silent.
|
|
409
|
-
*/
|
|
410
|
-
_ensurePrism() {
|
|
411
|
-
const _w = /** @type {any} */ (globalThis);
|
|
412
|
-
if (!this.context.options.codeHighlight || _w.Prism) return;
|
|
413
|
-
const cdn = this.context.options.codeHighlightCDN;
|
|
414
|
-
const themeHref = `${cdn}/themes/prism-tomorrow.min.css`;
|
|
415
|
-
const scriptSrc = `${cdn}/prism.min.js`;
|
|
416
|
-
|
|
417
|
-
if (!document.querySelector(`link[href="${themeHref}"]`)) {
|
|
418
|
-
const link = document.createElement('link');
|
|
419
|
-
link.rel = 'stylesheet';
|
|
420
|
-
link.href = themeHref;
|
|
421
|
-
document.head.appendChild(link);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const existingScript = document.querySelector(`script[src="${scriptSrc}"]`);
|
|
425
|
-
if (existingScript) {
|
|
426
|
-
this._prismScript = _w.Prism ? null : existingScript;
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const script = document.createElement('script');
|
|
431
|
-
script.dataset.manual = ''; // prevent auto-highlight on load
|
|
432
|
-
script.src = scriptSrc;
|
|
433
|
-
this._prismScript = script;
|
|
434
|
-
script.addEventListener('load', () => { this._prismScript = null; }, { once: true });
|
|
435
|
-
document.head.appendChild(script);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Dynamically loads a Prism language component script if not already available.
|
|
440
|
-
* Prism core only bundles: markup, css, clike, javascript.
|
|
441
|
-
* All other grammars (bash, python, etc.) need a component file.
|
|
442
|
-
* @param {string} lang – Prism language slug (e.g. 'bash', 'python')
|
|
443
|
-
* @param {Function} cb – called once the grammar is ready
|
|
444
|
-
*/
|
|
445
|
-
_loadPrismComponent(lang, cb) {
|
|
446
|
-
const _w = /** @type {any} */ (globalThis);
|
|
447
|
-
const cdn = this.context.options.codeHighlightCDN;
|
|
448
|
-
const src = `${cdn}/components/prism-${lang}.min.js`;
|
|
449
|
-
// Avoid loading the same component twice
|
|
450
|
-
if (document.querySelector(`script[src="${src}"]`)) {
|
|
451
|
-
// Already in DOM — might still be loading; poll briefly then call cb
|
|
452
|
-
const poll = setInterval(() => {
|
|
453
|
-
if (_w.Prism?.languages[lang]) {
|
|
454
|
-
clearInterval(poll);
|
|
455
|
-
cb();
|
|
456
|
-
}
|
|
457
|
-
}, 50);
|
|
458
|
-
setTimeout(() => clearInterval(poll), 3000); // give up after 3s
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
const s = document.createElement('script');
|
|
462
|
-
s.src = src;
|
|
463
|
-
s.addEventListener('load', /** @type {EventListener} */ (cb), { once: true });
|
|
464
|
-
document.head.appendChild(s);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
_toParagraph() {
|
|
468
|
-
const pre = this._activePre;
|
|
469
|
-
if (!pre) return;
|
|
470
|
-
const editable = this.context.layoutInfo.editable;
|
|
471
|
-
if (!editable) return;
|
|
472
|
-
|
|
473
|
-
// Replace <pre> with <p> preserving lines as <br>
|
|
474
|
-
const lines = (pre.textContent || '').split('\n');
|
|
475
|
-
const p = document.createElement('p');
|
|
476
|
-
lines.forEach((line, i) => {
|
|
477
|
-
if (i > 0) p.appendChild(document.createElement('br'));
|
|
478
|
-
p.appendChild(document.createTextNode(line));
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
pre.parentNode.replaceChild(p, pre);
|
|
482
|
-
this._hide();
|
|
483
|
-
this.context.invoke('editor.afterCommand');
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
_delete() {
|
|
487
|
-
const pre = this._activePre;
|
|
488
|
-
if (!pre) return;
|
|
489
|
-
this._hide();
|
|
490
|
-
pre.remove();
|
|
491
|
-
this.context.invoke('editor.afterCommand');
|
|
492
|
-
}
|
|
493
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codeview.js - HTML source code view / WYSIWYG toggle
|
|
3
|
-
* Inspired by Summernote's Codeview module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createElement } from '../core/dom.js';
|
|
7
|
-
import { sanitiseHTML } from '../core/sanitise.js';
|
|
8
|
-
|
|
9
|
-
export class Codeview {
|
|
10
|
-
/**
|
|
11
|
-
* @param {import('../Context.js').Context} context
|
|
12
|
-
*/
|
|
13
|
-
constructor(context) {
|
|
14
|
-
this.context = context;
|
|
15
|
-
this.options = context.options;
|
|
16
|
-
this._active = false;
|
|
17
|
-
/** @type {HTMLTextAreaElement|null} */
|
|
18
|
-
this._textarea = null;
|
|
19
|
-
this._disposers = [];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
initialize() {
|
|
23
|
-
return this;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
destroy() {
|
|
27
|
-
this._disposers.forEach((d) => d());
|
|
28
|
-
this._disposers = [];
|
|
29
|
-
this._textarea?.remove();
|
|
30
|
-
this._textarea = null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Public API
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
toggle() {
|
|
38
|
-
if (this._active) {
|
|
39
|
-
this.deactivate();
|
|
40
|
-
} else {
|
|
41
|
-
this.activate();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
isActive() {
|
|
46
|
-
return this._active;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
activate() {
|
|
50
|
-
if (this._active) return;
|
|
51
|
-
const { editable } = this.context.layoutInfo;
|
|
52
|
-
const html = editable.innerHTML;
|
|
53
|
-
|
|
54
|
-
this._textarea = /** @type {HTMLTextAreaElement} */ (createElement('textarea', {
|
|
55
|
-
class: 'an-codeview',
|
|
56
|
-
spellcheck: 'false',
|
|
57
|
-
autocomplete: 'off',
|
|
58
|
-
autocorrect: 'off',
|
|
59
|
-
autocapitalize: 'off',
|
|
60
|
-
}));
|
|
61
|
-
this._textarea.value = this._prettyPrint(html);
|
|
62
|
-
|
|
63
|
-
editable.style.display = 'none';
|
|
64
|
-
editable.parentNode.insertBefore(this._textarea, editable.nextSibling);
|
|
65
|
-
|
|
66
|
-
this._active = true;
|
|
67
|
-
this.context.invoke('toolbar.refresh');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
deactivate() {
|
|
71
|
-
if (!this._active || !this._textarea) return;
|
|
72
|
-
const { editable } = this.context.layoutInfo;
|
|
73
|
-
// Sanitise the HTML typed in the textarea before applying (allow iframes for video embeds)
|
|
74
|
-
editable.innerHTML = sanitiseHTML(this._textarea.value, { allowIframes: true });
|
|
75
|
-
this._textarea.remove();
|
|
76
|
-
this._textarea = null;
|
|
77
|
-
editable.style.display = '';
|
|
78
|
-
this._active = false;
|
|
79
|
-
this.context.invoke('toolbar.refresh');
|
|
80
|
-
this.context.invoke('editor.afterCommand');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Helpers
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Very simple HTML pretty-printer (indent nested tags).
|
|
89
|
-
* Inline elements (a, span, strong, em, etc.) are kept on one line to
|
|
90
|
-
* avoid injecting whitespace that would change rendered text.
|
|
91
|
-
* @param {string} html
|
|
92
|
-
* @returns {string}
|
|
93
|
-
*/
|
|
94
|
-
_prettyPrint(html) {
|
|
95
|
-
// Tags whose content must NOT be split with newlines (breaks text rendering)
|
|
96
|
-
const INLINE_RE = /^<(a|abbr|b|bdo|br|button|cite|code|dfn|em|i|img|input|kbd|label|output|q|samp|select|small|span|strong|sub|sup|textarea|time|tt|u|var)([\s>/])/i;
|
|
97
|
-
let indent = 0;
|
|
98
|
-
return html
|
|
99
|
-
.replace(/>\s*</g, (match, offset, str) => {
|
|
100
|
-
// Check what comes after the '>' — if it starts an inline tag, keep on same line
|
|
101
|
-
const remaining = str.slice(offset + 1);
|
|
102
|
-
if (INLINE_RE.test(remaining.trimStart())) return '><';
|
|
103
|
-
return '>\n<';
|
|
104
|
-
})
|
|
105
|
-
.split('\n')
|
|
106
|
-
.map((line) => {
|
|
107
|
-
const stripped = line.trim();
|
|
108
|
-
if (!stripped) return '';
|
|
109
|
-
if (stripped.startsWith('</')) indent = Math.max(0, indent - 1);
|
|
110
|
-
const out = ' '.repeat(indent) + stripped;
|
|
111
|
-
if (
|
|
112
|
-
/^<[^/!][^>]*[^/]>/.test(stripped) &&
|
|
113
|
-
!INLINE_RE.test(stripped) &&
|
|
114
|
-
!/^<(br|hr|img|input|link|meta)/.test(stripped)
|
|
115
|
-
) {
|
|
116
|
-
indent++;
|
|
117
|
-
}
|
|
118
|
-
return out;
|
|
119
|
-
})
|
|
120
|
-
.filter(Boolean)
|
|
121
|
-
.join('\n');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|