@replohq/sandbox-runtime 1.18.1 → 1.19.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/crypto.d.ts +1 -0
- package/_vendor/replo-utils/lib/crypto.mjs +1 -1
- package/_vendor/replo-utils/lib/fonts.mjs +2 -2
- package/_vendor/replo-utils/lib/math.mjs +3 -3
- package/_vendor/replo-utils/lib/misc.mjs +4 -4
- package/_vendor/replo-utils/lib/pageCapture.mjs +1 -1
- package/_vendor/schemas/errors.d.ts +0 -1
- package/_vendor/schemas/generated/pageHeatmap.d.ts +57 -0
- package/_vendor/schemas/generated/pageHeatmap.mjs +1 -0
- package/_vendor/schemas/iframeRpcChannel.mjs +6 -6
- package/_vendor/schemas/pageHeatmap.d.ts +132 -0
- package/_vendor/schemas/pageHeatmap.mjs +72 -0
- package/_vendor/schemas/previewHealthIframeRpc.mjs +6 -6
- package/_vendor/schemas/selectMode.mjs +5 -5
- package/_vendor/schemas/selectModeIframeRpc.mjs +5 -5
- package/buildMetadata.js +3 -3
- package/{chunk-KRA4LUIR.mjs → chunk-36IPO47A.mjs} +2 -2
- package/{chunk-TP4ZXK4R.mjs → chunk-QTGDSYSU.mjs} +1 -1
- package/{chunk-FLYEIRFK.mjs → chunk-SKVPFN7M.mjs} +1 -1
- package/{chunk-PH3S6LJI.mjs → chunk-THOBCJJA.mjs} +2 -2
- package/{chunk-LIPH4O4F.mjs → chunk-ZTT7SNYN.mjs} +5 -2
- package/heatmaps/HeatLayerMount.d.ts +1 -0
- package/heatmaps/HeatLayerMount.js +12 -0
- package/heatmaps/heatBadges.d.ts +25 -0
- package/heatmaps/heatBadges.js +98 -0
- package/heatmaps/heatCore.d.ts +34 -0
- package/heatmaps/heatCore.js +465 -0
- package/heatmaps/heatLayer.d.ts +1 -0
- package/heatmaps/heatLayer.js +85 -0
- package/heatmaps/heatRender.d.ts +36 -0
- package/heatmaps/heatRender.js +198 -0
- package/package.json +4 -4
- package/sandbox-runtime.js +2 -0
- /package/{chunk-TESFJQID.mjs → chunk-4ITJNSXZ.mjs} +0 -0
- /package/{chunk-G3S6ZNPF.mjs → chunk-XYS6G2ZC.mjs} +0 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { exhaustiveSwitch } from "../_vendor/replo-utils/lib/misc.mjs";
|
|
2
|
+
import { findClickedSelector, findSection, renderBadges } from "./heatBadges";
|
|
3
|
+
import {
|
|
4
|
+
drawHeatPoints,
|
|
5
|
+
drawMarkLine,
|
|
6
|
+
drawScrollBands,
|
|
7
|
+
getReachMarks,
|
|
8
|
+
getScrollReach
|
|
9
|
+
} from "./heatRender";
|
|
10
|
+
const HEAT_LAYER_HOST_ID = "replo-heat-layer";
|
|
11
|
+
const HEAT_LAYER_Z_INDEX = 2147483644;
|
|
12
|
+
const REMEASURE_DEBOUNCE_MS = 250;
|
|
13
|
+
const MAX_CANVAS_PIXELS = 16e6;
|
|
14
|
+
const CLICK_RADIUS_PX = 16;
|
|
15
|
+
const FOCUS_RING_PADDING_PX = 6;
|
|
16
|
+
const REVENUE_HEAT_OPACITY = "0.4";
|
|
17
|
+
function isNode(value) {
|
|
18
|
+
return typeof value === "object" && value !== null && "nodeType" in value;
|
|
19
|
+
}
|
|
20
|
+
function createHeatLayer({
|
|
21
|
+
document: doc,
|
|
22
|
+
onDrawn,
|
|
23
|
+
onElementClicked,
|
|
24
|
+
shouldIgnoreClick = () => false,
|
|
25
|
+
paintAsImage = false
|
|
26
|
+
}) {
|
|
27
|
+
let heatSet = null;
|
|
28
|
+
let focusSelector = null;
|
|
29
|
+
let lastDrawnReport = null;
|
|
30
|
+
let nodes = null;
|
|
31
|
+
let remeasureFrameId = null;
|
|
32
|
+
let remeasureDebounceId = null;
|
|
33
|
+
let resizeObserver = null;
|
|
34
|
+
let mutationObserver = null;
|
|
35
|
+
let observedView = null;
|
|
36
|
+
let clickView = null;
|
|
37
|
+
let badgedElements = [];
|
|
38
|
+
let imageUrl = null;
|
|
39
|
+
let imagePaint = 0;
|
|
40
|
+
const releaseImage = () => {
|
|
41
|
+
imagePaint += 1;
|
|
42
|
+
if (imageUrl) {
|
|
43
|
+
URL.revokeObjectURL(imageUrl);
|
|
44
|
+
imageUrl = null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const paintImage = ({
|
|
48
|
+
canvas,
|
|
49
|
+
image
|
|
50
|
+
}) => {
|
|
51
|
+
image.style.cssText = `${canvas.style.cssText}; max-width: none; max-height: none`;
|
|
52
|
+
const paint = ++imagePaint;
|
|
53
|
+
canvas.toBlob((blob) => {
|
|
54
|
+
if (!blob || paint !== imagePaint || !image.isConnected) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const previous = imageUrl;
|
|
58
|
+
imageUrl = URL.createObjectURL(blob);
|
|
59
|
+
image.src = imageUrl;
|
|
60
|
+
if (previous) {
|
|
61
|
+
URL.revokeObjectURL(previous);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
const ensureNodes = () => {
|
|
66
|
+
if (nodes?.host.isConnected) {
|
|
67
|
+
return nodes;
|
|
68
|
+
}
|
|
69
|
+
if (!doc.body) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const host = doc.createElement("div");
|
|
73
|
+
host.id = HEAT_LAYER_HOST_ID;
|
|
74
|
+
host.setAttribute("aria-hidden", "true");
|
|
75
|
+
host.style.cssText = [
|
|
76
|
+
"position: absolute",
|
|
77
|
+
"top: 0",
|
|
78
|
+
"left: 0",
|
|
79
|
+
"width: 0",
|
|
80
|
+
"height: 0",
|
|
81
|
+
"margin: 0",
|
|
82
|
+
"padding: 0",
|
|
83
|
+
"pointer-events: none",
|
|
84
|
+
`z-index: ${HEAT_LAYER_Z_INDEX}`
|
|
85
|
+
].join(";");
|
|
86
|
+
const canvas = doc.createElement("canvas");
|
|
87
|
+
canvas.style.cssText = [
|
|
88
|
+
"position: absolute",
|
|
89
|
+
"top: 0",
|
|
90
|
+
"left: 0",
|
|
91
|
+
"pointer-events: none"
|
|
92
|
+
].join(";");
|
|
93
|
+
const focusRing = doc.createElement("div");
|
|
94
|
+
focusRing.style.cssText = [
|
|
95
|
+
"display: none",
|
|
96
|
+
"box-sizing: border-box",
|
|
97
|
+
"border-radius: 8px",
|
|
98
|
+
"pointer-events: none",
|
|
99
|
+
"box-shadow: 0 0 0 3px #fff, 0 0 0 6px rgba(17, 17, 17, 0.85)"
|
|
100
|
+
].join(";");
|
|
101
|
+
const badges = doc.createElement("div");
|
|
102
|
+
badges.style.cssText = "position: absolute; top: 0; left: 0; pointer-events: none";
|
|
103
|
+
const image = paintAsImage ? doc.createElement("img") : null;
|
|
104
|
+
if (image) {
|
|
105
|
+
image.alt = "";
|
|
106
|
+
}
|
|
107
|
+
host.append(image ?? canvas, focusRing, badges);
|
|
108
|
+
doc.body.append(host);
|
|
109
|
+
nodes = { host, canvas, image, focusRing, badges };
|
|
110
|
+
return nodes;
|
|
111
|
+
};
|
|
112
|
+
const removeNodes = () => {
|
|
113
|
+
releaseImage();
|
|
114
|
+
nodes?.host.remove();
|
|
115
|
+
nodes = null;
|
|
116
|
+
};
|
|
117
|
+
const getPageSize = (host) => {
|
|
118
|
+
host.style.display = "none";
|
|
119
|
+
const root = doc.documentElement;
|
|
120
|
+
const width = Math.max(root.scrollWidth, doc.body.scrollWidth);
|
|
121
|
+
const height = Math.max(root.scrollHeight, doc.body.scrollHeight);
|
|
122
|
+
host.style.display = "";
|
|
123
|
+
return { width, height };
|
|
124
|
+
};
|
|
125
|
+
const hasFixedAncestor = (element) => {
|
|
126
|
+
const view = doc.defaultView;
|
|
127
|
+
let current = element;
|
|
128
|
+
while (current && current !== doc.documentElement) {
|
|
129
|
+
if (view?.getComputedStyle(current).position === "fixed") {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
current = current.parentElement;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
};
|
|
136
|
+
const findElement = (selector) => {
|
|
137
|
+
try {
|
|
138
|
+
return doc.querySelector(selector);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const getScrollOffset = () => {
|
|
144
|
+
const scrollingElement = doc.scrollingElement;
|
|
145
|
+
return {
|
|
146
|
+
left: scrollingElement?.scrollLeft ?? doc.defaultView?.scrollX ?? 0,
|
|
147
|
+
top: scrollingElement?.scrollTop ?? doc.defaultView?.scrollY ?? 0
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const getPageRect = (element) => {
|
|
151
|
+
const domRect = element.getBoundingClientRect();
|
|
152
|
+
if (domRect.width === 0 && domRect.height === 0) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const isFixed = hasFixedAncestor(element);
|
|
156
|
+
const scroll = isFixed ? { left: 0, top: 0 } : getScrollOffset();
|
|
157
|
+
return {
|
|
158
|
+
x: domRect.x + scroll.left,
|
|
159
|
+
y: domRect.y + scroll.top,
|
|
160
|
+
width: domRect.width,
|
|
161
|
+
height: domRect.height,
|
|
162
|
+
isFixed
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
const createElementLookup = () => {
|
|
166
|
+
const elements = /* @__PURE__ */ new Map();
|
|
167
|
+
return (selector) => {
|
|
168
|
+
let element = elements.get(selector);
|
|
169
|
+
if (element === void 0) {
|
|
170
|
+
element = findElement(selector);
|
|
171
|
+
elements.set(selector, element);
|
|
172
|
+
}
|
|
173
|
+
return element;
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
const getClickPoints = ({
|
|
177
|
+
set,
|
|
178
|
+
lookup
|
|
179
|
+
}) => {
|
|
180
|
+
const rectsBySelector = /* @__PURE__ */ new Map();
|
|
181
|
+
const points = [];
|
|
182
|
+
let placedWeight = 0;
|
|
183
|
+
let unplacedWeight = 0;
|
|
184
|
+
for (const click of set.clicks) {
|
|
185
|
+
let rect = rectsBySelector.get(click.selector);
|
|
186
|
+
if (rect === void 0) {
|
|
187
|
+
const element = lookup(click.selector);
|
|
188
|
+
rect = element ? getPageRect(element) : null;
|
|
189
|
+
rectsBySelector.set(click.selector, rect);
|
|
190
|
+
}
|
|
191
|
+
if (!rect) {
|
|
192
|
+
unplacedWeight += click.weight;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
placedWeight += click.weight;
|
|
196
|
+
points.push({
|
|
197
|
+
x: rect.x + click.rx * rect.width,
|
|
198
|
+
y: rect.y + click.ry * rect.height,
|
|
199
|
+
weight: click.weight
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return { points, placedWeight, unplacedWeight };
|
|
203
|
+
};
|
|
204
|
+
const drawSet = ({
|
|
205
|
+
set,
|
|
206
|
+
canvas,
|
|
207
|
+
page,
|
|
208
|
+
lookup
|
|
209
|
+
}) => {
|
|
210
|
+
const resolution = Math.min(
|
|
211
|
+
1,
|
|
212
|
+
Math.sqrt(MAX_CANVAS_PIXELS / Math.max(1, page.width * page.height))
|
|
213
|
+
);
|
|
214
|
+
canvas.width = Math.max(1, Math.round(page.width * resolution));
|
|
215
|
+
canvas.height = Math.max(1, Math.round(page.height * resolution));
|
|
216
|
+
canvas.style.width = `${page.width}px`;
|
|
217
|
+
canvas.style.height = `${page.height}px`;
|
|
218
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
219
|
+
const toCanvas = (point) => ({
|
|
220
|
+
x: point.x * resolution,
|
|
221
|
+
y: point.y * resolution,
|
|
222
|
+
weight: point.weight
|
|
223
|
+
});
|
|
224
|
+
canvas.style.opacity = set.mode === "revenue" ? REVENUE_HEAT_OPACITY : "1";
|
|
225
|
+
const drawClicks = () => {
|
|
226
|
+
const resolved = getClickPoints({ set, lookup });
|
|
227
|
+
if (context) {
|
|
228
|
+
drawHeatPoints({
|
|
229
|
+
context,
|
|
230
|
+
points: resolved.points.map(toCanvas),
|
|
231
|
+
radius: CLICK_RADIUS_PX * resolution
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
placedWeight: resolved.placedWeight,
|
|
236
|
+
unplacedWeight: resolved.unplacedWeight
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
const result = exhaustiveSwitch({ type: set.mode })({
|
|
240
|
+
revenue: drawClicks,
|
|
241
|
+
clicks: drawClicks,
|
|
242
|
+
scroll: () => {
|
|
243
|
+
if (context) {
|
|
244
|
+
const reach = getScrollReach(set.scroll);
|
|
245
|
+
drawScrollBands({ context, reach });
|
|
246
|
+
for (const mark of getReachMarks({ reach, height: canvas.height })) {
|
|
247
|
+
drawMarkLine({
|
|
248
|
+
context,
|
|
249
|
+
y: mark.y,
|
|
250
|
+
label: `${Math.round(mark.share * 100)}% of visitors reach here`,
|
|
251
|
+
dashed: true,
|
|
252
|
+
scale: resolution
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return { placedWeight: 0, unplacedWeight: 0 };
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
if (context && set.fold !== null && set.fold < page.height) {
|
|
260
|
+
drawMarkLine({
|
|
261
|
+
context,
|
|
262
|
+
y: set.fold * resolution,
|
|
263
|
+
label: "End of the first screen",
|
|
264
|
+
dashed: false,
|
|
265
|
+
scale: resolution
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
};
|
|
270
|
+
const positionFocusRing = (focusRing) => {
|
|
271
|
+
const element = focusSelector ? findElement(focusSelector) : null;
|
|
272
|
+
const rect = element ? getPageRect(element) : null;
|
|
273
|
+
if (!rect) {
|
|
274
|
+
focusRing.style.display = "none";
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
focusRing.style.display = "block";
|
|
278
|
+
focusRing.style.position = rect.isFixed ? "fixed" : "absolute";
|
|
279
|
+
focusRing.style.left = `${rect.x - FOCUS_RING_PADDING_PX}px`;
|
|
280
|
+
focusRing.style.top = `${rect.y - FOCUS_RING_PADDING_PX}px`;
|
|
281
|
+
focusRing.style.width = `${rect.width + FOCUS_RING_PADDING_PX * 2}px`;
|
|
282
|
+
focusRing.style.height = `${rect.height + FOCUS_RING_PADDING_PX * 2}px`;
|
|
283
|
+
return element;
|
|
284
|
+
};
|
|
285
|
+
const drawBadges = ({
|
|
286
|
+
set,
|
|
287
|
+
container,
|
|
288
|
+
lookup
|
|
289
|
+
}) => {
|
|
290
|
+
if (set.mode !== "revenue") {
|
|
291
|
+
badgedElements = [];
|
|
292
|
+
container.replaceChildren();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
badgedElements = set.badges.flatMap((badge) => {
|
|
296
|
+
const element = lookup(badge.selector);
|
|
297
|
+
return element ? [{ selector: badge.selector, element }] : [];
|
|
298
|
+
});
|
|
299
|
+
renderBadges({
|
|
300
|
+
container,
|
|
301
|
+
badges: set.badges,
|
|
302
|
+
rectFor: (selector) => {
|
|
303
|
+
const element = lookup(selector);
|
|
304
|
+
return element ? getPageRect(element) : null;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
const findSections = ({
|
|
309
|
+
set,
|
|
310
|
+
lookup
|
|
311
|
+
}) => {
|
|
312
|
+
const selectors = /* @__PURE__ */ new Set([
|
|
313
|
+
...set.clicks.map((click) => click.selector),
|
|
314
|
+
...set.badges.map((badge) => badge.selector)
|
|
315
|
+
]);
|
|
316
|
+
return [...selectors].flatMap((selector) => {
|
|
317
|
+
const element = lookup(selector);
|
|
318
|
+
const section = element ? findSection(element) : null;
|
|
319
|
+
return section ? [{ selector, section }] : [];
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
const handleRevenueClick = (event) => {
|
|
323
|
+
const target = event.target;
|
|
324
|
+
if (!isNode(target) || shouldIgnoreClick(target)) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
event.preventDefault();
|
|
328
|
+
event.stopPropagation();
|
|
329
|
+
const selector = findClickedSelector({ target, badged: badgedElements });
|
|
330
|
+
focusSelector = selector;
|
|
331
|
+
if (nodes) {
|
|
332
|
+
positionFocusRing(nodes.focusRing);
|
|
333
|
+
}
|
|
334
|
+
onElementClicked(selector);
|
|
335
|
+
};
|
|
336
|
+
const syncClickInterception = () => {
|
|
337
|
+
const view = heatSet?.mode === "revenue" ? doc.defaultView : null;
|
|
338
|
+
if (view === clickView) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
clickView?.removeEventListener("click", handleRevenueClick, true);
|
|
342
|
+
view?.addEventListener("click", handleRevenueClick, true);
|
|
343
|
+
clickView = view;
|
|
344
|
+
};
|
|
345
|
+
const scheduleRemeasure = () => {
|
|
346
|
+
remeasureFrameId ??= requestAnimationFrame(() => {
|
|
347
|
+
remeasureFrameId = null;
|
|
348
|
+
render();
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
const scheduleDebouncedRemeasure = () => {
|
|
352
|
+
if (remeasureDebounceId !== null) {
|
|
353
|
+
clearTimeout(remeasureDebounceId);
|
|
354
|
+
}
|
|
355
|
+
remeasureDebounceId = setTimeout(scheduleRemeasure, REMEASURE_DEBOUNCE_MS);
|
|
356
|
+
};
|
|
357
|
+
const stopObserving = () => {
|
|
358
|
+
observedView?.removeEventListener("resize", scheduleDebouncedRemeasure);
|
|
359
|
+
observedView = null;
|
|
360
|
+
resizeObserver?.disconnect();
|
|
361
|
+
resizeObserver = null;
|
|
362
|
+
mutationObserver?.disconnect();
|
|
363
|
+
mutationObserver = null;
|
|
364
|
+
if (remeasureDebounceId !== null) {
|
|
365
|
+
clearTimeout(remeasureDebounceId);
|
|
366
|
+
remeasureDebounceId = null;
|
|
367
|
+
}
|
|
368
|
+
if (remeasureFrameId !== null) {
|
|
369
|
+
cancelAnimationFrame(remeasureFrameId);
|
|
370
|
+
remeasureFrameId = null;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const syncObservers = () => {
|
|
374
|
+
const shouldObserve = heatSet !== null;
|
|
375
|
+
if (shouldObserve === (mutationObserver !== null)) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!shouldObserve) {
|
|
379
|
+
stopObserving();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
observedView = doc.defaultView;
|
|
383
|
+
observedView?.addEventListener("resize", scheduleDebouncedRemeasure);
|
|
384
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
385
|
+
resizeObserver = new ResizeObserver(scheduleDebouncedRemeasure);
|
|
386
|
+
resizeObserver.observe(doc.body);
|
|
387
|
+
}
|
|
388
|
+
mutationObserver = new MutationObserver((records) => {
|
|
389
|
+
const host = nodes?.host;
|
|
390
|
+
const hasForeignMutation = records.some((record) => {
|
|
391
|
+
return !host || !host.contains(record.target);
|
|
392
|
+
});
|
|
393
|
+
if (hasForeignMutation) {
|
|
394
|
+
scheduleDebouncedRemeasure();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
mutationObserver.observe(doc.body, { childList: true, subtree: true });
|
|
398
|
+
};
|
|
399
|
+
function render() {
|
|
400
|
+
if (!heatSet) {
|
|
401
|
+
removeNodes();
|
|
402
|
+
badgedElements = [];
|
|
403
|
+
syncObservers();
|
|
404
|
+
syncClickInterception();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const layer = ensureNodes();
|
|
408
|
+
if (!layer) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const lookup = createElementLookup();
|
|
412
|
+
const page = getPageSize(layer.host);
|
|
413
|
+
const weights = drawSet({
|
|
414
|
+
set: heatSet,
|
|
415
|
+
canvas: layer.canvas,
|
|
416
|
+
page,
|
|
417
|
+
lookup
|
|
418
|
+
});
|
|
419
|
+
if (layer.image) {
|
|
420
|
+
paintImage({ canvas: layer.canvas, image: layer.image });
|
|
421
|
+
}
|
|
422
|
+
drawBadges({ set: heatSet, container: layer.badges, lookup });
|
|
423
|
+
positionFocusRing(layer.focusRing);
|
|
424
|
+
const sections = findSections({ set: heatSet, lookup });
|
|
425
|
+
const report = JSON.stringify([heatSet.setId, weights, sections]);
|
|
426
|
+
if (report !== lastDrawnReport) {
|
|
427
|
+
lastDrawnReport = report;
|
|
428
|
+
onDrawn({ setId: heatSet.setId, ...weights, sections });
|
|
429
|
+
}
|
|
430
|
+
syncObservers();
|
|
431
|
+
syncClickInterception();
|
|
432
|
+
}
|
|
433
|
+
const show = (set) => {
|
|
434
|
+
heatSet = set;
|
|
435
|
+
if (!set) {
|
|
436
|
+
focusSelector = null;
|
|
437
|
+
}
|
|
438
|
+
lastDrawnReport = null;
|
|
439
|
+
render();
|
|
440
|
+
};
|
|
441
|
+
return {
|
|
442
|
+
show,
|
|
443
|
+
focus(selector) {
|
|
444
|
+
focusSelector = selector;
|
|
445
|
+
if (!heatSet || !nodes) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const element = positionFocusRing(nodes.focusRing);
|
|
449
|
+
if (!element) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const viewportRect = element.getBoundingClientRect();
|
|
453
|
+
const viewportHeight = doc.defaultView?.innerHeight ?? 0;
|
|
454
|
+
const isInView = viewportRect.top >= 0 && viewportRect.bottom <= viewportHeight;
|
|
455
|
+
if (!isInView) {
|
|
456
|
+
element.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
destroy: () => show(null)
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
export {
|
|
463
|
+
HEAT_LAYER_HOST_ID,
|
|
464
|
+
createHeatLayer
|
|
465
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initHeatLayer(): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { exhaustiveSwitch } from "../_vendor/replo-utils/lib/misc.mjs";
|
|
2
|
+
import {
|
|
3
|
+
PAGE_HEATMAP_PROTOCOL_VERSION,
|
|
4
|
+
pageHeatmapParentMessageSchema
|
|
5
|
+
} from "../_vendor/schemas/pageHeatmap.mjs";
|
|
6
|
+
import {
|
|
7
|
+
isAllowedEditorMessage,
|
|
8
|
+
isAllowedEditorOrigin
|
|
9
|
+
} from "../annotations/bridgeGate";
|
|
10
|
+
import { ANNOTATION_CONTAINER_ID } from "../select-mode/annotationFrames";
|
|
11
|
+
import { displayModeStore } from "../select-mode/displayModeStore";
|
|
12
|
+
import { createHeatLayer } from "./heatCore";
|
|
13
|
+
let isInitialized = false;
|
|
14
|
+
let editorOrigin = null;
|
|
15
|
+
let layer = null;
|
|
16
|
+
function postToEditor(message) {
|
|
17
|
+
if (!editorOrigin) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
window.parent.postMessage(message, editorOrigin);
|
|
21
|
+
}
|
|
22
|
+
function getLayer() {
|
|
23
|
+
layer ??= createHeatLayer({
|
|
24
|
+
document,
|
|
25
|
+
onDrawn: (drawn) => {
|
|
26
|
+
postToEditor({ type: "replo:heatmap:drawn", ...drawn });
|
|
27
|
+
},
|
|
28
|
+
onElementClicked: (selector) => {
|
|
29
|
+
postToEditor({ type: "replo:heatmap:elementClicked", selector });
|
|
30
|
+
},
|
|
31
|
+
shouldIgnoreClick: (target) => {
|
|
32
|
+
return displayModeStore.getSnapshot() === "selection" || Boolean(
|
|
33
|
+
document.getElementById(ANNOTATION_CONTAINER_ID)?.contains(target)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return layer;
|
|
38
|
+
}
|
|
39
|
+
function handleMessage(event) {
|
|
40
|
+
if (!isAllowedEditorMessage(event)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const parsed = pageHeatmapParentMessageSchema.safeParse(event.data);
|
|
44
|
+
if (!parsed.success) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
editorOrigin = event.origin;
|
|
48
|
+
exhaustiveSwitch(parsed.data)({
|
|
49
|
+
"replo:heatmap:set": (message) => {
|
|
50
|
+
getLayer().show(message);
|
|
51
|
+
},
|
|
52
|
+
"replo:heatmap:focus": (message) => {
|
|
53
|
+
getLayer().focus(message.selector);
|
|
54
|
+
},
|
|
55
|
+
"replo:heatmap:clear": () => {
|
|
56
|
+
getLayer().show(null);
|
|
57
|
+
},
|
|
58
|
+
"replo:heatmap:requestReady": () => {
|
|
59
|
+
postToEditor({
|
|
60
|
+
type: "replo:heatmap:ready",
|
|
61
|
+
protocolVersion: PAGE_HEATMAP_PROTOCOL_VERSION
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function initHeatLayer() {
|
|
67
|
+
if (isInitialized || window.self === window.top) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
isInitialized = true;
|
|
71
|
+
window.addEventListener("message", handleMessage);
|
|
72
|
+
const referrerOrigin = URL.canParse(document.referrer) ? new URL(document.referrer).origin : null;
|
|
73
|
+
if (referrerOrigin && isAllowedEditorOrigin(referrerOrigin)) {
|
|
74
|
+
window.parent.postMessage(
|
|
75
|
+
{
|
|
76
|
+
type: "replo:heatmap:ready",
|
|
77
|
+
protocolVersion: PAGE_HEATMAP_PROTOCOL_VERSION
|
|
78
|
+
},
|
|
79
|
+
referrerOrigin
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
initHeatLayer
|
|
85
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Heat drawing: alpha-summed discs recoloured through a thermal palette, or scroll-reach bands. */
|
|
2
|
+
export interface HeatPoint {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
weight: number;
|
|
6
|
+
}
|
|
7
|
+
/** Points are in canvas pixels; `radius` too. */
|
|
8
|
+
export declare function drawHeatPoints({ context, points, radius, }: {
|
|
9
|
+
context: CanvasRenderingContext2D;
|
|
10
|
+
points: HeatPoint[];
|
|
11
|
+
radius: number;
|
|
12
|
+
}): void;
|
|
13
|
+
/** `reach[p]` is the share of page views that saw p% of the page, from each view's deepest point. */
|
|
14
|
+
export declare function getScrollReach(buckets: {
|
|
15
|
+
pct: number;
|
|
16
|
+
views: number;
|
|
17
|
+
}[]): number[];
|
|
18
|
+
export declare function drawScrollBands({ context, reach, }: {
|
|
19
|
+
context: CanvasRenderingContext2D;
|
|
20
|
+
reach: number[];
|
|
21
|
+
}): void;
|
|
22
|
+
/** Page y (canvas px) where reach first drops below each mark. */
|
|
23
|
+
export declare function getReachMarks({ reach, height, }: {
|
|
24
|
+
reach: number[];
|
|
25
|
+
height: number;
|
|
26
|
+
}): {
|
|
27
|
+
share: number;
|
|
28
|
+
y: number;
|
|
29
|
+
}[];
|
|
30
|
+
export declare function drawMarkLine({ context, y, label, dashed, scale, }: {
|
|
31
|
+
context: CanvasRenderingContext2D;
|
|
32
|
+
y: number;
|
|
33
|
+
label: string;
|
|
34
|
+
dashed: boolean;
|
|
35
|
+
scale: number;
|
|
36
|
+
}): void;
|