autumnnote 1.0.3 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autumnnote",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "A modern, lightweight WYSIWYG editor — built with vanilla JavaScript, no jQuery required.",
5
5
  "main": "dist/autumnnote.umd.js",
6
6
  "module": "dist/autumnnote.es.js",
@@ -1,12 +1,12 @@
1
- /**
2
- * index.umd.js — UMD entry point for AutumnNote
3
- *
4
- * Re-exports only the default export so the UMD global is the factory object
5
- * directly, enabling the script-tag usage documented in the README:
6
- *
7
- * <script src="dist/autumnnote.umd.js"></script>
8
- * <script>
9
- * const editor = AutumnNote.create('#my-editor');
10
- * </script>
11
- */
12
- export { default } from './index.js';
1
+ /**
2
+ * index.umd.js — UMD entry point for AutumnNote
3
+ *
4
+ * Re-exports only the default export so the UMD global is the factory object
5
+ * directly, enabling the script-tag usage documented in the README:
6
+ *
7
+ * <script src="dist/autumnnote.umd.js"></script>
8
+ * <script>
9
+ * const editor = AutumnNote.create('#my-editor');
10
+ * </script>
11
+ */
12
+ export { default } from './index.js';
@@ -38,6 +38,14 @@ export class ImageResizer {
38
38
 
39
39
  const editable = this.context.layoutInfo.editable;
40
40
 
41
+ // Debounce window resize — _updateOverlayPosition already has rAF gating but
42
+ // every call cancels + re-schedules it; a debounce reduces that churn.
43
+ let _resizeDebounce = null;
44
+ const onWindowResize = () => {
45
+ clearTimeout(_resizeDebounce);
46
+ _resizeDebounce = setTimeout(() => this._updateOverlayPosition(), 100);
47
+ };
48
+
41
49
  this._disposers.push(
42
50
  on(editable, 'click', (e) => this._onEditorClick(e)),
43
51
  // Also select on right-click so the highlight shows before the context menu
@@ -47,7 +55,7 @@ export class ImageResizer {
47
55
  }),
48
56
  on(document, 'click', (e) => this._onDocClick(e)),
49
57
  on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
50
- on(window, 'resize', () => this._updateOverlayPosition()),
58
+ on(window, 'resize', onWindowResize),
51
59
  on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
52
60
  );
53
61
 
