@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/build/clarity.visualize.js +1085 -767
- package/build/clarity.visualize.min.js +1 -1
- package/build/clarity.visualize.module.js +1085 -767
- package/package.json +2 -3
- package/src/custom/README.md +60 -0
- package/src/custom/dialog.ts +129 -0
- package/src/custom/index.ts +6 -0
- package/src/heatmap.ts +9 -8
- package/src/layout.ts +51 -1
- package/src/styles/shared.css +18 -0
- package/tsconfig.json +1 -10
- package/types/visualize.d.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemx-dev/clarity-visualize",
|
|
3
|
-
"version": "0.8.
|
|
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.
|
|
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
|
+
}
|
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
|
|
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
|
|
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;};`
|
package/src/styles/shared.css
CHANGED
|
@@ -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/*"],
|
package/types/visualize.d.ts
CHANGED
|
@@ -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 {
|