@gem-sdk/clarity-visualize 0.8.94

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -0
  3. package/build/clarity.visualize.js +2753 -0
  4. package/build/clarity.visualize.min.js +1 -0
  5. package/build/clarity.visualize.module.js +2748 -0
  6. package/package.json +45 -0
  7. package/rollup.config.ts +79 -0
  8. package/src/clarity.ts +3 -0
  9. package/src/custom/README.md +87 -0
  10. package/src/custom/canvas-layer.ts +355 -0
  11. package/src/custom/dialog.ts +242 -0
  12. package/src/custom/index.ts +7 -0
  13. package/src/data.ts +103 -0
  14. package/src/enrich.ts +80 -0
  15. package/src/global.ts +9 -0
  16. package/src/heatmap.ts +512 -0
  17. package/src/index.ts +4 -0
  18. package/src/interaction.ts +524 -0
  19. package/src/layout.ts +805 -0
  20. package/src/styles/blobUnavailable/chineseSimplified.svg +5 -0
  21. package/src/styles/blobUnavailable/chineseTraditional.svg +5 -0
  22. package/src/styles/blobUnavailable/dutch.svg +5 -0
  23. package/src/styles/blobUnavailable/english.svg +5 -0
  24. package/src/styles/blobUnavailable/french.svg +5 -0
  25. package/src/styles/blobUnavailable/german.svg +5 -0
  26. package/src/styles/blobUnavailable/iconOnly.svg +4 -0
  27. package/src/styles/blobUnavailable/italian.svg +5 -0
  28. package/src/styles/blobUnavailable/japanese.svg +5 -0
  29. package/src/styles/blobUnavailable/korean.svg +5 -0
  30. package/src/styles/blobUnavailable/portuguese.svg +5 -0
  31. package/src/styles/blobUnavailable/russian.svg +5 -0
  32. package/src/styles/blobUnavailable/spanish.svg +5 -0
  33. package/src/styles/blobUnavailable/turkish.svg +5 -0
  34. package/src/styles/iframeUnavailable/chineseSimplified.svg +5 -0
  35. package/src/styles/iframeUnavailable/chineseTraditional.svg +5 -0
  36. package/src/styles/iframeUnavailable/dutch.svg +5 -0
  37. package/src/styles/iframeUnavailable/english.svg +5 -0
  38. package/src/styles/iframeUnavailable/french.svg +5 -0
  39. package/src/styles/iframeUnavailable/german.svg +5 -0
  40. package/src/styles/iframeUnavailable/iconOnly.svg +4 -0
  41. package/src/styles/iframeUnavailable/italian.svg +5 -0
  42. package/src/styles/iframeUnavailable/japanese.svg +5 -0
  43. package/src/styles/iframeUnavailable/korean.svg +5 -0
  44. package/src/styles/iframeUnavailable/portuguese.svg +5 -0
  45. package/src/styles/iframeUnavailable/russian.svg +5 -0
  46. package/src/styles/iframeUnavailable/spanish.svg +5 -0
  47. package/src/styles/iframeUnavailable/turkish.svg +5 -0
  48. package/src/styles/imageMasked/chineseSimplified.svg +5 -0
  49. package/src/styles/imageMasked/chineseTraditional.svg +5 -0
  50. package/src/styles/imageMasked/dutch.svg +5 -0
  51. package/src/styles/imageMasked/english.svg +5 -0
  52. package/src/styles/imageMasked/french.svg +5 -0
  53. package/src/styles/imageMasked/german.svg +5 -0
  54. package/src/styles/imageMasked/iconOnly.svg +4 -0
  55. package/src/styles/imageMasked/italian.svg +5 -0
  56. package/src/styles/imageMasked/japanese.svg +5 -0
  57. package/src/styles/imageMasked/korean.svg +5 -0
  58. package/src/styles/imageMasked/portuguese.svg +5 -0
  59. package/src/styles/imageMasked/russian.svg +5 -0
  60. package/src/styles/imageMasked/spanish.svg +5 -0
  61. package/src/styles/imageMasked/turkish.svg +5 -0
  62. package/src/styles/pointer/click.css +31 -0
  63. package/src/styles/pointer/pointerIcon.svg +18 -0
  64. package/src/styles/shared.css +6 -0
  65. package/src/visualizer.ts +308 -0
  66. package/tsconfig.json +13 -0
  67. package/tslint.json +33 -0
  68. package/types/heatmap.d.ts +18 -0
  69. package/types/index.d.ts +11 -0
  70. package/types/layout.d.ts +12 -0
  71. package/types/string-import.d.ts +9 -0
  72. package/types/visualize.d.ts +253 -0