@@ -187,37 +195,44 @@ export class ImageResizer {
187
195
  const isCorner = pos.length === 2; // 'nw','ne','se','sw'
188
196
 
189
197
  const editable = this.context.layoutInfo.editable;
198
+ let _raf = null; // rAF handle — ensures at most one write per paint frame
199
+
190
200
  const onMove = (me) => {
191
- const dx = me.clientX - startX;
192
- const dy = me.clientY - startY;
193
- const maxW = editable.clientWidth || Infinity;
194
- let newW = startW;
195
- let newH = startH;
196
-
197
- if (pos.includes('e')) newW = Math.max(20, startW + dx);
198
- if (pos.includes('w')) newW = Math.max(20, startW - dx);
199
- if (pos.includes('s')) newH = Math.max(20, startH + dy);
200
- if (pos.includes('n')) newH = Math.max(20, startH - dy);
201
-
202
- // Clamp to container width
203
- newW = Math.min(newW, maxW);
204
-
205
- if (isCorner) {
206
- // Lock aspect ratio: use larger absolute delta to drive both dimensions
207
- if (Math.abs(dx) >= Math.abs(dy)) {
208
- newH = Math.max(20, Math.round(newW / aspectRatio));
209
- } else {
210
- newW = Math.min(Math.max(20, Math.round(newH * aspectRatio)), maxW);
211
- newH = Math.max(20, Math.round(newW / aspectRatio));
201
+ if (_raf !== null) return; // frame already pending, discard this event
202
+ const clientX = me.clientX;
203
+ const clientY = me.clientY;
204
+ _raf = requestAnimationFrame(() => {
205
+ _raf = null;
206
+ const dx = clientX - startX;
207
+ const dy = clientY - startY;
208
+ const maxW = editable.clientWidth || Infinity;
209
+ let newW = startW;
210
+ let newH = startH;
211
+
212
+ if (pos.includes('e')) newW = Math.max(20, startW + dx);
213
+ if (pos.includes('w')) newW = Math.max(20, startW - dx);
214
+ if (pos.includes('s')) newH = Math.max(20, startH + dy);
215
+ if (pos.includes('n')) newH = Math.max(20, startH - dy);
216
+
217
+ newW = Math.min(newW, maxW);
218
+
219
+ if (isCorner) {
220
+ if (Math.abs(dx) >= Math.abs(dy)) {
221
+ newH = Math.max(20, Math.round(newW / aspectRatio));
222
+ } else {
223
+ newW = Math.min(Math.max(20, Math.round(newH * aspectRatio)), maxW);
224
+ newH = Math.max(20, Math.round(newW / aspectRatio));
225
+ }
212
226
  }
213
- }
214
227
 
215
- img.style.width = `${newW}px`;
216
- img.style.height = `${newH}px`;
217
- this._updateOverlayPosition();
228
+ img.style.width = `${newW}px`;
229
+ img.style.height = `${newH}px`;
230
+ this._updateOverlayPosition();
231
+ });
218
232
  };
219
233
 
220
234
  const onUp = () => {
235
+ if (_raf !== null) { cancelAnimationFrame(_raf); _raf = null; }
221
236
  document.removeEventListener('mousemove', onMove);
222
237
  document.removeEventListener('mouseup', onUp);
223
238
  this._dragDisposers = null;
@@ -11,6 +11,8 @@ const ICONS = {
11
11
  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>`,
12
12
  deleteImg: `<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>`,
13
13
  caption: `<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="18" height="11" rx="2"/><line x1="6" y1="18" x2="18" y2="18"/><line x1="9" y1="21" x2="15" y2="21"/></svg>`,
14
+ rotateLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><polyline points="3 3 3 8 8 8"/></svg>`,
15
+ rotateRight: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><polyline points="21 3 21 8 16 8"/></svg>`,
14
16
  };
15
17
 
16
18
  const SHOW_DELAY = 100;
@@ -40,13 +42,13 @@ export class ImageTooltip {
40
42
  if (img && editable.contains(img) && !img.closest('a[href]')) {
41
43
  this._scheduleShow(img);
42
44
  }
43
- }),
45
+ }, { passive: true }),
44
46
  on(editable, 'mouseout', (e) => {
45
47
  const to = e.relatedTarget;
46
48
  if (!to || (!editable.contains(to) && !this._el.contains(to))) {
47
49
  this._scheduleHide();
48
50
  }
49
- }),
51
+ }, { passive: true }),
50
52
  // Hide when image is deselected by clicking elsewhere
51
53
  on(document, 'click', (e) => {
52
54
  if (this._activeImg && !this._activeImg.contains(e.target) && !this._el.contains(e.target)) {
@@ -103,6 +105,11 @@ export class ImageTooltip {
103
105
 
104
106
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
105
107
 
108
+ el.appendChild(this._makeBtn(ICONS.rotateLeft, 'Rotate Left', () => this._rotate(-90)));
109
+ el.appendChild(this._makeBtn(ICONS.rotateRight, 'Rotate Right', () => this._rotate(90)));
110
+
111
+ el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
112
+
106
113
  this._captionBtn = this._makeBtn(ICONS.caption, 'Add / Edit Caption', () => this._toggleCaption());
107
114
  el.appendChild(this._captionBtn);
108
115
 
@@ -166,7 +173,10 @@ export class ImageTooltip {
166
173
 
167
174
  _show(img) {
168
175
  this._el.style.display = 'flex';
169
- this._positionNear(img);
176
+ // Defer positioning: offsetWidth on a newly-visible element forces layout
177
+ requestAnimationFrame(() => {
178
+ if (this._activeImg) this._positionNear(this._activeImg);
179
+ });
170
180
  }
171
181
 
172
182
  _hide() {
@@ -213,7 +223,7 @@ export class ImageTooltip {
213
223
  target.style.marginRight = value === 'left' ? '12px' : '';
214
224
  this.context.invoke('editor.afterCommand');
215
225
  this.context.invoke('imageResizer.updateOverlay');
216
- this._positionNear(img);
226
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
217
227
  }
218
228
 
219
229
  _setCenter() {
@@ -226,7 +236,7 @@ export class ImageTooltip {
226
236
  target.style.marginRight = 'auto';
227
237
  this.context.invoke('editor.afterCommand');
228
238
  this.context.invoke('imageResizer.updateOverlay');
229
- this._positionNear(img);
239
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
230
240
  }
231
241
 
232
242
  _resetSize() {
@@ -236,7 +246,31 @@ export class ImageTooltip {
236
246
  img.style.height = '';
237
247
  this.context.invoke('editor.afterCommand');
238
248
  this.context.invoke('imageResizer.updateOverlay');
239
- this._positionNear(img);
249
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
250
+ }
251
+
252
+ /**
253
+ * Rotate the active image by `delta` degrees (±90).
254
+ * Reads the existing rotate() value from the transform style so
255
+ * repeated clicks accumulate correctly.
256
+ * @param {number} delta
257
+ */
258
+ _rotate(delta) {
259
+ const img = this._activeImg;
260
+ if (!img) return;
261
+ // Parse current rotation angle from inline transform
262
+ const current = img.style.transform || '';
263
+ const match = current.match(/rotate\((-?[\d.]+)deg\)/);
264
+ const prev = match ? parseFloat(match[1]) : 0;
265
+ const next = (prev + delta + 360) % 360; // normalise to [0, 360)
266
+ // Preserve any other transform functions (e.g. scale), replace only rotate()
267
+ const cleaned = current.replace(/rotate\(-?[\d.]+deg\)/, '').trim();
268
+ img.style.transform = cleaned
269
+ ? `${cleaned} rotate(${next}deg)`
270
+ : next === 0 ? '' : `rotate(${next}deg)`;
271
+ this.context.invoke('editor.afterCommand');
272
+ this.context.invoke('imageResizer.updateOverlay');
273
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
240
274
  }
241
275
 
242
276
  _delete() {
@@ -110,7 +110,7 @@ export class TableTooltip {
110
110
  if (cell) this._activeCell = cell;
111
111
  this._scheduleShow(table);
112
112
  }
113
- }),
113
+ }, { passive: true }),
114
114
  on(editable, 'mouseout', (e) => {
115
115
  const to = e.relatedTarget;
116
116
  if (!to || (
@@ -120,7 +120,7 @@ export class TableTooltip {
120
120
  )) {
121
121
  this._scheduleHide();
122
122
  }
123
- }),
123
+ }, { passive: true }),
124
124
  on(document, 'click', (e) => {
125
125
  if (this._activeTable &&
126
126
  !this._activeTable.contains(e.target) &&
@@ -155,8 +155,11 @@ export class TableTooltip {
155
155
  let _startW = 0;
156
156
  let _startH = 0;
157
157
  let _colIdx = -1;
158
+ let _colCells = null; // cells cached at drag-start — avoids querySelectorAll every frame
158
159
  let _row = null;
159
160
  let _table = null;
161
+ let _rafDocMove = null; // pending rAF handle for resize drag
162
+ let _rafEditorMove = null; // pending rAF handle for cursor detection
160
163
 
161
164
  const clearHover = () => {
162
165
  if (_nearCell) { _nearCell.style.cursor = ''; _nearCell = null; }
@@ -165,27 +168,29 @@ export class TableTooltip {
165
168
 
166
169
  const onEditorMove = (e) => {
167
170
  if (_resizing) return;
168
- const cell = e.target.closest('td, th');
169
- if (!cell || !editable.contains(cell)) { clearHover(); return; }
170
- if (_nearCell && _nearCell !== cell) {
171
- _nearCell.style.cursor = '';
172
- }
173
- const rect = cell.getBoundingClientRect();
174
- const onRight = Math.abs(e.clientX - rect.right) < HIT;
175
- const onBottom = Math.abs(e.clientY - rect.bottom) < HIT;
176
- if (onRight && onBottom) {
177
- // Corner prefer col-resize
178
- cell.style.cursor = 'col-resize';
179
- _nearCell = cell; _nearEdge = 'col';
180
- } else if (onRight) {
181
- cell.style.cursor = 'col-resize';
182
- _nearCell = cell; _nearEdge = 'col';
183
- } else if (onBottom) {
184
- cell.style.cursor = 'row-resize';
185
- _nearCell = cell; _nearEdge = 'row';
186
- } else {
187
- clearHover();
188
- }
171
+ // Throttle to one check per animation frame — getBoundingClientRect forces reflow
172
+ if (_rafEditorMove !== null) return;
173
+ const target = e.target;
174
+ const clientX = e.clientX;
175
+ const clientY = e.clientY;
176
+ _rafEditorMove = requestAnimationFrame(() => {
177
+ _rafEditorMove = null;
178
+ const cell = target.closest('td, th');
179
+ if (!cell || !editable.contains(cell)) { clearHover(); return; }
180
+ if (_nearCell && _nearCell !== cell) _nearCell.style.cursor = '';
181
+ const rect = cell.getBoundingClientRect();
182
+ const onRight = Math.abs(clientX - rect.right) < HIT;
183
+ const onBottom = Math.abs(clientY - rect.bottom) < HIT;
184
+ if (onRight) {
185
+ cell.style.cursor = 'col-resize';
186
+ _nearCell = cell; _nearEdge = 'col';
187
+ } else if (onBottom) {
188
+ cell.style.cursor = 'row-resize';
189
+ _nearCell = cell; _nearEdge = 'row';
190
+ } else {
191
+ clearHover();
192
+ }
193
+ });
189
194
  };
190
195
 
191
196
  const onEditorDown = (e) => {
@@ -196,8 +201,14 @@ export class TableTooltip {
196
201
  _startY = e.clientY;
197
202
  _table = _nearCell.closest('table');
198
203
  if (_edge === 'col') {
199
- _startW = _nearCell.offsetWidth;
200
- _colIdx = getVisualColIndex(_nearCell);
204
+ _startW = _nearCell.offsetWidth;
205
+ _colIdx = getVisualColIndex(_nearCell);
206
+ // Cache column cells once so onDocMove never runs querySelectorAll per frame
207
+ _colCells = _colIdx >= 0
208
+ ? Array.from(_table.querySelectorAll('tr'))
209
+ .map(r => getCellAtVisualCol(r, _colIdx))
210
+ .filter(Boolean)
211
+ : [];
201
212
  document.body.style.cursor = 'col-resize';
202
213
  } else {
203
214
  _row = _nearCell.closest('tr');
@@ -211,34 +222,42 @@ export class TableTooltip {
211
222
 
212
223
  const onDocMove = (e) => {
213
224
  if (!_resizing) return;
214
- if (_edge === 'col') {
215
- const newW = Math.max(30, _startW + (e.clientX - _startX));
216
- if (_table && _colIdx >= 0) {
217
- Array.from(_table.querySelectorAll('tr')).forEach((r) => {
218
- const c = getCellAtVisualCol(r, _colIdx);
219
- if (c) { c.style.width = `${newW}px`; c.style.minWidth = `${newW}px`; }
220
- });
221
- }
222
- } else {
223
- const newH = Math.max(20, _startH + (e.clientY - _startY));
224
- if (_row) {
225
- Array.from(_row.cells).forEach((c) => {
226
- c.style.height = `${newH}px`;
227
- c.style.minHeight = `${newH}px`;
228
- });
225
+ // Skip if a frame is already scheduled — avoids per-pixel style thrashing
226
+ if (_rafDocMove !== null) return;
227
+ const clientX = e.clientX;
228
+ const clientY = e.clientY;
229
+ _rafDocMove = requestAnimationFrame(() => {
230
+ _rafDocMove = null;
231
+ if (_edge === 'col') {
232
+ const newW = Math.max(30, _startW + (clientX - _startX));
233
+ for (const c of _colCells) {
234
+ c.style.width = `${newW}px`;
235
+ c.style.minWidth = `${newW}px`;
236
+ }
237
+ } else {
238
+ const newH = Math.max(20, _startH + (clientY - _startY));
239
+ if (_row) {
240
+ for (const c of _row.cells) {
241
+ c.style.height = `${newH}px`;
242
+ c.style.minHeight = `${newH}px`;
243
+ }
244
+ }
229
245
  }
230
- }
246
+ });
231
247
  };
232
248
 
233
249
  const onDocUp = () => {
234
250
  if (!_resizing) return;
251
+ // Cancel any in-flight rAF so stale writes don't land after mouseup
252
+ if (_rafDocMove !== null) { cancelAnimationFrame(_rafDocMove); _rafDocMove = null; }
235
253
  _resizing = false;
236
254
  document.body.style.userSelect = '';
237
255
  document.body.style.cursor = '';
238
- _edge = null;
239
- _table = null;
240
- _row = null;
241
- _colIdx = -1;
256
+ _edge = null;
257
+ _table = null;
258
+ _row = null;
259
+ _colIdx = -1;
260
+ _colCells = null;
242
261
  this.context.invoke('editor.afterCommand');
243
262
  };
244
263
 
@@ -375,7 +394,10 @@ export class TableTooltip {
375
394
  _show() {
376
395
  if (!this._activeTable) return;
377
396
  this._el.style.display = 'flex';
378
- this._positionNear(this._activeTable);
397
+ // Defer: offsetWidth on a newly-visible element forces layout synchronously
398
+ requestAnimationFrame(() => {
399
+ if (this._activeTable) this._positionNear(this._activeTable);
400
+ });
379
401
  }
380
402
 
381
403
  _hide() {
@@ -452,7 +474,8 @@ export class TableTooltip {
452
474
  }
453
475
  if (position === 'above') row.parentElement?.insertBefore(newRow, row);
454
476
  else row.insertAdjacentElement('afterend', newRow);
455
- this._positionNear(this._activeTable);
477
+ // Defer positioning until the browser has painted the new layout
478
+ requestAnimationFrame(() => this._positionNear(this._activeTable));
456
479
  this.context.invoke('editor.afterCommand');
457
480
  }
458
481
 
@@ -462,18 +485,16 @@ export class TableTooltip {
462
485
  const table = cell.closest('table');
463
486
  if (!table) return;
464
487
  const visualColIdx = getVisualColIndex(cell);
465
- Array.from(table.querySelectorAll('tr')).forEach((r) => {
466
- const isHeader = r.closest('thead') !== null;
467
- const newCell = createElement(isHeader ? 'th' : 'td', {}, ['\u00a0']);
468
- if (position === 'left') {
469
- const ref = getCellAtVisualCol(r, visualColIdx);
470
- r.insertBefore(newCell, ref);
471
- } else {
472
- const ref = getCellAfterVisualCol(r, visualColIdx);
473
- r.insertBefore(newCell, ref);
474
- }
488
+ const rows = Array.from(table.querySelectorAll('tr'));
489
+ // Batch reads first, then writes — prevents layout thrashing inside the loop
490
+ const refs = rows.map(r => position === 'left'
491
+ ? getCellAtVisualCol(r, visualColIdx)
492
+ : getCellAfterVisualCol(r, visualColIdx));
493
+ const isHeaders = rows.map(r => r.closest('thead') !== null);
494
+ rows.forEach((r, i) => {
495
+ r.insertBefore(createElement(isHeaders[i] ? 'th' : 'td', {}, ['\u00a0']), refs[i]);
475
496
  });
476
- this._positionNear(this._activeTable);
497
+ requestAnimationFrame(() => this._positionNear(this._activeTable));
477
498
  this.context.invoke('editor.afterCommand');
478
499
  }
479
500
 
@@ -489,7 +510,7 @@ export class TableTooltip {
489
510
  if (bodyRows <= 1 && row.closest('tbody')) return;
490
511
  this._activeCell = null;
491
512
  row.parentElement?.removeChild(row);
492
- this._positionNear(this._activeTable);
513
+ requestAnimationFrame(() => this._positionNear(this._activeTable));
493
514
  this.context.invoke('editor.afterCommand');
494
515
  }
495
516
 
@@ -502,11 +523,11 @@ export class TableTooltip {
502
523
  if (row && row.cells.length <= 1) return;
503
524
  const visualColIdx = getVisualColIndex(cell);
504
525
  this._activeCell = null;
505
- Array.from(table.querySelectorAll('tr')).forEach((r) => {
506
- const c = getCellAtVisualCol(r, visualColIdx);
507
- if (c) r.removeChild(c);
508
- });
509
- this._positionNear(this._activeTable);
526
+ const rows = Array.from(table.querySelectorAll('tr'));
527
+ // Batch reads before writes to avoid forced reflows inside the loop
528
+ const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
529
+ cells.forEach((c, i) => { if (c) rows[i].removeChild(c); });
530
+ requestAnimationFrame(() => this._positionNear(this._activeTable));
510
531
  this.context.invoke('editor.afterCommand');
511
532
  }
512
533
 
@@ -628,15 +649,19 @@ export class TableTooltip {
628
649
 
629
650
  this._sizeApply = (val) => {
630
651
  if (isCol) {
631
- const table = cell.closest('table');
652
+ const table = cell.closest('table');
632
653
  const visualColIdx = getVisualColIndex(cell);
633
- Array.from(table.querySelectorAll('tr')).forEach((r) => {
634
- const c = getCellAtVisualCol(r, visualColIdx);
654
+ const rows = Array.from(table.querySelectorAll('tr'));
655
+ // Batch reads, then writes
656
+ const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
657
+ cells.forEach(c => {
635
658
  if (c) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
636
659
  });
637
660
  } else {
638
661
  const row = cell.closest('tr');
639
- if (row) Array.from(row.cells).forEach((c) => { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; });
662
+ if (row) {
663
+ for (const c of row.cells) { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; }
664
+ }
640
665
  }
641
666
  this.context.invoke('editor.afterCommand');
642
667
  };
@@ -32,6 +32,12 @@ export class VideoResizer {
32
32
 
33
33
  const editable = this.context.layoutInfo.editable;
34
34
 
35
+ let _resizeDebounce = null;
36
+ const onWindowResize = () => {
37
+ clearTimeout(_resizeDebounce);
38
+ _resizeDebounce = setTimeout(() => this._updateOverlayPosition(), 100);
39
+ };
40
+
35
41
  this._disposers.push(
36
42
  on(editable, 'click', (e) => this._onEditorClick(e)),
37
43
  on(editable, 'contextmenu', (e) => {
@@ -40,7 +46,7 @@ export class VideoResizer {
40
46
  }),
41
47
  on(document, 'click', (e) => this._onDocClick(e)),
42
48
  on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
43
- on(window, 'resize', () => this._updateOverlayPosition()),
49
+ on(window, 'resize', onWindowResize),
44
50
  on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
45
51
  );
46
52
 
@@ -192,43 +198,49 @@ export class VideoResizer {
192
198
  const isCorner = pos.length === 2;
193
199
 
194
200
  const editable = this.context.layoutInfo.editable;
201
+ let _raf = null; // rAF handle — at most one write per paint frame
202
+
195
203
  const onMove = (me) => {
196
- const dx = me.clientX - startX;
197
- const dy = me.clientY - startY;
198
- const maxW = editable.clientWidth || Infinity;
199
- let newW = startW;
200
- let newH = startH;
201
-
202
- if (pos.includes('e')) newW = Math.max(80, startW + dx);
203
- if (pos.includes('w')) newW = Math.max(80, startW - dx);
204
- if (pos.includes('s')) newH = Math.max(45, startH + dy);
205
- if (pos.includes('n')) newH = Math.max(45, startH - dy);
206
-
207
- // Clamp to container width
208
- newW = Math.min(newW, maxW);
209
-
210
- if (isCorner) {
211
- if (Math.abs(dx) >= Math.abs(dy)) {
212
- newH = Math.max(45, Math.round(newW / aspectRatio));
213
- } else {
214
- newW = Math.min(Math.max(80, Math.round(newH * aspectRatio)), maxW);
215
- newH = Math.max(45, Math.round(newW / aspectRatio));
204
+ if (_raf !== null) return;
205
+ const clientX = me.clientX;
206
+ const clientY = me.clientY;
207
+ _raf = requestAnimationFrame(() => {
208
+ _raf = null;
209
+ const dx = clientX - startX;
210
+ const dy = clientY - startY;
211
+ const maxW = editable.clientWidth || Infinity;
212
+ let newW = startW;
213
+ let newH = startH;
214
+
215
+ if (pos.includes('e')) newW = Math.max(80, startW + dx);
216
+ if (pos.includes('w')) newW = Math.max(80, startW - dx);
217
+ if (pos.includes('s')) newH = Math.max(45, startH + dy);
218
+ if (pos.includes('n')) newH = Math.max(45, startH - dy);
219
+
220
+ newW = Math.min(newW, maxW);
221
+
222
+ if (isCorner) {
223
+ if (Math.abs(dx) >= Math.abs(dy)) {
224
+ newH = Math.max(45, Math.round(newW / aspectRatio));
225
+ } else {
226
+ newW = Math.min(Math.max(80, Math.round(newH * aspectRatio)), maxW);
227
+ newH = Math.max(45, Math.round(newW / aspectRatio));
228
+ }
229
+ }
230
+
231
+ // Resize wrapper and inner embed via CSS only — attribute writes are redundant
232
+ wrapper.style.width = `${newW}px`;
233
+ wrapper.style.height = `${newH}px`;
234
+ if (embed) {
235
+ embed.style.width = `${newW}px`;
236
+ embed.style.height = `${newH}px`;
216
237
  }
217
- }
218
-
219
- // Resize both the wrapper and the inner embed element
220
- wrapper.style.width = `${newW}px`;
221
- wrapper.style.height = `${newH}px`;
222
- if (embed) {
223
- embed.width = newW;
224
- embed.height = newH;
225
- embed.style.width = `${newW}px`;
226
- embed.style.height = `${newH}px`;
227
- }
228
- this._updateOverlayPosition();
238
+ this._updateOverlayPosition();
239
+ });
229
240
  };
230
241
 
231
242
  const onUp = () => {
243
+ if (_raf !== null) { cancelAnimationFrame(_raf); _raf = null; }
232
244
  document.removeEventListener('mousemove', onMove);
233
245
  document.removeEventListener('mouseup', onUp);
234
246
  this._dragDisposers = null;
@@ -42,13 +42,13 @@ export class VideoTooltip {
42
42
  if (wrapper && editable.contains(wrapper)) {
43
43
  this._scheduleShow(wrapper);
44
44
  }
45
- }),
45
+ }, { passive: true }),
46
46
  on(editable, 'mouseout', (e) => {
47
47
  const to = e.relatedTarget;
48
48
  if (!to || (!editable.contains(to) && !this._el.contains(to))) {
49
49
  this._scheduleHide();
50
50
  }
51
- }),
51
+ }, { passive: true }),
52
52
  on(document, 'click', (e) => {
53
53
  if (
54
54
  this._activeWrapper &&
@@ -174,7 +174,10 @@ export class VideoTooltip {
174
174
 
175
175
  _show(wrapper) {
176
176
  this._el.style.display = 'flex';
177
- this._positionNear(wrapper);
177
+ // Defer: offsetWidth on a newly-visible element forces synchronous layout
178
+ requestAnimationFrame(() => {
179
+ if (this._activeWrapper) this._positionNear(this._activeWrapper);
180
+ });
178
181
  }
179
182
 
180
183
  _hide() {
@@ -221,7 +224,7 @@ export class VideoTooltip {
221
224
  wrapper.style.marginRight = value === 'left' ? '12px' : '';
222
225
  this.context.invoke('editor.afterCommand');
223
226
  this.context.invoke('videoResizer.updateOverlay');
224
- this._positionNear(wrapper);
227
+ requestAnimationFrame(() => { if (this._activeWrapper) this._positionNear(this._activeWrapper); });
225
228
  }
226
229
 
227
230
  _setCenter() {
@@ -233,7 +236,7 @@ export class VideoTooltip {
233
236
  wrapper.style.marginRight = 'auto';
234
237
  this.context.invoke('editor.afterCommand');
235
238
  this.context.invoke('videoResizer.updateOverlay');
236
- this._positionNear(wrapper);
239
+ requestAnimationFrame(() => { if (this._activeWrapper) this._positionNear(this._activeWrapper); });
237
240
  }
238
241
 
239
242
  _resetSize() {
@@ -250,7 +253,7 @@ export class VideoTooltip {
250
253
  }
251
254
  this.context.invoke('editor.afterCommand');
252
255
  this.context.invoke('videoResizer.updateOverlay');
253
- this._positionNear(wrapper);
256
+ requestAnimationFrame(() => { if (this._activeWrapper) this._positionNear(this._activeWrapper); });
254
257
  }
255
258
 
256
259
  _delete() {