@replohq/sandbox-runtime 1.6.8 → 1.7.1
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/_vendor/replo-utils/lib/viewportSnapshot.d.ts +28 -7
- package/_vendor/replo-utils/lib/viewportSnapshot.mjs +1 -1
- package/_vendor/replo-utils/react/use-viewport-snapshot-responder.d.ts +2 -3
- package/_vendor/replo-utils/react/use-viewport-snapshot-responder.mjs +18 -2
- package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +1 -0
- package/_vendor/schemas/selectModeIframeRpc.d.ts +2 -0
- package/_vendor/schemas/selectModeIframeRpc.mjs +11 -6
- package/buildMetadata.js +3 -3
- package/{chunk-RXYLMSNM.mjs → chunk-3YA3QXUQ.mjs} +27 -9
- package/package.json +4 -4
- package/select-mode/hooks/useEditModeStateMachine.js +14 -22
- package/select-mode/hooks/useEditModeStateMachine.js.map +2 -2
- package/select-mode/hooks/useParentMessaging.d.ts +4 -1
- package/select-mode/hooks/useParentMessaging.js +2 -2
- package/select-mode/hooks/useParentMessaging.js.map +2 -2
- package/select-mode/hooks/useSelectModeController.d.ts +4 -1
- package/select-mode/hooks/useSelectModeController.js +27 -2
- package/select-mode/hooks/useSelectModeController.js.map +2 -2
- package/select-mode/hooks/useSelectionInteractions.js +153 -53
- package/select-mode/hooks/useSelectionInteractions.js.map +2 -2
- package/select-mode/interactions/selectionGeometry.d.ts +25 -0
- package/select-mode/interactions/selectionGeometry.js +83 -1
- package/select-mode/interactions/selectionGeometry.js.map +2 -2
- package/select-mode/selectModeSession.d.ts +9 -1
- package/select-mode/selectModeSession.js +15 -1
- package/select-mode/selectModeSession.js.map +2 -2
- package/select-mode/selectionFrame.d.ts +7 -0
- package/select-mode/selectionFrame.js +20 -0
- package/select-mode/selectionFrame.js.map +2 -2
|
@@ -12,19 +12,40 @@ import {
|
|
|
12
12
|
getDragPositionFromClient,
|
|
13
13
|
getElementAnchor,
|
|
14
14
|
getPageRectFromViewportRect,
|
|
15
|
+
getPointRect,
|
|
16
|
+
getSelectionAfterAdditiveClick,
|
|
15
17
|
getViewportRectFromPageRect,
|
|
16
|
-
getViewportSelectionRect
|
|
18
|
+
getViewportSelectionRect,
|
|
19
|
+
isAdditiveSelectionClick,
|
|
20
|
+
unionElementPageRects
|
|
17
21
|
} from "../interactions/selectionGeometry";
|
|
18
22
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
hideSelectionFrame,
|
|
24
|
+
showAreaSelectionFrames,
|
|
25
|
+
showElementSelectionFrames
|
|
21
26
|
} from "../selectionFrame";
|
|
22
27
|
import { isSelectionStillCurrent } from "../selectionPublish";
|
|
23
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
getConnectedSelectedElement,
|
|
30
|
+
IDLE_SESSION
|
|
31
|
+
} from "../selectModeSession";
|
|
24
32
|
import { symbolicateSelectedElement } from "../symbolicate";
|
|
25
33
|
import { isTextEditableElement } from "./useEditModeStateMachine";
|
|
26
34
|
import { getRepetitionSiblingElements } from "./useSiblingResolution";
|
|
27
35
|
const DOUBLE_CLICK_THRESHOLD_MS = 500;
|
|
36
|
+
function getSelectedElementsFromSession(session) {
|
|
37
|
+
if (session.type === "element") {
|
|
38
|
+
const liveElement = getConnectedSelectedElement(session);
|
|
39
|
+
return liveElement ? [liveElement] : [];
|
|
40
|
+
}
|
|
41
|
+
if (session.type === "area") {
|
|
42
|
+
return session.items.flatMap((item) => {
|
|
43
|
+
const liveElement = getConnectedSelectedElement(item);
|
|
44
|
+
return liveElement ? [liveElement] : [];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
28
49
|
async function maybeSymbolicate(data) {
|
|
29
50
|
return data.kind === "react-element" ? symbolicateSelectedElement(data) : data;
|
|
30
51
|
}
|
|
@@ -42,6 +63,14 @@ function useSelectionInteractions({
|
|
|
42
63
|
const editingPointerIds = /* @__PURE__ */ new Set();
|
|
43
64
|
let lastClick = null;
|
|
44
65
|
let dragCandidates = [];
|
|
66
|
+
let committedSelectionAtPointerDown = [];
|
|
67
|
+
const clearCommittedSelection = ({
|
|
68
|
+
rect
|
|
69
|
+
}) => {
|
|
70
|
+
hideSelectionFrame();
|
|
71
|
+
interactionSessionRef.current = IDLE_SESSION;
|
|
72
|
+
getWebsiteBuilderRpc()?.areaSelected({ rect, items: [] });
|
|
73
|
+
};
|
|
45
74
|
const stopDragAnimation = () => {
|
|
46
75
|
if (dragAnimationFrameId === null) {
|
|
47
76
|
return;
|
|
@@ -133,6 +162,9 @@ function useSelectionInteractions({
|
|
|
133
162
|
dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);
|
|
134
163
|
};
|
|
135
164
|
const handlePointerDown = (event) => {
|
|
165
|
+
if (event.button !== 0) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
136
168
|
editingPointerIds.delete(event.pointerId);
|
|
137
169
|
if (editingElementRef?.current && event.target instanceof Node && editingElementRef.current.contains(event.target)) {
|
|
138
170
|
editingPointerIds.add(event.pointerId);
|
|
@@ -141,6 +173,9 @@ function useSelectionInteractions({
|
|
|
141
173
|
event.preventDefault();
|
|
142
174
|
event.stopPropagation();
|
|
143
175
|
const scrollOffset = getDocumentScrollOffset();
|
|
176
|
+
committedSelectionAtPointerDown = getSelectedElementsFromSession(
|
|
177
|
+
interactionSessionRef.current
|
|
178
|
+
);
|
|
144
179
|
const dragStart = getDragPositionFromClient({
|
|
145
180
|
clientX: event.clientX,
|
|
146
181
|
clientY: event.clientY,
|
|
@@ -180,16 +215,59 @@ function useSelectionInteractions({
|
|
|
180
215
|
}
|
|
181
216
|
dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);
|
|
182
217
|
};
|
|
218
|
+
const commitAreaSelection = ({
|
|
219
|
+
selectedElements,
|
|
220
|
+
pageRect,
|
|
221
|
+
anchorRect,
|
|
222
|
+
scrollOffset
|
|
223
|
+
}) => {
|
|
224
|
+
const items = selectedElements.map((element) => ({
|
|
225
|
+
element,
|
|
226
|
+
data: buildElementDescriptor({
|
|
227
|
+
element,
|
|
228
|
+
clickPosition: getElementAnchor({
|
|
229
|
+
element,
|
|
230
|
+
selectionRect: anchorRect
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
}));
|
|
234
|
+
interactionSessionRef.current = {
|
|
235
|
+
type: "area",
|
|
236
|
+
pageRect,
|
|
237
|
+
items
|
|
238
|
+
};
|
|
239
|
+
showAreaSelectionFrames({ elements: selectedElements, scrollOffset });
|
|
240
|
+
const rect = getViewportRectFromPageRect({
|
|
241
|
+
rect: pageRect,
|
|
242
|
+
scrollOffset
|
|
243
|
+
});
|
|
244
|
+
void Promise.all(items.map((item) => maybeSymbolicate(item.data))).then(
|
|
245
|
+
(symbolicatedItems) => {
|
|
246
|
+
if (!isSelectionStillCurrent({
|
|
247
|
+
session: interactionSessionRef.current,
|
|
248
|
+
selection: { type: "area", pageRect }
|
|
249
|
+
})) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
getWebsiteBuilderRpc()?.areaSelected({
|
|
253
|
+
rect,
|
|
254
|
+
items: symbolicatedItems
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
};
|
|
183
259
|
const commitElementSelection = ({
|
|
184
|
-
|
|
260
|
+
selectionElement,
|
|
185
261
|
pointerEvent
|
|
186
262
|
}) => {
|
|
187
|
-
const selectionElement = getNearestSelectableElement(targetElement);
|
|
188
263
|
const targetRect = selectionElement.getBoundingClientRect();
|
|
189
|
-
const clickPosition = {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
264
|
+
const clickPosition = getElementAnchor({
|
|
265
|
+
element: selectionElement,
|
|
266
|
+
selectionRect: getPointRect({
|
|
267
|
+
x: pointerEvent.clientX,
|
|
268
|
+
y: pointerEvent.clientY
|
|
269
|
+
})
|
|
270
|
+
});
|
|
193
271
|
const scrollOffset = getDocumentScrollOffset();
|
|
194
272
|
const siblingElements = selectionElement instanceof HTMLElement ? getRepetitionSiblingElements({ element: selectionElement }) : [];
|
|
195
273
|
showElementSelectionFrames({
|
|
@@ -224,8 +302,8 @@ function useSelectionInteractions({
|
|
|
224
302
|
element: selectionElement,
|
|
225
303
|
siblingElements,
|
|
226
304
|
clickPosition: {
|
|
227
|
-
x:
|
|
228
|
-
y:
|
|
305
|
+
x: clickPosition.x - targetRect.left,
|
|
306
|
+
y: clickPosition.y - targetRect.top
|
|
229
307
|
},
|
|
230
308
|
data
|
|
231
309
|
};
|
|
@@ -239,7 +317,58 @@ function useSelectionInteractions({
|
|
|
239
317
|
getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });
|
|
240
318
|
});
|
|
241
319
|
};
|
|
320
|
+
const commitClickSelection = ({
|
|
321
|
+
targetElement,
|
|
322
|
+
pointerEvent,
|
|
323
|
+
candidates
|
|
324
|
+
}) => {
|
|
325
|
+
const selectionElement = getNearestSelectableElement(targetElement);
|
|
326
|
+
if (!isAdditiveSelectionClick(pointerEvent)) {
|
|
327
|
+
commitElementSelection({ selectionElement, pointerEvent });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
lastClick = null;
|
|
331
|
+
const pointerRect = getPointRect({
|
|
332
|
+
x: pointerEvent.clientX,
|
|
333
|
+
y: pointerEvent.clientY
|
|
334
|
+
});
|
|
335
|
+
const nextElements = getSelectionAfterAdditiveClick({
|
|
336
|
+
candidates,
|
|
337
|
+
currentSelectedElements: committedSelectionAtPointerDown,
|
|
338
|
+
targetElement: selectionElement
|
|
339
|
+
});
|
|
340
|
+
const [firstElement] = nextElements;
|
|
341
|
+
if (!firstElement) {
|
|
342
|
+
clearCommittedSelection({ rect: pointerRect });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (nextElements.length === 1) {
|
|
346
|
+
commitElementSelection({
|
|
347
|
+
selectionElement: firstElement,
|
|
348
|
+
pointerEvent
|
|
349
|
+
});
|
|
350
|
+
lastClick = null;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const scrollOffset = getDocumentScrollOffset();
|
|
354
|
+
const pageRect = unionElementPageRects({
|
|
355
|
+
elements: nextElements,
|
|
356
|
+
scrollOffset
|
|
357
|
+
});
|
|
358
|
+
if (!pageRect) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
commitAreaSelection({
|
|
362
|
+
selectedElements: nextElements,
|
|
363
|
+
pageRect,
|
|
364
|
+
anchorRect: pointerRect,
|
|
365
|
+
scrollOffset
|
|
366
|
+
});
|
|
367
|
+
};
|
|
242
368
|
const handlePointerUp = (event) => {
|
|
369
|
+
if (event.button !== 0) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
243
372
|
if (editingPointerIds.delete(event.pointerId)) {
|
|
244
373
|
return;
|
|
245
374
|
}
|
|
@@ -258,9 +387,10 @@ function useSelectionInteractions({
|
|
|
258
387
|
if (!(targetElement instanceof Element)) {
|
|
259
388
|
return;
|
|
260
389
|
}
|
|
261
|
-
|
|
390
|
+
commitClickSelection({
|
|
262
391
|
targetElement,
|
|
263
|
-
pointerEvent: event
|
|
392
|
+
pointerEvent: event,
|
|
393
|
+
candidates
|
|
264
394
|
});
|
|
265
395
|
return;
|
|
266
396
|
}
|
|
@@ -275,9 +405,10 @@ function useSelectionInteractions({
|
|
|
275
405
|
if (!(targetElement instanceof Element)) {
|
|
276
406
|
return;
|
|
277
407
|
}
|
|
278
|
-
|
|
408
|
+
commitClickSelection({
|
|
279
409
|
targetElement,
|
|
280
|
-
pointerEvent: event
|
|
410
|
+
pointerEvent: event,
|
|
411
|
+
candidates
|
|
281
412
|
});
|
|
282
413
|
return;
|
|
283
414
|
}
|
|
@@ -289,45 +420,14 @@ function useSelectionInteractions({
|
|
|
289
420
|
candidates,
|
|
290
421
|
selectionPageRect: pageRect
|
|
291
422
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
element: candidate.element,
|
|
297
|
-
selectionRect
|
|
298
|
-
})
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
interactionSessionRef.current = {
|
|
302
|
-
type: "area",
|
|
423
|
+
commitAreaSelection({
|
|
424
|
+
selectedElements: selectedCandidates.map(
|
|
425
|
+
(candidate) => candidate.element
|
|
426
|
+
),
|
|
303
427
|
pageRect,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
showSelectionFrames({
|
|
307
|
-
frames: selectedCandidates.map((candidate) => ({
|
|
308
|
-
variant: "area",
|
|
309
|
-
pageRect: {
|
|
310
|
-
x: candidate.pageRect.x,
|
|
311
|
-
y: candidate.pageRect.y,
|
|
312
|
-
width: candidate.pageRect.width,
|
|
313
|
-
height: candidate.pageRect.height
|
|
314
|
-
}
|
|
315
|
-
}))
|
|
428
|
+
anchorRect: selectionRect,
|
|
429
|
+
scrollOffset
|
|
316
430
|
});
|
|
317
|
-
void Promise.all(items.map(maybeSymbolicate)).then(
|
|
318
|
-
(symbolicatedItems) => {
|
|
319
|
-
if (!isSelectionStillCurrent({
|
|
320
|
-
session: interactionSessionRef.current,
|
|
321
|
-
selection: { type: "area", pageRect }
|
|
322
|
-
})) {
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
getWebsiteBuilderRpc()?.areaSelected({
|
|
326
|
-
rect: selectionRect,
|
|
327
|
-
items: symbolicatedItems
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
);
|
|
331
431
|
};
|
|
332
432
|
const handleClick = (event) => {
|
|
333
433
|
if (editingElementRef?.current && event.target instanceof Node && editingElementRef.current.contains(event.target)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../select-mode/hooks/useSelectionInteractions.ts"],
|
|
4
|
-
"sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeClickPosition,\n} from \"schemas/generated/selectMode\";\nimport type { DragCandidate } from \"../interactions/selectionGeometry\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useEffect } from \"react\";\n\nimport {\n buildElementDescriptor,\n buildSelector,\n getNearestSelectableElement,\n} from \"../descriptor/elementDescriptor\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport {\n collectDragCandidates,\n getCandidatesInSelection,\n getDocumentScrollOffset,\n getDragPositionFromClient,\n getElementAnchor,\n getPageRectFromViewportRect,\n getViewportRectFromPageRect,\n getViewportSelectionRect,\n} from \"../interactions/selectionGeometry\";\nimport {\n showElementSelectionFrames,\n showSelectionFrames,\n} from \"../selectionFrame\";\nimport { isSelectionStillCurrent } from \"../selectionPublish\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { isTextEditableElement } from \"./useEditModeStateMachine\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\nconst DOUBLE_CLICK_THRESHOLD_MS = 500;\n\n// React fibers carry only Turbopack chunk paths; resolving them to the\n// user's source file is async and only meaningful for \"react-element\"\n// kinds. Plan selectables already point at the right thing.\nasync function maybeSymbolicate(data: SelectableData): Promise<SelectableData> {\n return data.kind === \"react-element\"\n ? symbolicateSelectedElement(data)\n : data;\n}\n\n/**\n * Owns the iframe-side pointer interactions that drive selection:\n *\n * - On `pointerdown` we open a `drag` session (storing both client + page\n * coords plus the current scroll offset). We don't yet know if this will\n * become a click or an actual drag.\n * - On `pointermove` we update the drag rect and post `DRAG_RECT_UPDATED`\n * to the parent so the live drag rectangle can render. We only flip\n * `hasExceededThreshold` once the rect grows past 6\u00D76 px so that a tiny\n * accidental drag still resolves as a click.\n * - On `pointerup` we either:\n * - resolve as an `ELEMENT_SELECTED` (single click / sub-threshold drag)\n * - resolve as an `AREA_SELECTED` (drag past threshold), filtering the\n * candidates snapshotted at drag start against the final rectangle to\n * enumerate which descendants the user enclosed.\n * - During an active drag a `requestAnimationFrame` loop keeps the rect in\n * sync with the page scroll so the rectangle visually \"sticks\" to the\n * page rather than the viewport.\n *\n * We capture-listen and `preventDefault`/`stopPropagation` clicks so the\n * underlying page can't navigate or trigger handlers while the user is\n * working in selection mode.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useSelectionInteractions({\n displayMode,\n editingElementRef,\n interactionSessionRef,\n onBeginEditing,\n}: {\n displayMode: DisplayMode;\n editingElementRef?: MutableRefObject<HTMLElement | null>;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n onBeginEditing: (args: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => void;\n}) {\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n if (displayMode !== \"selection\") {\n return;\n }\n\n let dragAnimationFrameId: number | null = null;\n const editingPointerIds = new Set<number>();\n let lastClick: {\n descriptorPromise: Promise<SelectableData>;\n target: Element;\n time: number;\n } | null = null;\n // Snapshot of the page's meaningful elements + their page rects, taken\n // once when a drag begins so per-move hit-testing avoids a full-DOM scan.\n let dragCandidates: DragCandidate[] = [];\n\n const stopDragAnimation = () => {\n if (dragAnimationFrameId === null) {\n return;\n }\n\n cancelAnimationFrame(dragAnimationFrameId);\n dragAnimationFrameId = null;\n };\n\n // `notifyParentDragCleared` posts a null drag rect so the parent tears\n // down the live drag overlay. The area-commit path suppresses it: the\n // parent maps a null drag rect to `selection = none`, which round-trips\n // back as `setSelectionType(\"none\")` and wipes the in-iframe frames we're\n // about to paint. The follow-up `areaSelected` supersedes the drag overlay\n // on its own, so the null post is pure collateral damage there.\n const resetDragState = ({\n notifyParentDragCleared = true,\n }: { notifyParentDragCleared?: boolean } = {}) => {\n const currentSession = interactionSessionRef.current;\n\n stopDragAnimation();\n\n if (currentSession.type !== \"drag\") {\n dragCandidates = [];\n return;\n }\n\n if (notifyParentDragCleared && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n\n dragCandidates = [];\n interactionSessionRef.current = IDLE_SESSION;\n };\n\n const postCurrentDragRect = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n const rect = getViewportSelectionRect({\n dragStart: session.dragStart,\n dragCurrent: session.dragCurrent,\n scrollOffset: session.scrollOffset,\n });\n\n if (\n rect.width <= 6 &&\n rect.height <= 6 &&\n !session.hasExceededThreshold\n ) {\n return;\n }\n\n if (!session.hasExceededThreshold) {\n interactionSessionRef.current = {\n ...session,\n hasExceededThreshold: true,\n };\n }\n\n // Cheap hit-test against the candidates we snapshotted at drag start,\n // projected back to viewport coords so the parent can paint a live\n // highlight on each element the drag will capture. No DOM scan here.\n const selectionPageRect = getPageRectFromViewportRect({\n rect,\n scrollOffset: session.scrollOffset,\n });\n const selectionRects = getCandidatesInSelection({\n candidates: dragCandidates,\n selectionPageRect,\n }).map((candidate) => ({\n id: candidate.id,\n rect: getViewportRectFromPageRect({\n rect: candidate.pageRect,\n scrollOffset: session.scrollOffset,\n }),\n }));\n\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: rect,\n selectionRects,\n });\n };\n\n const syncDragRectToViewport = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n dragAnimationFrameId = null;\n return;\n }\n\n const nextScrollOffset = getDocumentScrollOffset();\n const nextDragCurrent = getDragPositionFromClient({\n clientX: session.dragCurrent.clientX,\n clientY: session.dragCurrent.clientY,\n scrollOffset: nextScrollOffset,\n });\n\n if (\n nextDragCurrent.pageX !== session.dragCurrent.pageX ||\n nextDragCurrent.pageY !== session.dragCurrent.pageY\n ) {\n interactionSessionRef.current = {\n ...session,\n dragCurrent: nextDragCurrent,\n scrollOffset: nextScrollOffset,\n };\n postCurrentDragRect();\n } else {\n interactionSessionRef.current = {\n ...session,\n scrollOffset: nextScrollOffset,\n };\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n editingPointerIds.delete(event.pointerId);\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n editingPointerIds.add(event.pointerId);\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n\n const scrollOffset = getDocumentScrollOffset();\n const dragStart = getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset,\n });\n interactionSessionRef.current = {\n type: \"drag\",\n dragStart,\n dragCurrent: dragStart,\n scrollOffset,\n hasExceededThreshold: false,\n };\n dragCandidates = collectDragCandidates(scrollOffset);\n\n if (dragAnimationFrameId === null) {\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n }\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n interactionSessionRef.current = {\n ...session,\n dragCurrent: getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset: getDocumentScrollOffset(),\n }),\n };\n postCurrentDragRect();\n };\n\n const handleViewportChangeDuringDrag = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\" || dragAnimationFrameId !== null) {\n return;\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const commitElementSelection = ({\n targetElement,\n pointerEvent,\n }: {\n targetElement: Element;\n pointerEvent: PointerEvent;\n }) => {\n const selectionElement = getNearestSelectableElement(targetElement);\n const targetRect = selectionElement.getBoundingClientRect();\n const clickPosition: SelectModeClickPosition = {\n x: pointerEvent.clientX,\n y: pointerEvent.clientY,\n };\n\n // Paint the in-iframe selection frames synchronously for instant\n // feedback \u2014 symbolicate is a network roundtrip and we don't want\n // the visual selection to wait on it.\n const scrollOffset = getDocumentScrollOffset();\n const siblingElements =\n selectionElement instanceof HTMLElement\n ? getRepetitionSiblingElements({ element: selectionElement })\n : [];\n showElementSelectionFrames({\n elements: [selectionElement, ...siblingElements],\n scrollOffset,\n });\n\n const now = pointerEvent.timeStamp;\n const previousClick = lastClick;\n if (\n previousClick &&\n previousClick.target === selectionElement &&\n now - previousClick.time <= DOUBLE_CLICK_THRESHOLD_MS &&\n isTextEditableElement(selectionElement)\n ) {\n lastClick = null;\n onBeginEditing({\n element: selectionElement,\n descriptorPromise: previousClick.descriptorPromise,\n });\n return;\n }\n\n // NOTE (Noah, 2026-07-24): Commit the selection immediately rather than\n // deferring it behind the double-click window. A single click always\n // selects, and a double-click is just \"select + edit\", so there's nothing\n // to suppress \u2014 waiting only added a full DOUBLE_CLICK_THRESHOLD_MS of\n // latency to every click and left `interactionSessionRef` non-element in\n // the gap. Double-click is still detected via the `lastClick` timestamp\n // above; the follow-up click reuses this descriptor to open the editor.\n const data = buildElementDescriptor({\n element: selectionElement,\n clickPosition,\n siblingSelectors: siblingElements.map(buildSelector),\n });\n lastClick = {\n descriptorPromise: Promise.resolve(data),\n target: selectionElement,\n time: now,\n };\n\n if (!selectionElement.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element: selectionElement,\n siblingElements,\n clickPosition: {\n x: pointerEvent.clientX - targetRect.left,\n y: pointerEvent.clientY - targetRect.top,\n },\n data,\n };\n void maybeSymbolicate(data).then((symbolicated) => {\n if (\n !isSelectionStillCurrent({\n session: interactionSessionRef.current,\n selection: { type: \"element\", element: selectionElement },\n })\n ) {\n return;\n }\n getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });\n });\n };\n\n const handlePointerUp = (event: PointerEvent) => {\n if (editingPointerIds.delete(event.pointerId)) {\n return;\n }\n // Clicks inside the element being edited belong to the inline editor, not\n // select mode. Without this guard the double-click detection below re-runs\n // `onBeginEditing` on the same element (e.g. when repositioning the\n // caret), rebuilding the editor over itself and wiping the content.\n // Mirrors the guards in `handlePointerDown` and `handleClick`.\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n const snapshot = interactionSessionRef.current;\n const wasDragging =\n snapshot.type === \"drag\" ? snapshot.hasExceededThreshold : false;\n const dragStart = snapshot.type === \"drag\" ? snapshot.dragStart : null;\n const dragCurrent =\n snapshot.type === \"drag\" ? snapshot.dragCurrent : null;\n const scrollOffset =\n snapshot.type === \"drag\"\n ? snapshot.scrollOffset\n : getDocumentScrollOffset();\n // `resetDragState` clears `dragCandidates`, so grab them first \u2014 the\n // final committed set is the same cached candidates the live preview\n // used, so preview and result never disagree.\n const candidates = dragCandidates;\n\n // Suppress the null drag-rect post: if this resolves to an area\n // selection it would otherwise flash the parent to `selection = none`\n // and wipe the frames we paint below. Click/empty-drag paths don't\n // depend on it (a sub-threshold drag never posted a rect to clear).\n resetDragState({ notifyParentDragCleared: false });\n\n if (!dragStart || snapshot.type !== \"drag\") {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitElementSelection({\n targetElement,\n pointerEvent: event,\n });\n return;\n }\n\n const resolvedDragCurrent = dragCurrent ?? dragStart;\n const selectionRect = getViewportSelectionRect({\n dragStart,\n dragCurrent: resolvedDragCurrent,\n scrollOffset,\n });\n\n if (!wasDragging) {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitElementSelection({\n targetElement,\n pointerEvent: event,\n });\n return;\n }\n\n const pageRect = getPageRectFromViewportRect({\n rect: selectionRect,\n scrollOffset,\n });\n\n const selectedCandidates = getCandidatesInSelection({\n candidates,\n selectionPageRect: pageRect,\n });\n const items = selectedCandidates.map((candidate) => {\n return buildElementDescriptor({\n element: candidate.element,\n clickPosition: getElementAnchor({\n element: candidate.element,\n selectionRect,\n }),\n });\n });\n\n interactionSessionRef.current = {\n type: \"area\",\n pageRect,\n items,\n };\n\n // Highlight each captured element so the committed area selection\n // \"sticks\" inside the iframe \u2014 matching how a single click stays\n // outlined \u2014 rather than drawing one faint box over the drag region.\n showSelectionFrames({\n frames: selectedCandidates.map((candidate) => ({\n variant: \"area\" as const,\n pageRect: {\n x: candidate.pageRect.x,\n y: candidate.pageRect.y,\n width: candidate.pageRect.width,\n height: candidate.pageRect.height,\n },\n })),\n });\n\n void Promise.all(items.map(maybeSymbolicate)).then(\n (symbolicatedItems) => {\n if (\n !isSelectionStillCurrent({\n session: interactionSessionRef.current,\n selection: { type: \"area\", pageRect },\n })\n ) {\n return;\n }\n getWebsiteBuilderRpc()?.areaSelected({\n rect: selectionRect,\n items: symbolicatedItems,\n });\n },\n );\n };\n\n const handleClick = (event: MouseEvent) => {\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n };\n\n const handlePointerCancel = (event: PointerEvent) => {\n editingPointerIds.delete(event.pointerId);\n resetDragState();\n };\n\n const handleWindowBlur = () => {\n editingPointerIds.clear();\n resetDragState();\n };\n\n document.addEventListener(\"pointerdown\", handlePointerDown, true);\n document.addEventListener(\"pointermove\", handlePointerMove, true);\n document.addEventListener(\"pointerup\", handlePointerUp, true);\n document.addEventListener(\"pointercancel\", handlePointerCancel, true);\n document.addEventListener(\"click\", handleClick, true);\n document.addEventListener(\"scroll\", handleViewportChangeDuringDrag, true);\n window.addEventListener(\"blur\", handleWindowBlur);\n window.addEventListener(\"resize\", handleViewportChangeDuringDrag);\n\n return () => {\n document.removeEventListener(\"pointerdown\", handlePointerDown, true);\n document.removeEventListener(\"pointermove\", handlePointerMove, true);\n document.removeEventListener(\"pointerup\", handlePointerUp, true);\n document.removeEventListener(\"pointercancel\", handlePointerCancel, true);\n document.removeEventListener(\"click\", handleClick, true);\n document.removeEventListener(\n \"scroll\",\n handleViewportChangeDuringDrag,\n true,\n );\n window.removeEventListener(\"blur\", handleWindowBlur);\n window.removeEventListener(\"resize\", handleViewportChangeDuringDrag);\n resetDragState();\n };\n }, [displayMode, editingElementRef, interactionSessionRef, onBeginEditing]);\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeBoundingRect,\n} from \"schemas/generated/selectMode\";\nimport type { DragCandidate } from \"../interactions/selectionGeometry\";\nimport type {\n SelectModeAreaItem,\n SelectModeInteractionSession,\n} from \"../selectModeSession\";\n\nimport { useEffect } from \"react\";\n\nimport {\n buildElementDescriptor,\n buildSelector,\n getNearestSelectableElement,\n} from \"../descriptor/elementDescriptor\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport {\n collectDragCandidates,\n getCandidatesInSelection,\n getDocumentScrollOffset,\n getDragPositionFromClient,\n getElementAnchor,\n getPageRectFromViewportRect,\n getPointRect,\n getSelectionAfterAdditiveClick,\n getViewportRectFromPageRect,\n getViewportSelectionRect,\n isAdditiveSelectionClick,\n unionElementPageRects,\n} from \"../interactions/selectionGeometry\";\nimport {\n hideSelectionFrame,\n showAreaSelectionFrames,\n showElementSelectionFrames,\n} from \"../selectionFrame\";\nimport { isSelectionStillCurrent } from \"../selectionPublish\";\nimport {\n getConnectedSelectedElement,\n IDLE_SESSION,\n} from \"../selectModeSession\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { isTextEditableElement } from \"./useEditModeStateMachine\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\nconst DOUBLE_CLICK_THRESHOLD_MS = 500;\n\nfunction getSelectedElementsFromSession(\n session: SelectModeInteractionSession,\n): Element[] {\n if (session.type === \"element\") {\n const liveElement = getConnectedSelectedElement(session);\n return liveElement ? [liveElement] : [];\n }\n if (session.type === \"area\") {\n return session.items.flatMap((item) => {\n const liveElement = getConnectedSelectedElement(item);\n return liveElement ? [liveElement] : [];\n });\n }\n return [];\n}\n\n// React fibers carry only Turbopack chunk paths; resolving them to the\n// user's source file is async and only meaningful for \"react-element\"\n// kinds. Plan selectables already point at the right thing.\nasync function maybeSymbolicate(data: SelectableData): Promise<SelectableData> {\n return data.kind === \"react-element\"\n ? symbolicateSelectedElement(data)\n : data;\n}\n\n/**\n * Owns the iframe-side pointer interactions that drive selection:\n *\n * - On `pointerdown` we open a `drag` session (storing both client + page\n * coords plus the current scroll offset). We don't yet know if this will\n * become a click or an actual drag.\n * - On `pointermove` we update the drag rect and post `DRAG_RECT_UPDATED`\n * to the parent so the live drag rectangle can render. We only flip\n * `hasExceededThreshold` once the rect grows past 6\u00D76 px so that a tiny\n * accidental drag still resolves as a click.\n * - On `pointerup` we either:\n * - resolve as an `ELEMENT_SELECTED` (single click / sub-threshold drag)\n * - resolve as an `AREA_SELECTED` (drag past threshold), filtering the\n * candidates snapshotted at drag start against the final rectangle to\n * enumerate which descendants the user enclosed.\n * - During an active drag a `requestAnimationFrame` loop keeps the rect in\n * sync with the page scroll so the rectangle visually \"sticks\" to the\n * page rather than the viewport.\n *\n * We capture-listen and `preventDefault`/`stopPropagation` clicks so the\n * underlying page can't navigate or trigger handlers while the user is\n * working in selection mode.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useSelectionInteractions({\n displayMode,\n editingElementRef,\n interactionSessionRef,\n onBeginEditing,\n}: {\n displayMode: DisplayMode;\n editingElementRef?: MutableRefObject<HTMLElement | null>;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n onBeginEditing: (args: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => void;\n}) {\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n if (displayMode !== \"selection\") {\n return;\n }\n\n let dragAnimationFrameId: number | null = null;\n const editingPointerIds = new Set<number>();\n let lastClick: {\n descriptorPromise: Promise<SelectableData>;\n target: Element;\n time: number;\n } | null = null;\n // Snapshot of the page's meaningful elements + their page rects, taken\n // once when a drag begins so per-move hit-testing avoids a full-DOM scan.\n let dragCandidates: DragCandidate[] = [];\n // Pointerdown always opens a drag session, so stash the committed\n // selection here. A sub-threshold Shift/Command click reads it back.\n let committedSelectionAtPointerDown: Element[] = [];\n\n const clearCommittedSelection = ({\n rect,\n }: {\n rect: SelectModeBoundingRect;\n }) => {\n hideSelectionFrame();\n interactionSessionRef.current = IDLE_SESSION;\n getWebsiteBuilderRpc()?.areaSelected({ rect, items: [] });\n };\n\n const stopDragAnimation = () => {\n if (dragAnimationFrameId === null) {\n return;\n }\n\n cancelAnimationFrame(dragAnimationFrameId);\n dragAnimationFrameId = null;\n };\n\n // `notifyParentDragCleared` posts a null drag rect so the parent tears\n // down the live drag overlay. The area-commit path suppresses it: the\n // parent maps a null drag rect to `selection = none`, which round-trips\n // back as `setSelectionType(\"none\")` and wipes the in-iframe frames we're\n // about to paint. The follow-up `areaSelected` supersedes the drag overlay\n // on its own, so the null post is pure collateral damage there.\n const resetDragState = ({\n notifyParentDragCleared = true,\n }: { notifyParentDragCleared?: boolean } = {}) => {\n const currentSession = interactionSessionRef.current;\n\n stopDragAnimation();\n\n if (currentSession.type !== \"drag\") {\n dragCandidates = [];\n return;\n }\n\n if (notifyParentDragCleared && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n\n dragCandidates = [];\n interactionSessionRef.current = IDLE_SESSION;\n };\n\n const postCurrentDragRect = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n const rect = getViewportSelectionRect({\n dragStart: session.dragStart,\n dragCurrent: session.dragCurrent,\n scrollOffset: session.scrollOffset,\n });\n\n if (\n rect.width <= 6 &&\n rect.height <= 6 &&\n !session.hasExceededThreshold\n ) {\n return;\n }\n\n if (!session.hasExceededThreshold) {\n interactionSessionRef.current = {\n ...session,\n hasExceededThreshold: true,\n };\n }\n\n // Cheap hit-test against the candidates we snapshotted at drag start,\n // projected back to viewport coords so the parent can paint a live\n // highlight on each element the drag will capture. No DOM scan here.\n const selectionPageRect = getPageRectFromViewportRect({\n rect,\n scrollOffset: session.scrollOffset,\n });\n const selectionRects = getCandidatesInSelection({\n candidates: dragCandidates,\n selectionPageRect,\n }).map((candidate) => ({\n id: candidate.id,\n rect: getViewportRectFromPageRect({\n rect: candidate.pageRect,\n scrollOffset: session.scrollOffset,\n }),\n }));\n\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: rect,\n selectionRects,\n });\n };\n\n const syncDragRectToViewport = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n dragAnimationFrameId = null;\n return;\n }\n\n const nextScrollOffset = getDocumentScrollOffset();\n const nextDragCurrent = getDragPositionFromClient({\n clientX: session.dragCurrent.clientX,\n clientY: session.dragCurrent.clientY,\n scrollOffset: nextScrollOffset,\n });\n\n if (\n nextDragCurrent.pageX !== session.dragCurrent.pageX ||\n nextDragCurrent.pageY !== session.dragCurrent.pageY\n ) {\n interactionSessionRef.current = {\n ...session,\n dragCurrent: nextDragCurrent,\n scrollOffset: nextScrollOffset,\n };\n postCurrentDragRect();\n } else {\n interactionSessionRef.current = {\n ...session,\n scrollOffset: nextScrollOffset,\n };\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (event.button !== 0) {\n return;\n }\n editingPointerIds.delete(event.pointerId);\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n editingPointerIds.add(event.pointerId);\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n\n const scrollOffset = getDocumentScrollOffset();\n committedSelectionAtPointerDown = getSelectedElementsFromSession(\n interactionSessionRef.current,\n );\n const dragStart = getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset,\n });\n interactionSessionRef.current = {\n type: \"drag\",\n dragStart,\n dragCurrent: dragStart,\n scrollOffset,\n hasExceededThreshold: false,\n };\n dragCandidates = collectDragCandidates(scrollOffset);\n\n if (dragAnimationFrameId === null) {\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n }\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n interactionSessionRef.current = {\n ...session,\n dragCurrent: getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset: getDocumentScrollOffset(),\n }),\n };\n postCurrentDragRect();\n };\n\n const handleViewportChangeDuringDrag = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\" || dragAnimationFrameId !== null) {\n return;\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const commitAreaSelection = ({\n selectedElements,\n pageRect,\n anchorRect,\n scrollOffset,\n }: {\n selectedElements: Element[];\n pageRect: SelectModeBoundingRect;\n // Viewport rect each member's click anchor is clamped toward: the drag\n // rect for drag-select, the pointer for a modifier click.\n anchorRect: SelectModeBoundingRect;\n scrollOffset: ReturnType<typeof getDocumentScrollOffset>;\n }) => {\n const items: SelectModeAreaItem[] = selectedElements.map((element) => ({\n element,\n data: buildElementDescriptor({\n element,\n clickPosition: getElementAnchor({\n element,\n selectionRect: anchorRect,\n }),\n }),\n }));\n\n interactionSessionRef.current = {\n type: \"area\",\n pageRect,\n items,\n };\n\n // Highlight each captured element so the committed area selection\n // \"sticks\" inside the iframe \u2014 matching how a single click stays\n // outlined \u2014 rather than drawing one faint box over the drag region.\n showAreaSelectionFrames({ elements: selectedElements, scrollOffset });\n\n const rect = getViewportRectFromPageRect({\n rect: pageRect,\n scrollOffset,\n });\n void Promise.all(items.map((item) => maybeSymbolicate(item.data))).then(\n (symbolicatedItems) => {\n if (\n !isSelectionStillCurrent({\n session: interactionSessionRef.current,\n selection: { type: \"area\", pageRect },\n })\n ) {\n return;\n }\n getWebsiteBuilderRpc()?.areaSelected({\n rect,\n items: symbolicatedItems,\n });\n },\n );\n };\n\n const commitElementSelection = ({\n selectionElement,\n pointerEvent,\n }: {\n selectionElement: Element;\n pointerEvent: PointerEvent;\n }) => {\n const targetRect = selectionElement.getBoundingClientRect();\n // A modifier click can commit an element the pointer is not over\n // (deselecting down to one), so clamp the anchor into its bounds.\n const clickPosition = getElementAnchor({\n element: selectionElement,\n selectionRect: getPointRect({\n x: pointerEvent.clientX,\n y: pointerEvent.clientY,\n }),\n });\n\n // Paint the in-iframe selection frames synchronously for instant\n // feedback \u2014 symbolicate is a network roundtrip and we don't want\n // the visual selection to wait on it.\n const scrollOffset = getDocumentScrollOffset();\n const siblingElements =\n selectionElement instanceof HTMLElement\n ? getRepetitionSiblingElements({ element: selectionElement })\n : [];\n showElementSelectionFrames({\n elements: [selectionElement, ...siblingElements],\n scrollOffset,\n });\n\n const now = pointerEvent.timeStamp;\n const previousClick = lastClick;\n if (\n previousClick &&\n previousClick.target === selectionElement &&\n now - previousClick.time <= DOUBLE_CLICK_THRESHOLD_MS &&\n isTextEditableElement(selectionElement)\n ) {\n lastClick = null;\n onBeginEditing({\n element: selectionElement,\n descriptorPromise: previousClick.descriptorPromise,\n });\n return;\n }\n\n // NOTE (Noah, 2026-07-24): Commit the selection immediately rather than\n // deferring it behind the double-click window. A single click always\n // selects, and a double-click is just \"select + edit\", so there's nothing\n // to suppress \u2014 waiting only added a full DOUBLE_CLICK_THRESHOLD_MS of\n // latency to every click and left `interactionSessionRef` non-element in\n // the gap. Double-click is still detected via the `lastClick` timestamp\n // above; the follow-up click reuses this descriptor to open the editor.\n const data = buildElementDescriptor({\n element: selectionElement,\n clickPosition,\n siblingSelectors: siblingElements.map(buildSelector),\n });\n lastClick = {\n descriptorPromise: Promise.resolve(data),\n target: selectionElement,\n time: now,\n };\n\n if (!selectionElement.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element: selectionElement,\n siblingElements,\n clickPosition: {\n x: clickPosition.x - targetRect.left,\n y: clickPosition.y - targetRect.top,\n },\n data,\n };\n void maybeSymbolicate(data).then((symbolicated) => {\n if (\n !isSelectionStillCurrent({\n session: interactionSessionRef.current,\n selection: { type: \"element\", element: selectionElement },\n })\n ) {\n return;\n }\n getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });\n });\n };\n\n const commitClickSelection = ({\n targetElement,\n pointerEvent,\n candidates,\n }: {\n targetElement: Element;\n pointerEvent: PointerEvent;\n candidates: DragCandidate[];\n }) => {\n const selectionElement = getNearestSelectableElement(targetElement);\n if (!isAdditiveSelectionClick(pointerEvent)) {\n commitElementSelection({ selectionElement, pointerEvent });\n return;\n }\n\n // A modifier click is never the second half of a double-click.\n lastClick = null;\n const pointerRect = getPointRect({\n x: pointerEvent.clientX,\n y: pointerEvent.clientY,\n });\n const nextElements = getSelectionAfterAdditiveClick({\n candidates,\n currentSelectedElements: committedSelectionAtPointerDown,\n targetElement: selectionElement,\n });\n const [firstElement] = nextElements;\n if (!firstElement) {\n clearCommittedSelection({ rect: pointerRect });\n return;\n }\n if (nextElements.length === 1) {\n commitElementSelection({\n selectionElement: firstElement,\n pointerEvent,\n });\n // The survivor was not clicked, so it must not arm double-click.\n lastClick = null;\n return;\n }\n const scrollOffset = getDocumentScrollOffset();\n const pageRect = unionElementPageRects({\n elements: nextElements,\n scrollOffset,\n });\n if (!pageRect) {\n return;\n }\n commitAreaSelection({\n selectedElements: nextElements,\n pageRect,\n anchorRect: pointerRect,\n scrollOffset,\n });\n };\n\n const handlePointerUp = (event: PointerEvent) => {\n if (event.button !== 0) {\n return;\n }\n if (editingPointerIds.delete(event.pointerId)) {\n return;\n }\n // Clicks inside the element being edited belong to the inline editor, not\n // select mode. Without this guard the double-click detection below re-runs\n // `onBeginEditing` on the same element (e.g. when repositioning the\n // caret), rebuilding the editor over itself and wiping the content.\n // Mirrors the guards in `handlePointerDown` and `handleClick`.\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n const snapshot = interactionSessionRef.current;\n const wasDragging =\n snapshot.type === \"drag\" ? snapshot.hasExceededThreshold : false;\n const dragStart = snapshot.type === \"drag\" ? snapshot.dragStart : null;\n const dragCurrent =\n snapshot.type === \"drag\" ? snapshot.dragCurrent : null;\n const scrollOffset =\n snapshot.type === \"drag\"\n ? snapshot.scrollOffset\n : getDocumentScrollOffset();\n // `resetDragState` clears `dragCandidates`, so grab them first \u2014 the\n // final committed set is the same cached candidates the live preview\n // used, so preview and result never disagree.\n const candidates = dragCandidates;\n\n // Suppress the null drag-rect post: if this resolves to an area\n // selection it would otherwise flash the parent to `selection = none`\n // and wipe the frames we paint below. Click/empty-drag paths don't\n // depend on it (a sub-threshold drag never posted a rect to clear).\n resetDragState({ notifyParentDragCleared: false });\n\n if (!dragStart || snapshot.type !== \"drag\") {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitClickSelection({\n targetElement,\n pointerEvent: event,\n candidates,\n });\n return;\n }\n\n const resolvedDragCurrent = dragCurrent ?? dragStart;\n const selectionRect = getViewportSelectionRect({\n dragStart,\n dragCurrent: resolvedDragCurrent,\n scrollOffset,\n });\n\n if (!wasDragging) {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitClickSelection({\n targetElement,\n pointerEvent: event,\n candidates,\n });\n return;\n }\n\n const pageRect = getPageRectFromViewportRect({\n rect: selectionRect,\n scrollOffset,\n });\n\n const selectedCandidates = getCandidatesInSelection({\n candidates,\n selectionPageRect: pageRect,\n });\n commitAreaSelection({\n selectedElements: selectedCandidates.map(\n (candidate) => candidate.element,\n ),\n pageRect,\n anchorRect: selectionRect,\n scrollOffset,\n });\n };\n\n const handleClick = (event: MouseEvent) => {\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n };\n\n const handlePointerCancel = (event: PointerEvent) => {\n editingPointerIds.delete(event.pointerId);\n resetDragState();\n };\n\n const handleWindowBlur = () => {\n editingPointerIds.clear();\n resetDragState();\n };\n\n document.addEventListener(\"pointerdown\", handlePointerDown, true);\n document.addEventListener(\"pointermove\", handlePointerMove, true);\n document.addEventListener(\"pointerup\", handlePointerUp, true);\n document.addEventListener(\"pointercancel\", handlePointerCancel, true);\n document.addEventListener(\"click\", handleClick, true);\n document.addEventListener(\"scroll\", handleViewportChangeDuringDrag, true);\n window.addEventListener(\"blur\", handleWindowBlur);\n window.addEventListener(\"resize\", handleViewportChangeDuringDrag);\n\n return () => {\n document.removeEventListener(\"pointerdown\", handlePointerDown, true);\n document.removeEventListener(\"pointermove\", handlePointerMove, true);\n document.removeEventListener(\"pointerup\", handlePointerUp, true);\n document.removeEventListener(\"pointercancel\", handlePointerCancel, true);\n document.removeEventListener(\"click\", handleClick, true);\n document.removeEventListener(\n \"scroll\",\n handleViewportChangeDuringDrag,\n true,\n );\n window.removeEventListener(\"blur\", handleWindowBlur);\n window.removeEventListener(\"resize\", handleViewportChangeDuringDrag);\n resetDragState();\n };\n }, [displayMode, editingElementRef, interactionSessionRef, onBeginEditing]);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -86,6 +86,31 @@ export declare function getCandidatesInSelection({ candidates, selectionPageRect
|
|
|
86
86
|
candidates: DragCandidate[];
|
|
87
87
|
selectionPageRect: SelectModeBoundingRect;
|
|
88
88
|
}): DragCandidate[];
|
|
89
|
+
/**
|
|
90
|
+
* Shift / Command / Ctrl click toggles the clicked element in the current
|
|
91
|
+
* selection (theme-builder canvas rules): a click inside an already-selected
|
|
92
|
+
* ancestor is ignored, selecting a parent drops its selected descendants,
|
|
93
|
+
* and once every candidate child of a candidate parent is selected the
|
|
94
|
+
* parent replaces them — the same collapse drag-select applies.
|
|
95
|
+
*/
|
|
96
|
+
export declare function getSelectionAfterAdditiveClick({ candidates, currentSelectedElements, targetElement, }: {
|
|
97
|
+
candidates: DragCandidate[];
|
|
98
|
+
currentSelectedElements: Element[];
|
|
99
|
+
targetElement: Element;
|
|
100
|
+
}): Element[];
|
|
101
|
+
export declare function isAdditiveSelectionClick(event: {
|
|
102
|
+
shiftKey: boolean;
|
|
103
|
+
metaKey: boolean;
|
|
104
|
+
ctrlKey: boolean;
|
|
105
|
+
}): boolean;
|
|
106
|
+
export declare function getPointRect({ x, y, }: {
|
|
107
|
+
x: number;
|
|
108
|
+
y: number;
|
|
109
|
+
}): SelectModeBoundingRect;
|
|
110
|
+
export declare function unionElementPageRects({ elements, scrollOffset, }: {
|
|
111
|
+
elements: Element[];
|
|
112
|
+
scrollOffset: ScrollOffset;
|
|
113
|
+
}): SelectModeBoundingRect | null;
|
|
89
114
|
/**
|
|
90
115
|
* Picks a "click position" for an element that fell inside an area
|
|
91
116
|
* selection. We use the corner of the element nearest the top-left of the
|
|
@@ -154,6 +154,84 @@ function getCandidatesInSelection({
|
|
|
154
154
|
});
|
|
155
155
|
return collapseFullyCoveredChildren({ candidates, selected: intersecting });
|
|
156
156
|
}
|
|
157
|
+
function getSelectionAfterAdditiveClick({
|
|
158
|
+
candidates,
|
|
159
|
+
currentSelectedElements,
|
|
160
|
+
targetElement
|
|
161
|
+
}) {
|
|
162
|
+
if (currentSelectedElements.includes(targetElement)) {
|
|
163
|
+
return currentSelectedElements.filter(
|
|
164
|
+
(element) => element !== targetElement
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (currentSelectedElements.some((element) => element.contains(targetElement))) {
|
|
168
|
+
return currentSelectedElements;
|
|
169
|
+
}
|
|
170
|
+
const selected = new Set(
|
|
171
|
+
currentSelectedElements.filter(
|
|
172
|
+
(element) => !targetElement.contains(element)
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
selected.add(targetElement);
|
|
176
|
+
const candidateElements = new Set(
|
|
177
|
+
candidates.map((candidate) => candidate.element)
|
|
178
|
+
);
|
|
179
|
+
let parent = targetElement.parentElement;
|
|
180
|
+
while (parent && candidateElements.has(parent)) {
|
|
181
|
+
const candidateChildren = Array.from(parent.children).filter((child) => {
|
|
182
|
+
return candidateElements.has(child);
|
|
183
|
+
});
|
|
184
|
+
if (candidateChildren.length === 0 || !candidateChildren.every((child) => selected.has(child))) {
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
for (const child of candidateChildren) {
|
|
188
|
+
selected.delete(child);
|
|
189
|
+
}
|
|
190
|
+
selected.add(parent);
|
|
191
|
+
parent = parent.parentElement;
|
|
192
|
+
}
|
|
193
|
+
return [...selected];
|
|
194
|
+
}
|
|
195
|
+
function isAdditiveSelectionClick(event) {
|
|
196
|
+
return event.shiftKey || event.metaKey || event.ctrlKey;
|
|
197
|
+
}
|
|
198
|
+
function getPointRect({
|
|
199
|
+
x,
|
|
200
|
+
y
|
|
201
|
+
}) {
|
|
202
|
+
return { x, y, width: 0, height: 0, top: y, right: x, bottom: y, left: x };
|
|
203
|
+
}
|
|
204
|
+
function unionElementPageRects({
|
|
205
|
+
elements,
|
|
206
|
+
scrollOffset
|
|
207
|
+
}) {
|
|
208
|
+
const rects = elements.map((element) => {
|
|
209
|
+
return getPageRectFromViewportRect({
|
|
210
|
+
rect: getElementBoundingRect(element),
|
|
211
|
+
scrollOffset
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
const firstRect = rects[0];
|
|
215
|
+
if (!firstRect) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return rects.slice(1).reduce((union, rect) => {
|
|
219
|
+
const left = Math.min(union.left, rect.left);
|
|
220
|
+
const top = Math.min(union.top, rect.top);
|
|
221
|
+
const right = Math.max(union.right, rect.right);
|
|
222
|
+
const bottom = Math.max(union.bottom, rect.bottom);
|
|
223
|
+
return {
|
|
224
|
+
x: left,
|
|
225
|
+
y: top,
|
|
226
|
+
width: right - left,
|
|
227
|
+
height: bottom - top,
|
|
228
|
+
left,
|
|
229
|
+
top,
|
|
230
|
+
right,
|
|
231
|
+
bottom
|
|
232
|
+
};
|
|
233
|
+
}, firstRect);
|
|
234
|
+
}
|
|
157
235
|
function getElementAnchor({
|
|
158
236
|
element,
|
|
159
237
|
selectionRect
|
|
@@ -186,7 +264,11 @@ export {
|
|
|
186
264
|
getElementAnchor,
|
|
187
265
|
getElementBoundingRect,
|
|
188
266
|
getPageRectFromViewportRect,
|
|
267
|
+
getPointRect,
|
|
268
|
+
getSelectionAfterAdditiveClick,
|
|
189
269
|
getViewportRectFromPageRect,
|
|
190
|
-
getViewportSelectionRect
|
|
270
|
+
getViewportSelectionRect,
|
|
271
|
+
isAdditiveSelectionClick,
|
|
272
|
+
unionElementPageRects
|
|
191
273
|
};
|
|
192
274
|
//# sourceMappingURL=selectionGeometry.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../select-mode/interactions/selectionGeometry.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n SelectModeBoundingRect,\n SelectModeClickPosition,\n} from \"schemas/generated/selectMode\";\n\nimport { isIntersecting, isRectEnclosing } from \"replo-utils/lib/geometry\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { isInlineTextMarkElement } from \"../inlineTextMarks\";\n\nexport type DragPosition = {\n clientX: number;\n clientY: number;\n pageX: number;\n pageY: number;\n};\n\nexport type ScrollOffset = {\n left: number;\n top: number;\n};\n\nexport type SelectedAreaState = {\n pageRect: SelectModeBoundingRect;\n};\n\n// NOTE (Noah, 2026-04-21): A small fudge factor so elements\n// that visually appear to touch the selection rect (but whose bounding\n// box is a pixel or two off due to borders/sub-pixel rendering) still get\n// included.\nconst SELECTION_TOLERANCE_PX = 4;\n\nfunction normalizeSelectionRect({\n startX,\n startY,\n endX,\n endY,\n}: {\n startX: number;\n startY: number;\n endX: number;\n endY: number;\n}): SelectModeBoundingRect {\n const left = Math.min(startX, endX);\n const right = Math.max(startX, endX);\n const top = Math.min(startY, endY);\n const bottom = Math.max(startY, endY);\n\n return {\n x: left,\n y: top,\n width: right - left,\n height: bottom - top,\n top,\n right,\n bottom,\n left,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getDragPositionFromClient({\n clientX,\n clientY,\n scrollOffset,\n}: {\n clientX: number;\n clientY: number;\n scrollOffset: ScrollOffset;\n}): DragPosition {\n return {\n clientX,\n clientY,\n pageX: clientX + scrollOffset.left,\n pageY: clientY + scrollOffset.top,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getDocumentScrollOffset(): ScrollOffset {\n const scrollingElement = document.scrollingElement;\n return {\n left: scrollingElement?.scrollLeft ?? window.scrollX,\n top: scrollingElement?.scrollTop ?? window.scrollY,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getViewportSelectionRect({\n dragStart,\n dragCurrent,\n scrollOffset,\n}: {\n dragStart: DragPosition;\n dragCurrent: DragPosition;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return normalizeSelectionRect({\n startX: dragStart.pageX - scrollOffset.left,\n startY: dragStart.pageY - scrollOffset.top,\n endX: dragCurrent.pageX - scrollOffset.left,\n endY: dragCurrent.pageY - scrollOffset.top,\n });\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getPageRectFromViewportRect({\n rect,\n scrollOffset,\n}: {\n rect: SelectModeBoundingRect;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return {\n x: rect.x + scrollOffset.left,\n y: rect.y + scrollOffset.top,\n width: rect.width,\n height: rect.height,\n left: rect.left + scrollOffset.left,\n top: rect.top + scrollOffset.top,\n right: rect.right + scrollOffset.left,\n bottom: rect.bottom + scrollOffset.top,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getViewportRectFromPageRect({\n rect,\n scrollOffset,\n}: {\n rect: SelectModeBoundingRect;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return {\n x: rect.x - scrollOffset.left,\n y: rect.y - scrollOffset.top,\n width: rect.width,\n height: rect.height,\n left: rect.left - scrollOffset.left,\n top: rect.top - scrollOffset.top,\n right: rect.right - scrollOffset.left,\n bottom: rect.bottom - scrollOffset.top,\n };\n}\n\nfunction isMeaningfulElement(\n element: Element,\n elementRect: SelectModeBoundingRect,\n): boolean {\n const tagName = element.tagName.toLowerCase();\n if ([\"html\", \"body\", \"script\", \"style\", \"meta\", \"link\"].includes(tagName)) {\n return false;\n }\n\n if (isInlineTextMarkElement(element)) {\n return false;\n }\n\n if (elementRect.width < 4 || elementRect.height < 4) {\n return false;\n }\n\n return true;\n}\n\n/**\n * A drag captures an element when their boxes overlap (within\n * `tolerancePx`) \u2014 but NOT when the drag sits entirely inside the\n * element. The latter case is the user dragging *within* a large\n * container (a section, `<main>`, `<body>`) to lasso its children; the\n * container itself isn't a thing they're trying to select, so we drop it.\n */\nexport function doesRectIntersectSelection({\n elementRect,\n selectionRect,\n tolerancePx,\n}: {\n elementRect: SelectModeBoundingRect;\n selectionRect: SelectModeBoundingRect;\n tolerancePx: number;\n}) {\n return (\n isIntersecting(elementRect, selectionRect, { tolerance: tolerancePx }) &&\n !isRectEnclosing(elementRect, selectionRect)\n );\n}\n\n// A meaningful element plus its page-coordinate rect and a stable id,\n// snapshotted once when a drag begins. Page coordinates are scroll-\n// invariant, so per-move hit-testing never needs to re-measure.\nexport type DragCandidate = {\n id: string;\n element: Element;\n pageRect: SelectModeBoundingRect;\n};\n\n/**\n * Snapshots every meaningful element and its page rect when a drag begins.\n * Each candidate gets a stable id so the parent overlay can key its\n * live highlights without remounting them as the drag moves.\n */\nexport function collectDragCandidates(\n scrollOffset: ScrollOffset,\n): DragCandidate[] {\n const candidates: DragCandidate[] = [];\n for (const element of Array.from(document.querySelectorAll(\"*\"))) {\n const viewportRect = getElementBoundingRect(element);\n if (!isMeaningfulElement(element, viewportRect)) {\n continue;\n }\n candidates.push({\n id: uuidv4(),\n element,\n pageRect: getPageRectFromViewportRect({\n rect: viewportRect,\n scrollOffset,\n }),\n });\n }\n return candidates;\n}\n\n/**\n * Drop a child whose parent is also selected and every one of that\n * parent's *candidate* children is selected: the parent already represents\n * the whole group, so the children are redundant. Returns only the\n * candidates that survive the collapse.\n *\n * Only children that are themselves drag candidates count toward \"fully\n * covered\". A parent's DOM children can include decorative / non-selectable\n * elements (and `.children` excludes text nodes); those can never be\n * selected, so counting them would wedge the check permanently false and\n * the collapse would never fire.\n *\n * Membership is read once up front, so the collapse cascades up the tree\n * in a single pass \u2014 a parent kept this round is itself dropped when *its*\n * parent is likewise fully covered, leaving only the topmost ancestor of\n * any fully-enclosed subtree.\n */\nfunction collapseFullyCoveredChildren({\n candidates,\n selected,\n}: {\n candidates: DragCandidate[];\n selected: DragCandidate[];\n}): DragCandidate[] {\n const candidateElements = new Set(\n candidates.map((candidate) => candidate.element),\n );\n const selectedElements = new Set(\n selected.map((candidate) => candidate.element),\n );\n return selected.filter((candidate) => {\n const parent = candidate.element.parentElement;\n if (!parent || !selectedElements.has(parent)) {\n return true;\n }\n const everyCandidateChildSelected = Array.from(parent.children)\n .filter((child) => candidateElements.has(child))\n .every((child) => selectedElements.has(child));\n return !everyCandidateChildSelected;\n });\n}\n\n/**\n * Cheap per-move hit-test against the pre-collected candidates: keep the\n * ones whose page rect overlaps the selection (excluding any that wholly\n * enclose it), then collapse any fully-covered parent/children group down\n * to just the parent (see `collapseFullyCoveredChildren`). Does no DOM\n * queries or re-measures of geometry \u2014 the only DOM reads are cheap\n * parent/child structural lookups.\n *\n * Because the collapse depends on which siblings are currently captured,\n * elements legitimately move in and out of the result as the drag grows or\n * shrinks: a child drops out the moment the drag swallows its last sibling\n * (the parent takes over), and reappears if the drag retreats.\n */\nexport function getCandidatesInSelection({\n candidates,\n selectionPageRect,\n}: {\n candidates: DragCandidate[];\n selectionPageRect: SelectModeBoundingRect;\n}): DragCandidate[] {\n const intersecting = candidates.filter((candidate) => {\n return doesRectIntersectSelection({\n elementRect: candidate.pageRect,\n selectionRect: selectionPageRect,\n tolerancePx: SELECTION_TOLERANCE_PX,\n });\n });\n\n return collapseFullyCoveredChildren({ candidates, selected: intersecting });\n}\n\n/**\n * Picks a \"click position\" for an element that fell inside an area\n * selection. We use the corner of the element nearest the top-left of the\n * selection rect, clamped to the element's own bounds. This anchor is what\n * lets the selected-element overlay continue to track the element when the\n * user scrolls (`useSelectionScrollSync` re-projects relative to it).\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getElementAnchor({\n element,\n selectionRect,\n}: {\n element: Element;\n selectionRect: SelectModeBoundingRect;\n}): SelectModeClickPosition {\n const rect = element.getBoundingClientRect();\n\n return {\n x: Math.min(Math.max(selectionRect.left, rect.left), rect.right),\n y: Math.min(Math.max(selectionRect.top, rect.top), rect.bottom),\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getElementBoundingRect(\n element: Element,\n): SelectModeBoundingRect {\n const rect = element.getBoundingClientRect();\n\n return {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height,\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom,\n left: rect.left,\n };\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type {\n SelectModeBoundingRect,\n SelectModeClickPosition,\n} from \"schemas/generated/selectMode\";\n\nimport { isIntersecting, isRectEnclosing } from \"replo-utils/lib/geometry\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { isInlineTextMarkElement } from \"../inlineTextMarks\";\n\nexport type DragPosition = {\n clientX: number;\n clientY: number;\n pageX: number;\n pageY: number;\n};\n\nexport type ScrollOffset = {\n left: number;\n top: number;\n};\n\nexport type SelectedAreaState = {\n pageRect: SelectModeBoundingRect;\n};\n\n// NOTE (Noah, 2026-04-21): A small fudge factor so elements\n// that visually appear to touch the selection rect (but whose bounding\n// box is a pixel or two off due to borders/sub-pixel rendering) still get\n// included.\nconst SELECTION_TOLERANCE_PX = 4;\n\nfunction normalizeSelectionRect({\n startX,\n startY,\n endX,\n endY,\n}: {\n startX: number;\n startY: number;\n endX: number;\n endY: number;\n}): SelectModeBoundingRect {\n const left = Math.min(startX, endX);\n const right = Math.max(startX, endX);\n const top = Math.min(startY, endY);\n const bottom = Math.max(startY, endY);\n\n return {\n x: left,\n y: top,\n width: right - left,\n height: bottom - top,\n top,\n right,\n bottom,\n left,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getDragPositionFromClient({\n clientX,\n clientY,\n scrollOffset,\n}: {\n clientX: number;\n clientY: number;\n scrollOffset: ScrollOffset;\n}): DragPosition {\n return {\n clientX,\n clientY,\n pageX: clientX + scrollOffset.left,\n pageY: clientY + scrollOffset.top,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getDocumentScrollOffset(): ScrollOffset {\n const scrollingElement = document.scrollingElement;\n return {\n left: scrollingElement?.scrollLeft ?? window.scrollX,\n top: scrollingElement?.scrollTop ?? window.scrollY,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getViewportSelectionRect({\n dragStart,\n dragCurrent,\n scrollOffset,\n}: {\n dragStart: DragPosition;\n dragCurrent: DragPosition;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return normalizeSelectionRect({\n startX: dragStart.pageX - scrollOffset.left,\n startY: dragStart.pageY - scrollOffset.top,\n endX: dragCurrent.pageX - scrollOffset.left,\n endY: dragCurrent.pageY - scrollOffset.top,\n });\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getPageRectFromViewportRect({\n rect,\n scrollOffset,\n}: {\n rect: SelectModeBoundingRect;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return {\n x: rect.x + scrollOffset.left,\n y: rect.y + scrollOffset.top,\n width: rect.width,\n height: rect.height,\n left: rect.left + scrollOffset.left,\n top: rect.top + scrollOffset.top,\n right: rect.right + scrollOffset.left,\n bottom: rect.bottom + scrollOffset.top,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getViewportRectFromPageRect({\n rect,\n scrollOffset,\n}: {\n rect: SelectModeBoundingRect;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return {\n x: rect.x - scrollOffset.left,\n y: rect.y - scrollOffset.top,\n width: rect.width,\n height: rect.height,\n left: rect.left - scrollOffset.left,\n top: rect.top - scrollOffset.top,\n right: rect.right - scrollOffset.left,\n bottom: rect.bottom - scrollOffset.top,\n };\n}\n\nfunction isMeaningfulElement(\n element: Element,\n elementRect: SelectModeBoundingRect,\n): boolean {\n const tagName = element.tagName.toLowerCase();\n if ([\"html\", \"body\", \"script\", \"style\", \"meta\", \"link\"].includes(tagName)) {\n return false;\n }\n\n if (isInlineTextMarkElement(element)) {\n return false;\n }\n\n if (elementRect.width < 4 || elementRect.height < 4) {\n return false;\n }\n\n return true;\n}\n\n/**\n * A drag captures an element when their boxes overlap (within\n * `tolerancePx`) \u2014 but NOT when the drag sits entirely inside the\n * element. The latter case is the user dragging *within* a large\n * container (a section, `<main>`, `<body>`) to lasso its children; the\n * container itself isn't a thing they're trying to select, so we drop it.\n */\nexport function doesRectIntersectSelection({\n elementRect,\n selectionRect,\n tolerancePx,\n}: {\n elementRect: SelectModeBoundingRect;\n selectionRect: SelectModeBoundingRect;\n tolerancePx: number;\n}) {\n return (\n isIntersecting(elementRect, selectionRect, { tolerance: tolerancePx }) &&\n !isRectEnclosing(elementRect, selectionRect)\n );\n}\n\n// A meaningful element plus its page-coordinate rect and a stable id,\n// snapshotted once when a drag begins. Page coordinates are scroll-\n// invariant, so per-move hit-testing never needs to re-measure.\nexport type DragCandidate = {\n id: string;\n element: Element;\n pageRect: SelectModeBoundingRect;\n};\n\n/**\n * Snapshots every meaningful element and its page rect when a drag begins.\n * Each candidate gets a stable id so the parent overlay can key its\n * live highlights without remounting them as the drag moves.\n */\nexport function collectDragCandidates(\n scrollOffset: ScrollOffset,\n): DragCandidate[] {\n const candidates: DragCandidate[] = [];\n for (const element of Array.from(document.querySelectorAll(\"*\"))) {\n const viewportRect = getElementBoundingRect(element);\n if (!isMeaningfulElement(element, viewportRect)) {\n continue;\n }\n candidates.push({\n id: uuidv4(),\n element,\n pageRect: getPageRectFromViewportRect({\n rect: viewportRect,\n scrollOffset,\n }),\n });\n }\n return candidates;\n}\n\n/**\n * Drop a child whose parent is also selected and every one of that\n * parent's *candidate* children is selected: the parent already represents\n * the whole group, so the children are redundant. Returns only the\n * candidates that survive the collapse.\n *\n * Only children that are themselves drag candidates count toward \"fully\n * covered\". A parent's DOM children can include decorative / non-selectable\n * elements (and `.children` excludes text nodes); those can never be\n * selected, so counting them would wedge the check permanently false and\n * the collapse would never fire.\n *\n * Membership is read once up front, so the collapse cascades up the tree\n * in a single pass \u2014 a parent kept this round is itself dropped when *its*\n * parent is likewise fully covered, leaving only the topmost ancestor of\n * any fully-enclosed subtree.\n */\nfunction collapseFullyCoveredChildren({\n candidates,\n selected,\n}: {\n candidates: DragCandidate[];\n selected: DragCandidate[];\n}): DragCandidate[] {\n const candidateElements = new Set(\n candidates.map((candidate) => candidate.element),\n );\n const selectedElements = new Set(\n selected.map((candidate) => candidate.element),\n );\n return selected.filter((candidate) => {\n const parent = candidate.element.parentElement;\n if (!parent || !selectedElements.has(parent)) {\n return true;\n }\n const everyCandidateChildSelected = Array.from(parent.children)\n .filter((child) => candidateElements.has(child))\n .every((child) => selectedElements.has(child));\n return !everyCandidateChildSelected;\n });\n}\n\n/**\n * Cheap per-move hit-test against the pre-collected candidates: keep the\n * ones whose page rect overlaps the selection (excluding any that wholly\n * enclose it), then collapse any fully-covered parent/children group down\n * to just the parent (see `collapseFullyCoveredChildren`). Does no DOM\n * queries or re-measures of geometry \u2014 the only DOM reads are cheap\n * parent/child structural lookups.\n *\n * Because the collapse depends on which siblings are currently captured,\n * elements legitimately move in and out of the result as the drag grows or\n * shrinks: a child drops out the moment the drag swallows its last sibling\n * (the parent takes over), and reappears if the drag retreats.\n */\nexport function getCandidatesInSelection({\n candidates,\n selectionPageRect,\n}: {\n candidates: DragCandidate[];\n selectionPageRect: SelectModeBoundingRect;\n}): DragCandidate[] {\n const intersecting = candidates.filter((candidate) => {\n return doesRectIntersectSelection({\n elementRect: candidate.pageRect,\n selectionRect: selectionPageRect,\n tolerancePx: SELECTION_TOLERANCE_PX,\n });\n });\n\n return collapseFullyCoveredChildren({ candidates, selected: intersecting });\n}\n\n/**\n * Shift / Command / Ctrl click toggles the clicked element in the current\n * selection (theme-builder canvas rules): a click inside an already-selected\n * ancestor is ignored, selecting a parent drops its selected descendants,\n * and once every candidate child of a candidate parent is selected the\n * parent replaces them \u2014 the same collapse drag-select applies.\n */\nexport function getSelectionAfterAdditiveClick({\n candidates,\n currentSelectedElements,\n targetElement,\n}: {\n candidates: DragCandidate[];\n currentSelectedElements: Element[];\n targetElement: Element;\n}): Element[] {\n if (currentSelectedElements.includes(targetElement)) {\n return currentSelectedElements.filter(\n (element) => element !== targetElement,\n );\n }\n if (\n currentSelectedElements.some((element) => element.contains(targetElement))\n ) {\n return currentSelectedElements;\n }\n\n const selected = new Set(\n currentSelectedElements.filter(\n (element) => !targetElement.contains(element),\n ),\n );\n selected.add(targetElement);\n\n const candidateElements = new Set(\n candidates.map((candidate) => candidate.element),\n );\n let parent = targetElement.parentElement;\n while (parent && candidateElements.has(parent)) {\n const candidateChildren = Array.from(parent.children).filter((child) => {\n return candidateElements.has(child);\n });\n if (\n candidateChildren.length === 0 ||\n !candidateChildren.every((child) => selected.has(child))\n ) {\n break;\n }\n for (const child of candidateChildren) {\n selected.delete(child);\n }\n selected.add(parent);\n parent = parent.parentElement;\n }\n return [...selected];\n}\n\nexport function isAdditiveSelectionClick(event: {\n shiftKey: boolean;\n metaKey: boolean;\n ctrlKey: boolean;\n}): boolean {\n return event.shiftKey || event.metaKey || event.ctrlKey;\n}\n\nexport function getPointRect({\n x,\n y,\n}: {\n x: number;\n y: number;\n}): SelectModeBoundingRect {\n return { x, y, width: 0, height: 0, top: y, right: x, bottom: y, left: x };\n}\n\nexport function unionElementPageRects({\n elements,\n scrollOffset,\n}: {\n elements: Element[];\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect | null {\n const rects = elements.map((element) => {\n return getPageRectFromViewportRect({\n rect: getElementBoundingRect(element),\n scrollOffset,\n });\n });\n const firstRect = rects[0];\n if (!firstRect) {\n return null;\n }\n return rects.slice(1).reduce((union, rect) => {\n const left = Math.min(union.left, rect.left);\n const top = Math.min(union.top, rect.top);\n const right = Math.max(union.right, rect.right);\n const bottom = Math.max(union.bottom, rect.bottom);\n return {\n x: left,\n y: top,\n width: right - left,\n height: bottom - top,\n left,\n top,\n right,\n bottom,\n };\n }, firstRect);\n}\n\n/**\n * Picks a \"click position\" for an element that fell inside an area\n * selection. We use the corner of the element nearest the top-left of the\n * selection rect, clamped to the element's own bounds. This anchor is what\n * lets the selected-element overlay continue to track the element when the\n * user scrolls (`useSelectionScrollSync` re-projects relative to it).\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getElementAnchor({\n element,\n selectionRect,\n}: {\n element: Element;\n selectionRect: SelectModeBoundingRect;\n}): SelectModeClickPosition {\n const rect = element.getBoundingClientRect();\n\n return {\n x: Math.min(Math.max(selectionRect.left, rect.left), rect.right),\n y: Math.min(Math.max(selectionRect.top, rect.top), rect.bottom),\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getElementBoundingRect(\n element: Element,\n): SelectModeBoundingRect {\n const rect = element.getBoundingClientRect();\n\n return {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height,\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom,\n left: rect.left,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|