@gemx-dev/clarity-visualize 0.8.52 → 0.8.53
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/build/clarity.visualize.js +427 -176
- package/build/clarity.visualize.min.js +1 -1
- package/build/clarity.visualize.module.js +427 -176
- package/package.json +2 -2
- package/src/custom/README.md +27 -0
- package/src/custom/canvas-layer.ts +351 -0
- package/src/custom/dialog.ts +130 -19
- package/src/custom/index.ts +1 -0
- package/src/heatmap.ts +122 -3
- package/src/layout.ts +7 -16
- package/src/visualizer.ts +2 -2
- package/types/visualize.d.ts +2 -1
package/src/heatmap.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { Activity, Constant, Heatmap, Setting, ScrollMapInfo, PlaybackState } fr
|
|
|
2
2
|
import { Data } from "@gemx-dev/clarity-js";
|
|
3
3
|
import { LayoutHelper } from "./layout";
|
|
4
4
|
|
|
5
|
+
import * as canvasLayer from "./custom/canvas-layer";
|
|
6
|
+
import * as dialogCustom from "./custom/dialog";
|
|
7
|
+
|
|
5
8
|
export class HeatmapHelper {
|
|
6
9
|
static COLORS = ["blue", "cyan", "lime", "yellow", "red"];
|
|
7
10
|
data: Activity = null;
|
|
@@ -15,6 +18,7 @@ export class HeatmapHelper {
|
|
|
15
18
|
layout: LayoutHelper = null;
|
|
16
19
|
scrollAvgFold: number = null;
|
|
17
20
|
addScrollMakers: boolean = false;
|
|
21
|
+
parentCanvasInfo: canvasLayer.ParentCanvasInfo | null = null;
|
|
18
22
|
|
|
19
23
|
constructor(state: PlaybackState, layout: LayoutHelper) {
|
|
20
24
|
this.state = state;
|
|
@@ -29,6 +33,12 @@ export class HeatmapHelper {
|
|
|
29
33
|
this.gradientPixels = null;
|
|
30
34
|
this.timeout = null;
|
|
31
35
|
|
|
36
|
+
// Cleanup parent canvas if it exists
|
|
37
|
+
if (this.parentCanvasInfo) {
|
|
38
|
+
this.parentCanvasInfo.cleanup();
|
|
39
|
+
this.parentCanvasInfo = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
// Reset resize observer
|
|
33
43
|
if (this.observer) {
|
|
34
44
|
this.observer.disconnect();
|
|
@@ -55,10 +65,19 @@ export class HeatmapHelper {
|
|
|
55
65
|
canvas.style.top = win.pageYOffset + Constant.Pixel;
|
|
56
66
|
canvas.getContext(Constant.Context).clearRect(0, 0, canvas.width, canvas.height);
|
|
57
67
|
}
|
|
68
|
+
|
|
69
|
+
// Cleanup parent canvas before reset
|
|
70
|
+
if (this.parentCanvasInfo) {
|
|
71
|
+
this.parentCanvasInfo.cleanup();
|
|
72
|
+
this.parentCanvasInfo = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
this.reset();
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
public scroll = (activity: ScrollMapInfo[], avgFold: number, addMarkers: boolean): void => {
|
|
78
|
+
public scroll = async (activity: ScrollMapInfo[], avgFold: number, addMarkers: boolean): Promise<void> => {
|
|
79
|
+
await this.waitForDialogs();
|
|
80
|
+
|
|
62
81
|
this.scrollData = this.scrollData || activity;
|
|
63
82
|
this.scrollAvgFold = avgFold != null ? avgFold : this.scrollAvgFold;
|
|
64
83
|
this.addScrollMakers = addMarkers != null ? addMarkers : this.addScrollMakers;
|
|
@@ -123,13 +142,19 @@ export class HeatmapHelper {
|
|
|
123
142
|
context.fillText(label, Setting.MarkerPadding, markerY + Setting.MarkerPadding);
|
|
124
143
|
}
|
|
125
144
|
|
|
126
|
-
public click = (activity: Activity): void => {
|
|
145
|
+
public click = async (activity: Activity): Promise<void> => {
|
|
146
|
+
await this.waitForDialogs();
|
|
147
|
+
|
|
127
148
|
this.data = this.data || activity;
|
|
128
149
|
let heat = this.transform();
|
|
129
150
|
let canvas = this.overlay();
|
|
130
151
|
let ctx = canvas.getContext(Constant.Context);
|
|
131
152
|
|
|
132
153
|
if (canvas.width > 0 && canvas.height > 0) {
|
|
154
|
+
// TODO: GEMX DEBUG Draw a test rectangle to verify canvas is working
|
|
155
|
+
// ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
|
|
156
|
+
// ctx.fillRect(10, 10, 100, 100);
|
|
157
|
+
|
|
133
158
|
// To speed up canvas rendering, we draw ring & gradient on an offscreen canvas, so we can use drawImage API
|
|
134
159
|
// Canvas performance tips: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
|
|
135
160
|
// Pre-render similar primitives or repeating objects on an offscreen canvas
|
|
@@ -157,7 +182,7 @@ export class HeatmapHelper {
|
|
|
157
182
|
}
|
|
158
183
|
}
|
|
159
184
|
ctx.putImageData(pixels, 0, 0);
|
|
160
|
-
}
|
|
185
|
+
}
|
|
161
186
|
}
|
|
162
187
|
|
|
163
188
|
private overlay = (): HTMLCanvasElement => {
|
|
@@ -165,6 +190,10 @@ export class HeatmapHelper {
|
|
|
165
190
|
let doc = this.state.window.document;
|
|
166
191
|
let win = this.state.window;
|
|
167
192
|
let de = doc.documentElement;
|
|
193
|
+
|
|
194
|
+
const isPortalCanvas = this.state.options.portalCanvas;
|
|
195
|
+
if (isPortalCanvas) return this.createPortalCanvas(doc, win, de);
|
|
196
|
+
|
|
168
197
|
let canvas = doc.getElementById(Constant.HeatmapCanvas) as HTMLCanvasElement;
|
|
169
198
|
if (canvas === null) {
|
|
170
199
|
canvas = doc.createElement(Constant.Canvas) as HTMLCanvasElement;
|
|
@@ -297,4 +326,94 @@ export class HeatmapHelper {
|
|
|
297
326
|
}
|
|
298
327
|
return visibility && r.bottom >= 0 && r.top <= height;
|
|
299
328
|
}
|
|
329
|
+
|
|
330
|
+
private waitForDialogs = async (): Promise<void> => {
|
|
331
|
+
const isPortalCanvas = this.state.options.portalCanvas;
|
|
332
|
+
if (!isPortalCanvas) return ;
|
|
333
|
+
|
|
334
|
+
await dialogCustom.waitForDialogsRendered();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private createPortalCanvas = (doc: Document, win: Window, de: HTMLElement): HTMLCanvasElement => {
|
|
339
|
+
let canvas = null;
|
|
340
|
+
try {
|
|
341
|
+
canvas = this.parentCanvasInfo?.canvas;
|
|
342
|
+
if (!canvas) {
|
|
343
|
+
this.parentCanvasInfo = canvasLayer.createParentWindowCanvas(doc);
|
|
344
|
+
canvas = this.parentCanvasInfo.canvas;
|
|
345
|
+
|
|
346
|
+
// Add event listeners only once when canvas is created
|
|
347
|
+
win.addEventListener("scroll", this.redraw, true);
|
|
348
|
+
win.addEventListener("resize", this.redraw, true);
|
|
349
|
+
this.observer = this.state.window["ResizeObserver"] ? new ResizeObserver(this.redraw) : null;
|
|
350
|
+
|
|
351
|
+
if (this.observer) { this.observer.observe(doc.body); }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
canvas.width = de.clientWidth;
|
|
355
|
+
canvas.height = de.clientHeight + 4; // TODO: GEMX VERIFY
|
|
356
|
+
canvas.style.top = '0';
|
|
357
|
+
canvas.style.left = '0';
|
|
358
|
+
canvas.getContext(Constant.Context).clearRect(0, 0, canvas.width, canvas.height);
|
|
359
|
+
|
|
360
|
+
this.parentCanvasInfo.canvas = canvas;
|
|
361
|
+
return canvas;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.error(`🚀 🐥 ~ HeatmapHelper ~ createPortalCanvas:`, error);
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private visibleV2 = (el: HTMLElement, r: DOMRect, height: number): boolean => {
|
|
369
|
+
let doc: Document | ShadowRoot = this.state.window.document;
|
|
370
|
+
let visibility = r.height > height ? true : false;
|
|
371
|
+
if (visibility === false && r.width > 0 && r.height > 0) {
|
|
372
|
+
while (!visibility && doc) {
|
|
373
|
+
let shadowElement = null;
|
|
374
|
+
let elements = doc.elementsFromPoint(r.left + (r.width / 2), r.top + (r.height / 2));
|
|
375
|
+
|
|
376
|
+
// Check if only dialog and HTML are returned (element is behind dialog)
|
|
377
|
+
let hasOnlyDialogAndHtml = elements.length === 2 &&
|
|
378
|
+
elements[0].tagName === 'DIALOG' &&
|
|
379
|
+
elements[1].tagName === 'HTML';
|
|
380
|
+
|
|
381
|
+
// If element is behind a dialog, assume it's visible
|
|
382
|
+
// (we can't check through top-layer, so trust the element exists)
|
|
383
|
+
if (hasOnlyDialogAndHtml) {
|
|
384
|
+
visibility = true;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (let e of elements) {
|
|
389
|
+
// Check if this is the target element BEFORE skipping dialogs
|
|
390
|
+
// This handles case where target element IS a dialog
|
|
391
|
+
if (e === el) {
|
|
392
|
+
visibility = true;
|
|
393
|
+
shadowElement = e.shadowRoot && e.shadowRoot != doc ? e.shadowRoot : null;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Skip dialog elements - treat as transparent for checking elements behind
|
|
398
|
+
if (e.tagName === 'DIALOG') {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Skip canvas and clarity elements
|
|
403
|
+
if (e.tagName === Constant.Canvas ||
|
|
404
|
+
(e.id && e.id.indexOf(Constant.ClarityPrefix) === 0)) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// This is the first non-ignored element
|
|
409
|
+
visibility = e === el;
|
|
410
|
+
shadowElement = e.shadowRoot && e.shadowRoot != doc ? e.shadowRoot : null;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
doc = shadowElement;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return visibility && r.bottom >= 0 && r.top <= height;
|
|
418
|
+
}
|
|
300
419
|
}
|
package/src/layout.ts
CHANGED
|
@@ -141,6 +141,9 @@ export class LayoutHelper {
|
|
|
141
141
|
this.hashMapAlpha = {};
|
|
142
142
|
this.hashMapBeta = {};
|
|
143
143
|
this.primaryHtmlNodeId = null;
|
|
144
|
+
|
|
145
|
+
// Reset dialog render state for new render cycle
|
|
146
|
+
dialogCustom.resetDialogRenderState();
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
public get = (hash) => {
|
|
@@ -462,24 +465,12 @@ export class LayoutHelper {
|
|
|
462
465
|
}
|
|
463
466
|
case "DIALOG":
|
|
464
467
|
{
|
|
465
|
-
|
|
466
|
-
let dialogElement = this.element(node.id) as HTMLDialogElement;
|
|
467
|
-
dialogElement = dialogElement ? dialogElement : this.createElement(doc, node.tag) as HTMLDialogElement;
|
|
468
|
-
if (!node.attributes) { node.attributes = {}; }
|
|
469
|
-
|
|
470
|
-
// Extract render options before cleaning attributes
|
|
471
|
-
const renderOptions = dialogCustom.getDialogRenderOptions(node.attributes, dialogElement);
|
|
472
|
-
|
|
473
|
-
// TODO: Clean custom tracking attributes
|
|
474
|
-
// node.attributes = dialogCustom.cleanDialogAttributes(node.attributes);
|
|
475
|
-
// Set attributes and insert into DOM
|
|
476
|
-
this.setAttributes(dialogElement, node);
|
|
468
|
+
this.insertDefaultElement(node, parent, pivot, doc, insert);
|
|
477
469
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
// Render dialog with proper modal/non-modal handling
|
|
470
|
+
const domElement = this.element(node.id) as HTMLDialogElement;
|
|
471
|
+
const renderOptions = dialogCustom.getDialogRenderOptions(node.attributes, domElement);
|
|
481
472
|
dialogCustom.renderDialog(
|
|
482
|
-
|
|
473
|
+
domElement,
|
|
483
474
|
renderOptions,
|
|
484
475
|
this.state.options.logerror
|
|
485
476
|
);
|
package/src/visualizer.ts
CHANGED
|
@@ -53,12 +53,12 @@ export class Visualizer implements VisualizerType {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
public html = async (decoded: DecodedData.DecodedPayload[], target: Window, hash: string = null, useproxy?: LinkHandler, logerror?: ErrorLogger, shortCircuitStrategy: ShortCircuitStrategy = ShortCircuitStrategy.None): Promise<Visualizer> => {
|
|
56
|
+
public html = async (decoded: DecodedData.DecodedPayload[], target: Window, portalCanvas?: boolean, hash: string = null, useproxy?: LinkHandler, logerror?: ErrorLogger, shortCircuitStrategy: ShortCircuitStrategy = ShortCircuitStrategy.None): Promise<Visualizer> => {
|
|
57
57
|
if (decoded && decoded.length > 0 && target) {
|
|
58
58
|
try {
|
|
59
59
|
// Flatten the payload and parse all events out of them, sorted by time
|
|
60
60
|
let merged = this.merge(decoded);
|
|
61
|
-
await this.setup(target, { version: decoded[0].envelope.version, dom: merged.dom, useproxy });
|
|
61
|
+
await this.setup(target, { version: decoded[0].envelope.version, dom: merged.dom, useproxy, portalCanvas });
|
|
62
62
|
// Render all mutations on top of the initial markup
|
|
63
63
|
while (merged.events.length > 0) {
|
|
64
64
|
let entry = merged.events.shift();
|
package/types/visualize.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface Visualize {
|
|
|
20
20
|
export class Visualizer {
|
|
21
21
|
readonly state: PlaybackState;
|
|
22
22
|
dom: (event: Layout.DomEvent) => Promise<void>;
|
|
23
|
-
html: (decoded: Data.DecodedPayload[], target: Window, hash?: string, useproxy?: LinkHandler, logerror?: ErrorLogger, shortCircuitStrategy?: ShortCircuitStrategy) => Promise<Visualizer>;
|
|
23
|
+
html: (decoded: Data.DecodedPayload[], target: Window, portalCanvas?: boolean, hash?: string, useproxy?: LinkHandler, logerror?: ErrorLogger, shortCircuitStrategy?: ShortCircuitStrategy) => Promise<Visualizer>;
|
|
24
24
|
clickmap: (activity?: Activity) => void;
|
|
25
25
|
clearmap: () => void;
|
|
26
26
|
scrollmap: (data?: ScrollMapInfo[], averageFold?: number, addMarkers?: boolean) => void;
|
|
@@ -65,6 +65,7 @@ export interface Options {
|
|
|
65
65
|
metadata?: HTMLElement;
|
|
66
66
|
pointer?: boolean;
|
|
67
67
|
canvas?: boolean;
|
|
68
|
+
portalCanvas?: boolean;
|
|
68
69
|
keyframes?: boolean;
|
|
69
70
|
mobile?: boolean;
|
|
70
71
|
vNext?: boolean;
|