Skip to content

Commit cb75cc3

Browse files
committed
fix(studio): an inline element's turn saves the inline-block it was drawn with
The overlay passes the rotate target it read at press into the commit, so the save draws and patches exactly what the draft did instead of re-reading an element the draft already made inline-block. The GSAP-aware commit also routes on that press-time decision.
1 parent 818a1d4 commit cb75cc3

9 files changed

Lines changed: 119 additions & 15 deletions

‎packages/studio/src/components/editor/DomEditOverlay.tsx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { RotationCommit } from "./rotationDraft";
12
import { memo, useEffect, useMemo, useRef, type RefObject } from "react";
23
import { type DomEditSelection } from "./domEditing";
34
import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction";
@@ -90,10 +91,7 @@ export interface DomEditOverlayProps {
9091
offset?: { x: number; y: number },
9192
restore?: () => void,
9293
) => Promise<unknown> | void;
93-
onRotationCommit: (
94-
selection: DomEditSelection,
95-
next: { angle: number },
96-
) => Promise<unknown> | void;
94+
onRotationCommit: (selection: DomEditSelection, next: RotationCommit) => Promise<unknown> | void;
9795
onStyleCommit?: (property: string, value: string) => Promise<unknown> | void;
9896
recordingState?: GestureRecordingState;
9997
onToggleRecording?: () => void;

