autumnnote 1.16.0 → 2.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.
@@ -1,54 +1,48 @@
1
1
  // VideoResizer.js - Interactive resize handles for selected videos in the editor
2
2
  // Targets .an-video-wrapper divs (containing <iframe> or <video>)
3
3
  import { on } from '../core/dom.js';
4
+ import { BaseResizer } from './BaseResizer.js';
4
5
 
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
- ];
6
+ export class VideoResizer extends BaseResizer {
7
+ get _overlayClass() { return 'an-video-resizer'; }
8
+ get _selectedClass() { return 'an-video-selected'; }
15
9
 
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
- this._positionRaf = null;
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'));
25
20
  }
26
21
 
27
- initialize() {
28
- this._overlay = this._buildOverlay();
29
- const container = this.context.layoutInfo.editable.closest('.an-container') || document.body;
30
- container.appendChild(this._overlay);
31
- this._container = container;
22
+ /** @param {HTMLElement} wrapper */
23
+ _getStartSize(wrapper) {
24
+ return { w: wrapper.offsetWidth || 560, h: wrapper.offsetHeight || 315 };
25
+ }
32
26
 
33
- const editable = this.context.layoutInfo.editable;
27
+ _getMinSize() {
28
+ return { minW: 80, minH: 45 };
29
+ }
34
30
 
35
- let _resizeDebounce = null;
36
- const onWindowResize = () => {
37
- clearTimeout(_resizeDebounce);
38
- _resizeDebounce = setTimeout(() => this._updateOverlayPosition(), 100);
39
- };
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
+ }
40
42
 
41
- this._disposers.push(
42
- on(editable, 'click', (e) => this._onEditorClick(e)),
43
- on(editable, 'contextmenu', (e) => {
44
- if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
45
- const wrapper = this._findWrapper(e.target);
46
- if (wrapper) this._select(wrapper);
47
- }),
48
- on(document, 'click', (e) => this._onDocClick(e)),
49
- on(globalThis, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
50
- on(globalThis, 'resize', onWindowResize),
51
- on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
43
+ _extraListeners() {
44
+ const editable = this.context.layoutInfo.editable;
45
+ return [
52
46
  // D1: Prevent native browser drag of video wrappers. Without this, a user
53
47
  // can hold-and-drag to produce a "copy" that lands outside .an-editable,
54
48
  // where the .an-video-shield CSS loses its containing context so the
@@ -58,210 +52,15 @@ export class VideoResizer {
58
52
  e.preventDefault();
59
53
  }
60
54
  }),
61
- );
62
-
63
- return this;
64
- }
65
-
66
- destroy() {
67
- this._disposers.forEach((d) => d());
68
- this._disposers = [];
69
- if (this._dragDisposers) {
70
- this._dragDisposers.forEach((d) => d());
71
- this._dragDisposers = null;
72
- }
73
- if (this._positionRaf) {
74
- cancelAnimationFrame(this._positionRaf);
75
- this._positionRaf = null;
76
- }
77
- this._deselect();
78
- this._overlay?.remove();
79
- this._overlay = null;
55
+ ];
80
56
  }
81
57
 
82
58
  // ---------------------------------------------------------------------------
83
- // Public API
59
+ // Public API used by other modules
84
60
  // ---------------------------------------------------------------------------
85
61
 
86
62
  /** @returns {HTMLElement|null} */
