@hyperframes/studio 0.8.138 → 0.8.139
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/dist/assets/{hyperframes-player-B30apksz.js → hyperframes-player-CckOoCJ8.js} +1 -1
- package/dist/assets/{index-DIJuIBwI.js → index-BmIPrb8I.js} +1 -1
- package/dist/assets/{index-BdjA0CLD.js → index-Br1Whc0f.js} +192 -192
- package/dist/assets/{index-DXVvWfQG.js → index-DsEkE5_G.js} +1 -1
- package/dist/{chunk-4S3AYCOG.js → chunk-E422JDF5.js} +2 -2
- package/dist/{chunk-TDWGXBUT.js → chunk-TFLU2MGI.js} +1 -6
- package/dist/chunk-TFLU2MGI.js.map +1 -0
- package/dist/{chunk-7UQWVN52.js → chunk-UYWON5AX.js} +2 -2
- package/dist/{domEditingLayers-IEVLO54Z.js → domEditingLayers-KBTPQKGA.js} +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.html +1 -1
- package/dist/index.js +846 -469
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/package.json +8 -8
- package/src/App.tsx +9 -4
- package/src/components/editor/DomEditOverlay.tsx +9 -5
- package/src/components/editor/PropertyPanel.test.tsx +600 -748
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +2 -0
- package/src/components/editor/domEditOverlayGeometry.ts +7 -7
- package/src/components/editor/domEditOverlayGestures.ts +18 -0
- package/src/components/editor/domEditOverlayStartGesture.blocked.test.ts +1 -0
- package/src/components/editor/domEditOverlayStartGesture.test.ts +3 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +9 -1
- package/src/components/editor/dragUndoPaint.test.ts +2 -0
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +432 -15
- package/src/components/editor/groupDropKeepsSelection.test.ts +1 -0
- package/src/components/editor/manualEditsDom.ts +7 -7
- package/src/components/editor/manualEditsTypes.ts +5 -0
- package/src/components/editor/manualOffsetDrag.test.ts +23 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -0
- package/src/components/editor/previewGestureStarts.ts +284 -0
- package/src/components/editor/rotateInlineSave.test.ts +1 -0
- package/src/components/editor/shiftDragAxisLock.test.tsx +1 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +67 -71
- package/src/components/nle/NLEContext.test.ts +26 -0
- package/src/components/nle/NLEContext.tsx +8 -3
- package/src/hooks/draggedGsapPosition.ts +8 -0
- package/src/hooks/editMoment.ts +13 -0
- package/src/hooks/gsapDragCommit.ts +2 -1
- package/src/hooks/gsapDragPositionCommit.ts +19 -7
- package/src/hooks/gsapPositionDetection.ts +5 -3
- package/src/hooks/gsapResizeIntercept.ts +11 -4
- package/src/hooks/gsapRuntimeBridge.ts +31 -11
- package/src/hooks/gsapValueAtPlayhead.test.ts +15 -0
- package/src/hooks/gsapValueAtPlayhead.ts +12 -5
- package/src/hooks/timelineEditingHelpers.test.ts +22 -0
- package/src/hooks/timelineEditingHelpers.ts +18 -2
- package/src/hooks/useDomSelection.test.ts +21 -2
- package/src/hooks/useEditHistoryActions.test.tsx +20 -0
- package/src/hooks/useEditHistoryActions.ts +2 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +38 -0
- package/src/hooks/useGsapAwareEditing.ts +148 -136
- package/src/hooks/useGsapAwareGroupMove.ts +126 -113
- package/src/hooks/useGsapScriptCommits.ts +5 -4
- package/src/hooks/usePreviewPersistence.history.test.tsx +7 -2
- package/src/hooks/usePreviewPersistence.ts +1 -4
- package/src/hooks/useTimelineDeleteOps.test.tsx +10 -0
- package/src/hooks/useTimelineDeleteOps.ts +9 -10
- package/src/index.ts +1 -0
- package/src/player/hooks/useShadowPreviewReload.ts +6 -6
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +14 -0
- package/src/player/hooks/useTimelinePlayer.ts +8 -5
- package/src/player/lib/thumbnailScheduler.test.ts +20 -1
- package/src/player/lib/thumbnailScheduler.ts +1 -1
- package/src/player/lib/timelineElementHelpers.ts +0 -11
- package/src/player/previewReloading.test.ts +48 -0
- package/src/player/previewReloading.ts +48 -0
- package/src/utils/rootDuration.ts +5 -4
- package/src/utils/studioPendingEdits.test.ts +27 -0
- package/src/utils/studioPendingEdits.ts +9 -0
- package/src/utils/timelineAssetDrop.test.ts +13 -4
- package/src/utils/timelineAssetDrop.ts +6 -2
- package/dist/chunk-TDWGXBUT.js.map +0 -1
- /package/dist/{chunk-4S3AYCOG.js.map → chunk-E422JDF5.js.map} +0 -0
- /package/dist/{chunk-7UQWVN52.js.map → chunk-UYWON5AX.js.map} +0 -0
- /package/dist/{domEditingLayers-IEVLO54Z.js.map → domEditingLayers-KBTPQKGA.js.map} +0 -0
|
@@ -17,30 +17,55 @@ import "./domEditOverlayTestMocks";
|
|
|
17
17
|
import { DomEditOverlay } from "./DomEditOverlay";
|
|
18
18
|
import { PreviewReadOnlyProvider } from "./previewReadOnlyContext";
|
|
19
19
|
import { STUDIO_MANUAL_EDIT_GESTURE_ATTR } from "./manualEditsTypes";
|
|
20
|
+
import { PRESS_WAITING_ATTR } from "./domEditOverlayGestures";
|
|
21
|
+
import { PRESS_WAIT_MAX_MS } from "./previewGestureStarts";
|
|
22
|
+
import { readDragStamp } from "../../hooks/draggedGsapPosition";
|
|
23
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
24
|
+
import { whileScriptWrites } from "../../player/previewReloading";
|
|
25
|
+
import { cancelNewestStudioWaitingPress } from "../../utils/studioPendingEdits";
|
|
20
26
|
|
|
21
27
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
22
28
|
|
|
23
29
|
const layout = vi.hoisted(() => ({
|
|
24
30
|
group: [] as unknown[],
|
|
31
|
+
rafPaused: { current: false } as { current: boolean },
|
|
25
32
|
rect: { left: 100, top: 100, width: 200, height: 100, editScaleX: 1, editScaleY: 1 },
|
|
26
33
|
}));
|
|
27
34
|
vi.mock("./useDomEditOverlayRects", () => ({
|
|
28
|
-
useDomEditOverlayRects: () => {
|
|
35
|
+
useDomEditOverlayRects: (options: { rafPausedRef: { current: boolean } }) => {
|
|
36
|
+
layout.rafPaused = options.rafPausedRef;
|
|
29
37
|
const { rect, group } = layout;
|
|
38
|
+
const [overlayRect, setOverlayRect] = React.useState(rect);
|
|
39
|
+
const overlayRectRef = React.useRef(overlayRect);
|
|
40
|
+
overlayRectRef.current = overlayRect;
|
|
30
41
|
const groupOverlayItemsRef = { current: group };
|
|
31
42
|
const noop = () => undefined;
|
|
32
43
|
return {
|
|
33
|
-
overlayRect
|
|
34
|
-
overlayRectRef
|
|
44
|
+
overlayRect,
|
|
45
|
+
overlayRectRef,
|
|
35
46
|
groupOverlayItems: group,
|
|
36
47
|
groupOverlayItemsRef,
|
|
37
48
|
hoverRect: null,
|
|
38
49
|
childRects: [],
|
|
39
|
-
setOverlayRect
|
|
50
|
+
setOverlayRect,
|
|
40
51
|
setGroupOverlayItems: noop,
|
|
41
52
|
};
|
|
42
53
|
},
|
|
43
54
|
}));
|
|
55
|
+
vi.mock("./domEditOverlayGeometry", async (importOriginal) => ({
|
|
56
|
+
...(await importOriginal<typeof import("./domEditOverlayGeometry")>()),
|
|
57
|
+
orientedGroupAwareOverlayRect: (_overlay: unknown, _iframe: unknown, element: HTMLElement) => {
|
|
58
|
+
const [x = 0, y = 0] = element.style
|
|
59
|
+
.getPropertyValue("translate")
|
|
60
|
+
.split(" ")
|
|
61
|
+
.map(Number.parseFloat);
|
|
62
|
+
return {
|
|
63
|
+
...layout.rect,
|
|
64
|
+
left: 100 + (Number.isFinite(x) ? x : 0),
|
|
65
|
+
top: 100 + (Number.isFinite(y) ? y : 0),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
44
69
|
vi.mock("../../utils/gsapSoftReload", async (importOriginal) => ({
|
|
45
70
|
...(await importOriginal<typeof import("../../utils/gsapSoftReload")>()),
|
|
46
71
|
ensureMotionPathPluginLoaded: vi.fn(),
|
|
@@ -65,6 +90,14 @@ const marked = (doc: Document) => doc.querySelectorAll(`[${STUDIO_MANUAL_EDIT_GE
|
|
|
65
90
|
const byId = (iframe: HTMLIFrameElement | null, id: string) =>
|
|
66
91
|
iframe!.contentDocument!.getElementById(id) as HTMLElement;
|
|
67
92
|
const api = (): TimelinePlayerApi => player.getApi();
|
|
93
|
+
/** The south-east resize handle. */
|
|
94
|
+
const corner = (overlay: HTMLElement) =>
|
|
95
|
+
[...overlay.querySelectorAll<HTMLElement>("div.h-4.w-4")].reduce((a, b) =>
|
|
96
|
+
parseFloat(b.style.left) + parseFloat(b.style.top) >
|
|
97
|
+
parseFloat(a.style.left) + parseFloat(a.style.top)
|
|
98
|
+
? b
|
|
99
|
+
: a,
|
|
100
|
+
);
|
|
68
101
|
|
|
69
102
|
function pointer(
|
|
70
103
|
target: Element,
|
|
@@ -105,6 +138,7 @@ function mountEditor(
|
|
|
105
138
|
const write = (sel: DomEditSelection, next: { x: number; y: number }) =>
|
|
106
139
|
saveStyle(sel, `translate: ${next.x}px ${next.y}px`);
|
|
107
140
|
const onPathOffsetCommit = vi.fn(write);
|
|
141
|
+
const onCanvasMouseDown = vi.fn();
|
|
108
142
|
const onGroupPathOffsetCommit = vi.fn(
|
|
109
143
|
async (updates: { selection: DomEditSelection; next: { x: number; y: number } }[]) => {
|
|
110
144
|
for (const { selection, next } of updates) await write(selection, next);
|
|
@@ -112,17 +146,17 @@ function mountEditor(
|
|
|
112
146
|
);
|
|
113
147
|
const host = document.body.appendChild(document.createElement("div"));
|
|
114
148
|
overlayRoot = createRoot(host);
|
|
115
|
-
const render = (readOnly: boolean) =>
|
|
149
|
+
const render = (readOnly: boolean, selection = selections[0]!) =>
|
|
116
150
|
act(() =>
|
|
117
151
|
overlayRoot.render(
|
|
118
152
|
<PreviewReadOnlyProvider readOnly={readOnly}>
|
|
119
153
|
<DomEditOverlay
|
|
120
154
|
iframeRef={api().iframeRef}
|
|
121
155
|
activeCompositionPath={null}
|
|
122
|
-
selection={group ? null :
|
|
156
|
+
selection={group ? null : selection}
|
|
123
157
|
groupSelections={group ? selections : []}
|
|
124
158
|
hoverSelection={null}
|
|
125
|
-
onCanvasMouseDown={
|
|
159
|
+
onCanvasMouseDown={onCanvasMouseDown}
|
|
126
160
|
onCanvasPointerMove={() => Promise.resolve(null)}
|
|
127
161
|
onCanvasPointerLeave={() => undefined}
|
|
128
162
|
onSelectionChange={() => undefined}
|
|
@@ -138,7 +172,14 @@ function mountEditor(
|
|
|
138
172
|
);
|
|
139
173
|
render(false);
|
|
140
174
|
const overlay = host.querySelector('[aria-label="Composition canvas"]') as HTMLElement;
|
|
141
|
-
return {
|
|
175
|
+
return {
|
|
176
|
+
live,
|
|
177
|
+
overlay,
|
|
178
|
+
box: overlay.querySelector(BOX)!,
|
|
179
|
+
onPathOffsetCommit,
|
|
180
|
+
onCanvasMouseDown,
|
|
181
|
+
render,
|
|
182
|
+
};
|
|
142
183
|
}
|
|
143
184
|
|
|
144
185
|
/** A reload whose shadow paints while the gesture is still live. */
|
|
@@ -149,6 +190,9 @@ async function reloadMidGesture() {
|
|
|
149
190
|
return { shadow, gen };
|
|
150
191
|
}
|
|
151
192
|
|
|
193
|
+
/** Polls inside act, as testing-library's waitFor does, so a replayed press's renders are flushed. */
|
|
194
|
+
const waitFor = (check: () => void) => act(() => vi.waitFor(check));
|
|
195
|
+
|
|
152
196
|
async function settle() {
|
|
153
197
|
await act(async () => {
|
|
154
198
|
for (let tick = 0; tick < 10; tick++) await Promise.resolve();
|
|
@@ -160,7 +204,11 @@ beforeEach(() => {
|
|
|
160
204
|
HTMLElement.prototype.releasePointerCapture = () => undefined;
|
|
161
205
|
});
|
|
162
206
|
|
|
207
|
+
// Writes a test leaves out would hold every later press in the file.
|
|
208
|
+
const stuckWrites: Array<() => void> = [];
|
|
209
|
+
|
|
163
210
|
afterEach(() => {
|
|
211
|
+
for (const land of stuckWrites.splice(0)) land();
|
|
164
212
|
act(() => overlayRoot?.unmount());
|
|
165
213
|
act(() => player?.root.unmount());
|
|
166
214
|
layout.group = [];
|
|
@@ -199,13 +247,6 @@ describe("a reload during a drag", () => {
|
|
|
199
247
|
);
|
|
200
248
|
|
|
201
249
|
const handle = (overlay: HTMLElement, selector: string) => overlay.querySelector(selector)!;
|
|
202
|
-
const corner = (overlay: HTMLElement) =>
|
|
203
|
-
[...overlay.querySelectorAll<HTMLElement>("div.h-4.w-4")].reduce((a, b) =>
|
|
204
|
-
parseFloat(b.style.left) + parseFloat(b.style.top) >
|
|
205
|
-
parseFloat(a.style.left) + parseFloat(a.style.top)
|
|
206
|
-
? b
|
|
207
|
-
: a,
|
|
208
|
-
);
|
|
209
250
|
// Each edit presses at `from`, drags to `to`, and releases on `on`.
|
|
210
251
|
const edits = {
|
|
211
252
|
move: (o: HTMLElement) => ({ press: handle(o, BOX), on: o, from: [150, 150], to: [190, 170] }),
|
|
@@ -268,6 +309,354 @@ describe("a reload during a drag", () => {
|
|
|
268
309
|
});
|
|
269
310
|
});
|
|
270
311
|
|
|
312
|
+
describe("a press while the preview reloads", () => {
|
|
313
|
+
it("drags its element on the reloaded preview, at the time it was pressed", async () => {
|
|
314
|
+
const editor = mountEditor(false);
|
|
315
|
+
act(() => usePlayerStore.setState({ currentTime: 2 }));
|
|
316
|
+
act(() => api().refreshPlayer());
|
|
317
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
318
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
319
|
+
expect(marked(editor.live.contentDocument!), "the page being replaced").toHaveLength(0);
|
|
320
|
+
expect(editor.box.hasAttribute(PRESS_WAITING_ATTR), "drawn at the pointer").toBe(true);
|
|
321
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
322
|
+
act(() => usePlayerStore.setState({ currentTime: 3 }));
|
|
323
|
+
let pressedAt: number | undefined;
|
|
324
|
+
const save = editor.onPathOffsetCommit.getMockImplementation()!;
|
|
325
|
+
editor.onPathOffsetCommit.mockImplementationOnce((sel, ...rest) => {
|
|
326
|
+
pressedAt = readDragStamp(sel.element).at?.time;
|
|
327
|
+
return save(sel, ...rest);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const shown = served("?_t=1");
|
|
331
|
+
await paintShadow(api, shown);
|
|
332
|
+
editor.render(false, makeSelection("title", byId(shown, "title")));
|
|
333
|
+
await waitFor(() => expect(file.title).toBe("translate: 40px 20px"));
|
|
334
|
+
expect(byId(shown, "title").style.getPropertyValue("translate")).toBe("40px 20px");
|
|
335
|
+
expect(pressedAt).toBe(2);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("a press while a script write is out", () => {
|
|
340
|
+
it("waits for the write to land, then drags", async () => {
|
|
341
|
+
const editor = mountEditor(false);
|
|
342
|
+
let land: () => void = () => {};
|
|
343
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
344
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
345
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
346
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
347
|
+
expect(marked(editor.live.contentDocument!), "no drag before the write lands").toHaveLength(0);
|
|
348
|
+
expect(editor.onPathOffsetCommit).not.toHaveBeenCalled();
|
|
349
|
+
|
|
350
|
+
await act(async () => {
|
|
351
|
+
land();
|
|
352
|
+
await write;
|
|
353
|
+
});
|
|
354
|
+
await waitFor(() => expect(file.title).toBe("translate: 40px 20px"));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("keeps a released press when the browser releases pointer capture", async () => {
|
|
358
|
+
const editor = mountEditor(false);
|
|
359
|
+
let land: () => void = () => {};
|
|
360
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
361
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
362
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
363
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
364
|
+
pointer(editor.box, "lostpointercapture", 190, 170);
|
|
365
|
+
act(() => void editor.box.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
366
|
+
expect(editor.onCanvasMouseDown).not.toHaveBeenCalled();
|
|
367
|
+
|
|
368
|
+
await act(async () => {
|
|
369
|
+
land();
|
|
370
|
+
await write;
|
|
371
|
+
});
|
|
372
|
+
await waitFor(() => expect(file.title).toBe("translate: 40px 20px"));
|
|
373
|
+
expect(editor.onPathOffsetCommit).toHaveBeenCalledTimes(1);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("keeps the released waiting box still while the pointer moves to the next handle", async () => {
|
|
377
|
+
const editor = mountEditor(false);
|
|
378
|
+
let land: () => void = () => {};
|
|
379
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
380
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
381
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
382
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
383
|
+
expect((editor.box as HTMLElement).style.left).toBe("140px");
|
|
384
|
+
expect((editor.box as HTMLElement).style.top).toBe("120px");
|
|
385
|
+
pointer(editor.overlay, "pointermove", 340, 220, { buttons: 0 });
|
|
386
|
+
expect((editor.box as HTMLElement).style.left).toBe("140px");
|
|
387
|
+
expect((editor.box as HTMLElement).style.top).toBe("120px");
|
|
388
|
+
|
|
389
|
+
await act(async () => {
|
|
390
|
+
land();
|
|
391
|
+
await write;
|
|
392
|
+
});
|
|
393
|
+
await waitFor(() => expect(file.title).toBe("translate: 40px 20px"));
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("keeps the newest waiting box in place when an earlier press replays", async () => {
|
|
397
|
+
const editor = mountEditor(false);
|
|
398
|
+
let land: () => void = () => {};
|
|
399
|
+
let landFirst: () => void = () => {};
|
|
400
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
401
|
+
const firstSaved = new Promise<void>((resolve) => (landFirst = resolve));
|
|
402
|
+
const commit = editor.onPathOffsetCommit.getMockImplementation()!;
|
|
403
|
+
editor.onPathOffsetCommit.mockImplementationOnce((...args) =>
|
|
404
|
+
whileScriptWrites(async () => {
|
|
405
|
+
await firstSaved;
|
|
406
|
+
return commit(...args);
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
410
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
411
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
412
|
+
pointer(editor.box, "pointerdown", 190, 170);
|
|
413
|
+
pointer(editor.overlay, "pointermove", 210, 180);
|
|
414
|
+
pointer(editor.overlay, "pointerup", 210, 180);
|
|
415
|
+
expect((editor.box as HTMLElement).style.left).toBe("160px");
|
|
416
|
+
await act(async () => {
|
|
417
|
+
land();
|
|
418
|
+
await write;
|
|
419
|
+
});
|
|
420
|
+
await waitFor(() => expect(editor.onPathOffsetCommit).toHaveBeenCalledTimes(1));
|
|
421
|
+
expect((editor.box as HTMLElement).style.left).toBe("160px");
|
|
422
|
+
expect((editor.box as HTMLElement).style.top).toBe("130px");
|
|
423
|
+
await act(async () => {
|
|
424
|
+
landFirst();
|
|
425
|
+
await firstSaved;
|
|
426
|
+
});
|
|
427
|
+
await waitFor(() => expect(file.title).toBe("translate: 60px 30px"));
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("runs a second press after the first one, never instead of it", async () => {
|
|
431
|
+
const editor = mountEditor(false);
|
|
432
|
+
let land: () => void = () => {};
|
|
433
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
434
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
435
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
436
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
437
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
438
|
+
pointer(editor.overlay, "pointermove", 160, 150);
|
|
439
|
+
pointer(editor.overlay, "pointerup", 160, 150);
|
|
440
|
+
|
|
441
|
+
await act(async () => {
|
|
442
|
+
land();
|
|
443
|
+
await write;
|
|
444
|
+
});
|
|
445
|
+
await waitFor(() => expect(editor.onPathOffsetCommit).toHaveBeenCalledTimes(2));
|
|
446
|
+
expect(editor.onPathOffsetCommit.mock.calls.map((call) => call[1])).toEqual([
|
|
447
|
+
{ x: 40, y: 20 },
|
|
448
|
+
{ x: 50, y: 20 },
|
|
449
|
+
]);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("moves the element it pressed, even when another is selected before the write lands", async () => {
|
|
453
|
+
const editor = mountEditor(false);
|
|
454
|
+
let land: () => void = () => {};
|
|
455
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
456
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
457
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
458
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
459
|
+
editor.render(false, makeSelection("sub", byId(editor.live, "sub")));
|
|
460
|
+
|
|
461
|
+
await act(async () => {
|
|
462
|
+
land();
|
|
463
|
+
await write;
|
|
464
|
+
});
|
|
465
|
+
await waitFor(() => expect(file.title).toBe("translate: 40px 20px"));
|
|
466
|
+
expect(file.sub).toBe("");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("draws a waiting resize at the size it saves", async () => {
|
|
470
|
+
const editor = mountEditor(false);
|
|
471
|
+
let land: () => void = () => {};
|
|
472
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
473
|
+
pointer(corner(editor.overlay), "pointerdown", 300, 200);
|
|
474
|
+
pointer(editor.overlay, "pointermove", 340, 200);
|
|
475
|
+
pointer(editor.overlay, "pointerup", 340, 200);
|
|
476
|
+
const box = editor.box as HTMLElement;
|
|
477
|
+
const drawn = [box.style.width, box.style.height].map(Number.parseFloat);
|
|
478
|
+
|
|
479
|
+
await act(async () => {
|
|
480
|
+
land();
|
|
481
|
+
await write;
|
|
482
|
+
});
|
|
483
|
+
await waitFor(() => expect(file.title).toMatch(/^width: /));
|
|
484
|
+
const saved = Number.parseFloat(file.title.slice("width: ".length));
|
|
485
|
+
// The save rounds to whole px.
|
|
486
|
+
expect(drawn[0]).toBeCloseTo(saved, 0);
|
|
487
|
+
expect(drawn[1]).toBeCloseTo((saved * RECT.height) / RECT.width, 0);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("stops waiting on a write that never lands, and runs on the page shown", async () => {
|
|
491
|
+
vi.useFakeTimers({ toFake: ["requestAnimationFrame", "cancelAnimationFrame", "performance"] });
|
|
492
|
+
let land: () => void = () => {};
|
|
493
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
494
|
+
try {
|
|
495
|
+
const editor = mountEditor(false);
|
|
496
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
497
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
498
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
499
|
+
await act(async () => void vi.advanceTimersByTime(PRESS_WAIT_MAX_MS - 100));
|
|
500
|
+
expect(editor.onPathOffsetCommit, "still waiting").not.toHaveBeenCalled();
|
|
501
|
+
await act(async () => void vi.advanceTimersByTime(200));
|
|
502
|
+
expect(editor.onPathOffsetCommit.mock.calls.map((call) => call[1])).toEqual([
|
|
503
|
+
{ x: 40, y: 20 },
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
pointer(editor.box, "pointerdown", 190, 170);
|
|
507
|
+
expect(editor.box.hasAttribute(PRESS_WAITING_ATTR), "no second wait on the same write").toBe(
|
|
508
|
+
false,
|
|
509
|
+
);
|
|
510
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
511
|
+
} finally {
|
|
512
|
+
vi.useRealTimers();
|
|
513
|
+
await act(async () => {
|
|
514
|
+
land();
|
|
515
|
+
await write;
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("is what Cmd+Z undoes while it waits, so it never runs", async () => {
|
|
521
|
+
const editor = mountEditor(false);
|
|
522
|
+
let land: () => void = () => {};
|
|
523
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
524
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
525
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
526
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
527
|
+
|
|
528
|
+
let cancelled = false;
|
|
529
|
+
act(() => void (cancelled = cancelNewestStudioWaitingPress()));
|
|
530
|
+
expect(cancelled, "the waiting press is the newest edit").toBe(true);
|
|
531
|
+
expect((editor.box as HTMLElement).style.left, "the outline goes back").toBe("100px");
|
|
532
|
+
expect(editor.box.hasAttribute(PRESS_WAITING_ATTR)).toBe(false);
|
|
533
|
+
|
|
534
|
+
await act(async () => {
|
|
535
|
+
land();
|
|
536
|
+
await write;
|
|
537
|
+
});
|
|
538
|
+
await settle();
|
|
539
|
+
expect(editor.onPathOffsetCommit).not.toHaveBeenCalled();
|
|
540
|
+
expect(file.title).toBe("");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
/** Two presses wait on a write; the first runs and starts its own write, so the second still waits. */
|
|
544
|
+
function queuedBehindOneThatRan() {
|
|
545
|
+
const editor = mountEditor(false);
|
|
546
|
+
let land: () => void = () => {};
|
|
547
|
+
let landFirst: () => void = () => {};
|
|
548
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
549
|
+
const firstSaved = new Promise<void>((resolve) => (landFirst = resolve));
|
|
550
|
+
stuckWrites.push(
|
|
551
|
+
() => land(),
|
|
552
|
+
() => landFirst(),
|
|
553
|
+
);
|
|
554
|
+
const commit = editor.onPathOffsetCommit.getMockImplementation()!;
|
|
555
|
+
editor.onPathOffsetCommit.mockImplementationOnce((...args) =>
|
|
556
|
+
whileScriptWrites(async () => {
|
|
557
|
+
await firstSaved;
|
|
558
|
+
return commit(...args);
|
|
559
|
+
}),
|
|
560
|
+
);
|
|
561
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
562
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
563
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
564
|
+
pointer(editor.box, "pointerdown", 190, 170);
|
|
565
|
+
pointer(editor.overlay, "pointermove", 210, 180);
|
|
566
|
+
const ran = async () => {
|
|
567
|
+
await act(async () => {
|
|
568
|
+
land();
|
|
569
|
+
await write;
|
|
570
|
+
});
|
|
571
|
+
await waitFor(() => expect(editor.onPathOffsetCommit).toHaveBeenCalledTimes(1));
|
|
572
|
+
};
|
|
573
|
+
const landFirstSave = () =>
|
|
574
|
+
act(async () => {
|
|
575
|
+
landFirst();
|
|
576
|
+
await firstSaved;
|
|
577
|
+
});
|
|
578
|
+
return { editor, ran, landFirstSave };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
it("leaves Cmd+Z to history once the press it could take back has run", async () => {
|
|
582
|
+
const editor = mountEditor(false);
|
|
583
|
+
let land: () => void = () => {};
|
|
584
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
585
|
+
stuckWrites.push(() => land());
|
|
586
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
587
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
588
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
589
|
+
await act(async () => {
|
|
590
|
+
land();
|
|
591
|
+
await write;
|
|
592
|
+
});
|
|
593
|
+
await waitFor(() => expect(file.title).toBe("translate: 40px 20px"));
|
|
594
|
+
expect(cancelNewestStudioWaitingPress()).toBe(false);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("takes back a press queued behind one that ran, and leaves nothing waiting", async () => {
|
|
598
|
+
const { editor, ran, landFirstSave } = queuedBehindOneThatRan();
|
|
599
|
+
pointer(editor.overlay, "pointerup", 210, 180);
|
|
600
|
+
await ran();
|
|
601
|
+
expect(editor.box.hasAttribute(PRESS_WAITING_ATTR), "the second press waits").toBe(true);
|
|
602
|
+
|
|
603
|
+
act(() => void cancelNewestStudioWaitingPress());
|
|
604
|
+
expect(editor.box.hasAttribute(PRESS_WAITING_ATTR)).toBe(false);
|
|
605
|
+
expect(cancelNewestStudioWaitingPress(), "the following Cmd+Z steps history").toBe(false);
|
|
606
|
+
await landFirstSave();
|
|
607
|
+
await settle();
|
|
608
|
+
expect(editor.onPathOffsetCommit).toHaveBeenCalledTimes(1);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("drops a held press queued behind one that ran on blur, and leaves nothing waiting", async () => {
|
|
612
|
+
const { editor, ran, landFirstSave } = queuedBehindOneThatRan();
|
|
613
|
+
await ran();
|
|
614
|
+
act(() => void window.dispatchEvent(new Event("blur")));
|
|
615
|
+
expect(editor.box.hasAttribute(PRESS_WAITING_ATTR)).toBe(false);
|
|
616
|
+
expect(cancelNewestStudioWaitingPress()).toBe(false);
|
|
617
|
+
await landFirstSave();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it("lands a released drag when the window loses focus before the write does", async () => {
|
|
621
|
+
const editor = mountEditor(false);
|
|
622
|
+
let land: () => void = () => {};
|
|
623
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
624
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
625
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
626
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
627
|
+
act(() => void window.dispatchEvent(new Event("blur")));
|
|
628
|
+
expect(layout.rafPaused.current, "the box does not re-measure under a waiting press").toBe(
|
|
629
|
+
true,
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
await act(async () => {
|
|
633
|
+
land();
|
|
634
|
+
await write;
|
|
635
|
+
});
|
|
636
|
+
await waitFor(() => expect(file.title).toBe("translate: 40px 20px"));
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("drops only the press still held when the window loses focus", async () => {
|
|
640
|
+
const editor = mountEditor(false);
|
|
641
|
+
let land: () => void = () => {};
|
|
642
|
+
const write = whileScriptWrites(() => new Promise<void>((resolve) => (land = resolve)));
|
|
643
|
+
pointer(editor.box, "pointerdown", 150, 150);
|
|
644
|
+
pointer(editor.overlay, "pointermove", 190, 170);
|
|
645
|
+
pointer(editor.overlay, "pointerup", 190, 170);
|
|
646
|
+
pointer(editor.box, "pointerdown", 190, 170);
|
|
647
|
+
pointer(editor.overlay, "pointermove", 220, 170);
|
|
648
|
+
act(() => void window.dispatchEvent(new Event("blur")));
|
|
649
|
+
expect((editor.box as HTMLElement).style.left, "back to the released drop").toBe("140px");
|
|
650
|
+
|
|
651
|
+
await act(async () => {
|
|
652
|
+
land();
|
|
653
|
+
await write;
|
|
654
|
+
});
|
|
655
|
+
await waitFor(() => expect(file.title).toBe("translate: 40px 20px"));
|
|
656
|
+
expect(editor.onPathOffsetCommit).toHaveBeenCalledTimes(1);
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
271
660
|
describe("a drag moves only with its own pointer pressed", () => {
|
|
272
661
|
it.each([
|
|
273
662
|
["no button held, as Chromium sends after a layout change", { buttons: 0 }],
|
|
@@ -289,6 +678,34 @@ describe("a drag moves only with its own pointer pressed", () => {
|
|
|
289
678
|
);
|
|
290
679
|
});
|
|
291
680
|
|
|
681
|
+
describe("a press while the preview swaps scenes in place", () => {
|
|
682
|
+
it("waits for the swap and moves the swapped-in node", async () => {
|
|
683
|
+
const { live, overlay, box } = mountEditor(false);
|
|
684
|
+
Object.assign(live.contentWindow!, {
|
|
685
|
+
__hfSwapScenes: (html: string) => {
|
|
686
|
+
live.contentDocument!.body.innerHTML = html.replace(/^[\s\S]*<body>|<\/body>[\s\S]*$/g, "");
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
let reply: (r: Response) => void = () => {};
|
|
690
|
+
vi.stubGlobal("fetch", () => new Promise<Response>((resolve) => (reply = resolve)));
|
|
691
|
+
try {
|
|
692
|
+
const replaced = byId(live, "title");
|
|
693
|
+
act(() => api().refreshPlayer());
|
|
694
|
+
pointer(box, "pointerdown", 150, 150);
|
|
695
|
+
pointer(overlay, "pointermove", 190, 170);
|
|
696
|
+
pointer(overlay, "pointerup", 190, 170);
|
|
697
|
+
expect(box.hasAttribute(PRESS_WAITING_ATTR), "held until the new scenes show").toBe(true);
|
|
698
|
+
|
|
699
|
+
reply(new Response(served("").contentDocument!.documentElement.outerHTML));
|
|
700
|
+
await waitFor(() => expect(file.title).toBe("translate: 40px 20px"));
|
|
701
|
+
expect(byId(live, "title")).not.toBe(replaced);
|
|
702
|
+
expect(byId(live, "title").style.getPropertyValue("translate")).toBe("40px 20px");
|
|
703
|
+
} finally {
|
|
704
|
+
vi.unstubAllGlobals();
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
|
|
292
709
|
describe("an outside change during a drag, on a preview that can swap scenes in place", () => {
|
|
293
710
|
it("leaves the dragged node in place and shows the change after the drop, loaded fresh", async () => {
|
|
294
711
|
const { live, overlay, box } = mountEditor(false);
|
|
@@ -37,6 +37,7 @@ describe("dropping a dragged group eats the click that follows", () => {
|
|
|
37
37
|
gestureRef: { current: null },
|
|
38
38
|
groupGestureRef,
|
|
39
39
|
blockedMoveRef: { current: null },
|
|
40
|
+
waitingPressRef: { current: null },
|
|
40
41
|
rafPausedRef: { current: false },
|
|
41
42
|
suppressNextBoxClickRef,
|
|
42
43
|
setOverlayRect: vi.fn(),
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
STUDIO_ROTATION_PROP,
|
|
7
7
|
STUDIO_PATH_OFFSET_ATTR,
|
|
8
8
|
STUDIO_MANUAL_EDIT_GESTURE_ATTR,
|
|
9
|
+
STUDIO_EDIT_MOMENT_ATTR,
|
|
10
|
+
type EditMoment,
|
|
9
11
|
STUDIO_BOX_SIZE_ATTR,
|
|
10
12
|
STUDIO_ROTATION_ATTR,
|
|
11
13
|
STUDIO_ORIGINAL_TRANSLATE_ATTR,
|
|
@@ -52,10 +54,13 @@ const GESTURE_DRAWS: Record<StudioGestureDraws, readonly string[]> = {
|
|
|
52
54
|
export function beginStudioManualEditGesture(
|
|
53
55
|
element: HTMLElement,
|
|
54
56
|
draws: StudioGestureDraws,
|
|
57
|
+
at?: EditMoment,
|
|
55
58
|
): string {
|
|
56
59
|
studioManualEditGestureId += 1;
|
|
57
60
|
const token = `gesture-${studioManualEditGestureId}:${draws}`;
|
|
58
61
|
element.setAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR, token);
|
|
62
|
+
if (at) element.setAttribute(STUDIO_EDIT_MOMENT_ATTR, JSON.stringify(at));
|
|
63
|
+
else element.removeAttribute(STUDIO_EDIT_MOMENT_ATTR);
|
|
59
64
|
return token;
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -65,6 +70,7 @@ export function endStudioManualEditGesture(element: HTMLElement, token?: string)
|
|
|
65
70
|
if (token && element.getAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR) !== token) return;
|
|
66
71
|
if (!element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR)) return;
|
|
67
72
|
element.removeAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
|
|
73
|
+
element.removeAttribute(STUDIO_EDIT_MOMENT_ATTR);
|
|
68
74
|
const doc = element.ownerDocument;
|
|
69
75
|
doc.dispatchEvent(new (doc.defaultView?.Event ?? Event)(GESTURE_ENDED));
|
|
70
76
|
}
|
|
@@ -132,13 +138,7 @@ export function readStudioPathOffset(element: HTMLElement): { x: number; y: numb
|
|
|
132
138
|
};
|
|
133
139
|
}
|
|
134
140
|
|
|
135
|
-
|
|
136
|
-
* The path offset ACTUALLY applied right now. The `--hf-studio-offset` vars can
|
|
137
|
-
* linger after GSAP re-bakes the element's transform (`translate:"none"`), so the
|
|
138
|
-
* raw var isn't a safe drag base — using it re-commits a phantom offset and flings
|
|
139
|
-
* the element off-screen. The offset only counts when the inline `translate` is the
|
|
140
|
-
* studio var-translate; otherwise it's dormant and the applied offset is zero.
|
|
141
|
-
*/
|
|
141
|
+
// GSAP can leave dormant offset vars behind; only the live var-translate counts.
|
|
142
142
|
export function readAppliedStudioPathOffset(element: HTMLElement): { x: number; y: number } {
|
|
143
143
|
return (element.style.translate || "").includes(STUDIO_OFFSET_X_PROP)
|
|
144
144
|
? readStudioPathOffset(element)
|
|
@@ -10,6 +10,11 @@ export const STUDIO_ROTATION_PROP = "--hf-studio-rotation";
|
|
|
10
10
|
|
|
11
11
|
/* ── Internal DOM attribute names ─────────────────────────────────── */
|
|
12
12
|
export const STUDIO_PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
|
|
13
|
+
export const STUDIO_EDIT_MOMENT_ATTR = "data-hf-studio-edit-at";
|
|
14
|
+
export interface EditMoment {
|
|
15
|
+
time: number;
|
|
16
|
+
keyframePct: number | null;
|
|
17
|
+
}
|
|
13
18
|
export const STUDIO_BOX_SIZE_ATTR = "data-hf-studio-box-size";
|
|
14
19
|
export const STUDIO_ROTATION_ATTR = "data-hf-studio-rotation";
|
|
15
20
|
export const STUDIO_ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
|
|
@@ -452,6 +452,29 @@ describe("GSAP-element drag — dot-a flies regressions", () => {
|
|
|
452
452
|
});
|
|
453
453
|
});
|
|
454
454
|
|
|
455
|
+
describe("a drag that starts while an earlier gesture is still saving", () => {
|
|
456
|
+
it("keeps the timelines paused until the later drag ends", () => {
|
|
457
|
+
const { element, member } = makeGsapDot();
|
|
458
|
+
let paused = false;
|
|
459
|
+
const timeline = {
|
|
460
|
+
pause: () => (paused = true),
|
|
461
|
+
paused: (v?: boolean) => (paused = v ?? paused),
|
|
462
|
+
};
|
|
463
|
+
(element.ownerDocument.defaultView as unknown as { __timelines: unknown }).__timelines = {
|
|
464
|
+
main: timeline,
|
|
465
|
+
};
|
|
466
|
+
const earlier = member();
|
|
467
|
+
applyManualOffsetDragCommit(earlier, -50, 0);
|
|
468
|
+
const later = member();
|
|
469
|
+
|
|
470
|
+
endManualOffsetDragMembers([earlier]);
|
|
471
|
+
|
|
472
|
+
expect(paused).toBe(true);
|
|
473
|
+
endManualOffsetDragMembers([later]);
|
|
474
|
+
expect(paused).toBe(false);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
455
478
|
describe("sub-pixel drags", () => {
|
|
456
479
|
it("draft and commit both land the exact fractional position", () => {
|
|
457
480
|
const { element, sets, member } = makeGsapDot();
|
|
@@ -7,10 +7,12 @@ import {
|
|
|
7
7
|
captureStudioPathOffset,
|
|
8
8
|
clearStudioPathOffset,
|
|
9
9
|
endStudioManualEditGesture,
|
|
10
|
+
isStudioManualEditGestureCurrent,
|
|
10
11
|
readAppliedStudioPathOffset,
|
|
11
12
|
restoreStudioPathOffset,
|
|
12
13
|
type StudioPathOffsetSnapshot,
|
|
13
14
|
} from "./manualEdits";
|
|
15
|
+
import type { EditMoment } from "./manualEditsTypes";
|
|
14
16
|
import { computeDraggedGsapPosition } from "../../hooks/draggedGsapPosition";
|
|
15
17
|
import { editsPlainCss } from "../../hooks/gsapRuntimeKeyframes";
|
|
16
18
|
import { readTranslatePx, UNREADABLE_TRANSLATE, writeTranslatePx } from "./plainTranslate";
|
|
@@ -315,6 +317,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
315
317
|
element: HTMLElement;
|
|
316
318
|
rect: ManualOffsetDragRect;
|
|
317
319
|
gesture?: "drag" | "nudge" | "resize"; // resize: the anchor that keeps its centre planted
|
|
320
|
+
at?: EditMoment;
|
|
318
321
|
}): ManualOffsetDragMemberResult {
|
|
319
322
|
const plainTranslate =
|
|
320
323
|
!!input.gesture && editsPlainCss(input.element, input.gesture === "resize" ? "resize" : "move");
|
|
@@ -357,6 +360,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
357
360
|
const gestureToken = beginStudioManualEditGesture(
|
|
358
361
|
input.element,
|
|
359
362
|
input.gesture === "resize" ? "resize" : "move",
|
|
363
|
+
input.at,
|
|
360
364
|
);
|
|
361
365
|
const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset, {
|
|
362
366
|
scaleX: input.rect.editScaleX,
|
|
@@ -531,6 +535,7 @@ function settleDraftTranslate(element: HTMLElement, gsapOwnsPosition: boolean):
|
|
|
531
535
|
/** Teardown after a COMMITTED drag. */
|
|
532
536
|
export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): void {
|
|
533
537
|
for (const member of members) {
|
|
538
|
+
if (!isStudioManualEditGestureCurrent(member.element, member.gestureToken)) continue;
|
|
534
539
|
endStudioManualEditGesture(member.element, member.gestureToken);
|
|
535
540
|
member.element.removeAttribute("data-hf-drag-initial-offset-x");
|
|
536
541
|
member.element.removeAttribute("data-hf-drag-initial-offset-y");
|