@hyperframes/studio 0.8.177 → 0.8.179
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-BulZP0_1.js → hyperframes-player-C0SyMC21.js} +1 -1
- package/dist/assets/{index-CGncBUsJ.js → index-DHUassFP.js} +1 -1
- package/dist/assets/{index-B1Gypobp.js → index-ueMRyAJ2.js} +173 -172
- package/dist/{chunk-BLXESG6S.js → chunk-563JPIL4.js} +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +42 -53
- package/dist/player.js +1 -1
- package/package.json +8 -8
- package/src/hooks/previewPromotionSelection.test.tsx +5 -6
- package/src/hooks/useDomEditPreviewSync.test.ts +1 -5
- package/src/hooks/useDomEditPreviewSync.ts +9 -58
- package/src/hooks/useDomEditSession.ts +1 -5
- package/src/hooks/useDomEditWiring.ts +6 -16
- package/src/hooks/useDomSelection.test.ts +62 -0
- package/src/hooks/useDomSelection.ts +5 -4
- package/src/hooks/useDomSelectionTypes.ts +4 -2
- package/src/hooks/useFileTree.test.tsx +34 -0
- package/src/hooks/useFileTree.ts +22 -4
- package/src/main.tsx +7 -24
- package/src/player/hooks/useTimelinePlayer.ts +2 -2
- package/src/player/sceneSwap.test.ts +10 -3
- package/src/player/sceneSwap.ts +3 -0
- package/src/utils/rejectionTelemetry.test.ts +39 -0
- package/src/utils/rejectionTelemetry.ts +33 -0
|
@@ -1010,6 +1010,7 @@ function markScenesStale(iframe, files) {
|
|
|
1010
1010
|
}
|
|
1011
1011
|
meta.content = JSON.stringify(parts);
|
|
1012
1012
|
}
|
|
1013
|
+
var supersededByNewerEdit = () => new DOMException("superseded by a newer edit", "AbortError");
|
|
1013
1014
|
function sceneSwapFor(iframe) {
|
|
1014
1015
|
const win = iframe.contentWindow;
|
|
1015
1016
|
const swap = win?.__hfSwapScenes;
|
|
@@ -1535,6 +1536,7 @@ export {
|
|
|
1535
1536
|
announcePreviewPromoted,
|
|
1536
1537
|
onPreviewPromoted,
|
|
1537
1538
|
markScenesStale,
|
|
1539
|
+
supersededByNewerEdit,
|
|
1538
1540
|
sceneSwapFor,
|
|
1539
1541
|
Player
|
|
1540
1542
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -3463,7 +3463,7 @@ declare function useDomEditSession({ projectId, activeCompPath, compIdToSrc, cap
|
|
|
3463
3463
|
handlePreviewCanvasPointerLeave: () => void;
|
|
3464
3464
|
applyDomSelection: (selection: DomEditSelection | null, options?: ApplyDomSelectionOptions) => void;
|
|
3465
3465
|
clearDomSelection: () => void;
|
|
3466
|
-
refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => Promise<void>;
|
|
3466
|
+
refreshDomEditSelectionFromPreview: (selection: DomEditSelection, iframe?: HTMLIFrameElement | null) => Promise<void>;
|
|
3467
3467
|
handleDomStyleCommit: (property: string, value: string) => Promise<DomEditCommitOutcome>;
|
|
3468
3468
|
handleDomStyleCommitForSelection: (selection: DomEditSelection, propertyOrStylesAsOnePatch: string | Record<string, string>, value?: string) => Promise<DomEditCommitOutcome>;
|
|
3469
3469
|
handleDomAttributeCommit: (attr: string, value: string) => Promise<void>;
|
package/dist/index.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
document.documentElement.dataset.theme = "paper";
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-ueMRyAJ2.js"></script>
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/index-CESvLzz0.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
sceneSwapFor,
|
|
40
40
|
secondsToFrame,
|
|
41
41
|
stepFrameTime,
|
|
42
|
+
supersededByNewerEdit,
|
|
42
43
|
thumbnailRevisionOf,
|
|
43
44
|
timelineSliderToZoomPercent,
|
|
44
45
|
timelineZoomPercentToSlider,
|
|
@@ -47,7 +48,7 @@ import {
|
|
|
47
48
|
usePreviewVariablesStore,
|
|
48
49
|
whenPreviewBooted,
|
|
49
50
|
writeStudioUiPreferences
|
|
50
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-563JPIL4.js";
|
|
51
52
|
import {
|
|
52
53
|
Button,
|
|
53
54
|
ContextMenu,
|
|
@@ -32628,7 +32629,7 @@ function useTimelinePlayer({
|
|
|
32628
32629
|
const refreshPlayer = useCallback41(() => {
|
|
32629
32630
|
const iframe = iframeRef.current;
|
|
32630
32631
|
if (!iframe) return;
|
|
32631
|
-
swapCancelRef.current?.abort(
|
|
32632
|
+
swapCancelRef.current?.abort(supersededByNewerEdit());
|
|
32632
32633
|
const cancel = new AbortController();
|
|
32633
32634
|
swapCancelRef.current = cancel;
|
|
32634
32635
|
logReload("refreshPlayer", () => ({ stack: new Error("refreshPlayer").stack }));
|
|
@@ -75094,8 +75095,31 @@ import { useState as useState117, useCallback as useCallback104, useRef as useRe
|
|
|
75094
75095
|
|
|
75095
75096
|
// src/hooks/useFileTree.ts
|
|
75096
75097
|
import { useState as useState116, useCallback as useCallback101, useEffect as useEffect112, useMemo as useMemo71, useRef as useRef128 } from "react";
|
|
75098
|
+
|
|
75099
|
+
// src/utils/rejectionTelemetry.ts
|
|
75100
|
+
function errorProps(value) {
|
|
75101
|
+
if (value instanceof Error) {
|
|
75102
|
+
return {
|
|
75103
|
+
error_message: value.message,
|
|
75104
|
+
error_name: value.name,
|
|
75105
|
+
stack_trace: value.stack?.slice(0, 4e3) ?? null
|
|
75106
|
+
};
|
|
75107
|
+
}
|
|
75108
|
+
return { error_message: String(value), error_name: null, stack_trace: null };
|
|
75109
|
+
}
|
|
75110
|
+
|
|
75111
|
+
// src/hooks/useFileTree.ts
|
|
75097
75112
|
import { encodeUrlPath as encodeUrlPath4 } from "@hyperframes/parsers";
|
|
75098
75113
|
var EMPTY_FILE_LIST = [];
|
|
75114
|
+
async function readTreeListing(projectId, signal) {
|
|
75115
|
+
const res = await studioApiFetch(buildProjectApiPath(projectId), { signal });
|
|
75116
|
+
if (!res.ok) throw new Error(`tree fetch failed: ${res.status}`);
|
|
75117
|
+
return res.json();
|
|
75118
|
+
}
|
|
75119
|
+
function reportRefreshFailure(err) {
|
|
75120
|
+
const { error_name, error_message } = errorProps(err);
|
|
75121
|
+
trackStudioEvent("file_tree_refresh_failed", { error_name, error_message });
|
|
75122
|
+
}
|
|
75099
75123
|
function useFileTree({ projectId, projectIdRef }) {
|
|
75100
75124
|
const [fetched, setFetched] = useState116(null);
|
|
75101
75125
|
const current = fetched?.projectId === projectId ? fetched : null;
|
|
@@ -75131,6 +75155,7 @@ function useFileTree({ projectId, projectIdRef }) {
|
|
|
75131
75155
|
}, [projectId]);
|
|
75132
75156
|
const refreshRequestRef = useRef128(0);
|
|
75133
75157
|
const refreshAbortRef = useRef128(null);
|
|
75158
|
+
const failureReportedRef = useRef128(false);
|
|
75134
75159
|
const refreshFileTree = useCallback101(async () => {
|
|
75135
75160
|
const pid = projectIdRef.current;
|
|
75136
75161
|
if (!pid) return;
|
|
@@ -75140,12 +75165,14 @@ function useFileTree({ projectId, projectIdRef }) {
|
|
|
75140
75165
|
refreshAbortRef.current = controller;
|
|
75141
75166
|
let data;
|
|
75142
75167
|
try {
|
|
75143
|
-
|
|
75144
|
-
data = await res.json();
|
|
75168
|
+
data = await readTreeListing(pid, controller.signal);
|
|
75145
75169
|
} catch (err) {
|
|
75146
75170
|
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
75147
|
-
|
|
75171
|
+
if (!failureReportedRef.current) reportRefreshFailure(err);
|
|
75172
|
+
failureReportedRef.current = true;
|
|
75173
|
+
return;
|
|
75148
75174
|
}
|
|
75175
|
+
failureReportedRef.current = false;
|
|
75149
75176
|
if (data.files && requestId === refreshRequestRef.current) {
|
|
75150
75177
|
setFetched(
|
|
75151
75178
|
(prev) => prev?.projectId === pid ? {
|
|
@@ -79921,8 +79948,9 @@ function useDomSelection({
|
|
|
79921
79948
|
});
|
|
79922
79949
|
const refreshDomEditSelectionFromPreview = useCallback125(
|
|
79923
79950
|
// fallow-ignore-next-line complexity
|
|
79924
|
-
async (selection) => {
|
|
79925
|
-
const
|
|
79951
|
+
async (selection, iframe = previewIframeRef.current) => {
|
|
79952
|
+
const stillSelected = () => domEditSelectionsTargetSame(domEditSelectionRef.current, selection);
|
|
79953
|
+
if (!stillSelected()) return;
|
|
79926
79954
|
let doc = null;
|
|
79927
79955
|
try {
|
|
79928
79956
|
doc = iframe?.contentDocument ?? null;
|
|
@@ -79945,7 +79973,7 @@ function useDomSelection({
|
|
|
79945
79973
|
return;
|
|
79946
79974
|
}
|
|
79947
79975
|
const nextSelection = await buildDomSelectionFromTarget(element, { previous: selection });
|
|
79948
|
-
if (nextSelection) {
|
|
79976
|
+
if (nextSelection && stillSelected()) {
|
|
79949
79977
|
applyDomSelection(nextSelection, {
|
|
79950
79978
|
revealPanel: false,
|
|
79951
79979
|
preserveGroup: true
|
|
@@ -80073,7 +80101,6 @@ function useDomSelection({
|
|
|
80073
80101
|
buildDomSelectionForTimelineElement,
|
|
80074
80102
|
handleTimelineElementSelect,
|
|
80075
80103
|
refreshDomEditSelectionFromPreview,
|
|
80076
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
80077
80104
|
applyMarqueeSelection
|
|
80078
80105
|
};
|
|
80079
80106
|
}
|
|
@@ -83631,14 +83658,10 @@ import { useCallback as useCallback142, useEffect as useEffect119, useRef as use
|
|
|
83631
83658
|
import { useEffect as useEffect118, useRef as useRef149 } from "react";
|
|
83632
83659
|
function useDomEditPreviewSync({
|
|
83633
83660
|
previewIframe,
|
|
83634
|
-
activeCompPath,
|
|
83635
83661
|
captionEditMode,
|
|
83636
83662
|
domEditSelectionRef,
|
|
83637
|
-
domEditGroupSelectionsRef,
|
|
83638
83663
|
domEditSelection,
|
|
83639
|
-
|
|
83640
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
83641
|
-
buildDomSelectionFromTarget,
|
|
83664
|
+
refreshDomEditSelectionFromPreview,
|
|
83642
83665
|
refreshPreviewDocumentVersion,
|
|
83643
83666
|
syncPreviewHotkeys,
|
|
83644
83667
|
applyStudioManualEditsToPreviewRef,
|
|
@@ -83659,26 +83682,7 @@ function useDomEditPreviewSync({
|
|
|
83659
83682
|
}
|
|
83660
83683
|
if (!doc) return;
|
|
83661
83684
|
reapplyPositionEditsAfterSeek(doc);
|
|
83662
|
-
|
|
83663
|
-
if (!nextElement) {
|
|
83664
|
-
const group = domEditGroupSelectionsRef.current;
|
|
83665
|
-
logSelect("preview-sync-lost", {
|
|
83666
|
-
target: currentSelection.selector ?? currentSelection.id ?? null,
|
|
83667
|
-
group: group.length
|
|
83668
|
-
});
|
|
83669
|
-
if (group.length > 1) {
|
|
83670
|
-
await refreshDomEditGroupSelectionsFromPreview(group);
|
|
83671
|
-
return;
|
|
83672
|
-
}
|
|
83673
|
-
applyDomSelection(null, { revealPanel: false });
|
|
83674
|
-
return;
|
|
83675
|
-
}
|
|
83676
|
-
const nextSelection = await buildDomSelectionFromTarget(nextElement, {
|
|
83677
|
-
previous: currentSelection
|
|
83678
|
-
});
|
|
83679
|
-
if (nextSelection) {
|
|
83680
|
-
applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
|
|
83681
|
-
}
|
|
83685
|
+
await refreshDomEditSelectionFromPreview(currentSelection, previewIframe);
|
|
83682
83686
|
};
|
|
83683
83687
|
syncPreviewHotkeys(previewIframe);
|
|
83684
83688
|
void applyStudioManualEditsToPreviewRef.current(previewIframe);
|
|
@@ -83692,14 +83696,10 @@ function useDomEditPreviewSync({
|
|
|
83692
83696
|
};
|
|
83693
83697
|
return onPreviewContentReplaced(previewIframe, handleLoad);
|
|
83694
83698
|
}, [
|
|
83695
|
-
activeCompPath,
|
|
83696
|
-
applyDomSelection,
|
|
83697
|
-
buildDomSelectionFromTarget,
|
|
83698
83699
|
captionEditMode,
|
|
83699
|
-
domEditGroupSelectionsRef,
|
|
83700
83700
|
domEditSelectionRef,
|
|
83701
83701
|
previewIframe,
|
|
83702
|
-
|
|
83702
|
+
refreshDomEditSelectionFromPreview,
|
|
83703
83703
|
refreshPreviewDocumentVersion,
|
|
83704
83704
|
syncPreviewHotkeys,
|
|
83705
83705
|
applyStudioManualEditsToPreviewRef,
|
|
@@ -84079,8 +84079,7 @@ function useDomEditWiring(params) {
|
|
|
84079
84079
|
activeCompPath,
|
|
84080
84080
|
domEditSelection,
|
|
84081
84081
|
domEditSelectionRef,
|
|
84082
|
-
|
|
84083
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
84082
|
+
refreshDomEditSelectionFromPreview,
|
|
84084
84083
|
previewIframeRef,
|
|
84085
84084
|
previewIframe,
|
|
84086
84085
|
captionEditMode,
|
|
@@ -84091,8 +84090,6 @@ function useDomEditWiring(params) {
|
|
|
84091
84090
|
refreshPreviewDocumentVersion,
|
|
84092
84091
|
syncPreviewHotkeys,
|
|
84093
84092
|
applyStudioManualEditsToPreviewRef,
|
|
84094
|
-
applyDomSelection,
|
|
84095
|
-
buildDomSelectionFromTarget,
|
|
84096
84093
|
openSourceForSelection,
|
|
84097
84094
|
updateGsapProperty,
|
|
84098
84095
|
updateGsapMeta,
|
|
@@ -84189,14 +84186,10 @@ function useDomEditWiring(params) {
|
|
|
84189
84186
|
});
|
|
84190
84187
|
useDomEditPreviewSync({
|
|
84191
84188
|
previewIframe,
|
|
84192
|
-
activeCompPath,
|
|
84193
84189
|
captionEditMode,
|
|
84194
84190
|
domEditSelectionRef,
|
|
84195
|
-
domEditGroupSelectionsRef,
|
|
84196
84191
|
domEditSelection,
|
|
84197
|
-
|
|
84198
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
84199
|
-
buildDomSelectionFromTarget,
|
|
84192
|
+
refreshDomEditSelectionFromPreview,
|
|
84200
84193
|
refreshPreviewDocumentVersion,
|
|
84201
84194
|
syncPreviewHotkeys,
|
|
84202
84195
|
applyStudioManualEditsToPreviewRef,
|
|
@@ -86204,7 +86197,6 @@ function useDomEditSession({
|
|
|
86204
86197
|
buildDomSelectionForTimelineElement,
|
|
86205
86198
|
handleTimelineElementSelect,
|
|
86206
86199
|
refreshDomEditSelectionFromPreview,
|
|
86207
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
86208
86200
|
applyMarqueeSelection
|
|
86209
86201
|
} = useDomSelection({
|
|
86210
86202
|
projectId,
|
|
@@ -86444,8 +86436,7 @@ function useDomEditSession({
|
|
|
86444
86436
|
activeCompPath,
|
|
86445
86437
|
domEditSelection,
|
|
86446
86438
|
domEditSelectionRef,
|
|
86447
|
-
|
|
86448
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
86439
|
+
refreshDomEditSelectionFromPreview,
|
|
86449
86440
|
previewIframeRef,
|
|
86450
86441
|
previewIframe,
|
|
86451
86442
|
captionEditMode: previewCaptionEditMode,
|
|
@@ -86456,8 +86447,6 @@ function useDomEditSession({
|
|
|
86456
86447
|
refreshPreviewDocumentVersion,
|
|
86457
86448
|
syncPreviewHotkeys,
|
|
86458
86449
|
applyStudioManualEditsToPreviewRef,
|
|
86459
|
-
applyDomSelection,
|
|
86460
|
-
buildDomSelectionFromTarget,
|
|
86461
86450
|
openSourceForSelection,
|
|
86462
86451
|
...gsapScriptEdits,
|
|
86463
86452
|
handleDomManualEditsReset
|
package/dist/player.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.179",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"dockview-react": "8.3.1",
|
|
60
60
|
"gsap": "^3.13.0",
|
|
61
61
|
"mediabunny": "^1.45.3",
|
|
62
|
-
"@hyperframes/
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/shader-transitions": "0.8.
|
|
67
|
-
"@hyperframes/
|
|
62
|
+
"@hyperframes/core": "0.8.179",
|
|
63
|
+
"@hyperframes/studio-server": "0.8.179",
|
|
64
|
+
"@hyperframes/sdk": "0.8.179",
|
|
65
|
+
"@hyperframes/player": "0.8.179",
|
|
66
|
+
"@hyperframes/shader-transitions": "0.8.179",
|
|
67
|
+
"@hyperframes/parsers": "0.8.179"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.179"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import React, { act } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import type
|
|
5
|
+
import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
|
|
6
6
|
import { resolveElementForOverlay } from "../components/editor/domEditOverlayGeometry";
|
|
7
7
|
import {
|
|
8
8
|
makePreview,
|
|
@@ -44,14 +44,13 @@ function mountHost(followPromotions: boolean) {
|
|
|
44
44
|
const live = useLivePreviewIframe(host);
|
|
45
45
|
useDomEditPreviewSync({
|
|
46
46
|
previewIframe: followPromotions ? live : host,
|
|
47
|
-
activeCompPath: null,
|
|
48
47
|
captionEditMode: false,
|
|
49
48
|
domEditSelectionRef: selectionRef,
|
|
50
|
-
domEditGroupSelectionsRef: { current: [] },
|
|
51
49
|
domEditSelection: selectionRef.current,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
refreshDomEditSelectionFromPreview: async (selection, iframe) => {
|
|
51
|
+
const element = findElementForSelection(iframe.contentDocument!, selection, null);
|
|
52
|
+
selectionRef.current = element ? selectionOf(element) : null;
|
|
53
|
+
},
|
|
55
54
|
refreshPreviewDocumentVersion: () => {},
|
|
56
55
|
syncPreviewHotkeys: () => {},
|
|
57
56
|
applyStudioManualEditsToPreviewRef: { current: async () => {} },
|
|
@@ -25,14 +25,10 @@ describe("useDomEditPreviewSync", () => {
|
|
|
25
25
|
function Harness() {
|
|
26
26
|
useDomEditPreviewSync({
|
|
27
27
|
previewIframe: iframe,
|
|
28
|
-
activeCompPath: null,
|
|
29
28
|
captionEditMode: false,
|
|
30
29
|
domEditSelectionRef: { current: null },
|
|
31
|
-
domEditGroupSelectionsRef: { current: [] },
|
|
32
30
|
domEditSelection: null,
|
|
33
|
-
|
|
34
|
-
applyDomSelection: () => {},
|
|
35
|
-
buildDomSelectionFromTarget: async () => null,
|
|
31
|
+
refreshDomEditSelectionFromPreview: async () => {},
|
|
36
32
|
refreshPreviewDocumentVersion: refreshVersion,
|
|
37
33
|
syncPreviewHotkeys: () => {},
|
|
38
34
|
applyStudioManualEditsToPreviewRef: { current: applyManualEdits },
|
|
@@ -4,31 +4,22 @@
|
|
|
4
4
|
* Extracted from useDomEditSession to keep file sizes under the 600-line limit.
|
|
5
5
|
*/
|
|
6
6
|
import { useEffect, useRef } from "react";
|
|
7
|
-
import {
|
|
7
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
8
8
|
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
|
|
9
9
|
import { useDockLayoutStore } from "../components/dock/dockLayoutStore";
|
|
10
10
|
import type { PatchTarget } from "../utils/sourcePatcher";
|
|
11
|
-
import { logSelect } from "../utils/selectDebug";
|
|
12
11
|
import { onPreviewContentReplaced } from "../player/sceneSwap";
|
|
13
|
-
import type { ResolveDomSelectionOptions } from "./useDomSelectionTypes";
|
|
14
12
|
|
|
15
13
|
interface UseDomEditPreviewSyncParams {
|
|
16
14
|
previewIframe: HTMLIFrameElement | null;
|
|
17
|
-
activeCompPath: string | null;
|
|
18
15
|
captionEditMode: boolean;
|
|
19
16
|
domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
|
|
20
|
-
domEditGroupSelectionsRef: React.MutableRefObject<DomEditSelection[]>;
|
|
21
17
|
domEditSelection: DomEditSelection | null;
|
|
22
|
-
/** Re-resolves
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
) => void;
|
|
28
|
-
buildDomSelectionFromTarget: (
|
|
29
|
-
element: HTMLElement,
|
|
30
|
-
options?: Pick<ResolveDomSelectionOptions, "previous">,
|
|
31
|
-
) => Promise<DomEditSelection | null>;
|
|
18
|
+
/** Re-resolves the selection against the given preview, unless it was deselected meanwhile. */
|
|
19
|
+
refreshDomEditSelectionFromPreview: (
|
|
20
|
+
selection: DomEditSelection,
|
|
21
|
+
iframe: HTMLIFrameElement,
|
|
22
|
+
) => Promise<void>;
|
|
32
23
|
refreshPreviewDocumentVersion: () => void;
|
|
33
24
|
syncPreviewHotkeys: (iframe: HTMLIFrameElement | null) => void;
|
|
34
25
|
applyStudioManualEditsToPreviewRef: React.MutableRefObject<
|
|
@@ -40,14 +31,10 @@ interface UseDomEditPreviewSyncParams {
|
|
|
40
31
|
|
|
41
32
|
export function useDomEditPreviewSync({
|
|
42
33
|
previewIframe,
|
|
43
|
-
activeCompPath,
|
|
44
34
|
captionEditMode,
|
|
45
35
|
domEditSelectionRef,
|
|
46
|
-
domEditGroupSelectionsRef,
|
|
47
36
|
domEditSelection,
|
|
48
|
-
|
|
49
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
50
|
-
buildDomSelectionFromTarget,
|
|
37
|
+
refreshDomEditSelectionFromPreview,
|
|
51
38
|
refreshPreviewDocumentVersion,
|
|
52
39
|
syncPreviewHotkeys,
|
|
53
40
|
applyStudioManualEditsToPreviewRef,
|
|
@@ -73,39 +60,7 @@ export function useDomEditPreviewSync({
|
|
|
73
60
|
if (!doc) return;
|
|
74
61
|
|
|
75
62
|
reapplyPositionEditsAfterSeek(doc);
|
|
76
|
-
|
|
77
|
-
const nextElement = findElementForSelection(doc, currentSelection, activeCompPath);
|
|
78
|
-
if (!nextElement) {
|
|
79
|
-
// The selected element no longer resolves in the (re-synced) document
|
|
80
|
-
// — comp/hot reload, activeCompPath swap, or post-save replacement.
|
|
81
|
-
// Clear so overlay geometry isn't computed on a stale, detached node.
|
|
82
|
-
// (Drag-release-in-gray-zone is handled separately by
|
|
83
|
-
// suppressNextBoxClickRef; the dragged element still resolves here.)
|
|
84
|
-
//
|
|
85
|
-
// One lost member is not the whole selection though. A multi-select that
|
|
86
|
-
// loses its primary here used to be wiped entirely, so moving a group and
|
|
87
|
-
// having any one of its elements fail to re-resolve deselected all of
|
|
88
|
-
// them. Re-resolve the group instead and keep whoever survived; it only
|
|
89
|
-
// clears when nobody did.
|
|
90
|
-
const group = domEditGroupSelectionsRef.current;
|
|
91
|
-
logSelect("preview-sync-lost", {
|
|
92
|
-
target: currentSelection.selector ?? currentSelection.id ?? null,
|
|
93
|
-
group: group.length,
|
|
94
|
-
});
|
|
95
|
-
if (group.length > 1) {
|
|
96
|
-
await refreshDomEditGroupSelectionsFromPreview(group);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
applyDomSelection(null, { revealPanel: false });
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const nextSelection = await buildDomSelectionFromTarget(nextElement, {
|
|
104
|
-
previous: currentSelection,
|
|
105
|
-
});
|
|
106
|
-
if (nextSelection) {
|
|
107
|
-
applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
|
|
108
|
-
}
|
|
63
|
+
await refreshDomEditSelectionFromPreview(currentSelection, previewIframe);
|
|
109
64
|
};
|
|
110
65
|
|
|
111
66
|
syncPreviewHotkeys(previewIframe);
|
|
@@ -122,14 +77,10 @@ export function useDomEditPreviewSync({
|
|
|
122
77
|
|
|
123
78
|
return onPreviewContentReplaced(previewIframe, handleLoad);
|
|
124
79
|
}, [
|
|
125
|
-
activeCompPath,
|
|
126
|
-
applyDomSelection,
|
|
127
|
-
buildDomSelectionFromTarget,
|
|
128
80
|
captionEditMode,
|
|
129
|
-
domEditGroupSelectionsRef,
|
|
130
81
|
domEditSelectionRef,
|
|
131
82
|
previewIframe,
|
|
132
|
-
|
|
83
|
+
refreshDomEditSelectionFromPreview,
|
|
133
84
|
refreshPreviewDocumentVersion,
|
|
134
85
|
syncPreviewHotkeys,
|
|
135
86
|
applyStudioManualEditsToPreviewRef,
|
|
@@ -122,7 +122,6 @@ export function useDomEditSession({
|
|
|
122
122
|
buildDomSelectionForTimelineElement,
|
|
123
123
|
handleTimelineElementSelect,
|
|
124
124
|
refreshDomEditSelectionFromPreview,
|
|
125
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
126
125
|
applyMarqueeSelection,
|
|
127
126
|
} = useDomSelection({
|
|
128
127
|
projectId,
|
|
@@ -394,8 +393,7 @@ export function useDomEditSession({
|
|
|
394
393
|
activeCompPath,
|
|
395
394
|
domEditSelection,
|
|
396
395
|
domEditSelectionRef,
|
|
397
|
-
|
|
398
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
396
|
+
refreshDomEditSelectionFromPreview,
|
|
399
397
|
previewIframeRef,
|
|
400
398
|
previewIframe,
|
|
401
399
|
captionEditMode: previewCaptionEditMode,
|
|
@@ -406,8 +404,6 @@ export function useDomEditSession({
|
|
|
406
404
|
refreshPreviewDocumentVersion,
|
|
407
405
|
syncPreviewHotkeys,
|
|
408
406
|
applyStudioManualEditsToPreviewRef,
|
|
409
|
-
applyDomSelection,
|
|
410
|
-
buildDomSelectionFromTarget,
|
|
411
407
|
openSourceForSelection,
|
|
412
408
|
...gsapScriptEdits,
|
|
413
409
|
handleDomManualEditsReset,
|
|
@@ -23,8 +23,10 @@ export interface UseDomEditWiringParams {
|
|
|
23
23
|
activeCompPath: string | null;
|
|
24
24
|
domEditSelection: DomEditSelection | null;
|
|
25
25
|
domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
refreshDomEditSelectionFromPreview: (
|
|
27
|
+
selection: DomEditSelection,
|
|
28
|
+
iframe: HTMLIFrameElement,
|
|
29
|
+
) => Promise<void>;
|
|
28
30
|
previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
29
31
|
previewIframe: HTMLIFrameElement | null;
|
|
30
32
|
captionEditMode: boolean;
|
|
@@ -37,11 +39,6 @@ export interface UseDomEditWiringParams {
|
|
|
37
39
|
applyStudioManualEditsToPreviewRef: React.MutableRefObject<
|
|
38
40
|
(iframe: HTMLIFrameElement) => Promise<void>
|
|
39
41
|
>;
|
|
40
|
-
applyDomSelection: (
|
|
41
|
-
selection: DomEditSelection | null,
|
|
42
|
-
options?: { revealPanel?: boolean; preserveGroup?: boolean },
|
|
43
|
-
) => void;
|
|
44
|
-
buildDomSelectionFromTarget: (element: HTMLElement) => Promise<DomEditSelection | null>;
|
|
45
42
|
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
|
|
46
43
|
// GSAP script commit ops (from useGsapScriptCommits)
|
|
47
44
|
updateGsapProperty: (
|
|
@@ -117,8 +114,7 @@ export function useDomEditWiring(params: UseDomEditWiringParams) {
|
|
|
117
114
|
activeCompPath,
|
|
118
115
|
domEditSelection,
|
|
119
116
|
domEditSelectionRef,
|
|
120
|
-
|
|
121
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
117
|
+
refreshDomEditSelectionFromPreview,
|
|
122
118
|
previewIframeRef,
|
|
123
119
|
previewIframe,
|
|
124
120
|
captionEditMode,
|
|
@@ -129,8 +125,6 @@ export function useDomEditWiring(params: UseDomEditWiringParams) {
|
|
|
129
125
|
refreshPreviewDocumentVersion,
|
|
130
126
|
syncPreviewHotkeys,
|
|
131
127
|
applyStudioManualEditsToPreviewRef,
|
|
132
|
-
applyDomSelection,
|
|
133
|
-
buildDomSelectionFromTarget,
|
|
134
128
|
openSourceForSelection,
|
|
135
129
|
updateGsapProperty,
|
|
136
130
|
updateGsapMeta,
|
|
@@ -251,14 +245,10 @@ export function useDomEditWiring(params: UseDomEditWiringParams) {
|
|
|
251
245
|
|
|
252
246
|
useDomEditPreviewSync({
|
|
253
247
|
previewIframe,
|
|
254
|
-
activeCompPath,
|
|
255
248
|
captionEditMode,
|
|
256
249
|
domEditSelectionRef,
|
|
257
|
-
domEditGroupSelectionsRef,
|
|
258
250
|
domEditSelection,
|
|
259
|
-
|
|
260
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
261
|
-
buildDomSelectionFromTarget,
|
|
251
|
+
refreshDomEditSelectionFromPreview,
|
|
262
252
|
refreshPreviewDocumentVersion,
|
|
263
253
|
syncPreviewHotkeys,
|
|
264
254
|
applyStudioManualEditsToPreviewRef,
|
|
@@ -326,6 +326,68 @@ describe("useDomSelection", () => {
|
|
|
326
326
|
harness.cleanup();
|
|
327
327
|
});
|
|
328
328
|
|
|
329
|
+
describe("a save's resync after the user moved on", () => {
|
|
330
|
+
function selectHeadlineInPreview() {
|
|
331
|
+
const doc = document.implementation.createHTMLDocument("");
|
|
332
|
+
const element = doc.body.appendChild(
|
|
333
|
+
Object.assign(doc.createElement("div"), { id: "headline" }),
|
|
334
|
+
);
|
|
335
|
+
const headline = makeSelection("Headline", element);
|
|
336
|
+
const harness = renderHarness(
|
|
337
|
+
{ activeCompPath: "index.html", projectId: "project-1", refreshKey: 0 },
|
|
338
|
+
{ previewDoc: doc },
|
|
339
|
+
);
|
|
340
|
+
act(() => harness.current().applyDomSelection(headline));
|
|
341
|
+
return { headline, harness };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
it("keeps a deselect made while the save was in flight", async () => {
|
|
345
|
+
const { headline, harness } = selectHeadlineInPreview();
|
|
346
|
+
// Escape after a crop's release, before its save returns.
|
|
347
|
+
act(() => harness.current().applyDomSelection(null, { revealPanel: false }));
|
|
348
|
+
|
|
349
|
+
await act(() => harness.current().refreshDomEditSelectionFromPreview(headline));
|
|
350
|
+
|
|
351
|
+
expect(harness.current().domEditSelection).toBeNull();
|
|
352
|
+
harness.cleanup();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it.each([
|
|
356
|
+
["still in", false],
|
|
357
|
+
["gone from", true],
|
|
358
|
+
])(
|
|
359
|
+
"keeps a different pick made while the save was in flight, the saved element %s the preview",
|
|
360
|
+
async (_, removed) => {
|
|
361
|
+
const { headline, harness } = selectHeadlineInPreview();
|
|
362
|
+
const card = makeSelection(
|
|
363
|
+
"Card",
|
|
364
|
+
Object.assign(document.createElement("div"), { id: "card" }),
|
|
365
|
+
);
|
|
366
|
+
act(() => harness.current().applyDomSelection(card));
|
|
367
|
+
if (removed) headline.element.remove();
|
|
368
|
+
|
|
369
|
+
await act(() => harness.current().refreshDomEditSelectionFromPreview(headline));
|
|
370
|
+
|
|
371
|
+
expect(harness.current().domEditSelection).toBe(card);
|
|
372
|
+
harness.cleanup();
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
it("keeps a deselect made while the resync was rebuilding the selection", async () => {
|
|
377
|
+
const { headline, harness } = selectHeadlineInPreview();
|
|
378
|
+
let resync!: Promise<void>;
|
|
379
|
+
act(() => {
|
|
380
|
+
resync = harness.current().refreshDomEditSelectionFromPreview(headline);
|
|
381
|
+
});
|
|
382
|
+
act(() => harness.current().applyDomSelection(null, { revealPanel: false }));
|
|
383
|
+
|
|
384
|
+
await act(() => resync);
|
|
385
|
+
|
|
386
|
+
expect(harness.current().domEditSelection).toBeNull();
|
|
387
|
+
harness.cleanup();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
329
391
|
it("clears a committed selection when the active composition path changes", () => {
|
|
330
392
|
const { selection, harness } = setupSelectedHarness();
|
|
331
393
|
expect(harness.current().domEditSelection).toBe(selection);
|
|
@@ -356,8 +356,10 @@ export function useDomSelection({
|
|
|
356
356
|
|
|
357
357
|
const refreshDomEditSelectionFromPreview = useCallback(
|
|
358
358
|
// fallow-ignore-next-line complexity
|
|
359
|
-
async (selection: DomEditSelection) => {
|
|
360
|
-
const
|
|
359
|
+
async (selection: DomEditSelection, iframe = previewIframeRef.current) => {
|
|
360
|
+
const stillSelected = () =>
|
|
361
|
+
domEditSelectionsTargetSame(domEditSelectionRef.current, selection);
|
|
362
|
+
if (!stillSelected()) return;
|
|
361
363
|
let doc: Document | null = null;
|
|
362
364
|
try {
|
|
363
365
|
doc = iframe?.contentDocument ?? null;
|
|
@@ -384,7 +386,7 @@ export function useDomSelection({
|
|
|
384
386
|
}
|
|
385
387
|
|
|
386
388
|
const nextSelection = await buildDomSelectionFromTarget(element, { previous: selection });
|
|
387
|
-
if (nextSelection) {
|
|
389
|
+
if (nextSelection && stillSelected()) {
|
|
388
390
|
applyDomSelection(nextSelection, {
|
|
389
391
|
revealPanel: false,
|
|
390
392
|
preserveGroup: true,
|
|
@@ -547,7 +549,6 @@ export function useDomSelection({
|
|
|
547
549
|
buildDomSelectionForTimelineElement,
|
|
548
550
|
handleTimelineElementSelect,
|
|
549
551
|
refreshDomEditSelectionFromPreview,
|
|
550
|
-
refreshDomEditGroupSelectionsFromPreview,
|
|
551
552
|
applyMarqueeSelection,
|
|
552
553
|
};
|
|
553
554
|
}
|
|
@@ -89,7 +89,9 @@ export interface UseDomSelectionReturn {
|
|
|
89
89
|
element: TimelineElement,
|
|
90
90
|
) => Promise<DomEditSelection | null>;
|
|
91
91
|
handleTimelineElementSelect: (element: TimelineElement | null) => Promise<void>;
|
|
92
|
-
refreshDomEditSelectionFromPreview: (
|
|
93
|
-
|
|
92
|
+
refreshDomEditSelectionFromPreview: (
|
|
93
|
+
selection: DomEditSelection,
|
|
94
|
+
iframe?: HTMLIFrameElement | null,
|
|
95
|
+
) => Promise<void>;
|
|
94
96
|
applyMarqueeSelection: (selections: DomEditSelection[], additive: boolean) => DomSelectionResult;
|
|
95
97
|
}
|