87
63
  getActiveWrapper() {
88
- return this._activeWrapper;
89
- }
90
-
91
- updateOverlay() {
92
- this._updateOverlayPosition();
93
- }
94
-
95
- deselect() {
96
- this._deselect();
97
- }
98
-
99
- // ---------------------------------------------------------------------------
100
- // Internal
101
- // ---------------------------------------------------------------------------
102
-
103
- /**
104
- * Walk up the DOM from `el` to find the nearest .an-video-wrapper,
105
- * or an iframe/video whose parent is .an-video-wrapper.
106
- * @param {EventTarget} el
107
- * @returns {HTMLElement|null}
108
- */
109
- _findWrapper(el) {
110
- if (!el || !(el instanceof Element)) return null;
111
- // Direct hit on wrapper
112
- if (el.classList?.contains('an-video-wrapper')) return /** @type {HTMLElement} */ (el);
113
- // Child element (iframe, video, or nested)
114
- const w = el.closest('.an-video-wrapper');
115
- if (w) return /** @type {HTMLElement} */ (w);
116
- return null;
117
- }
118
-
119
- _buildOverlay() {
120
- const overlay = document.createElement('div');
121
- overlay.className = 'an-video-resizer';
122
- overlay.style.display = 'none';
123
-
124
- HANDLE_DEFS.forEach(({ pos }) => {
125
- const h = document.createElement('div');
126
- h.className = `an-resize-handle an-resize-${pos}`;
127
- h.dataset.handle = pos;
128
- this._disposers.push(
129
- on(h, 'mousedown', (e) => {
130
- e.preventDefault();
131
- e.stopPropagation();
132
- this._startResize(e, pos);
133
- }),
134
- );
135
- overlay.appendChild(h);
136
- });
137
-
138
- return overlay;
139
- }
140
-
141
- _onEditorClick(e) {
142
- if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
143
- const wrapper = this._findWrapper(e.target);
144
- if (wrapper) {
145
- e.preventDefault();
146
- this._select(wrapper);
147
- }
148
- }
149
-
150
- _onDocClick(e) {
151
- if (!this._activeWrapper) return;
152
- if (this._activeWrapper.contains(e.target)) return;
153
- if (this._overlay?.contains(e.target)) return;
154
- if (e.target.closest('.an-contextmenu')) return;
155
- this._deselect();
156
- }
157
-
158
- _select(wrapper) {
159
- if (this._activeWrapper && this._activeWrapper !== wrapper) {
160
- this._activeWrapper.classList.remove('an-video-selected');
161
- }
162
- this._activeWrapper = wrapper;
163
- wrapper.classList.add('an-video-selected');
164
- this._updateOverlayPosition();
165
- this._overlay.style.display = 'block';
166
- }
167
-
168
- _deselect() {
169
- if (this._activeWrapper) {
170
- this._activeWrapper.classList.remove('an-video-selected');
171
- this._activeWrapper = null;
172
- }
173
- if (this._overlay) this._overlay.style.display = 'none';
174
- }
175
-
176
- _updateOverlayPosition() {
177
- if (this._positionRaf) cancelAnimationFrame(this._positionRaf);
178
- this._positionRaf = requestAnimationFrame(() => {
179
- this._positionRaf = null;
180
- this._updateOverlayPositionNow();
181
- });
182
- }
183
-
184
- _updateOverlayPositionNow() {
185
- if (!this._activeWrapper || !this._overlay) return;
186
- const offsetParent = this._overlay.offsetParent || this._container;
187
- const containerRect = offsetParent.getBoundingClientRect();
188
- const rect = this._activeWrapper.getBoundingClientRect();
189
- const left = rect.left - containerRect.left + offsetParent.scrollLeft;
190
- const top = rect.top - containerRect.top + offsetParent.scrollTop;
191
- this._overlay.style.left = `${left}px`;
192
- this._overlay.style.top = `${top}px`;
193
- this._overlay.style.width = `${rect.width}px`;
194
- this._overlay.style.height = `${rect.height}px`;
195
- }
196
-
197
- _startResize(e, pos) {
198
- const wrapper = this._activeWrapper;
199
- if (!wrapper) return;
200
-
201
- const embed = /** @type {HTMLElement|null} */ (wrapper.querySelector('iframe, video'));
202
- const startX = e.clientX;
203
- const startY = e.clientY;
204
- const startW = wrapper.offsetWidth || 560;
205
- const startH = wrapper.offsetHeight || 315;
206
- const aspectRatio = startW / (startH || 1);
207
- const isCorner = pos.length === 2;
208
-
209
- const editable = this.context.layoutInfo.editable;
210
- let _raf = null; // rAF handle — at most one write per paint frame
211
-
212
- const onMove = (me) => {
213
- if (_raf !== null) return;
214
- const clientX = me.clientX;
215
- const clientY = me.clientY;
216
- _raf = requestAnimationFrame(() => {
217
- _raf = null;
218
- const dx = clientX - startX;
219
- const dy = clientY - startY;
220
- const maxW = editable.clientWidth || Infinity;
221
- let newW = startW;
222
- let newH = startH;
223
-
224
- if (pos.includes('e')) newW = Math.max(80, startW + dx);
225
- if (pos.includes('w')) newW = Math.max(80, startW - dx);
226
- if (pos.includes('s')) newH = Math.max(45, startH + dy);
227
- if (pos.includes('n')) newH = Math.max(45, startH - dy);
228
-
229
- newW = Math.min(newW, maxW);
230
-
231
- if (isCorner) {
232
- if (Math.abs(dx) >= Math.abs(dy)) {
233
- newH = Math.max(45, Math.round(newW / aspectRatio));
234
- } else {
235
- newW = Math.min(Math.max(80, Math.round(newH * aspectRatio)), maxW);
236
- newH = Math.max(45, Math.round(newW / aspectRatio));
237
- }
238
- }
239
-
240
- // Resize wrapper and inner embed via CSS only — attribute writes are redundant
241
- wrapper.style.width = `${newW}px`;
242
- wrapper.style.height = `${newH}px`;
243
- if (embed) {
244
- embed.style.width = `${newW}px`;
245
- embed.style.height = `${newH}px`;
246
- }
247
- this._updateOverlayPosition();
248
- });
249
- };
250
-
251
- const onUp = () => {
252
- if (_raf !== null) { cancelAnimationFrame(_raf); _raf = null; }
253
- document.removeEventListener('mousemove', onMove);
254
- document.removeEventListener('mouseup', onUp);
255
- this._dragDisposers = null;
256
- this.context.invoke('editor.afterCommand');
257
- };
258
-
259
- document.addEventListener('mousemove', onMove);
260
- document.addEventListener('mouseup', onUp);
261
- // Track these so destroy() can clean them up if called during an active drag
262
- this._dragDisposers = [
263
- () => document.removeEventListener('mousemove', onMove),
264
- () => document.removeEventListener('mouseup', onUp),
265
- ];
64
+ return this._active;
266
65
  }
