@replohq/sandbox-runtime 1.7.9 → 1.7.11
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/react/zustand.d.ts +13 -0
- package/_vendor/replo-utils/react/zustand.mjs +24 -0
- package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +12 -2
- package/_vendor/schemas/selectModeIframeRpc.d.ts +66 -4
- package/_vendor/schemas/selectModeIframeRpc.mjs +16 -3
- package/buildMetadata.js +3 -3
- package/package.json +6 -5
- package/select-mode/EditorSelection.d.ts +1 -22
- package/select-mode/EditorSelection.js +61 -100
- package/select-mode/SelectionFrames.d.ts +1 -0
- package/select-mode/SelectionFrames.js +58 -0
- package/select-mode/annotationFrames.d.ts +13 -0
- package/select-mode/annotationFrames.js +31 -1
- package/select-mode/descriptor/elementDescriptor.js +1 -1
- package/select-mode/displayModeStore.d.ts +0 -2
- package/select-mode/displayModeStore.js +11 -30
- package/select-mode/hooks/useParentMessaging.js +3 -0
- package/select-mode/hooks/useSelectionHover.d.ts +1 -19
- package/select-mode/hooks/useSelectionHover.js +37 -49
- package/select-mode/hooks/useSelectionInteractions.d.ts +1 -12
- package/select-mode/hooks/useSelectionInteractions.js +57 -39
- package/select-mode/hooks/useSelectionScrollSync.d.ts +1 -25
- package/select-mode/hooks/useSelectionScrollSync.js +48 -40
- package/select-mode/iframeRpcChannel.d.ts +24 -1
- package/select-mode/inlineEditFeedback.d.ts +6 -0
- package/select-mode/inlineEditFeedback.js +47 -0
- package/select-mode/isTextEditableElement.d.ts +1 -0
- package/select-mode/isTextEditableElement.js +15 -0
- package/select-mode/selectModeStore.d.ts +160 -0
- package/select-mode/selectModeStore.js +585 -0
- package/select-mode/selectionFrame.d.ts +10 -59
- package/select-mode/selectionFrame.js +26 -150
- package/select-mode/symbolicate.js +4 -0
- package/select-mode/tiptapInlineEditor.js +16 -1
- package/select-mode/hooks/useCrosshairCursor.d.ts +0 -10
- package/select-mode/hooks/useCrosshairCursor.js +0 -23
- package/select-mode/hooks/useEditModeStateMachine.d.ts +0 -16
- package/select-mode/hooks/useEditModeStateMachine.js +0 -228
- package/select-mode/hooks/useSelectModeController.d.ts +0 -11
- package/select-mode/hooks/useSelectModeController.js +0 -72
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createShallowStore, useShallowStore } from "../_vendor/replo-utils/react/zustand.mjs";
|
|
3
|
+
import {
|
|
4
|
+
buildElementDescriptor,
|
|
5
|
+
buildSelector
|
|
6
|
+
} from "./descriptor/elementDescriptor";
|
|
7
|
+
import { getRepetitionSiblingElements } from "./hooks/useSiblingResolution";
|
|
8
|
+
import {
|
|
9
|
+
getWebsiteBuilderRpc,
|
|
10
|
+
requestInlineTextEdit
|
|
11
|
+
} from "./iframeRpcChannel";
|
|
12
|
+
import { reportInlineEditFeedback } from "./inlineEditFeedback";
|
|
13
|
+
import { getDocumentScrollOffset } from "./interactions/selectionGeometry";
|
|
14
|
+
import { isTextEditableElement } from "./isTextEditableElement";
|
|
15
|
+
import {
|
|
16
|
+
getAreaSelectionFrameSpecs,
|
|
17
|
+
getElementSelectionFrameSpecs
|
|
18
|
+
} from "./selectionFrame";
|
|
19
|
+
import { getConnectedSelectedElement, IDLE_SESSION } from "./selectModeSession";
|
|
20
|
+
import {
|
|
21
|
+
isUserSourceLocation,
|
|
22
|
+
symbolicateSelectedElement
|
|
23
|
+
} from "./symbolicate";
|
|
24
|
+
import { createInlineTiptapEditor } from "./tiptapInlineEditor";
|
|
25
|
+
const EMPTY_ACTIVE_MARKS = {
|
|
26
|
+
bold: false,
|
|
27
|
+
italic: false,
|
|
28
|
+
underline: false,
|
|
29
|
+
strike: false
|
|
30
|
+
};
|
|
31
|
+
const FOCUS_STEAL_GUARD_MS = 1500;
|
|
32
|
+
function isPendingEdit({
|
|
33
|
+
editId,
|
|
34
|
+
request
|
|
35
|
+
}) {
|
|
36
|
+
return request?.status === "pending" && request.editId === editId;
|
|
37
|
+
}
|
|
38
|
+
function stopInlineEditRequest(request) {
|
|
39
|
+
if (!request) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (request.status === "pending") {
|
|
43
|
+
clearTimeout(request.timeout);
|
|
44
|
+
}
|
|
45
|
+
request.stopFeedback();
|
|
46
|
+
}
|
|
47
|
+
function reportInlineEditingState({
|
|
48
|
+
activeMarks,
|
|
49
|
+
isInlineEditing,
|
|
50
|
+
request
|
|
51
|
+
}) {
|
|
52
|
+
getWebsiteBuilderRpc()?.editorStateUpdate({
|
|
53
|
+
activeMarks,
|
|
54
|
+
isInlineEditing,
|
|
55
|
+
isInlineEditPending: request?.status === "pending"
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const INITIAL_STATE = {
|
|
59
|
+
displayMode: "browse",
|
|
60
|
+
interactionSession: IDLE_SESSION,
|
|
61
|
+
selectionFrames: [],
|
|
62
|
+
isSelectionFrameEditing: false,
|
|
63
|
+
lastPointerClientPosition: null,
|
|
64
|
+
lastPostedHoverRect: void 0,
|
|
65
|
+
hoverSyncAnimationFrameId: null,
|
|
66
|
+
lastClick: null,
|
|
67
|
+
dragCandidates: [],
|
|
68
|
+
committedSelectionAtPointerDown: [],
|
|
69
|
+
dragAnimationFrameId: null,
|
|
70
|
+
editingPointerIds: /* @__PURE__ */ new Set(),
|
|
71
|
+
scrollSyncAnimationFrameId: null,
|
|
72
|
+
inlineEditRequest: null,
|
|
73
|
+
editor: null,
|
|
74
|
+
editingElement: null,
|
|
75
|
+
editId: null,
|
|
76
|
+
originalInnerHTML: null,
|
|
77
|
+
initialHTML: null,
|
|
78
|
+
isCommitting: false,
|
|
79
|
+
beginEditingAt: 0,
|
|
80
|
+
activeMarks: EMPTY_ACTIVE_MARKS
|
|
81
|
+
};
|
|
82
|
+
const selectModeStore = createShallowStore(
|
|
83
|
+
(set, get) => {
|
|
84
|
+
const setInlineEditRequest = ({
|
|
85
|
+
activeMarks,
|
|
86
|
+
isInlineEditing,
|
|
87
|
+
next
|
|
88
|
+
}) => {
|
|
89
|
+
stopInlineEditRequest(get().inlineEditRequest);
|
|
90
|
+
if (!next) {
|
|
91
|
+
set({ inlineEditRequest: null });
|
|
92
|
+
reportInlineEditingState({
|
|
93
|
+
activeMarks,
|
|
94
|
+
isInlineEditing,
|
|
95
|
+
request: null
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const assigned = {
|
|
100
|
+
...next,
|
|
101
|
+
stopFeedback: reportInlineEditFeedback({
|
|
102
|
+
element: next.element,
|
|
103
|
+
status: next.status,
|
|
104
|
+
onDisconnected: () => {
|
|
105
|
+
const current = get().inlineEditRequest;
|
|
106
|
+
if (next.status === "pending") {
|
|
107
|
+
if (!isPendingEdit({ editId: next.editId, request: current })) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
} else if (current?.element !== next.element) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
setInlineEditRequest({
|
|
114
|
+
activeMarks: EMPTY_ACTIVE_MARKS,
|
|
115
|
+
isInlineEditing: false,
|
|
116
|
+
next: null
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
};
|
|
121
|
+
set({ inlineEditRequest: assigned });
|
|
122
|
+
reportInlineEditingState({
|
|
123
|
+
activeMarks,
|
|
124
|
+
isInlineEditing,
|
|
125
|
+
request: assigned
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
const destroyEditor = () => {
|
|
129
|
+
const { editor } = get();
|
|
130
|
+
editor?.destroy();
|
|
131
|
+
set({
|
|
132
|
+
editor: null,
|
|
133
|
+
editingElement: null,
|
|
134
|
+
editId: null,
|
|
135
|
+
originalInnerHTML: null,
|
|
136
|
+
initialHTML: null,
|
|
137
|
+
isCommitting: false,
|
|
138
|
+
isSelectionFrameEditing: false,
|
|
139
|
+
activeMarks: EMPTY_ACTIVE_MARKS
|
|
140
|
+
});
|
|
141
|
+
setInlineEditRequest({
|
|
142
|
+
activeMarks: EMPTY_ACTIVE_MARKS,
|
|
143
|
+
isInlineEditing: false,
|
|
144
|
+
next: null
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
const revertInlineEdit = () => {
|
|
148
|
+
const { editingElement, originalInnerHTML } = get();
|
|
149
|
+
destroyEditor();
|
|
150
|
+
if (editingElement && originalInnerHTML !== null) {
|
|
151
|
+
editingElement.innerHTML = originalInnerHTML;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const commitInlineEdit = () => {
|
|
155
|
+
const {
|
|
156
|
+
editor,
|
|
157
|
+
editingElement,
|
|
158
|
+
originalInnerHTML,
|
|
159
|
+
editId,
|
|
160
|
+
isCommitting,
|
|
161
|
+
initialHTML
|
|
162
|
+
} = get();
|
|
163
|
+
if (!editor || !editId || originalInnerHTML === null || isCommitting) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
set({ isCommitting: true });
|
|
167
|
+
const committedHTML = editor.getHTML();
|
|
168
|
+
if (committedHTML === initialHTML) {
|
|
169
|
+
destroyEditor();
|
|
170
|
+
if (editingElement) {
|
|
171
|
+
editingElement.innerHTML = originalInnerHTML;
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
getWebsiteBuilderRpc()?.textCommitted({
|
|
176
|
+
editId,
|
|
177
|
+
html: committedHTML,
|
|
178
|
+
json: editor.getJSON(),
|
|
179
|
+
originalInnerHTML
|
|
180
|
+
});
|
|
181
|
+
destroyEditor();
|
|
182
|
+
if (editingElement) {
|
|
183
|
+
editingElement.innerHTML = committedHTML;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const handleBlur = () => {
|
|
187
|
+
const { isCommitting, beginEditingAt, editor } = get();
|
|
188
|
+
if (isCommitting) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (typeof document !== "undefined" && document.hasFocus()) {
|
|
192
|
+
commitInlineEdit();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (Date.now() - beginEditingAt < FOCUS_STEAL_GUARD_MS) {
|
|
196
|
+
editor?.focus();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const finishPendingEdit = ({
|
|
200
|
+
editId,
|
|
201
|
+
status
|
|
202
|
+
}) => {
|
|
203
|
+
const pending = get().inlineEditRequest;
|
|
204
|
+
if (pending?.status !== "pending" || pending.editId !== editId) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!pending.element.isConnected) {
|
|
208
|
+
setInlineEditRequest({
|
|
209
|
+
activeMarks: EMPTY_ACTIVE_MARKS,
|
|
210
|
+
isInlineEditing: false,
|
|
211
|
+
next: null
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
setInlineEditRequest({
|
|
216
|
+
activeMarks: EMPTY_ACTIVE_MARKS,
|
|
217
|
+
isInlineEditing: false,
|
|
218
|
+
next: {
|
|
219
|
+
status,
|
|
220
|
+
element: pending.element
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
const resolveEditRequest = ({
|
|
225
|
+
editId,
|
|
226
|
+
allowed
|
|
227
|
+
}) => {
|
|
228
|
+
const pending = get().inlineEditRequest;
|
|
229
|
+
if (pending?.status !== "pending" || pending.editId !== editId) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!allowed) {
|
|
233
|
+
finishPendingEdit({ editId, status: "unavailable" });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (!pending.element.isConnected) {
|
|
237
|
+
setInlineEditRequest({
|
|
238
|
+
activeMarks: EMPTY_ACTIVE_MARKS,
|
|
239
|
+
isInlineEditing: false,
|
|
240
|
+
next: null
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
stopInlineEditRequest(pending);
|
|
245
|
+
set({ inlineEditRequest: null });
|
|
246
|
+
const element = pending.element;
|
|
247
|
+
const originalInnerHTML = element.innerHTML;
|
|
248
|
+
const editor = createInlineTiptapEditor({
|
|
249
|
+
element,
|
|
250
|
+
content: originalInnerHTML,
|
|
251
|
+
onCommit: commitInlineEdit,
|
|
252
|
+
onEscape: revertInlineEdit,
|
|
253
|
+
onBlur: handleBlur,
|
|
254
|
+
onSelectionUpdate: (activeMarks) => {
|
|
255
|
+
set({ activeMarks });
|
|
256
|
+
reportInlineEditingState({
|
|
257
|
+
activeMarks,
|
|
258
|
+
isInlineEditing: true,
|
|
259
|
+
request: null
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
set({
|
|
264
|
+
editor,
|
|
265
|
+
editingElement: element,
|
|
266
|
+
editId,
|
|
267
|
+
originalInnerHTML,
|
|
268
|
+
initialHTML: editor.getHTML(),
|
|
269
|
+
beginEditingAt: Date.now(),
|
|
270
|
+
isSelectionFrameEditing: true,
|
|
271
|
+
activeMarks: EMPTY_ACTIVE_MARKS
|
|
272
|
+
});
|
|
273
|
+
editor.focusAndSelectAll();
|
|
274
|
+
reportInlineEditingState({
|
|
275
|
+
activeMarks: EMPTY_ACTIVE_MARKS,
|
|
276
|
+
isInlineEditing: true,
|
|
277
|
+
request: null
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
const resetInteractionSession = () => {
|
|
281
|
+
const session = get().interactionSession;
|
|
282
|
+
if (session.type === "drag" && session.hasExceededThreshold) {
|
|
283
|
+
getWebsiteBuilderRpc()?.dragRectUpdated({
|
|
284
|
+
dragRect: null,
|
|
285
|
+
selectionRects: []
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const hoverFrameId = get().hoverSyncAnimationFrameId;
|
|
289
|
+
if (hoverFrameId !== null) {
|
|
290
|
+
cancelAnimationFrame(hoverFrameId);
|
|
291
|
+
}
|
|
292
|
+
const dragFrameId = get().dragAnimationFrameId;
|
|
293
|
+
if (dragFrameId !== null) {
|
|
294
|
+
cancelAnimationFrame(dragFrameId);
|
|
295
|
+
}
|
|
296
|
+
set({
|
|
297
|
+
interactionSession: IDLE_SESSION,
|
|
298
|
+
selectionFrames: [],
|
|
299
|
+
lastPostedHoverRect: void 0,
|
|
300
|
+
hoverSyncAnimationFrameId: null,
|
|
301
|
+
dragAnimationFrameId: null,
|
|
302
|
+
dragCandidates: []
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
...INITIAL_STATE,
|
|
307
|
+
actions: {
|
|
308
|
+
reset: () => {
|
|
309
|
+
const state = get();
|
|
310
|
+
stopInlineEditRequest(state.inlineEditRequest);
|
|
311
|
+
state.editor?.destroy();
|
|
312
|
+
if (state.hoverSyncAnimationFrameId !== null) {
|
|
313
|
+
cancelAnimationFrame(state.hoverSyncAnimationFrameId);
|
|
314
|
+
}
|
|
315
|
+
if (state.dragAnimationFrameId !== null) {
|
|
316
|
+
cancelAnimationFrame(state.dragAnimationFrameId);
|
|
317
|
+
}
|
|
318
|
+
if (state.scrollSyncAnimationFrameId !== null) {
|
|
319
|
+
cancelAnimationFrame(state.scrollSyncAnimationFrameId);
|
|
320
|
+
}
|
|
321
|
+
set({
|
|
322
|
+
...INITIAL_STATE,
|
|
323
|
+
editingPointerIds: /* @__PURE__ */ new Set(),
|
|
324
|
+
actions: state.actions
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
setDisplayMode: (displayMode) => {
|
|
328
|
+
if (get().displayMode === "selection" && displayMode !== "selection") {
|
|
329
|
+
resetInteractionSession();
|
|
330
|
+
revertInlineEdit();
|
|
331
|
+
}
|
|
332
|
+
set({ displayMode });
|
|
333
|
+
},
|
|
334
|
+
setSelectionType: ({ selectionType, selectors }) => {
|
|
335
|
+
if (selectionType === "none") {
|
|
336
|
+
resetInteractionSession();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const session = get().interactionSession;
|
|
340
|
+
if (selectionType !== "area" || !selectors || session.type !== "area") {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const retainedSelectors = new Set(selectors);
|
|
344
|
+
const items = session.items.filter((item) => {
|
|
345
|
+
return item.data.kind !== "react-element" || retainedSelectors.has(item.data.selector);
|
|
346
|
+
});
|
|
347
|
+
if (items.length === session.items.length) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (items.length === 0) {
|
|
351
|
+
resetInteractionSession();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
set({ interactionSession: { ...session, items } });
|
|
355
|
+
get().actions.showAreaSelectionFrames({
|
|
356
|
+
elements: items.map((item) => item.element),
|
|
357
|
+
scrollOffset: getDocumentScrollOffset()
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
setInteractionSession: (interactionSession) => {
|
|
361
|
+
set({ interactionSession });
|
|
362
|
+
},
|
|
363
|
+
resetInteractionSession,
|
|
364
|
+
postSelectedElementSnapshot: ({ clearMissing = false } = {}) => {
|
|
365
|
+
const { interactionSession } = get();
|
|
366
|
+
if (interactionSession.type !== "element") return true;
|
|
367
|
+
const element = getConnectedSelectedElement(interactionSession);
|
|
368
|
+
if (!element) {
|
|
369
|
+
if (!clearMissing) return false;
|
|
370
|
+
get().actions.setSelectionType({ selectionType: "none" });
|
|
371
|
+
getWebsiteBuilderRpc()?.areaSelected({
|
|
372
|
+
rect: interactionSession.data.rect,
|
|
373
|
+
items: []
|
|
374
|
+
});
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
const siblingElements = getRepetitionSiblingElements({ element });
|
|
378
|
+
const descriptor = buildElementDescriptor({
|
|
379
|
+
element,
|
|
380
|
+
clickPosition: interactionSession.clickPosition ?? (interactionSession.data.kind === "react-element" ? interactionSession.data.clickPosition : { x: 0, y: 0 }),
|
|
381
|
+
siblingSelectors: siblingElements.map(buildSelector)
|
|
382
|
+
});
|
|
383
|
+
if (descriptor.kind !== "react-element") return true;
|
|
384
|
+
const refreshedSession = {
|
|
385
|
+
...interactionSession,
|
|
386
|
+
element,
|
|
387
|
+
siblingElements
|
|
388
|
+
};
|
|
389
|
+
set({ interactionSession: refreshedSession });
|
|
390
|
+
get().actions.showElementSelectionFrames({
|
|
391
|
+
elements: [element, ...siblingElements],
|
|
392
|
+
scrollOffset: getDocumentScrollOffset()
|
|
393
|
+
});
|
|
394
|
+
void symbolicateSelectedElement(descriptor).then((symbolicated) => {
|
|
395
|
+
if (get().interactionSession !== refreshedSession) return;
|
|
396
|
+
if (symbolicated.sourceLocation && !isUserSourceLocation(symbolicated.sourceLocation))
|
|
397
|
+
return;
|
|
398
|
+
set({
|
|
399
|
+
interactionSession: { ...refreshedSession, data: symbolicated }
|
|
400
|
+
});
|
|
401
|
+
getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });
|
|
402
|
+
});
|
|
403
|
+
return true;
|
|
404
|
+
},
|
|
405
|
+
showElementSelectionFrames: ({ elements, scrollOffset }) => {
|
|
406
|
+
set({
|
|
407
|
+
selectionFrames: getElementSelectionFrameSpecs({
|
|
408
|
+
elements,
|
|
409
|
+
scrollOffset
|
|
410
|
+
})
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
showAreaSelectionFrames: ({ elements, scrollOffset }) => {
|
|
414
|
+
set({
|
|
415
|
+
selectionFrames: getAreaSelectionFrameSpecs({
|
|
416
|
+
elements,
|
|
417
|
+
scrollOffset
|
|
418
|
+
})
|
|
419
|
+
});
|
|
420
|
+
},
|
|
421
|
+
hideSelectionFrames: () => {
|
|
422
|
+
set({ selectionFrames: [] });
|
|
423
|
+
},
|
|
424
|
+
setLastPointerClientPosition: (lastPointerClientPosition) => {
|
|
425
|
+
set({ lastPointerClientPosition });
|
|
426
|
+
},
|
|
427
|
+
setLastPostedHoverRect: (lastPostedHoverRect) => {
|
|
428
|
+
set({ lastPostedHoverRect });
|
|
429
|
+
},
|
|
430
|
+
resetHoverRectDedup: () => {
|
|
431
|
+
set({ lastPostedHoverRect: void 0 });
|
|
432
|
+
},
|
|
433
|
+
setHoverSyncAnimationFrameId: (hoverSyncAnimationFrameId) => {
|
|
434
|
+
set({ hoverSyncAnimationFrameId });
|
|
435
|
+
},
|
|
436
|
+
setLastClick: (lastClick) => {
|
|
437
|
+
set({ lastClick });
|
|
438
|
+
},
|
|
439
|
+
setDragCandidates: (dragCandidates) => {
|
|
440
|
+
set({ dragCandidates });
|
|
441
|
+
},
|
|
442
|
+
setCommittedSelectionAtPointerDown: (committedSelectionAtPointerDown) => {
|
|
443
|
+
set({ committedSelectionAtPointerDown });
|
|
444
|
+
},
|
|
445
|
+
setDragAnimationFrameId: (dragAnimationFrameId) => {
|
|
446
|
+
set({ dragAnimationFrameId });
|
|
447
|
+
},
|
|
448
|
+
setScrollSyncAnimationFrameId: (scrollSyncAnimationFrameId) => {
|
|
449
|
+
set({ scrollSyncAnimationFrameId });
|
|
450
|
+
},
|
|
451
|
+
beginEditing: ({ element, descriptorPromise }) => {
|
|
452
|
+
const { editingElement, inlineEditRequest } = get();
|
|
453
|
+
if (editingElement === element) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (!isTextEditableElement(element)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (inlineEditRequest?.status === "pending" && inlineEditRequest.element === element) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (editingElement) {
|
|
463
|
+
revertInlineEdit();
|
|
464
|
+
}
|
|
465
|
+
const editId = crypto.randomUUID();
|
|
466
|
+
const timeout = window.setTimeout(
|
|
467
|
+
() => finishPendingEdit({ editId, status: "failed" }),
|
|
468
|
+
5e3
|
|
469
|
+
);
|
|
470
|
+
setInlineEditRequest({
|
|
471
|
+
activeMarks: EMPTY_ACTIVE_MARKS,
|
|
472
|
+
isInlineEditing: false,
|
|
473
|
+
next: {
|
|
474
|
+
status: "pending",
|
|
475
|
+
editId,
|
|
476
|
+
element,
|
|
477
|
+
timeout
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
void descriptorPromise.then(async (descriptor) => {
|
|
481
|
+
if (!isPendingEdit({
|
|
482
|
+
editId,
|
|
483
|
+
request: get().inlineEditRequest
|
|
484
|
+
})) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (!element.isConnected) {
|
|
488
|
+
setInlineEditRequest({
|
|
489
|
+
activeMarks: EMPTY_ACTIVE_MARKS,
|
|
490
|
+
isInlineEditing: false,
|
|
491
|
+
next: null
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (descriptor.kind !== "react-element") {
|
|
496
|
+
finishPendingEdit({ editId, status: "unavailable" });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
get().actions.setInteractionSession({
|
|
500
|
+
type: "element",
|
|
501
|
+
element,
|
|
502
|
+
siblingElements: getRepetitionSiblingElements({ element }),
|
|
503
|
+
clickPosition: null,
|
|
504
|
+
data: descriptor
|
|
505
|
+
});
|
|
506
|
+
const symbolicated = await symbolicateSelectedElement(descriptor);
|
|
507
|
+
if (!isPendingEdit({
|
|
508
|
+
editId,
|
|
509
|
+
request: get().inlineEditRequest
|
|
510
|
+
})) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const { allowed } = await requestInlineTextEdit({
|
|
514
|
+
editId,
|
|
515
|
+
element: symbolicated
|
|
516
|
+
});
|
|
517
|
+
resolveEditRequest({ editId, allowed });
|
|
518
|
+
}).catch(() => {
|
|
519
|
+
if (get().editId === editId) {
|
|
520
|
+
revertInlineEdit();
|
|
521
|
+
} else {
|
|
522
|
+
finishPendingEdit({ editId, status: "failed" });
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
},
|
|
526
|
+
commitInlineEdit,
|
|
527
|
+
revertInlineEdit,
|
|
528
|
+
formatCommand: (command) => {
|
|
529
|
+
get().editor?.runFormatCommand(command);
|
|
530
|
+
},
|
|
531
|
+
dismissInlineEditRequest: () => {
|
|
532
|
+
if (!get().inlineEditRequest) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
setInlineEditRequest({
|
|
536
|
+
activeMarks: EMPTY_ACTIVE_MARKS,
|
|
537
|
+
isInlineEditing: Boolean(get().editor),
|
|
538
|
+
next: null
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
function useSelectModeStore() {
|
|
546
|
+
return selectModeStore;
|
|
547
|
+
}
|
|
548
|
+
const useSelectModeStoreActions = () => {
|
|
549
|
+
return useShallowStore(useSelectModeStore(), (state) => state.actions);
|
|
550
|
+
};
|
|
551
|
+
const useDisplayMode = () => {
|
|
552
|
+
return useShallowStore(useSelectModeStore(), (state) => state.displayMode);
|
|
553
|
+
};
|
|
554
|
+
const useSelectionFrames = () => {
|
|
555
|
+
return useShallowStore(
|
|
556
|
+
useSelectModeStore(),
|
|
557
|
+
(state) => state.selectionFrames
|
|
558
|
+
);
|
|
559
|
+
};
|
|
560
|
+
const useIsSelectionFrameEditing = () => {
|
|
561
|
+
return useShallowStore(
|
|
562
|
+
useSelectModeStore(),
|
|
563
|
+
(state) => state.isSelectionFrameEditing
|
|
564
|
+
);
|
|
565
|
+
};
|
|
566
|
+
const useEditingElement = () => {
|
|
567
|
+
return useShallowStore(useSelectModeStore(), (state) => state.editingElement);
|
|
568
|
+
};
|
|
569
|
+
const useInteractionSession = () => {
|
|
570
|
+
return useShallowStore(
|
|
571
|
+
useSelectModeStore(),
|
|
572
|
+
(state) => state.interactionSession
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
export {
|
|
576
|
+
EMPTY_ACTIVE_MARKS,
|
|
577
|
+
selectModeStore,
|
|
578
|
+
useDisplayMode,
|
|
579
|
+
useEditingElement,
|
|
580
|
+
useInteractionSession,
|
|
581
|
+
useIsSelectionFrameEditing,
|
|
582
|
+
useSelectModeStore,
|
|
583
|
+
useSelectModeStoreActions,
|
|
584
|
+
useSelectionFrames
|
|
585
|
+
};
|
|
@@ -1,77 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* postMessage hop -> Zustand update -> React reconciliation, which
|
|
7
|
-
* shows up as visible lag when the user scrolls quickly.
|
|
8
|
-
*
|
|
9
|
-
* By rendering directly inside the canopy iframe's document we get two
|
|
10
|
-
* properties for free:
|
|
11
|
-
*
|
|
12
|
-
* 1. `position: absolute` + page coordinates means the document's own
|
|
13
|
-
* scroll naturally translates the frames — zero JavaScript per
|
|
14
|
-
* scroll frame, the browser composites them with the underlying page
|
|
15
|
-
* content.
|
|
16
|
-
* 2. No bridge serialization: the only cost when selection commits is
|
|
17
|
-
* a handful of style writes on the frame nodes.
|
|
18
|
-
*
|
|
19
|
-
* A click resolves to one frame per repeated element; an area drag resolves
|
|
20
|
-
* to one frame per captured element. The renderer keeps a pool of nodes under
|
|
21
|
-
* a shared container and reconciles it on each call.
|
|
22
|
-
*
|
|
23
|
-
* The canopy iframe loads a customer page and doesn't include lattice /
|
|
24
|
-
* tailwind, so styling is intentionally vanilla inline CSS rather than
|
|
25
|
-
* Tailwind classes.
|
|
26
|
-
*/
|
|
27
|
-
type SelectionFrameVariant = "element" | "area";
|
|
28
|
-
type SelectionFramePageRect = {
|
|
1
|
+
export declare const SELECTION_FRAMES_CONTAINER_ID = "replo-select-mode-selection-frames";
|
|
2
|
+
export declare const SELECTION_FRAME_BORDER_HEX = "#c026d3";
|
|
3
|
+
export declare const SELECTION_FRAME_EDITING_BORDER_HEX = "#e879f9";
|
|
4
|
+
export type SelectionFrameVariant = "element" | "area";
|
|
5
|
+
export type SelectionFramePageRect = {
|
|
29
6
|
x: number;
|
|
30
7
|
y: number;
|
|
31
8
|
width: number;
|
|
32
9
|
height: number;
|
|
33
10
|
};
|
|
34
|
-
type SelectionFrameSpec = {
|
|
11
|
+
export type SelectionFrameSpec = {
|
|
35
12
|
pageRect: SelectionFramePageRect;
|
|
36
13
|
variant: SelectionFrameVariant;
|
|
37
14
|
};
|
|
38
|
-
|
|
39
|
-
* Toggle the brighter inline-edit border and re-color any frames already on
|
|
40
|
-
* screen so the change is immediate, not deferred to the next redraw.
|
|
41
|
-
*/
|
|
42
|
-
export declare function setSelectionFrameEditing(editing: boolean): void;
|
|
43
|
-
/**
|
|
44
|
-
* Shared body-level layer container for in-iframe editor overlays (selection
|
|
45
|
-
* frames, annotation frames). Zero-size and pointer-transparent so it never
|
|
46
|
-
* affects the page; children opt into pointer events individually.
|
|
47
|
-
*/
|
|
48
|
-
export declare function getOrCreateFramesLayerContainer({ id, zIndex, }: {
|
|
49
|
-
id: string;
|
|
50
|
-
zIndex: number;
|
|
51
|
-
}): HTMLDivElement | null;
|
|
52
|
-
/**
|
|
53
|
-
* Position selection frames at the given *page* coordinates, one node per
|
|
54
|
-
* spec. Page coords don't shift during scroll, so the caller is free to
|
|
55
|
-
* call this once at commit time and let the browser handle the rest of the
|
|
56
|
-
* scroll lifetime. Reconciles the node pool against `frames.length` so group
|
|
57
|
-
* updates reuse the existing nodes when the repetition count stays stable.
|
|
58
|
-
*/
|
|
59
|
-
export declare function showSelectionFrames({ frames, }: {
|
|
60
|
-
frames: SelectionFrameSpec[];
|
|
61
|
-
}): void;
|
|
62
|
-
export declare function showElementSelectionFrames({ elements, scrollOffset, }: {
|
|
15
|
+
export declare function getElementSelectionFrameSpecs({ elements, scrollOffset, }: {
|
|
63
16
|
elements: Element[];
|
|
64
17
|
scrollOffset: {
|
|
65
18
|
left: number;
|
|
66
19
|
top: number;
|
|
67
20
|
};
|
|
68
|
-
}):
|
|
69
|
-
export declare function
|
|
21
|
+
}): SelectionFrameSpec[];
|
|
22
|
+
export declare function getAreaSelectionFrameSpecs({ elements, scrollOffset, }: {
|
|
70
23
|
elements: Element[];
|
|
71
24
|
scrollOffset: {
|
|
72
25
|
left: number;
|
|
73
26
|
top: number;
|
|
74
27
|
};
|
|
75
|
-
}):
|
|
76
|
-
export declare function hideSelectionFrame(): void;
|
|
77
|
-
export {};
|
|
28
|
+
}): SelectionFrameSpec[];
|