@hyperframes/studio 0.8.102 → 0.8.104
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-BuRiFtrF.js → hyperframes-player-DAoaajuM.js} +1 -1
- package/dist/assets/{index-BT3sauee.js → index-BDK4rTMb.js} +1 -1
- package/dist/assets/{index-xB5O0hPw.css → index-CQN3M56N.css} +1 -1
- package/dist/assets/{index-C2utDZt-.js → index-De2aO4XM.js} +179 -179
- package/dist/assets/{index-BM35fjFf.js → index-WV9LroAD.js} +1 -1
- package/dist/{chunk-MTAVNAGW.js → chunk-46XP7J3B.js} +2 -2
- package/dist/{chunk-S65AQH6M.js → chunk-DI4LCJHL.js} +3 -2
- package/dist/chunk-DI4LCJHL.js.map +1 -0
- package/dist/{chunk-7X4XRP3L.js → chunk-HIGHVWYD.js} +2 -2
- package/dist/{domEditingLayers-NU6Z5QWA.js → domEditingLayers-AQBDFNUC.js} +3 -3
- package/dist/index.d.ts +183 -170
- package/dist/index.html +2 -2
- package/dist/index.js +550 -389
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/components/editor/DomEditOverlay.tsx +7 -7
- package/src/components/editor/PropertyPanel.tsx +5 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +46 -6
- package/src/components/editor/domEditOverlayGestures.ts +14 -20
- package/src/components/editor/domEditOverlayStartGesture.test.ts +170 -13
- package/src/components/editor/domEditOverlayStartGesture.ts +17 -27
- package/src/components/editor/manualEdits.test.ts +3 -9
- package/src/components/editor/manualEdits.ts +0 -1
- package/src/components/editor/manualEditsDom.ts +0 -10
- package/src/components/editor/manualEditsDomPatches.test.ts +1 -45
- package/src/components/editor/manualEditsDomPatches.ts +1 -23
- package/src/components/editor/manualEditsSnapshot.ts +2 -0
- package/src/components/editor/manualEditsTypes.ts +2 -0
- package/src/components/editor/manualOffsetDrag.test.ts +57 -0
- package/src/components/editor/manualOffsetDrag.ts +11 -32
- package/src/components/editor/plainTranslate.test.ts +38 -22
- package/src/components/editor/plainTranslate.ts +75 -40
- package/src/components/editor/previewReadOnly.overlay.test.tsx +30 -0
- package/src/components/editor/previewReadOnlyContext.tsx +3 -1
- package/src/components/editor/propertyPanelTransformCommit.test.ts +80 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +11 -6
- package/src/components/editor/propertyPanelTypes.ts +1 -0
- package/src/components/editor/rotateInlineSave.test.ts +111 -0
- package/src/components/editor/rotationDraft.test.ts +111 -0
- package/src/components/editor/rotationDraft.ts +145 -0
- package/src/components/editor/useColorGradingController.test.ts +36 -1
- package/src/components/editor/useColorGradingController.ts +2 -2
- package/src/components/editor/useDomEditNudge.test.tsx +34 -0
- package/src/components/editor/useDomEditNudge.ts +12 -5
- package/src/components/editor/useDomEditOverlayGestures.ts +22 -24
- package/src/hooks/draggedGsapPosition.ts +28 -0
- package/src/hooks/elementOffsetStager.ts +26 -6
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +36 -2
- package/src/hooks/plainRotation.test.ts +122 -0
- package/src/hooks/plainRotation.ts +42 -0
- package/src/hooks/useDomEditSession.ts +5 -5
- package/src/hooks/useDomGeometryCommit.test.tsx +75 -0
- package/src/hooks/useDomGeometryCommit.ts +17 -17
- package/src/hooks/useDomGeometryCommits.test.tsx +56 -1
- package/src/hooks/useDomGeometryCommits.ts +28 -36
- package/src/hooks/useGsapAwareEditing.test.tsx +194 -11
- package/src/hooks/useGsapAwareEditing.ts +43 -42
- package/src/hooks/useInlineTextEdit.test.tsx +33 -3
- package/src/hooks/useInlineTextEdit.ts +2 -2
- package/src/hooks/usePreviewInteraction.test.ts +32 -0
- package/src/hooks/usePreviewInteraction.ts +3 -2
- package/src/player/components/TimelineClipFades.test.tsx +103 -3
- package/src/player/components/TimelineClipFades.tsx +53 -50
- package/src/styles/theme.css +1 -1
- package/src/utils/projectRouting.test.ts +9 -0
- package/src/utils/projectRouting.ts +3 -1
- package/dist/chunk-S65AQH6M.js.map +0 -1
- /package/dist/{chunk-MTAVNAGW.js.map → chunk-46XP7J3B.js.map} +0 -0
- /package/dist/{chunk-7X4XRP3L.js.map → chunk-HIGHVWYD.js.map} +0 -0
- /package/dist/{domEditingLayers-NU6Z5QWA.js.map → domEditingLayers-AQBDFNUC.js.map} +0 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
1
2
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
3
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
4
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
4
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
DomEditGroupPathOffsetCommit,
|
|
7
|
+
MoveCommitOptions,
|
|
8
|
+
} from "../components/editor/DomEditOverlay";
|
|
5
9
|
import { isPreviewBooted } from "../player/store/playerStore";
|
|
6
10
|
import type { DomEditCommitOutcome } from "./domEditCommitRunner";
|
|
7
11
|
import type { UseDomStyleCommitOptions } from "./useDomStyleCommit";
|
|
@@ -13,8 +17,8 @@ import { useGsapCacheVersion } from "./useGsapTweenCache";
|
|
|
13
17
|
import { createDomEditSaveQueue } from "../utils/domEditSaveQueue";
|
|
14
18
|
import { useDomEditPersist } from "./useDomEditPersist";
|
|
15
19
|
import { useDomEditPositionPatchCommit } from "./useDomEditPositionPatchCommit";
|
|
20
|
+
import { useDomGeometryCommits } from "./useDomGeometryCommits";
|
|
16
21
|
import { useMountEffect } from "./useMountEffect";
|
|
17
|
-
import { stageElementOffset } from "./elementOffsetStager";
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Studio's `Player` must show the project, with `beginTimelineSession(projectId)` run before it
|
|
@@ -29,7 +33,7 @@ export interface DomGeometryCommits {
|
|
|
29
33
|
commitPathOffset: (
|
|
30
34
|
selection: DomEditSelection,
|
|
31
35
|
next: { x: number; y: number },
|
|
32
|
-
modifiers?:
|
|
36
|
+
modifiers?: MoveCommitOptions,
|
|
33
37
|
) => Promise<DomEditCommitOutcome>;
|
|
34
38
|
commitGroupPathOffset: (updates: DomEditGroupPathOffsetCommit[]) => Promise<DomEditCommitOutcome>;
|
|
35
39
|
commitBoxSize: (
|
|
@@ -40,15 +44,13 @@ export interface DomGeometryCommits {
|
|
|
40
44
|
) => Promise<DomEditCommitOutcome>;
|
|
41
45
|
commitRotation: (
|
|
42
46
|
selection: DomEditSelection,
|
|
43
|
-
next:
|
|
47
|
+
next: RotationCommit,
|
|
44
48
|
) => Promise<DomEditCommitOutcome>;
|
|
45
49
|
waitForPendingSaves: () => Promise<void>;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
const noop = () => {};
|
|
49
53
|
const NO_SELECTED_ANIMATIONS: GsapAnimation[] = [];
|
|
50
|
-
// ponytail: unreachable, the GSAP writer always exists so a resize never takes the DOM route.
|
|
51
|
-
const noDomBoxSizeRoute = () => Promise.reject(new Error("Resize has no DOM route here"));
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Saves canvas moves, resizes and rotations through Studio's own GSAP-aware commits, for a host
|
|
@@ -107,16 +109,13 @@ export function useDomGeometryCommit({
|
|
|
107
109
|
},
|
|
108
110
|
[commitPositionPatchToHtml, queue],
|
|
109
111
|
);
|
|
110
|
-
const stageElementPositionOffset =
|
|
111
|
-
(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
),
|
|
118
|
-
[commitWithFreshQueue, showToast],
|
|
119
|
-
);
|
|
112
|
+
const { stageElementPositionOffset, handleDomBoxSizeCommit, handleDomRotationCommit } =
|
|
113
|
+
useDomGeometryCommits({
|
|
114
|
+
previewIframeRef: iframeRef,
|
|
115
|
+
showToast,
|
|
116
|
+
commitPositionPatchToHtml: commitWithFreshQueue,
|
|
117
|
+
readOnlyPreview: false,
|
|
118
|
+
});
|
|
120
119
|
const makeFetchFallback = useGsapAnimationFetchFallback(projectId);
|
|
121
120
|
const trackGsapInteractionFailure = useGsapInteractionFailureTelemetry(activeCompPath, showToast);
|
|
122
121
|
const {
|
|
@@ -135,7 +134,8 @@ export function useDomGeometryCommit({
|
|
|
135
134
|
makeFetchFallback,
|
|
136
135
|
trackGsapInteractionFailure,
|
|
137
136
|
stageElementPositionOffset,
|
|
138
|
-
handleDomBoxSizeCommit
|
|
137
|
+
handleDomBoxSizeCommit,
|
|
138
|
+
handleDomRotationCommit,
|
|
139
139
|
commitPositionPatchToHtml: commitWithFreshQueue,
|
|
140
140
|
addGsapAnimation: gsap.addGsapAnimation,
|
|
141
141
|
convertToKeyframes: gsap.convertToKeyframes,
|
|
@@ -162,7 +162,7 @@ describe("useDomGeometryCommits element position offset", () => {
|
|
|
162
162
|
.mockResolvedValue(undefined);
|
|
163
163
|
const { commits, unmount } = mountCommits(commitPositionPatchToHtml);
|
|
164
164
|
|
|
165
|
-
await commits().stageElementPositionOffset(selection, { x: 40, y: 20 }).save();
|
|
165
|
+
await commits().stageElementPositionOffset(selection, { x: 40, y: 20 }, false).save();
|
|
166
166
|
|
|
167
167
|
const patches = commitPositionPatchToHtml.mock.calls[0]![1];
|
|
168
168
|
expect(patches).toEqual([
|
|
@@ -175,6 +175,61 @@ describe("useDomGeometryCommits element position offset", () => {
|
|
|
175
175
|
});
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
describe("useDomGeometryCommits resize anchor", () => {
|
|
179
|
+
it("saves the anchor as the element's own plain px translate, sub-pixel, with the size", async () => {
|
|
180
|
+
const element = document.createElement("div");
|
|
181
|
+
element.id = "anchored";
|
|
182
|
+
element.style.setProperty("translate", "40px 30px");
|
|
183
|
+
document.body.append(element);
|
|
184
|
+
const selection = {
|
|
185
|
+
id: "anchored",
|
|
186
|
+
selector: "#anchored",
|
|
187
|
+
element,
|
|
188
|
+
} as unknown as DomEditSelection;
|
|
189
|
+
const commit = vi
|
|
190
|
+
.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
|
|
191
|
+
.mockResolvedValue(undefined);
|
|
192
|
+
const { commits, unmount } = mountCommits(commit);
|
|
193
|
+
|
|
194
|
+
await commits().handleDomBoxSizeCommit(
|
|
195
|
+
selection,
|
|
196
|
+
{ width: 340, height: 227 },
|
|
197
|
+
{ x: -10.25, y: 3.5 },
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(commit).toHaveBeenCalledTimes(1);
|
|
201
|
+
const patches = commit.mock.calls[0]![1];
|
|
202
|
+
expect(patches).toContainEqual({ type: "inline-style", property: "width", value: "340px" });
|
|
203
|
+
expect(patches).toContainEqual({
|
|
204
|
+
type: "inline-style",
|
|
205
|
+
property: "translate",
|
|
206
|
+
value: "-10.25px 3.5px",
|
|
207
|
+
});
|
|
208
|
+
expect(patches.some((p) => p.property.startsWith("--hf-studio-offset"))).toBe(false);
|
|
209
|
+
expect(element.style.getPropertyValue("translate")).toBe("-10.25px 3.5px");
|
|
210
|
+
unmount();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("useDomGeometryCommits resize rollback", () => {
|
|
215
|
+
it("rolls the whole gesture back once when the resize save fails", async () => {
|
|
216
|
+
const element = document.createElement("div");
|
|
217
|
+
document.body.append(element);
|
|
218
|
+
const selection = { id: "r", selector: "#r", element } as unknown as DomEditSelection;
|
|
219
|
+
const failure = new Error("save failed");
|
|
220
|
+
const commit = vi
|
|
221
|
+
.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
|
|
222
|
+
.mockRejectedValue(failure);
|
|
223
|
+
const restore = vi.fn();
|
|
224
|
+
const { commits, unmount } = mountCommits(commit);
|
|
225
|
+
await expect(
|
|
226
|
+
commits().handleDomBoxSizeCommit(selection, { width: 50, height: 40 }, undefined, restore),
|
|
227
|
+
).rejects.toBe(failure);
|
|
228
|
+
expect(restore).toHaveBeenCalledTimes(1);
|
|
229
|
+
unmount();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
178
233
|
describe("useDomGeometryCommits resize of a cropped element", () => {
|
|
179
234
|
it("saves the crop scaled per axis with the box in the resize's own commit", async () => {
|
|
180
235
|
const element = withInlineLayoutBox(document.createElement("div"));
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
1
2
|
import { useCallback } from "react";
|
|
2
3
|
import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
|
|
3
4
|
import {
|
|
4
5
|
applyStudioPathOffset,
|
|
5
6
|
applyStudioBoxSize,
|
|
6
|
-
applyStudioRotation,
|
|
7
7
|
captureStudioPathOffset,
|
|
8
8
|
captureStudioBoxSize,
|
|
9
9
|
captureStudioRotation,
|
|
@@ -15,11 +15,12 @@ import {
|
|
|
15
15
|
clearStudioRotation,
|
|
16
16
|
} from "../components/editor/manualEdits";
|
|
17
17
|
import { stageElementOffset } from "./elementOffsetStager";
|
|
18
|
+
import { savePlainRotation } from "./plainRotation";
|
|
18
19
|
import { prepareCropResize } from "../components/editor/cropResize";
|
|
20
|
+
import { translatePatch, writeTranslatePx } from "../components/editor/plainTranslate";
|
|
19
21
|
import {
|
|
20
22
|
buildPathOffsetPatches,
|
|
21
23
|
buildBoxSizePatches,
|
|
22
|
-
buildRotationPatches,
|
|
23
24
|
buildClearPathOffsetPatches,
|
|
24
25
|
buildClearBoxSizePatches,
|
|
25
26
|
buildClearRotationPatches,
|
|
@@ -61,11 +62,17 @@ export function useDomGeometryCommits({
|
|
|
61
62
|
readOnlyPreview,
|
|
62
63
|
}: UseDomGeometryCommitsParams) {
|
|
63
64
|
const stageElementPositionOffset = useCallback(
|
|
64
|
-
(
|
|
65
|
+
(
|
|
66
|
+
selection: DomEditSelection,
|
|
67
|
+
next: { x: number; y: number },
|
|
68
|
+
plainTranslate: boolean,
|
|
69
|
+
coalesceKey?: string,
|
|
70
|
+
) =>
|
|
65
71
|
stageElementOffset(
|
|
66
72
|
{ commitPositionPatchToHtml, showToast, readOnlyPreview },
|
|
67
73
|
selection,
|
|
68
74
|
next,
|
|
75
|
+
plainTranslate,
|
|
69
76
|
coalesceKey,
|
|
70
77
|
),
|
|
71
78
|
[commitPositionPatchToHtml, readOnlyPreview, showToast],
|
|
@@ -98,56 +105,41 @@ export function useDomGeometryCommits({
|
|
|
98
105
|
selection: DomEditSelection,
|
|
99
106
|
next: { width: number; height: number },
|
|
100
107
|
offset?: { x: number; y: number },
|
|
108
|
+
restore?: () => void,
|
|
101
109
|
) => {
|
|
102
110
|
if (readOnlyPreview) return Promise.resolve();
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
applyStudioBoxSize(selection.element, next);
|
|
111
|
+
const element = selection.element;
|
|
112
|
+
const beforeSize = captureStudioBoxSize(element);
|
|
113
|
+
const beforeTranslate = element.style.getPropertyValue("translate");
|
|
114
|
+
const stageCrop = prepareCropResize(element);
|
|
115
|
+
applyStudioBoxSize(element, next);
|
|
109
116
|
const crop = stageCrop();
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
// intermediate re-stamp where the new size is in source but the anchor
|
|
114
|
-
// offset is not (that frame was the release "jump"). Both builders read
|
|
115
|
-
// the already-mutated live element, so concatenation is safe.
|
|
116
|
-
const patches = buildBoxSizePatches(selection.element);
|
|
117
|
+
// One commit, one undo entry: the size, the crop that follows it, and the translate
|
|
118
|
+
// (as a move writes it) that keeps the centre planted.
|
|
119
|
+
const patches = buildBoxSizePatches(element);
|
|
117
120
|
if (crop) patches.push(crop.patch);
|
|
118
121
|
if (offset) {
|
|
119
|
-
|
|
120
|
-
patches.push(
|
|
122
|
+
writeTranslatePx(element, offset);
|
|
123
|
+
patches.push(translatePatch(offset));
|
|
121
124
|
}
|
|
122
125
|
return commitPositionPatchToHtml(selection, patches, {
|
|
123
126
|
label: "Resize layer box",
|
|
124
127
|
coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
|
|
125
128
|
}).catch((error) => {
|
|
126
|
-
restoreStudioBoxSize(
|
|
127
|
-
if (
|
|
129
|
+
restoreStudioBoxSize(element, beforeSize);
|
|
130
|
+
if (offset) element.style.setProperty("translate", beforeTranslate);
|
|
128
131
|
crop?.revert();
|
|
132
|
+
restore?.();
|
|
129
133
|
throw error;
|
|
130
134
|
});
|
|
131
135
|
},
|
|
132
|
-
[commitPositionPatchToHtml,
|
|
136
|
+
[commitPositionPatchToHtml, readOnlyPreview],
|
|
133
137
|
);
|
|
134
138
|
|
|
135
139
|
const handleDomRotationCommit = useCallback(
|
|
136
|
-
(selection: DomEditSelection, next:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (gsapFallback) return gsapFallback;
|
|
140
|
-
const before = captureStudioRotation(selection.element);
|
|
141
|
-
applyStudioRotation(selection.element, next);
|
|
142
|
-
return commitPositionPatchToHtml(selection, buildRotationPatches(selection.element), {
|
|
143
|
-
label: "Rotate layer",
|
|
144
|
-
coalesceKey: `rotation:${getDomEditTargetKey(selection)}`,
|
|
145
|
-
}).catch((error) => {
|
|
146
|
-
restoreStudioRotation(selection.element, before);
|
|
147
|
-
throw error;
|
|
148
|
-
});
|
|
149
|
-
},
|
|
150
|
-
[commitPositionPatchToHtml, previewIframeRef, showToast, readOnlyPreview],
|
|
140
|
+
(selection: DomEditSelection, next: RotationCommit) =>
|
|
141
|
+
savePlainRotation({ commitPositionPatchToHtml, readOnlyPreview }, selection, next),
|
|
142
|
+
[commitPositionPatchToHtml, readOnlyPreview],
|
|
151
143
|
);
|
|
152
144
|
|
|
153
145
|
const handleDomManualEditsReset = useCallback(
|
|
@@ -4,6 +4,7 @@ import React, { act } from "react";
|
|
|
4
4
|
import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
5
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
6
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
|
+
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
7
8
|
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
8
9
|
import { mountReactHarness, withInlineLayoutBox } from "./domSelectionTestHarness";
|
|
9
10
|
import { DomEditCropHandles } from "../components/editor/DomEditCropHandles";
|
|
@@ -41,6 +42,7 @@ vi.mock("./useSafeGsapCommitMutation", () => ({
|
|
|
41
42
|
}));
|
|
42
43
|
|
|
43
44
|
import { useGsapAwareEditing } from "./useGsapAwareEditing";
|
|
45
|
+
import { tryGsapRotationIntercept } from "./gsapRuntimeBridge";
|
|
44
46
|
|
|
45
47
|
afterEach(() => {
|
|
46
48
|
vi.clearAllMocks();
|
|
@@ -54,8 +56,10 @@ function gsapPositioned(tag: string): HTMLElement {
|
|
|
54
56
|
function mountResizeHandler(
|
|
55
57
|
animations: GsapAnimation[],
|
|
56
58
|
targetAnimations: GsapAnimation[] = animations,
|
|
59
|
+
gsapOwnsBox = true,
|
|
60
|
+
hasGsapWriter = true,
|
|
57
61
|
) {
|
|
58
|
-
const element = document.createElement("div");
|
|
62
|
+
const element = gsapOwnsBox ? gsapPositioned("div") : document.createElement("div");
|
|
59
63
|
const selection = { element, id: "clip", selector: "#clip" } as unknown as DomEditSelection;
|
|
60
64
|
const fallback = vi.fn().mockResolvedValue(undefined);
|
|
61
65
|
const anchorSave = vi.fn().mockResolvedValue(undefined);
|
|
@@ -63,6 +67,7 @@ function mountResizeHandler(
|
|
|
63
67
|
const elementOffset = vi.fn(() => ({ save: anchorSave, rollback: anchorRollback }));
|
|
64
68
|
const commitMutation = vi.fn().mockResolvedValue(undefined);
|
|
65
69
|
const commitPatch = vi.fn().mockResolvedValue(undefined);
|
|
70
|
+
const fetchAnimations = vi.fn(() => vi.fn().mockResolvedValue(targetAnimations));
|
|
66
71
|
let resize:
|
|
67
72
|
| ((
|
|
68
73
|
selection: DomEditSelection,
|
|
@@ -76,14 +81,15 @@ function mountResizeHandler(
|
|
|
76
81
|
const editing = useGsapAwareEditing({
|
|
77
82
|
domEditSelection: selection,
|
|
78
83
|
selectedGsapAnimations: animations,
|
|
79
|
-
gsapCommitMutation: commitMutation,
|
|
84
|
+
gsapCommitMutation: hasGsapWriter ? commitMutation : null,
|
|
80
85
|
previewIframeRef: { current: null },
|
|
81
86
|
showToast: vi.fn(),
|
|
82
87
|
bumpGsapCache: vi.fn(),
|
|
83
|
-
makeFetchFallback:
|
|
88
|
+
makeFetchFallback: fetchAnimations,
|
|
84
89
|
trackGsapInteractionFailure: vi.fn(),
|
|
85
90
|
stageElementPositionOffset: elementOffset,
|
|
86
91
|
handleDomBoxSizeCommit: fallback,
|
|
92
|
+
handleDomRotationCommit: vi.fn(),
|
|
87
93
|
commitPositionPatchToHtml: commitPatch,
|
|
88
94
|
addGsapAnimation: vi.fn(),
|
|
89
95
|
convertToKeyframes: vi.fn(),
|
|
@@ -103,6 +109,7 @@ function mountResizeHandler(
|
|
|
103
109
|
anchorRollback,
|
|
104
110
|
commitMutation,
|
|
105
111
|
commitPatch,
|
|
112
|
+
fetchAnimations,
|
|
106
113
|
resize: resize!,
|
|
107
114
|
property: property!,
|
|
108
115
|
root,
|
|
@@ -116,13 +123,21 @@ function mountGroupHandler({
|
|
|
116
123
|
makeFetchFallback,
|
|
117
124
|
trackGsapInteractionFailure = vi.fn(),
|
|
118
125
|
stageElementPositionOffset = vi.fn(),
|
|
126
|
+
handleDomRotationCommit = vi.fn(),
|
|
119
127
|
}: Pick<AwareEditingParams, "gsapCommitMutation" | "makeFetchFallback"> &
|
|
120
|
-
Partial<
|
|
128
|
+
Partial<
|
|
129
|
+
Pick<
|
|
130
|
+
AwareEditingParams,
|
|
131
|
+
"trackGsapInteractionFailure" | "stageElementPositionOffset" | "handleDomRotationCommit"
|
|
132
|
+
>
|
|
133
|
+
>) {
|
|
121
134
|
let groupCommit!: (updates: DomEditGroupPathOffsetCommit[]) => Promise<void>;
|
|
122
135
|
let pathOffsetCommit!: (
|
|
123
136
|
selection: DomEditSelection,
|
|
124
137
|
next: { x: number; y: number },
|
|
138
|
+
route?: { plainTranslate?: boolean },
|
|
125
139
|
) => Promise<void>;
|
|
140
|
+
let rotationCommit!: (selection: DomEditSelection, next: RotationCommit) => Promise<void>;
|
|
126
141
|
function Harness() {
|
|
127
142
|
const editing = useGsapAwareEditing({
|
|
128
143
|
domEditSelection: null,
|
|
@@ -135,6 +150,7 @@ function mountGroupHandler({
|
|
|
135
150
|
trackGsapInteractionFailure,
|
|
136
151
|
stageElementPositionOffset,
|
|
137
152
|
handleDomBoxSizeCommit: vi.fn(),
|
|
153
|
+
handleDomRotationCommit,
|
|
138
154
|
commitPositionPatchToHtml: vi.fn(),
|
|
139
155
|
addGsapAnimation: vi.fn(),
|
|
140
156
|
convertToKeyframes: vi.fn(),
|
|
@@ -143,13 +159,19 @@ function mountGroupHandler({
|
|
|
143
159
|
});
|
|
144
160
|
groupCommit = editing.handleGsapAwareGroupPathOffsetCommit;
|
|
145
161
|
pathOffsetCommit = editing.handleGsapAwarePathOffsetCommit;
|
|
162
|
+
rotationCommit = editing.handleGsapAwareRotationCommit;
|
|
146
163
|
return null;
|
|
147
164
|
}
|
|
148
165
|
const root = mountReactHarness(<Harness />);
|
|
149
166
|
return {
|
|
150
167
|
groupCommit: (updates: DomEditGroupPathOffsetCommit[]) => groupCommit(updates),
|
|
151
|
-
pathOffsetCommit: (
|
|
152
|
-
|
|
168
|
+
pathOffsetCommit: (
|
|
169
|
+
selection: DomEditSelection,
|
|
170
|
+
next: { x: number; y: number },
|
|
171
|
+
route?: { plainTranslate?: boolean },
|
|
172
|
+
) => pathOffsetCommit(selection, next, route),
|
|
173
|
+
rotationCommit: (selection: DomEditSelection, next: RotationCommit) =>
|
|
174
|
+
rotationCommit(selection, next),
|
|
153
175
|
root,
|
|
154
176
|
};
|
|
155
177
|
}
|
|
@@ -167,7 +189,7 @@ describe("useGsapAwareEditing moves of an element GSAP does not position", () =>
|
|
|
167
189
|
});
|
|
168
190
|
const box = { element: document.createElement("div"), id: "box", selector: "#box" };
|
|
169
191
|
await act(() => pathOffsetCommit(box as unknown as DomEditSelection, { x: 130.25, y: 90 }));
|
|
170
|
-
expect(stageElementPositionOffset).toHaveBeenCalledWith(box, { x: 130.25, y: 90 });
|
|
192
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(box, { x: 130.25, y: 90 }, true);
|
|
171
193
|
expect(save).toHaveBeenCalledTimes(1);
|
|
172
194
|
expect(gsapCommitMutation).not.toHaveBeenCalled();
|
|
173
195
|
expect(makeFetchFallback).not.toHaveBeenCalled();
|
|
@@ -176,6 +198,127 @@ describe("useGsapAwareEditing moves of an element GSAP does not position", () =>
|
|
|
176
198
|
});
|
|
177
199
|
});
|
|
178
200
|
|
|
201
|
+
describe("useGsapAwareEditing keeps the route a gesture chose at press", () => {
|
|
202
|
+
it.each([
|
|
203
|
+
["the CSS route, on an element GSAP has since taken over", true, () => gsapPositioned("div")],
|
|
204
|
+
[
|
|
205
|
+
"the GSAP route, on an element that now looks GSAP-free",
|
|
206
|
+
false,
|
|
207
|
+
() => document.createElement("div"),
|
|
208
|
+
],
|
|
209
|
+
])("%s", async (_, plainTranslate, makeElement) => {
|
|
210
|
+
mocks.drag.mockResolvedValue({ status: "persisted" });
|
|
211
|
+
const save = vi.fn().mockResolvedValue(undefined);
|
|
212
|
+
const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
|
|
213
|
+
const { pathOffsetCommit, groupCommit, root } = mountGroupHandler({
|
|
214
|
+
gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
|
|
215
|
+
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
216
|
+
stageElementPositionOffset,
|
|
217
|
+
});
|
|
218
|
+
const box = {
|
|
219
|
+
element: makeElement(),
|
|
220
|
+
id: "box",
|
|
221
|
+
selector: "#box",
|
|
222
|
+
} as unknown as DomEditSelection;
|
|
223
|
+
await act(() => pathOffsetCommit(box, { x: 40, y: 20 }, { plainTranslate }));
|
|
224
|
+
await act(() => groupCommit([{ selection: box, next: { x: 40, y: 20 }, plainTranslate }]));
|
|
225
|
+
expect(stageElementPositionOffset.mock.calls.length).toBe(plainTranslate ? 2 : 0);
|
|
226
|
+
expect(mocks.drag.mock.calls.some((call) => call[0] === box)).toBe(!plainTranslate);
|
|
227
|
+
act(() => root.unmount());
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("useGsapAwareEditing refuses a group GSAP took over before writing any member", () => {
|
|
232
|
+
it("writes no member when one CSS-route member has been folded since the press", async () => {
|
|
233
|
+
const stageElementPositionOffset = vi.fn(() => ({ save: vi.fn(), rollback: vi.fn() }));
|
|
234
|
+
const { groupCommit, root } = mountGroupHandler({
|
|
235
|
+
gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
|
|
236
|
+
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
237
|
+
stageElementPositionOffset,
|
|
238
|
+
});
|
|
239
|
+
const card = {
|
|
240
|
+
element: document.createElement("div"),
|
|
241
|
+
id: "card",
|
|
242
|
+
} as unknown as DomEditSelection;
|
|
243
|
+
const folded = Object.assign(document.createElement("div"), {
|
|
244
|
+
_gsap: { renderTransform: () => {}, x: "94px", y: "66px" },
|
|
245
|
+
});
|
|
246
|
+
const box = { element: folded, id: "box" } as unknown as DomEditSelection;
|
|
247
|
+
await expect(
|
|
248
|
+
act(() =>
|
|
249
|
+
groupCommit([
|
|
250
|
+
{ selection: card, next: { x: 1, y: 2 }, plainTranslate: true },
|
|
251
|
+
{ selection: box, next: { x: 1, y: 2 }, plainTranslate: true },
|
|
252
|
+
]),
|
|
253
|
+
),
|
|
254
|
+
).rejects.toThrow(/animation took over/);
|
|
255
|
+
expect(stageElementPositionOffset).not.toHaveBeenCalled();
|
|
256
|
+
act(() => root.unmount());
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("useGsapAwareEditing rotation routing", () => {
|
|
261
|
+
function rotate(element: HTMLElement) {
|
|
262
|
+
const handleDomRotationCommit = vi.fn().mockResolvedValue(undefined);
|
|
263
|
+
const gsapCommitMutation = vi.fn();
|
|
264
|
+
const h = mountGroupHandler({
|
|
265
|
+
gsapCommitMutation,
|
|
266
|
+
makeFetchFallback: () => vi.fn().mockResolvedValue([]),
|
|
267
|
+
handleDomRotationCommit,
|
|
268
|
+
});
|
|
269
|
+
const box = { element, id: "box", selector: "#box" } as unknown as DomEditSelection;
|
|
270
|
+
return { ...h, box, handleDomRotationCommit, gsapCommitMutation };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
it("turns an element GSAP does not turn by the CSS writer, with no GSAP write", async () => {
|
|
274
|
+
const h = rotate(document.createElement("div"));
|
|
275
|
+
await act(() => h.rotationCommit(h.box, { angle: 55 }));
|
|
276
|
+
expect(h.handleDomRotationCommit).toHaveBeenCalledWith(h.box, { angle: 55 });
|
|
277
|
+
expect(tryGsapRotationIntercept).not.toHaveBeenCalled();
|
|
278
|
+
expect(h.gsapCommitMutation).not.toHaveBeenCalled();
|
|
279
|
+
act(() => h.root.unmount());
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("leaves an element a GSAP tween turns to the GSAP rotation route", async () => {
|
|
283
|
+
vi.mocked(tryGsapRotationIntercept).mockResolvedValue({ status: "persisted" });
|
|
284
|
+
const element = document.createElement("div");
|
|
285
|
+
const h = rotate(element);
|
|
286
|
+
const tween = { vars: { rotation: 30 }, targets: () => [element] };
|
|
287
|
+
Object.assign(window, { __timelines: { main: { getChildren: () => [tween] } } });
|
|
288
|
+
try {
|
|
289
|
+
await act(() => h.rotationCommit(h.box, { angle: 55 }));
|
|
290
|
+
} finally {
|
|
291
|
+
delete (window as { __timelines?: unknown }).__timelines;
|
|
292
|
+
}
|
|
293
|
+
expect(tryGsapRotationIntercept).toHaveBeenCalledTimes(1);
|
|
294
|
+
expect(h.handleDomRotationCommit).not.toHaveBeenCalled();
|
|
295
|
+
act(() => h.root.unmount());
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("saves a turn the press drew as CSS by the CSS writer, even once a GSAP tween turns the element", async () => {
|
|
299
|
+
const element = document.createElement("div");
|
|
300
|
+
const h = rotate(element);
|
|
301
|
+
const tween = { vars: { rotation: 30 }, targets: () => [element] };
|
|
302
|
+
Object.assign(window, { __timelines: { main: { getChildren: () => [tween] } } });
|
|
303
|
+
const plain = {
|
|
304
|
+
property: "rotate",
|
|
305
|
+
before: "",
|
|
306
|
+
after: "",
|
|
307
|
+
share: 0,
|
|
308
|
+
sign: 1,
|
|
309
|
+
inline: false,
|
|
310
|
+
} as const;
|
|
311
|
+
try {
|
|
312
|
+
await act(() => h.rotationCommit(h.box, { angle: 55, plain }));
|
|
313
|
+
} finally {
|
|
314
|
+
delete (window as { __timelines?: unknown }).__timelines;
|
|
315
|
+
}
|
|
316
|
+
expect(h.handleDomRotationCommit).toHaveBeenCalledWith(h.box, { angle: 55, plain });
|
|
317
|
+
expect(tryGsapRotationIntercept).not.toHaveBeenCalled();
|
|
318
|
+
act(() => h.root.unmount());
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
179
322
|
describe("useGsapAwareEditing shared-tween moves", () => {
|
|
180
323
|
it("saves a single drag through the element's own offset", async () => {
|
|
181
324
|
mocks.drag.mockResolvedValue({ status: "element-offset" });
|
|
@@ -189,7 +332,7 @@ describe("useGsapAwareEditing shared-tween moves", () => {
|
|
|
189
332
|
});
|
|
190
333
|
const word = { element: gsapPositioned("span"), hfId: "w0", selector: ".w" };
|
|
191
334
|
await act(() => pathOffsetCommit(word as unknown as DomEditSelection, { x: 40, y: 20 }));
|
|
192
|
-
expect(stageElementPositionOffset).toHaveBeenCalledWith(word, { x: 40, y: 20 });
|
|
335
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(word, { x: 40, y: 20 }, false);
|
|
193
336
|
expect(save).toHaveBeenCalledTimes(1);
|
|
194
337
|
expect(commitMutation).not.toHaveBeenCalled();
|
|
195
338
|
act(() => root.unmount());
|
|
@@ -219,8 +362,18 @@ describe("useGsapAwareEditing shared-tween moves", () => {
|
|
|
219
362
|
] as unknown as DomEditGroupPathOffsetCommit[]),
|
|
220
363
|
);
|
|
221
364
|
const groupKey = expect.stringMatching(/^group-drag:\d+$/);
|
|
222
|
-
expect(stageElementPositionOffset).toHaveBeenCalledWith(
|
|
223
|
-
|
|
365
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(
|
|
366
|
+
word,
|
|
367
|
+
{ x: 40, y: 20 },
|
|
368
|
+
false,
|
|
369
|
+
groupKey,
|
|
370
|
+
);
|
|
371
|
+
expect(stageElementPositionOffset).toHaveBeenCalledWith(
|
|
372
|
+
plain,
|
|
373
|
+
{ x: 40, y: 20 },
|
|
374
|
+
true,
|
|
375
|
+
groupKey,
|
|
376
|
+
);
|
|
224
377
|
expect(mocks.drag.mock.calls.some((call) => call[0] === plain)).toBe(false);
|
|
225
378
|
act(() => root.unmount());
|
|
226
379
|
});
|
|
@@ -252,6 +405,36 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
252
405
|
act(() => h.root.unmount());
|
|
253
406
|
});
|
|
254
407
|
|
|
408
|
+
it("resizes a box GSAP does not own through the CSS writer, with no GSAP write or fetch", async () => {
|
|
409
|
+
const h = mountResizeHandler([], [], false);
|
|
410
|
+
const restore = vi.fn();
|
|
411
|
+
await act(() =>
|
|
412
|
+
h.resize(h.selection, { width: 300, height: 200 }, { x: -50.5, y: -25 }, restore),
|
|
413
|
+
);
|
|
414
|
+
expect(h.fallback).toHaveBeenCalledWith(
|
|
415
|
+
h.selection,
|
|
416
|
+
{ width: 300, height: 200 },
|
|
417
|
+
{ x: -50.5, y: -25 },
|
|
418
|
+
restore,
|
|
419
|
+
);
|
|
420
|
+
expect(mocks.resize).not.toHaveBeenCalled();
|
|
421
|
+
expect(mocks.drag).not.toHaveBeenCalled();
|
|
422
|
+
expect(h.commitMutation).not.toHaveBeenCalled();
|
|
423
|
+
expect(h.fetchAnimations).not.toHaveBeenCalled();
|
|
424
|
+
act(() => h.root.unmount());
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("fails a GSAP-owned resize loudly when there is no GSAP writer, never writing CSS", async () => {
|
|
428
|
+
const h = mountResizeHandler([], [], true, false);
|
|
429
|
+
const restore = vi.fn();
|
|
430
|
+
await expect(
|
|
431
|
+
act(() => h.resize(h.selection, { width: 300, height: 200 }, undefined, restore)),
|
|
432
|
+
).rejects.toThrow("no GSAP writer");
|
|
433
|
+
expect(h.fallback).not.toHaveBeenCalled();
|
|
434
|
+
expect(restore).toHaveBeenCalledTimes(1);
|
|
435
|
+
act(() => h.root.unmount());
|
|
436
|
+
});
|
|
437
|
+
|
|
255
438
|
it("rejects a blocked resize instead of falling through to a competing DOM write", async () => {
|
|
256
439
|
mocks.resize.mockResolvedValue({ status: "blocked", reason: "source-uneditable" });
|
|
257
440
|
const h = mountResizeHandler([]);
|
|
@@ -287,7 +470,7 @@ describe("useGsapAwareEditing anchored resize", () => {
|
|
|
287
470
|
await act(() => h.resize(h.selection, { width: 300, height: 200 }, { x: -50, y: -25 }));
|
|
288
471
|
const key = h.commitMutation.mock.calls[0]![2].coalesceKey;
|
|
289
472
|
expect(key).toMatch(/^tx:/);
|
|
290
|
-
expect(h.elementOffset).toHaveBeenCalledWith(h.selection, { x: -50, y: -25 }, key);
|
|
473
|
+
expect(h.elementOffset).toHaveBeenCalledWith(h.selection, { x: -50, y: -25 }, false, key);
|
|
291
474
|
expect(h.anchorSave.mock.invocationCallOrder[0]).toBeGreaterThan(
|
|
292
475
|
h.commitMutation.mock.invocationCallOrder[0]!,
|
|
293
476
|
);
|