autumnnote 2.1.0 → 2.3.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 +74 -6
- package/dist/autumnnote.cjs +27 -22
- package/dist/autumnnote.es.js +474 -591
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +27 -22
- package/dist/autumnnote.umd.js +27 -22
- 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 +12 -3
- 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 -38
- 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 -304
- 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 -322
- 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 -1392
- package/src/js/module/Toolbar.js +0 -855
- 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/module/table-grid.js +0 -102
- package/src/js/module/table-icons.js +0 -33
- 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 -2877
|
@@ -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
|
-
}
|