mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
Merge pull request #2501 from heygen-com/task3-position-edit-force-reset
feat(core): applyPositionEdits gains force option and undo reset path
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// AUTO-GENERATED by scripts/build-position-edits-render.ts - do not edit
|
||||
const POSITION_EDITS_RENDER_IIFE: string =
|
||||
'"use strict";(()=>{function v(){return globalThis}function A(e,t){if(typeof window>"u")return;let o=v(),r=o.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(i){}(o.__hfDebug||o.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${e} swallowed:`,t)}var P=null;function T(e,t){if(P)try{P({source:"hf-preview",type:"analytics",event:e,properties:t??{}})}catch(o){A("runtime.analytics.site1",o)}}var w="data-hf-edit-base-x",y="data-hf-edit-base-y",_="data-hf-edit-original-translate",g=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},H=e=>{let t=[],o=0,r="";for(let i of e.trim())i==="("&&(o+=1),i===")"&&(o=Math.max(0,o-1)),/\\s/.test(i)&&o===0?(r&&t.push(r),r=""):r+=i;return r&&t.push(r),t},R=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,b=(e,t)=>R.test(e)&&R.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,I=(e,t,o)=>{if(!e||e==="none")return`${t} ${o}`;let[r,i,a]=H(e);if(r===void 0)return`${t} ${o}`;if(i===void 0)return`${b(r,t)} ${o}`;let c=a===void 0?"":` ${a}`;return`${b(r,t)} ${b(i,o)}${c}`},O=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},j=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let o=e.ownerDocument.defaultView,r=o?o.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},x=new WeakMap;function V(e,t){let o=x.get(e);if(!t?.force&&o!==void 0&&e.style.getPropertyValue("translate")!==o){T("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=g(e.getAttribute("data-x"))-g(e.getAttribute(w)),i=g(e.getAttribute("data-y"))-g(e.getAttribute(y));e.hasAttribute(_)||e.setAttribute(_,j(e)),o===void 0&&O(e);let a=e.getAttribute(_)??"",c=I(a,`${r}px`,`${i}px`);e.style.setProperty("translate",c),x.set(e,e.style.getPropertyValue("translate"))}function m(e){let t=e.querySelectorAll(`[${w}], [${y}]`),o=e.defaultView?.HTMLElement,r=0;for(let i=0;i<t.length;i++){let a=t[i];(o?a instanceof o:typeof a.style?.setProperty=="function")&&(V(a),r+=1)}return r}var D="__hfPositionEditsSeekReapplyWrapped",$=new WeakSet,F=new WeakMap,L=new WeakMap;function M(e){let t=e,o=()=>{try{m(t.document)}catch{}},r=n=>typeof n=="function"&&($.has(n)||!!n[D]),i=n=>{$.add(n);try{Object.defineProperty(n,D,{value:!0})}catch{}},a=n=>{if(typeof n!="function"||r(n))return n;let s=function(...u){let d=n.apply(this,u);return o(),d};return i(s),s},c=(n,s)=>{let u=F.get(n);if(u?.has(s))return!0;let d=Object.getOwnPropertyDescriptor(n,s);if(d?.configurable===!1){let l=n[s];return typeof l=="function"&&(n[s]=a(l),o()),!1}let p=n[s],f=d?.set;return Object.defineProperty(n,s,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>p,set:l=>{p=a(l),f?.call(n,l)}}),p=a(p),u??(u=new Set),u.add(s),F.set(n,u),o(),!0},k=(n,s)=>{let u=L.get(t),d=Object.getOwnPropertyDescriptor(t,n);if(!u?.has(n)){if(d?.configurable===!1){let l=t[n];return l?c(l,s):!1}let f=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>f,set:l=>{f=l,f&&c(f,s)}}),u??(u=new Set),u.add(n),L.set(t,u)}let p=t[n];return p?c(p,s):!1},E=()=>{let n=k("__hf","seek"),s=k("__player","renderSeek");return n&&s};if(E())return;let S=120,h=t.setInterval(()=>{if(E()){t.clearInterval(h);return}S-=1,S<=0&&t.clearInterval(h)},50)}function W(){document.querySelector(`[${w}], [${y}]`)&&(m(document),M(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",W,{once:!0}):W();})();\n';
|
||||
'"use strict";(()=>{function A(){return globalThis}function P(e,t){if(typeof window>"u")return;let o=A(),r=o.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(i){}(o.__hfDebug||o.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${e} swallowed:`,t)}var T=null;function R(e,t){if(T)try{T({source:"hf-preview",type:"analytics",event:e,properties:t??{}})}catch(o){P("runtime.analytics.site1",o)}}var _="data-hf-edit-base-x",m="data-hf-edit-base-y",w="data-hf-edit-original-translate",k=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},H=e=>{let t=[],o=0,r="";for(let i of e.trim())i==="("&&(o+=1),i===")"&&(o=Math.max(0,o-1)),/\\s/.test(i)&&o===0?(r&&t.push(r),r=""):r+=i;return r&&t.push(r),t},$=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,E=(e,t)=>$.test(e)&&$.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,I=(e,t,o)=>{if(!e||e==="none")return`${t} ${o}`;let[r,i,l]=H(e);if(r===void 0)return`${t} ${o}`;if(i===void 0)return`${E(r,t)} ${o}`;let d=l===void 0?"":` ${l}`;return`${E(r,t)} ${E(i,o)}${d}`},O=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},j=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let o=e.ownerDocument.defaultView,r=o?o.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},S=new WeakMap;function V(e,t){let o=S.get(e);if(!t?.force&&o!==void 0&&e.style.getPropertyValue("translate")!==o){R("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=k(e.getAttribute("data-x"))-k(e.getAttribute(_)),i=k(e.getAttribute("data-y"))-k(e.getAttribute(m));e.hasAttribute(w)||e.setAttribute(w,j(e)),o===void 0&&O(e);let l=e.getAttribute(w)??"",d=I(l,`${r}px`,`${i}px`);e.style.setProperty("translate",d),S.set(e,e.style.getPropertyValue("translate"))}function h(e,t){let o=e.defaultView?.HTMLElement,r=u=>o?u instanceof o:typeof u.style?.setProperty=="function",i=e.querySelectorAll(`[${w}]:not([${_}]):not([${m}])`);for(let u=0;u<i.length;u++){let a=i[u];if(a===void 0||!r(a))continue;let b=a.getAttribute(w)??"";b===""?a.style.removeProperty("translate"):a.style.setProperty("translate",b),a.removeAttribute(w),S.delete(a)}let l=e.querySelectorAll(`[${_}], [${m}]`),d=0;for(let u=0;u<l.length;u++){let a=l[u];a===void 0||!r(a)||(V(a,t),d+=1)}return d}var x="__hfPositionEditsSeekReapplyWrapped",D=new WeakSet,F=new WeakMap,L=new WeakMap;function M(e){let t=e,o=()=>{try{h(t.document)}catch{}},r=n=>typeof n=="function"&&(D.has(n)||!!n[x]),i=n=>{D.add(n);try{Object.defineProperty(n,x,{value:!0})}catch{}},l=n=>{if(typeof n!="function"||r(n))return n;let s=function(...c){let f=n.apply(this,c);return o(),f};return i(s),s},d=(n,s)=>{let c=F.get(n);if(c?.has(s))return!0;let f=Object.getOwnPropertyDescriptor(n,s);if(f?.configurable===!1){let p=n[s];return typeof p=="function"&&(n[s]=l(p),o()),!1}let g=n[s],y=f?.set;return Object.defineProperty(n,s,{configurable:!0,enumerable:f?.enumerable??!0,get:()=>g,set:p=>{g=l(p),y?.call(n,p)}}),g=l(g),c??(c=new Set),c.add(s),F.set(n,c),o(),!0},u=(n,s)=>{let c=L.get(t),f=Object.getOwnPropertyDescriptor(t,n);if(!c?.has(n)){if(f?.configurable===!1){let p=t[n];return p?d(p,s):!1}let y=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:f?.enumerable??!0,get:()=>y,set:p=>{y=p,y&&d(y,s)}}),c??(c=new Set),c.add(n),L.set(t,c)}let g=t[n];return g?d(g,s):!1},a=()=>{let n=u("__hf","seek"),s=u("__player","renderSeek");return n&&s};if(a())return;let b=120,v=t.setInterval(()=>{if(a()){t.clearInterval(v);return}b-=1,b<=0&&t.clearInterval(v)},50)}function W(){document.querySelector(`[${_}], [${m}]`)&&(h(document),M(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",W,{once:!0}):W();})();\n';
|
||||
|
||||
/** Returns the pre-built position-edits render IIFE as a string constant. */
|
||||
export function getPositionEditsRenderScript(): string {
|
||||
|
||||
@@ -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 (el === undefined || !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 (el === undefined || !isStylable(el)) continue;
|
||||
applyPositionEditToElement(el, opts);
|
||||
applied += 1;
|
||||
}
|
||||
return applied;
|
||||
|
||||
Reference in New Issue
Block a user