@replohq/sandbox-runtime 1.16.2 → 1.17.0
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/historyKeys.d.ts +2 -0
- package/_vendor/replo-utils/lib/historyKeys.mjs +18 -0
- package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +43 -0
- package/_vendor/schemas/selectModeIframeRpc.d.ts +146 -0
- package/_vendor/schemas/selectModeIframeRpc.mjs +72 -2
- package/buildMetadata.js +3 -3
- package/package.json +4 -4
- package/select-mode/EditorSelection.js +5 -1
- package/select-mode/fast-refresh-relay.js +82 -3
- package/select-mode/fiber.d.ts +6 -0
- package/select-mode/fiber.js +33 -0
- package/select-mode/hooks/useParentMessaging.js +10 -3
- package/select-mode/hooks/useSelectionHover.js +2 -1
- package/select-mode/hooks/useSelectionInteractions.js +10 -0
- package/select-mode/iframeRpcChannel.d.ts +45 -1
- package/select-mode/iframeRpcChannel.js +7 -0
- package/select-mode/section-canvas/SectionCanvas.d.ts +7 -0
- package/select-mode/section-canvas/SectionCanvas.js +599 -0
- package/select-mode/section-canvas/optimisticSectionMove.d.ts +39 -0
- package/select-mode/section-canvas/optimisticSectionMove.js +172 -0
- package/select-mode/section-canvas/sectionCanvasControls.d.ts +3 -0
- package/select-mode/section-canvas/sectionCanvasControls.js +8 -0
- package/select-mode/section-canvas/sectionCanvasStore.d.ts +71 -0
- package/select-mode/section-canvas/sectionCanvasStore.js +275 -0
- package/select-mode/section-canvas/sectionElements.d.ts +16 -0
- package/select-mode/section-canvas/sectionElements.js +94 -0
- package/select-mode/section-canvas/sectionGhost.d.ts +5 -0
- package/select-mode/section-canvas/sectionGhost.js +23 -0
- package/select-mode/symbolicate.d.ts +2 -0
- package/select-mode/symbolicate.js +18 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type SectionBox = {
|
|
2
|
+
index: number;
|
|
3
|
+
elements: HTMLElement[];
|
|
4
|
+
};
|
|
5
|
+
/** How far each section shifts once `from` moves to `to`, keeping the original gaps between slots. */
|
|
6
|
+
export declare function getMovedSectionOffsets({ sections, from, to, }: {
|
|
7
|
+
sections: readonly {
|
|
8
|
+
index: number;
|
|
9
|
+
top: number;
|
|
10
|
+
bottom: number;
|
|
11
|
+
}[];
|
|
12
|
+
from: number;
|
|
13
|
+
to: number;
|
|
14
|
+
}): Map<number, number>;
|
|
15
|
+
export type ViewportBox = {
|
|
16
|
+
top: number;
|
|
17
|
+
bottom: number;
|
|
18
|
+
left: number;
|
|
19
|
+
right: number;
|
|
20
|
+
};
|
|
21
|
+
/** A section's on-screen box: the union of its connected elements, or null when it renders nothing visible. */
|
|
22
|
+
export declare function getSectionViewportBox(elements: readonly HTMLElement[] | undefined): ViewportBox | null;
|
|
23
|
+
/**
|
|
24
|
+
* Shows a dropped section in its new place before the source change reaches the preview, by
|
|
25
|
+
* shifting sections with transforms instead of moving DOM nodes: React keeps positional children,
|
|
26
|
+
* so nodes moved behind its back would get the wrong content when the refresh commits. The shift
|
|
27
|
+
* clears itself the moment the preview's own DOM reflects the move (checked before paint), so the
|
|
28
|
+
* page never shows the old order in between.
|
|
29
|
+
*/
|
|
30
|
+
export declare function startOptimisticSectionMove({ sections, from, to, onLanded, }: {
|
|
31
|
+
sections: readonly SectionBox[];
|
|
32
|
+
from: number;
|
|
33
|
+
to: number;
|
|
34
|
+
onLanded: () => void;
|
|
35
|
+
}): {
|
|
36
|
+
revert: () => void;
|
|
37
|
+
settle: () => void;
|
|
38
|
+
};
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const SETTLE_TRANSITION = "transform 180ms cubic-bezier(0.2, 0, 0, 1)";
|
|
2
|
+
function getTargetPosition({
|
|
3
|
+
indices,
|
|
4
|
+
from,
|
|
5
|
+
to
|
|
6
|
+
}) {
|
|
7
|
+
return indices.filter((index) => {
|
|
8
|
+
return index !== from && (index < from ? index : index - 1) < to;
|
|
9
|
+
}).length;
|
|
10
|
+
}
|
|
11
|
+
function getMovedSectionOffsets({
|
|
12
|
+
sections,
|
|
13
|
+
from,
|
|
14
|
+
to
|
|
15
|
+
}) {
|
|
16
|
+
const fromPosition = sections.findIndex((section) => section.index === from);
|
|
17
|
+
const toPosition = getTargetPosition({
|
|
18
|
+
indices: sections.map((section) => section.index),
|
|
19
|
+
from,
|
|
20
|
+
to
|
|
21
|
+
});
|
|
22
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
23
|
+
if (fromPosition === -1 || fromPosition === toPosition) {
|
|
24
|
+
return offsets;
|
|
25
|
+
}
|
|
26
|
+
const low = Math.min(fromPosition, toPosition);
|
|
27
|
+
const high = Math.max(fromPosition, toPosition);
|
|
28
|
+
const original = sections.slice(low, high + 1);
|
|
29
|
+
const reordered = [...original];
|
|
30
|
+
const [moved] = reordered.splice(fromPosition - low, 1);
|
|
31
|
+
reordered.splice(toPosition - low, 0, moved);
|
|
32
|
+
let cursor = original[0].top;
|
|
33
|
+
reordered.forEach((section, slot) => {
|
|
34
|
+
offsets.set(section.index, cursor - section.top);
|
|
35
|
+
const gapAfterSlot = slot < original.length - 1 ? original[slot + 1].top - original[slot].bottom : 0;
|
|
36
|
+
cursor += section.bottom - section.top + gapAfterSlot;
|
|
37
|
+
});
|
|
38
|
+
return offsets;
|
|
39
|
+
}
|
|
40
|
+
function getSectionViewportBox(elements) {
|
|
41
|
+
const rects = (elements ?? []).filter((element) => element.isConnected).map((element) => element.getBoundingClientRect()).filter((rect) => rect.width !== 0 || rect.height !== 0);
|
|
42
|
+
if (rects.length === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
top: Math.min(...rects.map((rect) => rect.top)),
|
|
47
|
+
bottom: Math.max(...rects.map((rect) => rect.bottom)),
|
|
48
|
+
left: Math.min(...rects.map((rect) => rect.left)),
|
|
49
|
+
right: Math.max(...rects.map((rect) => rect.right))
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function startOptimisticSectionMove({
|
|
53
|
+
sections,
|
|
54
|
+
from,
|
|
55
|
+
to,
|
|
56
|
+
onLanded
|
|
57
|
+
}) {
|
|
58
|
+
const placed = sections.flatMap((section) => {
|
|
59
|
+
const box = getSectionViewportBox(section.elements);
|
|
60
|
+
return box ? [{ ...section, ...box }] : [];
|
|
61
|
+
});
|
|
62
|
+
const offsets = getMovedSectionOffsets({ sections: placed, from, to });
|
|
63
|
+
const shifted = [];
|
|
64
|
+
for (const section of placed) {
|
|
65
|
+
const offset = offsets.get(section.index);
|
|
66
|
+
if (!offset) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
for (const element of section.elements) {
|
|
70
|
+
const { transform, transition, position, zIndex } = element.style;
|
|
71
|
+
shifted.push({
|
|
72
|
+
element,
|
|
73
|
+
saved: { transform, transition, position, zIndex }
|
|
74
|
+
});
|
|
75
|
+
element.style.transition = SETTLE_TRANSITION;
|
|
76
|
+
element.style.transform = `${transform} translateY(${offset}px)`.trim();
|
|
77
|
+
if (section.index === from) {
|
|
78
|
+
element.style.position = position || "relative";
|
|
79
|
+
element.style.zIndex = "1";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const order = placed.map((section) => section.index);
|
|
84
|
+
const fromPosition = order.indexOf(from);
|
|
85
|
+
const reordered = [...order];
|
|
86
|
+
reordered.splice(fromPosition, 1);
|
|
87
|
+
reordered.splice(getTargetPosition({ indices: order, from, to }), 0, from);
|
|
88
|
+
const textBySection = new Map(
|
|
89
|
+
placed.map((section) => [
|
|
90
|
+
section.index,
|
|
91
|
+
section.elements.map((element) => element.textContent ?? "").join("\n")
|
|
92
|
+
])
|
|
93
|
+
);
|
|
94
|
+
const watchedSlots = placed.flatMap((section, slot) => {
|
|
95
|
+
const expected = textBySection.get(reordered[slot]);
|
|
96
|
+
return expected === textBySection.get(section.index) ? [] : [{ elements: section.elements, expected }];
|
|
97
|
+
});
|
|
98
|
+
const containers = new Set(
|
|
99
|
+
placed.flatMap((section) => {
|
|
100
|
+
return section.elements.flatMap((element) => {
|
|
101
|
+
return element.parentNode ? [element.parentNode] : [];
|
|
102
|
+
});
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
let isDone = false;
|
|
106
|
+
let fallbackTimeout;
|
|
107
|
+
const finish = ({ animate }) => {
|
|
108
|
+
if (isDone) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
isDone = true;
|
|
112
|
+
observer.disconnect();
|
|
113
|
+
clearTimeout(fallbackTimeout);
|
|
114
|
+
for (const { element, saved } of shifted) {
|
|
115
|
+
element.style.transition = animate ? SETTLE_TRANSITION : "none";
|
|
116
|
+
element.style.transform = saved.transform;
|
|
117
|
+
element.style.position = saved.position;
|
|
118
|
+
element.style.zIndex = saved.zIndex;
|
|
119
|
+
}
|
|
120
|
+
const restoreTransitions = () => {
|
|
121
|
+
for (const { element, saved } of shifted) {
|
|
122
|
+
element.style.transition = saved.transition;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
if (animate) {
|
|
126
|
+
setTimeout(restoreTransitions, 200);
|
|
127
|
+
} else {
|
|
128
|
+
restoreTransitions();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const hasLanded = (records) => {
|
|
132
|
+
if (records.some((record) => {
|
|
133
|
+
return record.type === "childList" && containers.has(record.target);
|
|
134
|
+
})) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return watchedSlots.length > 0 && watchedSlots.every(({ elements, expected }) => {
|
|
138
|
+
return elements.some((element) => !element.isConnected) || elements.map((element) => element.textContent ?? "").join("\n") === expected;
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
const observer = new MutationObserver((records) => {
|
|
142
|
+
if (hasLanded(records)) {
|
|
143
|
+
finish({ animate: false });
|
|
144
|
+
onLanded();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
for (const container of containers) {
|
|
148
|
+
observer.observe(container, {
|
|
149
|
+
childList: true,
|
|
150
|
+
subtree: true,
|
|
151
|
+
characterData: true
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
revert: () => finish({ animate: true }),
|
|
156
|
+
settle: () => {
|
|
157
|
+
if (isDone) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
clearTimeout(fallbackTimeout);
|
|
161
|
+
fallbackTimeout = setTimeout(() => {
|
|
162
|
+
finish({ animate: false });
|
|
163
|
+
onLanded();
|
|
164
|
+
}, 8e3);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
getMovedSectionOffsets,
|
|
170
|
+
getSectionViewportBox,
|
|
171
|
+
startOptimisticSectionMove
|
|
172
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const SECTION_CANVAS_ATTRIBUTE = "data-replo-section-canvas";
|
|
2
|
+
function isSectionCanvasControl(target) {
|
|
3
|
+
return target instanceof Element && Boolean(target.closest(`[${SECTION_CANVAS_ATTRIBUTE}]`));
|
|
4
|
+
}
|
|
5
|
+
export {
|
|
6
|
+
SECTION_CANVAS_ATTRIBUTE,
|
|
7
|
+
isSectionCanvasControl
|
|
8
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { SectionCanvasRequest, SectionCanvasState } from "../../_vendor/schemas/generated/selectModeIframeRpc";
|
|
2
|
+
export type SectionDrag = {
|
|
3
|
+
index: number;
|
|
4
|
+
pointerId: number;
|
|
5
|
+
startX: number;
|
|
6
|
+
startY: number;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
isActive: boolean;
|
|
10
|
+
ghostSrc: string | null;
|
|
11
|
+
gap: number | null;
|
|
12
|
+
};
|
|
13
|
+
type PendingMove = {
|
|
14
|
+
moveId: string;
|
|
15
|
+
settle: () => void;
|
|
16
|
+
revert: () => void;
|
|
17
|
+
};
|
|
18
|
+
type SectionCanvasStoreState = {
|
|
19
|
+
canvas: SectionCanvasState | null;
|
|
20
|
+
elementsByIndex: ReadonlyMap<number, readonly HTMLElement[]>;
|
|
21
|
+
hoveredIndex: number | null;
|
|
22
|
+
drag: SectionDrag | null;
|
|
23
|
+
pendingMove: PendingMove | null;
|
|
24
|
+
geometryVersion: number;
|
|
25
|
+
actions: {
|
|
26
|
+
setCanvas: (canvas: SectionCanvasState | null) => void;
|
|
27
|
+
resolveSections: () => Promise<void>;
|
|
28
|
+
handlePreviewRefresh: () => void;
|
|
29
|
+
bumpGeometry: () => void;
|
|
30
|
+
setHoveredIndex: (index: number | null) => void;
|
|
31
|
+
request: (request: SectionCanvasRequest) => void;
|
|
32
|
+
beginDrag: (input: {
|
|
33
|
+
index: number;
|
|
34
|
+
pointerId: number;
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
}) => void;
|
|
38
|
+
updateDrag: (input: {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
}) => void;
|
|
42
|
+
endDrag: (input: {
|
|
43
|
+
shouldDrop: boolean;
|
|
44
|
+
}) => void;
|
|
45
|
+
move: (input: {
|
|
46
|
+
index: number;
|
|
47
|
+
to: number;
|
|
48
|
+
}) => void;
|
|
49
|
+
settleMove: (input: {
|
|
50
|
+
moveId: string;
|
|
51
|
+
outcome: "persisted" | "failed";
|
|
52
|
+
}) => void;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export declare const GHOST_WIDTH_PX = 240;
|
|
56
|
+
/** On-screen sections in page order, with their boxes. */
|
|
57
|
+
export declare function getPlacedSections(state: Pick<SectionCanvasStoreState, "canvas" | "elementsByIndex">): {
|
|
58
|
+
section: import("../../_vendor/schemas/generated/selectModeIframeRpc").SectionCanvasSection;
|
|
59
|
+
box: import("./optimisticSectionMove").ViewportBox;
|
|
60
|
+
}[];
|
|
61
|
+
/**
|
|
62
|
+
* The insertion points between on-screen sections: `at` is the index a new or moved section goes
|
|
63
|
+
* before in the page's list, `y` the viewport line drawn for it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getSectionGaps(placed: ReturnType<typeof getPlacedSections>, sectionCount: number): {
|
|
66
|
+
at: number;
|
|
67
|
+
y: number;
|
|
68
|
+
}[];
|
|
69
|
+
export declare const sectionCanvasStore: import("zustand/traditional").UseBoundStoreWithEqualityFn<import("zustand").StoreApi<SectionCanvasStoreState>>;
|
|
70
|
+
export declare function useSectionCanvasStore<Selected>(selector: (state: SectionCanvasStoreState) => Selected): Selected;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createShallowStore } from "../../_vendor/replo-utils/react/zustand.mjs";
|
|
3
|
+
import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
|
|
4
|
+
import { getDocumentScrollOffset } from "../interactions/selectionGeometry";
|
|
5
|
+
import { useShallowStore } from "../useShallowStore";
|
|
6
|
+
import {
|
|
7
|
+
getSectionViewportBox,
|
|
8
|
+
startOptimisticSectionMove
|
|
9
|
+
} from "./optimisticSectionMove";
|
|
10
|
+
import { findSectionElements } from "./sectionElements";
|
|
11
|
+
import { captureSectionGhost } from "./sectionGhost";
|
|
12
|
+
const DRAG_THRESHOLD_PX = 4;
|
|
13
|
+
const GHOST_WIDTH_PX = 240;
|
|
14
|
+
let resolveToken = 0;
|
|
15
|
+
let moveCounter = 0;
|
|
16
|
+
function sameSections(previous, next) {
|
|
17
|
+
if (!previous || !next || previous.pageFilePath !== next.pageFilePath) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return previous.sections.length === next.sections.length && previous.sections.every((section, position) => {
|
|
21
|
+
const other = next.sections[position];
|
|
22
|
+
return section.index === other.index && section.startLine === other.startLine && section.endLine === other.endLine;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function getPlacedSections(state) {
|
|
26
|
+
return (state.canvas?.sections ?? []).flatMap((section) => {
|
|
27
|
+
const box = getSectionViewportBox(state.elementsByIndex.get(section.index));
|
|
28
|
+
return box ? [{ section, box }] : [];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function getSectionGaps(placed, sectionCount) {
|
|
32
|
+
const gaps = placed.map(({ section, box }, position) => {
|
|
33
|
+
const previous = placed[position - 1];
|
|
34
|
+
return {
|
|
35
|
+
at: section.index,
|
|
36
|
+
y: previous ? (previous.box.bottom + box.top) / 2 : box.top
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
const last = placed.at(-1);
|
|
40
|
+
if (last) {
|
|
41
|
+
gaps.push({
|
|
42
|
+
at: Math.max(last.section.index + 1, sectionCount),
|
|
43
|
+
y: last.box.bottom
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return gaps;
|
|
47
|
+
}
|
|
48
|
+
function getDropGap({
|
|
49
|
+
state,
|
|
50
|
+
index,
|
|
51
|
+
y
|
|
52
|
+
}) {
|
|
53
|
+
const gaps = getSectionGaps(
|
|
54
|
+
getPlacedSections(state),
|
|
55
|
+
state.canvas?.sections.length ?? 0
|
|
56
|
+
);
|
|
57
|
+
let nearest = null;
|
|
58
|
+
for (const gap of gaps) {
|
|
59
|
+
const distance = Math.abs(gap.y - y);
|
|
60
|
+
if (!nearest || distance < nearest.distance) {
|
|
61
|
+
nearest = { at: gap.at, distance };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!nearest || nearest.at === index || nearest.at === index + 1) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return nearest.at;
|
|
68
|
+
}
|
|
69
|
+
function reportLayout(state) {
|
|
70
|
+
if (!state.canvas) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const scroll = getDocumentScrollOffset();
|
|
74
|
+
const sections = getPlacedSections(state).map(
|
|
75
|
+
({ section, box }) => {
|
|
76
|
+
const left = box.left + scroll.left;
|
|
77
|
+
const top = box.top + scroll.top;
|
|
78
|
+
const width = box.right - box.left;
|
|
79
|
+
const height = box.bottom - box.top;
|
|
80
|
+
return {
|
|
81
|
+
index: section.index,
|
|
82
|
+
startLine: section.startLine,
|
|
83
|
+
endLine: section.endLine,
|
|
84
|
+
rect: {
|
|
85
|
+
x: left,
|
|
86
|
+
y: top,
|
|
87
|
+
left,
|
|
88
|
+
top,
|
|
89
|
+
right: left + width,
|
|
90
|
+
bottom: top + height,
|
|
91
|
+
width,
|
|
92
|
+
height
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
getWebsiteBuilderRpc()?.sectionCanvasLayoutChanged({ sections });
|
|
98
|
+
}
|
|
99
|
+
const sectionCanvasStore = createShallowStore(
|
|
100
|
+
(set, get) => ({
|
|
101
|
+
canvas: null,
|
|
102
|
+
elementsByIndex: /* @__PURE__ */ new Map(),
|
|
103
|
+
hoveredIndex: null,
|
|
104
|
+
drag: null,
|
|
105
|
+
pendingMove: null,
|
|
106
|
+
geometryVersion: 0,
|
|
107
|
+
actions: {
|
|
108
|
+
setCanvas: (canvas) => {
|
|
109
|
+
const previous = get().canvas;
|
|
110
|
+
set({ canvas });
|
|
111
|
+
if (!canvas) {
|
|
112
|
+
resolveToken += 1;
|
|
113
|
+
get().pendingMove?.revert();
|
|
114
|
+
set({
|
|
115
|
+
elementsByIndex: /* @__PURE__ */ new Map(),
|
|
116
|
+
hoveredIndex: null,
|
|
117
|
+
drag: null,
|
|
118
|
+
pendingMove: null
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (canvas.isDisabled && get().drag) {
|
|
123
|
+
set({ drag: null });
|
|
124
|
+
}
|
|
125
|
+
if (!sameSections(previous, canvas) && !get().pendingMove) {
|
|
126
|
+
void get().actions.resolveSections();
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
resolveSections: async () => {
|
|
130
|
+
const canvas = get().canvas;
|
|
131
|
+
if (!canvas || typeof document === "undefined" || !document.body) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const token = ++resolveToken;
|
|
135
|
+
const elementsByIndex = await findSectionElements({
|
|
136
|
+
root: document.body,
|
|
137
|
+
pageFilePath: canvas.pageFilePath,
|
|
138
|
+
sections: canvas.sections
|
|
139
|
+
}).catch(() => /* @__PURE__ */ new Map());
|
|
140
|
+
if (token !== resolveToken) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
set((state) => ({
|
|
144
|
+
elementsByIndex,
|
|
145
|
+
geometryVersion: state.geometryVersion + 1
|
|
146
|
+
}));
|
|
147
|
+
reportLayout(get());
|
|
148
|
+
},
|
|
149
|
+
handlePreviewRefresh: () => {
|
|
150
|
+
if (!get().canvas || get().pendingMove) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
void get().actions.resolveSections();
|
|
154
|
+
},
|
|
155
|
+
bumpGeometry: () => {
|
|
156
|
+
set((state) => ({ geometryVersion: state.geometryVersion + 1 }));
|
|
157
|
+
},
|
|
158
|
+
setHoveredIndex: (hoveredIndex) => {
|
|
159
|
+
if (get().hoveredIndex !== hoveredIndex) {
|
|
160
|
+
set({ hoveredIndex });
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
request: (request) => {
|
|
164
|
+
getWebsiteBuilderRpc()?.sectionCanvasRequest({ request });
|
|
165
|
+
},
|
|
166
|
+
beginDrag: ({ index, pointerId, x, y }) => {
|
|
167
|
+
const elements = get().elementsByIndex.get(index) ?? [];
|
|
168
|
+
set({
|
|
169
|
+
drag: {
|
|
170
|
+
index,
|
|
171
|
+
pointerId,
|
|
172
|
+
startX: x,
|
|
173
|
+
startY: y,
|
|
174
|
+
x,
|
|
175
|
+
y,
|
|
176
|
+
isActive: false,
|
|
177
|
+
ghostSrc: null,
|
|
178
|
+
gap: null
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
const scale = get().canvas?.previewScale ?? 1;
|
|
182
|
+
void captureSectionGhost({
|
|
183
|
+
elements,
|
|
184
|
+
width: GHOST_WIDTH_PX / scale
|
|
185
|
+
}).then((ghostSrc) => {
|
|
186
|
+
const drag = get().drag;
|
|
187
|
+
if (ghostSrc && drag?.pointerId === pointerId && drag.index === index) {
|
|
188
|
+
set({ drag: { ...drag, ghostSrc } });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
updateDrag: ({ x, y }) => {
|
|
193
|
+
const state = get();
|
|
194
|
+
const drag = state.drag;
|
|
195
|
+
if (!drag) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const scale = state.canvas?.previewScale ?? 1;
|
|
199
|
+
const isActive = drag.isActive || Math.hypot(x - drag.startX, y - drag.startY) * scale > DRAG_THRESHOLD_PX;
|
|
200
|
+
set({
|
|
201
|
+
drag: {
|
|
202
|
+
...drag,
|
|
203
|
+
x,
|
|
204
|
+
y,
|
|
205
|
+
isActive,
|
|
206
|
+
gap: isActive ? getDropGap({ state, index: drag.index, y }) : null
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
endDrag: ({ shouldDrop }) => {
|
|
211
|
+
const drag = get().drag;
|
|
212
|
+
set({ drag: null });
|
|
213
|
+
if (!shouldDrop || !drag?.isActive || drag.gap === null) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
get().actions.move({
|
|
217
|
+
index: drag.index,
|
|
218
|
+
to: drag.gap > drag.index ? drag.gap - 1 : drag.gap
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
move: ({ index, to }) => {
|
|
222
|
+
const state = get();
|
|
223
|
+
const sectionCount = state.canvas?.sections.length ?? 0;
|
|
224
|
+
if (!state.canvas || state.canvas.isDisabled || state.pendingMove || state.canvas.sections[index]?.isLocked !== false || to === index || to < 0 || to >= sectionCount) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
moveCounter += 1;
|
|
228
|
+
const moveId = `section-move-${Date.now()}-${moveCounter}`;
|
|
229
|
+
const optimistic = startOptimisticSectionMove({
|
|
230
|
+
sections: state.canvas.sections.map((section) => ({
|
|
231
|
+
index: section.index,
|
|
232
|
+
elements: [...state.elementsByIndex.get(section.index) ?? []]
|
|
233
|
+
})),
|
|
234
|
+
from: index,
|
|
235
|
+
to,
|
|
236
|
+
onLanded: () => {
|
|
237
|
+
if (get().pendingMove?.moveId !== moveId) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
set({ pendingMove: null });
|
|
241
|
+
void get().actions.resolveSections();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
set({
|
|
245
|
+
pendingMove: { moveId, ...optimistic },
|
|
246
|
+
hoveredIndex: null
|
|
247
|
+
});
|
|
248
|
+
get().actions.request({ type: "move", moveId, index, to });
|
|
249
|
+
},
|
|
250
|
+
settleMove: ({ moveId, outcome }) => {
|
|
251
|
+
const pendingMove = get().pendingMove;
|
|
252
|
+
if (pendingMove?.moveId !== moveId) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (outcome === "persisted") {
|
|
256
|
+
pendingMove.settle();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
pendingMove.revert();
|
|
260
|
+
set({ pendingMove: null });
|
|
261
|
+
void get().actions.resolveSections();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
function useSectionCanvasStore(selector) {
|
|
267
|
+
return useShallowStore(sectionCanvasStore, selector);
|
|
268
|
+
}
|
|
269
|
+
export {
|
|
270
|
+
GHOST_WIDTH_PX,
|
|
271
|
+
getPlacedSections,
|
|
272
|
+
getSectionGaps,
|
|
273
|
+
sectionCanvasStore,
|
|
274
|
+
useSectionCanvasStore
|
|
275
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SelectModeSourceLocation } from "../../_vendor/schemas/generated/selectMode";
|
|
2
|
+
import type { SectionCanvasSection } from "../../_vendor/schemas/generated/selectModeIframeRpc";
|
|
3
|
+
type Resolve = (locations: readonly SelectModeSourceLocation[]) => Promise<SelectModeSourceLocation[]>;
|
|
4
|
+
/**
|
|
5
|
+
* The DOM elements each page section renders, found from source, not layout: an element belongs to
|
|
6
|
+
* a section when a JSX call site in its owner chain lies in the page file inside that section's lines. Walks down from <body> a level at a time, symbolicating each level
|
|
7
|
+
* in one request and stopping at the first element that matches, so a section's descendants are
|
|
8
|
+
* never read.
|
|
9
|
+
*/
|
|
10
|
+
export declare function findSectionElements({ root, pageFilePath, sections, resolve, }: {
|
|
11
|
+
root: Element;
|
|
12
|
+
pageFilePath: string;
|
|
13
|
+
sections: readonly Pick<SectionCanvasSection, "index" | "startLine" | "endLine">[];
|
|
14
|
+
resolve?: Resolve;
|
|
15
|
+
}): Promise<Map<number, HTMLElement[]>>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isSectionCanvasPageFile } from "../../_vendor/schemas/selectModeIframeRpc.mjs";
|
|
2
|
+
import { getElementSourceChain } from "../fiber";
|
|
3
|
+
import { symbolicateSourceLocations } from "../symbolicate";
|
|
4
|
+
import { isSectionCanvasControl } from "./sectionCanvasControls";
|
|
5
|
+
const MAX_DEPTH = 14;
|
|
6
|
+
const MAX_ELEMENTS = 2e3;
|
|
7
|
+
const SKIPPED_TAGS = /* @__PURE__ */ new Set([
|
|
8
|
+
"SCRIPT",
|
|
9
|
+
"STYLE",
|
|
10
|
+
"LINK",
|
|
11
|
+
"META",
|
|
12
|
+
"TEMPLATE",
|
|
13
|
+
"NOSCRIPT"
|
|
14
|
+
]);
|
|
15
|
+
async function findSectionElements({
|
|
16
|
+
root,
|
|
17
|
+
pageFilePath,
|
|
18
|
+
sections,
|
|
19
|
+
resolve = symbolicateSourceLocations
|
|
20
|
+
}) {
|
|
21
|
+
const found = /* @__PURE__ */ new Map();
|
|
22
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
23
|
+
let level = Array.from(root.children);
|
|
24
|
+
let visited = 0;
|
|
25
|
+
for (let depth = 0; depth < MAX_DEPTH && level.length > 0; depth += 1) {
|
|
26
|
+
const candidates = level.flatMap((element) => {
|
|
27
|
+
if (!(element instanceof HTMLElement) || SKIPPED_TAGS.has(element.tagName) || isSectionCanvasControl(element)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const chain = getElementSourceChain(element).map(
|
|
31
|
+
(source) => ({
|
|
32
|
+
filePath: source.fileName,
|
|
33
|
+
lineNumber: source.lineNumber,
|
|
34
|
+
columnNumber: source.columnNumber
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
return [{ element, chain }];
|
|
38
|
+
});
|
|
39
|
+
visited += candidates.length;
|
|
40
|
+
const pending = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const { chain } of candidates) {
|
|
42
|
+
for (const location of chain) {
|
|
43
|
+
const key = locationKey(location);
|
|
44
|
+
if (!resolved.has(key)) {
|
|
45
|
+
pending.set(key, location);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (pending.size > 0) {
|
|
50
|
+
const keys = [...pending.keys()];
|
|
51
|
+
const results = await resolve([...pending.values()]);
|
|
52
|
+
keys.forEach((key, index) => {
|
|
53
|
+
resolved.set(key, results[index] ?? pending.get(key));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const next = [];
|
|
57
|
+
for (const { element, chain } of candidates) {
|
|
58
|
+
let section;
|
|
59
|
+
for (const location of chain) {
|
|
60
|
+
const original = resolved.get(locationKey(location));
|
|
61
|
+
if (!original || !isSectionCanvasPageFile({
|
|
62
|
+
filePath: original.filePath,
|
|
63
|
+
pageFilePath
|
|
64
|
+
})) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
section = sections.find((candidate) => {
|
|
68
|
+
return candidate.startLine <= original.lineNumber && original.lineNumber <= candidate.endLine;
|
|
69
|
+
});
|
|
70
|
+
if (section) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (section) {
|
|
75
|
+
const elements = found.get(section.index) ?? [];
|
|
76
|
+
elements.push(element);
|
|
77
|
+
found.set(section.index, elements);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
next.push(...Array.from(element.children));
|
|
81
|
+
}
|
|
82
|
+
if (visited >= MAX_ELEMENTS) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
level = next;
|
|
86
|
+
}
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
function locationKey(location) {
|
|
90
|
+
return `${location.filePath}:${location.lineNumber}:${location.columnNumber ?? ""}`;
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
findSectionElements
|
|
94
|
+
};
|