@gemx-dev/clarity-visualize 0.8.51 → 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/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
- // Use custom module for dialog rendering
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
- insert(node, parent, dialogElement, pivot);
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
- dialogElement,
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();
@@ -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;