autumnnote 1.0.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/LICENSE +21 -0
- package/README.md +874 -0
- package/dist/autumnnote.css +1 -0
- package/dist/autumnnote.es.js +5888 -0
- package/dist/autumnnote.es.js.map +1 -0
- package/dist/autumnnote.umd.js +74 -0
- package/dist/autumnnote.umd.js.map +1 -0
- package/package.json +55 -0
- package/src/js/Context.js +497 -0
- package/src/js/core/dom.js +315 -0
- package/src/js/core/env.js +25 -0
- package/src/js/core/func.js +153 -0
- package/src/js/core/key.js +66 -0
- package/src/js/core/lists.js +121 -0
- package/src/js/core/markdown.js +294 -0
- package/src/js/core/range.js +194 -0
- package/src/js/core/sanitise.js +78 -0
- package/src/js/editing/History.js +205 -0
- package/src/js/editing/Style.js +329 -0
- package/src/js/editing/Table.js +59 -0
- package/src/js/editing/Typing.js +142 -0
- package/src/js/index.js +126 -0
- package/src/js/module/Buttons.js +300 -0
- package/src/js/module/Clipboard.js +460 -0
- package/src/js/module/CodeTooltip.js +428 -0
- package/src/js/module/Codeview.js +122 -0
- package/src/js/module/ContextMenu.js +470 -0
- package/src/js/module/Editor.js +528 -0
- package/src/js/module/EmojiDialog.js +726 -0
- package/src/js/module/FindReplace.js +440 -0
- package/src/js/module/Fullscreen.js +80 -0
- package/src/js/module/IconDialog.js +620 -0
- package/src/js/module/ImageDialog.js +208 -0
- package/src/js/module/ImageResizer.js +216 -0
- package/src/js/module/ImageTooltip.js +286 -0
- package/src/js/module/LinkDialog.js +204 -0
- package/src/js/module/LinkTooltip.js +242 -0
- package/src/js/module/Placeholder.js +44 -0
- package/src/js/module/ShortcutsDialog.js +141 -0
- package/src/js/module/Statusbar.js +238 -0
- package/src/js/module/TableTooltip.js +568 -0
- package/src/js/module/Toolbar.js +562 -0
- package/src/js/module/VideoDialog.js +263 -0
- package/src/js/module/VideoResizer.js +227 -0
- package/src/js/module/VideoTooltip.js +252 -0
- package/src/js/renderer.js +107 -0
- package/src/js/settings.js +134 -0
- package/src/styles/_variables.scss +48 -0
- package/src/styles/autumnnote.scss +1740 -0
- package/types/index.d.ts +324 -0
|
@@ -0,0 +1,263 @@
|
|
|
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, trapFocus } from '../core/dom.js';
|
|
11
|
+
import { withSavedRange } from '../core/range.js';
|
|
12
|
+
|
|
13
|
+
export class VideoDialog {
|
|
14
|
+
/** @param {import('../Context.js').Context} context */
|
|
15
|
+
constructor(context) {
|
|
16
|
+
this.context = context;
|
|
17
|
+
this.options = context.options;
|
|
18
|
+
/** @type {HTMLElement|null} */
|
|
19
|
+
this._dialog = null;
|
|
20
|
+
this._disposers = [];
|
|
21
|
+
this._savedRange = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Lifecycle
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
initialize() {
|
|
29
|
+
this._dialog = this._buildDialog();
|
|
30
|
+
document.body.appendChild(this._dialog);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
destroy() {
|
|
35
|
+
this._disposers.forEach((d) => d());
|
|
36
|
+
this._disposers = [];
|
|
37
|
+
if (this._dialog && this._dialog.parentNode) {
|
|
38
|
+
this._dialog.parentNode.removeChild(this._dialog);
|
|
39
|
+
}
|
|
40
|
+
this._dialog = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Public API
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
show() {
|
|
48
|
+
withSavedRange((range) => {
|
|
49
|
+
this._savedRange = range;
|
|
50
|
+
});
|
|
51
|
+
this._urlInput.value = '';
|
|
52
|
+
this._widthInput.value = '560';
|
|
53
|
+
this._hintEl.textContent = '';
|
|
54
|
+
this._open();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Build dialog
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
_buildDialog() {
|
|
62
|
+
const overlay = createElement('div', {
|
|
63
|
+
class: 'an-dialog-overlay',
|
|
64
|
+
role: 'dialog',
|
|
65
|
+
'aria-modal': 'true',
|
|
66
|
+
'aria-label': 'Insert video',
|
|
67
|
+
});
|
|
68
|
+
const box = createElement('div', { class: 'an-dialog-box' });
|
|
69
|
+
|
|
70
|
+
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
71
|
+
title.textContent = 'Insert Video';
|
|
72
|
+
|
|
73
|
+
// URL input
|
|
74
|
+
const urlLabel = createElement('label', { class: 'an-label' });
|
|
75
|
+
urlLabel.textContent = 'Video URL';
|
|
76
|
+
const urlInput = createElement('input', {
|
|
77
|
+
type: 'url',
|
|
78
|
+
class: 'an-input',
|
|
79
|
+
placeholder: 'YouTube, Vimeo, or direct .mp4 URL',
|
|
80
|
+
autocomplete: 'off',
|
|
81
|
+
});
|
|
82
|
+
this._urlInput = urlInput;
|
|
83
|
+
|
|
84
|
+
// Hint (detected source)
|
|
85
|
+
const hintEl = createElement('p', { class: 'an-dialog-hint' });
|
|
86
|
+
this._hintEl = hintEl;
|
|
87
|
+
|
|
88
|
+
// Width
|
|
89
|
+
const widthLabel = createElement('label', { class: 'an-label' });
|
|
90
|
+
widthLabel.textContent = 'Width (px)';
|
|
91
|
+
const widthInput = createElement('input', {
|
|
92
|
+
type: 'number',
|
|
93
|
+
class: 'an-input',
|
|
94
|
+
placeholder: '560',
|
|
95
|
+
min: '80',
|
|
96
|
+
max: '1920',
|
|
97
|
+
value: '560',
|
|
98
|
+
});
|
|
99
|
+
this._widthInput = widthInput;
|
|
100
|
+
|
|
101
|
+
// Buttons
|
|
102
|
+
const btnRow = createElement('div', { class: 'an-dialog-actions' });
|
|
103
|
+
const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
|
|
104
|
+
insertBtn.textContent = 'Insert';
|
|
105
|
+
const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
|
|
106
|
+
cancelBtn.textContent = 'Cancel';
|
|
107
|
+
btnRow.appendChild(insertBtn);
|
|
108
|
+
btnRow.appendChild(cancelBtn);
|
|
109
|
+
|
|
110
|
+
box.append(title, urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
|
|
111
|
+
overlay.appendChild(box);
|
|
112
|
+
|
|
113
|
+
// Live URL hint
|
|
114
|
+
const d0 = on(urlInput, 'input', () => {
|
|
115
|
+
const info = this._parseVideoUrl(urlInput.value.trim());
|
|
116
|
+
hintEl.textContent = info ? `Detected: ${info.type}` : (urlInput.value ? 'Unknown format — will try direct video embed' : '');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const d1 = on(insertBtn, 'click', () => this._onInsert());
|
|
120
|
+
const d2 = on(cancelBtn, 'click', () => this._close());
|
|
121
|
+
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
122
|
+
const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
|
|
123
|
+
this._disposers.push(d0, d1, d2, d3, d4);
|
|
124
|
+
|
|
125
|
+
return overlay;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Actions
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
_onInsert() {
|
|
133
|
+
const rawUrl = this._urlInput.value.trim();
|
|
134
|
+
const width = Math.max(80, parseInt(this._widthInput.value, 10) || 560);
|
|
135
|
+
|
|
136
|
+
if (!rawUrl) {
|
|
137
|
+
this._urlInput.focus();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const html = this._buildEmbedHtml(rawUrl, width);
|
|
142
|
+
if (!html) {
|
|
143
|
+
this._hintEl.textContent = 'Invalid URL — please enter a valid video link.';
|
|
144
|
+
this._urlInput.focus();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (this._savedRange) this._savedRange.select();
|
|
149
|
+
this.context.invoke('editor.insertVideo', html);
|
|
150
|
+
this._close();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_open() {
|
|
154
|
+
if (this._dialog) {
|
|
155
|
+
this._dialog.style.display = 'flex';
|
|
156
|
+
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
157
|
+
setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_close() {
|
|
162
|
+
if (this._dialog) this._dialog.style.display = 'none';
|
|
163
|
+
if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
|
|
164
|
+
this._savedRange = null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// URL parsing & HTML building
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parses a video URL and returns { type, embedUrl } or null.
|
|
173
|
+
* @param {string} url
|
|
174
|
+
* @returns {{ type: string, embedUrl: string }|null}
|
|
175
|
+
*/
|
|
176
|
+
_parseVideoUrl(url) {
|
|
177
|
+
if (!url) return null;
|
|
178
|
+
|
|
179
|
+
// Validate — block javascript: and other dangerous protocols
|
|
180
|
+
try {
|
|
181
|
+
const parsed = new URL(url);
|
|
182
|
+
if (/^javascript:/i.test(parsed.protocol) || /^vbscript:/i.test(parsed.protocol)) return null;
|
|
183
|
+
} catch { return null; }
|
|
184
|
+
|
|
185
|
+
// YouTube watch: https://www.youtube.com/watch?v=ID
|
|
186
|
+
const ytWatch = url.match(/(?:youtube\.com\/watch\?(?:.*&)?v=|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/);
|
|
187
|
+
if (ytWatch) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytWatch[1]}` };
|
|
188
|
+
|
|
189
|
+
// YouTube short: https://youtu.be/ID
|
|
190
|
+
const ytShort = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
|
|
191
|
+
if (ytShort) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytShort[1]}` };
|
|
192
|
+
|
|
193
|
+
// YouTube Shorts: https://www.youtube.com/shorts/ID
|
|
194
|
+
const ytShorts = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/);
|
|
195
|
+
if (ytShorts) return { type: 'YouTube Shorts', embedUrl: `https://www.youtube.com/embed/${ytShorts[1]}` };
|
|
196
|
+
|
|
197
|
+
// Vimeo: https://vimeo.com/ID
|
|
198
|
+
const vimeo = url.match(/vimeo\.com\/(\d+)/);
|
|
199
|
+
if (vimeo) return { type: 'Vimeo', embedUrl: `https://player.vimeo.com/video/${vimeo[1]}` };
|
|
200
|
+
|
|
201
|
+
// Direct video file
|
|
202
|
+
if (/\.(mp4|webm|ogg|ogv|mov)(#.*|\?.*)?$/i.test(url)) {
|
|
203
|
+
return { type: 'Direct video', embedUrl: url };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Builds the HTML string to insert.
|
|
211
|
+
* @param {string} url
|
|
212
|
+
* @param {number} width
|
|
213
|
+
* @returns {string|null}
|
|
214
|
+
*/
|
|
215
|
+
_buildEmbedHtml(url, width) {
|
|
216
|
+
const info = this._parseVideoUrl(url);
|
|
217
|
+
const height = Math.round(width * 9 / 16); // 16:9
|
|
218
|
+
|
|
219
|
+
if (info && (info.type === 'YouTube' || info.type === 'YouTube Shorts' || info.type === 'Vimeo')) {
|
|
220
|
+
const iframeTitle = `${info.type} video player`;
|
|
221
|
+
return (
|
|
222
|
+
`<div class="an-video-wrapper" style="position:relative;display:inline-block;max-width:100%">` +
|
|
223
|
+
`<iframe src="${info.embedUrl}" width="${width}" height="${height}" ` +
|
|
224
|
+
`title="${iframeTitle}" ` +
|
|
225
|
+
`frameborder="0" allowfullscreen ` +
|
|
226
|
+
`allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" ` +
|
|
227
|
+
`style="display:block;max-width:100%"></iframe>` +
|
|
228
|
+
`<div class="an-video-shield"></div>` +
|
|
229
|
+
`</div>`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (info && info.type === 'Direct video') {
|
|
234
|
+
const src = info.embedUrl.replace(/"/g, '%22');
|
|
235
|
+
return (
|
|
236
|
+
`<div class="an-video-wrapper" style="position:relative;display:inline-block;max-width:100%">` +
|
|
237
|
+
`<video src="${src}" width="${width}" height="${height}" controls ` +
|
|
238
|
+
`style="display:block;max-width:100%"></video>` +
|
|
239
|
+
`<div class="an-video-shield"></div>` +
|
|
240
|
+
`</div>`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Unknown URL — let the user try as a direct video
|
|
245
|
+
const safeSrc = (() => {
|
|
246
|
+
try {
|
|
247
|
+
const p = new URL(url);
|
|
248
|
+
if (/^javascript:/i.test(p.protocol) || /^vbscript:/i.test(p.protocol)) return null;
|
|
249
|
+
return url;
|
|
250
|
+
} catch { return null; }
|
|
251
|
+
})();
|
|
252
|
+
if (!safeSrc) return null;
|
|
253
|
+
|
|
254
|
+
const escapedSrc = safeSrc.replace(/"/g, '%22');
|
|
255
|
+
return (
|
|
256
|
+
`<div class="an-video-wrapper" style="position:relative;display:inline-block;max-width:100%">` +
|
|
257
|
+
`<video src="${escapedSrc}" width="${width}" height="${height}" controls ` +
|
|
258
|
+
`style="display:block;max-width:100%"></video>` +
|
|
259
|
+
`<div class="an-video-shield"></div>` +
|
|
260
|
+
`</div>`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
|
|
5
|
+
const HANDLE_DEFS = [
|
|
6
|
+
{ pos: 'nw', cursor: 'nw-resize' },
|
|
7
|
+
{ pos: 'n', cursor: 'n-resize' },
|
|
8
|
+
{ pos: 'ne', cursor: 'ne-resize' },
|
|
9
|
+
{ pos: 'e', cursor: 'e-resize' },
|
|
10
|
+
{ pos: 'se', cursor: 'se-resize' },
|
|
11
|
+
{ pos: 's', cursor: 's-resize' },
|
|
12
|
+
{ pos: 'sw', cursor: 'sw-resize' },
|
|
13
|
+
{ pos: 'w', cursor: 'w-resize' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export class VideoResizer {
|
|
17
|
+
/** @param {import('../Context.js').Context} context */
|
|
18
|
+
constructor(context) {
|
|
19
|
+
this.context = context;
|
|
20
|
+
/** @type {HTMLElement|null} — the .an-video-wrapper div */
|
|
21
|
+
this._activeWrapper = null;
|
|
22
|
+
this._overlay = null;
|
|
23
|
+
this._disposers = [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
initialize() {
|
|
27
|
+
this._overlay = this._buildOverlay();
|
|
28
|
+
document.body.appendChild(this._overlay);
|
|
29
|
+
|
|
30
|
+
const editable = this.context.layoutInfo.editable;
|
|
31
|
+
|
|
32
|
+
this._disposers.push(
|
|
33
|
+
on(editable, 'click', (e) => this._onEditorClick(e)),
|
|
34
|
+
on(editable, 'contextmenu', (e) => {
|
|
35
|
+
const wrapper = this._findWrapper(e.target);
|
|
36
|
+
if (wrapper) this._select(wrapper);
|
|
37
|
+
}),
|
|
38
|
+
on(document, 'click', (e) => this._onDocClick(e)),
|
|
39
|
+
on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
40
|
+
on(window, 'resize', () => this._updateOverlayPosition()),
|
|
41
|
+
on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
destroy() {
|
|
48
|
+
this._disposers.forEach((d) => d());
|
|
49
|
+
this._disposers = [];
|
|
50
|
+
if (this._dragDisposers) {
|
|
51
|
+
this._dragDisposers.forEach((d) => d());
|
|
52
|
+
this._dragDisposers = null;
|
|
53
|
+
}
|
|
54
|
+
this._deselect();
|
|
55
|
+
if (this._overlay && this._overlay.parentNode) {
|
|
56
|
+
this._overlay.parentNode.removeChild(this._overlay);
|
|
57
|
+
}
|
|
58
|
+
this._overlay = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Public API
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/** @returns {HTMLElement|null} */
|
|
66
|
+
getActiveWrapper() {
|
|
67
|
+
return this._activeWrapper;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
updateOverlay() {
|
|
71
|
+
this._updateOverlayPosition();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
deselect() {
|
|
75
|
+
this._deselect();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Internal
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Walk up the DOM from `el` to find the nearest .an-video-wrapper,
|
|
84
|
+
* or an iframe/video whose parent is .an-video-wrapper.
|
|
85
|
+
* @param {EventTarget} el
|
|
86
|
+
* @returns {HTMLElement|null}
|
|
87
|
+
*/
|
|
88
|
+
_findWrapper(el) {
|
|
89
|
+
if (!el || !(el instanceof Element)) return null;
|
|
90
|
+
// Direct hit on wrapper
|
|
91
|
+
if (el.classList && el.classList.contains('an-video-wrapper')) return el;
|
|
92
|
+
// Child element (iframe, video, or nested)
|
|
93
|
+
const w = el.closest('.an-video-wrapper');
|
|
94
|
+
if (w) return w;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_buildOverlay() {
|
|
99
|
+
const overlay = document.createElement('div');
|
|
100
|
+
overlay.className = 'an-video-resizer';
|
|
101
|
+
overlay.style.display = 'none';
|
|
102
|
+
|
|
103
|
+
HANDLE_DEFS.forEach(({ pos }) => {
|
|
104
|
+
const h = document.createElement('div');
|
|
105
|
+
h.className = `an-resize-handle an-resize-${pos}`;
|
|
106
|
+
h.dataset.handle = pos;
|
|
107
|
+
this._disposers.push(
|
|
108
|
+
on(h, 'mousedown', (e) => {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
e.stopPropagation();
|
|
111
|
+
this._startResize(e, pos);
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
overlay.appendChild(h);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return overlay;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_onEditorClick(e) {
|
|
121
|
+
const wrapper = this._findWrapper(e.target);
|
|
122
|
+
if (wrapper) {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
this._select(wrapper);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_onDocClick(e) {
|
|
129
|
+
if (!this._activeWrapper) return;
|
|
130
|
+
if (this._activeWrapper.contains(e.target)) return;
|
|
131
|
+
if (this._overlay && this._overlay.contains(e.target)) return;
|
|
132
|
+
if (e.target.closest('.an-contextmenu')) return;
|
|
133
|
+
this._deselect();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_select(wrapper) {
|
|
137
|
+
if (this._activeWrapper && this._activeWrapper !== wrapper) {
|
|
138
|
+
this._activeWrapper.classList.remove('an-video-selected');
|
|
139
|
+
}
|
|
140
|
+
this._activeWrapper = wrapper;
|
|
141
|
+
wrapper.classList.add('an-video-selected');
|
|
142
|
+
this._updateOverlayPosition();
|
|
143
|
+
this._overlay.style.display = 'block';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_deselect() {
|
|
147
|
+
if (this._activeWrapper) {
|
|
148
|
+
this._activeWrapper.classList.remove('an-video-selected');
|
|
149
|
+
this._activeWrapper = null;
|
|
150
|
+
}
|
|
151
|
+
if (this._overlay) this._overlay.style.display = 'none';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_updateOverlayPosition() {
|
|
155
|
+
if (!this._activeWrapper || !this._overlay) return;
|
|
156
|
+
const rect = this._activeWrapper.getBoundingClientRect();
|
|
157
|
+
this._overlay.style.left = `${rect.left}px`;
|
|
158
|
+
this._overlay.style.top = `${rect.top}px`;
|
|
159
|
+
this._overlay.style.width = `${rect.width}px`;
|
|
160
|
+
this._overlay.style.height = `${rect.height}px`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_startResize(e, pos) {
|
|
164
|
+
const wrapper = this._activeWrapper;
|
|
165
|
+
if (!wrapper) return;
|
|
166
|
+
|
|
167
|
+
const embed = wrapper.querySelector('iframe, video');
|
|
168
|
+
const startX = e.clientX;
|
|
169
|
+
const startY = e.clientY;
|
|
170
|
+
const startW = wrapper.offsetWidth || 560;
|
|
171
|
+
const startH = wrapper.offsetHeight || 315;
|
|
172
|
+
const aspectRatio = startW / (startH || 1);
|
|
173
|
+
const isCorner = pos.length === 2;
|
|
174
|
+
|
|
175
|
+
const editable = this.context.layoutInfo.editable;
|
|
176
|
+
const onMove = (me) => {
|
|
177
|
+
const dx = me.clientX - startX;
|
|
178
|
+
const dy = me.clientY - startY;
|
|
179
|
+
const maxW = editable.clientWidth || Infinity;
|
|
180
|
+
let newW = startW;
|
|
181
|
+
let newH = startH;
|
|
182
|
+
|
|
183
|
+
if (pos.includes('e')) newW = Math.max(80, startW + dx);
|
|
184
|
+
if (pos.includes('w')) newW = Math.max(80, startW - dx);
|
|
185
|
+
if (pos.includes('s')) newH = Math.max(45, startH + dy);
|
|
186
|
+
if (pos.includes('n')) newH = Math.max(45, startH - dy);
|
|
187
|
+
|
|
188
|
+
// Clamp to container width
|
|
189
|
+
newW = Math.min(newW, maxW);
|
|
190
|
+
|
|
191
|
+
if (isCorner) {
|
|
192
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
193
|
+
newH = Math.max(45, Math.round(newW / aspectRatio));
|
|
194
|
+
} else {
|
|
195
|
+
newW = Math.min(Math.max(80, Math.round(newH * aspectRatio)), maxW);
|
|
196
|
+
newH = Math.max(45, Math.round(newW / aspectRatio));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Resize both the wrapper and the inner embed element
|
|
201
|
+
wrapper.style.width = `${newW}px`;
|
|
202
|
+
wrapper.style.height = `${newH}px`;
|
|
203
|
+
if (embed) {
|
|
204
|
+
embed.width = newW;
|
|
205
|
+
embed.height = newH;
|
|
206
|
+
embed.style.width = `${newW}px`;
|
|
207
|
+
embed.style.height = `${newH}px`;
|
|
208
|
+
}
|
|
209
|
+
this._updateOverlayPosition();
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const onUp = () => {
|
|
213
|
+
document.removeEventListener('mousemove', onMove);
|
|
214
|
+
document.removeEventListener('mouseup', onUp);
|
|
215
|
+
this._dragDisposers = null;
|
|
216
|
+
this.context.invoke('editor.afterCommand');
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
document.addEventListener('mousemove', onMove);
|
|
220
|
+
document.addEventListener('mouseup', onUp);
|
|
221
|
+
// Track these so destroy() can clean them up if called during an active drag
|
|
222
|
+
this._dragDisposers = [
|
|
223
|
+
() => document.removeEventListener('mousemove', onMove),
|
|
224
|
+
() => document.removeEventListener('mouseup', onUp),
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
}
|