autumnnote 2.1.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.
Files changed (65) hide show
  1. package/README.md +72 -5
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.es.js +385 -619
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.min.js +20 -20
  6. package/dist/autumnnote.umd.js +20 -20
  7. package/dist/autumnnote.umd.js.map +1 -1
  8. package/dist/icon-data-V0Xqv-wX.js +255 -0
  9. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  10. package/package.json +12 -3
  11. package/types/index.d.ts +8 -0
  12. package/src/js/Context.js +0 -854
  13. package/src/js/core/detectLang.js +0 -98
  14. package/src/js/core/dom.js +0 -372
  15. package/src/js/core/env.js +0 -38
  16. package/src/js/core/key.js +0 -66
  17. package/src/js/core/lists.js +0 -121
  18. package/src/js/core/markdown.js +0 -695
  19. package/src/js/core/range.js +0 -194
  20. package/src/js/core/sanitise.js +0 -304
  21. package/src/js/editing/History.js +0 -266
  22. package/src/js/editing/Style.js +0 -812
  23. package/src/js/editing/Table.js +0 -105
  24. package/src/js/editing/Typing.js +0 -397
  25. package/src/js/index.js +0 -193
  26. package/src/js/index.umd.js +0 -17
  27. package/src/js/module/AutoSaveRestore.js +0 -125
  28. package/src/js/module/BaseDialog.js +0 -133
  29. package/src/js/module/BaseMediaTooltip.js +0 -142
  30. package/src/js/module/BaseResizer.js +0 -322
  31. package/src/js/module/BubbleToolbar.js +0 -483
  32. package/src/js/module/Buttons.js +0 -399
  33. package/src/js/module/Clipboard.js +0 -579
  34. package/src/js/module/CodeTooltip.js +0 -493
  35. package/src/js/module/Codeview.js +0 -125
  36. package/src/js/module/ContextMenu.js +0 -621
  37. package/src/js/module/Editor.js +0 -747
  38. package/src/js/module/EmojiDialog.js +0 -254
  39. package/src/js/module/FindReplace.js +0 -512
  40. package/src/js/module/Fullscreen.js +0 -80
  41. package/src/js/module/IconDialog.js +0 -618
  42. package/src/js/module/ImageCropOverlay.js +0 -586
  43. package/src/js/module/ImageDialog.js +0 -193
  44. package/src/js/module/ImageResizer.js +0 -42
  45. package/src/js/module/ImageTooltip.js +0 -285
  46. package/src/js/module/LinkDialog.js +0 -145
  47. package/src/js/module/LinkTooltip.js +0 -250
  48. package/src/js/module/MarkdownShortcuts.js +0 -250
  49. package/src/js/module/Mention.js +0 -365
  50. package/src/js/module/Placeholder.js +0 -51
  51. package/src/js/module/ShortcutsDialog.js +0 -111
  52. package/src/js/module/SlashMenu.js +0 -376
  53. package/src/js/module/Statusbar.js +0 -246
  54. package/src/js/module/TableTooltip.js +0 -1392
  55. package/src/js/module/Toolbar.js +0 -855
  56. package/src/js/module/VideoDialog.js +0 -193
  57. package/src/js/module/VideoResizer.js +0 -66
  58. package/src/js/module/VideoTooltip.js +0 -248
  59. package/src/js/module/emoji-data.js +0 -496
  60. package/src/js/module/table-grid.js +0 -102
  61. package/src/js/module/table-icons.js +0 -33
  62. package/src/js/renderer.js +0 -120
  63. package/src/js/settings.js +0 -214
  64. package/src/styles/_variables.scss +0 -48
  65. package/src/styles/autumnnote.scss +0 -2877
@@ -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
-