@hyperframes/studio 0.8.81 → 0.8.83
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-C68UawR3.js → hyperframes-player-BAerMCex.js} +1 -1
- package/dist/assets/{index-CMzPGv9P.js → index-BUhcVhNy.js} +1 -1
- package/dist/assets/{index-hNVM9Pd2.js → index-BYhK_KMc.js} +187 -187
- package/dist/assets/{index-CS5SImRT.js → index-Iv0HI7eM.js} +1 -1
- package/dist/{chunk-I7XNAIOF.js → chunk-3IOCVWB6.js} +6 -97
- package/dist/chunk-3IOCVWB6.js.map +1 -0
- package/dist/chunk-62DDWOSH.js +189 -0
- package/dist/chunk-62DDWOSH.js.map +1 -0
- package/dist/chunk-LPZMAGS7.js +1871 -0
- package/dist/chunk-LPZMAGS7.js.map +1 -0
- package/dist/chunk-QZ6HNNFG.js +934 -0
- package/dist/chunk-QZ6HNNFG.js.map +1 -0
- package/dist/chunk-S65AQH6M.js +101 -0
- package/dist/chunk-S65AQH6M.js.map +1 -0
- package/dist/{domEditingLayers-MHFDKAQ7.js → domEditingLayers-ZOOMN7CQ.js} +3 -2
- package/dist/index.d.ts +834 -330
- package/dist/index.html +1 -1
- package/dist/index.js +26378 -28802
- package/dist/index.js.map +1 -1
- package/dist/player.d.ts +30 -0
- package/dist/player.js +10 -0
- package/dist/player.js.map +1 -0
- package/dist/ui.d.ts +294 -0
- package/dist/ui.js +66 -0
- package/dist/ui.js.map +1 -0
- package/package.json +16 -8
- package/src/components/StudioHeader.tsx +1 -121
- package/src/components/TimelineHistoryButtons.dom.test.tsx +38 -1
- package/src/components/TimelineHistoryButtons.tsx +26 -14
- package/src/components/TimelineToolbar.test.tsx +9 -1
- package/src/components/TimelineToolbar.tsx +13 -5
- package/src/components/editor/BlockParamsPanel.tsx +14 -10
- package/src/components/editor/CanvasContextMenu.test.tsx +3 -3
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ConnectedDomEditOverlay.test.tsx +163 -0
- package/src/components/editor/ConnectedDomEditOverlay.tsx +206 -0
- package/src/components/editor/DomEditOverlay.tsx +36 -21
- package/src/components/editor/LayersPanel.tsx +1 -1
- package/src/components/editor/MarqueeOverlay.tsx +1 -1
- package/src/components/editor/SnapGuideOverlay.tsx +1 -1
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +2 -6
- package/src/components/editor/clipAudioClock.test.ts +61 -0
- package/src/components/editor/clipAudioClock.ts +78 -0
- package/src/components/editor/manualEdits.test.ts +24 -0
- package/src/components/editor/manualEdits.ts +6 -2
- package/src/components/editor/manualEditsDom.ts +0 -65
- package/src/components/editor/manualEditsSeekReapply.ts +136 -0
- package/src/components/editor/marqueeCommit.ts +100 -37
- package/src/components/editor/previewReadOnly.overlay.test.tsx +220 -17
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +206 -32
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +62 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +4 -6
- package/src/components/editor/propertyPanelHelpers.ts +15 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +3 -5
- package/src/components/editor/reapplyBoxSizeAfterSeek.test.ts +1 -1
- package/src/components/editor/reapplyPositionEditsAfterSeek.test.ts +117 -0
- package/src/components/editor/useFxCarve.ts +11 -15
- package/src/components/editor/useFxCarveNodes.ts +27 -15
- package/src/components/editor/useFxLevelling.ts +20 -31
- package/src/components/editor/useInlineTextEditing.tsx +35 -19
- package/src/components/feedback/projectProvenance.ts +2 -3
- package/src/components/nle/AudioMeterStrip.test.tsx +26 -3
- package/src/components/nle/AudioMeterStrip.tsx +23 -12
- package/src/components/nle/PreviewOverlays.tsx +7 -188
- package/src/components/nle/PreviewOverlaysTestMocks.ts +2 -2
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +1 -1
- package/src/components/pointerTargetSize.test.tsx +1 -1
- package/src/components/studioColorGradingScope.ts +12 -36
- package/src/components/ui/HyperframesLogo.tsx +120 -0
- package/src/contexts/TimelineEditContext.test.tsx +49 -0
- package/src/contexts/TimelineEditContext.tsx +27 -23
- package/src/domEditExports.test.tsx +69 -0
- package/src/editorGesturesExports.test.tsx +236 -0
- package/src/hooks/canvasQuickSaves.test.tsx +195 -0
- package/src/hooks/gsapMutationClient.ts +116 -0
- package/src/hooks/resizeObserverTestUtils.ts +14 -0
- package/src/hooks/timelineAudioGroupCreate.ts +18 -40
- package/src/hooks/timelineEditingHelpers.test.ts +21 -0
- package/src/hooks/timelineEditingHelpers.ts +66 -61
- package/src/hooks/timelineQuickSaves.test.ts +195 -0
- package/src/hooks/timelineTimingSync.test.ts +12 -0
- package/src/hooks/timelineTimingSync.ts +31 -123
- package/src/hooks/timelineTrackVisibility.ts +15 -29
- package/src/hooks/useAskAgentModal.test.tsx +78 -0
- package/src/hooks/useAskAgentModal.ts +3 -1
- package/src/hooks/useClipboard.source.test.ts +32 -0
- package/src/hooks/useClipboard.ts +21 -20
- package/src/hooks/useDomEditCommits.test.tsx +13 -9
- package/src/hooks/useDomEditCommits.ts +88 -97
- package/src/hooks/useDomEditCommitsHelpers.ts +69 -6
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useElementLifecycleOps.ts +37 -38
- package/src/hooks/useFileManager.projectOwnership.test.tsx +51 -4
- package/src/hooks/useFileManager.ts +21 -12
- package/src/hooks/useGroupCommits.ts +23 -24
- package/src/hooks/useGsapAnimationOps.ts +26 -8
- package/src/hooks/useGsapScriptCommits.ts +1 -0
- package/src/hooks/useKeyframeKeyboard.test.tsx +99 -0
- package/src/hooks/useKeyframeKeyboard.ts +9 -71
- package/src/hooks/usePreviewPersistence.ts +1 -1
- package/src/hooks/useThumbnailStripSize.test.tsx +47 -0
- package/src/hooks/useThumbnailStripSize.ts +31 -0
- package/src/hooks/useTimelineAssetDropOps.ts +38 -35
- package/src/hooks/useTimelineDeleteOps.ts +53 -56
- package/src/hooks/useTimelineEditing.ts +3 -1
- package/src/hooks/useTimelineGroupEditing.ts +5 -1
- package/src/index.ts +53 -32
- package/src/lightEntries.test.ts +32 -0
- package/src/player/components/CompositionThumbnail.test.ts +40 -6
- package/src/player/components/CompositionThumbnail.tsx +6 -21
- package/src/player/components/ImageThumbnail.test.tsx +27 -6
- package/src/player/components/ImageThumbnail.tsx +4 -18
- package/src/player/components/PlayerControls.test.tsx +52 -0
- package/src/player/components/PlayerControls.tsx +2 -1
- package/src/player/components/Timeline.test.ts +28 -3
- package/src/player/components/TimelineCanvas.tsx +5 -3
- package/src/player/components/TimelineClipFades.test.tsx +31 -10
- package/src/player/components/TimelineClipFades.tsx +2 -1
- package/src/player/components/TimelineTypes.ts +4 -2
- package/src/player/components/VideoThumbnail.test.tsx +33 -11
- package/src/player/components/VideoThumbnail.tsx +7 -29
- package/src/player/components/studioShortcuts.ts +8 -5
- package/src/player/components/thumbnailUtils.test.ts +16 -20
- package/src/player/components/thumbnailUtils.ts +6 -7
- package/src/player/components/timelineClipChildren.test.ts +34 -1
- package/src/player/components/timelineClipChildren.tsx +7 -4
- package/src/player/components/timelineClipDragGestureLifecycle.test.ts +141 -85
- package/src/player/components/timelineClipDragGestureLifecycle.ts +2 -0
- package/src/player/components/timelineEditing.test.ts +55 -0
- package/src/player/components/timelineGroupEditing.ts +13 -6
- package/src/player/components/timelinePlayheadTransform.test.ts +33 -0
- package/src/player/components/timelinePlayheadTransform.ts +17 -0
- package/src/player/components/timelinePointerRelease.ts +4 -0
- package/src/player/components/useTimelinePlayhead.ts +26 -8
- package/src/player/components/useTimelineProviderState.tsx +4 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +46 -0
- package/src/player/components/useTimelineStackingSync.ts +18 -11
- package/src/player/hooks/timelinePlayerSync.test.ts +20 -0
- package/src/player/hooks/timelinePlayerSync.ts +18 -8
- package/src/player/hooks/useShadowPreviewReload.ts +15 -4
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +19 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +93 -1
- package/src/player/lib/playbackShortcuts.ts +18 -0
- package/src/player/lib/playbackTypes.ts +1 -0
- package/src/player/lib/timelineDOM.test.ts +92 -0
- package/src/player/lib/timelineDOM.ts +0 -2
- package/src/player/lib/timelineElementHelpers.ts +47 -10
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/player/store/timelineElement.ts +2 -1
- package/src/playerEntry.ts +1 -0
- package/src/timelineChromeExports.test.tsx +79 -0
- package/src/timelineStackingSyncExport.test.tsx +67 -0
- package/src/ui.ts +31 -0
- package/src/utils/authoredSource.ts +15 -0
- package/src/utils/blockInstaller.ts +32 -27
- package/src/utils/mediaTypes.test.ts +33 -0
- package/src/utils/mediaTypes.ts +12 -5
- package/src/utils/studioFileHistory.test.ts +14 -14
- package/src/utils/studioFileHistory.ts +31 -2
- package/dist/chunk-I7XNAIOF.js.map +0 -1
- /package/dist/{domEditingLayers-MHFDKAQ7.js.map → domEditingLayers-ZOOMN7CQ.js.map} +0 -0
|
@@ -14,6 +14,7 @@ interface MarqueeState {
|
|
|
14
14
|
currentY: number;
|
|
15
15
|
pointerId: number;
|
|
16
16
|
pastThreshold: boolean;
|
|
17
|
+
target: Element;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const MARQUEE_THRESHOLD_PX = 4;
|
|
@@ -80,14 +81,13 @@ function hitsWithin(rect: Rect, candidates: MarqueeHit[]): MarqueeHit[] {
|
|
|
80
81
|
return candidates.filter((candidate) => rectsOverlap(rect, candidate.rect));
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
async function
|
|
84
|
-
|
|
85
|
-
candidates: MarqueeHit[],
|
|
84
|
+
async function resolveDomEditSelections(
|
|
85
|
+
elements: HTMLElement[],
|
|
86
86
|
activeCompositionPath: string,
|
|
87
87
|
): Promise<DomEditSelection[]> {
|
|
88
88
|
const isMasterView = !activeCompositionPath || activeCompositionPath === "index.html";
|
|
89
89
|
const hits: DomEditSelection[] = [];
|
|
90
|
-
for (const
|
|
90
|
+
for (const element of elements) {
|
|
91
91
|
const sel = await resolveDomEditSelection(element, {
|
|
92
92
|
activeCompositionPath,
|
|
93
93
|
isMasterView,
|
|
@@ -98,23 +98,48 @@ async function runMarqueeIntersection(
|
|
|
98
98
|
return hits;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
interface MarqueeGesturesDeps {
|
|
101
|
+
export interface MarqueeGesturesDeps<T = DomEditSelection> {
|
|
102
102
|
iframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
103
103
|
overlayRef: React.RefObject<HTMLDivElement | null>;
|
|
104
104
|
activeCompositionPathRef: React.RefObject<string | null>;
|
|
105
|
-
onMarqueeSelectRef: React.RefObject<
|
|
106
|
-
|
|
107
|
-
>;
|
|
108
|
-
selectionRef
|
|
109
|
-
|
|
105
|
+
onMarqueeSelectRef: React.RefObject<((selections: T[], additive: boolean) => void) | undefined>;
|
|
106
|
+
/** Turns the elements a drag touched into picks; without it, Studio's edit selections. */
|
|
107
|
+
resolveHits?: (elements: HTMLElement[]) => T[] | Promise<T[]>;
|
|
108
|
+
selectionRef?: React.RefObject<DomEditSelection | null>;
|
|
109
|
+
/** Studio's pointer handling, for the events that are not part of a marquee. */
|
|
110
|
+
gestures?: {
|
|
110
111
|
onPointerMove: (event: React.PointerEvent<HTMLDivElement>) => void;
|
|
111
112
|
onPointerUp: (event: React.PointerEvent<HTMLDivElement>) => void;
|
|
112
113
|
clearPointerState: (ref: React.RefObject<DomEditSelection | null>) => void;
|
|
113
114
|
};
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
export interface MarqueeGestures {
|
|
118
|
+
marqueeRect: Rect | null;
|
|
119
|
+
candidateRects: Rect[];
|
|
120
|
+
/** Starts a marquee at this press; the host has decided it landed on empty canvas. */
|
|
121
|
+
begin: (event: React.PointerEvent<HTMLElement>) => void;
|
|
122
|
+
/** Drops an active marquee without selecting anything. */
|
|
123
|
+
cancel: () => void;
|
|
124
|
+
onPointerMove: (event: React.PointerEvent<HTMLDivElement>) => void;
|
|
125
|
+
onPointerUp: (event: React.PointerEvent<HTMLDivElement>) => void;
|
|
126
|
+
onPointerCancel: () => void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function releaseCapture(m: MarqueeState): void {
|
|
130
|
+
try {
|
|
131
|
+
m.target.releasePointerCapture(m.pointerId);
|
|
132
|
+
} catch {
|
|
133
|
+
/* already released */
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function useMarqueeGestures(deps: MarqueeGesturesDeps): MarqueeGestures;
|
|
138
|
+
export function useMarqueeGestures<T>(
|
|
139
|
+
deps: MarqueeGesturesDeps<T> & Required<Pick<MarqueeGesturesDeps<T>, "resolveHits">>,
|
|
140
|
+
): MarqueeGestures;
|
|
116
141
|
// fallow-ignore-next-line complexity
|
|
117
|
-
export function useMarqueeGestures(deps: MarqueeGesturesDeps) {
|
|
142
|
+
export function useMarqueeGestures<T>(deps: MarqueeGesturesDeps<T>): MarqueeGestures {
|
|
118
143
|
const marqueeRef = useRef<MarqueeState | null>(null);
|
|
119
144
|
const [marqueeRect, setMarqueeRect] = useState<Rect | null>(null);
|
|
120
145
|
// Live "candidate" highlight: the elements the marquee currently touches,
|
|
@@ -135,12 +160,55 @@ export function useMarqueeGestures(deps: MarqueeGesturesDeps) {
|
|
|
135
160
|
if (!iframe || !overlay || !deps.onMarqueeSelectRef.current) return;
|
|
136
161
|
const acp = deps.activeCompositionPathRef.current ?? "index.html";
|
|
137
162
|
const candidates = candidatesRef.current ?? collectMarqueeCandidates(iframe, overlay, acp);
|
|
138
|
-
const
|
|
139
|
-
deps.
|
|
163
|
+
const elements = hitsWithin(rect, candidates).map((hit) => hit.element);
|
|
164
|
+
const resolveHits = deps.resolveHits;
|
|
165
|
+
const picks = resolveHits
|
|
166
|
+
? await resolveHits(elements)
|
|
167
|
+
: ((await resolveDomEditSelections(elements, acp)) as T[]);
|
|
168
|
+
deps.onMarqueeSelectRef.current?.(picks, additive);
|
|
140
169
|
},
|
|
141
|
-
[
|
|
170
|
+
[
|
|
171
|
+
deps.iframeRef,
|
|
172
|
+
deps.overlayRef,
|
|
173
|
+
deps.onMarqueeSelectRef,
|
|
174
|
+
deps.activeCompositionPathRef,
|
|
175
|
+
deps.resolveHits,
|
|
176
|
+
],
|
|
142
177
|
);
|
|
143
178
|
|
|
179
|
+
const reset = useCallback(() => {
|
|
180
|
+
marqueeRef.current = null;
|
|
181
|
+
setMarqueeRect(null);
|
|
182
|
+
setCandidateRects([]);
|
|
183
|
+
candidatesRef.current = null;
|
|
184
|
+
}, []);
|
|
185
|
+
|
|
186
|
+
const begin = useCallback(
|
|
187
|
+
(event: React.PointerEvent<HTMLElement>) => {
|
|
188
|
+
const oRect = deps.overlayRef.current?.getBoundingClientRect();
|
|
189
|
+
if (!oRect) return;
|
|
190
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
191
|
+
const x = event.clientX - oRect.left;
|
|
192
|
+
const y = event.clientY - oRect.top;
|
|
193
|
+
marqueeRef.current = {
|
|
194
|
+
startX: x,
|
|
195
|
+
startY: y,
|
|
196
|
+
currentX: x,
|
|
197
|
+
currentY: y,
|
|
198
|
+
pointerId: event.pointerId,
|
|
199
|
+
pastThreshold: false,
|
|
200
|
+
target: event.currentTarget,
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
[deps.overlayRef],
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const cancel = useCallback(() => {
|
|
207
|
+
if (!marqueeRef.current) return;
|
|
208
|
+
releaseCapture(marqueeRef.current);
|
|
209
|
+
reset();
|
|
210
|
+
}, [reset]);
|
|
211
|
+
|
|
144
212
|
const onPointerMove = useCallback(
|
|
145
213
|
(event: React.PointerEvent<HTMLDivElement>) => {
|
|
146
214
|
const m = marqueeRef.current;
|
|
@@ -172,7 +240,7 @@ export function useMarqueeGestures(deps: MarqueeGesturesDeps) {
|
|
|
172
240
|
}
|
|
173
241
|
return;
|
|
174
242
|
}
|
|
175
|
-
deps.gestures
|
|
243
|
+
deps.gestures?.onPointerMove(event);
|
|
176
244
|
},
|
|
177
245
|
[deps.gestures, deps.overlayRef, deps.iframeRef, deps.activeCompositionPathRef],
|
|
178
246
|
);
|
|
@@ -181,12 +249,7 @@ export function useMarqueeGestures(deps: MarqueeGesturesDeps) {
|
|
|
181
249
|
(event: React.PointerEvent<HTMLDivElement>) => {
|
|
182
250
|
const m = marqueeRef.current;
|
|
183
251
|
if (m) {
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
(event.currentTarget as HTMLElement).releasePointerCapture(m.pointerId);
|
|
187
|
-
} catch {
|
|
188
|
-
/* already released */
|
|
189
|
-
}
|
|
252
|
+
releaseCapture(m);
|
|
190
253
|
if (m.pastThreshold) {
|
|
191
254
|
commitMarquee(
|
|
192
255
|
{
|
|
@@ -200,26 +263,26 @@ export function useMarqueeGestures(deps: MarqueeGesturesDeps) {
|
|
|
200
263
|
} else {
|
|
201
264
|
deps.onMarqueeSelectRef.current?.([], false);
|
|
202
265
|
}
|
|
203
|
-
|
|
204
|
-
setCandidateRects([]);
|
|
205
|
-
candidatesRef.current = null;
|
|
266
|
+
reset();
|
|
206
267
|
return;
|
|
207
268
|
}
|
|
208
|
-
deps.gestures
|
|
269
|
+
deps.gestures?.onPointerUp(event);
|
|
209
270
|
},
|
|
210
|
-
[deps.gestures, commitMarquee, deps.onMarqueeSelectRef],
|
|
271
|
+
[deps.gestures, commitMarquee, deps.onMarqueeSelectRef, reset],
|
|
211
272
|
);
|
|
212
273
|
|
|
213
274
|
const onPointerCancel = useCallback(() => {
|
|
214
|
-
if (marqueeRef.current)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
275
|
+
if (marqueeRef.current) return cancel();
|
|
276
|
+
if (deps.selectionRef) deps.gestures?.clearPointerState(deps.selectionRef);
|
|
277
|
+
}, [deps.gestures, deps.selectionRef, cancel]);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
marqueeRect,
|
|
281
|
+
candidateRects,
|
|
282
|
+
begin,
|
|
283
|
+
cancel,
|
|
284
|
+
onPointerMove,
|
|
285
|
+
onPointerUp,
|
|
286
|
+
onPointerCancel,
|
|
287
|
+
};
|
|
225
288
|
}
|
|
@@ -13,7 +13,19 @@ import { DomEditOverlay } from "./DomEditOverlay";
|
|
|
13
13
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
14
|
|
|
15
15
|
const RECT = { left: 100, top: 100, width: 200, height: 100, editScaleX: 1, editScaleY: 1 };
|
|
16
|
-
const layout = vi.hoisted(() => ({
|
|
16
|
+
const layout = vi.hoisted(() => ({
|
|
17
|
+
group: [] as unknown[],
|
|
18
|
+
hover: null as unknown,
|
|
19
|
+
offCanvas: [] as unknown[],
|
|
20
|
+
offCanvasElements: new Map<string, HTMLElement>(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("./useOffCanvasIndicators", () => ({
|
|
24
|
+
useOffCanvasIndicators: () => ({
|
|
25
|
+
offCanvasRects: layout.offCanvas,
|
|
26
|
+
offCanvasElementsRef: { current: layout.offCanvasElements },
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
17
29
|
|
|
18
30
|
vi.mock("./useDomEditOverlayRects", () => ({
|
|
19
31
|
useDomEditOverlayRects: () => ({
|
|
@@ -22,7 +34,7 @@ vi.mock("./useDomEditOverlayRects", () => ({
|
|
|
22
34
|
current: { left: 100, top: 100, width: 200, height: 100, editScaleX: 1, editScaleY: 1 },
|
|
23
35
|
},
|
|
24
36
|
setOverlayRect: () => undefined,
|
|
25
|
-
hoverRect:
|
|
37
|
+
hoverRect: layout.hover,
|
|
26
38
|
groupOverlayItems: layout.group,
|
|
27
39
|
groupOverlayItemsRef: { current: layout.group },
|
|
28
40
|
setGroupOverlayItems: () => undefined,
|
|
@@ -57,6 +69,8 @@ function fixture(
|
|
|
57
69
|
onStyleCommit: vi.fn(),
|
|
58
70
|
onDeleteSelection: vi.fn(),
|
|
59
71
|
onApplyZIndex: vi.fn(),
|
|
72
|
+
onMarqueeSelect: vi.fn(),
|
|
73
|
+
onTextEditingChange: vi.fn(),
|
|
60
74
|
};
|
|
61
75
|
const selection = makeSelection("Title", textElement("title"));
|
|
62
76
|
selection.capabilities.canApplyManualRotation = true;
|
|
@@ -92,6 +106,33 @@ const fire = (target: Element, type: string, init: MouseEventInit = {}) => {
|
|
|
92
106
|
return event;
|
|
93
107
|
};
|
|
94
108
|
|
|
109
|
+
const pressEnter = (overlay: HTMLElement) =>
|
|
110
|
+
act(() => {
|
|
111
|
+
overlay.dispatchEvent(
|
|
112
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const clickBox = (overlay: HTMLElement) => {
|
|
117
|
+
const box = overlay.querySelector(BOX)!;
|
|
118
|
+
for (const type of ["pointerdown", "pointerup", "click"]) fire(box, type);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const enterOn = (target: Element) =>
|
|
122
|
+
act(() => {
|
|
123
|
+
target.dispatchEvent(
|
|
124
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const rightClick = async (overlay: HTMLElement) => {
|
|
129
|
+
await act(async () => {
|
|
130
|
+
overlay.dispatchEvent(
|
|
131
|
+
new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: 5, clientY: 5 }),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
95
136
|
beforeEach(() => {
|
|
96
137
|
vi.useFakeTimers();
|
|
97
138
|
__resetForTests();
|
|
@@ -102,6 +143,9 @@ afterEach(() => {
|
|
|
102
143
|
act(() => root.unmount());
|
|
103
144
|
document.body.innerHTML = "";
|
|
104
145
|
layout.group = [];
|
|
146
|
+
layout.hover = null;
|
|
147
|
+
layout.offCanvas = [];
|
|
148
|
+
layout.offCanvasElements.clear();
|
|
105
149
|
vi.useRealTimers();
|
|
106
150
|
});
|
|
107
151
|
|
|
@@ -196,13 +240,6 @@ describe("DomEditOverlay with the preview read-only", () => {
|
|
|
196
240
|
expect(spies.onPathOffsetCommit).not.toHaveBeenCalled();
|
|
197
241
|
});
|
|
198
242
|
|
|
199
|
-
const pressEnter = (overlay: HTMLElement) =>
|
|
200
|
-
act(() => {
|
|
201
|
-
overlay.dispatchEvent(
|
|
202
|
-
new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }),
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
243
|
it("control: with the flag off Enter opens the text for editing", () => {
|
|
207
244
|
const { selection, overlay } = fixture();
|
|
208
245
|
pressEnter(overlay);
|
|
@@ -215,14 +252,6 @@ describe("DomEditOverlay with the preview read-only", () => {
|
|
|
215
252
|
expect(selection.element.hasAttribute("contenteditable")).toBe(false);
|
|
216
253
|
});
|
|
217
254
|
|
|
218
|
-
const rightClick = async (overlay: HTMLElement) => {
|
|
219
|
-
await act(async () => {
|
|
220
|
-
overlay.dispatchEvent(
|
|
221
|
-
new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: 5, clientY: 5 }),
|
|
222
|
-
);
|
|
223
|
-
});
|
|
224
|
-
};
|
|
225
|
-
|
|
226
255
|
it("control: with the flag off right-click offers delete and z-order", async () => {
|
|
227
256
|
const { overlay } = fixture();
|
|
228
257
|
await rightClick(overlay);
|
|
@@ -258,3 +287,177 @@ describe("DomEditOverlay with the preview read-only", () => {
|
|
|
258
287
|
expect(spies.onCanvasMouseDown).toHaveBeenCalledTimes(1);
|
|
259
288
|
});
|
|
260
289
|
});
|
|
290
|
+
|
|
291
|
+
describe("DomEditOverlay with canvasInput host", () => {
|
|
292
|
+
const HOST = { canvasInput: "host" } as const;
|
|
293
|
+
|
|
294
|
+
it("lets presses through its root, and the overlay default keeps them", () => {
|
|
295
|
+
expect(fixture().overlay.className).toContain("pointer-events-auto");
|
|
296
|
+
act(() => root.unmount());
|
|
297
|
+
expect(fixture(HOST).overlay.className).toContain("pointer-events-none");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("draws no hover box", () => {
|
|
301
|
+
layout.hover = RECT;
|
|
302
|
+
const hovered = (props = {}) => {
|
|
303
|
+
const { overlay } = fixture(props);
|
|
304
|
+
const box = overlay.querySelector('[data-dom-edit-hover-box="true"]');
|
|
305
|
+
act(() => root.unmount());
|
|
306
|
+
return box;
|
|
307
|
+
};
|
|
308
|
+
const selection = makeSelection("Hover", textElement("hover"));
|
|
309
|
+
expect(hovered({ hoverSelection: selection })).not.toBeNull();
|
|
310
|
+
expect(hovered({ ...HOST, hoverSelection: selection })).toBeNull();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("starts no marquee and makes no selection from a press on empty canvas", () => {
|
|
314
|
+
const off = fixture({ selection: null });
|
|
315
|
+
fire(off.overlay, "pointerdown");
|
|
316
|
+
fire(off.overlay, "pointerup");
|
|
317
|
+
expect(off.spies.onMarqueeSelect).toHaveBeenCalledWith([], false);
|
|
318
|
+
act(() => root.unmount());
|
|
319
|
+
const { spies, overlay } = fixture({ ...HOST, selection: null });
|
|
320
|
+
fire(overlay, "pointerdown");
|
|
321
|
+
fire(overlay, "mousedown");
|
|
322
|
+
fire(overlay, "pointerup");
|
|
323
|
+
expect(spies.onMarqueeSelect).not.toHaveBeenCalled();
|
|
324
|
+
expect(spies.onCanvasMouseDown).not.toHaveBeenCalled();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("opens no context menu and selects nothing on right-click", async () => {
|
|
328
|
+
const { spies, overlay } = fixture({ ...HOST, selection: null });
|
|
329
|
+
await rightClick(overlay);
|
|
330
|
+
expect(spies.onSelectionChange).not.toHaveBeenCalled();
|
|
331
|
+
act(() => root.unmount());
|
|
332
|
+
await rightClick(fixture(HOST).overlay);
|
|
333
|
+
expect(document.body.textContent).not.toContain("Delete");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("does not re-select on a click of the selection box", () => {
|
|
337
|
+
const off = fixture();
|
|
338
|
+
clickBox(off.overlay);
|
|
339
|
+
expect(off.spies.onCanvasMouseDown).toHaveBeenCalledTimes(1);
|
|
340
|
+
act(() => root.unmount());
|
|
341
|
+
const { spies, overlay } = fixture(HOST);
|
|
342
|
+
clickBox(overlay);
|
|
343
|
+
fire(overlay.querySelector(BOX)!, "click");
|
|
344
|
+
expect(spies.onCanvasMouseDown).not.toHaveBeenCalled();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("offers no off-canvas indicator to press, so nothing re-selects from one", async () => {
|
|
348
|
+
const MARK = '[aria-label="Select off-canvas element stray"]';
|
|
349
|
+
const offCanvas = (props = {}) => {
|
|
350
|
+
layout.offCanvasElements.set("stray", textElement("stray"));
|
|
351
|
+
layout.offCanvas = [{ key: "stray", left: 820, top: 10, width: 120, height: 40 }];
|
|
352
|
+
return fixture(props);
|
|
353
|
+
};
|
|
354
|
+
const off = offCanvas();
|
|
355
|
+
fire(off.overlay.querySelector(MARK)!, "click");
|
|
356
|
+
await vi.waitFor(() => expect(off.spies.onSelectionChange).toHaveBeenCalledTimes(1));
|
|
357
|
+
act(() => root.unmount());
|
|
358
|
+
const { spies, overlay } = offCanvas(HOST);
|
|
359
|
+
expect(overlay.querySelector(MARK)).toBeNull();
|
|
360
|
+
expect(spies.onSelectionChange).not.toHaveBeenCalled();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("reports each selection-box click to the host once, by either route", () => {
|
|
364
|
+
const cases = [
|
|
365
|
+
{ props: {}, movable: true, reselects: 1 },
|
|
366
|
+
{ props: HOST, movable: true, reselects: 0 },
|
|
367
|
+
{ props: HOST, movable: false, reselects: 0 },
|
|
368
|
+
];
|
|
369
|
+
for (const { props, movable, reselects } of cases) {
|
|
370
|
+
const onSelectionBoxClick = vi.fn();
|
|
371
|
+
const { spies, selection, overlay } = fixture({ ...props, onSelectionBoxClick });
|
|
372
|
+
selection.capabilities.canApplyManualOffset = movable;
|
|
373
|
+
clickBox(overlay);
|
|
374
|
+
expect(onSelectionBoxClick).toHaveBeenCalledTimes(1);
|
|
375
|
+
expect(onSelectionBoxClick).toHaveBeenCalledWith(expect.anything(), selection);
|
|
376
|
+
expect(spies.onCanvasMouseDown).toHaveBeenCalledTimes(reselects);
|
|
377
|
+
act(() => root.unmount());
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("does not report a press off the box, or a click on a group's box", () => {
|
|
382
|
+
const onSelectionBoxClick = vi.fn();
|
|
383
|
+
const off = fixture({ onSelectionBoxClick, onMarqueeSelect: undefined });
|
|
384
|
+
for (const type of ["pointerdown", "mousedown", "pointerup", "click"]) fire(off.overlay, type);
|
|
385
|
+
expect(off.spies.onCanvasMouseDown).toHaveBeenCalledTimes(1);
|
|
386
|
+
act(() => root.unmount());
|
|
387
|
+
const members = ["a", "b"].map((id) => {
|
|
388
|
+
const selection = makeSelection(id, textElement(id));
|
|
389
|
+
return { key: id, selection, element: selection.element, rect: RECT };
|
|
390
|
+
});
|
|
391
|
+
members[1].selection.capabilities.canApplyManualOffset = false;
|
|
392
|
+
layout.group = members;
|
|
393
|
+
const { spies, overlay } = fixture({
|
|
394
|
+
onSelectionBoxClick,
|
|
395
|
+
selection: members[0].selection,
|
|
396
|
+
groupSelections: members.map((m) => m.selection),
|
|
397
|
+
});
|
|
398
|
+
clickBox(overlay);
|
|
399
|
+
expect(spies.onCanvasMouseDown).toHaveBeenCalledTimes(1);
|
|
400
|
+
expect(onSelectionBoxClick).not.toHaveBeenCalled();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("reports the first box click after a handle resize", () => {
|
|
404
|
+
const onSelectionBoxClick = vi.fn();
|
|
405
|
+
const { selection, overlay } = fixture({ ...HOST, onSelectionBoxClick });
|
|
406
|
+
for (const type of ["pointerdown", "pointerup", "click"]) {
|
|
407
|
+
fire(overlay.querySelector("div.h-4.w-4")!, type);
|
|
408
|
+
}
|
|
409
|
+
selection.capabilities.canApplyManualOffset = false;
|
|
410
|
+
clickBox(overlay);
|
|
411
|
+
expect(onSelectionBoxClick).toHaveBeenCalledTimes(1);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("opens the text on Enter when nothing else has focus, not from a focused field", () => {
|
|
415
|
+
const off = fixture();
|
|
416
|
+
enterOn(document.body);
|
|
417
|
+
expect(off.selection.element.hasAttribute("contenteditable")).toBe(false);
|
|
418
|
+
act(() => root.unmount());
|
|
419
|
+
const { spies, selection } = fixture(HOST);
|
|
420
|
+
const field = document.createElement("input");
|
|
421
|
+
document.body.append(field);
|
|
422
|
+
enterOn(field);
|
|
423
|
+
expect(selection.element.hasAttribute("contenteditable")).toBe(false);
|
|
424
|
+
enterOn(document.body);
|
|
425
|
+
expect(selection.element.hasAttribute("contenteditable")).toBe(true);
|
|
426
|
+
expect(spies.onTextEditingChange.mock.calls).toEqual([[true]]);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("keeps the handles working: drag, resize dots and rotate", () => {
|
|
430
|
+
const { spies, overlay } = fixture(HOST);
|
|
431
|
+
fire(overlay.querySelector(BOX)!, "pointerdown");
|
|
432
|
+
expect(spies.onManualDragStart).toHaveBeenCalledTimes(1);
|
|
433
|
+
fire(overlay.querySelector(BOX)!, "pointerup");
|
|
434
|
+
expect(overlay.querySelectorAll("div.h-4.w-4")).toHaveLength(4);
|
|
435
|
+
const rotate = overlay.querySelector('[aria-label="Rotate selection"]')!;
|
|
436
|
+
fire(rotate, "pointerdown", { clientX: 200, clientY: 250 });
|
|
437
|
+
fire(rotate, "pointermove", { clientX: 300, clientY: 150 });
|
|
438
|
+
fire(rotate, "pointerup", { clientX: 300, clientY: 150 });
|
|
439
|
+
expect(spies.onRotationCommit).toHaveBeenCalledTimes(1);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("DomEditOverlay onTextEditingChange", () => {
|
|
444
|
+
it("reports true when the caret goes live and false when the edit ends", () => {
|
|
445
|
+
const { spies, selection, overlay } = fixture();
|
|
446
|
+
expect(spies.onTextEditingChange).not.toHaveBeenCalled();
|
|
447
|
+
pressEnter(overlay);
|
|
448
|
+
expect(spies.onTextEditingChange.mock.calls).toEqual([[true]]);
|
|
449
|
+
act(() => {
|
|
450
|
+
selection.element.dispatchEvent(
|
|
451
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
expect(spies.onTextEditingChange.mock.calls).toEqual([[true], [false]]);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("reports false when the overlay unmounts mid-edit", () => {
|
|
458
|
+
const { spies, overlay } = fixture();
|
|
459
|
+
pressEnter(overlay);
|
|
460
|
+
act(() => root.unmount());
|
|
461
|
+
expect(spies.onTextEditingChange.mock.calls).toEqual([[true], [false]]);
|
|
462
|
+
});
|
|
463
|
+
});
|