@gemx-dev/clarity-visualize 0.8.46 → 0.8.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemx-dev/clarity-visualize",
3
- "version": "0.8.46",
3
+ "version": "0.8.48",
4
4
  "description": "Clarity visualize",
5
5
  "author": "Microsoft Corp.",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
  "unpkg": "build/clarity.visualize.min.js",
10
10
  "types": "types/index.d.ts",
11
11
  "dependencies": {
12
- "@gemx-dev/clarity-decode": "^0.8.46"
12
+ "@gemx-dev/clarity-decode": "^0.8.48"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@rollup/plugin-commonjs": "^24.0.0",
@@ -21,7 +21,6 @@
21
21
  "husky": "^8.0.0",
22
22
  "lint-staged": "^13.1.0",
23
23
  "rollup": "^3.0.0",
24
- "ts-node": "^10.1.0",
25
24
  "tslint": "^6.1.3",
26
25
  "typescript": "^4.3.5"
27
26
  },
@@ -0,0 +1,60 @@
1
+ # Custom Clarity Visualize Modules
2
+
3
+ This directory contains custom rendering modules that extend the base Clarity visualization for GemX.
4
+
5
+ ## Structure
6
+
7
+ All custom modules follow the functional module pattern:
8
+ - Pure functions for testability
9
+ - No side effects in utility functions
10
+ - Clear separation of concerns
11
+ - Type-safe interfaces
12
+
13
+ ## Modules
14
+
15
+ ### Dialog Module (`dialog.ts`)
16
+
17
+ Renders HTML `<dialog>` elements with proper top-layer and backdrop support.
18
+
19
+ **Functions:**
20
+
21
+ - `getDialogRenderOptions(attributes, dialogElement)` - Extracts rendering options from attributes
22
+ - `showDialog(dialogElement, isModal, onError)` - Shows dialog with proper modal/non-modal handling
23
+ - `closeDialog(dialogElement)` - Safely closes a dialog
24
+ - `renderDialog(dialogElement, options, onError)` - Main entry point for dialog rendering
25
+ - `cleanDialogAttributes(attributes)` - Removes internal tracking attributes
26
+
27
+ **Usage:**
28
+
29
+ ```typescript
30
+ import * as dialogCustom from "./custom/dialog";
31
+
32
+ const renderOptions = dialogCustom.getDialogRenderOptions(node.attributes, dialogElement);
33
+ node.attributes = dialogCustom.cleanDialogAttributes(node.attributes);
34
+
35
+ dialogCustom.renderDialog(
36
+ dialogElement,
37
+ renderOptions,
38
+ this.state.options.logerror
39
+ );
40
+ ```
41
+
42
+ **Features:**
43
+
44
+ - Automatic detection of modal vs non-modal dialogs
45
+ - Proper top-layer rendering via `showModal()`
46
+ - Backdrop support for modal dialogs
47
+ - Handles dialogs already open from HTML attributes
48
+ - Error handling and logging
49
+
50
+ ## Adding New Custom Modules
51
+
52
+ 1. Create a new file in this directory (e.g., `my-feature.ts`)
53
+ 2. Follow the functional module pattern
54
+ 3. Export functions with clear type signatures
55
+ 4. Add exports to `index.ts`
56
+ 5. Document the module in this README
57
+
58
+ ## Testing
59
+
60
+ Custom modules should be unit tested separately from the main Clarity codebase.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Custom module for rendering HTML Dialog elements
3
+ * Handles proper visualization of modal dialogs in the top-layer
4
+ */
5
+
6
+ export interface DialogRenderOptions {
7
+ isModal: boolean;
8
+ shouldBeOpen: boolean;
9
+ isExistingDialog: boolean;
10
+ }
11
+
12
+ /**
13
+ * Extracts dialog rendering options from node attributes
14
+ *
15
+ * @param attributes - Node attributes containing dialog state
16
+ * @param dialogElement - The dialog element being rendered
17
+ * @returns Dialog render options
18
+ */
19
+ export function getDialogRenderOptions(
20
+ attributes: { [key: string]: string },
21
+ dialogElement: HTMLDialogElement | null
22
+ ): DialogRenderOptions {
23
+ return {
24
+ isModal: attributes["data-clarity-modal"] === "true",
25
+ shouldBeOpen: attributes["open"] !== undefined,
26
+ isExistingDialog: !!dialogElement
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Shows a dialog element with proper modal/non-modal handling
32
+ * Modal dialogs are shown via showModal() to render in top-layer with backdrop
33
+ * Non-modal dialogs are shown via show() method
34
+ *
35
+ * @param dialogElement - The dialog element to show
36
+ * @param isModal - Whether this is a modal dialog
37
+ * @param onError - Optional error callback
38
+ */
39
+ export function showDialog(
40
+ dialogElement: HTMLDialogElement,
41
+ isModal: boolean,
42
+ onError?: (error: Error) => void
43
+ ): void {
44
+ try {
45
+ if (!dialogElement.isConnected) {
46
+ console.warn('⚠️ Dialog not connected to DOM, skipping show');
47
+ return;
48
+ }
49
+
50
+ // IMPORTANT: If dialog is already open (from HTML attribute),
51
+ // we need to close and reopen to ensure showModal() is called
52
+ // and dialog enters top-layer properly
53
+ if (dialogElement.open) {
54
+ dialogElement.close();
55
+ }
56
+
57
+ if (isModal) {
58
+ dialogElement.showModal();
59
+ } else {
60
+ dialogElement.show();
61
+ }
62
+ } catch (e) {
63
+ console.error('❌ Error showing dialog:', e);
64
+ if (onError) {
65
+ onError(e as Error);
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Closes a dialog element safely
72
+ *
73
+ * @param dialogElement - The dialog element to close
74
+ */
75
+ export function closeDialog(dialogElement: HTMLDialogElement): void {
76
+ try {
77
+ if (dialogElement.open) {
78
+ dialogElement.close();
79
+ }
80
+ } catch (e) {
81
+ console.warn('⚠️ Error closing dialog:', e);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Handles dialog rendering based on its state
87
+ * This is the main entry point for dialog rendering logic
88
+ *
89
+ * @param dialogElement - The dialog element to render
90
+ * @param options - Dialog render options
91
+ * @param onError - Optional error callback
92
+ */
93
+ export function renderDialog(
94
+ dialogElement: HTMLDialogElement,
95
+ options: DialogRenderOptions,
96
+ onError?: (error: Error) => void
97
+ ): void {
98
+ const { isModal, shouldBeOpen, isExistingDialog } = options;
99
+
100
+ if (shouldBeOpen) {
101
+ const doShow = () => showDialog(dialogElement, isModal, onError);
102
+
103
+ if (isExistingDialog) {
104
+ // Dialog already exists, call immediately
105
+ doShow();
106
+ } else {
107
+ // New dialog, wait for DOM insertion
108
+ setTimeout(doShow, 0);
109
+ }
110
+ } else if (dialogElement.open) {
111
+ // Dialog should be closed
112
+ closeDialog(dialogElement);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Removes custom tracking attributes before rendering
118
+ * These attributes are only for internal use
119
+ *
120
+ * @param attributes - Attributes object to clean
121
+ * @returns Cleaned attributes
122
+ */
123
+ export function cleanDialogAttributes(
124
+ attributes: { [key: string]: string }
125
+ ): { [key: string]: string } {
126
+ const cleaned = { ...attributes };
127
+ delete cleaned["data-clarity-modal"];
128
+ return cleaned;
129
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Custom modules for GemX Clarity Visualize extensions
3
+ * Contains custom rendering logic that extends the base Clarity visualization
4
+ */
5
+
6
+ export * from "./dialog";
package/src/heatmap.ts CHANGED
@@ -20,7 +20,7 @@ export class HeatmapHelper {
20
20
  this.state = state;
21
21
  this.layout = layout;
22
22
  }
23
-
23
+
24
24
  public reset = (): void => {
25
25
  this.data = null;
26
26
  this.scrollData = null;
@@ -43,7 +43,7 @@ export class HeatmapHelper {
43
43
  }
44
44
  }
45
45
 
46
- public clear = (): void => {
46
+ public clear = () : void => {
47
47
  let doc = this.state.window.document;
48
48
  let win = this.state.window;
49
49
  let canvas = doc.getElementById(Constant.HeatmapCanvas) as HTMLCanvasElement;
@@ -68,8 +68,8 @@ export class HeatmapHelper {
68
68
  let doc = this.state.window.document;
69
69
  var body = doc.body;
70
70
  var de = doc.documentElement;
71
- var height = Math.max(body.scrollHeight, body.offsetHeight,
72
- de.clientHeight, de.scrollHeight, de.offsetHeight);
71
+ var height = Math.max( body.scrollHeight, body.offsetHeight,
72
+ de.clientHeight, de.scrollHeight, de.offsetHeight );
73
73
  canvas.height = Math.min(height, Setting.ScrollCanvasMaxHeight);
74
74
  canvas.style.top = 0 + Constant.Pixel;
75
75
  if (canvas.width > 0 && canvas.height > 0) {
@@ -148,7 +148,7 @@ export class HeatmapHelper {
148
148
  // For each pixel, we have 4 entries in data array: (r,g,b,a)
149
149
  // To pick the right color from gradient pixels, we look at the alpha value of the pixel
150
150
  // Alpha value ranges from 0-255
151
- let alpha = pixels.data[i + 3];
151
+ let alpha = pixels.data[i+3];
152
152
  if (alpha > 0) {
153
153
  let offset = (alpha - 1) * 4;
154
154
  pixels.data[i] = gradient.data[offset];
@@ -177,7 +177,7 @@ export class HeatmapHelper {
177
177
  win.addEventListener("scroll", this.redraw, true);
178
178
  win.addEventListener("resize", this.redraw, true);
179
179
  this.observer = this.state.window["ResizeObserver"] ? new ResizeObserver(this.redraw) : null;
180
-
180
+
181
181
  if (this.observer) { this.observer.observe(doc.body); }
182
182
  }
183
183
 
@@ -253,7 +253,7 @@ export class HeatmapHelper {
253
253
  let v = this.visible(el, r, height);
254
254
  // Process clicks for only visible elements
255
255
  if (this.max === null || v) {
256
- for (let i = 0; i < element.points; i++) {
256
+ for(let i = 0; i < element.points; i++) {
257
257
  let x = Math.round(r.left + (element.x[i] / Data.Setting.ClickPrecision) * r.width);
258
258
  let y = Math.round(r.top + (element.y[i] / Data.Setting.ClickPrecision) * r.height);
259
259
  let k = `${x}${Constant.Separator}${y}${Constant.Separator}${v ? 1 : 0}`;
@@ -281,7 +281,8 @@ export class HeatmapHelper {
281
281
  let doc: Document | ShadowRoot = this.state.window.document;
282
282
  let visibility = r.height > height ? true : false;
283
283
  if (visibility === false && r.width > 0 && r.height > 0) {
284
- while (!visibility && doc) {
284
+ while (!visibility && doc)
285
+ {
285
286
  let shadowElement = null;
286
287
  let elements = doc.elementsFromPoint(r.left + (r.width / 2), r.top + (r.height / 2));
287
288
  for (let e of elements) {
package/src/layout.ts CHANGED
@@ -5,6 +5,7 @@ import { StyleSheetOperation } from "@gemx-dev/clarity-js/types/layout";
5
5
  import { AnimationOperation } from "@gemx-dev/clarity-js/types/layout";
6
6
  import { Constant as LayoutConstants } from "@gemx-dev/clarity-js/types/layout";
7
7
  import sharedStyle from "./styles/shared.css";
8
+ import * as dialogCustom from "./custom/dialog";
8
9
 
9
10
  /* BEGIN blobUnavailableSvgs */
10
11
  import blobUnavailableSvgEnglish from "./styles/blobUnavailable/english.svg";
@@ -452,6 +453,38 @@ export class LayoutHelper {
452
453
  this.setAttributes(iframeElement, node);
453
454
  insert(node, parent, iframeElement, pivot);
454
455
  break;
456
+ case "SCRIPT":
457
+ {
458
+ node.id = -1; // We want to ensure children of script tags are not processed
459
+ node.value = null; // We don't want to set any potential script content
460
+ this.insertDefaultElement(node, parent, pivot, doc, insert);
461
+ break;
462
+ }
463
+ case "DIALOG":
464
+ {
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
+ // Clean custom tracking attributes
474
+ node.attributes = dialogCustom.cleanDialogAttributes(node.attributes);
475
+
476
+ // Set attributes and insert into DOM
477
+ this.setAttributes(dialogElement, node);
478
+ insert(node, parent, dialogElement, pivot);
479
+
480
+ // Render dialog with proper modal/non-modal handling
481
+ dialogCustom.renderDialog(
482
+ dialogElement,
483
+ renderOptions,
484
+ this.state.options.logerror
485
+ );
486
+ break;
487
+ }
455
488
  default:
456
489
  this.insertDefaultElement(node, parent, pivot, doc, insert);
457
490
  break;
@@ -634,7 +667,7 @@ export class LayoutHelper {
634
667
  node.setAttribute(Constant.Hide, size);
635
668
  }
636
669
  } else {
637
- node.setAttribute(attribute, v);
670
+ node.setAttribute(attribute, this.isSuspiciousAttribute(attribute, v) ? Constant.Empty : v);
638
671
  }
639
672
  } catch (ex) {
640
673
  console.warn("Node: " + node + " | " + JSON.stringify(attributes));
@@ -663,6 +696,23 @@ export class LayoutHelper {
663
696
  }
664
697
  }
665
698
 
699
+ private isSuspiciousAttribute(name: string, value: string): boolean {
700
+ // Block event handlers entirely
701
+ if (name.startsWith('on')) {
702
+ return true;
703
+ }
704
+
705
+ // Check for JavaScript protocols and dangerous patterns
706
+ const dangerous = [
707
+ /^\s*javascript:/i,
708
+ /^\s*data:text\/html/i,
709
+ /^\s*vbscript:/i
710
+ ];
711
+
712
+ return dangerous.some(pattern => pattern.test(value));
713
+ }
714
+
715
+
666
716
  private getMobileCustomStyle = (): string => {
667
717
  if(this.isMobile){
668
718
  return `*{scrollbar-width: none; scrollbar-gutter: unset;};`
@@ -3,4 +3,22 @@ iframe[data-clarity-unavailable-small], iframe[data-clarity-unavailable], img[da
3
3
  border-style: dashed;
4
4
  border-width: 6px;
5
5
  border-color: #827DFF;
6
+ }
7
+
8
+ /* Dialog top-layer styles */
9
+ /* Ensure modal dialogs appear in the top-layer with proper z-index */
10
+ dialog[open] {
11
+ /* Modal dialogs shown via showModal() are automatically placed in top-layer by the browser */
12
+ /* This ensures proper stacking even during replay */
13
+ z-index: auto;
14
+ }
15
+
16
+ /* Style the backdrop for modal dialogs */
17
+ dialog::backdrop {
18
+ background: rgba(0, 0, 0, 0.5);
19
+ }
20
+
21
+ /* Ensure non-modal dialogs (shown via show()) are positioned correctly */
22
+ dialog:not([open]) {
23
+ display: none;
6
24
  }
package/tsconfig.json CHANGED
@@ -1,15 +1,6 @@
1
1
  {
2
+ "extends": "../../tsconfig.json",
2
3
  "compilerOptions": {
3
- "module": "esnext",
4
- "target": "es6",
5
- "lib": ["es6", "dom", "es2016", "es2017"],
6
- "moduleResolution": "node",
7
- "forceConsistentCasingInFileNames": true,
8
- "noImplicitReturns": true,
9
- "noUnusedLocals": true,
10
- "noUnusedParameters": true,
11
- "resolveJsonModule": true,
12
- "esModuleInterop": true,
13
4
  "baseUrl": ".",
14
5
  "paths": {
15
6
  "@src/*": ["src/*"],
@@ -199,7 +199,7 @@ export const enum Constant {
199
199
  NewPassword = "new-password",
200
200
  StyleSheet = "stylesheet",
201
201
  OriginalBackgroundColor = "data-clarity-background-color",
202
- OriginalOpacity = "data-clarity-opacity"
202
+ OriginalOpacity = "data-clarity-opacity",
203
203
  }
204
204
 
205
205
  export const enum Setting {