@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.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -0
- package/build/clarity.visualize.js +2753 -0
- package/build/clarity.visualize.min.js +1 -0
- package/build/clarity.visualize.module.js +2748 -0
- package/package.json +45 -0
- package/rollup.config.ts +79 -0
- package/src/clarity.ts +3 -0
- package/src/custom/README.md +87 -0
- package/src/custom/canvas-layer.ts +355 -0
- package/src/custom/dialog.ts +242 -0
- package/src/custom/index.ts +7 -0
- package/src/data.ts +103 -0
- package/src/enrich.ts +80 -0
- package/src/global.ts +9 -0
- package/src/heatmap.ts +512 -0
- package/src/index.ts +4 -0
- package/src/interaction.ts +524 -0
- package/src/layout.ts +805 -0
- package/src/styles/blobUnavailable/chineseSimplified.svg +5 -0
- package/src/styles/blobUnavailable/chineseTraditional.svg +5 -0
- package/src/styles/blobUnavailable/dutch.svg +5 -0
- package/src/styles/blobUnavailable/english.svg +5 -0
- package/src/styles/blobUnavailable/french.svg +5 -0
- package/src/styles/blobUnavailable/german.svg +5 -0
- package/src/styles/blobUnavailable/iconOnly.svg +4 -0
- package/src/styles/blobUnavailable/italian.svg +5 -0
- package/src/styles/blobUnavailable/japanese.svg +5 -0
- package/src/styles/blobUnavailable/korean.svg +5 -0
- package/src/styles/blobUnavailable/portuguese.svg +5 -0
- package/src/styles/blobUnavailable/russian.svg +5 -0
- package/src/styles/blobUnavailable/spanish.svg +5 -0
- package/src/styles/blobUnavailable/turkish.svg +5 -0
- package/src/styles/iframeUnavailable/chineseSimplified.svg +5 -0
- package/src/styles/iframeUnavailable/chineseTraditional.svg +5 -0
- package/src/styles/iframeUnavailable/dutch.svg +5 -0
- package/src/styles/iframeUnavailable/english.svg +5 -0
- package/src/styles/iframeUnavailable/french.svg +5 -0
- package/src/styles/iframeUnavailable/german.svg +5 -0
- package/src/styles/iframeUnavailable/iconOnly.svg +4 -0
- package/src/styles/iframeUnavailable/italian.svg +5 -0
- package/src/styles/iframeUnavailable/japanese.svg +5 -0
- package/src/styles/iframeUnavailable/korean.svg +5 -0
- package/src/styles/iframeUnavailable/portuguese.svg +5 -0
- package/src/styles/iframeUnavailable/russian.svg +5 -0
- package/src/styles/iframeUnavailable/spanish.svg +5 -0
- package/src/styles/iframeUnavailable/turkish.svg +5 -0
- package/src/styles/imageMasked/chineseSimplified.svg +5 -0
- package/src/styles/imageMasked/chineseTraditional.svg +5 -0
- package/src/styles/imageMasked/dutch.svg +5 -0
- package/src/styles/imageMasked/english.svg +5 -0
- package/src/styles/imageMasked/french.svg +5 -0
- package/src/styles/imageMasked/german.svg +5 -0
- package/src/styles/imageMasked/iconOnly.svg +4 -0
- package/src/styles/imageMasked/italian.svg +5 -0
- package/src/styles/imageMasked/japanese.svg +5 -0
- package/src/styles/imageMasked/korean.svg +5 -0
- package/src/styles/imageMasked/portuguese.svg +5 -0
- package/src/styles/imageMasked/russian.svg +5 -0
- package/src/styles/imageMasked/spanish.svg +5 -0
- package/src/styles/imageMasked/turkish.svg +5 -0
- package/src/styles/pointer/click.css +31 -0
- package/src/styles/pointer/pointerIcon.svg +18 -0
- package/src/styles/shared.css +6 -0
- package/src/visualizer.ts +308 -0
- package/tsconfig.json +13 -0
- package/tslint.json +33 -0
- package/types/heatmap.d.ts +18 -0
- package/types/index.d.ts +11 -0
- package/types/layout.d.ts +12 -0
- package/types/string-import.d.ts +9 -0
- package/types/visualize.d.ts +253 -0
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gem-sdk/clarity-visualize",
|
|
3
|
+
"version": "0.8.94",
|
|
4
|
+
"description": "Clarity visualize",
|
|
5
|
+
"author": "Microsoft Corp.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "build/clarity.visualize.js",
|
|
8
|
+
"module": "build/clarity.visualize.module.js",
|
|
9
|
+
"unpkg": "build/clarity.visualize.min.js",
|
|
10
|
+
"types": "types/index.d.ts",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@gem-sdk/clarity-decode": "^0.8.94"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@rollup/plugin-commonjs": "^24.0.0",
|
|
16
|
+
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
17
|
+
"@rollup/plugin-terser": "^0.4.0",
|
|
18
|
+
"@rollup/plugin-typescript": "^11.0.0",
|
|
19
|
+
"rollup-plugin-string-import": "^1.2.5",
|
|
20
|
+
"del-cli": "^5.0.0",
|
|
21
|
+
"husky": "^8.0.0",
|
|
22
|
+
"lint-staged": "^13.1.0",
|
|
23
|
+
"rollup": "^3.0.0",
|
|
24
|
+
"tslint": "^6.1.3",
|
|
25
|
+
"typescript": "^4.3.5"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "yarn build:clean && yarn build:main",
|
|
29
|
+
"build:main": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript",
|
|
30
|
+
"build:clean": "del-cli build/*",
|
|
31
|
+
"tslint": "tslint --project ./",
|
|
32
|
+
"tslint:fix": "tslint --fix --project ./ --force"
|
|
33
|
+
},
|
|
34
|
+
"husky": {
|
|
35
|
+
"hooks": {
|
|
36
|
+
"pre-commit": "lint-staged"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"lint-staged": {
|
|
40
|
+
"*.ts": [
|
|
41
|
+
"tslint --format codeFrame"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"source": "src/index.ts"
|
|
45
|
+
}
|
package/rollup.config.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import commonjs from "@rollup/plugin-commonjs";
|
|
2
|
+
import resolve from "@rollup/plugin-node-resolve";
|
|
3
|
+
import terser from "@rollup/plugin-terser";
|
|
4
|
+
import typescript from "@rollup/plugin-typescript";
|
|
5
|
+
import { importAsString } from 'rollup-plugin-string-import';
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
|
|
8
|
+
const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));
|
|
9
|
+
|
|
10
|
+
function wrapWithBackground(svg){
|
|
11
|
+
return `background: url("data:image/svg+xml,${svg}") no-repeat center center;`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function regexEncode(svg){
|
|
15
|
+
svg = svg.replace(/>\s{1,}</g, `><`);
|
|
16
|
+
svg = svg.replace(/\s{2,}/g, ` `);
|
|
17
|
+
return svg.replace(/[\r\n%#()<>?[\\\]^`{|}]/g, encodeURIComponent)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function doublesToSingles(svg){
|
|
21
|
+
return svg.replace(/"/g, "'")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function preprocessFile(content: string, fileName: string): string {
|
|
25
|
+
if (fileName.endsWith(".css")) {
|
|
26
|
+
// remove comments/newlines
|
|
27
|
+
return content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "").replace(/\r\n/g, "");
|
|
28
|
+
}
|
|
29
|
+
else if (fileName.endsWith(".svg")) {
|
|
30
|
+
// removes comments/newlines while also encoding the svg and adding some scaffolding to import it
|
|
31
|
+
return wrapWithBackground(regexEncode(doublesToSingles(content)));
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default [
|
|
37
|
+
{
|
|
38
|
+
input: "src/index.ts",
|
|
39
|
+
output: [
|
|
40
|
+
{ file: pkg.main, format: "cjs", exports: "named" },
|
|
41
|
+
{ file: pkg.module, format: "es", exports: "named" }
|
|
42
|
+
],
|
|
43
|
+
plugins: [
|
|
44
|
+
resolve(),
|
|
45
|
+
importAsString({
|
|
46
|
+
include: ["**/*.css", "**/*.svg"],
|
|
47
|
+
transform: preprocessFile,
|
|
48
|
+
}),
|
|
49
|
+
typescript(),
|
|
50
|
+
commonjs({ include: ["node_modules/**"] })
|
|
51
|
+
],
|
|
52
|
+
onwarn(message, warn) {
|
|
53
|
+
if (message.code === 'NON_EXISTENT_EXPORT') { return; }
|
|
54
|
+
if (message.code === 'CIRCULAR_DEPENDENCY') { return; }
|
|
55
|
+
if (message.code === 'SOURCEMAP_ERROR') { return; }
|
|
56
|
+
warn(message);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
input: "src/global.ts",
|
|
61
|
+
output: [ { file: pkg.unpkg, format: "iife", exports: "named" } ],
|
|
62
|
+
plugins: [
|
|
63
|
+
resolve(),
|
|
64
|
+
importAsString({
|
|
65
|
+
include: ["**/*.css", "**/*.svg"],
|
|
66
|
+
transform: preprocessFile,
|
|
67
|
+
}),
|
|
68
|
+
typescript(),
|
|
69
|
+
terser({output: {comments: false}}),
|
|
70
|
+
commonjs({ include: ["node_modules/**"] })
|
|
71
|
+
],
|
|
72
|
+
onwarn(message, warn) {
|
|
73
|
+
if (message.code === 'NON_EXISTENT_EXPORT') { return; }
|
|
74
|
+
if (message.code === 'CIRCULAR_DEPENDENCY') { return; }
|
|
75
|
+
if (message.code === 'SOURCEMAP_ERROR') { return; }
|
|
76
|
+
warn(message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
];
|
package/src/clarity.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
### Canvas Layer Module (`canvas-layer.ts`)
|
|
16
|
+
|
|
17
|
+
Manages canvas z-index and stacking context with top-layer elements (modal dialogs).
|
|
18
|
+
|
|
19
|
+
**Functions:**
|
|
20
|
+
|
|
21
|
+
- `getOpenDialogs(doc)` - Gets all open modal dialogs
|
|
22
|
+
- `hideDialogsTemporarily(doc)` - Hides modal dialogs and returns restore function
|
|
23
|
+
- `setupCanvasLayer(canvas, options)` - Sets up canvas with proper layer handling
|
|
24
|
+
- `applyCanvasLayerWithDialogAwareness(canvas, onShow, onHide)` - Dialog-aware canvas management
|
|
25
|
+
|
|
26
|
+
**Usage:**
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import * as canvasLayer from "./custom/canvas-layer";
|
|
30
|
+
|
|
31
|
+
// Hide dialogs when showing heatmap
|
|
32
|
+
const restoreDialogs = canvasLayer.hideDialogsTemporarily(doc);
|
|
33
|
+
|
|
34
|
+
// Later, restore dialogs
|
|
35
|
+
restoreDialogs();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Why this is needed:**
|
|
39
|
+
|
|
40
|
+
Modal dialogs use the browser's **top-layer**, which has a higher stacking context than any z-index value (even `z-index: 2147483647`). This module temporarily hides modal dialogs when the heatmap canvas is active, ensuring the canvas is visible.
|
|
41
|
+
|
|
42
|
+
### Dialog Module (`dialog.ts`)
|
|
43
|
+
|
|
44
|
+
Renders HTML `<dialog>` elements with proper top-layer and backdrop support.
|
|
45
|
+
|
|
46
|
+
**Functions:**
|
|
47
|
+
|
|
48
|
+
- `getDialogRenderOptions(attributes, dialogElement)` - Extracts rendering options from attributes
|
|
49
|
+
- `showDialog(dialogElement, isModal, onError)` - Shows dialog with proper modal/non-modal handling
|
|
50
|
+
- `closeDialog(dialogElement)` - Safely closes a dialog
|
|
51
|
+
- `renderDialog(dialogElement, options, onError)` - Main entry point for dialog rendering
|
|
52
|
+
- `cleanDialogAttributes(attributes)` - Removes internal tracking attributes
|
|
53
|
+
|
|
54
|
+
**Usage:**
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import * as dialogCustom from "./custom/dialog";
|
|
58
|
+
|
|
59
|
+
const renderOptions = dialogCustom.getDialogRenderOptions(node.attributes, dialogElement);
|
|
60
|
+
node.attributes = dialogCustom.cleanDialogAttributes(node.attributes);
|
|
61
|
+
|
|
62
|
+
dialogCustom.renderDialog(
|
|
63
|
+
dialogElement,
|
|
64
|
+
renderOptions,
|
|
65
|
+
this.state.options.logerror
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Features:**
|
|
70
|
+
|
|
71
|
+
- Automatic detection of modal vs non-modal dialogs
|
|
72
|
+
- Proper top-layer rendering via `showModal()`
|
|
73
|
+
- Backdrop support for modal dialogs
|
|
74
|
+
- Handles dialogs already open from HTML attributes
|
|
75
|
+
- Error handling and logging
|
|
76
|
+
|
|
77
|
+
## Adding New Custom Modules
|
|
78
|
+
|
|
79
|
+
1. Create a new file in this directory (e.g., `my-feature.ts`)
|
|
80
|
+
2. Follow the functional module pattern
|
|
81
|
+
3. Export functions with clear type signatures
|
|
82
|
+
4. Add exports to `index.ts`
|
|
83
|
+
5. Document the module in this README
|
|
84
|
+
|
|
85
|
+
## Testing
|
|
86
|
+
|
|
87
|
+
Custom modules should be unit tested separately from the main Clarity codebase.
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { Constant } from "@clarity-types/visualize";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom module for managing canvas layer z-index with dialog top-layer
|
|
5
|
+
* Handles stacking context when modal dialogs are present
|
|
6
|
+
*
|
|
7
|
+
* Strategy: Render canvas in parent window (outside iframe) to avoid
|
|
8
|
+
* z-index conflicts with top-layer elements inside the iframe
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface CanvasLayerOptions {
|
|
12
|
+
strategy: 'parent-window' | 'max-z-index' | 'hide-dialogs' | 'popover';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ParentCanvasInfo {
|
|
16
|
+
canvas: HTMLCanvasElement;
|
|
17
|
+
iframe: HTMLIFrameElement;
|
|
18
|
+
cleanup: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a canvas as sibling to the iframe (same parent container)
|
|
23
|
+
* This allows the canvas to avoid z-index conflicts with top-layer elements inside iframe
|
|
24
|
+
*
|
|
25
|
+
* @param iframeDoc - Document inside the iframe
|
|
26
|
+
* @param canvasId - ID for the canvas element
|
|
27
|
+
* @returns Canvas info with cleanup function
|
|
28
|
+
*/
|
|
29
|
+
export function createParentWindowCanvas(
|
|
30
|
+
iframeDoc: Document,
|
|
31
|
+
portalCanvasId: string
|
|
32
|
+
): ParentCanvasInfo | null {
|
|
33
|
+
// Check if we're inside an iframe
|
|
34
|
+
if (iframeDoc.defaultView === iframeDoc.defaultView?.top) {
|
|
35
|
+
throw new Error('Not in iframe: ' + iframeDoc.defaultView);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const parentWindow = iframeDoc.defaultView?.parent;
|
|
40
|
+
const parentDoc = parentWindow?.document;
|
|
41
|
+
if (!parentDoc) throw new Error('Parent document not found');
|
|
42
|
+
|
|
43
|
+
const iframeId = `clarity-iframe-${portalCanvasId}`;
|
|
44
|
+
const iframe = parentDoc.getElementById(iframeId);
|
|
45
|
+
if (!iframe) throw new Error('Iframe not found');
|
|
46
|
+
|
|
47
|
+
const targetIframe = iframe as HTMLIFrameElement;
|
|
48
|
+
const iframeParent = targetIframe.parentElement;
|
|
49
|
+
if (!iframeParent) throw new Error('Iframe parent not found');
|
|
50
|
+
|
|
51
|
+
// Create canvas as sibling to iframe
|
|
52
|
+
const canvasId = `${Constant.HeatmapCanvas}-${portalCanvasId}`;
|
|
53
|
+
let canvas = parentDoc.getElementById(canvasId) as HTMLCanvasElement;
|
|
54
|
+
if (canvas === null) {
|
|
55
|
+
canvas = parentDoc.createElement('canvas');
|
|
56
|
+
canvas.id = canvasId;
|
|
57
|
+
canvas.classList.add(Constant.HeatmapCanvas);
|
|
58
|
+
canvas.width = 0;
|
|
59
|
+
canvas.height = 0;
|
|
60
|
+
canvas.style.position = Constant.Absolute;
|
|
61
|
+
// canvas.style.zIndex = `${Setting.ZIndex}`;
|
|
62
|
+
canvas.style.pointerEvents = 'none'; // Allow clicks to pass through
|
|
63
|
+
iframeParent.appendChild(canvas);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// // Set canvas size to match iframe
|
|
67
|
+
// const updateCanvasSize = () => {
|
|
68
|
+
// canvas.width = targetIframe!.offsetWidth;
|
|
69
|
+
// canvas.height = targetIframe!.offsetHeight;
|
|
70
|
+
// };
|
|
71
|
+
|
|
72
|
+
// updateCanvasSize();
|
|
73
|
+
|
|
74
|
+
// Insert canvas as sibling to iframe (in same parent container)
|
|
75
|
+
// iframeParent.appendChild(canvas);
|
|
76
|
+
|
|
77
|
+
// Update size on resize
|
|
78
|
+
// const handleUpdate = () => updateCanvasSize();
|
|
79
|
+
// parentWindow!.addEventListener('resize', handleUpdate, true);
|
|
80
|
+
|
|
81
|
+
const cleanup = () => {
|
|
82
|
+
// parentWindow!.removeEventListener('resize', handleUpdate, true);
|
|
83
|
+
if (canvas.parentNode) {
|
|
84
|
+
canvas.parentNode.removeChild(canvas);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
canvas,
|
|
90
|
+
iframe: targetIframe,
|
|
91
|
+
cleanup
|
|
92
|
+
};
|
|
93
|
+
} catch (e) {
|
|
94
|
+
throw new Error(e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ======================================================== //
|
|
99
|
+
// ======================================================== //
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Gets all open modal dialogs in the document
|
|
104
|
+
*/
|
|
105
|
+
export function getOpenDialogs(doc: Document): HTMLDialogElement[] {
|
|
106
|
+
const dialogs = Array.from(doc.querySelectorAll('dialog[open]'));
|
|
107
|
+
return dialogs.filter(d => {
|
|
108
|
+
const dialog = d as HTMLDialogElement;
|
|
109
|
+
// Check if it's a modal dialog (in top-layer)
|
|
110
|
+
try {
|
|
111
|
+
const backdropStyle = window.getComputedStyle(dialog, '::backdrop');
|
|
112
|
+
return backdropStyle.getPropertyValue('display') !== 'none';
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}) as HTMLDialogElement[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Temporarily hides modal dialogs to allow canvas to be visible
|
|
121
|
+
* Returns a restore function to bring dialogs back
|
|
122
|
+
*/
|
|
123
|
+
export function hideDialogsTemporarily(doc: Document): () => void {
|
|
124
|
+
const dialogs = getOpenDialogs(doc);
|
|
125
|
+
const dialogStates: Array<{ dialog: HTMLDialogElement; wasOpen: boolean }> = [];
|
|
126
|
+
|
|
127
|
+
dialogs.forEach(dialog => {
|
|
128
|
+
if (dialog.open) {
|
|
129
|
+
dialogStates.push({ dialog, wasOpen: true });
|
|
130
|
+
dialog.close();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Return restore function
|
|
135
|
+
return () => {
|
|
136
|
+
dialogStates.forEach(({ dialog, wasOpen }) => {
|
|
137
|
+
if (wasOpen && !dialog.open) {
|
|
138
|
+
try {
|
|
139
|
+
dialog.showModal();
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.warn('Failed to restore dialog:', e);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sets up canvas with proper z-index handling
|
|
150
|
+
*
|
|
151
|
+
* @param canvas - The canvas element
|
|
152
|
+
* @param options - Layer options
|
|
153
|
+
* @returns Cleanup function
|
|
154
|
+
*/
|
|
155
|
+
export function setupCanvasLayer(
|
|
156
|
+
canvas: HTMLCanvasElement,
|
|
157
|
+
options: CanvasLayerOptions = { strategy: 'max-z-index' }
|
|
158
|
+
): () => void {
|
|
159
|
+
switch (options.strategy) {
|
|
160
|
+
case 'hide-dialogs':
|
|
161
|
+
// Hide dialogs when canvas is active
|
|
162
|
+
return hideDialogsTemporarily(canvas.ownerDocument);
|
|
163
|
+
|
|
164
|
+
case 'popover':
|
|
165
|
+
// Use Popover API if available (also uses top-layer)
|
|
166
|
+
if ('popover' in canvas) {
|
|
167
|
+
try {
|
|
168
|
+
(canvas as any).popover = 'manual';
|
|
169
|
+
(canvas as any).showPopover();
|
|
170
|
+
return () => {
|
|
171
|
+
try {
|
|
172
|
+
(canvas as any).hidePopover();
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// Ignore
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.warn('Popover API not available, falling back to max z-index');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Fallback to max-z-index
|
|
182
|
+
return () => {};
|
|
183
|
+
|
|
184
|
+
case 'max-z-index':
|
|
185
|
+
default:
|
|
186
|
+
// Use maximum z-index (works for non-modal content)
|
|
187
|
+
// Modal dialogs will still be on top due to top-layer
|
|
188
|
+
return () => {};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Synchronizes canvas content from iframe to parent window canvas
|
|
196
|
+
*
|
|
197
|
+
* @param iframeCanvas - Canvas inside iframe
|
|
198
|
+
* @param parentCanvas - Canvas in parent window
|
|
199
|
+
*/
|
|
200
|
+
export function syncCanvasContent(
|
|
201
|
+
iframeCanvas: HTMLCanvasElement,
|
|
202
|
+
parentCanvas: HTMLCanvasElement
|
|
203
|
+
): void {
|
|
204
|
+
const ctx = parentCanvas.getContext('2d');
|
|
205
|
+
if (ctx) {
|
|
206
|
+
ctx.clearRect(0, 0, parentCanvas.width, parentCanvas.height);
|
|
207
|
+
ctx.drawImage(iframeCanvas, 0, 0);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Alternative approach: Make dialogs "transparent" for rendering
|
|
213
|
+
* Sets pointer-events: none on dialogs so canvas can render on top
|
|
214
|
+
*/
|
|
215
|
+
export function makeDialogsTransparent(doc: Document): () => void {
|
|
216
|
+
const dialogs = Array.from(doc.querySelectorAll('dialog[open]')) as HTMLDialogElement[];
|
|
217
|
+
const originalStyles: Map<HTMLDialogElement, string> = new Map();
|
|
218
|
+
|
|
219
|
+
dialogs.forEach((dialog) => {
|
|
220
|
+
originalStyles.set(dialog, dialog.style.pointerEvents || '');
|
|
221
|
+
dialog.style.pointerEvents = 'none';
|
|
222
|
+
dialog.style.zIndex = '2147483646'; // Just below canvas
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Return restore function
|
|
226
|
+
return () => {
|
|
227
|
+
dialogs.forEach((dialog) => {
|
|
228
|
+
const original = originalStyles.get(dialog);
|
|
229
|
+
if (original !== undefined) {
|
|
230
|
+
dialog.style.pointerEvents = original;
|
|
231
|
+
dialog.style.zIndex = '';
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Solution 3: Lower dialog z-index temporarily
|
|
239
|
+
* Moves dialog out of top-layer by closing and re-showing as non-modal
|
|
240
|
+
*/
|
|
241
|
+
export function lowerDialogZIndex(doc: Document): () => void {
|
|
242
|
+
const dialogs = getOpenDialogs(doc);
|
|
243
|
+
const dialogStates: Array<{ dialog: HTMLDialogElement; wasModal: boolean }> = [];
|
|
244
|
+
|
|
245
|
+
dialogs.forEach((dialog) => {
|
|
246
|
+
const wasModal = true; // If it's in getOpenDialogs, it's modal
|
|
247
|
+
dialogStates.push({ dialog, wasModal });
|
|
248
|
+
|
|
249
|
+
// Close modal and reopen as non-modal (removes from top-layer)
|
|
250
|
+
dialog.close();
|
|
251
|
+
dialog.show(); // Non-modal, can be controlled by z-index
|
|
252
|
+
dialog.style.zIndex = '2147483646'; // Below canvas
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Return restore function
|
|
256
|
+
return () => {
|
|
257
|
+
dialogStates.forEach(({ dialog, wasModal }) => {
|
|
258
|
+
if (wasModal) {
|
|
259
|
+
dialog.close();
|
|
260
|
+
try {
|
|
261
|
+
dialog.showModal(); // Restore to top-layer
|
|
262
|
+
dialog.style.zIndex = '';
|
|
263
|
+
} catch (e) {
|
|
264
|
+
console.warn('Failed to restore modal dialog:', e);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Solution 4: Use mix-blend-mode to render canvas "through" dialog
|
|
273
|
+
* This is experimental and may not work in all browsers
|
|
274
|
+
*/
|
|
275
|
+
export function setupCanvasBlendMode(canvas: HTMLCanvasElement): void {
|
|
276
|
+
canvas.style.mixBlendMode = 'multiply';
|
|
277
|
+
canvas.style.isolation = 'isolate';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Master function: Apply best strategy based on context
|
|
282
|
+
* Tries multiple approaches in order of preference
|
|
283
|
+
*/
|
|
284
|
+
export function applyBestCanvasStrategy(
|
|
285
|
+
doc: Document,
|
|
286
|
+
canvasId: string,
|
|
287
|
+
strategy?: 'parent-window' | 'lower-dialog' | 'transparent' | 'auto'
|
|
288
|
+
): { canvas: HTMLCanvasElement | null; cleanup: () => void } | null {
|
|
289
|
+
const selectedStrategy = strategy || 'auto';
|
|
290
|
+
|
|
291
|
+
// Strategy 1: Parent window canvas (best for iframe contexts)
|
|
292
|
+
if (selectedStrategy === 'auto' || selectedStrategy === 'parent-window') {
|
|
293
|
+
const parentCanvas = createParentWindowCanvas(doc, canvasId);
|
|
294
|
+
if (parentCanvas) {
|
|
295
|
+
return {
|
|
296
|
+
canvas: parentCanvas.canvas,
|
|
297
|
+
cleanup: parentCanvas.cleanup
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Strategy 2: Lower dialog z-index (converts modal to non-modal temporarily)
|
|
303
|
+
if (selectedStrategy === 'auto' || selectedStrategy === 'lower-dialog') {
|
|
304
|
+
const canvas = doc.getElementById(canvasId) as HTMLCanvasElement;
|
|
305
|
+
if (canvas) {
|
|
306
|
+
const restore = lowerDialogZIndex(doc);
|
|
307
|
+
return {
|
|
308
|
+
canvas,
|
|
309
|
+
cleanup: restore
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Strategy 3: Make dialogs transparent (least intrusive)
|
|
315
|
+
if (selectedStrategy === 'auto' || selectedStrategy === 'transparent') {
|
|
316
|
+
const canvas = doc.getElementById(canvasId) as HTMLCanvasElement;
|
|
317
|
+
if (canvas) {
|
|
318
|
+
const restore = makeDialogsTransparent(doc);
|
|
319
|
+
return {
|
|
320
|
+
canvas,
|
|
321
|
+
cleanup: restore
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Applies canvas layer with dialog awareness
|
|
331
|
+
* This is the recommended approach: hide dialogs temporarily while heatmap is active
|
|
332
|
+
*/
|
|
333
|
+
export function applyCanvasLayerWithDialogAwareness(
|
|
334
|
+
canvas: HTMLCanvasElement,
|
|
335
|
+
onShow?: () => void,
|
|
336
|
+
onHide?: () => void
|
|
337
|
+
): { show: () => void; hide: () => void } {
|
|
338
|
+
let restoreDialogs: (() => void) | null = null;
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
show: () => {
|
|
342
|
+
// Hide dialogs when showing canvas
|
|
343
|
+
restoreDialogs = hideDialogsTemporarily(canvas.ownerDocument);
|
|
344
|
+
if (onShow) onShow();
|
|
345
|
+
},
|
|
346
|
+
hide: () => {
|
|
347
|
+
// Restore dialogs when hiding canvas
|
|
348
|
+
if (restoreDialogs) {
|
|
349
|
+
restoreDialogs();
|
|
350
|
+
restoreDialogs = null;
|
|
351
|
+
}
|
|
352
|
+
if (onHide) onHide();
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|