mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat: data-timeline-locked + fix font loss in sub-compositions
Timeline locking: - Add data-timeline-locked attribute support — fully disables move, trim-start, and trim-end in Studio for clips that carry this attr - Runtime propagates the attribute from inner composition root to host element so component authors control it from their HTML - All 15 caption components in the registry now carry the attribute Font fix: - Extract <link rel="stylesheet"> and <link rel="preconnect"> from sub-composition <head> alongside existing <style>/<script> extraction - Fixes caption components (and any sub-comp using Google Fonts via <link> tags) losing their font-family when loaded as sub-compositions - Applied in both runtime (compositionLoader) and compiler (inlineSubCompositions) paths
This commit is contained in:
@@ -685,6 +685,7 @@ export async function bundleToSingleHtml(
|
|||||||
const compStyleChunks: string[] = [...subCompResult.styles];
|
const compStyleChunks: string[] = [...subCompResult.styles];
|
||||||
const compScriptChunks: string[] = [...subCompResult.scripts];
|
const compScriptChunks: string[] = [...subCompResult.scripts];
|
||||||
const compExternalScriptSrcs: string[] = [...subCompResult.externalScriptSrcs];
|
const compExternalScriptSrcs: string[] = [...subCompResult.externalScriptSrcs];
|
||||||
|
const compExternalLinkHrefs: string[] = [...subCompResult.externalLinkHrefs];
|
||||||
const compVariablesByComp: Record<string, Record<string, unknown>> = {
|
const compVariablesByComp: Record<string, Record<string, unknown>> = {
|
||||||
...subCompResult.variablesByComp,
|
...subCompResult.variablesByComp,
|
||||||
};
|
};
|
||||||
@@ -811,6 +812,18 @@ export async function bundleToSingleHtml(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const href of compExternalLinkHrefs) {
|
||||||
|
if (!document.querySelector(`link[href="${href}"]`)) {
|
||||||
|
const linkEl = document.createElement("link");
|
||||||
|
linkEl.setAttribute(
|
||||||
|
"rel",
|
||||||
|
href.includes(".css") || href.includes("css2?") ? "stylesheet" : "preconnect",
|
||||||
|
);
|
||||||
|
linkEl.setAttribute("href", href);
|
||||||
|
document.head.appendChild(linkEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (compStyleChunks.length) {
|
if (compStyleChunks.length) {
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.textContent = compStyleChunks.join("\n\n");
|
style.textContent = compStyleChunks.join("\n\n");
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export interface InlineSubCompositionsResult {
|
|||||||
styles: string[];
|
styles: string[];
|
||||||
scripts: string[];
|
scripts: string[];
|
||||||
externalScriptSrcs: string[];
|
externalScriptSrcs: string[];
|
||||||
|
externalLinkHrefs: string[];
|
||||||
variablesByComp: Record<string, Record<string, unknown>>;
|
variablesByComp: Record<string, Record<string, unknown>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +150,7 @@ export function inlineSubCompositions(
|
|||||||
const styles: string[] = [];
|
const styles: string[] = [];
|
||||||
const scripts: string[] = [];
|
const scripts: string[] = [];
|
||||||
const externalScriptSrcs: string[] = [];
|
const externalScriptSrcs: string[] = [];
|
||||||
|
const externalLinkHrefs: string[] = [];
|
||||||
const variablesByComp: Record<string, Record<string, unknown>> = {};
|
const variablesByComp: Record<string, Record<string, unknown>> = {};
|
||||||
|
|
||||||
for (const hostEl of hosts) {
|
for (const hostEl of hosts) {
|
||||||
@@ -221,6 +223,14 @@ export function inlineSubCompositions(
|
|||||||
externalScriptSrcs.push(externalSrc);
|
externalScriptSrcs.push(externalSrc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const link of [
|
||||||
|
...compDoc.head.querySelectorAll('link[rel="stylesheet"], link[rel="preconnect"]'),
|
||||||
|
]) {
|
||||||
|
const href = (link.getAttribute("href") || "").trim();
|
||||||
|
if (href && !externalLinkHrefs.includes(href)) {
|
||||||
|
externalLinkHrefs.push(href);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract styles from content
|
// Extract styles from content
|
||||||
@@ -325,5 +335,5 @@ export function inlineSubCompositions(
|
|||||||
hostEl.removeAttribute("data-composition-src");
|
hostEl.removeAttribute("data-composition-src");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { styles, scripts, externalScriptSrcs, variablesByComp };
|
return { styles, scripts, externalScriptSrcs, externalLinkHrefs, variablesByComp };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,8 @@ async function mountCompositionContent(params: {
|
|||||||
headStyles?: HTMLStyleElement[];
|
headStyles?: HTMLStyleElement[];
|
||||||
/** Extra <script> elements from the parsed document <head> (non-template sub-compositions). */
|
/** Extra <script> elements from the parsed document <head> (non-template sub-compositions). */
|
||||||
headScripts?: HTMLScriptElement[];
|
headScripts?: HTMLScriptElement[];
|
||||||
|
/** Extra <link> elements from the parsed document <head> (font stylesheets, preconnects). */
|
||||||
|
headLinks?: HTMLLinkElement[];
|
||||||
/**
|
/**
|
||||||
* Defaults extracted from the sub-composition's own
|
* Defaults extracted from the sub-composition's own
|
||||||
* `<html data-composition-variables="...">` attribute. Layered under the
|
* `<html data-composition-variables="...">` attribute. Layered under the
|
||||||
@@ -297,6 +299,15 @@ async function mountCompositionContent(params: {
|
|||||||
? `[data-composition-id="${CSS.escape(runtimeScopeCompositionId)}"]`
|
? `[data-composition-id="${CSS.escape(runtimeScopeCompositionId)}"]`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
if (params.headLinks) {
|
||||||
|
for (const link of params.headLinks) {
|
||||||
|
const href = link.getAttribute("href") || "";
|
||||||
|
if (!href) continue;
|
||||||
|
if (document.head.querySelector(`link[href="${CSS.escape(href)}"]`)) continue;
|
||||||
|
document.head.appendChild(link.cloneNode(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Inject <head> styles from non-template sub-compositions first (they define
|
// Inject <head> styles from non-template sub-compositions first (they define
|
||||||
// element styles like backgrounds and positioning that the composition needs).
|
// element styles like backgrounds and positioning that the composition needs).
|
||||||
if (params.headStyles) {
|
if (params.headStyles) {
|
||||||
@@ -395,6 +406,9 @@ async function mountCompositionContent(params: {
|
|||||||
if (heightRaw) params.host.setAttribute("data-height", heightRaw);
|
if (heightRaw) params.host.setAttribute("data-height", heightRaw);
|
||||||
if (widthPx && params.host instanceof HTMLElement) params.host.style.width = widthPx;
|
if (widthPx && params.host instanceof HTMLElement) params.host.style.width = widthPx;
|
||||||
if (heightPx && params.host instanceof HTMLElement) params.host.style.height = heightPx;
|
if (heightPx && params.host instanceof HTMLElement) params.host.style.height = heightPx;
|
||||||
|
if (innerRoot.hasAttribute("data-timeline-locked")) {
|
||||||
|
params.host.setAttribute("data-timeline-locked", "");
|
||||||
|
}
|
||||||
params.host.appendChild(prepareFlattenedInnerRoot(innerRoot));
|
params.host.appendChild(prepareFlattenedInnerRoot(innerRoot));
|
||||||
} else if (params.hasTemplate) {
|
} else if (params.hasTemplate) {
|
||||||
params.host.appendChild(document.importNode(contentNode, true));
|
params.host.appendChild(document.importNode(contentNode, true));
|
||||||
@@ -581,6 +595,13 @@ export async function loadExternalCompositions(
|
|||||||
const headScripts = !template
|
const headScripts = !template
|
||||||
? Array.from(doc.head.querySelectorAll<HTMLScriptElement>("script"))
|
? Array.from(doc.head.querySelectorAll<HTMLScriptElement>("script"))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const headLinks = !template
|
||||||
|
? Array.from(
|
||||||
|
doc.head.querySelectorAll<HTMLLinkElement>(
|
||||||
|
'link[rel="stylesheet"], link[rel="preconnect"]',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
await mountCompositionContent({
|
await mountCompositionContent({
|
||||||
host,
|
host,
|
||||||
authoredCompositionId,
|
authoredCompositionId,
|
||||||
@@ -595,6 +616,7 @@ export async function loadExternalCompositions(
|
|||||||
parseDimensionPx: params.parseDimensionPx,
|
parseDimensionPx: params.parseDimensionPx,
|
||||||
headStyles,
|
headStyles,
|
||||||
headScripts,
|
headScripts,
|
||||||
|
headLinks,
|
||||||
declaredVariableDefaults: readDeclaredDefaults(doc.documentElement),
|
declaredVariableDefaults: readDeclaredDefaults(doc.documentElement),
|
||||||
onDiagnostic: params.onDiagnostic,
|
onDiagnostic: params.onDiagnostic,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -612,6 +612,19 @@ function inlineSubCompositions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.externalLinkHrefs.length && head) {
|
||||||
|
for (const href of result.externalLinkHrefs) {
|
||||||
|
if (document.querySelector(`link[href="${href}"]`)) continue;
|
||||||
|
const el = document.createElement("link");
|
||||||
|
el.setAttribute(
|
||||||
|
"rel",
|
||||||
|
href.includes(".css") || href.includes("css2?") ? "stylesheet" : "preconnect",
|
||||||
|
);
|
||||||
|
el.setAttribute("href", href);
|
||||||
|
head.appendChild(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Append collected styles to <head>
|
// Append collected styles to <head>
|
||||||
if (result.styles.length && head) {
|
if (result.styles.length && head) {
|
||||||
const styleEl = document.createElement("style");
|
const styleEl = document.createElement("style");
|
||||||
|
|||||||
@@ -263,6 +263,22 @@ describe("getTimelineEditCapabilities", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("locks all timeline edits for clips with data-timeline-locked", () => {
|
||||||
|
expect(
|
||||||
|
getTimelineEditCapabilities({
|
||||||
|
tag: "div",
|
||||||
|
duration: 8,
|
||||||
|
selector: '[data-composition-id="caption-highlight"]',
|
||||||
|
compositionSrc: "compositions/components/caption-highlight.html",
|
||||||
|
timelineLocked: true,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
canMove: false,
|
||||||
|
canTrimStart: false,
|
||||||
|
canTrimEnd: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("allows full editing of explicitly authored generic elements", () => {
|
it("allows full editing of explicitly authored generic elements", () => {
|
||||||
expect(
|
expect(
|
||||||
getTimelineEditCapabilities({
|
getTimelineEditCapabilities({
|
||||||
|
|||||||
@@ -211,8 +211,9 @@ export function getTimelineEditCapabilities(input: {
|
|||||||
playbackStartAttr?: "media-start" | "playback-start";
|
playbackStartAttr?: "media-start" | "playback-start";
|
||||||
sourceDuration?: number;
|
sourceDuration?: number;
|
||||||
timingSource?: "authored" | "implicit";
|
timingSource?: "authored" | "implicit";
|
||||||
|
timelineLocked?: boolean;
|
||||||
}): TimelineEditCapabilities {
|
}): TimelineEditCapabilities {
|
||||||
if (input.timingSource === "implicit") {
|
if (input.timingSource === "implicit" || input.timelineLocked) {
|
||||||
return {
|
return {
|
||||||
canMove: false,
|
canMove: false,
|
||||||
canTrimStart: false,
|
canTrimStart: false,
|
||||||
|
|||||||
@@ -280,6 +280,10 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|||||||
applyMediaMetadataFromElement(entry, el);
|
applyMediaMetadataFromElement(entry, el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (el.hasAttribute("data-timeline-locked")) {
|
||||||
|
entry.timelineLocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Sub-compositions
|
// Sub-compositions
|
||||||
const compSrc =
|
const compSrc =
|
||||||
el.getAttribute("data-composition-src") || el.getAttribute("data-composition-file");
|
el.getAttribute("data-composition-src") || el.getAttribute("data-composition-file");
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export interface TimelineElement {
|
|||||||
compositionSrc?: string;
|
compositionSrc?: string;
|
||||||
/** Whether this row came from authored clip timing or Studio's full-duration layer fallback. */
|
/** Whether this row came from authored clip timing or Studio's full-duration layer fallback. */
|
||||||
timingSource?: "authored" | "implicit";
|
timingSource?: "authored" | "implicit";
|
||||||
|
/** Set by data-timeline-locked on the host element — disables move and trim in Studio. */
|
||||||
|
timelineLocked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZoomMode = "fit" | "manual";
|
export type ZoomMode = "fit" | "manual";
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="clip-wipe"
|
id="clip-wipe"
|
||||||
data-composition-id="caption-clip-wipe"
|
data-composition-id="caption-clip-wipe" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
+1
-1
@@ -123,7 +123,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="editorial-emphasis"
|
id="editorial-emphasis"
|
||||||
data-composition-id="caption-editorial-emphasis"
|
data-composition-id="caption-editorial-emphasis" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="emoji-pop"
|
id="emoji-pop"
|
||||||
data-composition-id="caption-emoji-pop"
|
data-composition-id="caption-emoji-pop" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="caption-glitch-rgb"
|
id="caption-glitch-rgb"
|
||||||
data-composition-id="caption-glitch-rgb"
|
data-composition-id="caption-glitch-rgb" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="gradient-fill"
|
id="gradient-fill"
|
||||||
data-composition-id="caption-gradient-fill"
|
data-composition-id="caption-gradient-fill" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="highlight"
|
id="highlight"
|
||||||
data-composition-id="caption-highlight"
|
data-composition-id="caption-highlight" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="kinetic-slam"
|
id="kinetic-slam"
|
||||||
data-composition-id="caption-kinetic-slam"
|
data-composition-id="caption-kinetic-slam" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="matrix-decode"
|
id="matrix-decode"
|
||||||
data-composition-id="caption-matrix-decode"
|
data-composition-id="caption-matrix-decode" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="neon-accent"
|
id="neon-accent"
|
||||||
data-composition-id="caption-neon-accent"
|
data-composition-id="caption-neon-accent" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="neon-glow"
|
id="neon-glow"
|
||||||
data-composition-id="caption-neon-glow"
|
data-composition-id="caption-neon-glow" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="parallax-layers"
|
id="parallax-layers"
|
||||||
data-composition-id="caption-parallax-layers"
|
data-composition-id="caption-parallax-layers" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="particle-burst"
|
id="particle-burst"
|
||||||
data-composition-id="caption-particle-burst"
|
data-composition-id="caption-particle-burst" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="pill-karaoke"
|
id="pill-karaoke"
|
||||||
data-composition-id="caption-pill-karaoke"
|
data-composition-id="caption-pill-karaoke" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="caption-texture"
|
id="caption-texture"
|
||||||
data-composition-id="caption-texture"
|
data-composition-id="caption-texture" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
id="weight-shift"
|
id="weight-shift"
|
||||||
data-composition-id="caption-weight-shift"
|
data-composition-id="caption-weight-shift" data-timeline-locked
|
||||||
data-start="0"
|
data-start="0"
|
||||||
data-duration="8"
|
data-duration="8"
|
||||||
data-fps="30"
|
data-fps="30"
|
||||||
|
|||||||
Reference in New Issue
Block a user