@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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemx-dev/clarity-visualize",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.53",
|
|
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.53"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@rollup/plugin-commonjs": "^24.0.0",
|
package/src/custom/README.md
CHANGED
|
@@ -12,6 +12,33 @@ All custom modules follow the functional module pattern:
|
|
|
12
12
|
|
|
13
13
|
## Modules
|
|
14
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
|
+
|
|
15
42
|
### Dialog Module (`dialog.ts`)
|
|
16
43
|
|
|
17
44
|
Renders HTML `<dialog>` elements with proper top-layer and backdrop support.
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { Constant, Setting } 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
|
+
): ParentCanvasInfo | null {
|
|
32
|
+
// Check if we're inside an iframe
|
|
33
|
+
if (iframeDoc.defaultView === iframeDoc.defaultView?.top) {
|
|
34
|
+
throw new Error('Not in iframe: ' + iframeDoc.defaultView);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const parentWindow = iframeDoc.defaultView?.parent;
|
|
39
|
+
const parentDoc = parentWindow?.document;
|
|
40
|
+
if (!parentDoc) throw new Error('Parent document not found');
|
|
41
|
+
|
|
42
|
+
const iframe = parentDoc.getElementById('clarity-iframe');
|
|
43
|
+
if (!iframe) throw new Error('Iframe not found');
|
|
44
|
+
|
|
45
|
+
const targetIframe = iframe as HTMLIFrameElement;
|
|
46
|
+
const iframeParent = targetIframe.parentElement;
|
|
47
|
+
if (!iframeParent) throw new Error('Iframe parent not found');
|
|
48
|
+
|
|
49
|
+
// Create canvas as sibling to iframe
|
|
50
|
+
let canvas = parentDoc.getElementById(Constant.HeatmapCanvas) as HTMLCanvasElement;
|
|
51
|
+
if (canvas === null) {
|
|
52
|
+
canvas = parentDoc.createElement('canvas');
|
|
53
|
+
canvas.id = Constant.HeatmapCanvas;
|
|
54
|
+
canvas.width = 0;
|
|
55
|
+
canvas.height = 0;
|
|
56
|
+
canvas.style.position = Constant.Absolute;
|
|
57
|
+
canvas.style.zIndex = `${Setting.ZIndex}`;
|
|
58
|
+
canvas.style.pointerEvents = 'none'; // Allow clicks to pass through
|
|
59
|
+
iframeParent.appendChild(canvas);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// // Set canvas size to match iframe
|
|
63
|
+
// const updateCanvasSize = () => {
|
|
64
|
+
// canvas.width = targetIframe!.offsetWidth;
|
|
65
|
+
// canvas.height = targetIframe!.offsetHeight;
|
|
66
|
+
// };
|
|
67
|
+
|
|
68
|
+
// updateCanvasSize();
|
|
69
|
+
|
|
70
|
+
// Insert canvas as sibling to iframe (in same parent container)
|
|
71
|
+
// iframeParent.appendChild(canvas);
|
|
72
|
+
|
|
73
|
+
// Update size on resize
|
|
74
|
+
// const handleUpdate = () => updateCanvasSize();
|
|
75
|
+
// parentWindow!.addEventListener('resize', handleUpdate, true);
|
|
76
|
+
|
|
77
|
+
const cleanup = () => {
|
|
78
|
+
// parentWindow!.removeEventListener('resize', handleUpdate, true);
|
|
79
|
+
if (canvas.parentNode) {
|
|
80
|
+
canvas.parentNode.removeChild(canvas);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
canvas,
|
|
86
|
+
iframe: targetIframe,
|
|
87
|
+
cleanup
|
|
88
|
+
};
|
|
89
|
+
} catch (e) {
|
|
90
|
+
throw new Error(e);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ======================================================== //
|
|
95
|
+
// ======================================================== //
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Gets all open modal dialogs in the document
|
|
100
|
+
*/
|
|
101
|
+
export function getOpenDialogs(doc: Document): HTMLDialogElement[] {
|
|
102
|
+
const dialogs = Array.from(doc.querySelectorAll('dialog[open]'));
|
|
103
|
+
return dialogs.filter(d => {
|
|
104
|
+
const dialog = d as HTMLDialogElement;
|
|
105
|
+
// Check if it's a modal dialog (in top-layer)
|
|
106
|
+
try {
|
|
107
|
+
const backdropStyle = window.getComputedStyle(dialog, '::backdrop');
|
|
108
|
+
return backdropStyle.getPropertyValue('display') !== 'none';
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}) as HTMLDialogElement[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Temporarily hides modal dialogs to allow canvas to be visible
|
|
117
|
+
* Returns a restore function to bring dialogs back
|
|
118
|
+
*/
|
|
119
|
+
export function hideDialogsTemporarily(doc: Document): () => void {
|
|
120
|
+
const dialogs = getOpenDialogs(doc);
|
|
121
|
+
const dialogStates: Array<{ dialog: HTMLDialogElement; wasOpen: boolean }> = [];
|
|
122
|
+
|
|
123
|
+
dialogs.forEach(dialog => {
|
|
124
|
+
if (dialog.open) {
|
|
125
|
+
dialogStates.push({ dialog, wasOpen: true });
|
|
126
|
+
dialog.close();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Return restore function
|
|
131
|
+
return () => {
|
|
132
|
+
dialogStates.forEach(({ dialog, wasOpen }) => {
|
|
133
|
+
if (wasOpen && !dialog.open) {
|
|
134
|
+
try {
|
|
135
|
+
dialog.showModal();
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.warn('Failed to restore dialog:', e);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Sets up canvas with proper z-index handling
|
|
146
|
+
*
|
|
147
|
+
* @param canvas - The canvas element
|
|
148
|
+
* @param options - Layer options
|
|
149
|
+
* @returns Cleanup function
|
|
150
|
+
*/
|
|
151
|
+
export function setupCanvasLayer(
|
|
152
|
+
canvas: HTMLCanvasElement,
|
|
153
|
+
options: CanvasLayerOptions = { strategy: 'max-z-index' }
|
|
154
|
+
): () => void {
|
|
155
|
+
switch (options.strategy) {
|
|
156
|
+
case 'hide-dialogs':
|
|
157
|
+
// Hide dialogs when canvas is active
|
|
158
|
+
return hideDialogsTemporarily(canvas.ownerDocument);
|
|
159
|
+
|
|
160
|
+
case 'popover':
|
|
161
|
+
// Use Popover API if available (also uses top-layer)
|
|
162
|
+
if ('popover' in canvas) {
|
|
163
|
+
try {
|
|
164
|
+
(canvas as any).popover = 'manual';
|
|
165
|
+
(canvas as any).showPopover();
|
|
166
|
+
return () => {
|
|
167
|
+
try {
|
|
168
|
+
(canvas as any).hidePopover();
|
|
169
|
+
} catch (e) {
|
|
170
|
+
// Ignore
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.warn('Popover API not available, falling back to max z-index');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Fallback to max-z-index
|
|
178
|
+
return () => {};
|
|
179
|
+
|
|
180
|
+
case 'max-z-index':
|
|
181
|
+
default:
|
|
182
|
+
// Use maximum z-index (works for non-modal content)
|
|
183
|
+
// Modal dialogs will still be on top due to top-layer
|
|
184
|
+
return () => {};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Synchronizes canvas content from iframe to parent window canvas
|
|
192
|
+
*
|
|
193
|
+
* @param iframeCanvas - Canvas inside iframe
|
|
194
|
+
* @param parentCanvas - Canvas in parent window
|
|
195
|
+
*/
|
|
196
|
+
export function syncCanvasContent(
|
|
197
|
+
iframeCanvas: HTMLCanvasElement,
|
|
198
|
+
parentCanvas: HTMLCanvasElement
|
|
199
|
+
): void {
|
|
200
|
+
const ctx = parentCanvas.getContext('2d');
|
|
201
|
+
if (ctx) {
|
|
202
|
+
ctx.clearRect(0, 0, parentCanvas.width, parentCanvas.height);
|
|
203
|
+
ctx.drawImage(iframeCanvas, 0, 0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Alternative approach: Make dialogs "transparent" for rendering
|
|
209
|
+
* Sets pointer-events: none on dialogs so canvas can render on top
|
|
210
|
+
*/
|
|
211
|
+
export function makeDialogsTransparent(doc: Document): () => void {
|
|
212
|
+
const dialogs = Array.from(doc.querySelectorAll('dialog[open]')) as HTMLDialogElement[];
|
|
213
|
+
const originalStyles: Map<HTMLDialogElement, string> = new Map();
|
|
214
|
+
|
|
215
|
+
dialogs.forEach((dialog) => {
|
|
216
|
+
originalStyles.set(dialog, dialog.style.pointerEvents || '');
|
|
217
|
+
dialog.style.pointerEvents = 'none';
|
|
218
|
+
dialog.style.zIndex = '2147483646'; // Just below canvas
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Return restore function
|
|
222
|
+
return () => {
|
|
223
|
+
dialogs.forEach((dialog) => {
|
|
224
|
+
const original = originalStyles.get(dialog);
|
|
225
|
+
if (original !== undefined) {
|
|
226
|
+
dialog.style.pointerEvents = original;
|
|
227
|
+
dialog.style.zIndex = '';
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Solution 3: Lower dialog z-index temporarily
|
|
235
|
+
* Moves dialog out of top-layer by closing and re-showing as non-modal
|
|
236
|
+
*/
|
|
237
|
+
export function lowerDialogZIndex(doc: Document): () => void {
|
|
238
|
+
const dialogs = getOpenDialogs(doc);
|
|
239
|
+
const dialogStates: Array<{ dialog: HTMLDialogElement; wasModal: boolean }> = [];
|
|
240
|
+
|
|
241
|
+
dialogs.forEach((dialog) => {
|
|
242
|
+
const wasModal = true; // If it's in getOpenDialogs, it's modal
|
|
243
|
+
dialogStates.push({ dialog, wasModal });
|
|
244
|
+
|
|
245
|
+
// Close modal and reopen as non-modal (removes from top-layer)
|
|
246
|
+
dialog.close();
|
|
247
|
+
dialog.show(); // Non-modal, can be controlled by z-index
|
|
248
|
+
dialog.style.zIndex = '2147483646'; // Below canvas
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Return restore function
|
|
252
|
+
return () => {
|
|
253
|
+
dialogStates.forEach(({ dialog, wasModal }) => {
|
|
254
|
+
if (wasModal) {
|
|
255
|
+
dialog.close();
|
|
256
|
+
try {
|
|
257
|
+
dialog.showModal(); // Restore to top-layer
|
|
258
|
+
dialog.style.zIndex = '';
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.warn('Failed to restore modal dialog:', e);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Solution 4: Use mix-blend-mode to render canvas "through" dialog
|
|
269
|
+
* This is experimental and may not work in all browsers
|
|
270
|
+
*/
|
|
271
|
+
export function setupCanvasBlendMode(canvas: HTMLCanvasElement): void {
|
|
272
|
+
canvas.style.mixBlendMode = 'multiply';
|
|
273
|
+
canvas.style.isolation = 'isolate';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Master function: Apply best strategy based on context
|
|
278
|
+
* Tries multiple approaches in order of preference
|
|
279
|
+
*/
|
|
280
|
+
export function applyBestCanvasStrategy(
|
|
281
|
+
doc: Document,
|
|
282
|
+
canvasId: string,
|
|
283
|
+
strategy?: 'parent-window' | 'lower-dialog' | 'transparent' | 'auto'
|
|
284
|
+
): { canvas: HTMLCanvasElement | null; cleanup: () => void } | null {
|
|
285
|
+
const selectedStrategy = strategy || 'auto';
|
|
286
|
+
|
|
287
|
+
// Strategy 1: Parent window canvas (best for iframe contexts)
|
|
288
|
+
if (selectedStrategy === 'auto' || selectedStrategy === 'parent-window') {
|
|
289
|
+
const parentCanvas = createParentWindowCanvas(doc);
|
|
290
|
+
if (parentCanvas) {
|
|
291
|
+
return {
|
|
292
|
+
canvas: parentCanvas.canvas,
|
|
293
|
+
cleanup: parentCanvas.cleanup
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Strategy 2: Lower dialog z-index (converts modal to non-modal temporarily)
|
|
299
|
+
if (selectedStrategy === 'auto' || selectedStrategy === 'lower-dialog') {
|
|
300
|
+
const canvas = doc.getElementById(canvasId) as HTMLCanvasElement;
|
|
301
|
+
if (canvas) {
|
|
302
|
+
const restore = lowerDialogZIndex(doc);
|
|
303
|
+
return {
|
|
304
|
+
canvas,
|
|
305
|
+
cleanup: restore
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Strategy 3: Make dialogs transparent (least intrusive)
|
|
311
|
+
if (selectedStrategy === 'auto' || selectedStrategy === 'transparent') {
|
|
312
|
+
const canvas = doc.getElementById(canvasId) as HTMLCanvasElement;
|
|
313
|
+
if (canvas) {
|
|
314
|
+
const restore = makeDialogsTransparent(doc);
|
|
315
|
+
return {
|
|
316
|
+
canvas,
|
|
317
|
+
cleanup: restore
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Applies canvas layer with dialog awareness
|
|
327
|
+
* This is the recommended approach: hide dialogs temporarily while heatmap is active
|
|
328
|
+
*/
|
|
329
|
+
export function applyCanvasLayerWithDialogAwareness(
|
|
330
|
+
canvas: HTMLCanvasElement,
|
|
331
|
+
onShow?: () => void,
|
|
332
|
+
onHide?: () => void
|
|
333
|
+
): { show: () => void; hide: () => void } {
|
|
334
|
+
let restoreDialogs: (() => void) | null = null;
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
show: () => {
|
|
338
|
+
// Hide dialogs when showing canvas
|
|
339
|
+
restoreDialogs = hideDialogsTemporarily(canvas.ownerDocument);
|
|
340
|
+
if (onShow) onShow();
|
|
341
|
+
},
|
|
342
|
+
hide: () => {
|
|
343
|
+
// Restore dialogs when hiding canvas
|
|
344
|
+
if (restoreDialogs) {
|
|
345
|
+
restoreDialogs();
|
|
346
|
+
restoreDialogs = null;
|
|
347
|
+
}
|
|
348
|
+
if (onHide) onHide();
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
package/src/custom/dialog.ts
CHANGED
|
@@ -11,6 +11,47 @@ export interface DialogRenderOptions {
|
|
|
11
11
|
isExistingDialog: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Pending dialogs tracking for synchronization
|
|
16
|
+
* Used to track when all dialogs have finished rendering
|
|
17
|
+
* Uses Set to avoid counting the same dialog multiple times
|
|
18
|
+
*/
|
|
19
|
+
let pendingDialogs: Set<HTMLDialogElement> = new Set();
|
|
20
|
+
let resolveDialogsRendered: (() => void) | null = null;
|
|
21
|
+
let dialogsRenderedPromise: Promise<void> | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns a promise that resolves when all pending dialogs are rendered
|
|
25
|
+
*/
|
|
26
|
+
export function waitForDialogsRendered(): Promise<void> {
|
|
27
|
+
if (pendingDialogs.size === 0) {
|
|
28
|
+
// No pending dialogs, resolve immediately
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Return existing promise if already waiting
|
|
33
|
+
if (dialogsRenderedPromise) {
|
|
34
|
+
return dialogsRenderedPromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create new promise
|
|
38
|
+
dialogsRenderedPromise = new Promise<void>((resolve) => {
|
|
39
|
+
resolveDialogsRendered = resolve;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return dialogsRenderedPromise;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resets the dialog rendering state
|
|
47
|
+
* Should be called before starting a new render cycle
|
|
48
|
+
*/
|
|
49
|
+
export function resetDialogRenderState(): void {
|
|
50
|
+
pendingDialogs.clear();
|
|
51
|
+
resolveDialogsRendered = null;
|
|
52
|
+
dialogsRenderedPromise = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
14
55
|
/**
|
|
15
56
|
* Extracts dialog rendering options from node attributes
|
|
16
57
|
*
|
|
@@ -45,27 +86,101 @@ export function showDialog(
|
|
|
45
86
|
): void {
|
|
46
87
|
try {
|
|
47
88
|
if (!dialogElement.isConnected) {
|
|
48
|
-
|
|
89
|
+
notifyDialogComplete(dialogElement);
|
|
49
90
|
return;
|
|
50
91
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// and dialog enters top-layer properly
|
|
55
|
-
if (dialogElement.open) {
|
|
56
|
-
dialogElement.close();
|
|
92
|
+
if (!dialogElement.open) {
|
|
93
|
+
notifyDialogComplete(dialogElement);
|
|
94
|
+
return;
|
|
57
95
|
}
|
|
58
96
|
|
|
97
|
+
dialogElement.close();
|
|
59
98
|
if (isModal) {
|
|
60
99
|
dialogElement.showModal();
|
|
61
100
|
} else {
|
|
62
101
|
dialogElement.show();
|
|
63
102
|
}
|
|
103
|
+
|
|
104
|
+
// Wait for animations/transitions to complete
|
|
105
|
+
waitForDialogAnimation(dialogElement).then(() => {
|
|
106
|
+
notifyDialogComplete(dialogElement);
|
|
107
|
+
});
|
|
64
108
|
} catch (e) {
|
|
65
109
|
console.error('❌ Error showing dialog:', e);
|
|
66
110
|
if (onError) {
|
|
67
111
|
onError(e as Error);
|
|
68
112
|
}
|
|
113
|
+
// Still mark as complete even on error
|
|
114
|
+
notifyDialogComplete(dialogElement);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Waits for dialog animations/transitions to complete
|
|
120
|
+
* Listens for animationend and transitionend events with timeout fallback
|
|
121
|
+
*/
|
|
122
|
+
function waitForDialogAnimation(dialogElement: HTMLDialogElement): Promise<void> {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
let resolved = false;
|
|
125
|
+
const timeoutMs = 1000; // Maximum wait time for animations
|
|
126
|
+
|
|
127
|
+
const complete = () => {
|
|
128
|
+
if (resolved) return;
|
|
129
|
+
resolved = true;
|
|
130
|
+
cleanup();
|
|
131
|
+
resolve();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Fallback timeout in case no animation events fire
|
|
135
|
+
const timeout = setTimeout(complete, timeoutMs);
|
|
136
|
+
|
|
137
|
+
// Listen for animation end
|
|
138
|
+
const onAnimationEnd = (e: AnimationEvent) => {
|
|
139
|
+
if (e.target === dialogElement) {
|
|
140
|
+
complete();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Listen for transition end
|
|
145
|
+
const onTransitionEnd = (TransitionEvent: TransitionEvent) => {
|
|
146
|
+
if (TransitionEvent.target === dialogElement) {
|
|
147
|
+
complete();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
clearTimeout(timeout);
|
|
153
|
+
dialogElement.removeEventListener('animationend', onAnimationEnd as EventListener);
|
|
154
|
+
dialogElement.removeEventListener('transitionend', onTransitionEnd as EventListener);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
dialogElement.addEventListener('animationend', onAnimationEnd as EventListener, { once: true });
|
|
158
|
+
dialogElement.addEventListener('transitionend', onTransitionEnd as EventListener, { once: true });
|
|
159
|
+
|
|
160
|
+
// If no animations/transitions, resolve after one frame
|
|
161
|
+
requestAnimationFrame(() => {
|
|
162
|
+
const computedStyle = window.getComputedStyle(dialogElement);
|
|
163
|
+
const hasAnimation = computedStyle.animationName !== 'none';
|
|
164
|
+
const hasTransition = computedStyle.transitionDuration !== '0s';
|
|
165
|
+
|
|
166
|
+
if (!hasAnimation && !hasTransition) {
|
|
167
|
+
complete();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Internal: Notify that a dialog has completed rendering
|
|
175
|
+
*/
|
|
176
|
+
function notifyDialogComplete(dialogElement: HTMLDialogElement): void {
|
|
177
|
+
pendingDialogs.delete(dialogElement);
|
|
178
|
+
|
|
179
|
+
// If all dialogs are done, resolve the promise
|
|
180
|
+
if (pendingDialogs.size === 0 && resolveDialogsRendered) {
|
|
181
|
+
resolveDialogsRendered();
|
|
182
|
+
resolveDialogsRendered = null;
|
|
183
|
+
dialogsRenderedPromise = null;
|
|
69
184
|
}
|
|
70
185
|
}
|
|
71
186
|
|
|
@@ -98,20 +213,16 @@ export function renderDialog(
|
|
|
98
213
|
onError?: (error: Error) => void
|
|
99
214
|
): void {
|
|
100
215
|
const { isModal, shouldBeOpen, isExistingDialog } = options;
|
|
216
|
+
if (!shouldBeOpen) return;
|
|
101
217
|
|
|
102
|
-
|
|
103
|
-
|
|
218
|
+
// Add dialog to pending set (Set automatically handles duplicates)
|
|
219
|
+
pendingDialogs.add(dialogElement);
|
|
104
220
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
setTimeout(doShow, 0);
|
|
111
|
-
}
|
|
112
|
-
} else if (dialogElement.open) {
|
|
113
|
-
// Dialog should be closed
|
|
114
|
-
closeDialog(dialogElement);
|
|
221
|
+
const doShow = () => showDialog(dialogElement, isModal, onError);
|
|
222
|
+
if (isExistingDialog) {
|
|
223
|
+
doShow();
|
|
224
|
+
} else {
|
|
225
|
+
setTimeout(doShow, 0);
|
|
115
226
|
}
|
|
116
227
|
}
|
|
117
228
|
|
package/src/custom/index.ts
CHANGED