‎packages/studio/src/components/editor/domEditOverlayGestures.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
StudioRotationSnapshot,
77
} from "./manualEdits";
88
import type { ManualOffsetDragMember } from "./manualOffsetDrag";
9-
import type { CssRotationTarget } from "./rotationDraft";
9+
import type { CssRotationTarget, RotationCommit } from "./rotationDraft";
1010
import type { GroupOverlayItem, OverlayRect } from "./domEditOverlayGeometry";
1111
import type { SnapContext } from "./snapTargetCollection";
1212
import type { SnapGuidesState } from "./SnapGuideOverlay";
@@ -262,7 +262,7 @@ export type UseDomEditOverlayGesturesOptions = {
262262
) => Promise<unknown> | void
263263
>;
264264
onRotationCommitRef: RefObject<
265-
(s: DomEditSelection, n: { angle: number }) => Promise<unknown> | void
265+
(s: DomEditSelection, n: RotationCommit) => Promise<unknown> | void
266266
>;
267267
onCanvasPointerMoveRef: RefObject<
268268
(
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// @vitest-environment happy-dom
2+
3+
import { afterEach, expect, it, vi } from "vitest";
4+
import { savePlainRotation } from "../../hooks/plainRotation";
5+
import type { ElementOffsetStagerDeps } from "../../hooks/elementOffsetStager";
6+
import type { DomEditSelection } from "./domEditing";
7+
import type { GestureState, UseDomEditOverlayGesturesOptions } from "./domEditOverlayGestures";
8+
import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
9+
10+
const ref = <T>(current: T) => ({ current });
11+
12+
function evt(clientX: number, clientY: number) {
13+
return {
14+
clientX,
15+
clientY,
16+
pointerId: 1,
17+
button: 0,
18+
altKey: false,
19+
shiftKey: false,
20+
preventDefault() {},
21+
stopPropagation() {},
22+
currentTarget: { setPointerCapture() {} },
23+
} as unknown as React.PointerEvent<HTMLDivElement>;
24+
}
25+
26+
afterEach(() => {
27+
document.head.innerHTML = "";
28+
document.body.innerHTML = "";
29+
});
30+
31+
it("saves an inline span's turn with the inline-block it was drawn with, after the draft and the hold", async () => {
32+
const sheet = document.head.appendChild(document.createElement("style"));
33+
sheet.textContent = "#word { display: inline; }";
34+
const element = document.body.appendChild(document.createElement("span"));
35+
element.id = "word";
36+
element.textContent = "Word";
37+
const selection = {
38+
element,
39+
id: "word",
40+
selector: "#word",
41+
selectorIndex: 0,
42+
sourceFile: "index.html",
43+
tagName: "span",
44+
label: "Word",
45+
textContent: "Word",
46+
textFields: [],
47+
capabilities: { canMove: true, canApplyManualRotation: true },
48+
} as unknown as DomEditSelection;
49+
const commitPositionPatchToHtml = vi.fn<ElementOffsetStagerDeps["commitPositionPatchToHtml"]>(
50+
() => Promise.resolve(),
51+
);
52+
const saves: Promise<void>[] = [];
53+
const opts: UseDomEditOverlayGesturesOptions = {
54+
overlayRef: ref<HTMLDivElement | null>(document.createElement("div")),
55+
iframeRef: ref<HTMLIFrameElement | null>(document.createElement("iframe")),
56+
boxRef: ref<HTMLDivElement | null>(document.createElement("div")),
57+
selectionRef: ref<DomEditSelection | null>(selection),
58+
hoverSelectionRef: ref<DomEditSelection | null>(null),
59+
overlayRectRef: ref({ left: 0, top: 0, width: 200, height: 100, editScaleX: 1, editScaleY: 1 }),
60+
groupOverlayItemsRef: ref([]),
61+
gestureRef: ref<GestureState | null>(null),
62+
groupGestureRef: ref(null),
63+
blockedMoveRef: ref(null),
64+
rafPausedRef: ref(false),
65+
suppressNextBoxClickRef: ref(false),
66+
setOverlayRect: () => {},
67+
setGroupOverlayItems: () => {},
68+
onBlockedMoveRef: ref(() => {}),
69+
onManualDragStartRef: ref(() => {}),
70+
onPathOffsetCommitRef: ref(() => {}),
71+
onGroupPathOffsetCommitRef: ref(() => {}),
72+
onBoxSizeCommitRef: ref(() => {}),
73+
onRotationCommitRef: ref((s, next) => {
74+
const saved = savePlainRotation({ commitPositionPatchToHtml }, s, next);
75+
saves.push(saved);
76+
return saved;
77+
}),
78+
onCanvasPointerMoveRef: ref(() => Promise.resolve(null)),
79+
onCanvasMouseDown: () => {},
80+
snapGuidesRef: ref(null),
81+
} as unknown as UseDomEditOverlayGesturesOptions;
82+
const handlers = createDomEditOverlayGestureHandlers(opts);
83+
84+
handlers.startGesture("rotate", evt(200, 50));
85+
handlers.onPointerMove(evt(100, 150));
86+
handlers.onPointerUp(evt(100, 150));
87+
await Promise.all(saves);
88+
89+
expect(saves).toHaveLength(1);
90+
expect(commitPositionPatchToHtml.mock.calls[0]![1]).toContainEqual({
91+
type: "inline-style",
92+
property: "display",
93+
value: "inline-block",
94+
});
95+
});

‎packages/studio/src/components/editor/rotationDraft.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export interface CssRotationTarget {
5151
inline: boolean;
5252
}
5353

54+
export type RotationCommit = { angle: number; plain?: CssRotationTarget };
55+
5456
const TRANSLATE = /^translate(?:3d|X|Y|Z)?\(/i;
5557
const OWN_TURN = /^rotate\(\s*-?[\d.]+(?:e[+-]?\d+)?deg\s*\)$/i;
5658

‎packages/studio/src/components/editor/useDomEditOverlayGestures.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,8 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
430430
}
431431
// Hold the final angle while the commit lands.
432432
applyRotationDraft(sel.element, finalRotation.angle, g.plainRotation);
433-
void Promise.resolve(opts.onRotationCommitRef.current(sel, finalRotation))
433+
const commit = g.plainRotation ? { ...finalRotation, plain: g.plainRotation } : finalRotation;
434+
void Promise.resolve(opts.onRotationCommitRef.current(sel, commit))
434435
.catch((error) => {
435436
logGestureCommitFailure("rotate commit failed", error);
436437
if (

‎packages/studio/src/hooks/plainRotation.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import type { DomEditSelection } from "../components/editor/domEditing";
22
import { captureStudioRotation, clearStudioRotation } from "../components/editor/manualEdits";
33
import { buildClearRotationPatches } from "../components/editor/manualEditsDomPatches";
44
import { STUDIO_ROTATION_ATTR } from "../components/editor/manualEditsTypes";
5-
import { applyCssRotation, restorePlainRotation } from "../components/editor/rotationDraft";
5+
import {
6+
applyCssRotation,
7+
restorePlainRotation,
8+
type RotationCommit,
9+
} from "../components/editor/rotationDraft";
610
import type { PatchOperation } from "../utils/sourcePatcher";
711
import type { ElementOffsetStagerDeps } from "./elementOffsetStager";
812

@@ -12,7 +16,7 @@ let plainRotateCounter = 0;
1216
export function savePlainRotation(
1317
{ commitPositionPatchToHtml, readOnlyPreview }: Omit<ElementOffsetStagerDeps, "showToast">,
1418
selection: DomEditSelection,
15-
next: { angle: number },
19+
next: RotationCommit,
1620
): Promise<void> {
1721
if (readOnlyPreview) return Promise.resolve();
1822
const { element } = selection;
@@ -22,7 +26,7 @@ export function savePlainRotation(
2226
? buildClearRotationPatches(element)
2327
: [];
2428
if (patches.length) clearStudioRotation(element);
25-
const drawn = applyCssRotation(element, next.angle);
29+
const drawn = applyCssRotation(element, next.angle, next.plain);
2630
patches.push(...drawn);
2731
const turn = drawn.at(-1)!;
2832
return commitPositionPatchToHtml(selection, patches, {

‎packages/studio/src/hooks/useDomGeometryCommit.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { RotationCommit } from "../components/editor/rotationDraft";
12
import { useCallback, useMemo, useRef, useState } from "react";
23
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
34
import type { DomEditSelection } from "../components/editor/domEditing";
@@ -40,7 +41,7 @@ export interface DomGeometryCommits {
4041
) => Promise<DomEditCommitOutcome>;
4142
commitRotation: (
4243
selection: DomEditSelection,
43-
next: { angle: number },
44+
next: RotationCommit,
4445
) => Promise<DomEditCommitOutcome>;
4546
waitForPendingSaves: () => Promise<void>;
4647
}

‎packages/studio/src/hooks/useDomGeometryCommits.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { RotationCommit } from "../components/editor/rotationDraft";
12
import { useCallback } from "react";
23
import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
34
import {
@@ -130,7 +131,7 @@ export function useDomGeometryCommits({
130131
);
131132

132133
const handleDomRotationCommit = useCallback(
133-
(selection: DomEditSelection, next: { angle: number }) =>
134+
(selection: DomEditSelection, next: RotationCommit) =>
134135
savePlainRotation({ commitPositionPatchToHtml, readOnlyPreview }, selection, next),
135136
[commitPositionPatchToHtml, readOnlyPreview],
136137
);

‎packages/studio/src/hooks/useGsapAwareEditing.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* Extracted from useDomEditSession to isolate the GSAP intercept routing
88
* from the rest of the editing orchestration.
99
*/
10+
import type { RotationCommit } from "../components/editor/rotationDraft";
1011
import { useCallback } from "react";
1112
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
1213
import type { DomEditSelection } from "../components/editor/domEditingTypes";
@@ -84,7 +85,7 @@ export interface UseGsapAwareEditingParams {
8485
next: { x: number; y: number },
8586
coalesceKey?: string,
8687
) => { save: () => Promise<void>; rollback: () => void };
87-
handleDomRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise<void>;
88+
handleDomRotationCommit: (selection: DomEditSelection, next: RotationCommit) => Promise<void>;
8889
handleDomBoxSizeCommit: (
8990
selection: DomEditSelection,
9091
next: { width: number; height: number },
@@ -457,8 +458,9 @@ export function useGsapAwareEditing({
457458
);
458459

459460
const handleGsapAwareRotationCommit = useCallback(
460-
async (selection: DomEditSelection, next: { angle: number }) => {
461-
if (!gsapWritesRotation(selection.element)) return handleDomRotationCommit(selection, next);
461+
async (selection: DomEditSelection, next: RotationCommit) => {
462+
if (next.plain || !gsapWritesRotation(selection.element))
463+
return handleDomRotationCommit(selection, next);
462464
if (gsapCommitMutation) {
463465
try {
464466
const targetAnimations = await getGsapAnimationsForSelection(selection);

0 commit comments

Comments
 (0)