@replohq/sandbox-runtime 1.19.5 → 1.21.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/PagePreviewHeightResponder.d.ts +1 -0
- package/PagePreviewHeightResponder.js +159 -0
- package/_vendor/schemas/comments.d.ts +898 -0
- package/_vendor/schemas/commentsOverlayRpc.d.ts +348 -0
- package/_vendor/schemas/commentsOverlayRpc.mjs +92 -0
- package/_vendor/schemas/generated/comments.d.ts +237 -0
- package/_vendor/schemas/generated/commentsOverlayRpc.d.ts +18 -0
- package/_vendor/schemas/generated/selectMode.d.ts +3 -1
- package/_vendor/schemas/pagePreviewHeight.d.ts +14 -0
- package/_vendor/schemas/pagePreviewHeight.mjs +18 -0
- package/_vendor/schemas/selectMode.d.ts +113 -0
- package/_vendor/schemas/selectMode.mjs +1 -1
- package/_vendor/schemas/selectModeIframeRpc.d.ts +226 -0
- package/_vendor/schemas/selectModeIframeRpc.mjs +1 -1
- package/buildMetadata.js +3 -3
- package/chunk-NVOBD46N.mjs +541 -0
- package/comments-overlay/CommentsOverlayMount.d.ts +1 -0
- package/comments-overlay/CommentsOverlayMount.js +44 -0
- package/comments-overlay/LICENSE-THIRD-PARTY.md +45 -0
- package/comments-overlay/bridge.d.ts +11 -0
- package/comments-overlay/bridge.js +136 -0
- package/comments-overlay/capture.d.ts +55 -0
- package/comments-overlay/capture.js +250 -0
- package/comments-overlay/originGate.d.ts +1 -0
- package/comments-overlay/originGate.js +11 -0
- package/comments-overlay/overlay.d.ts +52 -0
- package/comments-overlay/overlay.js +781 -0
- package/comments-overlay/resolve.d.ts +27 -0
- package/comments-overlay/resolve.js +201 -0
- package/comments-overlay/sourceLookup.d.ts +11 -0
- package/comments-overlay/sourceLookup.js +63 -0
- package/comments-overlay/textQuote.d.ts +17 -0
- package/comments-overlay/textQuote.js +87 -0
- package/comments-overlay/vendor/bippy.d.ts +53 -0
- package/comments-overlay/vendor/bippy.js +402 -0
- package/comments-overlay/vendor/elementAnchors.d.ts +9 -0
- package/comments-overlay/vendor/elementAnchors.js +144 -0
- package/comments-overlay/vendor/fiberElement.d.ts +21 -0
- package/comments-overlay/vendor/fiberElement.js +251 -0
- package/comments-overlay/vendor/htmlPreview.d.ts +13 -0
- package/comments-overlay/vendor/htmlPreview.js +179 -0
- package/comments-overlay/vendor/selector.d.ts +38 -0
- package/comments-overlay/vendor/selector.js +372 -0
- package/package.json +5 -4
- package/pinPreviewViewportUnits.d.ts +4 -0
- package/pinPreviewViewportUnits.js +76 -0
- package/sandbox-runtime.js +4 -0
- package/select-mode/descriptor/elementDescriptor.d.ts +4 -3
- package/select-mode/descriptor/elementDescriptor.js +20 -30
- package/select-mode/iframeRpcChannel.d.ts +84 -0
- package/select-mode/symbolicate.js +25 -7
- package/chunk-QTGDSYSU.mjs +0 -249
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
import { captureAnchor, getAnchorElement } from "./capture";
|
|
2
|
+
import { isResolutionVisible, resolveAnchor } from "./resolve";
|
|
3
|
+
import { OVERLAY_ROOT_ATTRIBUTE } from "./textQuote";
|
|
4
|
+
const OVERLAY_Z_INDEX = 2147483645;
|
|
5
|
+
const HIGHLIGHT_BACKGROUND = "rgba(250, 204, 21, 0.28)";
|
|
6
|
+
const QUOTE_UNDERLINE = "2px solid rgba(250, 204, 21, 0.9)";
|
|
7
|
+
const BADGE_SIZE_PX = 20;
|
|
8
|
+
const BADGE_HIT_TARGET_PX = 44;
|
|
9
|
+
const COMPACT_GUTTER_MAX_WIDTH_PX = 600;
|
|
10
|
+
const RERESOLVE_DEBOUNCE_MS = 250;
|
|
11
|
+
const SELECTION_PROMPT_DEBOUNCE_MS = 150;
|
|
12
|
+
const TEXT_PICK_CLICK_SUPPRESS_MS = 750;
|
|
13
|
+
const HOVER_OUTLINE_STYLE = "position:absolute;pointer-events:none;box-sizing:border-box;border:1px solid rgba(37,99,235,.45);background:rgba(37,99,235,.025);border-radius:3px";
|
|
14
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
15
|
+
const EDITABLE_SELECTOR = "input, textarea, select, [contenteditable]:not([contenteditable='false'])";
|
|
16
|
+
function getReportedStatus(resolution) {
|
|
17
|
+
if (!resolution || resolution.status === "detached") {
|
|
18
|
+
return "detached";
|
|
19
|
+
}
|
|
20
|
+
return isResolutionVisible(resolution) ? resolution.status : "hidden";
|
|
21
|
+
}
|
|
22
|
+
function getResolutionRects(resolution) {
|
|
23
|
+
if (resolution.range) {
|
|
24
|
+
return Array.from(resolution.range.getClientRects());
|
|
25
|
+
}
|
|
26
|
+
return resolution.element ? [resolution.element.getBoundingClientRect()] : [];
|
|
27
|
+
}
|
|
28
|
+
function createCommentsOverlay(options) {
|
|
29
|
+
let mode = "browse";
|
|
30
|
+
let pins = [];
|
|
31
|
+
let resolutions = /* @__PURE__ */ new Map();
|
|
32
|
+
const boundElements = /* @__PURE__ */ new Map();
|
|
33
|
+
let focusedThreadId = null;
|
|
34
|
+
let hoveredBadgeThreadId = null;
|
|
35
|
+
let draftAnchor = null;
|
|
36
|
+
let draftResolution = null;
|
|
37
|
+
let draftElement = null;
|
|
38
|
+
let statusesKey = "";
|
|
39
|
+
let hoveredElement = null;
|
|
40
|
+
let hoverOutline = null;
|
|
41
|
+
let selectionPrompt = null;
|
|
42
|
+
let dismissedSelectionKey = null;
|
|
43
|
+
let promptButton = null;
|
|
44
|
+
let pointerIsDown = false;
|
|
45
|
+
let capturedTextSelection = false;
|
|
46
|
+
let suppressClickUntil = 0;
|
|
47
|
+
let paintFrame = null;
|
|
48
|
+
let resolveTimer = null;
|
|
49
|
+
let selectionTimer = null;
|
|
50
|
+
let originalCursor = null;
|
|
51
|
+
let isDestroyed = false;
|
|
52
|
+
const root = document.createElement("div");
|
|
53
|
+
root.setAttribute(OVERLAY_ROOT_ATTRIBUTE, "true");
|
|
54
|
+
root.style.cssText = `position:fixed;inset:0;z-index:${OVERLAY_Z_INDEX};pointer-events:none;overflow:hidden`;
|
|
55
|
+
document.documentElement.append(root);
|
|
56
|
+
const isInOverlay = (node) => {
|
|
57
|
+
return node instanceof Node && root.contains(node);
|
|
58
|
+
};
|
|
59
|
+
const getEventElement = (event) => {
|
|
60
|
+
const inner = event.composedPath().find((node) => {
|
|
61
|
+
return node instanceof Element;
|
|
62
|
+
});
|
|
63
|
+
return inner ?? event.target;
|
|
64
|
+
};
|
|
65
|
+
const isPageElement = (node) => {
|
|
66
|
+
return node instanceof Element && node !== document.body && node !== document.documentElement && !isInOverlay(node);
|
|
67
|
+
};
|
|
68
|
+
const isEditableSelection = (selection) => {
|
|
69
|
+
const node = selection.anchorNode;
|
|
70
|
+
const element = node instanceof Element ? node : node?.parentElement;
|
|
71
|
+
return Boolean(
|
|
72
|
+
element?.closest(EDITABLE_SELECTOR) || document.activeElement?.matches(EDITABLE_SELECTOR)
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
const getUsableSelectionRange = () => {
|
|
76
|
+
const selection = window.getSelection();
|
|
77
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (isEditableSelection(selection)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const range = selection.getRangeAt(0);
|
|
84
|
+
if (!range.toString().trim() || isInOverlay(range.commonAncestorContainer)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return range;
|
|
88
|
+
};
|
|
89
|
+
const getSelectionKey = (range) => {
|
|
90
|
+
const before = range.cloneRange();
|
|
91
|
+
before.selectNodeContents(document.body);
|
|
92
|
+
before.setEnd(range.startContainer, range.startOffset);
|
|
93
|
+
return JSON.stringify([range.toString(), before.toString().length]);
|
|
94
|
+
};
|
|
95
|
+
const schedulePaint = () => {
|
|
96
|
+
if (paintFrame !== null || isDestroyed) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
paintFrame = window.requestAnimationFrame(() => {
|
|
100
|
+
paintFrame = null;
|
|
101
|
+
paint();
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const getElementAnchors = () => {
|
|
105
|
+
return pins.flatMap((pin) => {
|
|
106
|
+
return pin.anchor.type === "element" ? [{ pin, anchor: pin.anchor }] : [];
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const resolveAll = () => {
|
|
110
|
+
resolveTimer = null;
|
|
111
|
+
observeShadowRoots(document);
|
|
112
|
+
const sourceCache = /* @__PURE__ */ new Map();
|
|
113
|
+
const getSource = options.getSource;
|
|
114
|
+
const cachedGetSource = getSource ? (element) => {
|
|
115
|
+
if (!sourceCache.has(element)) {
|
|
116
|
+
sourceCache.set(element, getSource(element));
|
|
117
|
+
}
|
|
118
|
+
return sourceCache.get(element) ?? null;
|
|
119
|
+
} : null;
|
|
120
|
+
const next = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const { pin, anchor } of getElementAnchors()) {
|
|
122
|
+
const resolution = resolveAnchor(anchor, document, {
|
|
123
|
+
previous: boundElements.get(pin.threadId) ?? null,
|
|
124
|
+
getSource: cachedGetSource
|
|
125
|
+
});
|
|
126
|
+
next.set(pin.threadId, resolution);
|
|
127
|
+
if (resolution.element) {
|
|
128
|
+
boundElements.set(pin.threadId, resolution.element);
|
|
129
|
+
} else {
|
|
130
|
+
boundElements.delete(pin.threadId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
resolutions = next;
|
|
134
|
+
draftResolution = draftAnchor ? resolveAnchor(draftAnchor, document, {
|
|
135
|
+
previous: draftElement,
|
|
136
|
+
getSource: cachedGetSource
|
|
137
|
+
}) : null;
|
|
138
|
+
draftElement = draftResolution?.element ?? null;
|
|
139
|
+
const statuses = getElementAnchors().map(({ pin }) => {
|
|
140
|
+
return {
|
|
141
|
+
threadId: pin.threadId,
|
|
142
|
+
status: getReportedStatus(resolutions.get(pin.threadId))
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
const nextKey = JSON.stringify(statuses);
|
|
146
|
+
if (nextKey !== statusesKey) {
|
|
147
|
+
statusesKey = nextKey;
|
|
148
|
+
options.onAnchorStatuses({ statuses });
|
|
149
|
+
}
|
|
150
|
+
schedulePaint();
|
|
151
|
+
};
|
|
152
|
+
const scheduleResolve = () => {
|
|
153
|
+
if (resolveTimer !== null || isDestroyed) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
resolveTimer = setTimeout(resolveAll, RERESOLVE_DEBOUNCE_MS);
|
|
157
|
+
};
|
|
158
|
+
const paintHighlight = (resolution, threadId) => {
|
|
159
|
+
const isQuote = Boolean(resolution.range);
|
|
160
|
+
for (const rect of getResolutionRects(resolution)) {
|
|
161
|
+
if (rect.width <= 0 || rect.height <= 0 || rect.bottom < 0 || rect.top > window.innerHeight) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const highlight = document.createElement("div");
|
|
165
|
+
if (threadId) {
|
|
166
|
+
highlight.dataset.commentHighlight = threadId;
|
|
167
|
+
} else {
|
|
168
|
+
highlight.dataset.commentDraft = "true";
|
|
169
|
+
}
|
|
170
|
+
highlight.style.cssText = `position:absolute;pointer-events:none;background:${HIGHLIGHT_BACKGROUND};border-radius:2px;box-sizing:border-box`;
|
|
171
|
+
if (isQuote) {
|
|
172
|
+
highlight.style.borderBottom = QUOTE_UNDERLINE;
|
|
173
|
+
}
|
|
174
|
+
highlight.style.left = `${rect.left}px`;
|
|
175
|
+
highlight.style.top = `${rect.top}px`;
|
|
176
|
+
highlight.style.width = `${rect.width}px`;
|
|
177
|
+
highlight.style.height = `${rect.height}px`;
|
|
178
|
+
root.append(highlight);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const isHighlightShown = (threadId) => {
|
|
182
|
+
return mode === "comment" || threadId === focusedThreadId || threadId === hoveredBadgeThreadId;
|
|
183
|
+
};
|
|
184
|
+
const createIcon = (size, path) => {
|
|
185
|
+
const icon = document.createElementNS(SVG_NAMESPACE, "svg");
|
|
186
|
+
for (const [name, value] of Object.entries({
|
|
187
|
+
viewBox: "0 0 24 24",
|
|
188
|
+
width: size,
|
|
189
|
+
height: size,
|
|
190
|
+
"aria-hidden": "true",
|
|
191
|
+
fill: "none",
|
|
192
|
+
stroke: "currentColor",
|
|
193
|
+
"stroke-width": "2"
|
|
194
|
+
})) {
|
|
195
|
+
icon.setAttribute(name, value);
|
|
196
|
+
}
|
|
197
|
+
const bubble2 = document.createElementNS(SVG_NAMESPACE, "path");
|
|
198
|
+
bubble2.setAttribute("d", path);
|
|
199
|
+
icon.append(bubble2);
|
|
200
|
+
return icon;
|
|
201
|
+
};
|
|
202
|
+
const paintHover = () => {
|
|
203
|
+
if (mode !== "comment" || !hoveredElement?.isConnected) {
|
|
204
|
+
hoverOutline?.remove();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const rect = hoveredElement.getBoundingClientRect();
|
|
208
|
+
hoverOutline ??= document.createElement("div");
|
|
209
|
+
hoverOutline.style.cssText = HOVER_OUTLINE_STYLE;
|
|
210
|
+
hoverOutline.style.left = `${rect.left}px`;
|
|
211
|
+
hoverOutline.style.top = `${rect.top}px`;
|
|
212
|
+
hoverOutline.style.width = `${rect.width}px`;
|
|
213
|
+
hoverOutline.style.height = `${rect.height}px`;
|
|
214
|
+
root.append(hoverOutline);
|
|
215
|
+
};
|
|
216
|
+
const getPromptButton = () => {
|
|
217
|
+
if (promptButton) {
|
|
218
|
+
return promptButton;
|
|
219
|
+
}
|
|
220
|
+
const button = document.createElement("button");
|
|
221
|
+
button.type = "button";
|
|
222
|
+
button.dataset.commentSelectionPrompt = "true";
|
|
223
|
+
button.setAttribute("aria-label", "Comment on selection");
|
|
224
|
+
button.style.cssText = "position:absolute;pointer-events:auto;display:flex;align-items:center;justify-content:center;gap:6px;box-sizing:border-box;margin:0;padding:0 12px;border:0;border-radius:999px;background:#111827;color:#fff;font:500 13px/1 system-ui,-apple-system,sans-serif;box-shadow:0 4px 14px rgba(0,0,0,.28);cursor:pointer;outline-offset:2px";
|
|
225
|
+
const icon = createIcon(
|
|
226
|
+
"14",
|
|
227
|
+
"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
|
|
228
|
+
);
|
|
229
|
+
const label = document.createElement("span");
|
|
230
|
+
label.textContent = "Comment";
|
|
231
|
+
button.append(icon, label);
|
|
232
|
+
button.addEventListener("pointerdown", (event) => event.preventDefault());
|
|
233
|
+
button.addEventListener("mousedown", (event) => event.preventDefault());
|
|
234
|
+
button.addEventListener("focus", () => {
|
|
235
|
+
button.style.outline = button.matches(":focus-visible") ? "2px solid #60a5fa" : "none";
|
|
236
|
+
});
|
|
237
|
+
button.addEventListener("blur", () => {
|
|
238
|
+
button.style.outline = "none";
|
|
239
|
+
});
|
|
240
|
+
button.addEventListener("click", (event) => {
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
event.stopPropagation();
|
|
243
|
+
const range = getUsableSelectionRange();
|
|
244
|
+
if (!range) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
dismissedSelectionKey = getSelectionKey(range);
|
|
248
|
+
selectionPrompt = null;
|
|
249
|
+
const rect = range.getBoundingClientRect();
|
|
250
|
+
pick(range.cloneRange(), { x: rect.right, y: rect.bottom }, (anchor) => {
|
|
251
|
+
options.onComposeFromSelection({ anchor });
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
promptButton = button;
|
|
255
|
+
return button;
|
|
256
|
+
};
|
|
257
|
+
const paintSelectionPrompt = () => {
|
|
258
|
+
const rects = selectionPrompt ? Array.from(selectionPrompt.range.getClientRects()).filter((rect) => {
|
|
259
|
+
return rect.width > 0 && rect.height > 0;
|
|
260
|
+
}) : [];
|
|
261
|
+
const first = rects[0];
|
|
262
|
+
const last = rects.at(-1);
|
|
263
|
+
if (!first || !last || last.bottom < 0 || first.top > window.innerHeight) {
|
|
264
|
+
promptButton?.remove();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const isCoarse = window.matchMedia?.("(pointer: coarse)")?.matches ?? false;
|
|
268
|
+
const height = isCoarse ? 44 : 32;
|
|
269
|
+
const width = isCoarse ? 120 : 104;
|
|
270
|
+
const gap = 6;
|
|
271
|
+
const left = Math.max(
|
|
272
|
+
8,
|
|
273
|
+
Math.min(window.innerWidth - width - 8, last.right - width)
|
|
274
|
+
);
|
|
275
|
+
const below = last.bottom + gap;
|
|
276
|
+
const top = below + height <= window.innerHeight - 8 ? below : Math.max(8, first.top - gap - height);
|
|
277
|
+
const button = getPromptButton();
|
|
278
|
+
button.style.left = `${left}px`;
|
|
279
|
+
button.style.top = `${top}px`;
|
|
280
|
+
button.style.width = `${width}px`;
|
|
281
|
+
button.style.height = `${height}px`;
|
|
282
|
+
if (button.parentNode !== root) {
|
|
283
|
+
root.append(button);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const paintBadges = () => {
|
|
287
|
+
const isCompact = window.innerWidth <= COMPACT_GUTTER_MAX_WIDTH_PX;
|
|
288
|
+
const size = BADGE_HIT_TARGET_PX;
|
|
289
|
+
const maxLeft = Math.max(0, window.innerWidth - size);
|
|
290
|
+
const maxTop = Math.max(0, window.innerHeight - size);
|
|
291
|
+
const placed = [];
|
|
292
|
+
const place = (rect) => {
|
|
293
|
+
const baseLeft = isCompact ? maxLeft : Math.max(0, Math.min(maxLeft, rect.right - size / 2));
|
|
294
|
+
const baseTop = Math.max(0, Math.min(maxTop, rect.top - size / 2));
|
|
295
|
+
const columns = Math.ceil(maxLeft / size);
|
|
296
|
+
const rows = Math.ceil(maxTop / size);
|
|
297
|
+
for (let column = 0; column <= columns * 2; column++) {
|
|
298
|
+
const left = baseLeft + (column % 2 ? 1 : -1) * Math.ceil(column / 2) * size;
|
|
299
|
+
if (left < 0 || left > maxLeft) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
for (let row = 0; row <= rows * 2; row++) {
|
|
303
|
+
const top = baseTop + (row % 2 ? 1 : -1) * Math.ceil(row / 2) * size;
|
|
304
|
+
if (top < 0 || top > maxTop) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const collides = placed.some((slot) => {
|
|
308
|
+
return Math.abs(slot.left - left) < size && Math.abs(slot.top - top) < size;
|
|
309
|
+
});
|
|
310
|
+
if (!collides) {
|
|
311
|
+
placed.push({ left, top });
|
|
312
|
+
return { left, top };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
placed.push({ left: baseLeft, top: baseTop });
|
|
317
|
+
return { left: baseLeft, top: baseTop };
|
|
318
|
+
};
|
|
319
|
+
for (const pin of pins) {
|
|
320
|
+
const resolution = resolutions.get(pin.threadId);
|
|
321
|
+
const isFocused = pin.threadId === focusedThreadId;
|
|
322
|
+
if (!resolution || pin.status === "resolved" && !isFocused || !isResolutionVisible(resolution)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (isHighlightShown(pin.threadId)) {
|
|
326
|
+
paintHighlight(resolution, pin.threadId);
|
|
327
|
+
}
|
|
328
|
+
const visibleRects = getResolutionRects(resolution).filter((rect) => {
|
|
329
|
+
return rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.top < window.innerHeight;
|
|
330
|
+
});
|
|
331
|
+
const first = visibleRects[0];
|
|
332
|
+
if (!first) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const position = place(first);
|
|
336
|
+
const badge = document.createElement("button");
|
|
337
|
+
badge.type = "button";
|
|
338
|
+
badge.dataset.commentBadge = pin.threadId;
|
|
339
|
+
badge.setAttribute(
|
|
340
|
+
"aria-label",
|
|
341
|
+
pin.count === 1 ? "Open comment" : `Open ${pin.count} comments`
|
|
342
|
+
);
|
|
343
|
+
badge.style.cssText = `position:absolute;pointer-events:auto;display:flex;align-items:center;justify-content:center;width:${size}px;height:${size}px;padding:0;margin:0;border:0;background:transparent;cursor:pointer`;
|
|
344
|
+
if (isCompact) {
|
|
345
|
+
badge.style.justifyContent = "flex-end";
|
|
346
|
+
}
|
|
347
|
+
badge.style.left = `${position.left}px`;
|
|
348
|
+
badge.style.top = `${position.top}px`;
|
|
349
|
+
const isActive = isFocused || pin.threadId === hoveredBadgeThreadId;
|
|
350
|
+
const dot = document.createElement("span");
|
|
351
|
+
dot.style.cssText = `display:flex;align-items:center;justify-content:center;gap:3px;box-sizing:border-box;min-width:${BADGE_SIZE_PX}px;height:${BADGE_SIZE_PX}px;padding:0 ${pin.count === 1 ? 0 : 6}px;border-radius:${BADGE_SIZE_PX / 2}px;border:1px solid rgba(17,24,39,.14);background:#fff;color:#6b7280;box-shadow:0 1px 3px rgba(0,0,0,.18);font:600 11px/1 system-ui,-apple-system,sans-serif;font-variant-numeric:tabular-nums;white-space:nowrap`;
|
|
352
|
+
if (isCompact) {
|
|
353
|
+
dot.style.marginRight = "3px";
|
|
354
|
+
}
|
|
355
|
+
if (isActive) {
|
|
356
|
+
dot.style.borderColor = "#f59e0b";
|
|
357
|
+
dot.style.background = "#fef3c7";
|
|
358
|
+
dot.style.color = "#b45309";
|
|
359
|
+
}
|
|
360
|
+
if (resolution.status === "ambiguous") {
|
|
361
|
+
dot.style.borderStyle = "dashed";
|
|
362
|
+
}
|
|
363
|
+
const icon = createIcon("12", "M7.9 20A9 9 0 1 0 4 16.1L2 22Z");
|
|
364
|
+
icon.setAttribute("stroke-linecap", "round");
|
|
365
|
+
icon.setAttribute("stroke-linejoin", "round");
|
|
366
|
+
dot.append(icon);
|
|
367
|
+
if (pin.count > 1) {
|
|
368
|
+
dot.append(pin.count > 99 ? "99+" : String(pin.count));
|
|
369
|
+
}
|
|
370
|
+
badge.append(dot);
|
|
371
|
+
badge.addEventListener("click", (event) => {
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
event.stopPropagation();
|
|
374
|
+
options.onPinClicked({ threadId: pin.threadId });
|
|
375
|
+
});
|
|
376
|
+
root.append(badge);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
function paint() {
|
|
380
|
+
for (const child of Array.from(root.children)) {
|
|
381
|
+
if (child !== promptButton || !selectionPrompt) {
|
|
382
|
+
child.remove();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (draftResolution && isResolutionVisible(draftResolution)) {
|
|
386
|
+
paintHighlight(draftResolution, null);
|
|
387
|
+
}
|
|
388
|
+
paintBadges();
|
|
389
|
+
paintHover();
|
|
390
|
+
paintSelectionPrompt();
|
|
391
|
+
}
|
|
392
|
+
async function finishAnchor(anchor) {
|
|
393
|
+
if (!options.prepareAnchor) {
|
|
394
|
+
return anchor;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
return await options.prepareAnchor(anchor);
|
|
398
|
+
} catch {
|
|
399
|
+
return anchor;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const reportPicked = (anchor, rect) => {
|
|
403
|
+
const { left: x, top: y, width, height } = rect;
|
|
404
|
+
options.onAnchorPicked({ anchor, rect: { x, y, width, height } });
|
|
405
|
+
};
|
|
406
|
+
const pick = (target, point, report) => {
|
|
407
|
+
const element = getAnchorElement(target);
|
|
408
|
+
if (!element) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const anchor = captureAnchor(target, point);
|
|
412
|
+
const range = "tagName" in target ? void 0 : target;
|
|
413
|
+
const rect = (range ?? element).getBoundingClientRect();
|
|
414
|
+
draftAnchor = anchor;
|
|
415
|
+
draftElement = element;
|
|
416
|
+
draftResolution = {
|
|
417
|
+
status: "attached",
|
|
418
|
+
element,
|
|
419
|
+
...range ? { range } : {}
|
|
420
|
+
};
|
|
421
|
+
hoveredElement = null;
|
|
422
|
+
void finishAnchor(anchor).then((prepared) => report(prepared, rect));
|
|
423
|
+
schedulePaint();
|
|
424
|
+
};
|
|
425
|
+
const clearDraft = () => {
|
|
426
|
+
draftAnchor = null;
|
|
427
|
+
draftResolution = null;
|
|
428
|
+
draftElement = null;
|
|
429
|
+
};
|
|
430
|
+
const refreshSelectionPrompt = () => {
|
|
431
|
+
selectionTimer = null;
|
|
432
|
+
const range = mode !== "comment" && !pointerIsDown ? getUsableSelectionRange() : null;
|
|
433
|
+
if (!range) {
|
|
434
|
+
dismissedSelectionKey = null;
|
|
435
|
+
if (selectionPrompt) {
|
|
436
|
+
selectionPrompt = null;
|
|
437
|
+
schedulePaint();
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const key = getSelectionKey(range);
|
|
442
|
+
if (key === dismissedSelectionKey) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
selectionPrompt = { range: range.cloneRange(), key };
|
|
446
|
+
schedulePaint();
|
|
447
|
+
};
|
|
448
|
+
const scheduleSelectionPrompt = () => {
|
|
449
|
+
if (selectionTimer !== null) {
|
|
450
|
+
clearTimeout(selectionTimer);
|
|
451
|
+
}
|
|
452
|
+
selectionTimer = setTimeout(
|
|
453
|
+
refreshSelectionPrompt,
|
|
454
|
+
SELECTION_PROMPT_DEBOUNCE_MS
|
|
455
|
+
);
|
|
456
|
+
};
|
|
457
|
+
const captureTextPick = (event) => {
|
|
458
|
+
if (mode !== "comment") {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const range = getUsableSelectionRange();
|
|
462
|
+
if (!range) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
capturedTextSelection = true;
|
|
466
|
+
suppressClickUntil = Date.now() + TEXT_PICK_CLICK_SUPPRESS_MS;
|
|
467
|
+
pick(
|
|
468
|
+
range.cloneRange(),
|
|
469
|
+
{ x: event.clientX, y: event.clientY },
|
|
470
|
+
reportPicked
|
|
471
|
+
);
|
|
472
|
+
return true;
|
|
473
|
+
};
|
|
474
|
+
const getBadgeThreadId = (target) => {
|
|
475
|
+
if (!(target instanceof Element) || !isInOverlay(target)) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
return target.closest("[data-comment-badge]")?.dataset.commentBadge ?? null;
|
|
479
|
+
};
|
|
480
|
+
const handlePointerMove = (event) => {
|
|
481
|
+
const target = getEventElement(event);
|
|
482
|
+
const nextBadgeThreadId = getBadgeThreadId(target);
|
|
483
|
+
if (nextBadgeThreadId !== hoveredBadgeThreadId) {
|
|
484
|
+
hoveredBadgeThreadId = nextBadgeThreadId;
|
|
485
|
+
schedulePaint();
|
|
486
|
+
}
|
|
487
|
+
if (mode !== "comment") {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const nextHovered = isPageElement(target) ? getAnchorElement(target) : null;
|
|
491
|
+
if (nextHovered !== hoveredElement) {
|
|
492
|
+
hoveredElement = nextHovered;
|
|
493
|
+
schedulePaint();
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const handlePointerLeave = (event) => {
|
|
497
|
+
if (event.target === document || event.target === document.documentElement) {
|
|
498
|
+
pointerIsDown = false;
|
|
499
|
+
scheduleSelectionPrompt();
|
|
500
|
+
if (hoveredBadgeThreadId) {
|
|
501
|
+
hoveredBadgeThreadId = null;
|
|
502
|
+
schedulePaint();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (hoveredElement) {
|
|
506
|
+
hoveredElement = null;
|
|
507
|
+
schedulePaint();
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
const handlePress = (event) => {
|
|
511
|
+
const target = getEventElement(event);
|
|
512
|
+
const inOverlay = isInOverlay(target);
|
|
513
|
+
if (event.type === "pointerdown") {
|
|
514
|
+
capturedTextSelection = false;
|
|
515
|
+
suppressClickUntil = 0;
|
|
516
|
+
if (!inOverlay) {
|
|
517
|
+
pointerIsDown = true;
|
|
518
|
+
if (selectionPrompt) {
|
|
519
|
+
selectionPrompt = null;
|
|
520
|
+
schedulePaint();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (event.type === "pointerup" && !inOverlay) {
|
|
525
|
+
pointerIsDown = false;
|
|
526
|
+
if (mode !== "comment") {
|
|
527
|
+
scheduleSelectionPrompt();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (mode === "comment" && !inOverlay) {
|
|
531
|
+
event.stopImmediatePropagation();
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
const handleMouseUp = (event) => {
|
|
535
|
+
const target = getEventElement(event);
|
|
536
|
+
if (mode !== "comment" || isInOverlay(target)) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
event.stopImmediatePropagation();
|
|
540
|
+
captureTextPick(event);
|
|
541
|
+
};
|
|
542
|
+
const getPinAt = (x, y) => {
|
|
543
|
+
let hit = null;
|
|
544
|
+
for (const pin of pins) {
|
|
545
|
+
const resolution = resolutions.get(pin.threadId);
|
|
546
|
+
if (!resolution || pin.status === "resolved" || !isHighlightShown(pin.threadId) || !isResolutionVisible(resolution)) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
for (const rect of getResolutionRects(resolution)) {
|
|
550
|
+
const area = rect.width * rect.height;
|
|
551
|
+
const isInside = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
|
552
|
+
if (isInside && (!hit || area < hit.area)) {
|
|
553
|
+
hit = { threadId: pin.threadId, area };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return hit?.threadId ?? null;
|
|
558
|
+
};
|
|
559
|
+
const handleClick = (event) => {
|
|
560
|
+
if (capturedTextSelection && Date.now() <= suppressClickUntil) {
|
|
561
|
+
event.preventDefault();
|
|
562
|
+
event.stopImmediatePropagation();
|
|
563
|
+
capturedTextSelection = false;
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
capturedTextSelection = false;
|
|
567
|
+
const target = getEventElement(event);
|
|
568
|
+
if (isInOverlay(target)) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const pinThreadId = mode === "browse" ? getPinAt(event.clientX, event.clientY) : null;
|
|
572
|
+
if (pinThreadId) {
|
|
573
|
+
options.onPinClicked({ threadId: pinThreadId });
|
|
574
|
+
}
|
|
575
|
+
if (mode !== "comment") {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
event.preventDefault();
|
|
579
|
+
event.stopImmediatePropagation();
|
|
580
|
+
if (window.getSelection()?.toString().trim()) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (!isPageElement(target)) {
|
|
584
|
+
options.onModeShortcut({ mode: "browse" });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
pick(target, { x: event.clientX, y: event.clientY }, reportPicked);
|
|
588
|
+
};
|
|
589
|
+
const handleKeyDown = (event) => {
|
|
590
|
+
if (event.defaultPrevented) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (isCommentShortcut(event)) {
|
|
594
|
+
options.onModeShortcut({ mode: "comment" });
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (event.key !== "Escape") {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (mode === "comment" || draftAnchor) {
|
|
601
|
+
if (mode === "comment") {
|
|
602
|
+
event.preventDefault();
|
|
603
|
+
event.stopImmediatePropagation();
|
|
604
|
+
}
|
|
605
|
+
if (draftAnchor) {
|
|
606
|
+
clearDraft();
|
|
607
|
+
options.onPickCancelled();
|
|
608
|
+
schedulePaint();
|
|
609
|
+
} else {
|
|
610
|
+
options.onModeShortcut({ mode: "browse" });
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (selectionPrompt) {
|
|
615
|
+
dismissedSelectionKey = selectionPrompt.key;
|
|
616
|
+
selectionPrompt = null;
|
|
617
|
+
schedulePaint();
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
const isCommentShortcut = (event) => {
|
|
621
|
+
if (mode === "comment" || event.repeat || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.key.toLowerCase() !== "c") {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
const target = event.target;
|
|
625
|
+
return !(target instanceof Element && target.closest(EDITABLE_SELECTOR));
|
|
626
|
+
};
|
|
627
|
+
const handlePointerCancel = () => {
|
|
628
|
+
pointerIsDown = false;
|
|
629
|
+
scheduleSelectionPrompt();
|
|
630
|
+
};
|
|
631
|
+
const handleBlur = () => {
|
|
632
|
+
pointerIsDown = false;
|
|
633
|
+
};
|
|
634
|
+
const handleViewportChange = () => {
|
|
635
|
+
schedulePaint();
|
|
636
|
+
scheduleResolve();
|
|
637
|
+
};
|
|
638
|
+
const observer = new MutationObserver((mutations) => {
|
|
639
|
+
const onlyOverlay = mutations.every((mutation) => {
|
|
640
|
+
return isInOverlay(mutation.target) || mutation.type === "childList" && mutation.removedNodes.length === 0 && Array.from(mutation.addedNodes).every(
|
|
641
|
+
(node) => node === root || isInOverlay(node)
|
|
642
|
+
);
|
|
643
|
+
});
|
|
644
|
+
if (!onlyOverlay) {
|
|
645
|
+
scheduleResolve();
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
const observeOptions = {
|
|
649
|
+
childList: true,
|
|
650
|
+
subtree: true,
|
|
651
|
+
characterData: true,
|
|
652
|
+
attributes: true
|
|
653
|
+
};
|
|
654
|
+
observer.observe(document.documentElement, observeOptions);
|
|
655
|
+
const observedShadowRoots = /* @__PURE__ */ new WeakSet();
|
|
656
|
+
function observeShadowRoots(container) {
|
|
657
|
+
for (const element of Array.from(container.querySelectorAll("*"))) {
|
|
658
|
+
const shadowRoot = element.shadowRoot;
|
|
659
|
+
if (!shadowRoot) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (!observedShadowRoots.has(shadowRoot)) {
|
|
663
|
+
observedShadowRoots.add(shadowRoot);
|
|
664
|
+
observer.observe(shadowRoot, observeOptions);
|
|
665
|
+
}
|
|
666
|
+
observeShadowRoots(shadowRoot);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
observeShadowRoots(document);
|
|
670
|
+
const listeners = new AbortController();
|
|
671
|
+
const capture = { capture: true, signal: listeners.signal };
|
|
672
|
+
const bubble = { signal: listeners.signal };
|
|
673
|
+
for (const type of ["pointerdown", "pointerup", "mousedown"]) {
|
|
674
|
+
document.addEventListener(type, handlePress, capture);
|
|
675
|
+
}
|
|
676
|
+
document.addEventListener("pointermove", handlePointerMove, capture);
|
|
677
|
+
document.addEventListener("pointerleave", handlePointerLeave, capture);
|
|
678
|
+
document.addEventListener("pointercancel", handlePointerCancel, capture);
|
|
679
|
+
document.addEventListener("mouseup", handleMouseUp, capture);
|
|
680
|
+
document.addEventListener("click", handleClick, capture);
|
|
681
|
+
document.addEventListener("keydown", handleKeyDown, capture);
|
|
682
|
+
document.addEventListener("selectionchange", scheduleSelectionPrompt, bubble);
|
|
683
|
+
window.addEventListener("scroll", handleViewportChange, capture);
|
|
684
|
+
window.addEventListener("resize", handleViewportChange, bubble);
|
|
685
|
+
window.addEventListener("blur", handleBlur, bubble);
|
|
686
|
+
const setCursor = (nextMode, previousMode) => {
|
|
687
|
+
const style = document.documentElement.style;
|
|
688
|
+
if (nextMode === "comment") {
|
|
689
|
+
originalCursor = {
|
|
690
|
+
value: style.getPropertyValue("cursor"),
|
|
691
|
+
priority: style.getPropertyPriority("cursor")
|
|
692
|
+
};
|
|
693
|
+
style.setProperty("cursor", "crosshair", "important");
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (previousMode === "comment") {
|
|
697
|
+
if (originalCursor?.value) {
|
|
698
|
+
style.setProperty(
|
|
699
|
+
"cursor",
|
|
700
|
+
originalCursor.value,
|
|
701
|
+
originalCursor.priority
|
|
702
|
+
);
|
|
703
|
+
} else {
|
|
704
|
+
style.removeProperty("cursor");
|
|
705
|
+
}
|
|
706
|
+
originalCursor = null;
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
return {
|
|
710
|
+
setMode: (nextMode) => {
|
|
711
|
+
if (nextMode === mode) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const previousMode = mode;
|
|
715
|
+
mode = nextMode;
|
|
716
|
+
selectionPrompt = null;
|
|
717
|
+
hoveredElement = null;
|
|
718
|
+
focusedThreadId = null;
|
|
719
|
+
setCursor(nextMode, previousMode);
|
|
720
|
+
if (previousMode === "comment") {
|
|
721
|
+
window.getSelection()?.removeAllRanges();
|
|
722
|
+
}
|
|
723
|
+
schedulePaint();
|
|
724
|
+
},
|
|
725
|
+
setPins: (nextPins) => {
|
|
726
|
+
pins = nextPins;
|
|
727
|
+
const threadIds = new Set(nextPins.map((pin) => pin.threadId));
|
|
728
|
+
for (const threadId of boundElements.keys()) {
|
|
729
|
+
if (!threadIds.has(threadId)) {
|
|
730
|
+
boundElements.delete(threadId);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
resolveAll();
|
|
734
|
+
},
|
|
735
|
+
focusThread: (threadId) => {
|
|
736
|
+
focusedThreadId = threadId;
|
|
737
|
+
const resolution = resolutions.get(threadId);
|
|
738
|
+
const target = resolution?.range?.startContainer.parentElement ?? resolution?.element ?? null;
|
|
739
|
+
if (resolution && isResolutionVisible(resolution)) {
|
|
740
|
+
const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
|
|
741
|
+
target?.scrollIntoView?.({
|
|
742
|
+
block: "center",
|
|
743
|
+
behavior: reduceMotion ? "auto" : "smooth"
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
schedulePaint();
|
|
747
|
+
},
|
|
748
|
+
clearDraft: () => {
|
|
749
|
+
clearDraft();
|
|
750
|
+
schedulePaint();
|
|
751
|
+
},
|
|
752
|
+
setDraftAnchor: (anchor) => {
|
|
753
|
+
draftAnchor = anchor?.type === "element" ? anchor : null;
|
|
754
|
+
resolveAll();
|
|
755
|
+
},
|
|
756
|
+
refresh: resolveAll,
|
|
757
|
+
destroy: () => {
|
|
758
|
+
isDestroyed = true;
|
|
759
|
+
observer.disconnect();
|
|
760
|
+
listeners.abort();
|
|
761
|
+
if (paintFrame !== null) {
|
|
762
|
+
window.cancelAnimationFrame(paintFrame);
|
|
763
|
+
}
|
|
764
|
+
if (resolveTimer !== null) {
|
|
765
|
+
clearTimeout(resolveTimer);
|
|
766
|
+
}
|
|
767
|
+
if (selectionTimer !== null) {
|
|
768
|
+
clearTimeout(selectionTimer);
|
|
769
|
+
}
|
|
770
|
+
setCursor("browse", mode);
|
|
771
|
+
root.remove();
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
export {
|
|
776
|
+
BADGE_SIZE_PX,
|
|
777
|
+
HIGHLIGHT_BACKGROUND,
|
|
778
|
+
OVERLAY_Z_INDEX,
|
|
779
|
+
RERESOLVE_DEBOUNCE_MS,
|
|
780
|
+
createCommentsOverlay
|
|
781
|
+
};
|