@hyperframes/studio 0.7.102 → 0.7.104
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-BTnkvhf3.js → hyperframes-player-Bj2HAymq.js} +1 -1
- package/dist/assets/{index-CnrSn9Ua.js → index-8UzO6PkK.js} +1 -1
- package/dist/assets/{index-DN614u1l.js → index-DzfDa1ci.js} +212 -212
- package/dist/assets/{index-B4MocVKM.js → index-TDfRaO1Y.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +779 -294
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +6 -2
- package/src/captions/hooks/useCaptionSync.ts +2 -1
- package/src/components/editor/DomEditOverlay.test.ts +89 -2
- package/src/components/editor/DomEditOverlay.tsx +40 -23
- package/src/components/editor/domEditOverlayGeometry.test.ts +94 -0
- package/src/components/editor/domEditOverlayGeometry.ts +59 -2
- package/src/components/editor/domEditOverlayGestures.ts +42 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +17 -0
- package/src/components/editor/groupDragMove.ts +110 -0
- package/src/components/editor/groupDropKeepsSelection.test.ts +70 -0
- package/src/components/editor/manualEditsDom.ts +15 -13
- package/src/components/editor/manualOffsetDrag.test.ts +92 -5
- package/src/components/editor/manualOffsetDrag.ts +22 -17
- package/src/components/editor/reapplyBoxSizeAfterSeek.test.ts +64 -0
- package/src/components/editor/snapEngageTravel.test.ts +65 -0
- package/src/components/editor/snapEngine.ts +21 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +40 -53
- package/src/components/editor/useDomEditOverlayRects.ts +4 -2
- package/src/hooks/domSelectionTimelineMirror.ts +73 -0
- package/src/hooks/gestureTransaction.test.ts +7 -12
- package/src/hooks/gestureTransaction.ts +7 -20
- package/src/hooks/gsapRuntimePatch.test.ts +43 -0
- package/src/hooks/gsapRuntimePatch.ts +11 -1
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +20 -0
- package/src/hooks/keyframeCacheAstLoad.test.ts +108 -0
- package/src/hooks/keyframeCacheAstLoad.ts +29 -1
- package/src/hooks/timelineTimingSync.ts +4 -1
- package/src/hooks/useDomEditCommits.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +2 -1
- package/src/hooks/useDomEditPreviewSync.ts +23 -0
- package/src/hooks/useDomEditSession.test.tsx +43 -62
- package/src/hooks/useDomEditSession.ts +10 -2
- package/src/hooks/useDomEditWiring.ts +6 -0
- package/src/hooks/useDomSelection.test.ts +133 -3
- package/src/hooks/useDomSelection.ts +67 -40
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +6 -2
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useExternalFileChangeCoordinator.ts +16 -3
- package/src/hooks/useFileManager.ts +2 -8
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAnimationFetchFallback.ts +6 -1
- package/src/hooks/useGsapAwareEditing.test.tsx +36 -0
- package/src/hooks/useGsapAwareEditing.ts +88 -15
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -5
- package/src/hooks/useGsapScriptCommits.test.tsx +193 -7
- package/src/hooks/useGsapScriptCommits.ts +52 -15
- package/src/hooks/useStudioUrlState.ts +130 -7
- package/src/hooks/useTimelineEditing.ts +2 -1
- package/src/hooks/useTimelineSelectionPreviewSync.ts +13 -0
- package/src/player/hooks/useTimelinePlayer.ts +2 -0
- package/src/utils/dragDebug.ts +98 -0
- package/src/utils/razorSplitTransaction.test.ts +15 -3
- package/src/utils/razorSplitTransaction.ts +2 -0
- package/src/utils/reloadDebug.ts +10 -0
- package/src/utils/resizeDebug.ts +19 -29
- package/src/utils/selectDebug.ts +9 -0
- package/src/utils/studioDebug.test.ts +32 -0
- package/src/utils/studioDebug.ts +30 -0
- package/src/utils/studioFileVersion.ts +16 -1
- package/src/utils/studioUrlState.test.ts +163 -1
- package/src/utils/studioUrlState.ts +60 -3
- package/src/utils/timelineCompositionInsert.ts +2 -1
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
readErrorResponseBody,
|
|
34
34
|
} from "./useDomEditCommitsHelpers";
|
|
35
35
|
import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
|
|
36
|
+
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
|
36
37
|
interface RecordEditInput {
|
|
37
38
|
label: string;
|
|
38
39
|
kind: EditHistoryKind;
|
|
@@ -201,7 +202,7 @@ export function useDomEditCommits({
|
|
|
201
202
|
`/api/projects/${pid}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
|
|
202
203
|
{
|
|
203
204
|
method: "POST",
|
|
204
|
-
headers: { "Content-Type": "application/json" },
|
|
205
|
+
headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
|
|
205
206
|
body: JSON.stringify(patchBody),
|
|
206
207
|
},
|
|
207
208
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StudioSaveHttpError, trackStudioSaveFailure } from "../utils/studioSaveDiagnostics";
|
|
2
2
|
import type { DomEditPatchBatch } from "./domEditCommitTypes";
|
|
3
3
|
import { formatFieldsSuffix } from "./gsapScriptCommitHelpers";
|
|
4
|
+
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
|
4
5
|
|
|
5
6
|
export function formatUnsafeFieldList(fields: Array<{ path: string }>): string {
|
|
6
7
|
return fields.map((field) => field.path).join(", ");
|
|
@@ -99,7 +100,7 @@ export async function patchElementBatches(projectId: string, batches: DomEditPat
|
|
|
99
100
|
`/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
|
|
100
101
|
{
|
|
101
102
|
method: "POST",
|
|
102
|
-
headers: { "Content-Type": "application/json" },
|
|
103
|
+
headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
|
|
103
104
|
body,
|
|
104
105
|
},
|
|
105
106
|
);
|
|
@@ -8,13 +8,17 @@ import { findElementForSelection, type DomEditSelection } from "../components/ed
|
|
|
8
8
|
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
|
|
9
9
|
import type { SidebarTab } from "../components/sidebar/LeftSidebar";
|
|
10
10
|
import type { PatchTarget } from "../utils/sourcePatcher";
|
|
11
|
+
import { logSelect } from "../utils/selectDebug";
|
|
11
12
|
|
|
12
13
|
interface UseDomEditPreviewSyncParams {
|
|
13
14
|
previewIframe: HTMLIFrameElement | null;
|
|
14
15
|
activeCompPath: string | null;
|
|
15
16
|
captionEditMode: boolean;
|
|
16
17
|
domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
|
|
18
|
+
domEditGroupSelectionsRef: React.MutableRefObject<DomEditSelection[]>;
|
|
17
19
|
domEditSelection: DomEditSelection | null;
|
|
20
|
+
/** Re-resolves a whole multi-selection against the current preview document. */
|
|
21
|
+
refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
|
|
18
22
|
applyDomSelection: (
|
|
19
23
|
selection: DomEditSelection | null,
|
|
20
24
|
options?: { revealPanel?: boolean; preserveGroup?: boolean },
|
|
@@ -35,8 +39,10 @@ export function useDomEditPreviewSync({
|
|
|
35
39
|
activeCompPath,
|
|
36
40
|
captionEditMode,
|
|
37
41
|
domEditSelectionRef,
|
|
42
|
+
domEditGroupSelectionsRef,
|
|
38
43
|
domEditSelection,
|
|
39
44
|
applyDomSelection,
|
|
45
|
+
refreshDomEditGroupSelectionsFromPreview,
|
|
40
46
|
buildDomSelectionFromTarget,
|
|
41
47
|
refreshPreviewDocumentVersion,
|
|
42
48
|
syncPreviewHistoryHotkey,
|
|
@@ -72,6 +78,21 @@ export function useDomEditPreviewSync({
|
|
|
72
78
|
// Clear so overlay geometry isn't computed on a stale, detached node.
|
|
73
79
|
// (Drag-release-in-gray-zone is handled separately by
|
|
74
80
|
// suppressNextBoxClickRef; the dragged element still resolves here.)
|
|
81
|
+
//
|
|
82
|
+
// One lost member is not the whole selection though. A multi-select that
|
|
83
|
+
// loses its primary here used to be wiped entirely, so moving a group and
|
|
84
|
+
// having any one of its elements fail to re-resolve deselected all of
|
|
85
|
+
// them. Re-resolve the group instead and keep whoever survived; it only
|
|
86
|
+
// clears when nobody did.
|
|
87
|
+
const group = domEditGroupSelectionsRef.current;
|
|
88
|
+
logSelect("preview-sync-lost", {
|
|
89
|
+
target: currentSelection.selector ?? currentSelection.id ?? null,
|
|
90
|
+
group: group.length,
|
|
91
|
+
});
|
|
92
|
+
if (group.length > 1) {
|
|
93
|
+
await refreshDomEditGroupSelectionsFromPreview(group);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
75
96
|
applyDomSelection(null, { revealPanel: false });
|
|
76
97
|
return;
|
|
77
98
|
}
|
|
@@ -103,8 +124,10 @@ export function useDomEditPreviewSync({
|
|
|
103
124
|
applyDomSelection,
|
|
104
125
|
buildDomSelectionFromTarget,
|
|
105
126
|
captionEditMode,
|
|
127
|
+
domEditGroupSelectionsRef,
|
|
106
128
|
domEditSelectionRef,
|
|
107
129
|
previewIframe,
|
|
130
|
+
refreshDomEditGroupSelectionsFromPreview,
|
|
108
131
|
refreshPreviewDocumentVersion,
|
|
109
132
|
syncPreviewHistoryHotkey,
|
|
110
133
|
applyStudioManualEditsToPreviewRef,
|
|
@@ -60,6 +60,46 @@ const capturedOnReorderShadow: { fn: ((targets: string[]) => void) | undefined }
|
|
|
60
60
|
const domEditSelectionRef: { current: DomEditSelection | null } = { current: null };
|
|
61
61
|
const gsapCommitMutation = Object.assign(vi.fn(), { batch: vi.fn() });
|
|
62
62
|
|
|
63
|
+
function createSessionParams(
|
|
64
|
+
overrides: Partial<UseDomEditSessionParams> = {},
|
|
65
|
+
): UseDomEditSessionParams {
|
|
66
|
+
return {
|
|
67
|
+
projectId: "proj-1",
|
|
68
|
+
activeCompPath: "index.html",
|
|
69
|
+
compIdToSrc: new Map(),
|
|
70
|
+
captionEditMode: false,
|
|
71
|
+
compositionLoading: false,
|
|
72
|
+
previewIframeRef: { current: null },
|
|
73
|
+
timelineElements: [],
|
|
74
|
+
getTimelineSelectionSet: () => new Set(),
|
|
75
|
+
setSelectedTimelineElementId: vi.fn(),
|
|
76
|
+
setTimelineSelectionSet: vi.fn(),
|
|
77
|
+
setRightCollapsed: vi.fn(),
|
|
78
|
+
setRightPanelTab: vi.fn(),
|
|
79
|
+
showToast: vi.fn(),
|
|
80
|
+
refreshPreviewDocumentVersion: vi.fn(),
|
|
81
|
+
queueDomEditSave: async <T,>(save: () => Promise<T>) => save(),
|
|
82
|
+
readProjectFile: async () => "",
|
|
83
|
+
writeProjectFile: async () => {},
|
|
84
|
+
updateEditingFileContent: vi.fn(),
|
|
85
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
86
|
+
editHistory: { recordEdit: async () => {} },
|
|
87
|
+
fileTree: [],
|
|
88
|
+
importedFontAssetsRef: { current: [] },
|
|
89
|
+
projectDir: null,
|
|
90
|
+
projectIdRef: { current: "proj-1" },
|
|
91
|
+
previewIframe: null,
|
|
92
|
+
refreshKey: 0,
|
|
93
|
+
previewDocumentVersion: 0,
|
|
94
|
+
rightPanelTab: "design",
|
|
95
|
+
applyStudioManualEditsToPreviewRef: { current: async () => {} },
|
|
96
|
+
syncPreviewHistoryHotkey: vi.fn(),
|
|
97
|
+
reloadPreview: vi.fn(),
|
|
98
|
+
setRefreshKey: vi.fn(),
|
|
99
|
+
...overrides,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
63
103
|
vi.mock("../utils/sdkResolverShadow", () => ({
|
|
64
104
|
runResolverShadow: vi.fn(),
|
|
65
105
|
recordResolverParity: (...args: unknown[]) => recordResolverParity(...args),
|
|
@@ -220,43 +260,16 @@ describe("onReorderShadow source filter", () => {
|
|
|
220
260
|
const sdkSession = {} as unknown as Composition;
|
|
221
261
|
|
|
222
262
|
function Probe() {
|
|
223
|
-
const params
|
|
224
|
-
projectId: "proj-1",
|
|
225
|
-
activeCompPath: "index.html",
|
|
226
|
-
isMasterView: false,
|
|
227
|
-
compIdToSrc: new Map(),
|
|
228
|
-
captionEditMode: false,
|
|
229
|
-
compositionLoading: false,
|
|
230
|
-
previewIframeRef: { current: null },
|
|
231
|
-
timelineElements: [],
|
|
232
|
-
setSelectedTimelineElementId: vi.fn(),
|
|
233
|
-
setRightCollapsed: vi.fn(),
|
|
234
|
-
setRightPanelTab: vi.fn(),
|
|
235
|
-
showToast: vi.fn(),
|
|
236
|
-
refreshPreviewDocumentVersion: vi.fn(),
|
|
263
|
+
const params = createSessionParams({
|
|
237
264
|
queueDomEditSave: vi.fn(async <T,>(save: () => Promise<T>) => save()) as <T>(
|
|
238
265
|
save: () => Promise<T>,
|
|
239
266
|
) => Promise<T>,
|
|
240
267
|
readProjectFile,
|
|
241
268
|
writeProjectFile: vi.fn(async () => {}),
|
|
242
|
-
updateEditingFileContent: vi.fn(),
|
|
243
|
-
domEditSaveTimestampRef: { current: 0 },
|
|
244
269
|
editHistory: { recordEdit: vi.fn(async () => {}) },
|
|
245
|
-
fileTree: [],
|
|
246
|
-
importedFontAssetsRef: { current: [] },
|
|
247
|
-
projectDir: null,
|
|
248
|
-
projectIdRef: { current: "proj-1" },
|
|
249
|
-
previewIframe: null,
|
|
250
|
-
refreshKey: 0,
|
|
251
|
-
previewDocumentVersion: 0,
|
|
252
|
-
rightPanelTab: "design",
|
|
253
|
-
applyStudioManualEditsToPreviewRef: { current: async () => {} },
|
|
254
|
-
syncPreviewHistoryHotkey: vi.fn(),
|
|
255
|
-
reloadPreview: vi.fn(),
|
|
256
|
-
setRefreshKey: vi.fn(),
|
|
257
270
|
sdkSession,
|
|
258
271
|
forceReloadSdkSession: vi.fn(),
|
|
259
|
-
};
|
|
272
|
+
});
|
|
260
273
|
useDomEditSession(params);
|
|
261
274
|
return null;
|
|
262
275
|
}
|
|
@@ -318,39 +331,7 @@ describe("bulk segment ease commits", () => {
|
|
|
318
331
|
| undefined;
|
|
319
332
|
|
|
320
333
|
function Probe() {
|
|
321
|
-
const params
|
|
322
|
-
projectId: "proj-1",
|
|
323
|
-
activeCompPath: "index.html",
|
|
324
|
-
isMasterView: false,
|
|
325
|
-
compIdToSrc: new Map(),
|
|
326
|
-
captionEditMode: false,
|
|
327
|
-
compositionLoading: false,
|
|
328
|
-
previewIframeRef: { current: null },
|
|
329
|
-
timelineElements: [],
|
|
330
|
-
setSelectedTimelineElementId: vi.fn(),
|
|
331
|
-
setRightCollapsed: vi.fn(),
|
|
332
|
-
setRightPanelTab: vi.fn(),
|
|
333
|
-
showToast: vi.fn(),
|
|
334
|
-
refreshPreviewDocumentVersion: vi.fn(),
|
|
335
|
-
queueDomEditSave: async <T,>(save: () => Promise<T>) => save(),
|
|
336
|
-
readProjectFile: async () => "",
|
|
337
|
-
writeProjectFile: async () => {},
|
|
338
|
-
updateEditingFileContent: vi.fn(),
|
|
339
|
-
domEditSaveTimestampRef: { current: 0 },
|
|
340
|
-
editHistory: { recordEdit: async () => {} },
|
|
341
|
-
fileTree: [],
|
|
342
|
-
importedFontAssetsRef: { current: [] },
|
|
343
|
-
projectDir: null,
|
|
344
|
-
projectIdRef: { current: "proj-1" },
|
|
345
|
-
previewIframe: null,
|
|
346
|
-
refreshKey: 0,
|
|
347
|
-
previewDocumentVersion: 0,
|
|
348
|
-
rightPanelTab: "design",
|
|
349
|
-
applyStudioManualEditsToPreviewRef: { current: async () => {} },
|
|
350
|
-
syncPreviewHistoryHotkey: vi.fn(),
|
|
351
|
-
reloadPreview: vi.fn(),
|
|
352
|
-
setRefreshKey: vi.fn(),
|
|
353
|
-
};
|
|
334
|
+
const params = createSessionParams();
|
|
354
335
|
updateSegmentEase = useDomEditSession(params).handleUpdateSegmentEase;
|
|
355
336
|
return null;
|
|
356
337
|
}
|
|
@@ -31,13 +31,14 @@ interface RecordEditInput {
|
|
|
31
31
|
export interface UseDomEditSessionParams {
|
|
32
32
|
projectId: string | null;
|
|
33
33
|
activeCompPath: string | null;
|
|
34
|
-
isMasterView: boolean;
|
|
35
34
|
compIdToSrc: Map<string, string>;
|
|
36
35
|
captionEditMode: boolean;
|
|
37
36
|
compositionLoading: boolean;
|
|
38
37
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
39
38
|
timelineElements: TimelineElement[];
|
|
39
|
+
getTimelineSelectionSet: () => ReadonlySet<string>;
|
|
40
40
|
setSelectedTimelineElementId: (id: string | null, options?: SelectElementOptions) => void;
|
|
41
|
+
setTimelineSelectionSet: (ids: Set<string>) => void;
|
|
41
42
|
setRightCollapsed: (collapsed: boolean) => void;
|
|
42
43
|
setRightPanelTab: (tab: RightPanelTab) => void;
|
|
43
44
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
@@ -73,13 +74,14 @@ export interface UseDomEditSessionParams {
|
|
|
73
74
|
export function useDomEditSession({
|
|
74
75
|
projectId,
|
|
75
76
|
activeCompPath,
|
|
76
|
-
isMasterView,
|
|
77
77
|
compIdToSrc,
|
|
78
78
|
captionEditMode,
|
|
79
79
|
compositionLoading,
|
|
80
80
|
previewIframeRef,
|
|
81
81
|
timelineElements,
|
|
82
|
+
getTimelineSelectionSet,
|
|
82
83
|
setSelectedTimelineElementId,
|
|
84
|
+
setTimelineSelectionSet,
|
|
83
85
|
setRightCollapsed,
|
|
84
86
|
setRightPanelTab,
|
|
85
87
|
showToast,
|
|
@@ -109,6 +111,7 @@ export function useDomEditSession({
|
|
|
109
111
|
publishSdkSession,
|
|
110
112
|
forceReloadSdkSession,
|
|
111
113
|
}: UseDomEditSessionParams) {
|
|
114
|
+
const isMasterView = !activeCompPath || activeCompPath === "index.html";
|
|
112
115
|
void _setRefreshKey;
|
|
113
116
|
const {
|
|
114
117
|
domEditSelection,
|
|
@@ -127,6 +130,7 @@ export function useDomEditSession({
|
|
|
127
130
|
buildDomSelectionForTimelineElement,
|
|
128
131
|
handleTimelineElementSelect,
|
|
129
132
|
refreshDomEditSelectionFromPreview,
|
|
133
|
+
refreshDomEditGroupSelectionsFromPreview,
|
|
130
134
|
applyMarqueeSelection,
|
|
131
135
|
} = useDomSelection({
|
|
132
136
|
projectId,
|
|
@@ -136,7 +140,9 @@ export function useDomEditSession({
|
|
|
136
140
|
captionEditMode,
|
|
137
141
|
previewIframeRef,
|
|
138
142
|
timelineElements,
|
|
143
|
+
getTimelineSelectionSet,
|
|
139
144
|
setSelectedTimelineElementId,
|
|
145
|
+
setTimelineSelectionSet,
|
|
140
146
|
setRightCollapsed,
|
|
141
147
|
setRightPanelTab,
|
|
142
148
|
previewIframe,
|
|
@@ -382,6 +388,8 @@ export function useDomEditSession({
|
|
|
382
388
|
activeCompPath,
|
|
383
389
|
domEditSelection,
|
|
384
390
|
domEditSelectionRef,
|
|
391
|
+
domEditGroupSelectionsRef,
|
|
392
|
+
refreshDomEditGroupSelectionsFromPreview,
|
|
385
393
|
previewIframeRef,
|
|
386
394
|
previewIframe,
|
|
387
395
|
captionEditMode,
|
|
@@ -23,6 +23,8 @@ export interface UseDomEditWiringParams {
|
|
|
23
23
|
activeCompPath: string | null;
|
|
24
24
|
domEditSelection: DomEditSelection | null;
|
|
25
25
|
domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
|
|
26
|
+
domEditGroupSelectionsRef: React.MutableRefObject<DomEditSelection[]>;
|
|
27
|
+
refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
|
|
26
28
|
previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
27
29
|
previewIframe: HTMLIFrameElement | null;
|
|
28
30
|
captionEditMode: boolean;
|
|
@@ -115,6 +117,8 @@ export function useDomEditWiring({
|
|
|
115
117
|
activeCompPath,
|
|
116
118
|
domEditSelection,
|
|
117
119
|
domEditSelectionRef,
|
|
120
|
+
domEditGroupSelectionsRef,
|
|
121
|
+
refreshDomEditGroupSelectionsFromPreview,
|
|
118
122
|
previewIframeRef,
|
|
119
123
|
previewIframe,
|
|
120
124
|
captionEditMode,
|
|
@@ -254,8 +258,10 @@ export function useDomEditWiring({
|
|
|
254
258
|
activeCompPath,
|
|
255
259
|
captionEditMode,
|
|
256
260
|
domEditSelectionRef,
|
|
261
|
+
domEditGroupSelectionsRef,
|
|
257
262
|
domEditSelection,
|
|
258
263
|
applyDomSelection,
|
|
264
|
+
refreshDomEditGroupSelectionsFromPreview,
|
|
259
265
|
buildDomSelectionFromTarget,
|
|
260
266
|
refreshPreviewDocumentVersion,
|
|
261
267
|
syncPreviewHistoryHotkey,
|
|
@@ -5,6 +5,7 @@ import { createRoot } from "react-dom/client";
|
|
|
5
5
|
import { describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { installReactActEnvironment, makeSelection } from "./domSelectionTestHarness";
|
|
7
7
|
import { useDomSelection } from "./useDomSelection";
|
|
8
|
+
import type { TimelineElement } from "../player";
|
|
8
9
|
|
|
9
10
|
installReactActEnvironment();
|
|
10
11
|
|
|
@@ -14,11 +15,24 @@ interface HarnessProps {
|
|
|
14
15
|
refreshKey: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
interface TimelineSpies {
|
|
19
|
+
setSelectedTimelineElementId: ReturnType<typeof vi.fn>;
|
|
20
|
+
setTimelineSelectionSet: ReturnType<typeof vi.fn>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderHarness(
|
|
24
|
+
initialProps: HarnessProps,
|
|
25
|
+
options: { timelineElements?: TimelineElement[] } = {},
|
|
26
|
+
): {
|
|
18
27
|
current: () => ReturnType<typeof useDomSelection>;
|
|
19
28
|
rerender: (props: HarnessProps) => void;
|
|
20
29
|
cleanup: () => void;
|
|
30
|
+
timeline: TimelineSpies;
|
|
21
31
|
} {
|
|
32
|
+
const timeline: TimelineSpies = {
|
|
33
|
+
setSelectedTimelineElementId: vi.fn(),
|
|
34
|
+
setTimelineSelectionSet: vi.fn(),
|
|
35
|
+
};
|
|
22
36
|
const host = document.createElement("div");
|
|
23
37
|
document.body.append(host);
|
|
24
38
|
const root = createRoot(host);
|
|
@@ -32,8 +46,10 @@ function renderHarness(initialProps: HarnessProps): {
|
|
|
32
46
|
compIdToSrc: new Map(),
|
|
33
47
|
captionEditMode: false,
|
|
34
48
|
previewIframeRef: { current: null },
|
|
35
|
-
timelineElements: [],
|
|
36
|
-
|
|
49
|
+
timelineElements: options.timelineElements ?? [],
|
|
50
|
+
getTimelineSelectionSet: () => new Set(),
|
|
51
|
+
setSelectedTimelineElementId: timeline.setSelectedTimelineElementId,
|
|
52
|
+
setTimelineSelectionSet: timeline.setTimelineSelectionSet,
|
|
37
53
|
setRightCollapsed: vi.fn(),
|
|
38
54
|
setRightPanelTab: vi.fn(),
|
|
39
55
|
previewIframe: null,
|
|
@@ -61,6 +77,7 @@ function renderHarness(initialProps: HarnessProps): {
|
|
|
61
77
|
act(() => root.unmount());
|
|
62
78
|
host.remove();
|
|
63
79
|
},
|
|
80
|
+
timeline,
|
|
64
81
|
};
|
|
65
82
|
}
|
|
66
83
|
|
|
@@ -77,6 +94,119 @@ function setupSelectedHarness() {
|
|
|
77
94
|
return { selection, harness };
|
|
78
95
|
}
|
|
79
96
|
|
|
97
|
+
function timelineElement(domId: string): TimelineElement {
|
|
98
|
+
return {
|
|
99
|
+
id: domId,
|
|
100
|
+
key: domId,
|
|
101
|
+
domId,
|
|
102
|
+
tag: "div",
|
|
103
|
+
start: 0,
|
|
104
|
+
duration: 1,
|
|
105
|
+
track: 0,
|
|
106
|
+
sourceFile: "index.html",
|
|
107
|
+
} as TimelineElement;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A marquee builds the group correctly and then used to lose it: it announced only
|
|
112
|
+
* the primary to the timeline, the timeline is the source of truth for what is
|
|
113
|
+
* selected, and the sync back to the canvas replaced the group with that one
|
|
114
|
+
* element a moment after the drop. The whole set has to be announced, with the
|
|
115
|
+
* primary as its anchor rather than as a new single selection.
|
|
116
|
+
*/
|
|
117
|
+
describe("useDomSelection marquee", () => {
|
|
118
|
+
it("announces every marquee'd element to the timeline, anchored on the primary", () => {
|
|
119
|
+
const first = document.createElement("div");
|
|
120
|
+
first.id = "card";
|
|
121
|
+
const second = document.createElement("div");
|
|
122
|
+
second.id = "chip";
|
|
123
|
+
document.body.append(first, second);
|
|
124
|
+
const harness = renderHarness(
|
|
125
|
+
{ activeCompPath: "index.html", projectId: "project-1", refreshKey: 0 },
|
|
126
|
+
{ timelineElements: [timelineElement("card"), timelineElement("chip")] },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
act(() =>
|
|
130
|
+
harness
|
|
131
|
+
.current()
|
|
132
|
+
.applyMarqueeSelection(
|
|
133
|
+
[makeSelection("Card", first), makeSelection("Chip", second)],
|
|
134
|
+
false,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(harness.current().domEditGroupSelections).toHaveLength(2);
|
|
139
|
+
expect(harness.timeline.setTimelineSelectionSet).toHaveBeenCalledWith(
|
|
140
|
+
new Set(["card", "chip"]),
|
|
141
|
+
);
|
|
142
|
+
expect(harness.timeline.setSelectedTimelineElementId).toHaveBeenCalledWith("card", {
|
|
143
|
+
preserveSet: true,
|
|
144
|
+
});
|
|
145
|
+
harness.cleanup();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("uses a surviving group member as the timeline anchor when the canvas primary has no row", () => {
|
|
149
|
+
const canvasOnly = document.createElement("div");
|
|
150
|
+
canvasOnly.id = "canvas-only";
|
|
151
|
+
const card = document.createElement("div");
|
|
152
|
+
card.id = "card";
|
|
153
|
+
document.body.append(canvasOnly, card);
|
|
154
|
+
const harness = renderHarness(
|
|
155
|
+
{ activeCompPath: "index.html", projectId: "project-1", refreshKey: 0 },
|
|
156
|
+
{ timelineElements: [timelineElement("card")] },
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
act(() =>
|
|
160
|
+
harness
|
|
161
|
+
.current()
|
|
162
|
+
.applyMarqueeSelection(
|
|
163
|
+
[makeSelection("Canvas only", canvasOnly), makeSelection("Card", card)],
|
|
164
|
+
false,
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(harness.timeline.setTimelineSelectionSet).toHaveBeenCalledWith(new Set(["card"]));
|
|
169
|
+
expect(harness.timeline.setSelectedTimelineElementId).toHaveBeenCalledWith("card", {
|
|
170
|
+
preserveSet: true,
|
|
171
|
+
});
|
|
172
|
+
harness.cleanup();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Adding a second element announced only that element, with preserveSet — and
|
|
178
|
+
* preserving a set that does not contain the id empties it. An empty timeline
|
|
179
|
+
* selection syncs back as "nothing is selected", so growing a group could wipe
|
|
180
|
+
* it instead, and so could re-resolving one after a move.
|
|
181
|
+
*/
|
|
182
|
+
describe("useDomSelection additive", () => {
|
|
183
|
+
it("announces both members when a second element joins the selection", () => {
|
|
184
|
+
const first = document.createElement("div");
|
|
185
|
+
first.id = "card";
|
|
186
|
+
const second = document.createElement("div");
|
|
187
|
+
second.id = "chip";
|
|
188
|
+
document.body.append(first, second);
|
|
189
|
+
const harness = renderHarness(
|
|
190
|
+
{ activeCompPath: "index.html", projectId: "project-1", refreshKey: 0 },
|
|
191
|
+
{ timelineElements: [timelineElement("card"), timelineElement("chip")] },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
act(() => harness.current().applyDomSelection(makeSelection("Card", first)));
|
|
195
|
+
act(() =>
|
|
196
|
+
harness.current().applyDomSelection(makeSelection("Chip", second), { additive: true }),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(harness.current().domEditGroupSelections).toHaveLength(2);
|
|
200
|
+
expect(harness.timeline.setTimelineSelectionSet).toHaveBeenLastCalledWith(
|
|
201
|
+
new Set(["card", "chip"]),
|
|
202
|
+
);
|
|
203
|
+
expect(harness.timeline.setSelectedTimelineElementId).toHaveBeenLastCalledWith("chip", {
|
|
204
|
+
preserveSet: true,
|
|
205
|
+
});
|
|
206
|
+
harness.cleanup();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
80
210
|
describe("useDomSelection", () => {
|
|
81
211
|
it("clears a committed selection when the active composition path changes", () => {
|
|
82
212
|
const { selection, harness } = setupSelectedHarness();
|