267
66
  }
@@ -2,6 +2,7 @@
2
2
  // Displays a horizontal action bar below (or above) the selected video,
3
3
  // similar in appearance and interaction to LinkTooltip.
4
4
  import { createElement, on } from '../core/dom.js';
5
+ import { BaseMediaTooltip } from './BaseMediaTooltip.js';
5
6
 
6
7
  const ICONS = {
7
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>`,
@@ -13,22 +14,7 @@ const ICONS = {
13
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>`,
14
15
  };
15
16
 
16
- const SHOW_DELAY = 100;
17
- const HIDE_DELAY = 180;
18
-
19
- export class VideoTooltip {
20
- /** @param {import('../Context.js').Context} context */
21
- constructor(context) {
22
- this.context = context;
23
- this._el = null;
24
- this._activeWrapper = null;
25
- this._showTimer = null;
26
- this._hideTimer = null;
27
- this._disposers = [];
28
- this._previewMode = false;
29
- this._previewClickOff = null;
30
- }
31
-
17
+ export class VideoTooltip extends BaseMediaTooltip {
32
18
  initialize() {
33
19
  this._el = this._buildTooltip();
34
20
  document.body.appendChild(this._el);
@@ -54,8 +40,8 @@ export class VideoTooltip {
54
40
  on(document, 'click', (e) => {
55
41
  const target = /** @type {Node} */ (e.target);
56
42
  if (
57
- this._activeWrapper &&
58
- !this._activeWrapper.contains(target) &&
43
+ this._active &&
44
+ !this._active.contains(target) &&
59
45
  !this._el.contains(target)
60
46
  ) {
61
47
  this._hide();
@@ -78,6 +64,17 @@ export class VideoTooltip {
78
64
  this._el = null;
79
65
  }
80
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
+
81
78
  // ---------------------------------------------------------------------------
82
79
  // Build
83
80
  // ---------------------------------------------------------------------------
@@ -134,96 +131,12 @@ export class VideoTooltip {
134
131
  return el;
135
132
  }
136
133
 
137
- /**
138
- * @param {string} icon
139
- * @param {string} title
140
- * @param {Function} handler
141
- * @param {boolean} [isDanger]
142
- */
143
- _makeBtn(icon, title, handler, isDanger = false) {
144
- const btn = createElement('button', {
145
- type: 'button',
146
- class: isDanger ? 'an-link-tooltip-btn an-link-tooltip-btn--danger' : 'an-link-tooltip-btn',
147
- title,
148
- });
149
- btn.innerHTML = icon;
150
- this._disposers.push(on(btn, 'click', (e) => {
151
- e.preventDefault();
152
- e.stopPropagation();
153
- handler();
154
- }));
155
- return btn;
156
- }
157
-
158
- // ---------------------------------------------------------------------------
159
- // Show / Hide
160
- // ---------------------------------------------------------------------------
161
-
162
- _scheduleShow(wrapper) {
163
- if (this._activeWrapper === wrapper && this._el.style.display !== 'none') return;
164
- clearTimeout(this._hideTimer);
165
- this._hideTimer = null;
166
- clearTimeout(this._showTimer);
167
- this._showTimer = setTimeout(() => {
168
- this._activeWrapper = wrapper;
169
- this._show(wrapper);
170
- }, SHOW_DELAY);
171
- }
172
-
173
- _scheduleHide() {
174
- // Keep tooltip alive while preview mode is active
175
- if (this._previewMode) return;
176
- clearTimeout(this._showTimer);
177
- this._showTimer = null;
178
- if (this._hideTimer) return;
179
- this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
180
- }
181
-
182
- _show(_wrapper) {
183
- this._el.style.display = 'flex';
184
- // Defer: offsetWidth on a newly-visible element forces synchronous layout
185
- requestAnimationFrame(() => {
186
- if (this._activeWrapper) this._positionNear(this._activeWrapper);
187
- });
188
- }
189
-
190
- _hide() {
191
- if (this._previewMode) this._exitPreview();
192
- this._el.style.display = 'none';
193
- this._activeWrapper = null;
194
- this._clearTimers();
195
- }
196
-
197
- _clearTimers() {
198
- clearTimeout(this._showTimer);
199
- clearTimeout(this._hideTimer);
200
- this._showTimer = null;
201
- this._hideTimer = null;
202
- }
203
-
204
- _positionNear(wrapper) {
205
- const rect = wrapper.getBoundingClientRect();
206
- const tipW = this._el.offsetWidth || 220;
207
- const tipH = this._el.offsetHeight || 32;
208
- const margin = 6;
209
-
210
- let top = rect.bottom + margin;
211
- let left = rect.left + (rect.width - tipW) / 2;
212
-
213
- if (top + tipH > globalThis.innerHeight - margin) top = rect.top - tipH - margin;
214
- if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
215
- if (left < margin) left = margin;
216
-
217
- this._el.style.top = `${top}px`;
218
- this._el.style.left = `${left}px`;
219
- }
220
-
221
134
  // ---------------------------------------------------------------------------
222
135
  // Actions
223
136
  // ---------------------------------------------------------------------------
224
137
 
225
138
  _setFloat(value) {
226
- const wrapper = this._activeWrapper;
139
+ const wrapper = this._active;
227
140
  if (!wrapper) return;
228
141
  wrapper.style.float = value;
229
142
  wrapper.style.display = value ? 'inline-block' : '';
@@ -231,11 +144,11 @@ export class VideoTooltip {
231
144
  wrapper.style.marginRight = value === 'left' ? '12px' : '';
232
145
  this.context.invoke('editor.afterCommand');
233
146
  this.context.invoke('videoResizer.updateOverlay');
234
- requestAnimationFrame(() => { if (this._activeWrapper) this._positionNear(this._activeWrapper); });
147
+ requestAnimationFrame(() => { if (this._active) this._positionNear(this._active); });
235
148
  }
236
149
 
237
150
  _setCenter() {
238
- const wrapper = this._activeWrapper;
151
+ const wrapper = this._active;
239
152
  if (!wrapper) return;
240
153
  wrapper.style.float = '';
241
154
  wrapper.style.display = 'block';
@@ -243,13 +156,13 @@ export class VideoTooltip {
243
156
  wrapper.style.marginRight = 'auto';
244
157
  this.context.invoke('editor.afterCommand');
245
158
  this.context.invoke('videoResizer.updateOverlay');
246
- requestAnimationFrame(() => { if (this._activeWrapper) this._positionNear(this._activeWrapper); });
159
+ requestAnimationFrame(() => { if (this._active) this._positionNear(this._active); });
247
160
  }
248
161
 
249
162
  _resetSize() {
250
- const wrapper = this._activeWrapper;
163
+ const wrapper = this._active;
251
164
  if (!wrapper) return;
252
- const embed = wrapper.querySelector('iframe, video');
165
+ const embed = /** @type {HTMLElement|null} */ (wrapper.querySelector('iframe, video'));
253
166
  wrapper.style.width = '';
254
167
  wrapper.style.height = '';
255
168
  if (embed) {
@@ -260,11 +173,11 @@ export class VideoTooltip {
260
173
  }
261
174
  this.context.invoke('editor.afterCommand');
262
175
  this.context.invoke('videoResizer.updateOverlay');
263
- requestAnimationFrame(() => { if (this._activeWrapper) this._positionNear(this._activeWrapper); });
176
+ requestAnimationFrame(() => { if (this._active) this._positionNear(this._active); });
264
177
  }
265
178
 
266
179
  _delete() {
267
- const wrapper = this._activeWrapper;
180
+ const wrapper = this._active;
268
181
  if (!wrapper) return;
269
182
  this._hide();
270
183
  this.context.invoke('videoResizer.deselect');
@@ -285,12 +198,12 @@ export class VideoTooltip {
285
198
  }
286
199
 
287
200
  _enterPreview() {
288
- const wrapper = this._activeWrapper;
201
+ const wrapper = this._active;
289
202
  if (!wrapper) return;
290
203
  this._previewMode = true;
291
204
 
292
205
  // Hide the shield so the iframe / video receives pointer events directly
293
- const shield = wrapper.querySelector('.an-video-shield');
206
+ const shield = /** @type {HTMLElement|null} */ (wrapper.querySelector('.an-video-shield'));
294
207
  if (shield) shield.style.display = 'none';
295
208
 
296
209
  // Hide the resize overlay — it would block interaction with the embed
@@ -316,10 +229,10 @@ export class VideoTooltip {
316
229
  _exitPreview() {
317
230
  this._previewMode = false;
318
231
 
319
- const wrapper = this._activeWrapper;
232
+ const wrapper = this._active;
320
233
  if (wrapper) {
321
234
  // Restore the shield
322
- const shield = wrapper.querySelector('.an-video-shield');
235
+ const shield = /** @type {HTMLElement|null} */ (wrapper.querySelector('.an-video-shield'));
323
236
  if (shield) shield.style.display = '';
324
237
  }
325
238