mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-12 20:47:30 +00:00
fix(studio): a pasted element keeps the look its id gave the original
This commit is contained in:
@@ -52,12 +52,16 @@ afterEach(() => {
|
||||
|
||||
// The preview page: the saved markup with the preview's in-memory ids, a sub-composition
|
||||
// mounted inline, and the runtime's hide on both clips.
|
||||
function mountPreview(sub: string, bundle: (host: Element) => void): HTMLIFrameElement {
|
||||
function mountPreview(
|
||||
sub: string,
|
||||
bundle: (host: Element) => void,
|
||||
saved = SAVED,
|
||||
): HTMLIFrameElement {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.appendChild(iframe);
|
||||
const doc = iframe.contentDocument as Document;
|
||||
doc.open();
|
||||
doc.write(ensureHfIds(SAVED));
|
||||
doc.write(ensureHfIds(saved));
|
||||
doc.close();
|
||||
const parsed = new DOMParser().parseFromString(ensureHfIds(sub), "text/html");
|
||||
const subRoot = (parsed.querySelector("template") as HTMLTemplateElement).content
|
||||
@@ -93,14 +97,15 @@ function mountClipboard(
|
||||
domSelection: DomEditSelection | null = null,
|
||||
sub = SUB,
|
||||
bundle: (host: Element) => void = () => {},
|
||||
saved = SAVED,
|
||||
) {
|
||||
const files: Record<string, string> = { "index.html": SAVED, "compositions/sub.html": sub };
|
||||
const files: Record<string, string> = { "index.html": saved, "compositions/sub.html": sub };
|
||||
const fail = { on: false };
|
||||
const delayMs: Record<string, number> = {};
|
||||
const domEditSave = { pending: Promise.resolve() };
|
||||
const domSelectionRef = { current: domSelection };
|
||||
stubFiles(files, fail, delayMs);
|
||||
const iframe = mountPreview(sub, bundle);
|
||||
const iframe = mountPreview(sub, bundle, saved);
|
||||
const writes: string[] = [];
|
||||
const deleted: string[] = [];
|
||||
const writeProjectFile = async (_path: string, content: string) => {
|
||||
@@ -248,6 +253,39 @@ const SUB_SELECTION = {
|
||||
sourceFile: "compositions/sub.html",
|
||||
} as DomEditSelection;
|
||||
|
||||
describe("paste of an element styled by its id", () => {
|
||||
it("gives the copy the look its original has", async () => {
|
||||
clearSelection();
|
||||
const saved = SAVED.replace(
|
||||
"<body>",
|
||||
"<body><style>#badge { color: rgb(200, 30, 40); letter-spacing: 3px; }</style>",
|
||||
).replace("</h1>", '</h1><p id="badge">New</p>');
|
||||
const selection = {
|
||||
hfId: stampedHfId(saved, "#badge"),
|
||||
selector: "#badge",
|
||||
selectorIndex: 0,
|
||||
sourceFile: "index.html",
|
||||
} as DomEditSelection;
|
||||
const { clipboard, writes } = mountClipboard(selection, SUB, () => {}, saved);
|
||||
clipboard().handleCopy();
|
||||
await clipboard().handlePaste();
|
||||
const page = document.createElement("iframe");
|
||||
document.body.appendChild(page);
|
||||
const doc = page.contentDocument as Document;
|
||||
doc.open();
|
||||
doc.write(writes[0] ?? "");
|
||||
doc.close();
|
||||
const look = (id: string) => {
|
||||
const style = (page.contentWindow as Window).getComputedStyle(
|
||||
doc.getElementById(id) as Element,
|
||||
);
|
||||
return { color: style.color, letterSpacing: style.letterSpacing };
|
||||
};
|
||||
expect(look("badge")).toEqual({ color: "rgb(200, 30, 40)", letterSpacing: "3px" });
|
||||
expect(look("badge-2")).toEqual(look("badge"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("copy of a sub-composition clip", () => {
|
||||
it("rebases its relative asset paths to the project root, as the preview does", async () => {
|
||||
clearSelection();
|
||||
|
||||
@@ -157,13 +157,21 @@ const HF_ID_ATTR_RE = /\bdata-hf-id\s*=\s*["']?([^"'\s>]+)/gi;
|
||||
const hfIdsInFile = (content: string) =>
|
||||
new Set(Array.from(content.matchAll(HF_ID_ATTR_RE), (match) => match[1]));
|
||||
|
||||
/** `fromThisFile`: the element came from `content`, so a renamed copy takes its original's look, as a clip's does. */
|
||||
export function pasteElementHtml(
|
||||
content: string,
|
||||
payload: { html: string; originSelector?: string; originSelectorIndex?: number },
|
||||
fromThisFile = false,
|
||||
): string {
|
||||
const reminted = remintHfIds(payload.html, new DOMParser(), hfIdsInFile(content));
|
||||
const deduped = deduplicateIds(reminted, collectHtmlIds(content));
|
||||
return insertAsSibling(content, deduped, payload.originSelector, payload.originSelectorIndex);
|
||||
const result = insertAsSibling(
|
||||
content,
|
||||
deduped,
|
||||
payload.originSelector,
|
||||
payload.originSelectorIndex,
|
||||
);
|
||||
return fromThisFile ? carryLook(result, renamedIds(reminted, deduped), 0) : result;
|
||||
}
|
||||
|
||||
/** Shared insertion path for paste and duplicate, anchored at the playhead or the selection's end. Returns the
|
||||
@@ -324,6 +332,7 @@ export function useClipboard({
|
||||
kind: "dom-element",
|
||||
html: savedMarkupElseLive(parseSavedSource(content), live, sourceFile),
|
||||
sourceFile,
|
||||
projectId: projectIdRef.current ?? undefined,
|
||||
originSelector: domSelection.selector,
|
||||
originSelectorIndex: domSelection.selectorIndex,
|
||||
};
|
||||
@@ -373,15 +382,17 @@ export function useClipboard({
|
||||
const targetPath = activeCompPath || "index.html";
|
||||
try {
|
||||
let pastedIds: string[] = [];
|
||||
const fromThisFile = payload.sourceFile === targetPath && payload.projectId === pid;
|
||||
const paste = (originalContent: string) => {
|
||||
if (payload.kind !== "timeline-clip") return pasteElementHtml(originalContent, payload);
|
||||
if (payload.kind !== "timeline-clip")
|
||||
return pasteElementHtml(originalContent, payload, fromThisFile);
|
||||
const { currentTime, elements } = usePlayerStore.getState();
|
||||
const pasted = pasteTimelineClips(
|
||||
originalContent,
|
||||
payload.clips,
|
||||
currentTime,
|
||||
elements,
|
||||
payload.sourceFile === targetPath && payload.projectId === pid,
|
||||
fromThisFile,
|
||||
);
|
||||
pastedIds = pasted.ids;
|
||||
// A clip pasted past the current composition end would exist in the
|
||||
|
||||
@@ -22,6 +22,7 @@ export type ClipboardPayload =
|
||||
kind: "dom-element";
|
||||
html: string;
|
||||
sourceFile: string;
|
||||
projectId?: string;
|
||||
originSelector?: string;
|
||||
originSelectorIndex?: number;
|
||||
};
|
||||
@@ -70,6 +71,7 @@ export function deserializeClipboardPayload(json: string): ClipboardPayload | nu
|
||||
kind: "dom-element",
|
||||
html: obj.html,
|
||||
sourceFile: obj.sourceFile,
|
||||
projectId: typeof obj.projectId === "string" ? obj.projectId : undefined,
|
||||
originSelector: typeof obj.originSelector === "string" ? obj.originSelector : undefined,
|
||||
originSelectorIndex:
|
||||
typeof obj.originSelectorIndex === "number" ? obj.originSelectorIndex : undefined,
|
||||
|
||||
Reference in New Issue
Block a user