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,193 +0,0 @@
1
- /**
2
- * VideoDialog.js - Dialog for inserting videos (YouTube, Vimeo, or direct file)
3
- * Supports:
4
- * • YouTube watch URLs → <iframe> embed
5
- * • YouTube short URLs → <iframe> embed
6
- * • Vimeo URLs → <iframe> embed
7
- * • Direct video URLs → <video> element (.mp4 / .webm / .ogg)
8
- */
9
-
10
- import { createElement, on } from '../core/dom.js';
11
- import { sanitiseUrl } from '../core/sanitise.js';
12
- import { BaseDialog } from './BaseDialog.js';
13
-
14
- const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`;
15
-
16
- export class VideoDialog extends BaseDialog {
17
- // ---------------------------------------------------------------------------
18
- // Public API
19
- // ---------------------------------------------------------------------------
20
-
21
- show() {
22
- this._saveRange();
23
- this._urlInput.value = '';
24
- this._widthInput.value = '560';
25
- this._hintEl.textContent = '';
26
- this._open();
27
- }
28
-
29
- // ---------------------------------------------------------------------------
30
- // Build dialog
31
- // ---------------------------------------------------------------------------
32
-
33
- _buildDialog() {
34
- const L = this.context.locale.videoDialog;
35
- const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
36
-
37
- // URL input
38
- const urlLabel = createElement('label', { class: 'an-label' });
39
- urlLabel.textContent = L.videoUrl;
40
- const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
41
- type: 'url',
42
- class: 'an-input',
43
- placeholder: L.urlPlaceholder,
44
- autocomplete: 'off',
45
- }));
46
- this._urlInput = urlInput;
47
- this._firstInput = urlInput;
48
-
49
- // Hint (detected source)
50
- const hintEl = createElement('p', { class: 'an-dialog-hint' });
51
- this._hintEl = hintEl;
52
-
53
- // Width
54
- const widthLabel = createElement('label', { class: 'an-label' });
55
- widthLabel.textContent = L.widthLabel;
56
- const widthInput = /** @type {HTMLInputElement} */ (createElement('input', {
57
- type: 'number',
58
- class: 'an-input',
59
- placeholder: '560',
60
- min: '80',
61
- max: '1920',
62
- value: '560',
63
- }));
64
- this._widthInput = widthInput;
65
-
66
- const btnRow = this._buildButtonRow(L.insertBtn, L.cancelBtn, () => this._onInsert());
67
- box.append(urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
68
-
69
- // Live URL hint
70
- const d0 = on(urlInput, 'input', () => {
71
- const info = this._parseVideoUrl(urlInput.value.trim());
72
- let hint = '';
73
- if (info) { hint = this.context.locale.videoDialog.detected(info.type); }
74
- else if (urlInput.value) { hint = this.context.locale.videoDialog.unknownFormat; }
75
- hintEl.textContent = hint;
76
- });
77
- const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
78
- this._disposers.push(d0, d4);
79
-
80
- return overlay;
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // Actions
85
- // ---------------------------------------------------------------------------
86
-
87
- _onInsert() {
88
- const rawUrl = this._urlInput.value.trim();
89
- const width = Math.max(80, Number.parseInt(this._widthInput.value, 10) || 560);
90
-
91
- if (!rawUrl) {
92
- this._urlInput.focus();
93
- return;
94
- }
95
-
96
- const html = this._buildEmbedHtml(rawUrl, width);
97
- if (!html) {
98
- this._hintEl.textContent = this.context.locale.videoDialog.invalidUrl;
99
- this._urlInput.focus();
100
- return;
101
- }
102
-
103
- if (this._savedRange) this._savedRange.select();
104
- this.context.invoke('editor.insertVideo', html);
105
- this._close();
106
- }
107
-
108
- // ---------------------------------------------------------------------------
109
- // URL parsing & HTML building
110
- // ---------------------------------------------------------------------------
111
-
112
- /**
113
- * Parses a video URL and returns { type, embedUrl } or null.
114
- * @param {string} url
115
- * @returns {{ type: string, embedUrl: string }|null}
116
- */
117
- _parseVideoUrl(url) {
118
- if (!url) return null;
119
- if (!sanitiseUrl(url, { media: true })) return null;
120
-
121
- // YouTube watch: https://www.youtube.com/watch?v=ID
122
- const ytWatch = /(?:youtube\.com\/watch\?(?:.*&)?v=|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/.exec(url);
123
- if (ytWatch) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytWatch[1]}` };
124
-
125
- // YouTube short: https://youtu.be/ID
126
- const ytShort = /youtu\.be\/([a-zA-Z0-9_-]{11})/.exec(url);
127
- if (ytShort) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytShort[1]}` };
128
-
129
- // YouTube Shorts: https://www.youtube.com/shorts/ID
130
- const ytShorts = /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/.exec(url);
131
- if (ytShorts) return { type: 'YouTube Shorts', embedUrl: `https://www.youtube.com/embed/${ytShorts[1]}` };
132
-
133
- // Vimeo: https://vimeo.com/ID
134
- const vimeo = /vimeo\.com\/(\d+)/.exec(url);
135
- if (vimeo) return { type: 'Vimeo', embedUrl: `https://player.vimeo.com/video/${vimeo[1]}` };
136
-
137
- // Direct video file
138
- if (/\.(mp4|webm|ogg|ogv|mov)(#.*|\?.*)?$/i.test(url)) {
139
- return { type: 'Direct video', embedUrl: url };
140
- }
141
-
142
- return null;
143
- }
144
-
145
- /**
146
- * Builds the HTML string to insert.
147
- * @param {string} url
148
- * @param {number} width
149
- * @returns {string|null}
150
- */
151
- _buildEmbedHtml(url, width) {
152
- const info = this._parseVideoUrl(url);
153
- const height = Math.round(width * 9 / 16); // 16:9
154
-
155
- if (info && (info.type === 'YouTube' || info.type === 'YouTube Shorts' || info.type === 'Vimeo')) {
156
- const iframeTitle = `${info.type} video player`;
157
- return (
158
- `<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
159
- `<iframe src="${info.embedUrl}" width="${width}" height="${height}" ` +
160
- `title="${iframeTitle}" ` +
161
- `frameborder="0" allowfullscreen ` +
162
- `allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" ` +
163
- `style="display:block;max-width:100%"></iframe>` +
164
- `<div class="an-video-shield"></div>` +
165
- `</div>`
166
- );
167
- }
168
-
169
- if (info?.type === 'Direct video') {
170
- const src = info.embedUrl.replaceAll('"', '%22');
171
- return (
172
- `<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
173
- `<video src="${src}" width="${width}" height="${height}" controls ` +
174
- `style="display:block;max-width:100%"></video>` +
175
- `<div class="an-video-shield"></div>` +
176
- `</div>`
177
- );
178
- }
179
-
180
- // Unknown URL — let the user try as a direct video
181
- const safeSrc = sanitiseUrl(url, { media: true });
182
- if (!safeSrc) return null;
183
-
184
- const escapedSrc = safeSrc.replaceAll('"', '%22');
185
- return (
186
- `<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
187
- `<video src="${escapedSrc}" width="${width}" height="${height}" controls ` +
188
- `style="display:block;max-width:100%"></video>` +
189
- `<div class="an-video-shield"></div>` +
190
- `</div>`
191
- );
192
- }
193
- }
@@ -1,66 +0,0 @@
1
- // VideoResizer.js - Interactive resize handles for selected videos in the editor
2
- // Targets .an-video-wrapper divs (containing <iframe> or <video>)
3
- import { on } from '../core/dom.js';
4
- import { BaseResizer } from './BaseResizer.js';
5
-
6
- export class VideoResizer extends BaseResizer {
7
- get _overlayClass() { return 'an-video-resizer'; }
8
- get _selectedClass() { return 'an-video-selected'; }
9
-
10
- /**
11
- * Walk up the DOM from `el` to find the nearest .an-video-wrapper,
12
- * or an iframe/video whose parent is .an-video-wrapper.
13
- * @param {EventTarget|null} el
14
- * @returns {HTMLElement|null}
15
- */
16
- _findTarget(el) {
17
- if (!el || !(el instanceof Element)) return null;
18
- if (el.classList?.contains('an-video-wrapper')) return /** @type {HTMLElement} */ (el);
19
- return /** @type {HTMLElement|null} */ (el.closest('.an-video-wrapper'));
20
- }
21
-
22
- /** @param {HTMLElement} wrapper */
23
- _getStartSize(wrapper) {
24
- return { w: wrapper.offsetWidth || 560, h: wrapper.offsetHeight || 315 };
25
- }
26
-
27
- _getMinSize() {
28
- return { minW: 80, minH: 45 };
29
- }
30
-
31
- /** @param {HTMLElement} wrapper @param {number} w @param {number} h */
32
- _applySize(wrapper, w, h) {
33
- // Resize wrapper and inner embed via CSS only — attribute writes are redundant
34
- wrapper.style.width = `${w}px`;
35
- wrapper.style.height = `${h}px`;
36
- const embed = /** @type {HTMLElement|null} */ (wrapper.querySelector('iframe, video'));
37
- if (embed) {
38
- embed.style.width = `${w}px`;
39
- embed.style.height = `${h}px`;
40
- }
41
- }
42
-
43
- _extraListeners() {
44
- const editable = this.context.layoutInfo.editable;
45
- return [
46
- // D1: Prevent native browser drag of video wrappers. Without this, a user
47
- // can hold-and-drag to produce a "copy" that lands outside .an-editable,
48
- // where the .an-video-shield CSS loses its containing context so the
49
- // copied video becomes directly playable.
50
- on(editable, 'dragstart', (e) => {
51
- if (e.target instanceof Element && e.target.closest('.an-video-wrapper')) {
52
- e.preventDefault();
53
- }
54
- }),
55
- ];
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Public API used by other modules
60
- // ---------------------------------------------------------------------------
61
-
62
- /** @returns {HTMLElement|null} */
63
- getActiveWrapper() {
64
- return this._active;
65
- }
66
- }
@@ -1,248 +0,0 @@
1
- // VideoTooltip.js - Hover tooltip for video wrappers inside the editor
2
- // Displays a horizontal action bar below (or above) the selected video,
3
- // similar in appearance and interaction to LinkTooltip.
4
- import { createElement, on } from '../core/dom.js';
5
- import { BaseMediaTooltip } from './BaseMediaTooltip.js';
6
-
7
- const ICONS = {
8
- floatLeft: `<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="2" y="4" width="8" height="8" rx="1"/><line x1="12" y1="6" x2="22" y2="6"/><line x1="12" y1="9" x2="22" y2="9"/><line x1="12" y1="12" x2="22" y2="12"/><line x1="2" y1="16" x2="22" y2="16"/><line x1="2" y1="20" x2="18" y2="20"/></svg>`,
9
- floatRight: `<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="14" y="4" width="8" height="8" rx="1"/><line x1="2" y1="6" x2="12" y2="6"/><line x1="2" y1="9" x2="12" y2="9"/><line x1="2" y1="12" x2="12" y2="12"/><line x1="2" y1="16" x2="22" y2="16"/><line x1="2" y1="20" x2="18" y2="20"/></svg>`,
10
- floatNone: `<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="3" y="3" width="8" height="8" rx="1"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="3" y1="19" x2="17" y2="19"/></svg>`,
11
- alignCenter: `<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="7" y="4" width="10" height="8" rx="1"/><line x1="3" y1="16" x2="21" y2="16"/><line x1="6" y1="20" x2="18" y2="20"/></svg>`,
12
- originalSize:`<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="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`,
13
- deleteVideo: `<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>`,
14
- preview: `<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"><polygon points="5 3 19 12 5 21 5 3"/></svg>`,
15
- };
16
-
17
- export class VideoTooltip extends BaseMediaTooltip {
18
- initialize() {
19
- this._el = this._buildTooltip();
20
- document.body.appendChild(this._el);
21
-
22
- const editable = this.context.layoutInfo.editable;
23
-
24
- this._disposers.push(
25
- on(editable, 'mouseover', (e) => {
26
- if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
27
- // The shield div sits on top of iframes — we detect hover via it or the wrapper
28
- const target = /** @type {Element} */ (e.target);
29
- const wrapper = target?.closest('.an-video-wrapper');
30
- if (wrapper && editable.contains(wrapper)) {
31
- this._scheduleShow(wrapper);
32
- }
33
- }, { passive: true }),
34
- on(editable, 'mouseout', (e) => {
35
- const to = /** @type {MouseEvent} */ (e).relatedTarget;
36
- if (!to || (!editable.contains(/** @type {Node} */ (to)) && !this._el.contains(/** @type {Node} */ (to)))) {
37
- this._scheduleHide();
38
- }
39
- }, { passive: true }),
40
- on(document, 'click', (e) => {
41
- const target = /** @type {Node} */ (e.target);
42
- if (
43
- this._active &&
44
- !this._active.contains(target) &&
45
- !this._el.contains(target)
46
- ) {
47
- this._hide();
48
- }
49
- }),
50
- // Hide when the page scrolls or resizes — the tooltip position becomes stale
51
- on(globalThis, 'scroll', () => this._hide(), { passive: true }),
52
- on(globalThis, 'resize', () => this._hide(), { passive: true }),
53
- );
54
-
55
- return this;
56
- }
57
-
58
- destroy() {
59
- if (this._previewMode) this._exitPreview();
60
- this._clearTimers();
61
- this._disposers.forEach((d) => d());
62
- this._disposers = [];
63
- this._el?.remove();
64
- this._el = null;
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // Base hooks — keep the bar pinned while a preview is playing
69
- // ---------------------------------------------------------------------------
70
-
71
- /** @returns {boolean} */
72
- _canHide() { return !this._previewMode; }
73
-
74
- _beforeHide() {
75
- if (this._previewMode) this._exitPreview();
76
- }
77
-
78
- // ---------------------------------------------------------------------------
79
- // Build
80
- // ---------------------------------------------------------------------------
81
-
82
- _buildTooltip() {
83
- const L = this.context.locale.tooltips.video;
84
- const el = createElement('div', {
85
- class: 'an-link-tooltip an-video-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(createElement('div', { class: 'an-link-tooltip-sep' }));
97
-
98
- this._floatLeftBtn = this._makeBtn(ICONS.floatLeft, L.floatLeft, () => this._setFloat('left'));
99
- this._floatNoneBtn = this._makeBtn(ICONS.floatNone, L.noFloat, () => this._setFloat(''));
100
- this._alignCenterBtn = this._makeBtn(ICONS.alignCenter, L.alignCenter, () => this._setCenter());
101
- this._floatRightBtn = this._makeBtn(ICONS.floatRight, L.floatRight, () => this._setFloat('right'));
102
-
103
- el.appendChild(this._floatLeftBtn);
104
- el.appendChild(this._floatNoneBtn);
105
- el.appendChild(this._alignCenterBtn);
106
- el.appendChild(this._floatRightBtn);
107
-
108
- el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
109
-
110
- this._originalBtn = this._makeBtn(ICONS.originalSize, L.originalSize, () => this._resetSize());
111
-
112
- el.appendChild(this._originalBtn);
113
-
114
- el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
115
-
116
- this._previewBtn = this._makeBtn(ICONS.preview, L.previewVideo, () => this._togglePreview());
117
- el.appendChild(this._previewBtn);
118
-
119
- el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
120
-
121
- this._deleteBtn = this._makeBtn(ICONS.deleteVideo, L.deleteVideo, () => this._delete(), true);
122
-
123
- el.appendChild(this._deleteBtn);
124
-
125
- // Keep tooltip alive while hovering it
126
- this._disposers.push(
127
- on(el, 'mouseenter', () => this._clearTimers()),
128
- on(el, 'mouseleave', () => this._scheduleHide()),
129
- );
130
-
131
- return el;
132
- }
133
-
134
- // ---------------------------------------------------------------------------
135
- // Actions
136
- // ---------------------------------------------------------------------------
137
-
138
- _setFloat(value) {
139
- const wrapper = this._active;
140
- if (!wrapper) return;
141
- wrapper.style.float = value;
142
- wrapper.style.display = value ? 'inline-block' : '';
143
- wrapper.style.marginLeft = value === 'right' ? '12px' : '';
144
- wrapper.style.marginRight = value === 'left' ? '12px' : '';
145
- this.context.invoke('editor.afterCommand');
146
- this.context.invoke('videoResizer.updateOverlay');
147
- requestAnimationFrame(() => { if (this._active) this._positionNear(this._active); });
148
- }
149
-
150
- _setCenter() {
151
- const wrapper = this._active;
152
- if (!wrapper) return;
153
- wrapper.style.float = '';
154
- wrapper.style.display = 'block';
155
- wrapper.style.marginLeft = 'auto';
156
- wrapper.style.marginRight = 'auto';
157
- this.context.invoke('editor.afterCommand');
158
- this.context.invoke('videoResizer.updateOverlay');
159
- requestAnimationFrame(() => { if (this._active) this._positionNear(this._active); });
160
- }
161
-
162
- _resetSize() {
163
- const wrapper = this._active;
164
- if (!wrapper) return;
165
- const embed = /** @type {HTMLElement|null} */ (wrapper.querySelector('iframe, video'));
166
- wrapper.style.width = '';
167
- wrapper.style.height = '';
168
- if (embed) {
169
- embed.removeAttribute('width');
170
- embed.removeAttribute('height');
171
- embed.style.width = '';
172
- embed.style.height = '';
173
- }
174
- this.context.invoke('editor.afterCommand');
175
- this.context.invoke('videoResizer.updateOverlay');
176
- requestAnimationFrame(() => { if (this._active) this._positionNear(this._active); });
177
- }
178
-
179
- _delete() {
180
- const wrapper = this._active;
181
- if (!wrapper) return;
182
- this._hide();
183
- this.context.invoke('videoResizer.deselect');
184
- wrapper.remove();
185
- this.context.invoke('editor.afterCommand');
186
- }
187
-
188
- // ---------------------------------------------------------------------------
189
- // Preview mode — temporarily disables the shield so the video is interactive
190
- // ---------------------------------------------------------------------------
191
-
192
- _togglePreview() {
193
- if (this._previewMode) {
194
- this._exitPreview();
195
- } else {
196
- this._enterPreview();
197
- }
198
- }
199
-
200
- _enterPreview() {
201
- const wrapper = this._active;
202
- if (!wrapper) return;
203
- this._previewMode = true;
204
-
205
- // Hide the shield so the iframe / video receives pointer events directly
206
- const shield = /** @type {HTMLElement|null} */ (wrapper.querySelector('.an-video-shield'));
207
- if (shield) shield.style.display = 'none';
208
-
209
- // Hide the resize overlay — it would block interaction with the embed
210
- this.context.invoke('videoResizer.deselect');
211
-
212
- // Visual feedback: button turns primary-coloured
213
- this._previewBtn.classList.add('an-link-tooltip-btn--copied');
214
- this._previewBtn.title = this.context.locale.tooltips.video.exitPreview;
215
-
216
- // Exit preview on mousedown outside the wrapper.
217
- // Using 'mousedown' (not 'click') so clicks inside an iframe
218
- // — which never bubble to the outer document — don't accidentally
219
- // leave preview mode stuck. For <video> elements, mousedown on the
220
- // video is inside the wrapper so contains() returns true → no exit.
221
- this._previewClickOff = (e) => {
222
- if (!wrapper.contains(e.target) && !this._el.contains(e.target)) {
223
- this._exitPreview();
224
- }
225
- };
226
- document.addEventListener('mousedown', this._previewClickOff, true);
227
- }
228
-
229
- _exitPreview() {
230
- this._previewMode = false;
231
-
232
- const wrapper = this._active;
233
- if (wrapper) {
234
- // Restore the shield
235
- const shield = /** @type {HTMLElement|null} */ (wrapper.querySelector('.an-video-shield'));
236
- if (shield) shield.style.display = '';
237
- }
238
-
239
- // Reset button appearance
240
- this._previewBtn.classList.remove('an-link-tooltip-btn--copied');
241
- this._previewBtn.title = this.context.locale.tooltips.video.previewVideo;
242
-
243
- if (this._previewClickOff) {
244
- document.removeEventListener('mousedown', this._previewClickOff, true);
245
- this._previewClickOff = null;
246
- }
247
- }
248
- }