package/src/heatmap.ts ADDED
@@ -0,0 +1,512 @@
1
+ import { Activity, Constant, Heatmap, Setting, ScrollMapInfo, PlaybackState } from "@clarity-types/visualize";
2
+ import { Data } from "@gem-sdk/clarity-js";
3
+ import { LayoutHelper } from "./layout";
4
+
5
+ import * as canvasLayer from "./custom/canvas-layer";
6
+ import * as dialogCustom from "./custom/dialog";
7
+
8
+ export class HeatmapHelper {
9
+ static COLORS = ["blue", "cyan", "lime", "yellow", "red"];
10
+ data: Activity = null;
11
+ scrollData: ScrollMapInfo[] = null;
12
+ max: number = null;
13
+ offscreenRing: HTMLCanvasElement = null;
14
+ gradientPixels: ImageData = null;
15
+ timeout = null;
16
+ observer: ResizeObserver = null;
17
+ state: PlaybackState = null;
18
+ layout: LayoutHelper = null;
19
+ scrollAvgFold: number = null;
20
+ addScrollMakers: boolean = false;
21
+ parentCanvasInfo: canvasLayer.ParentCanvasInfo | null = null;
22
+
23
+ constructor(state: PlaybackState, layout: LayoutHelper) {
24
+ this.state = state;
25
+ this.layout = layout;
26
+ }
27
+
28
+ public reset = (): void => {
29
+ this.data = null;
30
+ this.scrollData = null;
31
+ this.max = null;
32
+ this.offscreenRing = null;
33
+ this.gradientPixels = null;
34
+ this.timeout = null;
35
+
36
+ // Cleanup parent canvas if it exists
37
+ if (this.parentCanvasInfo) {
38
+ this.parentCanvasInfo.cleanup();
39
+ this.parentCanvasInfo = null;
40
+ }
41
+
42
+ // Reset resize observer
43
+ if (this.observer) {
44
+ this.observer.disconnect();
45
+ this.observer = null;
46
+ }
47
+
48
+ // Remove scroll, resize, and animation event listeners
49
+ if (this.state && this.state.window) {
50
+ let win = this.state.window;
51
+ win.removeEventListener("scroll", this.redraw, { capture: true });
52
+ win.removeEventListener("resize", this.redraw, { capture: true });
53
+ win.removeEventListener("animationend", this.redraw, true);
54
+ win.removeEventListener("transitionend", this.redraw, true);
55
+ }
56
+ }
57
+
58
+ public clear = (): void => {
59
+ let doc = this.state.window.document;
60
+ let win = this.state.window;
61
+ let canvas = doc.getElementById(Constant.HeatmapCanvas) as HTMLCanvasElement;
62
+ let de = doc.documentElement;
63
+ if (canvas) {
64
+ canvas.width = de.clientWidth;
65
+ canvas.height = de.clientHeight;
66
+ canvas.style.left = win.pageXOffset + Constant.Pixel;
67
+ canvas.style.top = win.pageYOffset + Constant.Pixel;
68
+ canvas.getContext(Constant.Context).clearRect(0, 0, canvas.width, canvas.height);
69
+ }
70
+
71
+ // Cleanup parent canvas before reset
72
+ if (this.parentCanvasInfo) {
73
+ this.parentCanvasInfo.cleanup();
74
+ this.parentCanvasInfo = null;
75
+ }
76
+
77
+ this.reset();
78
+ }
79
+
80
+ public scroll = async (activity: ScrollMapInfo[], avgFold: number, addMarkers: boolean): Promise<void> => {
81
+ await this.waitForDialogs();
82
+ await this.waitForAllAnimations();
83
+
84
+ this.scrollData = this.scrollData || activity;
85
+ this.scrollAvgFold = avgFold != null ? avgFold : this.scrollAvgFold;
86
+ this.addScrollMakers = addMarkers != null ? addMarkers : this.addScrollMakers;
87
+
88
+ let canvas = this.overlay();
89
+ let context = canvas.getContext(Constant.Context);
90
+ let doc = this.state.window.document;
91
+ var body = doc.body;
92
+ var de = doc.documentElement;
93
+ var height = Math.max(body.scrollHeight, body.offsetHeight,
94
+ de.clientHeight, de.scrollHeight, de.offsetHeight);
95
+ canvas.height = Math.min(height, Setting.ScrollCanvasMaxHeight);
96
+ canvas.style.top = 0 + Constant.Pixel;
97
+ if (canvas.width > 0 && canvas.height > 0) {
98
+ if (this.scrollData) {
99
+ const grd = context.createLinearGradient(0, 0, 0, canvas.height);
100
+ for (const currentCombination of this.scrollData) {
101
+ const huePercentView = 1 - (currentCombination.cumulativeSum / this.scrollData[0].cumulativeSum);
102
+ const percentView = (currentCombination.scrollReachY / 100) * (height / canvas.height);
103
+ const hue = huePercentView * Setting.MaxHue;
104
+ if (percentView <= 1) {
105
+ if (!isNaN(hue)) {
106
+ grd.addColorStop(percentView, `hsla(${hue}, 100%, 50%, 0.6)`);
107
+ }
108
+ }
109
+ }
110
+
111
+ // Fill with gradient
112
+ context.fillStyle = grd;
113
+ context.fillRect(0, 0, canvas.width, canvas.height);
114
+ if (this.addScrollMakers) {
115
+ this.addInfoMarkers(context, this.scrollData, canvas.width, canvas.height, this.scrollAvgFold);
116
+ }
117
+ }
118
+ };
119
+ }
120
+
121
+ private addInfoMarkers = (context: CanvasRenderingContext2D, scrollMapInfo: ScrollMapInfo[], width: number, height: number, avgFold: number): void => {
122
+ this.addMarker(context, width, Constant.AverageFold, avgFold, Setting.MarkerMediumWidth);
123
+ const markers = [75, 50, 25];
124
+ for (const marker of markers) {
125
+ const closest = scrollMapInfo.reduce((prev: ScrollMapInfo, curr: ScrollMapInfo): ScrollMapInfo => {
126
+ return ((Math.abs(curr.percUsers - marker)) < (Math.abs(prev.percUsers - marker)) ? curr : prev);
127
+ });
128
+ if (closest.percUsers >= marker - Setting.MarkerRange && closest.percUsers <= marker + Setting.MarkerRange) {
129
+ const markerLine = (closest.scrollReachY / 100) * height;
130
+ this.addMarker(context, width, `${marker}%`, markerLine, Setting.MarkerSmallWidth);
131
+ }
132
+ }
133
+ }
134
+
135
+ private addMarker = (context: CanvasRenderingContext2D, heatmapWidth: number, label: string, markerY: number, markerWidth: number): void => {
136
+ context.beginPath();
137
+ context.moveTo(0, markerY);
138
+ context.lineTo(heatmapWidth, markerY);
139
+ context.setLineDash([2, 2]);
140
+ context.lineWidth = Setting.MarkerLineHeight;
141
+ context.strokeStyle = Setting.MarkerColor;
142
+ context.stroke();
143
+ context.fillStyle = Setting.CanvasTextColor;
144
+ context.fillRect(0, (markerY - Setting.MarkerHeight / 2), markerWidth, Setting.MarkerHeight);
145
+ context.fillStyle = Setting.MarkerColor;
146
+ context.font = Setting.CanvasTextFont;
147
+ context.fillText(label, Setting.MarkerPadding, markerY + Setting.MarkerPadding);
148
+ }
149
+
150
+ public click = async (activity: Activity): Promise<void> => {
151
+ await this.waitForDialogs();
152
+ await this.waitForAllAnimations();
153
+
154
+ this.data = this.data || activity;
155
+ let heat = this.transform();
156
+ let canvas = this.overlay();
157
+ let ctx = canvas.getContext(Constant.Context);
158
+
159
+ if (canvas.width > 0 && canvas.height > 0) {
160
+ // TODO: GEMX DEBUG Draw a test rectangle to verify canvas is working
161
+ // ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
162
+ // ctx.fillRect(10, 10, 100, 100);
163
+
164
+ // To speed up canvas rendering, we draw ring & gradient on an offscreen canvas, so we can use drawImage API
165
+ // Canvas performance tips: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
166
+ // Pre-render similar primitives or repeating objects on an offscreen canvas
167
+ let ring = this.getRing();
168
+ let gradient = this.getGradient();
169
+
170
+ // Render activity for each (x,y) coordinate in our data
171
+ for (let entry of heat) {
172
+ ctx.globalAlpha = entry.a;
173
+ ctx.drawImage(ring, entry.x - Setting.Radius, entry.y - Setting.Radius);
174
+ }
175
+
176
+ // Add color to the canvas based on alpha value of each pixel
177
+ let pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
178
+ for (let i = 0; i < pixels.data.length; i += 4) {
179
+ // For each pixel, we have 4 entries in data array: (r,g,b,a)
180
+ // To pick the right color from gradient pixels, we look at the alpha value of the pixel
181
+ // Alpha value ranges from 0-255
182
+ let alpha = pixels.data[i + 3];
183
+ if (alpha > 0) {
184
+ let offset = (alpha - 1) * 4;
185
+ pixels.data[i] = gradient.data[offset];
186
+ pixels.data[i + 1] = gradient.data[offset + 1];
187
+ pixels.data[i + 2] = gradient.data[offset + 2];
188
+ }
189
+ }
190
+ ctx.putImageData(pixels, 0, 0);
191
+ }
192
+ }
193
+
194
+ public overlay = (): HTMLCanvasElement => {
195
+ // Create canvas for visualizing heatmap
196
+ let doc = this.state.window.document;
197
+ let win = this.state.window;
198
+ let de = doc.documentElement;
199
+
200
+ const isPortalCanvas = !!this.state.options.portalCanvasId;
201
+ if (isPortalCanvas) return this.createPortalCanvas(doc, win, de);
202
+
203
+ let canvas = doc.getElementById(Constant.HeatmapCanvas) as HTMLCanvasElement;
204
+ if (canvas === null) {
205
+ canvas = doc.createElement(Constant.Canvas) as HTMLCanvasElement;
206
+ canvas.id = Constant.HeatmapCanvas;
207
+ canvas.classList.add(Constant.HeatmapCanvas);
208
+ canvas.width = 0;
209
+ canvas.height = 0;
210
+ canvas.style.position = Constant.Absolute;
211
+ canvas.style.zIndex = `${Setting.ZIndex}`;
212
+ de.appendChild(canvas);
213
+ win.addEventListener("scroll", this.redraw, { passive: true, capture: true });
214
+ win.addEventListener("resize", this.redraw, { passive: true, capture: true });
215
+ win.addEventListener("animationend", this.redraw, true);
216
+ win.addEventListener("transitionend", this.redraw, true);
217
+ this.observer = this.state.window["ResizeObserver"] ? new ResizeObserver(this.redraw) : null;
218
+
219
+ if (this.observer) { this.observer.observe(doc.body); }
220
+ }
221
+
222
+ // Ensure canvas is positioned correctly
223
+ canvas.width = de.clientWidth;
224
+ canvas.height = de.clientHeight;
225
+ canvas.style.left = win.pageXOffset + Constant.Pixel;
226
+ canvas.style.top = win.pageYOffset + Constant.Pixel;
227
+ canvas.getContext(Constant.Context).clearRect(0, 0, canvas.width, canvas.height);
228
+
229
+ return canvas;
230
+ }
231
+
232
+ private getRing = (): HTMLCanvasElement => {
233
+ if (this.offscreenRing === null) {
234
+ let doc = this.state.window.document;
235
+ this.offscreenRing = doc.createElement(Constant.Canvas) as HTMLCanvasElement;
236
+ this.offscreenRing.width = Setting.Radius * 2;
237
+ this.offscreenRing.height = Setting.Radius * 2;
238
+ let ctx = this.offscreenRing.getContext(Constant.Context);
239
+ ctx.shadowOffsetX = Setting.Radius * 2;
240
+ ctx.shadowBlur = Setting.Radius / 2;
241
+ ctx.shadowColor = Constant.Black;
242
+ ctx.beginPath();
243
+ ctx.arc(-Setting.Radius, Setting.Radius, Setting.Radius / 2, 0, Math.PI * 2, true);
244
+ ctx.closePath();
245
+ ctx.fill();
246
+ }
247
+ return this.offscreenRing;
248
+ }
249
+
250
+ private getGradient = (): ImageData => {
251
+ if (this.gradientPixels === null) {
252
+ let doc = this.state.window.document;
253
+ let offscreenGradient = doc.createElement(Constant.Canvas) as HTMLCanvasElement;
254
+ offscreenGradient.width = 1;
255
+ offscreenGradient.height = Setting.Colors;
256
+ let ctx = offscreenGradient.getContext(Constant.Context);
257
+ let gradient = ctx.createLinearGradient(0, 0, 0, Setting.Colors);
258
+ let step = 1 / HeatmapHelper.COLORS.length;
259
+ for (let i = 0; i < HeatmapHelper.COLORS.length; i++) {
260
+ gradient.addColorStop(step * (i + 1), HeatmapHelper.COLORS[i]);
261
+ }
262
+ ctx.fillStyle = gradient;
263
+ ctx.fillRect(0, 0, 1, Setting.Colors);
264
+ this.gradientPixels = ctx.getImageData(0, 0, 1, Setting.Colors);
265
+ }
266
+ return this.gradientPixels;
267
+ }
268
+
269
+ private redraw = (event): void => {
270
+ if (this.data) {
271
+ if (this.timeout) { clearTimeout(this.timeout); }
272
+ this.timeout = setTimeout(this.click, Setting.Interval);
273
+ }
274
+ else if (this.scrollData) {
275
+ if (event.type != 'scroll') {
276
+ if (this.timeout) { clearTimeout(this.timeout); }
277
+ this.timeout = setTimeout(this.scroll, Setting.Interval);
278
+ }
279
+ }
280
+ }
281
+
282
+ private transform = (): Heatmap[] => {
283
+ let output: Heatmap[] = [];
284
+ let points: { [key: string]: number } = {};
285
+ let localMax = 0;
286
+ let height = this.state.window && this.state.window.document ? this.state.window.document.documentElement.clientHeight : 0;
287
+
288
+ // DEBUG LOG
289
+ console.groupCollapsed(`[Heatmap] transform — total elements: ${this.data.length}, height: ${height}, max: ${this.max}`);
290
+
291
+ for (let element of this.data) {
292
+ let el = this.layout.get(element.hash) as HTMLElement;
293
+ if (!el) {
294
+ console.warn(`[Heatmap] SKIP hash="${element.hash}" — element not found in DOM`);
295
+ continue;
296
+ }
297
+ if (typeof el.getBoundingClientRect !== "function") {
298
+ console.warn(`[Heatmap] SKIP hash="${element.hash}" — getBoundingClientRect not available`, el);
299
+ continue;
300
+ }
301
+
302
+ let r = el.getBoundingClientRect();
303
+ let v = this.visible(el, r, height, this.state.options.debugHash);
304
+
305
+ if (!v && this.max !== null) {
306
+ console.warn(`[Heatmap] SKIP hash="${element.hash}" — not visible on re-render`, { rect: { top: r.top, left: r.left, width: r.width, height: r.height }, maxIsSet: this.max !== null });
307
+ continue;
308
+ }
309
+
310
+ if (el && typeof el.getBoundingClientRect === "function") {
311
+ console.log(`[Heatmap] PROCESS hash="${element.hash}" visible=${v} points=${element.points}`, { rect: { top: r.top, left: r.left, width: r.width, height: r.height } });
312
+ // Process clicks for only visible elements
313
+ if (this.max === null || v) {
314
+ for (let i = 0; i < element.points; i++) {
315
+ let x = Math.round(r.left + (element.x[i] / Data.Setting.ClickPrecision) * r.width);
316
+ let y = Math.round(r.top + (element.y[i] / Data.Setting.ClickPrecision) * r.height);
317
+ let k = `${x}${Constant.Separator}${y}${Constant.Separator}${v ? 1 : 0}`;
318
+ points[k] = k in points ? points[k] + element.clicks[i] : element.clicks[i];
319
+ localMax = Math.max(points[k], localMax);
320
+ console.log(` point[${i}] x=${x} y=${y} visible=${v} clicks=${element.clicks[i]}`);
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ // Set the max value from the firs t
327
+ this.max = this.max ? this.max : localMax;
328
+
329
+ // Once all points are accounted for, convert everything into absolute (x, y)
330
+ let skippedInvisible = 0;
331
+ for (let coordinates of Object.keys(points)) {
332
+ let parts = coordinates.split(Constant.Separator);
333
+ let alpha = Math.min((points[coordinates] / this.max) + Setting.AlphaBoost, 1);
334
+ if (parts[2] === "1") {
335
+ output.push({ x: parseInt(parts[0], 10), y: parseInt(parts[1], 10), a: alpha });
336
+ } else {
337
+ skippedInvisible++;
338
+ }
339
+ }
340
+
341
+ console.log(`[Heatmap] Result — drawn: ${output.length}, skipped invisible: ${skippedInvisible}, max: ${this.max}`);
342
+ console.groupEnd();
343
+
344
+ return output;
345
+ }
346
+
347
+ private visible = (el: HTMLElement, r: DOMRect, height: number, debugHash?: string): boolean => {
348
+ let doc: Document | ShadowRoot = this.state.window.document;
349
+ let visibility = r.height > height ? true : false;
350
+
351
+ if (debugHash) {
352
+ console.groupCollapsed(`[Heatmap visible] hash=${debugHash} tag=${el?.tagName}`);
353
+ console.log(` rect: top=${r.top.toFixed(1)} bottom=${r.bottom.toFixed(1)} left=${r.left.toFixed(1)} width=${r.width.toFixed(1)} height=${r.height.toFixed(1)}`);
354
+ console.log(` viewport height=${height} | r.height > height? ${r.height > height}`);
355
+ }
356
+
357
+ if (visibility === false && r.width > 0 && r.height > 0) {
358
+ const checkX = r.left + (r.width / 2);
359
+ const checkY = r.top + (r.height / 2);
360
+ if (debugHash) { console.log(` elementsFromPoint(${checkX.toFixed(1)}, ${checkY.toFixed(1)})`); }
361
+
362
+ while (!visibility && doc) {
363
+ let shadowElement = null;
364
+ let caretEl: Node | null = null;
365
+ if ('caretPositionFromPoint' in doc) {
366
+ caretEl = (doc as any).caretPositionFromPoint(checkX, checkY)?.offsetNode ?? null;
367
+ }
368
+ const elementAtPoint = caretEl as Element | null;
369
+ let elements = doc.elementsFromPoint(checkX, checkY);
370
+ if (elementAtPoint && !elements.includes(elementAtPoint)) { elements = [elementAtPoint, ...elements]; }
371
+
372
+ if (debugHash) {
373
+ const topElements = elements.slice(0, 5).map(e => `${e.tagName}${e.id ? '#' + e.id : ''}${e.className ? '.' + String(e.className).trim().split(/\s+/).slice(0, 2).join('.') : ''}`);
374
+ console.log(` top elements at center: [${topElements.join(', ')}]`);
375
+ console.log(` is el in top elements? ${elements.includes(el)} | el matches first non-canvas? ${elements.find(e => !(e.tagName === Constant.Canvas || (e.id && e.id.indexOf(Constant.ClarityPrefix) === 0))) === el}`);
376
+ }
377
+
378
+ for (let e of elements) {
379
+ // Ignore if top element ends up being the canvas element we added for heatmap visualization
380
+ if (e.tagName === Constant.Canvas || (e.id && e.id.indexOf(Constant.ClarityPrefix) === 0)) { continue; }
381
+ // Consider visible if the top element is the element itself OR a descendant of it
382
+ // (e.g. a BUTTON containing P/SPAN children — children will appear on top in elementsFromPoint)
383
+ visibility = e === el || el.contains(e) || e.contains(el);
384
+ shadowElement = e.shadowRoot && e.shadowRoot != doc ? e.shadowRoot : null;
385
+ break;
386
+ }
387
+ doc = shadowElement;
388
+ }
389
+ } else if (debugHash) {
390
+ if (r.width === 0 || r.height === 0) {
391
+ console.log(` SKIP elementsFromPoint — zero dimension: width=${r.width} height=${r.height}`);
392
+ }
393
+ }
394
+
395
+ const result = visibility && r.bottom >= 0 && r.top <= height;
396
+ if (debugHash) {
397
+ console.log(` visibility=${visibility} | r.bottom(${r.bottom.toFixed(1)}) >= 0? ${r.bottom >= 0} | r.top(${r.top.toFixed(1)}) <= height(${height})? ${r.top <= height}`);
398
+ console.log(` => final result: ${result}`);
399
+ console.groupEnd();
400
+ }
401
+ return result;
402
+ }
403
+
404
+ public waitForDialogs = async (): Promise<void> => {
405
+ const isPortalCanvas = !!this.state.options.portalCanvasId;
406
+ if (!isPortalCanvas) return ;
407
+
408
+ await dialogCustom.waitForDialogsRendered();
409
+ return;
410
+ }
411
+
412
+ private waitForAllAnimations = async (): Promise<void> => {
413
+ const win = this.state.window;
414
+ if (!win || !win.document) return;
415
+
416
+ const animations = win.document.getAnimations().filter(a =>
417
+ a.playState === 'running' &&
418
+ (a.effect as KeyframeEffect)?.getTiming().iterations !== Infinity
419
+ );
420
+ if (animations.length === 0) return;
421
+
422
+ const timeout = new Promise<void>(resolve => setTimeout(resolve, 1500));
423
+ await Promise.race([
424
+ Promise.allSettled(animations.map(a => a.finished)),
425
+ timeout
426
+ ]);
427
+ }
428
+
429
+ protected createPortalCanvas = (doc: Document, win: Window, de: HTMLElement): HTMLCanvasElement => {
430
+ let canvas = null;
431
+ try {
432
+ canvas = this.parentCanvasInfo?.canvas;
433
+ if (!canvas) {
434
+ this.parentCanvasInfo = canvasLayer.createParentWindowCanvas(doc, this.state.options.portalCanvasId);
435
+ canvas = this.parentCanvasInfo.canvas;
436
+
437
+ // Add event listeners only once when canvas is created
438
+ win.addEventListener("scroll", this.redraw, { passive: true, capture: true });
439
+ win.addEventListener("resize", this.redraw, { passive: true, capture: true });
440
+ win.addEventListener("animationend", this.redraw, true);
441
+ win.addEventListener("transitionend", this.redraw, true);
442
+ this.observer = this.state.window["ResizeObserver"] ? new ResizeObserver(this.redraw) : null;
443
+
444
+ if (this.observer) { this.observer.observe(doc.body); }
445
+ }
446
+
447
+ canvas.width = de.clientWidth;
448
+ canvas.height = de.clientHeight; // TODO: GEMX VERIFY
449
+ canvas.style.top = '0';
450
+ canvas.style.left = '0';
451
+ canvas.getContext(Constant.Context).clearRect(0, 0, canvas.width, canvas.height);
452
+
453
+ this.parentCanvasInfo.canvas = canvas;
454
+ return canvas;
455
+ } catch (error) {
456
+ console.error(`[Heatmap] createPortalCanvas:`, error);
457
+ }
458
+ return null;
459
+ }
460
+
461
+ // private _visibleV2 = (el: HTMLElement, r: DOMRect, height: number): boolean => {
462
+ // let doc: Document | ShadowRoot = this.state.window.document;
463
+ // let visibility = r.height > height ? true : false;
464
+ // if (visibility === false && r.width > 0 && r.height > 0) {
465
+ // while (!visibility && doc) {
466
+ // let shadowElement = null;
467
+ // let elements = doc.elementsFromPoint(r.left + (r.width / 2), r.top + (r.height / 2));
468
+
469
+ // // Check if only dialog and HTML are returned (element is behind dialog)
470
+ // let hasOnlyDialogAndHtml = elements.length === 2 &&
471
+ // elements[0].tagName === 'DIALOG' &&
472
+ // elements[1].tagName === 'HTML';
473
+
474
+ // // If element is behind a dialog, assume it's visible
475
+ // // (we can't check through top-layer, so trust the element exists)
476
+ // if (hasOnlyDialogAndHtml) {
477
+ // visibility = true;
478
+ // break;
479
+ // }
480
+
481
+ // for (let e of elements) {
482
+ // // Check if this is the target element BEFORE skipping dialogs
483
+ // // This handles case where target element IS a dialog
484
+ // if (e === el) {
485
+ // visibility = true;
486
+ // shadowElement = e.shadowRoot && e.shadowRoot != doc ? e.shadowRoot : null;
487
+ // break;
488
+ // }
489
+
490
+ // // Skip dialog elements - treat as transparent for checking elements behind
491
+ // if (e.tagName === 'DIALOG') {
492
+ // continue;
493
+ // }
494
+
495
+ // // Skip canvas and clarity elements
496
+ // if (e.tagName === Constant.Canvas ||
497
+ // (e.id && e.id.indexOf(Constant.ClarityPrefix) === 0)) {
498
+ // continue;
499
+ // }
500
+
501
+ // // This is the first non-ignored element
502
+ // visibility = e === el;
503
+ // shadowElement = e.shadowRoot && e.shadowRoot != doc ? e.shadowRoot : null;
504
+ // break;
505
+ // }
506
+
507
+ // doc = shadowElement;
508
+ // }
509
+ // }
510
+ // return visibility && r.bottom >= 0 && r.top <= height;
511
+ // }
512
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * as visualize from "./clarity";
2
+ export { Visualizer } from "./visualizer";
3
+ export { HeatmapHelper } from "./heatmap";
4
+ export { LayoutHelper } from "./layout";