mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(core): apply position edits to SVG elements, not just HTML (#2724)
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 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 m="data-hf-edit-base-x",b="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(m)),i=k(e.getAttribute("data-y"))-k(e.getAttribute(b));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([${m}]):not([${b}])`);for(let u=0;u<i.length;u++){let a=i[u];if(a===void 0||!r(a))continue;let _=a.getAttribute(w)??"";_===""?a.style.removeProperty("translate"):a.style.setProperty("translate",_),a.removeAttribute(w),S.delete(a)}let l=e.querySelectorAll(`[${m}], [${b}]`),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 _=120,v=t.setInterval(()=>{if(a()){t.clearInterval(v);return}_-=1,_<=0&&t.clearInterval(v)},50)}function W(){document.querySelector(`[${m}], [${b}]`)&&(h(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 n=A(),r=n.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(i){}(n.__hfDebug||n.__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(n){P("runtime.analytics.site1",n)}}var _="data-hf-edit-base-x",S="data-hf-edit-base-y",m="data-hf-edit-original-translate",E=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},V=e=>{let t=[],n=0,r="";for(let i of e.trim())i==="("&&(n+=1),i===")"&&(n=Math.max(0,n-1)),/\\s/.test(i)&&n===0?(r&&t.push(r),r=""):r+=i;return r&&t.push(r),t},$=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,k=(e,t)=>$.test(e)&&$.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,I=(e,t,n)=>{if(!e||e==="none")return`${t} ${n}`;let[r,i,u]=V(e);if(r===void 0)return`${t} ${n}`;if(i===void 0)return`${k(r,t)} ${n}`;let d=u===void 0?"":` ${u}`;return`${k(r,t)} ${k(i,n)}${d}`},O=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},G=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let n=e.ownerDocument.defaultView,r=n?n.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},h=new WeakMap;function H(e,t){let n=h.get(e);if(!t?.force&&n!==void 0&&e.style.getPropertyValue("translate")!==n){R("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=E(e.getAttribute("data-x"))-E(e.getAttribute(_)),i=E(e.getAttribute("data-y"))-E(e.getAttribute(S));e.hasAttribute(m)||e.setAttribute(m,G(e)),n===void 0&&O(e);let u=e.getAttribute(m)??"",d=I(u,`${r}px`,`${i}px`);e.style.setProperty("translate",d),h.set(e,e.style.getPropertyValue("translate"))}function v(e,t){let n=e.defaultView?.HTMLElement,r=e.defaultView?.SVGElement,i=a=>n||r?n!==void 0&&a instanceof n||r!==void 0&&a instanceof r:typeof a.style?.setProperty=="function",u=e.querySelectorAll(`[${m}]:not([${_}]):not([${S}])`);for(let a=0;a<u.length;a++){let l=u[a];if(l===void 0||!i(l))continue;let w=l.getAttribute(m)??"";w===""?l.style.removeProperty("translate"):l.style.setProperty("translate",w),l.removeAttribute(m),h.delete(l)}let d=e.querySelectorAll(`[${_}], [${S}]`),b=0;for(let a=0;a<d.length;a++){let l=d[a];l===void 0||!i(l)||(H(l,t),b+=1)}return b}var x="__hfPositionEditsSeekReapplyWrapped",D=new WeakSet,F=new WeakMap,W=new WeakMap;function L(e){let t=e,n=()=>{try{v(t.document)}catch{}},r=o=>typeof o=="function"&&(D.has(o)||!!o[x]),i=o=>{D.add(o);try{Object.defineProperty(o,x,{value:!0})}catch{}},u=o=>{if(typeof o!="function"||r(o))return o;let s=function(...c){let f=o.apply(this,c);return n(),f};return i(s),s},d=(o,s)=>{let c=F.get(o);if(c?.has(s))return!0;let f=Object.getOwnPropertyDescriptor(o,s);if(f?.configurable===!1){let p=o[s];return typeof p=="function"&&(o[s]=u(p),n()),!1}let g=o[s],y=f?.set;return Object.defineProperty(o,s,{configurable:!0,enumerable:f?.enumerable??!0,get:()=>g,set:p=>{g=u(p),y?.call(o,p)}}),g=u(g),c??(c=new Set),c.add(s),F.set(o,c),n(),!0},b=(o,s)=>{let c=W.get(t),f=Object.getOwnPropertyDescriptor(t,o);if(!c?.has(o)){if(f?.configurable===!1){let p=t[o];return p?d(p,s):!1}let y=t[o];Object.defineProperty(t,o,{configurable:!0,enumerable:f?.enumerable??!0,get:()=>y,set:p=>{y=p,y&&d(y,s)}}),c??(c=new Set),c.add(o),W.set(t,c)}let g=t[o];return g?d(g,s):!1},a=()=>{let o=b("__hf","seek"),s=b("__player","renderSeek");return o&&s};if(a())return;let l=120,w=t.setInterval(()=>{if(a()){t.clearInterval(w);return}l-=1,l<=0&&t.clearInterval(w)},50)}function M(){document.querySelector(`[${_}], [${S}]`)&&(v(document),L(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",M,{once:!0}):M();})();\n';
|
||||
|
||||
/** Returns the pre-built position-edits render IIFE as a string constant. */
|
||||
export function getPositionEditsRenderScript(): string {
|
||||
|
||||
@@ -59,6 +59,20 @@ describe("applyPositionEdits", () => {
|
||||
el.remove();
|
||||
});
|
||||
|
||||
it("applies the delta to an SVG element (e.g. an authored <text> label), not just HTML", () => {
|
||||
// SVG graphics are positioned via the same CSS `translate` longhand; the old HTML-only guard
|
||||
// silently dropped SVG moves. Regression guard: an SVG <text> must be counted AND translated.
|
||||
const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
el.setAttribute("data-x", "145");
|
||||
el.setAttribute("data-y", "1");
|
||||
el.setAttribute(EDIT_BASE_X_ATTR, "0");
|
||||
el.setAttribute(EDIT_BASE_Y_ATTR, "0");
|
||||
document.body.appendChild(el);
|
||||
expect(applyPositionEdits(document)).toBe(1);
|
||||
expect(el.style.getPropertyValue("translate")).toBe("145px 1px");
|
||||
el.remove();
|
||||
});
|
||||
|
||||
it("treats missing data-x/y or baseline attributes as 0", () => {
|
||||
const el = makeElement({ "data-x": "40", [EDIT_BASE_X_ATTR]: "0" });
|
||||
applyPositionEdits(document);
|
||||
|
||||
@@ -30,6 +30,14 @@ export const EDIT_BASE_X_ATTR = "data-hf-edit-base-x";
|
||||
export const EDIT_BASE_Y_ATTR = "data-hf-edit-base-y";
|
||||
export const EDIT_ORIGINAL_TRANSLATE_ATTR = "data-hf-edit-original-translate";
|
||||
|
||||
/**
|
||||
* Elements a position edit can apply to: HTML elements AND SVG graphics (authored `<text>` labels,
|
||||
* shapes, groups). Both expose `.style` and honor the CSS `translate` longhand in modern Chrome, so
|
||||
* the same delta→translate compose logic works for either. (SVG was previously excluded by an
|
||||
* `instanceof HTMLElement`-only guard, which silently dropped every move on an SVG element.)
|
||||
*/
|
||||
type StylableElement = HTMLElement | SVGElement;
|
||||
|
||||
const num = (value: string | null): number => {
|
||||
const n = parseFloat(value ?? "");
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
@@ -80,7 +88,7 @@ export const composeTranslate = (original: string, x: string, y: string): string
|
||||
* reuse it and never read the translate again. gsap.getProperty parses
|
||||
* without mutating the element. Best-effort — absent or failing GSAP is fine.
|
||||
*/
|
||||
const primeGsapTransformCache = (el: HTMLElement): void => {
|
||||
const primeGsapTransformCache = (el: StylableElement): void => {
|
||||
try {
|
||||
const view = el.ownerDocument.defaultView as
|
||||
| (Window & { gsap?: { getProperty?: (t: Element, p: string) => unknown } })
|
||||
@@ -92,7 +100,7 @@ const primeGsapTransformCache = (el: HTMLElement): void => {
|
||||
};
|
||||
|
||||
/** The element's effective translate: inline if set, computed otherwise ("" = none). */
|
||||
export const readCurrentTranslate = (el: HTMLElement): string => {
|
||||
export const readCurrentTranslate = (el: StylableElement): string => {
|
||||
const inline = el.style.getPropertyValue("translate").trim();
|
||||
if (inline) return inline === "none" ? "" : inline;
|
||||
try {
|
||||
@@ -112,7 +120,7 @@ export const readCurrentTranslate = (el: HTMLElement): string => {
|
||||
* it then would DOUBLE the offset on every axis the tween doesn't animate, so
|
||||
* the non-forced path skips instead (degrading to the documented fold-loss).
|
||||
*/
|
||||
const lastAppliedTranslate = new WeakMap<HTMLElement, string>();
|
||||
const lastAppliedTranslate = new WeakMap<StylableElement, string>();
|
||||
|
||||
/**
|
||||
* Apply one element's position edit. Idempotent — the pre-edit translate is
|
||||
@@ -124,7 +132,7 @@ const lastAppliedTranslate = new WeakMap<HTMLElement, string>();
|
||||
* externally — used by editor commits, where the current inline translate is
|
||||
* the draft-composed one and must be overwritten.
|
||||
*/
|
||||
export function applyPositionEditToElement(el: HTMLElement, opts?: { force?: boolean }): void {
|
||||
export function applyPositionEditToElement(el: StylableElement, opts?: { force?: boolean }): void {
|
||||
const previous = lastAppliedTranslate.get(el);
|
||||
if (
|
||||
!opts?.force &&
|
||||
@@ -175,9 +183,15 @@ export function applyPositionEdits(doc: Document, opts?: { force?: boolean }): n
|
||||
// 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
|
||||
const RealmSVGElement = doc.defaultView?.SVGElement;
|
||||
// Stylable = HTML OR SVG element (SVG `<text>`/shapes are positioned via the same CSS `translate`
|
||||
// longhand). The old HTML-only check silently dropped every SVG move. Cross-realm safe (uses the
|
||||
// document's own realm constructors — see the note above); duck-type on `.style` when defaultView
|
||||
// is unavailable.
|
||||
const isStylable = (el: Element): el is StylableElement =>
|
||||
RealmHTMLElement || RealmSVGElement
|
||||
? (RealmHTMLElement !== undefined && el instanceof RealmHTMLElement) ||
|
||||
(RealmSVGElement !== undefined && el instanceof RealmSVGElement)
|
||||
: typeof (el as HTMLElement).style?.setProperty === "function";
|
||||
|
||||
const orphaned = doc.querySelectorAll(
|
||||
|
||||
Reference in New Issue
Block a user