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