Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
94 changes: 94 additions & 0 deletions packages/core/src/runtime/positionEdits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
45 changes: 38 additions & 7 deletions packages/core/src/runtime/positionEdits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,24 +153,55 @@ 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
// module-scope `instanceof HTMLElement` check silently no-ops on every element
// 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;
Expand Down
Loading