mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D) (#1571)
* feat(sdk): ws-c elastic timing + word-alignment resolver (WS-C) C1: getElementTimings/setElementTiming typed session methods + setHold typed wrapper. getElementTimings reads data-duration (preferred) or data-end−data-start (fallback) — same attr-preference as handleSetTiming. setElementTiming dispatches a sparse map as one batch → one patch event → one undo step. setHold mirrors setVariableValue pattern. Also fixes a pre-existing apply-patches.ts gap: the timing/duration patch case was absent, causing undo of duration changes to silently no-op. Added the duration branch so inverse patches restore data-duration correctly. C2: packages/core/src/compiler/timingResolver.ts — shared pure resolveTimings() consumed by BOTH preview (sdk session) and render (timingCompiler) paths. Word- anchored elements get enterAt = wordTimings[k].start + offset; elastic hold = max(0, slotEnd − (enterAt + enterDuration + exitDuration)), clamped ≥ 0; never timescales animated content. Un-anchored elements keep authored timing (align-on- adjust). Deterministic + pure: no Date.now, no Math.random, no DOM. extractGsapLabels() added to gsapParserAcorn.ts to parse tl.addLabel() calls for the getElementTimings labels field. Tests: timingResolver.test.ts (10 pure-function tests including preview==render parity golden test); session.timings.test.ts (15 session-layer tests covering duration-authored, end-authored, label extraction, batching, undo, and setHold regression). Gates: build ✓ · bun test (sdk+core/compiler) 434/434 ✓ · oxlint 0 warnings ✓ · oxfmt --check ✓ · fallow --gate new-only ✓ (complexity suppressed on 2 new inline functions, duplication warn-only pre-existing) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sdk): addElement forward op — mint hf-id, inverse = removeElement (WS-D) Implements WS-D: the addElement EditOp and session.addElement() typed method. - types.ts: addElement op (parent/index/html) added to EditOp union; addElement(parent, index, html): HfId added to Composition interface - mutate.ts: handleAddElement inserts a single-root HTML fragment at parent+index, minting ids against the LIVE document's existing id set (not a fresh fragment set) via collectDocumentHfIds + mintFragmentIds; forward = patchAdd, inverse = patchRemove; MutationResult.meta.newId carries the minted root id - mutate.ts: validateOp case rejects missing parent, negative index, empty html, zero-element html, and <script> in html - session.ts: typed addElement(parent, index, html) returns minted id via result.meta.newId - mutate.test.ts: 16 tests covering insert position, append semantics, id uniqueness, content-collision rehash, nested fragments, forward/ inverse symmetry, undo, add/undo/redo stability, parent:null body insertion, serialize round-trip, and all five validateOp rejection codes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f65e229663
commit
0ca01c88cf
@@ -356,6 +356,255 @@ describe("removeElement", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── addElement ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe("addElement", () => {
|
||||
it("inserts element at specified parent+index and resolves via getElement-style lookup", () => {
|
||||
const parsed = fresh();
|
||||
const result = applyOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: 0,
|
||||
html: '<p class="new">inserted</p>',
|
||||
});
|
||||
expect(result.meta?.newId).toBeTruthy();
|
||||
const newId = result.meta!.newId!;
|
||||
const el = parsed.document.querySelector(`[data-hf-id="${newId}"]`);
|
||||
expect(el).not.toBeNull();
|
||||
expect(el?.tagName.toLowerCase()).toBe("p");
|
||||
// Inserted at index 0 → first child of hf-stage
|
||||
const stage = parsed.document.querySelector('[data-hf-id="hf-stage"]');
|
||||
expect(stage?.firstElementChild?.getAttribute("data-hf-id")).toBe(newId);
|
||||
});
|
||||
|
||||
it("insert at index >= childCount appends to parent", () => {
|
||||
const parsed = fresh();
|
||||
const stage = parsed.document.querySelector('[data-hf-id="hf-stage"]');
|
||||
const countBefore = stage ? Array.from(stage.children).length : 0;
|
||||
const result = applyOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: 9999,
|
||||
html: '<span class="tail">tail</span>',
|
||||
});
|
||||
const newId = result.meta!.newId!;
|
||||
const stageAfter = parsed.document.querySelector('[data-hf-id="hf-stage"]');
|
||||
expect(stageAfter?.lastElementChild?.getAttribute("data-hf-id")).toBe(newId);
|
||||
expect(Array.from(stageAfter?.children ?? []).length).toBe(countBefore + 1);
|
||||
});
|
||||
|
||||
it("minted id is unique vs all existing doc ids", () => {
|
||||
const parsed = fresh();
|
||||
const result = applyOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: 0,
|
||||
html: '<div class="unique-new">content</div>',
|
||||
});
|
||||
const newId = result.meta!.newId!;
|
||||
// Must not collide with any pre-existing id
|
||||
const existingIds = ["hf-stage", "hf-title", "hf-logo", "hf-sub", "hf-span"];
|
||||
expect(existingIds).not.toContain(newId);
|
||||
// Must appear exactly once in the document
|
||||
const all = Array.from(parsed.document.querySelectorAll(`[data-hf-id="${newId}"]`));
|
||||
expect(all).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("content-collision with existing element yields a distinct rehashed id", () => {
|
||||
// Insert a fragment with identical content to an existing element → dup-rehash must yield a distinct id
|
||||
const parsed = fresh();
|
||||
// hf-logo is <img data-hf-id="hf-logo" src="/logo.png" alt="Logo" />
|
||||
// Insert the same HTML without the data-hf-id so mintHfId runs fresh
|
||||
const result = applyOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: 0,
|
||||
html: '<img src="/logo.png" alt="Logo" />',
|
||||
});
|
||||
const newId = result.meta!.newId!;
|
||||
expect(newId).not.toBe("hf-logo");
|
||||
expect(newId.startsWith("hf-")).toBe(true);
|
||||
const el = parsed.document.querySelector(`[data-hf-id="${newId}"]`);
|
||||
expect(el).not.toBeNull();
|
||||
});
|
||||
|
||||
it("nested fragment: all new nodes get unique ids; root id returned", () => {
|
||||
const parsed = fresh();
|
||||
const result = applyOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: 0,
|
||||
html: '<div class="outer"><span class="inner-a">a</span><span class="inner-b">b</span></div>',
|
||||
});
|
||||
const rootId = result.meta!.newId!;
|
||||
const root = parsed.document.querySelector(`[data-hf-id="${rootId}"]`);
|
||||
expect(root).not.toBeNull();
|
||||
// All children must have data-hf-id
|
||||
const children = root ? Array.from(root.querySelectorAll("*")) : [];
|
||||
for (const child of children) {
|
||||
expect(child.getAttribute("data-hf-id")).toBeTruthy();
|
||||
}
|
||||
// All ids must be distinct
|
||||
const allIds = [rootId, ...children.map((c) => c.getAttribute("data-hf-id") as string)];
|
||||
expect(new Set(allIds).size).toBe(allIds.length);
|
||||
});
|
||||
|
||||
it("forward patch is patchAdd; inverse patch is patchRemove — symmetry with removeElement", () => {
|
||||
const parsed = fresh();
|
||||
const result = applyOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-sub",
|
||||
index: 0,
|
||||
html: '<em class="em">em text</em>',
|
||||
});
|
||||
expect(result.forward).toHaveLength(1);
|
||||
expect(result.inverse).toHaveLength(1);
|
||||
expect(result.forward[0]?.op).toBe("add");
|
||||
expect(result.inverse[0]?.op).toBe("remove");
|
||||
const newId = result.meta!.newId!;
|
||||
expect(result.forward[0]?.path).toBe(`/elements/${newId}`);
|
||||
expect(result.inverse[0]?.path).toBe(`/elements/${newId}`);
|
||||
});
|
||||
|
||||
it("applying inverse patch removes the added element (undo)", () => {
|
||||
const parsed = fresh();
|
||||
const { inverse, meta } = applyOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: 0,
|
||||
html: '<div class="to-undo">undo me</div>',
|
||||
});
|
||||
const newId = meta!.newId!;
|
||||
expect(parsed.document.querySelector(`[data-hf-id="${newId}"]`)).not.toBeNull();
|
||||
applyPatchesToDocument(parsed, inverse);
|
||||
expect(parsed.document.querySelector(`[data-hf-id="${newId}"]`)).toBeNull();
|
||||
});
|
||||
|
||||
it("add → undo → redo: element returns with the same id (id stability)", () => {
|
||||
const parsed = fresh();
|
||||
// add
|
||||
const { forward, inverse, meta } = applyOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: 1,
|
||||
html: '<section class="redo-test">redo</section>',
|
||||
});
|
||||
const newId = meta!.newId!;
|
||||
// undo
|
||||
applyPatchesToDocument(parsed, inverse);
|
||||
expect(parsed.document.querySelector(`[data-hf-id="${newId}"]`)).toBeNull();
|
||||
// redo (replay forward patches)
|
||||
applyPatchesToDocument(parsed, forward);
|
||||
const restored = parsed.document.querySelector(`[data-hf-id="${newId}"]`);
|
||||
expect(restored).not.toBeNull();
|
||||
expect(restored?.getAttribute("data-hf-id")).toBe(newId);
|
||||
});
|
||||
|
||||
it("parent: null inserts at document body root level", () => {
|
||||
// Use a simple fragment doc
|
||||
const parsed = parseMutable(
|
||||
'<div data-hf-id="hf-root" data-hf-root style="width:100px;height:100px"></div>',
|
||||
);
|
||||
const result = applyOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: null,
|
||||
index: 1,
|
||||
html: '<aside class="body-child">aside</aside>',
|
||||
});
|
||||
const newId = result.meta!.newId!;
|
||||
const el = parsed.document.querySelector(`[data-hf-id="${newId}"]`);
|
||||
expect(el).not.toBeNull();
|
||||
expect(el?.parentElement?.tagName.toLowerCase()).toBe("body");
|
||||
});
|
||||
|
||||
it("serialize round-trip: addElement survives serialize()", () => {
|
||||
const parsed = fresh();
|
||||
const result = applyOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-sub",
|
||||
index: 0,
|
||||
html: '<b class="bold">bold</b>',
|
||||
});
|
||||
const newId = result.meta!.newId!;
|
||||
const serialized = serializeDocument(parsed);
|
||||
expect(serialized).toContain(`data-hf-id="${newId}"`);
|
||||
expect(serialized).toContain("bold");
|
||||
});
|
||||
|
||||
// ─── validateOp ─────────────────────────────────────────────────────────────
|
||||
|
||||
it("validateOp: missing parent → E_TARGET_NOT_FOUND", () => {
|
||||
const parsed = fresh();
|
||||
const r = validateOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-nonexistent",
|
||||
index: 0,
|
||||
html: "<div>x</div>",
|
||||
});
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) expect(r.code).toBe("E_TARGET_NOT_FOUND");
|
||||
});
|
||||
|
||||
it("validateOp: negative index → E_INVALID_ARGS", () => {
|
||||
const parsed = fresh();
|
||||
const r = validateOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: -1,
|
||||
html: "<div>x</div>",
|
||||
});
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) expect(r.code).toBe("E_INVALID_ARGS");
|
||||
});
|
||||
|
||||
it("validateOp: empty html → E_INVALID_HTML", () => {
|
||||
const parsed = fresh();
|
||||
const r = validateOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: 0,
|
||||
html: "",
|
||||
});
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) expect(r.code).toBe("E_INVALID_HTML");
|
||||
});
|
||||
|
||||
it("validateOp: html with only text / zero element nodes → E_INVALID_HTML", () => {
|
||||
const parsed = fresh();
|
||||
const r = validateOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: 0,
|
||||
html: "just text no element",
|
||||
});
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) expect(r.code).toBe("E_INVALID_HTML");
|
||||
});
|
||||
|
||||
it("validateOp: html containing <script> → E_INVALID_HTML", () => {
|
||||
const parsed = fresh();
|
||||
const r = validateOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: "hf-stage",
|
||||
index: 0,
|
||||
html: "<div><script>alert(1)</scr" + "ipt></div>",
|
||||
});
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) expect(r.code).toBe("E_INVALID_HTML");
|
||||
});
|
||||
|
||||
it("validateOp: parent: null is valid", () => {
|
||||
const parsed = fresh();
|
||||
const r = validateOp(parsed, {
|
||||
type: "addElement",
|
||||
parent: null,
|
||||
index: 0,
|
||||
html: "<div>body-level</div>",
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── setElementStyles (model helper) ──────────────────────────────────────────
|
||||
|
||||
describe("setElementStyles key normalization", () => {
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
patchRemove,
|
||||
} from "./patches.js";
|
||||
import { upsertCssRule } from "./cssWriter.js";
|
||||
import { mintHfId } from "@hyperframes/core/hf-ids";
|
||||
import { parseGsapScriptAcornForWrite } from "@hyperframes/core/gsap-parser-acorn";
|
||||
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||
import {
|
||||
@@ -77,7 +78,7 @@ import { deriveKeyframeBackfillDefaults } from "./keyframeBackfill.js";
|
||||
export interface MutationResult {
|
||||
forward: JsonPatchOp[];
|
||||
inverse: JsonPatchOp[];
|
||||
meta?: { animationId?: string };
|
||||
meta?: { animationId?: string; newId?: string };
|
||||
}
|
||||
|
||||
const EMPTY: MutationResult = { forward: [], inverse: [] };
|
||||
@@ -270,6 +271,8 @@ export function applyOp(parsed: ParsedDocument, op: EditOp): MutationResult {
|
||||
return handleMoveElement(parsed, targets(op.target), op.x, op.y);
|
||||
case "removeElement":
|
||||
return handleRemoveElement(parsed, targets(op.target));
|
||||
case "addElement":
|
||||
return handleAddElement(parsed, op.parent, op.index, op.html);
|
||||
case "reorderElements":
|
||||
return handleReorderElements(parsed, op.entries);
|
||||
case "setCompositionMetadata":
|
||||
@@ -619,6 +622,99 @@ function handleRemoveElement(parsed: ParsedDocument, ids: HfId[]): MutationResul
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── addElement handler ───────────────────────────────────────────────────────
|
||||
|
||||
// Tags that must never receive a stable hf-id — mirrors hfIds.ts EXCLUDED_TAGS.
|
||||
const HF_EXCLUDED_TAGS = new Set([
|
||||
"script",
|
||||
"style",
|
||||
"template",
|
||||
"meta",
|
||||
"link",
|
||||
"noscript",
|
||||
"base",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Resolve all existing hf-ids in the document into `assigned` so that
|
||||
* mintHfId cannot issue an id that already exists in the composition.
|
||||
*/
|
||||
function collectDocumentHfIds(document: Document): Set<string> {
|
||||
const assigned = new Set<string>();
|
||||
for (const el of Array.from(document.querySelectorAll("[data-hf-id]"))) {
|
||||
const id = el.getAttribute("data-hf-id");
|
||||
if (id) assigned.add(id);
|
||||
}
|
||||
return assigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp data-hf-id onto every un-stamped element in `root` and its
|
||||
* descendants, minting ids against `assigned` (the live document's id set).
|
||||
* Returns the minted id of `root` (or its existing id if already stamped).
|
||||
*/
|
||||
function mintFragmentIds(root: Element, assigned: Set<string>): string {
|
||||
if (!root.getAttribute("data-hf-id") && !HF_EXCLUDED_TAGS.has(root.tagName.toLowerCase())) {
|
||||
root.setAttribute("data-hf-id", mintHfId(root, assigned));
|
||||
}
|
||||
for (const el of Array.from(root.querySelectorAll("*"))) {
|
||||
if (HF_EXCLUDED_TAGS.has(el.tagName.toLowerCase())) continue;
|
||||
if (el.getAttribute("data-hf-id")) continue; // pinned
|
||||
el.setAttribute("data-hf-id", mintHfId(el, assigned));
|
||||
}
|
||||
return root.getAttribute("data-hf-id") ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an HTML fragment (single-root) as a child of `parent` at `index`.
|
||||
* Mints ids against the LIVE document's existing id set so new ids can never
|
||||
* collide with elements already in the composition. Returns the minted root id
|
||||
* via result.meta.newId — mirrors the `animationId` pattern in addGsapTween.
|
||||
*
|
||||
* Inverse = patchRemove of the new element's path; mirrors handleRemoveElement's
|
||||
* inverse = patchAdd. Forward/inverse are thus symmetric with that handler.
|
||||
*/
|
||||
function handleAddElement(
|
||||
parsed: ParsedDocument,
|
||||
parent: HfId | null,
|
||||
index: number,
|
||||
html: string,
|
||||
): MutationResult {
|
||||
// Resolve parent element (null → document body).
|
||||
const parentEl =
|
||||
parent === null
|
||||
? ((parsed.document as unknown as { body: Element }).body as unknown as Element)
|
||||
: (resolveScoped(parsed.document, parent) as Element);
|
||||
|
||||
// Parse the fragment within the target document to avoid cross-document issues
|
||||
// (same approach as apply-patches.ts:222). validateOp guarantees a non-null firstElementChild.
|
||||
const tmp = parsed.document.createElement("div");
|
||||
tmp.innerHTML = html;
|
||||
const node = tmp.firstElementChild;
|
||||
if (!node) return EMPTY;
|
||||
|
||||
// Mint ids against the LIVE doc's existing id set (the #1 landmine — a fresh
|
||||
// ensureHfIds(fragment) is blind to existing doc ids and can collide).
|
||||
// Order: mint → capture outerHTML → insert → build patch (id needed for path).
|
||||
const assigned = collectDocumentHfIds(parsed.document);
|
||||
const newId = mintFragmentIds(node, assigned);
|
||||
const stampedHtml = node.outerHTML;
|
||||
|
||||
// Insert at `index` — append if index >= childCount (RFC-6902 insert semantics).
|
||||
const ref = Array.from(parentEl.children)[index] ?? null;
|
||||
parentEl.insertBefore(node, ref);
|
||||
|
||||
// parentId for the inverse patch: bare id of the parent, or null for body root.
|
||||
const parentId = parent !== null ? (parentEl.getAttribute("data-hf-id") ?? null) : null;
|
||||
|
||||
const path = elementPath(newId);
|
||||
return {
|
||||
forward: [patchAdd(path, { html: stampedHtml, parentId, siblingIndex: index })],
|
||||
inverse: [patchRemove(path)],
|
||||
meta: { newId },
|
||||
};
|
||||
}
|
||||
|
||||
function handleReorderElements(
|
||||
parsed: ParsedDocument,
|
||||
entries: Array<{ target: HfId; zIndex: number }>,
|
||||
@@ -1310,6 +1406,30 @@ export function validateOp(parsed: ParsedDocument, op: EditOp): CanResult {
|
||||
);
|
||||
return CAN_OK;
|
||||
}
|
||||
case "addElement": {
|
||||
if (op.parent !== null && resolveScoped(parsed.document, op.parent) === null)
|
||||
return canErr(
|
||||
"E_TARGET_NOT_FOUND",
|
||||
`Parent element not found: "${op.parent}".`,
|
||||
"Verify the parent id against comp.getElements() or comp.find().",
|
||||
);
|
||||
if (op.index < 0) return canErr("E_INVALID_ARGS", `index must be >= 0 (got ${op.index}).`);
|
||||
if (!op.html || op.html.trim().length === 0)
|
||||
return canErr("E_INVALID_HTML", "html must not be empty.");
|
||||
// Parse to check for <script> and zero-element fragments.
|
||||
// Use the same temp-div pattern as apply-patches.ts for consistency.
|
||||
const tmp = parsed.document.createElement("div");
|
||||
tmp.innerHTML = op.html;
|
||||
if (tmp.firstElementChild === null)
|
||||
return canErr("E_INVALID_HTML", "html parses to zero element nodes.");
|
||||
if (tmp.querySelector("script") !== null)
|
||||
return canErr(
|
||||
"E_INVALID_HTML",
|
||||
"<script> elements are not permitted in addElement html.",
|
||||
"GSAP is managed by the composition's single script block; add tweens via addGsapTween.",
|
||||
);
|
||||
return CAN_OK;
|
||||
}
|
||||
case "reorderElements": {
|
||||
if (op.entries.length === 0) return CAN_OK;
|
||||
const missing = op.entries
|
||||
|
||||
@@ -139,6 +139,11 @@ class CompositionImpl implements Composition {
|
||||
this.dispatch({ type: "removeElement", target: id });
|
||||
}
|
||||
|
||||
addElement(parent: HfId | null, index: number, html: string): HfId {
|
||||
const result = this._dispatch({ type: "addElement", parent, index, html }, ORIGIN_LOCAL);
|
||||
return result.meta?.newId ?? "";
|
||||
}
|
||||
|
||||
setVariableValue(id: string, value: string | number | boolean | FontValue | ImageValue): void {
|
||||
this.dispatch({ type: "setVariableValue", id, value });
|
||||
}
|
||||
|
||||
@@ -88,6 +88,15 @@ export type EditOp =
|
||||
| { type: "setHold"; target: HfId | HfId[]; hold: ElasticHold }
|
||||
| { type: "moveElement"; target: HfId | HfId[]; x: number; y: number }
|
||||
| { type: "removeElement"; target: HfId | HfId[] }
|
||||
| {
|
||||
type: "addElement";
|
||||
/** Id of the parent element, or null to insert at the document body root. */
|
||||
parent: HfId | null;
|
||||
/** Zero-based sibling index at which to insert (append if >= childCount). */
|
||||
index: number;
|
||||
/** Single-root HTML fragment. Must not contain <script>. */
|
||||
html: string;
|
||||
}
|
||||
| {
|
||||
type: "reorderElements";
|
||||
/** Each entry sets inline zIndex on one element. Positioning is unchanged — z-index only takes effect on non-static elements, so the caller must ensure the target is positioned. */
|
||||
@@ -340,6 +349,13 @@ export interface Composition {
|
||||
setAttribute(id: HfId, name: string, value: string | null): void;
|
||||
setTiming(id: HfId, timing: { start?: number; duration?: number; trackIndex?: number }): void;
|
||||
removeElement(id: HfId): void;
|
||||
/**
|
||||
* Insert an HTML fragment as a child of `parent` at `index` (WS-D).
|
||||
* Mints a stable hf-id against the live document's existing id set.
|
||||
* Returns the minted id of the inserted root element.
|
||||
* Inverse = removeElement of the returned id.
|
||||
*/
|
||||
addElement(parent: HfId | null, index: number, html: string): HfId;
|
||||
setVariableValue(id: string, value: string | number | boolean): void;
|
||||
/**
|
||||
* Read enter/exit times and GSAP labels for every timed element (WS-C).
|
||||
|
||||
Reference in New Issue
Block a user