refactor(studio): resolve tween selector ids through the shared reader

The local extractIdFromSelector duplicated the `#id`-only regex that
idFromSelector replaced, so both DOM-less paths in
resolveSelectorElementIds (no-iframe fallback and querySelectorAll-throw
recovery) read no id at all for the bracketed `[id="..."]` form writers
emit for CSS-unsafe ids. Deleted the duplicate and imported the shared
reader; both forms now resolve.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-27 19:51:51 +02:00
parent b386b55f73
commit 521bba6437
2 changed files with 23 additions and 10 deletions
@@ -90,4 +90,20 @@ describe("resolveSelectorElementIds", () => {
expect(resolveSelectorElementIds("#card .label", null)).toEqual(["card"]);
expect(resolveSelectorElementIds(".dot", null)).toEqual([]);
});
// The `[id="…"]` form is what writers emit for a CSS-unsafe id (digit-leading,
// dotted). The old local `#id`-only regex read no id at all for those, so they
// silently dropped out of both DOM-less paths.
it("falls back to a bracketed id when there is no DOM", () => {
expect(resolveSelectorElementIds('[id="01-hook"] .label', null)).toEqual(["01-hook"]);
});
it("falls back to a bracketed id when querySelectorAll rejects the selector", () => {
const doc = {
querySelectorAll: () => {
throw new SyntaxError("bad selector");
},
} as unknown as Document;
expect(resolveSelectorElementIds('[id="01-hook"]:has(>*)', doc)).toEqual(["01-hook"]);
});
});
+7 -10
View File
@@ -8,25 +8,22 @@ import {
clearKeyframeCacheForFile,
writeGsapAnimationsForElement,
} from "./gsapKeyframeCacheHelpers";
import { toAbsoluteTime, toClipPercentage, toClipKeyframes } from "./gsapShared";
import { idFromSelector, toAbsoluteTime, toClipPercentage, toClipKeyframes } from "./gsapShared";
import {
deduplicateKeyframes,
isStaticPositionHold,
synthesizeFlatTweenKeyframes,
} from "./gsapTweenSynth";
function extractIdFromSelector(selector: string): string | null {
const match = selector.match(/^#([\w-]+)/);
return match ? match[1] : null;
}
/**
* Resolve a tween's target selector to the ids of the element(s) it animates.
* A bare `#id` resolves directly; anything else (a class like `.dot`, a group
* `.a, .b`, or a descendant selector) is matched against the live preview DOM so
* class/selector tweens (e.g. `gsap.from(".dot", {stagger})`) attribute to every
* element they animate — not just one parsed from the string. Falls back to a
* leading `#id` when there's no DOM (so the cache still populates pre-iframe).
* element they animate — not just one parsed from the string. Falls back to the
* leading id when there's no DOM (so the cache still populates pre-iframe);
* `idFromSelector` reads both `#id` and the `[id="…"]` form writers emit for
* CSS-unsafe ids, so those elements resolve pre-iframe too.
*/
// fallow-ignore-next-line complexity
export function resolveSelectorElementIds(
@@ -36,7 +33,7 @@ export function resolveSelectorElementIds(
const bareId = selector.match(/^#([\w-]+)$/);
if (bareId) return [bareId[1]];
if (!doc) {
const lead = extractIdFromSelector(selector);
const lead = idFromSelector(selector);
return lead ? [lead] : [];
}
const ids = new Set<string>();
@@ -48,7 +45,7 @@ export function resolveSelectorElementIds(
if (el.id) ids.add(el.id);
}
} catch {
const lead = extractIdFromSelector(sel);
const lead = idFromSelector(sel);
if (lead) ids.add(lead);
}
}