mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
fix(studio): give the motion path and the fallbacks a one-element target
The narrowing this branch adds missed the motion-path overlay, and every caller that could not narrow fell back to the exact bare class the narrowing exists to replace. - motionPathSelection.selectorFor now goes through writeTargetSelector. It feeds both the geometry read and the "set destination" write, so a class sibling measured its home off the FIRST sibling and then authored add-motion-path onto all of them. The toolbar toggle hides when no one-element form exists rather than arming a press that is dropped. - The five new-tween writers that fell back to the selection's own selector now drop the commit instead. A gesture that does not persist reverts on the next reload; a tween silently aimed at five elements does not. - tweenTargetsElement only follows the DOM to a target that matches exactly one element. A target the element merely shares with its siblings is a group tween, and these callers mutate what they find, so an individual nudge was rewriting the group's own tween and moving all five.
This commit is contained in:
@@ -132,7 +132,10 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
|
||||
const createMode = geometryResolved && !geometry && Boolean(selection?.element) && !isPlaying;
|
||||
const createSelector = createMode ? selectorFor(selection) : null;
|
||||
const compW = compositionSize?.width ?? null;
|
||||
const canCreate = createMode && hasMotionPathPlugin(iframeRef.current);
|
||||
// No one-element selector means the path could only be authored onto the
|
||||
// element's class siblings, so the toolbar toggle stays hidden instead of
|
||||
// arming a press that the effect below would silently drop.
|
||||
const canCreate = createMode && !!createSelector && hasMotionPathPlugin(iframeRef.current);
|
||||
|
||||
// Publish whether the selected element can take a path so the preview toolbar
|
||||
// shows its "set destination" toggle. Drops to false when this overlay unmounts
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { DomEditSelection } from "./domEditingTypes";
|
||||
import { buildStableSelector, getSelectorIndex } from "./domEditingDom";
|
||||
import { selectorFor } from "./motionPathSelection";
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
function selectionFor(el: HTMLElement): DomEditSelection {
|
||||
const selector = buildStableSelector(el);
|
||||
return {
|
||||
element: el,
|
||||
id: el.id || undefined,
|
||||
hfId: el.getAttribute("data-hf-id") || undefined,
|
||||
selector,
|
||||
selectorIndex: getSelectorIndex(document, el, selector, "index.html", null),
|
||||
sourceFile: "index.html",
|
||||
dataAttributes: { start: "0", duration: "2" },
|
||||
} as unknown as DomEditSelection;
|
||||
}
|
||||
|
||||
function mountGroupSiblings(): HTMLElement[] {
|
||||
document.body.innerHTML = `
|
||||
<div id="scene" class="clip" data-start="0" data-duration="2">
|
||||
<div class="group"></div>
|
||||
<div class="group"></div>
|
||||
<div class="group"></div>
|
||||
</div>
|
||||
`;
|
||||
return Array.from(document.querySelectorAll<HTMLElement>(".group"));
|
||||
}
|
||||
|
||||
describe("selectorFor", () => {
|
||||
it("addresses one element for a class-only sibling", () => {
|
||||
const groups = mountGroupSiblings();
|
||||
const selector = selectorFor(selectionFor(groups[2]!));
|
||||
|
||||
// The bare ".group" both measured home off the FIRST sibling and wrote the
|
||||
// new motion path onto all three.
|
||||
expect(selector).not.toBe(".group");
|
||||
expect(document.querySelectorAll(selector!)).toHaveLength(1);
|
||||
expect(document.querySelector(selector!)).toBe(groups[2]);
|
||||
});
|
||||
|
||||
it("keeps a unique id target", () => {
|
||||
document.body.innerHTML = `<div id="hero"></div>`;
|
||||
const el = document.querySelector<HTMLElement>("#hero")!;
|
||||
|
||||
expect(selectorFor(selectionFor(el))).toBe("#hero");
|
||||
});
|
||||
|
||||
it("returns null with no selection", () => {
|
||||
expect(selectorFor(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when no rung addresses one element", () => {
|
||||
const groups = mountGroupSiblings();
|
||||
const selection = selectionFor(groups[1]!);
|
||||
groups[1]!.remove();
|
||||
|
||||
expect(selectorFor(selection)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -5,11 +5,21 @@
|
||||
*/
|
||||
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
||||
import type { DomEditSelection } from "./domEditing";
|
||||
import { writeTargetSelector } from "../../hooks/gsapShared";
|
||||
|
||||
/**
|
||||
* The selector the overlay both MEASURES the element by and authors a new
|
||||
* motion path against.
|
||||
*
|
||||
* Both halves need exactly one element. The selection's own selector is a bare
|
||||
* class for an id-less element, so a `.group` sibling read its home position off
|
||||
* the FIRST sibling (skewing the destination the click computes) and then wrote
|
||||
* `add-motion-path` onto all five. `writeTargetSelector` is the same one-element
|
||||
* narrowing every other new-tween writer goes through; null means no such form
|
||||
* exists, and the overlay hides "set destination" rather than write a wrong one.
|
||||
*/
|
||||
export function selectorFor(sel: DomEditSelection | null): string | null {
|
||||
if (!sel) return null;
|
||||
if (sel.id) return `#${CSS.escape(sel.id)}`;
|
||||
return sel.selector ?? null;
|
||||
return sel ? writeTargetSelector(sel) : null;
|
||||
}
|
||||
|
||||
/** The animation whose path is editable on-canvas: literal, statically resolved,
|
||||
|
||||
Reference in New Issue
Block a user