import { findUnsafeDomPatchValues } from "@hyperframes/core/studio-api/finite-mutation"; import type { DomEditSelection } from "../components/editor/domEditingTypes"; export { PROPERTY_DEFAULTS } from "./gsapShared"; export function ensureElementAddressable(selection: DomEditSelection): { selector: string; autoId?: string; } { if (selection.id) return { selector: `#${selection.id}` }; if (selection.selector) return { selector: selection.selector }; const el = selection.element; const doc = el.ownerDocument; const tag = el.tagName.toLowerCase(); let id = tag; let n = 1; while (doc.getElementById(id)) { n += 1; id = `${tag}-${n}`; } el.setAttribute("id", id); return { selector: `#${id}`, autoId: id }; } export class GsapMutationHttpError extends Error { constructor( readonly statusCode: number, readonly responseBody: unknown, ) { super(formatGsapMutationHttpErrorMessage(statusCode, responseBody)); this.name = "GsapMutationHttpError"; } } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } export function formatFieldsSuffix(rawFields: unknown): string { const fields = Array.isArray(rawFields) ? rawFields.filter((f): f is string => typeof f === "string") : []; return fields.length > 0 ? ` (${fields.join(", ")})` : ""; } export async function readJsonResponseBody(res: Response): Promise { const contentType = res.headers.get("content-type") ?? ""; if (!contentType.includes("application/json")) { return await res.text().catch(() => null); } return await res.json().catch(() => null); } function formatGsapMutationHttpErrorMessage(statusCode: number, body: unknown): string { if (isRecord(body) && typeof body.error === "string") { return body.error; } return `GSAP mutation failed with status ${statusCode}`; } export function formatGsapMutationRejectionToast(error: GsapMutationHttpError): string { const body = error.responseBody; if (isRecord(body)) { return `Couldn't save animation: ${formatGsapMutationHttpErrorMessage( error.statusCode, body, )}${formatFieldsSuffix(body.fields)}`; } return `Couldn't save animation: ${error.message}`; } interface AssignAutoIdParams { projectId: string; targetPath: string; selection: DomEditSelection; autoId: string; showToast?: (message: string, tone?: "error" | "info") => void; } export async function assignGsapTargetAutoIdIfNeeded({ projectId, targetPath, selection, autoId, showToast, }: AssignAutoIdParams): Promise { const patchBody = { target: { id: selection.id, hfId: selection.hfId, selector: selection.selector, selectorIndex: selection.selectorIndex, }, operations: [{ type: "html-attribute", property: "id", value: autoId }], }; const unsafePatchFields = findUnsafeDomPatchValues(patchBody); if (unsafePatchFields.length > 0) { showToast?.("Couldn't assign element id because the patch contains invalid values", "error"); return false; } const res = await fetch( `/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patchBody), }, ); if (!res.ok) { showToast?.( formatGsapMutationRejectionToast( new GsapMutationHttpError(res.status, await readJsonResponseBody(res)), ), "error", ); return false; } const data = (await res.json()) as { changed?: boolean }; return data.changed === true; }