mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
feat(core): applyPositionEdits gains force option and undo reset path
This commit is contained in:
@@ -239,3 +239,97 @@ describe("installPositionEditsSeekReapply", () => {
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyPositionEdits — force option", () => {
|
||||
it("skips a clobbered translate non-forced, re-applies with force", () => {
|
||||
const el = makeElement({
|
||||
"data-x": "10",
|
||||
"data-y": "0",
|
||||
[EDIT_BASE_X_ATTR]: "0",
|
||||
[EDIT_BASE_Y_ATTR]: "0",
|
||||
});
|
||||
applyPositionEdits(document);
|
||||
expect(el.style.getPropertyValue("translate")).toBe("10px 0px");
|
||||
|
||||
// External clobber (GSAP folding it into a cached transform, a draft
|
||||
// write, …) — the non-forced fold-guard must skip, force must overwrite.
|
||||
el.style.setProperty("translate", "999px 0px");
|
||||
applyPositionEdits(document);
|
||||
expect(el.style.getPropertyValue("translate")).toBe("999px 0px");
|
||||
|
||||
applyPositionEdits(document, { force: true });
|
||||
expect(el.style.getPropertyValue("translate")).toBe("10px 0px");
|
||||
el.remove();
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyPositionEdits — reset path", () => {
|
||||
it("restores the captured pre-edit translate when base attrs were removed (undo)", () => {
|
||||
const el = makeElement(
|
||||
{
|
||||
"data-x": "10",
|
||||
"data-y": "5",
|
||||
[EDIT_BASE_X_ATTR]: "0",
|
||||
[EDIT_BASE_Y_ATTR]: "0",
|
||||
},
|
||||
"translate: 3px 4px",
|
||||
);
|
||||
applyPositionEdits(document);
|
||||
expect(el.style.getPropertyValue("translate")).toBe("13px 9px");
|
||||
expect(el.getAttribute(EDIT_ORIGINAL_TRANSLATE_ATTR)).toBe("3px 4px");
|
||||
|
||||
// Undo: the host removes the whole edit channel.
|
||||
el.removeAttribute("data-x");
|
||||
el.removeAttribute("data-y");
|
||||
el.removeAttribute(EDIT_BASE_X_ATTR);
|
||||
el.removeAttribute(EDIT_BASE_Y_ATTR);
|
||||
applyPositionEdits(document);
|
||||
expect(el.style.getPropertyValue("translate")).toBe("3px 4px");
|
||||
expect(el.hasAttribute(EDIT_ORIGINAL_TRANSLATE_ATTR)).toBe(false);
|
||||
el.remove();
|
||||
});
|
||||
|
||||
it("removes the inline translate entirely when the captured original was none", () => {
|
||||
const el = makeElement({
|
||||
"data-x": "10",
|
||||
"data-y": "0",
|
||||
[EDIT_BASE_X_ATTR]: "0",
|
||||
[EDIT_BASE_Y_ATTR]: "0",
|
||||
});
|
||||
applyPositionEdits(document);
|
||||
expect(el.style.getPropertyValue("translate")).toBe("10px 0px");
|
||||
|
||||
el.removeAttribute("data-x");
|
||||
el.removeAttribute("data-y");
|
||||
el.removeAttribute(EDIT_BASE_X_ATTR);
|
||||
el.removeAttribute(EDIT_BASE_Y_ATTR);
|
||||
applyPositionEdits(document);
|
||||
expect(el.style.getPropertyValue("translate")).toBe("");
|
||||
expect(el.hasAttribute(EDIT_ORIGINAL_TRANSLATE_ATTR)).toBe(false);
|
||||
el.remove();
|
||||
});
|
||||
|
||||
it("a redo after reset re-captures a clean baseline", () => {
|
||||
const el = makeElement({
|
||||
"data-x": "10",
|
||||
"data-y": "0",
|
||||
[EDIT_BASE_X_ATTR]: "0",
|
||||
[EDIT_BASE_Y_ATTR]: "0",
|
||||
});
|
||||
applyPositionEdits(document);
|
||||
el.removeAttribute("data-x");
|
||||
el.removeAttribute("data-y");
|
||||
el.removeAttribute(EDIT_BASE_X_ATTR);
|
||||
el.removeAttribute(EDIT_BASE_Y_ATTR);
|
||||
applyPositionEdits(document); // reset
|
||||
|
||||
// Redo: the host restores the edit channel with a new position.
|
||||
el.setAttribute("data-x", "20");
|
||||
el.setAttribute("data-y", "0");
|
||||
el.setAttribute(EDIT_BASE_X_ATTR, "0");
|
||||
el.setAttribute(EDIT_BASE_Y_ATTR, "0");
|
||||
applyPositionEdits(document, { force: true });
|
||||
expect(el.style.getPropertyValue("translate")).toBe("20px 0px");
|
||||
el.remove();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,9 +153,21 @@ export function applyPositionEditToElement(el: HTMLElement, opts?: { force?: boo
|
||||
/**
|
||||
* Apply all pending position edits in the document. Returns the number of
|
||||
* elements updated.
|
||||
*
|
||||
* Runs the RESET path first: an element still carrying the captured pre-edit
|
||||
* translate marker (EDIT_ORIGINAL_TRANSLATE_ATTR) but NO base attrs had its
|
||||
* edit undone — the attrs were removed, so it no longer matches the apply
|
||||
* selector, and the inline translate written by an earlier application would
|
||||
* stay orphaned (the element visually displaced after its edit was reverted).
|
||||
* Restore the captured translate and clear the marker so a later redo
|
||||
* re-captures a clean baseline.
|
||||
*
|
||||
* `force` forwards to applyPositionEditToElement: re-apply even when the
|
||||
* previously written translate was clobbered externally. Hosts replaying
|
||||
* undo/redo should force — after a reset the fold-guard's bookkeeping no
|
||||
* longer matches and the non-forced path would silently skip the redo.
|
||||
*/
|
||||
export function applyPositionEdits(doc: Document): number {
|
||||
const marked = doc.querySelectorAll(`[${EDIT_BASE_X_ATTR}], [${EDIT_BASE_Y_ATTR}]`);
|
||||
export function applyPositionEdits(doc: Document, opts?: { force?: boolean }): number {
|
||||
// Not `instanceof HTMLElement`: `doc` is frequently an iframe's document (the
|
||||
// SDK's edit preview, a host embedding a composition), and its elements are
|
||||
// HTMLElement instances of THAT frame's realm — never this module's. A
|
||||
@@ -163,14 +175,33 @@ export function applyPositionEdits(doc: Document): number {
|
||||
// cross-realm. Use the document's own realm's constructor; duck-type on
|
||||
// `.style` when defaultView is unavailable (a detached/synthetic document).
|
||||
const RealmHTMLElement = doc.defaultView?.HTMLElement;
|
||||
const isStylable = (el: Element): el is HTMLElement =>
|
||||
RealmHTMLElement
|
||||
? el instanceof RealmHTMLElement
|
||||
: typeof (el as HTMLElement).style?.setProperty === "function";
|
||||
|
||||
const orphaned = doc.querySelectorAll(
|
||||
`[${EDIT_ORIGINAL_TRANSLATE_ATTR}]:not([${EDIT_BASE_X_ATTR}]):not([${EDIT_BASE_Y_ATTR}])`,
|
||||
);
|
||||
for (let i = 0; i < orphaned.length; i++) {
|
||||
const el = orphaned[i];
|
||||
if (!isStylable(el)) continue;
|
||||
const original = el.getAttribute(EDIT_ORIGINAL_TRANSLATE_ATTR) ?? "";
|
||||
if (original === "") {
|
||||
el.style.removeProperty("translate");
|
||||
} else {
|
||||
el.style.setProperty("translate", original);
|
||||
}
|
||||
el.removeAttribute(EDIT_ORIGINAL_TRANSLATE_ATTR);
|
||||
lastAppliedTranslate.delete(el);
|
||||
}
|
||||
|
||||
const marked = doc.querySelectorAll(`[${EDIT_BASE_X_ATTR}], [${EDIT_BASE_Y_ATTR}]`);
|
||||
let applied = 0;
|
||||
for (let i = 0; i < marked.length; i++) {
|
||||
const el = marked[i];
|
||||
const isStylable = RealmHTMLElement
|
||||
? el instanceof RealmHTMLElement
|
||||
: typeof (el as HTMLElement).style?.setProperty === "function";
|
||||
if (!isStylable) continue;
|
||||
applyPositionEditToElement(el as HTMLElement);
|
||||
if (!isStylable(el)) continue;
|
||||
applyPositionEditToElement(el, opts);
|
||||
applied += 1;
|
||||
}
|
||||
return applied;
|
||||
|
||||
Reference in New Issue
Block a user