@hyperframes/studio 0.7.55 → 0.7.57
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--Z69cEkE.js +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +126 -125
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +53 -2
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +260 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +56 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +23 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -174,7 +174,11 @@ export function useDomEditTextCommits({
|
|
|
174
174
|
let editedElement: HTMLElement | null = null;
|
|
175
175
|
let previousInlineValue: string | null = null;
|
|
176
176
|
const operations = buildDomStyleCommitOperations(property, value, isImageBackgroundCommit);
|
|
177
|
-
|
|
177
|
+
// Inline-style commits never full-reload the preview (that blanks the iframe
|
|
178
|
+
// until it re-renders): the live element was already mutated optimistically in
|
|
179
|
+
// apply(). z-index is no exception — setting `element.style.zIndex` restacks the
|
|
180
|
+
// element in-browser immediately, so a reload would only cost a black blink.
|
|
181
|
+
const skipRefresh = true;
|
|
178
182
|
|
|
179
183
|
await runDomEditCommit({
|
|
180
184
|
capture: () => {
|
|
@@ -305,6 +309,7 @@ export function useDomEditTextCommits({
|
|
|
305
309
|
nextTextFields: DomEditTextField[],
|
|
306
310
|
options?: { importedFont?: ImportedFontAsset | null },
|
|
307
311
|
) => {
|
|
312
|
+
const isLatestTextCommit = bumpDomEditCommitVersion(domTextCommitVersionRef);
|
|
308
313
|
const textCommit = planDomTextCommit(
|
|
309
314
|
selection.textFields,
|
|
310
315
|
nextTextFields,
|
|
@@ -344,14 +349,14 @@ export function useDomEditTextCommits({
|
|
|
344
349
|
: undefined,
|
|
345
350
|
});
|
|
346
351
|
},
|
|
347
|
-
shouldRevert: () =>
|
|
352
|
+
shouldRevert: () => isLatestTextCommit(),
|
|
348
353
|
revert: () => {
|
|
349
354
|
if (!editedElement || previousInnerHtml === null) return;
|
|
350
355
|
editedElement.innerHTML = previousInnerHtml;
|
|
351
356
|
},
|
|
352
357
|
onError: (error) =>
|
|
353
358
|
reportDomEditPersistFailure(selection, textCommit.operations, error, showToast),
|
|
354
|
-
shouldResync:
|
|
359
|
+
shouldResync: isLatestTextCommit,
|
|
355
360
|
resync: () =>
|
|
356
361
|
resyncDomTextSelectionFromPreview(
|
|
357
362
|
doc,
|
|
@@ -11,7 +11,6 @@ import { useCallback, useEffect, useRef } from "react";
|
|
|
11
11
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
12
12
|
import { STUDIO_GSAP_PANEL_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
13
13
|
import { usePlayerStore } from "../player";
|
|
14
|
-
import { resolveTimelineIdForSelection } from "../utils/studioHelpers";
|
|
15
14
|
import { useDomEditPreviewSync } from "./useDomEditPreviewSync";
|
|
16
15
|
import { useGsapAnimationsForElement, usePopulateKeyframeCacheForFile } from "./useGsapTweenCache";
|
|
17
16
|
import { useGsapAnimationFetchFallback } from "./useGsapAnimationFetchFallback";
|
|
@@ -171,14 +170,13 @@ export function useDomEditWiring({
|
|
|
171
170
|
|
|
172
171
|
useEffect(() => {
|
|
173
172
|
if (!domEditSelection?.id) return;
|
|
174
|
-
const { selectedElementId, elements,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}, [domEditSelection, activeCompPath]);
|
|
173
|
+
const { selectedElementId, elements, setSelectedElementId } = usePlayerStore.getState();
|
|
174
|
+
const matchKey = elements.find(
|
|
175
|
+
(el) => el.domId === domEditSelection.id || el.id === domEditSelection.id,
|
|
176
|
+
);
|
|
177
|
+
const key = matchKey ? (matchKey.key ?? matchKey.id) : null;
|
|
178
|
+
if (key && key !== selectedElementId) setSelectedElementId(key);
|
|
179
|
+
}, [domEditSelection?.id]);
|
|
182
180
|
|
|
183
181
|
// ── GSAP cache sync ──
|
|
184
182
|
|
|
@@ -61,14 +61,29 @@ export function useDomGeometryCommits({
|
|
|
61
61
|
);
|
|
62
62
|
|
|
63
63
|
const handleDomBoxSizeCommit = useCallback(
|
|
64
|
-
(
|
|
64
|
+
(
|
|
65
|
+
selection: DomEditSelection,
|
|
66
|
+
next: { width: number; height: number },
|
|
67
|
+
offset?: { x: number; y: number },
|
|
68
|
+
) => {
|
|
65
69
|
if (isElementGsapTargeted(previewIframeRef.current, selection.element)) {
|
|
66
70
|
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
67
71
|
showToast(error.message, "error");
|
|
68
72
|
return Promise.reject(error);
|
|
69
73
|
}
|
|
70
74
|
applyStudioBoxSize(selection.element, next);
|
|
71
|
-
|
|
75
|
+
// Anchored-corner resize (NW/NE/SW) also moves the element to keep the
|
|
76
|
+
// opposite corner fixed. Apply the offset and emit BOTH patch sets in a
|
|
77
|
+
// SINGLE commit: one persist = one undo entry, and there is no
|
|
78
|
+
// intermediate re-stamp where the new size is in source but the anchor
|
|
79
|
+
// offset is not (that frame was the release "jump"). Both builders read
|
|
80
|
+
// the already-mutated live element, so concatenation is safe.
|
|
81
|
+
const patches = buildBoxSizePatches(selection.element);
|
|
82
|
+
if (offset) {
|
|
83
|
+
applyStudioPathOffset(selection.element, offset);
|
|
84
|
+
patches.push(...buildPathOffsetPatches(selection.element));
|
|
85
|
+
}
|
|
86
|
+
return commitPositionPatchToHtml(selection, patches, {
|
|
72
87
|
label: "Resize layer box",
|
|
73
88
|
coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
|
|
74
89
|
});
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
|
-
import {
|
|
6
|
-
import type { TimelineElement } from "../player";
|
|
7
|
-
import { usePlayerStore } from "../player/store/playerStore";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
8
6
|
import { installReactActEnvironment, makeSelection } from "./domSelectionTestHarness";
|
|
9
7
|
import { useDomSelection } from "./useDomSelection";
|
|
10
8
|
|
|
@@ -14,7 +12,6 @@ interface HarnessProps {
|
|
|
14
12
|
activeCompPath: string | null;
|
|
15
13
|
projectId: string | null;
|
|
16
14
|
refreshKey: number;
|
|
17
|
-
timelineElements?: TimelineElement[];
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
function renderHarness(initialProps: HarnessProps): {
|
|
@@ -35,7 +32,7 @@ function renderHarness(initialProps: HarnessProps): {
|
|
|
35
32
|
compIdToSrc: new Map(),
|
|
36
33
|
captionEditMode: false,
|
|
37
34
|
previewIframeRef: { current: null },
|
|
38
|
-
timelineElements:
|
|
35
|
+
timelineElements: [],
|
|
39
36
|
setSelectedTimelineElementId: vi.fn(),
|
|
40
37
|
setRightCollapsed: vi.fn(),
|
|
41
38
|
setRightPanelTab: vi.fn(),
|
|
@@ -67,10 +64,6 @@ function renderHarness(initialProps: HarnessProps): {
|
|
|
67
64
|
};
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
afterEach(() => {
|
|
71
|
-
usePlayerStore.getState().reset();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
67
|
function setupSelectedHarness() {
|
|
75
68
|
const element = document.createElement("div");
|
|
76
69
|
element.id = "headline";
|
|
@@ -138,31 +131,4 @@ describe("useDomSelection", () => {
|
|
|
138
131
|
expect(harness.current().domEditSelection).toBe(selection);
|
|
139
132
|
harness.cleanup();
|
|
140
133
|
});
|
|
141
|
-
|
|
142
|
-
it("keeps preview marquee selections mirrored to the full timeline selection set", () => {
|
|
143
|
-
const first = document.createElement("div");
|
|
144
|
-
first.id = "clip-1";
|
|
145
|
-
const second = document.createElement("div");
|
|
146
|
-
second.id = "clip-2";
|
|
147
|
-
const firstSelection = makeSelection("First", first);
|
|
148
|
-
const secondSelection = makeSelection("Second", second);
|
|
149
|
-
const harness = renderHarness({
|
|
150
|
-
activeCompPath: "intro.html",
|
|
151
|
-
projectId: "project-1",
|
|
152
|
-
refreshKey: 0,
|
|
153
|
-
timelineElements: [
|
|
154
|
-
{ id: "clip-1", domId: "clip-1", tag: "div", start: 0, duration: 1, track: 0 },
|
|
155
|
-
{ id: "clip-2", domId: "clip-2", tag: "div", start: 1, duration: 1, track: 1 },
|
|
156
|
-
],
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
act(() => harness.current().applyMarqueeSelection([secondSelection, firstSelection], false));
|
|
160
|
-
|
|
161
|
-
const state = usePlayerStore.getState();
|
|
162
|
-
expect([...state.selectedElementIds]).toEqual(["clip-2", "clip-1"]);
|
|
163
|
-
expect(state.selectedElementId).toBe("clip-2");
|
|
164
|
-
expect(harness.current().domEditGroupSelections).toHaveLength(2);
|
|
165
|
-
expect(harness.current().domEditSelection).toBe(secondSelection);
|
|
166
|
-
harness.cleanup();
|
|
167
|
-
});
|
|
168
134
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
-
import type { TimelineElement } from "../player";
|
|
2
|
+
import type { SelectElementOptions, TimelineElement } from "../player";
|
|
3
3
|
import {
|
|
4
4
|
getAllPreviewTargetsFromPointer,
|
|
5
5
|
getPreviewTargetFromPointer,
|
|
6
6
|
} from "../utils/studioPreviewHelpers";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
findMatchingTimelineElementId,
|
|
9
|
+
findTimelineIdByAncestor,
|
|
10
|
+
type RightPanelTab,
|
|
11
|
+
} from "../utils/studioHelpers";
|
|
8
12
|
import {
|
|
9
13
|
domEditSelectionsTargetSame,
|
|
10
14
|
domEditSelectionInGroup,
|
|
@@ -20,7 +24,6 @@ import {
|
|
|
20
24
|
type DomEditSelection,
|
|
21
25
|
} from "../components/editor/domEditing";
|
|
22
26
|
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
|
|
23
|
-
import { usePlayerStore } from "../player/store/playerStore";
|
|
24
27
|
|
|
25
28
|
// ── Types ──
|
|
26
29
|
|
|
@@ -44,7 +47,7 @@ export interface UseDomSelectionParams {
|
|
|
44
47
|
captionEditMode: boolean;
|
|
45
48
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
46
49
|
timelineElements: TimelineElement[];
|
|
47
|
-
setSelectedTimelineElementId: (id: string | null) => void;
|
|
50
|
+
setSelectedTimelineElementId: (id: string | null, options?: SelectElementOptions) => void;
|
|
48
51
|
setRightCollapsed: (collapsed: boolean) => void;
|
|
49
52
|
setRightPanelTab: (tab: RightPanelTab) => void;
|
|
50
53
|
previewIframe: HTMLIFrameElement | null;
|
|
@@ -92,6 +95,7 @@ export interface UseDomSelectionReturn {
|
|
|
92
95
|
) => Promise<DomEditSelection | null>;
|
|
93
96
|
handleTimelineElementSelect: (element: TimelineElement | null) => Promise<void>;
|
|
94
97
|
refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => Promise<void>;
|
|
98
|
+
refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
|
|
95
99
|
applyMarqueeSelection: (selections: DomEditSelection[], additive: boolean) => void;
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -206,42 +210,27 @@ export function useDomSelection({
|
|
|
206
210
|
if (nextSelection) {
|
|
207
211
|
if (options?.revealPanel !== false) {
|
|
208
212
|
setRightCollapsed(false);
|
|
209
|
-
// Keep the Variables tab in place — selecting elements is part of
|
|
210
|
-
//
|
|
213
|
+
// Keep the Variables tab in place — selecting elements is part of the bind
|
|
214
|
+
// flow there; yanking to Design would lose the context.
|
|
211
215
|
if (rightPanelTabRef.current !== "variables") {
|
|
212
216
|
setRightPanelTab("design");
|
|
213
217
|
}
|
|
214
218
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
.map((selection) =>
|
|
225
|
-
resolveTimelineIdForSelection(selection, timelineElements, activeCompPath),
|
|
226
|
-
)
|
|
227
|
-
.filter((id): id is string => Boolean(id));
|
|
228
|
-
if (groupIds.length > 0) {
|
|
229
|
-
usePlayerStore.getState().setSelection(groupIds, anchorId);
|
|
230
|
-
} else {
|
|
231
|
-
setSelectedTimelineElementId(anchorId);
|
|
232
|
-
}
|
|
219
|
+
const nextSelectedTimelineId =
|
|
220
|
+
findMatchingTimelineElementId(nextSelection, timelineElements) ??
|
|
221
|
+
findTimelineIdByAncestor(
|
|
222
|
+
nextSelection.element,
|
|
223
|
+
timelineElements,
|
|
224
|
+
nextSelection.sourceFile || "index.html",
|
|
225
|
+
);
|
|
226
|
+
// Late marquee notify: a primary already in the live set must not collapse it.
|
|
227
|
+
setSelectedTimelineElementId(nextSelectedTimelineId, { preserveSet: true });
|
|
233
228
|
return;
|
|
234
229
|
}
|
|
235
230
|
|
|
236
231
|
setSelectedTimelineElementId(null);
|
|
237
232
|
},
|
|
238
|
-
[
|
|
239
|
-
setSelectedTimelineElementId,
|
|
240
|
-
timelineElements,
|
|
241
|
-
setRightCollapsed,
|
|
242
|
-
setRightPanelTab,
|
|
243
|
-
activeCompPath,
|
|
244
|
-
],
|
|
233
|
+
[setSelectedTimelineElementId, timelineElements, setRightCollapsed, setRightPanelTab],
|
|
245
234
|
);
|
|
246
235
|
|
|
247
236
|
const clearDomSelection = useCallback(() => {
|
|
@@ -424,6 +413,55 @@ export function useDomSelection({
|
|
|
424
413
|
[activeCompPath, applyDomSelection, buildDomSelectionFromTarget, previewIframeRef],
|
|
425
414
|
);
|
|
426
415
|
|
|
416
|
+
const refreshDomEditGroupSelectionsFromPreview = useCallback(
|
|
417
|
+
// fallow-ignore-next-line complexity
|
|
418
|
+
async (selections: DomEditSelection[]) => {
|
|
419
|
+
const iframe = previewIframeRef.current;
|
|
420
|
+
let doc: Document | null = null;
|
|
421
|
+
try {
|
|
422
|
+
doc = iframe?.contentDocument ?? null;
|
|
423
|
+
} catch {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (!doc) return;
|
|
427
|
+
|
|
428
|
+
const nextGroup: DomEditSelection[] = [];
|
|
429
|
+
for (const selection of selections) {
|
|
430
|
+
const element = findElementForSelection(doc, selection, activeCompPath);
|
|
431
|
+
if (!element) continue;
|
|
432
|
+
const nextSelection = await buildDomSelectionFromTarget(element);
|
|
433
|
+
if (nextSelection) nextGroup.push(nextSelection);
|
|
434
|
+
}
|
|
435
|
+
if (nextGroup.length === 0) return;
|
|
436
|
+
|
|
437
|
+
const currentSelection = domEditSelectionRef.current;
|
|
438
|
+
const nextSelection =
|
|
439
|
+
nextGroup.find((selection) => domEditSelectionsTargetSame(selection, currentSelection)) ??
|
|
440
|
+
nextGroup[0] ??
|
|
441
|
+
null;
|
|
442
|
+
|
|
443
|
+
domEditSelectionRef.current = nextSelection;
|
|
444
|
+
domEditGroupSelectionsRef.current = nextGroup;
|
|
445
|
+
setDomEditSelection(nextSelection);
|
|
446
|
+
setDomEditGroupSelections(nextGroup);
|
|
447
|
+
|
|
448
|
+
if (nextSelection) {
|
|
449
|
+
setSelectedTimelineElementId(
|
|
450
|
+
findMatchingTimelineElementId(nextSelection, timelineElements),
|
|
451
|
+
);
|
|
452
|
+
} else {
|
|
453
|
+
setSelectedTimelineElementId(null);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
[
|
|
457
|
+
activeCompPath,
|
|
458
|
+
buildDomSelectionFromTarget,
|
|
459
|
+
setSelectedTimelineElementId,
|
|
460
|
+
timelineElements,
|
|
461
|
+
previewIframeRef,
|
|
462
|
+
],
|
|
463
|
+
);
|
|
464
|
+
|
|
427
465
|
// ── Effects ──
|
|
428
466
|
|
|
429
467
|
// Clear hover unconditionally on composition/project/preview change
|
|
@@ -471,8 +509,7 @@ export function useDomSelection({
|
|
|
471
509
|
const applyMarqueeSelection = useCallback(
|
|
472
510
|
// fallow-ignore-next-line complexity
|
|
473
511
|
(selections: DomEditSelection[], additive: boolean) => {
|
|
474
|
-
// Honor the inspector-panels kill switch like applyDomSelection does
|
|
475
|
-
// marquee can't land selections while the inspector UI is suppressed.
|
|
512
|
+
// Honor the inspector-panels kill switch like applyDomSelection does.
|
|
476
513
|
if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
|
|
477
514
|
domEditSelectionRef.current = null;
|
|
478
515
|
domEditGroupSelectionsRef.current = [];
|
|
@@ -493,8 +530,7 @@ export function useDomSelection({
|
|
|
493
530
|
if (!domEditSelectionInGroup(nextGroup, s)) nextGroup = [...nextGroup, s];
|
|
494
531
|
}
|
|
495
532
|
} else {
|
|
496
|
-
// Dedupe by target:
|
|
497
|
-
// to the same group, which must count as one selection, not many duplicates.
|
|
533
|
+
// Dedupe by target: select-as-unit collapses marquee'd members to one group.
|
|
498
534
|
nextGroup = [];
|
|
499
535
|
for (const s of selections) {
|
|
500
536
|
if (!domEditSelectionInGroup(nextGroup, s)) nextGroup.push(s);
|
|
@@ -505,23 +541,16 @@ export function useDomSelection({
|
|
|
505
541
|
domEditGroupSelectionsRef.current = nextGroup;
|
|
506
542
|
setDomEditSelection(nextSelection);
|
|
507
543
|
setDomEditGroupSelections(nextGroup);
|
|
508
|
-
const nextTimelineId =
|
|
509
|
-
nextSelection,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
)
|
|
517
|
-
.filter((id): id is string => Boolean(id));
|
|
518
|
-
if (nextTimelineIds.length > 0) {
|
|
519
|
-
usePlayerStore.getState().setSelection(nextTimelineIds, nextTimelineId);
|
|
520
|
-
} else {
|
|
521
|
-
setSelectedTimelineElementId(null);
|
|
522
|
-
}
|
|
544
|
+
const nextTimelineId =
|
|
545
|
+
findMatchingTimelineElementId(nextSelection, timelineElements) ??
|
|
546
|
+
findTimelineIdByAncestor(
|
|
547
|
+
nextSelection.element,
|
|
548
|
+
timelineElements,
|
|
549
|
+
nextSelection.sourceFile || "index.html",
|
|
550
|
+
);
|
|
551
|
+
setSelectedTimelineElementId(nextTimelineId);
|
|
523
552
|
},
|
|
524
|
-
[applyDomSelection, timelineElements, setSelectedTimelineElementId
|
|
553
|
+
[applyDomSelection, timelineElements, setSelectedTimelineElementId],
|
|
525
554
|
);
|
|
526
555
|
|
|
527
556
|
// Disabled inspector effect
|
|
@@ -558,6 +587,7 @@ export function useDomSelection({
|
|
|
558
587
|
buildDomSelectionForTimelineElement,
|
|
559
588
|
handleTimelineElementSelect,
|
|
560
589
|
refreshDomEditSelectionFromPreview,
|
|
590
|
+
refreshDomEditGroupSelectionsFromPreview,
|
|
561
591
|
applyMarqueeSelection,
|
|
562
592
|
};
|
|
563
593
|
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { installReactActEnvironment, makeSelection } from "./domSelectionTestHarness";
|
|
7
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
8
|
+
import type { SelectElementOptions, TimelineElement } from "../player";
|
|
9
|
+
import { usePlayerStore } from "../player";
|
|
10
|
+
|
|
11
|
+
installReactActEnvironment();
|
|
12
|
+
|
|
13
|
+
// ── Module mocks ──
|
|
14
|
+
// Control the async selection-resolution ordering so the race guard can be
|
|
15
|
+
// exercised deterministically, and neutralise the DOM re-apply side effect.
|
|
16
|
+
vi.mock("../components/editor/manualEdits", () => ({
|
|
17
|
+
reapplyPositionEditsAfterSeek: () => undefined,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const deferreds = new Map<string, { promise: Promise<DomEditSelection>; resolve: () => void }>();
|
|
21
|
+
|
|
22
|
+
function deferredFor(el: HTMLElement): Promise<DomEditSelection> {
|
|
23
|
+
const id = el.id;
|
|
24
|
+
let resolveFn: () => void = () => undefined;
|
|
25
|
+
const promise = new Promise<DomEditSelection>((resolve) => {
|
|
26
|
+
resolveFn = () => resolve(makeSelection(id.toUpperCase(), el));
|
|
27
|
+
});
|
|
28
|
+
deferreds.set(id, { promise, resolve: resolveFn });
|
|
29
|
+
return promise;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
vi.mock("../components/editor/domEditing", async (importOriginal) => {
|
|
33
|
+
const actual = await importOriginal<typeof import("../components/editor/domEditing")>();
|
|
34
|
+
return {
|
|
35
|
+
...actual,
|
|
36
|
+
findElementForTimelineElement: (doc: Document, element: { id?: string }) =>
|
|
37
|
+
element.id ? doc.getElementById(element.id) : null,
|
|
38
|
+
resolveDomEditSelection: (startEl: HTMLElement | null) =>
|
|
39
|
+
startEl ? deferredFor(startEl) : Promise.resolve(null),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Imported after the mocks so the hook picks up the mocked modules.
|
|
44
|
+
const { useDomSelection } = await import("./useDomSelection");
|
|
45
|
+
|
|
46
|
+
interface HarnessProps {
|
|
47
|
+
rightPanelTab: "design" | "variables";
|
|
48
|
+
setRightPanelTab: (tab: "design" | "variables") => void;
|
|
49
|
+
iframe: HTMLIFrameElement | null;
|
|
50
|
+
timelineElements: TimelineElement[];
|
|
51
|
+
setSelectedTimelineElementId?: (id: string | null, options?: SelectElementOptions) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderHarness(props: HarnessProps) {
|
|
55
|
+
const host = document.createElement("div");
|
|
56
|
+
document.body.append(host);
|
|
57
|
+
const root = createRoot(host);
|
|
58
|
+
let currentHook: ReturnType<typeof useDomSelection> | null = null;
|
|
59
|
+
|
|
60
|
+
function Harness() {
|
|
61
|
+
currentHook = useDomSelection({
|
|
62
|
+
projectId: "project-1",
|
|
63
|
+
activeCompPath: "index.html",
|
|
64
|
+
isMasterView: false,
|
|
65
|
+
compIdToSrc: new Map(),
|
|
66
|
+
captionEditMode: false,
|
|
67
|
+
previewIframeRef: { current: props.iframe },
|
|
68
|
+
timelineElements: props.timelineElements,
|
|
69
|
+
setSelectedTimelineElementId: props.setSelectedTimelineElementId ?? vi.fn(),
|
|
70
|
+
setRightCollapsed: vi.fn(),
|
|
71
|
+
setRightPanelTab: props.setRightPanelTab,
|
|
72
|
+
previewIframe: props.iframe,
|
|
73
|
+
refreshKey: 0,
|
|
74
|
+
rightPanelTab: props.rightPanelTab,
|
|
75
|
+
});
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
act(() => root.render(React.createElement(Harness)));
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
current: () => {
|
|
83
|
+
if (!currentHook) throw new Error("Expected hook result");
|
|
84
|
+
return currentHook;
|
|
85
|
+
},
|
|
86
|
+
cleanup: () => {
|
|
87
|
+
act(() => root.unmount());
|
|
88
|
+
host.remove();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("useDomSelection — Variables tab preservation", () => {
|
|
94
|
+
it("does not yank the user off the Variables tab when selecting on canvas", () => {
|
|
95
|
+
const setRightPanelTab = vi.fn();
|
|
96
|
+
const el = document.createElement("div");
|
|
97
|
+
el.id = "headline";
|
|
98
|
+
const harness = renderHarness({
|
|
99
|
+
rightPanelTab: "variables",
|
|
100
|
+
setRightPanelTab,
|
|
101
|
+
iframe: null,
|
|
102
|
+
timelineElements: [],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
act(() => harness.current().applyDomSelection(makeSelection("Headline", el)));
|
|
106
|
+
|
|
107
|
+
expect(setRightPanelTab).not.toHaveBeenCalled();
|
|
108
|
+
harness.cleanup();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("switches to the Design tab when not on Variables (control)", () => {
|
|
112
|
+
const setRightPanelTab = vi.fn();
|
|
113
|
+
const el = document.createElement("div");
|
|
114
|
+
el.id = "headline";
|
|
115
|
+
const harness = renderHarness({
|
|
116
|
+
rightPanelTab: "design",
|
|
117
|
+
setRightPanelTab,
|
|
118
|
+
iframe: null,
|
|
119
|
+
timelineElements: [],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
act(() => harness.current().applyDomSelection(makeSelection("Headline", el)));
|
|
123
|
+
|
|
124
|
+
expect(setRightPanelTab).toHaveBeenCalledWith("design");
|
|
125
|
+
harness.cleanup();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("useDomSelection — timeline-select race guard", () => {
|
|
130
|
+
beforeEach(() => deferreds.clear());
|
|
131
|
+
afterEach(() => deferreds.clear());
|
|
132
|
+
|
|
133
|
+
it("a stale async resolution never clobbers a newer selection", async () => {
|
|
134
|
+
const iframe = document.createElement("iframe");
|
|
135
|
+
document.body.append(iframe);
|
|
136
|
+
const doc = iframe.contentDocument!;
|
|
137
|
+
const elA = doc.createElement("div");
|
|
138
|
+
elA.id = "a";
|
|
139
|
+
const elB = doc.createElement("div");
|
|
140
|
+
elB.id = "b";
|
|
141
|
+
doc.body.append(elA, elB);
|
|
142
|
+
|
|
143
|
+
const elementA: TimelineElement = { id: "a", tag: "div", start: 0, duration: 1, track: 0 };
|
|
144
|
+
const elementB: TimelineElement = { id: "b", tag: "div", start: 0, duration: 1, track: 0 };
|
|
145
|
+
|
|
146
|
+
const harness = renderHarness({
|
|
147
|
+
rightPanelTab: "design",
|
|
148
|
+
setRightPanelTab: vi.fn(),
|
|
149
|
+
iframe,
|
|
150
|
+
timelineElements: [elementA, elementB],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Fire A then B; both suspend on their (pending) resolveDomEditSelection.
|
|
154
|
+
let pA: Promise<void> = Promise.resolve();
|
|
155
|
+
let pB: Promise<void> = Promise.resolve();
|
|
156
|
+
act(() => {
|
|
157
|
+
pA = harness.current().handleTimelineElementSelect(elementA);
|
|
158
|
+
pB = harness.current().handleTimelineElementSelect(elementB);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Resolve the NEWER select (B) first, then the older one (A) last.
|
|
162
|
+
await act(async () => {
|
|
163
|
+
deferreds.get("b")?.resolve();
|
|
164
|
+
await pB;
|
|
165
|
+
});
|
|
166
|
+
await act(async () => {
|
|
167
|
+
deferreds.get("a")?.resolve();
|
|
168
|
+
await pA;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// The stale A resolution must be dropped: B wins.
|
|
172
|
+
expect(harness.current().domEditSelection?.id).toBe("b");
|
|
173
|
+
harness.cleanup();
|
|
174
|
+
iframe.remove();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("useDomSelection — marquee multi-select survives the late async primary", () => {
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
deferreds.clear();
|
|
181
|
+
const store = usePlayerStore.getState();
|
|
182
|
+
store.setSelectedElementId(null);
|
|
183
|
+
store.clearSelectedElementIds();
|
|
184
|
+
});
|
|
185
|
+
afterEach(() => {
|
|
186
|
+
deferreds.clear();
|
|
187
|
+
const store = usePlayerStore.getState();
|
|
188
|
+
store.setSelectedElementId(null);
|
|
189
|
+
store.clearSelectedElementIds();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
function timelineEl(id: string): TimelineElement {
|
|
193
|
+
return { id, domId: id, tag: "div", start: 0, duration: 1, track: 0 };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Reproduces the reported regression: a marquee over N clips leaves N members in
|
|
197
|
+
// selectedElementIds, then the inspector-open notify (finishMarquee →
|
|
198
|
+
// handleTimelineElementSelect → applyDomSelection) resolves LATE and writes the
|
|
199
|
+
// primary again. Before the fix that late write collapsed the set to one clip.
|
|
200
|
+
it("keeps all N members when a late primary-set targets a set member", async () => {
|
|
201
|
+
const iframe = document.createElement("iframe");
|
|
202
|
+
document.body.append(iframe);
|
|
203
|
+
const doc = iframe.contentDocument!;
|
|
204
|
+
for (const id of ["a", "b", "c"]) {
|
|
205
|
+
const el = doc.createElement("div");
|
|
206
|
+
el.id = id;
|
|
207
|
+
doc.body.append(el);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const store = usePlayerStore.getState();
|
|
211
|
+
// Marquee end state: primary written first, then the full set (real ordering).
|
|
212
|
+
store.setSelectedElementId("a");
|
|
213
|
+
store.setSelectedElementIds(new Set(["a", "b", "c"]));
|
|
214
|
+
|
|
215
|
+
const harness = renderHarness({
|
|
216
|
+
rightPanelTab: "design",
|
|
217
|
+
setRightPanelTab: vi.fn(),
|
|
218
|
+
iframe,
|
|
219
|
+
timelineElements: [timelineEl("a"), timelineEl("b"), timelineEl("c")],
|
|
220
|
+
// Wire the real store write so applyDomSelection's collapse decision is exercised.
|
|
221
|
+
setSelectedTimelineElementId: usePlayerStore.getState().setSelectedElementId,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
let pending: Promise<void> = Promise.resolve();
|
|
225
|
+
act(() => {
|
|
226
|
+
pending = harness.current().handleTimelineElementSelect(timelineEl("a"));
|
|
227
|
+
});
|
|
228
|
+
await act(async () => {
|
|
229
|
+
deferreds.get("a")?.resolve();
|
|
230
|
+
await pending;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(usePlayerStore.getState().selectedElementIds.size).toBe(3);
|
|
234
|
+
expect(usePlayerStore.getState().selectedElementId).toBe("a");
|
|
235
|
+
harness.cleanup();
|
|
236
|
+
iframe.remove();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("collapses the set when a late primary-set targets a non-member (fresh click)", async () => {
|
|
240
|
+
const iframe = document.createElement("iframe");
|
|
241
|
+
document.body.append(iframe);
|
|
242
|
+
const doc = iframe.contentDocument!;
|
|
243
|
+
for (const id of ["a", "b", "d"]) {
|
|
244
|
+
const el = doc.createElement("div");
|
|
245
|
+
el.id = id;
|
|
246
|
+
doc.body.append(el);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const store = usePlayerStore.getState();
|
|
250
|
+
// Stale set left over from a previous gesture.
|
|
251
|
+
store.setSelectedElementId("a");
|
|
252
|
+
store.setSelectedElementIds(new Set(["a", "b"]));
|
|
253
|
+
|
|
254
|
+
const harness = renderHarness({
|
|
255
|
+
rightPanelTab: "design",
|
|
256
|
+
setRightPanelTab: vi.fn(),
|
|
257
|
+
iframe,
|
|
258
|
+
timelineElements: [timelineEl("a"), timelineEl("b"), timelineEl("d")],
|
|
259
|
+
setSelectedTimelineElementId: usePlayerStore.getState().setSelectedElementId,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
let pending: Promise<void> = Promise.resolve();
|
|
263
|
+
act(() => {
|
|
264
|
+
pending = harness.current().handleTimelineElementSelect(timelineEl("d"));
|
|
265
|
+
});
|
|
266
|
+
await act(async () => {
|
|
267
|
+
deferreds.get("d")?.resolve();
|
|
268
|
+
await pending;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(usePlayerStore.getState().selectedElementIds.size).toBe(0);
|
|
272
|
+
expect(usePlayerStore.getState().selectedElementId).toBe("d");
|
|
273
|
+
harness.cleanup();
|
|
274
|
+
iframe.remove();
|
|
275
|
+
});
|
|
276
|
+
});
|