@hyperframes/studio 0.8.88 → 0.8.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-w3Bbv0kd.js → hyperframes-player-DDjER_ii.js} +1 -1
- package/dist/assets/{index-DldknZPp.js → index-5SFeUXP8.js} +1 -1
- package/dist/assets/{index-DE6cZYR5.js → index-BKE3_N4v.js} +180 -180
- package/dist/assets/{index-B6PGJTzp.js → index-CVsCxnlz.js} +1 -1
- package/dist/index.d.ts +39 -5
- package/dist/index.html +1 -1
- package/dist/index.js +156 -65
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/components/editor/DomEditOverlay.tsx +7 -4
- package/src/components/editor/domEditOverlayGestures.ts +4 -4
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +2 -4
- package/src/hooks/useDomGeometryCommit.test.tsx +226 -0
- package/src/hooks/useDomGeometryCommit.ts +147 -0
- package/src/hooks/useGsapAwareEditing.ts +9 -4
- package/src/index.ts +3 -1
- package/src/player/sceneSwap.ts +21 -0
- package/src/utils/gsapUndoRestore.test.ts +58 -0
- package/src/utils/gsapUndoRestore.ts +6 -1
- package/src/utils/studioPreviewHelpers.test.ts +29 -0
- package/src/utils/studioPreviewHelpers.ts +37 -80
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
import {
|
|
5
5
|
coversComposition,
|
|
6
|
+
getAllPreviewTargetsFromPointer,
|
|
6
7
|
getPreviewTargetFromPointer,
|
|
7
8
|
pauseStudioPreviewPlayback,
|
|
8
9
|
} from "./studioPreviewHelpers";
|
|
@@ -141,6 +142,34 @@ describe("getPreviewTargetFromPointer", () => {
|
|
|
141
142
|
iframe.remove();
|
|
142
143
|
});
|
|
143
144
|
|
|
145
|
+
it("hit-tests without adding styles to the preview document", () => {
|
|
146
|
+
const { iframe, doc } = createPreviewIframe();
|
|
147
|
+
doc.body.innerHTML = `<main data-composition-id="scene"><h1 id="headline">Title</h1></main>`;
|
|
148
|
+
const scene = doc.querySelector<HTMLElement>("main")!;
|
|
149
|
+
const headline = doc.getElementById("headline")!;
|
|
150
|
+
stubRect(iframe, domRect(0, 0, 400, 300));
|
|
151
|
+
stubRect(scene, domRect(0, 0, 400, 300));
|
|
152
|
+
stubRect(headline, domRect(40, 40, 160, 48));
|
|
153
|
+
const stylesAtHitTest: number[] = [];
|
|
154
|
+
doc.elementsFromPoint = () => {
|
|
155
|
+
stylesAtHitTest.push(doc.querySelectorAll("style").length + doc.adoptedStyleSheets.length);
|
|
156
|
+
return [headline, scene];
|
|
157
|
+
};
|
|
158
|
+
// First call runs the once-per-document inheritance probe, a body mutation.
|
|
159
|
+
expect(getPreviewTargetFromPointer(iframe, 80, 64, "index.html")).toBe(headline);
|
|
160
|
+
const observer = new MutationObserver(() => {});
|
|
161
|
+
observer.observe(doc, { subtree: true, childList: true, attributes: true });
|
|
162
|
+
|
|
163
|
+
expect(getPreviewTargetFromPointer(iframe, 80, 64, "index.html")).toBe(headline);
|
|
164
|
+
expect(getAllPreviewTargetsFromPointer(iframe, 80, 64, "index.html")).toEqual([headline]);
|
|
165
|
+
const mutations = observer.takeRecords();
|
|
166
|
+
observer.disconnect();
|
|
167
|
+
|
|
168
|
+
expect(stylesAtHitTest).toEqual([0, 0, 0]);
|
|
169
|
+
expect(mutations).toEqual([]);
|
|
170
|
+
iframe.remove();
|
|
171
|
+
});
|
|
172
|
+
|
|
144
173
|
it("skips candidates hidden from author hit-testing by inherited pointer-events:none", () => {
|
|
145
174
|
const { iframe, doc } = createPreviewIframe();
|
|
146
175
|
|
|
@@ -70,28 +70,6 @@ function resolvePreviewLocalPointer(
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const POINTER_EVENTS_OVERRIDE_ID = "__hf_studio_pointer_events_override__";
|
|
74
|
-
|
|
75
|
-
function forcePointerEventsAuto(doc: Document): HTMLStyleElement | null {
|
|
76
|
-
try {
|
|
77
|
-
const style = doc.createElement("style");
|
|
78
|
-
style.id = POINTER_EVENTS_OVERRIDE_ID;
|
|
79
|
-
style.textContent = "* { pointer-events: auto !important; }";
|
|
80
|
-
doc.head.appendChild(style);
|
|
81
|
-
return style;
|
|
82
|
-
} catch {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function removePointerEventsOverride(style: HTMLStyleElement | null): void {
|
|
88
|
-
try {
|
|
89
|
-
style?.remove();
|
|
90
|
-
} catch {
|
|
91
|
-
// cross-origin or detached doc
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
73
|
const pointerEventsInheritanceFallbackByDocument = new WeakMap<Document, boolean>();
|
|
96
74
|
|
|
97
75
|
function needsPointerEventsInheritanceFallback(doc: Document, win: Window): boolean {
|
|
@@ -222,45 +200,33 @@ export function getPreviewTargetFromPointer(
|
|
|
222
200
|
const localPointer = resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY);
|
|
223
201
|
if (!localPointer) return null;
|
|
224
202
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
candidates.find((el) => !isFullBleedTarget(el, localPointer.viewport)) ?? null;
|
|
234
|
-
if (visualTarget) return visualTarget;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Belt-and-suspenders: elementsFromPoint is universally supported in the
|
|
238
|
-
// browsers this ships in, so the override is already removed by this
|
|
239
|
-
// point in practice — but guard the environment without it too, so
|
|
240
|
-
// hasAuthorPointerEventsNone below never reads a forced-auto value.
|
|
241
|
-
removePointerEventsOverride(overrideStyle);
|
|
242
|
-
overrideStyle = null;
|
|
243
|
-
|
|
244
|
-
// No element hit (e.g. empty space inside an animated group's overlay) — fall
|
|
245
|
-
// back to the group whose member-union contains the point, so the whole group
|
|
246
|
-
// area is hoverable/selectable, not just where a member currently sits.
|
|
247
|
-
const groupHit = findGroupAtPoint(doc, localPointer.x, localPointer.y);
|
|
248
|
-
if (
|
|
249
|
-
groupHit &&
|
|
250
|
-
!hasAuthorPointerEventsNone(groupHit) &&
|
|
251
|
-
getDomLayerPatchTarget(groupHit, activeCompositionPath)
|
|
252
|
-
)
|
|
253
|
-
return groupHit;
|
|
254
|
-
|
|
255
|
-
const fallback = getEventTargetElement(doc.elementFromPoint(localPointer.x, localPointer.y));
|
|
256
|
-
if (!fallback || !getDomLayerPatchTarget(fallback, activeCompositionPath)) return null;
|
|
257
|
-
if (hasAuthorPointerEventsNone(fallback)) return null;
|
|
258
|
-
if (!isElementComputedVisible(fallback)) return null;
|
|
259
|
-
if (isFullBleedTarget(fallback, localPointer.viewport)) return null;
|
|
260
|
-
return fallback;
|
|
261
|
-
} finally {
|
|
262
|
-
removePointerEventsOverride(overrideStyle);
|
|
203
|
+
// Hit-test without touching the preview document: any style added here, even
|
|
204
|
+
// briefly, restyles every element in the film on each click and hover.
|
|
205
|
+
if (typeof doc.elementsFromPoint === "function") {
|
|
206
|
+
const elements = doc.elementsFromPoint(localPointer.x, localPointer.y);
|
|
207
|
+
const candidates = filterAuthorInteractiveTargets(elements, activeCompositionPath);
|
|
208
|
+
const visualTarget =
|
|
209
|
+
candidates.find((el) => !isFullBleedTarget(el, localPointer.viewport)) ?? null;
|
|
210
|
+
if (visualTarget) return visualTarget;
|
|
263
211
|
}
|
|
212
|
+
|
|
213
|
+
// No element hit (e.g. empty space inside an animated group's overlay) — fall
|
|
214
|
+
// back to the group whose member-union contains the point, so the whole group
|
|
215
|
+
// area is hoverable/selectable, not just where a member currently sits.
|
|
216
|
+
const groupHit = findGroupAtPoint(doc, localPointer.x, localPointer.y);
|
|
217
|
+
if (
|
|
218
|
+
groupHit &&
|
|
219
|
+
!hasAuthorPointerEventsNone(groupHit) &&
|
|
220
|
+
getDomLayerPatchTarget(groupHit, activeCompositionPath)
|
|
221
|
+
)
|
|
222
|
+
return groupHit;
|
|
223
|
+
|
|
224
|
+
const fallback = getEventTargetElement(doc.elementFromPoint(localPointer.x, localPointer.y));
|
|
225
|
+
if (!fallback || !getDomLayerPatchTarget(fallback, activeCompositionPath)) return null;
|
|
226
|
+
if (hasAuthorPointerEventsNone(fallback)) return null;
|
|
227
|
+
if (!isElementComputedVisible(fallback)) return null;
|
|
228
|
+
if (isFullBleedTarget(fallback, localPointer.viewport)) return null;
|
|
229
|
+
return fallback;
|
|
264
230
|
}
|
|
265
231
|
|
|
266
232
|
/** Returns all independently-selectable elements at the pointer (topmost first). */
|
|
@@ -283,27 +249,18 @@ export function getAllPreviewTargetsFromPointer(
|
|
|
283
249
|
const localPointer = resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY);
|
|
284
250
|
if (!localPointer) return [];
|
|
285
251
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
overrideStyle = null;
|
|
292
|
-
return filterAuthorInteractiveTargets(elements, activeCompositionPath).filter(
|
|
293
|
-
(el) => !isFullBleedTarget(el, localPointer.viewport),
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
const fallback = getEventTargetElement(doc.elementFromPoint(localPointer.x, localPointer.y));
|
|
297
|
-
if (!fallback || !getDomLayerPatchTarget(fallback, activeCompositionPath)) return [];
|
|
298
|
-
removePointerEventsOverride(overrideStyle);
|
|
299
|
-
overrideStyle = null;
|
|
300
|
-
if (hasAuthorPointerEventsNone(fallback)) return [];
|
|
301
|
-
if (!isElementComputedVisible(fallback)) return [];
|
|
302
|
-
if (isFullBleedTarget(fallback, localPointer.viewport)) return [];
|
|
303
|
-
return [fallback];
|
|
304
|
-
} finally {
|
|
305
|
-
removePointerEventsOverride(overrideStyle);
|
|
252
|
+
if (typeof doc.elementsFromPoint === "function") {
|
|
253
|
+
const elements = doc.elementsFromPoint(localPointer.x, localPointer.y);
|
|
254
|
+
return filterAuthorInteractiveTargets(elements, activeCompositionPath).filter(
|
|
255
|
+
(el) => !isFullBleedTarget(el, localPointer.viewport),
|
|
256
|
+
);
|
|
306
257
|
}
|
|
258
|
+
const fallback = getEventTargetElement(doc.elementFromPoint(localPointer.x, localPointer.y));
|
|
259
|
+
if (!fallback || !getDomLayerPatchTarget(fallback, activeCompositionPath)) return [];
|
|
260
|
+
if (hasAuthorPointerEventsNone(fallback)) return [];
|
|
261
|
+
if (!isElementComputedVisible(fallback)) return [];
|
|
262
|
+
if (isFullBleedTarget(fallback, localPointer.viewport)) return [];
|
|
263
|
+
return [fallback];
|
|
307
264
|
}
|
|
308
265
|
|
|
309
266
|
function objectLike(value: unknown): object | null {
|