@gemx-dev/heatmap-react 3.5.33 → 3.5.35
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/dist/esm/components/Layout/ContentMetricBar.d.ts.map +1 -1
- package/dist/esm/components/Layout/ContentToolbar.d.ts.map +1 -1
- package/dist/esm/components/Layout/ContentTopBar.d.ts.map +1 -1
- package/dist/esm/components/Layout/HeatmapLayout.d.ts.map +1 -1
- package/dist/esm/components/VizDom/VizDomRenderer.d.ts.map +1 -1
- package/dist/esm/components/VizElement/DefaultRankBadges.d.ts +0 -3
- package/dist/esm/components/VizElement/DefaultRankBadges.d.ts.map +1 -1
- package/dist/esm/components/VizElement/ElementCallout.d.ts +5 -12
- package/dist/esm/components/VizElement/ElementCallout.d.ts.map +1 -1
- package/dist/esm/components/VizElement/ElementMissing.d.ts +6 -0
- package/dist/esm/components/VizElement/ElementMissing.d.ts.map +1 -0
- package/dist/esm/components/VizElement/ElementOverlay.d.ts +10 -0
- package/dist/esm/components/VizElement/ElementOverlay.d.ts.map +1 -0
- package/dist/esm/components/VizElement/HeatmapElements.d.ts +4 -8
- package/dist/esm/components/VizElement/HeatmapElements.d.ts.map +1 -1
- package/dist/esm/components/VizElement/VizElements.d.ts +1 -2
- package/dist/esm/components/VizElement/VizElements.d.ts.map +1 -1
- package/dist/esm/configs/style.d.ts +2 -0
- package/dist/esm/configs/style.d.ts.map +1 -1
- package/dist/esm/constants/index.d.ts +4 -4
- package/dist/esm/constants/index.d.ts.map +1 -1
- package/dist/esm/helpers/elm-callout.d.ts +9 -0
- package/dist/esm/helpers/elm-callout.d.ts.map +1 -0
- package/dist/esm/helpers/elm-getter.d.ts +6 -0
- package/dist/esm/helpers/elm-getter.d.ts.map +1 -0
- package/dist/esm/helpers/index.d.ts +3 -0
- package/dist/esm/helpers/index.d.ts.map +1 -1
- package/dist/esm/helpers/viz-elements.d.ts +1 -2
- package/dist/esm/helpers/viz-elements.d.ts.map +1 -1
- package/dist/esm/hooks/register/useRegisterControl.d.ts.map +1 -1
- package/dist/esm/hooks/vix-elements/index.d.ts +1 -0
- package/dist/esm/hooks/vix-elements/index.d.ts.map +1 -1
- package/dist/esm/hooks/vix-elements/useClickedElement.d.ts +4 -4
- package/dist/esm/hooks/vix-elements/useClickedElement.d.ts.map +1 -1
- package/dist/esm/hooks/vix-elements/useElementCalloutVisible.d.ts +7 -0
- package/dist/esm/hooks/vix-elements/useElementCalloutVisible.d.ts.map +1 -0
- package/dist/esm/hooks/vix-elements/useHeatmapElementPosition.d.ts +2 -5
- package/dist/esm/hooks/vix-elements/useHeatmapElementPosition.d.ts.map +1 -1
- package/dist/esm/hooks/vix-elements/useHoveredElement.d.ts +1 -5
- package/dist/esm/hooks/vix-elements/useHoveredElement.d.ts.map +1 -1
- package/dist/esm/hooks/viz-render/useHeatmapRender.d.ts.map +1 -1
- package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts +0 -2
- package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts.map +1 -1
- package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts +1 -4
- package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
- package/dist/esm/hooks/viz-scale/useScrollSync.d.ts +1 -2
- package/dist/esm/hooks/viz-scale/useScrollSync.d.ts.map +1 -1
- package/dist/esm/index.js +552 -410
- package/dist/esm/index.mjs +552 -410
- package/dist/esm/stores/comp.d.ts.map +1 -1
- package/dist/esm/stores/interaction.d.ts +2 -0
- package/dist/esm/stores/interaction.d.ts.map +1 -1
- package/dist/esm/types/control.d.ts +3 -0
- package/dist/esm/types/control.d.ts.map +1 -1
- package/dist/esm/types/elm-callout.d.ts +9 -0
- package/dist/esm/types/elm-callout.d.ts.map +1 -0
- package/dist/esm/types/index.d.ts +1 -0
- package/dist/esm/types/index.d.ts.map +1 -1
- package/dist/esm/types/viz-element.d.ts +7 -5
- package/dist/esm/types/viz-element.d.ts.map +1 -1
- package/dist/esm/ui/BoxStack/BoxStack.d.ts +11 -1
- package/dist/esm/ui/BoxStack/BoxStack.d.ts.map +1 -1
- package/dist/esm/utils/debounce.d.ts +2 -0
- package/dist/esm/utils/debounce.d.ts.map +1 -0
- package/dist/style.css +42 -8
- package/dist/umd/components/Layout/ContentMetricBar.d.ts.map +1 -1
- package/dist/umd/components/Layout/ContentToolbar.d.ts.map +1 -1
- package/dist/umd/components/Layout/ContentTopBar.d.ts.map +1 -1
- package/dist/umd/components/Layout/HeatmapLayout.d.ts.map +1 -1
- package/dist/umd/components/VizDom/VizDomRenderer.d.ts.map +1 -1
- package/dist/umd/components/VizElement/DefaultRankBadges.d.ts +0 -3
- package/dist/umd/components/VizElement/DefaultRankBadges.d.ts.map +1 -1
- package/dist/umd/components/VizElement/ElementCallout.d.ts +5 -12
- package/dist/umd/components/VizElement/ElementCallout.d.ts.map +1 -1
- package/dist/umd/components/VizElement/ElementMissing.d.ts +6 -0
- package/dist/umd/components/VizElement/ElementMissing.d.ts.map +1 -0
- package/dist/umd/components/VizElement/ElementOverlay.d.ts +10 -0
- package/dist/umd/components/VizElement/ElementOverlay.d.ts.map +1 -0
- package/dist/umd/components/VizElement/HeatmapElements.d.ts +4 -8
- package/dist/umd/components/VizElement/HeatmapElements.d.ts.map +1 -1
- package/dist/umd/components/VizElement/VizElements.d.ts +1 -2
- package/dist/umd/components/VizElement/VizElements.d.ts.map +1 -1
- package/dist/umd/configs/style.d.ts +2 -0
- package/dist/umd/configs/style.d.ts.map +1 -1
- package/dist/umd/constants/index.d.ts +4 -4
- package/dist/umd/constants/index.d.ts.map +1 -1
- package/dist/umd/helpers/elm-callout.d.ts +9 -0
- package/dist/umd/helpers/elm-callout.d.ts.map +1 -0
- package/dist/umd/helpers/elm-getter.d.ts +6 -0
- package/dist/umd/helpers/elm-getter.d.ts.map +1 -0
- package/dist/umd/helpers/index.d.ts +3 -0
- package/dist/umd/helpers/index.d.ts.map +1 -1
- package/dist/umd/helpers/viz-elements.d.ts +1 -2
- package/dist/umd/helpers/viz-elements.d.ts.map +1 -1
- package/dist/umd/hooks/register/useRegisterControl.d.ts.map +1 -1
- package/dist/umd/hooks/vix-elements/index.d.ts +1 -0
- package/dist/umd/hooks/vix-elements/index.d.ts.map +1 -1
- package/dist/umd/hooks/vix-elements/useClickedElement.d.ts +4 -4
- package/dist/umd/hooks/vix-elements/useClickedElement.d.ts.map +1 -1
- package/dist/umd/hooks/vix-elements/useElementCalloutVisible.d.ts +7 -0
- package/dist/umd/hooks/vix-elements/useElementCalloutVisible.d.ts.map +1 -0
- package/dist/umd/hooks/vix-elements/useHeatmapElementPosition.d.ts +2 -5
- package/dist/umd/hooks/vix-elements/useHeatmapElementPosition.d.ts.map +1 -1
- package/dist/umd/hooks/vix-elements/useHoveredElement.d.ts +1 -5
- package/dist/umd/hooks/vix-elements/useHoveredElement.d.ts.map +1 -1
- package/dist/umd/hooks/viz-render/useHeatmapRender.d.ts.map +1 -1
- package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts +0 -2
- package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts.map +1 -1
- package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts +1 -4
- package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
- package/dist/umd/hooks/viz-scale/useScrollSync.d.ts +1 -2
- package/dist/umd/hooks/viz-scale/useScrollSync.d.ts.map +1 -1
- package/dist/umd/index.js +2 -2
- package/dist/umd/stores/comp.d.ts.map +1 -1
- package/dist/umd/stores/interaction.d.ts +2 -0
- package/dist/umd/stores/interaction.d.ts.map +1 -1
- package/dist/umd/types/control.d.ts +3 -0
- package/dist/umd/types/control.d.ts.map +1 -1
- package/dist/umd/types/elm-callout.d.ts +9 -0
- package/dist/umd/types/elm-callout.d.ts.map +1 -0
- package/dist/umd/types/index.d.ts +1 -0
- package/dist/umd/types/index.d.ts.map +1 -1
- package/dist/umd/types/viz-element.d.ts +7 -5
- package/dist/umd/types/viz-element.d.ts.map +1 -1
- package/dist/umd/ui/BoxStack/BoxStack.d.ts +11 -1
- package/dist/umd/ui/BoxStack/BoxStack.d.ts.map +1 -1
- package/dist/umd/utils/debounce.d.ts +2 -0
- package/dist/umd/utils/debounce.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/esm/components/VizElement/ClickedElementOverlay.d.ts +0 -17
- package/dist/esm/components/VizElement/ClickedElementOverlay.d.ts.map +0 -1
- package/dist/esm/components/VizElement/HoveredElementOverlay.d.ts +0 -12
- package/dist/esm/components/VizElement/HoveredElementOverlay.d.ts.map +0 -1
- package/dist/esm/components/VizElement/MissingElementMessage.d.ts +0 -7
- package/dist/esm/components/VizElement/MissingElementMessage.d.ts.map +0 -1
- package/dist/umd/components/VizElement/ClickedElementOverlay.d.ts +0 -17
- package/dist/umd/components/VizElement/ClickedElementOverlay.d.ts.map +0 -1
- package/dist/umd/components/VizElement/HoveredElementOverlay.d.ts +0 -12
- package/dist/umd/components/VizElement/HoveredElementOverlay.d.ts.map +0 -1
- package/dist/umd/components/VizElement/MissingElementMessage.d.ts +0 -7
- package/dist/umd/components/VizElement/MissingElementMessage.d.ts.map +0 -1
package/dist/esm/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
3
3
|
import { useNodesState, ReactFlow, Controls, Background } from '@xyflow/react';
|
|
4
|
-
import { useEffect, useCallback, useState, useRef, useMemo, Fragment as Fragment$1 } from 'react';
|
|
4
|
+
import { useEffect, useCallback, useState, useRef, useMemo, forwardRef, Fragment as Fragment$1 } from 'react';
|
|
5
5
|
import { create } from 'zustand';
|
|
6
6
|
import { Visualizer } from '@gemx-dev/clarity-visualize';
|
|
7
7
|
import { createPortal } from 'react-dom';
|
|
@@ -49,7 +49,9 @@ const HEATMAP_IFRAME = {
|
|
|
49
49
|
const HEATMAP_CONFIG = {
|
|
50
50
|
padding: 8,
|
|
51
51
|
borderWidth: 1,
|
|
52
|
+
borderWidthIframe: 2,
|
|
52
53
|
borderColor: '#E3E3E3',
|
|
54
|
+
heightToolbar: 60, // height of the toolbar
|
|
53
55
|
};
|
|
54
56
|
const HEATMAP_STYLE = {
|
|
55
57
|
viz: {
|
|
@@ -70,6 +72,7 @@ const useHeatmapControlStore = create()((set, get) => {
|
|
|
70
72
|
Toolbar: null,
|
|
71
73
|
MetricBar: null,
|
|
72
74
|
VizLoading: null,
|
|
75
|
+
ElementCallout: null,
|
|
73
76
|
},
|
|
74
77
|
registerControl: (key, control) => {
|
|
75
78
|
set({
|
|
@@ -118,6 +121,8 @@ const useHeatmapInteractionStore = create()((set, get) => {
|
|
|
118
121
|
setSelectedElement: (selectedElement) => set({ selectedElement }),
|
|
119
122
|
hoveredElement: null,
|
|
120
123
|
setHoveredElement: (hoveredElement) => set({ hoveredElement }),
|
|
124
|
+
shouldShowCallout: false,
|
|
125
|
+
setShouldShowCallout: (shouldShowCallout) => set({ shouldShowCallout }),
|
|
121
126
|
};
|
|
122
127
|
});
|
|
123
128
|
|
|
@@ -150,6 +155,7 @@ const useRegisterControl = (control) => {
|
|
|
150
155
|
registerControl('Toolbar', control.Toolbar);
|
|
151
156
|
registerControl('MetricBar', control.MetricBar);
|
|
152
157
|
registerControl('VizLoading', control.VizLoading);
|
|
158
|
+
registerControl('ElementCallout', control.ElementCallout);
|
|
153
159
|
};
|
|
154
160
|
|
|
155
161
|
const useRegisterData = (data, dataInfo) => {
|
|
@@ -187,17 +193,282 @@ const useRegisterHeatmap = (clickmap) => {
|
|
|
187
193
|
}, [clickmap]);
|
|
188
194
|
};
|
|
189
195
|
|
|
190
|
-
const
|
|
196
|
+
const PADDING = 0;
|
|
197
|
+
const ARROW_SIZE = 8;
|
|
198
|
+
const HORIZONTAL_OFFSET = 0;
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// Viewport & Dimensions
|
|
201
|
+
// ============================================================================
|
|
202
|
+
const getViewportDimensions = () => ({
|
|
203
|
+
width: window.innerWidth,
|
|
204
|
+
height: window.innerHeight,
|
|
205
|
+
});
|
|
206
|
+
const getElementDimensions = (targetElm, calloutElm) => ({
|
|
207
|
+
targetRect: targetElm.getBoundingClientRect(),
|
|
208
|
+
calloutRect: calloutElm.getBoundingClientRect(),
|
|
209
|
+
});
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Alignment Order
|
|
212
|
+
// ============================================================================
|
|
213
|
+
const getAlignmentOrder = (alignment) => {
|
|
214
|
+
switch (alignment) {
|
|
215
|
+
case 'center':
|
|
216
|
+
return ['center', 'left', 'right'];
|
|
217
|
+
case 'left':
|
|
218
|
+
return ['left', 'center', 'right'];
|
|
219
|
+
case 'right':
|
|
220
|
+
return ['right', 'center', 'left'];
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// Position Calculation
|
|
225
|
+
// ============================================================================
|
|
226
|
+
const calculateLeftPosition = ({ targetRect, calloutRect, hozOffset, align, }) => {
|
|
227
|
+
switch (align) {
|
|
228
|
+
case 'left':
|
|
229
|
+
return targetRect.left + hozOffset;
|
|
230
|
+
case 'right':
|
|
231
|
+
return targetRect.right - calloutRect.width - hozOffset;
|
|
232
|
+
case 'center':
|
|
233
|
+
default:
|
|
234
|
+
return targetRect.left + targetRect.width / 2 - calloutRect.width / 2;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const calculateVerticalPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
|
|
238
|
+
return placement === 'top'
|
|
239
|
+
? targetRect.top - calloutRect.height - padding - arrowSize
|
|
240
|
+
: targetRect.bottom + padding + arrowSize;
|
|
241
|
+
};
|
|
242
|
+
const calculateHorizontalPlacementPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
|
|
243
|
+
const top = targetRect.top + targetRect.height / 2 - calloutRect.height / 2;
|
|
244
|
+
const left = placement === 'right'
|
|
245
|
+
? targetRect.right + padding + arrowSize
|
|
246
|
+
: targetRect.left - calloutRect.width - padding - arrowSize;
|
|
247
|
+
return { top, left };
|
|
248
|
+
};
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Validation
|
|
251
|
+
// ============================================================================
|
|
252
|
+
const isLeftPositionValid = (leftPos, calloutWidth, viewportWidth, padding) => {
|
|
253
|
+
return leftPos >= padding && leftPos + calloutWidth <= viewportWidth - padding;
|
|
254
|
+
};
|
|
255
|
+
const isVerticalPositionValid = (targetRect, calloutRect, placement, viewportHeight, padding, arrowSize) => {
|
|
256
|
+
return placement === 'top'
|
|
257
|
+
? targetRect.top - calloutRect.height - padding - arrowSize > 0
|
|
258
|
+
: targetRect.bottom + calloutRect.height + padding + arrowSize < viewportHeight;
|
|
259
|
+
};
|
|
260
|
+
const isHorizontalPlacementValid = (targetRect, calloutRect, placement, viewportWidth, padding, arrowSize) => {
|
|
261
|
+
return placement === 'right'
|
|
262
|
+
? targetRect.right + calloutRect.width + padding + arrowSize < viewportWidth
|
|
263
|
+
: targetRect.left - calloutRect.width - padding - arrowSize > 0;
|
|
264
|
+
};
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// Position Candidates Generation
|
|
267
|
+
// ============================================================================
|
|
268
|
+
const generateVerticalPositionCandidates = (targetRect, calloutRect, viewportHeight, viewportWidth, alignment, hozOffset, padding, arrowSize) => {
|
|
269
|
+
const candidates = [];
|
|
270
|
+
const placements = ['top', 'bottom'];
|
|
271
|
+
placements.forEach((placement) => {
|
|
272
|
+
const verticalPos = calculateVerticalPosition(targetRect, calloutRect, placement, padding, arrowSize);
|
|
273
|
+
const verticalValid = isVerticalPositionValid(targetRect, calloutRect, placement, viewportHeight, padding, arrowSize);
|
|
274
|
+
const alignmentOrder = getAlignmentOrder(alignment);
|
|
275
|
+
alignmentOrder.forEach((align) => {
|
|
276
|
+
const horizontalPos = calculateLeftPosition({
|
|
277
|
+
targetRect,
|
|
278
|
+
calloutRect,
|
|
279
|
+
hozOffset,
|
|
280
|
+
align,
|
|
281
|
+
});
|
|
282
|
+
candidates.push({
|
|
283
|
+
placement,
|
|
284
|
+
top: verticalPos,
|
|
285
|
+
left: horizontalPos,
|
|
286
|
+
horizontalAlign: align,
|
|
287
|
+
valid: verticalValid &&
|
|
288
|
+
isLeftPositionValid(horizontalPos, calloutRect.width, viewportWidth, padding),
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
return candidates;
|
|
293
|
+
};
|
|
294
|
+
const generateHorizontalPositionCandidates = (targetRect, calloutRect, viewportWidth, padding, arrowSize) => {
|
|
295
|
+
const placements = ['left', 'right'];
|
|
296
|
+
return placements.map((placement) => {
|
|
297
|
+
const { top, left } = calculateHorizontalPlacementPosition(targetRect, calloutRect, placement, padding, arrowSize);
|
|
298
|
+
return {
|
|
299
|
+
placement,
|
|
300
|
+
top,
|
|
301
|
+
left,
|
|
302
|
+
horizontalAlign: 'center',
|
|
303
|
+
valid: isHorizontalPlacementValid(targetRect, calloutRect, placement, viewportWidth, padding, arrowSize),
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
const generateAllPositionCandidates = (rectDimensions, viewport, alignment, hozOffset, padding, arrowSize) => {
|
|
308
|
+
const { targetRect, calloutRect } = rectDimensions;
|
|
309
|
+
const verticalCandidates = generateVerticalPositionCandidates(targetRect, calloutRect, viewport.height, viewport.width, alignment, hozOffset, padding, arrowSize);
|
|
310
|
+
const horizontalCandidates = generateHorizontalPositionCandidates(targetRect, calloutRect, viewport.width, padding, arrowSize);
|
|
311
|
+
return [...verticalCandidates, ...horizontalCandidates];
|
|
312
|
+
};
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Position Selection
|
|
315
|
+
// ============================================================================
|
|
316
|
+
const selectBestPosition = (candidates) => {
|
|
317
|
+
return candidates.find((p) => p.valid) || candidates[0];
|
|
318
|
+
};
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// Viewport Boundary Adjustment
|
|
321
|
+
// ============================================================================
|
|
322
|
+
const constrainToViewport = (position, calloutRect, viewport, padding) => {
|
|
323
|
+
const left = Math.max(padding, Math.min(position.left, viewport.width - calloutRect.width - padding));
|
|
324
|
+
const top = Math.max(padding, Math.min(position.top, viewport.height - calloutRect.height - padding));
|
|
325
|
+
return { top, left };
|
|
326
|
+
};
|
|
327
|
+
// ============================================================================
|
|
328
|
+
// Main Function
|
|
329
|
+
// ============================================================================
|
|
330
|
+
const calcCalloutPosition = ({ targetElm, calloutElm, setPosition, hozOffset = HORIZONTAL_OFFSET, alignment = 'center', }) => {
|
|
331
|
+
return () => {
|
|
332
|
+
// 1. Get dimensions
|
|
333
|
+
const rectDimensions = getElementDimensions(targetElm, calloutElm);
|
|
334
|
+
const viewport = getViewportDimensions();
|
|
335
|
+
const padding = PADDING;
|
|
336
|
+
const arrowSize = ARROW_SIZE;
|
|
337
|
+
// 2. Generate all position candidates
|
|
338
|
+
const candidates = generateAllPositionCandidates(rectDimensions, viewport, alignment, hozOffset, padding, arrowSize);
|
|
339
|
+
// 3. Select best position
|
|
340
|
+
const bestPosition = selectBestPosition(candidates);
|
|
341
|
+
// 4. Constrain to viewport
|
|
342
|
+
const constrainedPosition = constrainToViewport({ top: bestPosition.top, left: bestPosition.left }, rectDimensions.calloutRect, viewport, padding);
|
|
343
|
+
// 5. Create final position object
|
|
344
|
+
const finalPosition = {
|
|
345
|
+
top: constrainedPosition.top,
|
|
346
|
+
left: constrainedPosition.left,
|
|
347
|
+
placement: bestPosition.placement,
|
|
348
|
+
horizontalAlign: bestPosition.horizontalAlign,
|
|
349
|
+
};
|
|
350
|
+
setPosition(finalPosition);
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
function getElementLayout(element) {
|
|
355
|
+
if (!element?.getBoundingClientRect)
|
|
356
|
+
return null;
|
|
357
|
+
const rect = element.getBoundingClientRect();
|
|
358
|
+
if (rect.width === 0 && rect.height === 0)
|
|
359
|
+
return null;
|
|
360
|
+
return {
|
|
361
|
+
top: rect.top,
|
|
362
|
+
left: rect.left,
|
|
363
|
+
width: rect.width,
|
|
364
|
+
height: rect.height,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const getElementAtPoint = (doc, x, y) => {
|
|
368
|
+
let el = null;
|
|
369
|
+
if ('caretPositionFromPoint' in doc) {
|
|
370
|
+
el = doc.caretPositionFromPoint(x, y)?.offsetNode ?? null;
|
|
371
|
+
}
|
|
372
|
+
el = el ?? doc.elementFromPoint(x, y);
|
|
373
|
+
let element = el;
|
|
374
|
+
while (element && element.nodeType === Node.TEXT_NODE) {
|
|
375
|
+
element = element.parentElement;
|
|
376
|
+
}
|
|
377
|
+
return element;
|
|
378
|
+
};
|
|
379
|
+
function getElementHash(element) {
|
|
380
|
+
return (element.getAttribute('data-clarity-hash') ||
|
|
381
|
+
element.getAttribute('data-clarity-hashalpha') ||
|
|
382
|
+
element.getAttribute('data-clarity-hashbeta'));
|
|
383
|
+
}
|
|
384
|
+
const getElementRank = (hash, elements) => {
|
|
385
|
+
if (!elements)
|
|
386
|
+
return 0;
|
|
387
|
+
return elements.findIndex((e) => e.hash === hash) + 1;
|
|
388
|
+
};
|
|
389
|
+
const buildElementInfo = (hash, rect, heatmapInfo) => {
|
|
390
|
+
if (!rect || !heatmapInfo)
|
|
391
|
+
return null;
|
|
392
|
+
const info = heatmapInfo.elementMapInfo?.[hash];
|
|
393
|
+
if (!info)
|
|
394
|
+
return null;
|
|
395
|
+
const rank = getElementRank(hash, heatmapInfo.sortedElements);
|
|
396
|
+
const clicks = info.totalclicks ?? 0;
|
|
397
|
+
const selector = info.selector ?? '';
|
|
398
|
+
const baseInfo = {
|
|
399
|
+
hash,
|
|
400
|
+
clicks,
|
|
401
|
+
rank,
|
|
402
|
+
selector,
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
...baseInfo,
|
|
406
|
+
...rect,
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
function calculateRankPosition(rect, widthScale) {
|
|
411
|
+
const top = rect.top <= 18 ? rect.top + 3 : rect.top - 18;
|
|
412
|
+
const left = rect.left <= 18 ? rect.left + 3 : rect.left - 18;
|
|
413
|
+
return {
|
|
414
|
+
transform: `scale(${1.2 * widthScale})`,
|
|
415
|
+
top: Number.isNaN(top) ? undefined : top,
|
|
416
|
+
left: Number.isNaN(left) ? undefined : left,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function isElementInViewport(elementRect, visualRef, scale) {
|
|
420
|
+
if (!elementRect)
|
|
421
|
+
return false;
|
|
422
|
+
const visualRect = visualRef.current?.getBoundingClientRect();
|
|
423
|
+
if (!visualRect)
|
|
424
|
+
return false;
|
|
425
|
+
// Element position relative to the document (or container's content)
|
|
426
|
+
const elementTop = elementRect.top * scale;
|
|
427
|
+
const elementBottom = (elementRect.top + elementRect.height) * scale;
|
|
428
|
+
// Current scroll position
|
|
429
|
+
const scrollTop = visualRef.current?.scrollTop || 0;
|
|
430
|
+
const viewportHeight = visualRect.height;
|
|
431
|
+
// Visible viewport range in the scrollable content
|
|
432
|
+
const viewportTop = scrollTop;
|
|
433
|
+
const viewportBottom = scrollTop + viewportHeight;
|
|
434
|
+
// Check if element is within the visible viewport
|
|
435
|
+
// Element is visible if it overlaps with the viewport
|
|
436
|
+
return elementBottom > viewportTop && elementTop < viewportBottom;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const scrollToElementIfNeeded = (visualRef, rect, scale) => {
|
|
440
|
+
if (!visualRef.current)
|
|
441
|
+
return;
|
|
442
|
+
const visualRect = visualRef.current.getBoundingClientRect();
|
|
443
|
+
if (isElementInViewport(rect, visualRef, scale)) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const topRaw = rect.top; // - visualRect.top
|
|
447
|
+
const topScaled = topRaw * scale;
|
|
448
|
+
const viewportHeight = visualRect.height;
|
|
449
|
+
const elementHeightScaled = rect.height * scale;
|
|
450
|
+
const scrollTop = topScaled - (viewportHeight - elementHeightScaled) / 2;
|
|
451
|
+
visualRef.current.scrollTo({
|
|
452
|
+
top: Math.max(0, scrollTop),
|
|
453
|
+
behavior: 'smooth',
|
|
454
|
+
});
|
|
455
|
+
};
|
|
456
|
+
const useClickedElement = ({ visualRef, getRect }) => {
|
|
457
|
+
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
191
458
|
const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
|
|
459
|
+
const shouldShowCallout = useHeatmapInteractionStore((state) => state.shouldShowCallout);
|
|
460
|
+
const setShouldShowCallout = useHeatmapInteractionStore((state) => state.setShouldShowCallout);
|
|
461
|
+
const scale = useHeatmapVizStore((state) => state.scale);
|
|
192
462
|
const [clickedElement, setClickedElement] = useState(null);
|
|
193
463
|
const [showMissingElement, setShowMissingElement] = useState(false);
|
|
194
|
-
const
|
|
464
|
+
const reset = () => {
|
|
465
|
+
setClickedElement(null);
|
|
466
|
+
setShowMissingElement(false);
|
|
467
|
+
setShouldShowCallout(false);
|
|
468
|
+
};
|
|
195
469
|
useEffect(() => {
|
|
196
|
-
console.log(`🚀 🐥 ~ useClickedElement ~ heatmapInfo?.elementMapInfo:`, heatmapInfo?.elementMapInfo);
|
|
197
470
|
if (!selectedElement || !heatmapInfo?.elementMapInfo) {
|
|
198
|
-
|
|
199
|
-
setShowMissingElement(false);
|
|
200
|
-
setShouldShowCallout(false);
|
|
471
|
+
reset();
|
|
201
472
|
return;
|
|
202
473
|
}
|
|
203
474
|
const info = heatmapInfo.elementMapInfo[selectedElement];
|
|
@@ -205,38 +476,58 @@ const useClickedElement = ({ heatmapInfo, getRect }) => {
|
|
|
205
476
|
setClickedElement(null);
|
|
206
477
|
return;
|
|
207
478
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
clicks: info.totalclicks ?? 0,
|
|
215
|
-
rank,
|
|
216
|
-
selector: info.selector ?? '',
|
|
217
|
-
});
|
|
218
|
-
setShowMissingElement(false);
|
|
219
|
-
setShouldShowCallout(true);
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
const rank = (heatmapInfo.sortedElements?.findIndex((e) => e.hash === selectedElement) ?? -1) + 1;
|
|
223
|
-
setClickedElement({
|
|
224
|
-
hash: selectedElement,
|
|
225
|
-
clicks: info.totalclicks ?? 0,
|
|
226
|
-
rank,
|
|
227
|
-
selector: info.selector ?? '',
|
|
228
|
-
left: 0,
|
|
229
|
-
top: 0,
|
|
230
|
-
width: 0,
|
|
231
|
-
height: 0,
|
|
232
|
-
});
|
|
479
|
+
const hash = selectedElement;
|
|
480
|
+
const selector = info.selector;
|
|
481
|
+
const rect = getRect({ hash: selectedElement, selector });
|
|
482
|
+
const elementInfo = buildElementInfo(hash, rect, heatmapInfo);
|
|
483
|
+
if (!rect) {
|
|
484
|
+
setClickedElement(elementInfo);
|
|
233
485
|
setShowMissingElement(true);
|
|
234
486
|
setShouldShowCallout(false);
|
|
487
|
+
return;
|
|
235
488
|
}
|
|
236
|
-
|
|
489
|
+
setShowMissingElement(false);
|
|
490
|
+
scrollToElementIfNeeded(visualRef, rect, scale);
|
|
491
|
+
setShouldShowCallout(true);
|
|
492
|
+
requestAnimationFrame(() => {
|
|
493
|
+
setClickedElement(elementInfo);
|
|
494
|
+
});
|
|
495
|
+
}, [selectedElement, heatmapInfo, getRect, visualRef, scale]);
|
|
237
496
|
return { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout };
|
|
238
497
|
};
|
|
239
498
|
|
|
499
|
+
const useElementCalloutVisible = ({ visualRef, getRect }) => {
|
|
500
|
+
const scale = useHeatmapVizStore((state) => state.scale);
|
|
501
|
+
const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
|
|
502
|
+
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
503
|
+
const setShouldShowCallout = useHeatmapInteractionStore((state) => state.setShouldShowCallout);
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
if (!selectedElement)
|
|
506
|
+
return;
|
|
507
|
+
const elementIsInViewportFn = () => {
|
|
508
|
+
const elementInfo = heatmapInfo?.elementMapInfo?.[selectedElement];
|
|
509
|
+
if (!elementInfo)
|
|
510
|
+
return;
|
|
511
|
+
const rect = getRect({ hash: selectedElement, selector: elementInfo.selector });
|
|
512
|
+
const isInViewport = isElementInViewport(rect, visualRef, scale);
|
|
513
|
+
setShouldShowCallout(isInViewport);
|
|
514
|
+
};
|
|
515
|
+
elementIsInViewportFn();
|
|
516
|
+
const handleUpdate = () => {
|
|
517
|
+
requestAnimationFrame(elementIsInViewportFn);
|
|
518
|
+
};
|
|
519
|
+
window.addEventListener('scroll', handleUpdate, true);
|
|
520
|
+
window.addEventListener('resize', handleUpdate);
|
|
521
|
+
visualRef?.current?.addEventListener('scroll', handleUpdate);
|
|
522
|
+
return () => {
|
|
523
|
+
window.removeEventListener('scroll', handleUpdate, true);
|
|
524
|
+
window.removeEventListener('resize', handleUpdate);
|
|
525
|
+
visualRef?.current?.removeEventListener('scroll', handleUpdate);
|
|
526
|
+
};
|
|
527
|
+
}, [selectedElement, visualRef, getRect, scale, heatmapInfo, setShouldShowCallout]);
|
|
528
|
+
return {};
|
|
529
|
+
};
|
|
530
|
+
|
|
240
531
|
const useHeatmapEffects = ({ isVisible, isElementSidebarOpen, setShouldShowCallout, resetAll, }) => {
|
|
241
532
|
const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
|
|
242
533
|
// Reset khi ẩn
|
|
@@ -255,34 +546,10 @@ const useHeatmapEffects = ({ isVisible, isElementSidebarOpen, setShouldShowCallo
|
|
|
255
546
|
}, [isElementSidebarOpen, selectedElement, setShouldShowCallout]);
|
|
256
547
|
};
|
|
257
548
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return null;
|
|
261
|
-
const rect = element.getBoundingClientRect();
|
|
262
|
-
if (rect.width === 0 && rect.height === 0)
|
|
263
|
-
return null;
|
|
264
|
-
return {
|
|
265
|
-
top: rect.top,
|
|
266
|
-
left: rect.left,
|
|
267
|
-
width: rect.width,
|
|
268
|
-
height: rect.height,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
function formatPercentage(value, decimals = 2) {
|
|
272
|
-
return value.toFixed(decimals);
|
|
273
|
-
}
|
|
274
|
-
function calculateRankPosition(rect, widthScale) {
|
|
275
|
-
const top = rect.top <= 18 ? rect.top + 3 : rect.top - 18;
|
|
276
|
-
const left = rect.left <= 18 ? rect.left + 3 : rect.left - 18;
|
|
277
|
-
return {
|
|
278
|
-
transform: `scale(${1.2 * widthScale})`,
|
|
279
|
-
top: Number.isNaN(top) ? undefined : top,
|
|
280
|
-
left: Number.isNaN(left) ? undefined : left,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWidth, widthScale, projectId, }) => {
|
|
549
|
+
const useHeatmapElementPosition = ({ iframeRef, wrapperRef, visualizer }) => {
|
|
550
|
+
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
285
551
|
const iframeHeight = useHeatmapVizStore((state) => state.iframeHeight);
|
|
552
|
+
const heatmapWidth = useHeatmapDataStore((state) => state.config?.width);
|
|
286
553
|
return useCallback((element) => {
|
|
287
554
|
const hash = element?.hash;
|
|
288
555
|
if (!iframeRef.current?.contentDocument || !hash || !visualizer)
|
|
@@ -292,7 +559,7 @@ const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWi
|
|
|
292
559
|
domElement = visualizer.get(hash);
|
|
293
560
|
}
|
|
294
561
|
catch (error) {
|
|
295
|
-
console.error('Visualizer error:', {
|
|
562
|
+
console.error('Visualizer error:', { hash, error });
|
|
296
563
|
return null;
|
|
297
564
|
}
|
|
298
565
|
if (!domElement)
|
|
@@ -300,7 +567,7 @@ const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWi
|
|
|
300
567
|
const layout = getElementLayout(domElement);
|
|
301
568
|
if (!layout)
|
|
302
569
|
return null;
|
|
303
|
-
const parentEl =
|
|
570
|
+
const parentEl = wrapperRef.current;
|
|
304
571
|
if (!parentEl)
|
|
305
572
|
return null;
|
|
306
573
|
const scrollOffset = parentEl.scrollTop / widthScale;
|
|
@@ -316,8 +583,9 @@ const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWi
|
|
|
316
583
|
top: adjustedTop,
|
|
317
584
|
width: Math.min(layout.width, heatmapWidth || layout.width),
|
|
318
585
|
height: layout.height,
|
|
586
|
+
outOfBounds,
|
|
319
587
|
};
|
|
320
|
-
}, [iframeRef,
|
|
588
|
+
}, [iframeRef, wrapperRef, visualizer, heatmapWidth, iframeHeight, widthScale]);
|
|
321
589
|
};
|
|
322
590
|
|
|
323
591
|
const debounce = (fn, delay) => {
|
|
@@ -327,68 +595,63 @@ const debounce = (fn, delay) => {
|
|
|
327
595
|
timeout = setTimeout(() => fn(...args), delay);
|
|
328
596
|
};
|
|
329
597
|
};
|
|
330
|
-
|
|
598
|
+
|
|
599
|
+
const useHoveredElement = ({ iframeRef, getRect }) => {
|
|
331
600
|
const hoveredElement = useHeatmapInteractionStore((state) => state.hoveredElement);
|
|
332
601
|
const setHoveredElement = useHeatmapInteractionStore((state) => state.setHoveredElement);
|
|
333
602
|
const onSelect = useHeatmapInteractionStore((state) => state.setSelectedElement);
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
603
|
+
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
604
|
+
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
605
|
+
const reset = useCallback(() => {
|
|
606
|
+
setHoveredElement(null);
|
|
607
|
+
}, [setHoveredElement]);
|
|
608
|
+
const handleMouseLeave = useCallback(() => {
|
|
609
|
+
reset();
|
|
610
|
+
}, [reset]);
|
|
611
|
+
const getHashFromEvent = useCallback((event) => {
|
|
612
|
+
if (!heatmapInfo || !isIframeReady(iframeRef, heatmapInfo)) {
|
|
613
|
+
reset();
|
|
337
614
|
return;
|
|
338
615
|
}
|
|
339
616
|
const iframe = iframeRef.current;
|
|
340
|
-
const iframeRect = iframe.getBoundingClientRect();
|
|
341
|
-
let x = event.clientX - iframeRect.left;
|
|
342
|
-
let y = event.clientY - iframeRect.top;
|
|
343
|
-
if (widthScale !== 1) {
|
|
344
|
-
x /= widthScale;
|
|
345
|
-
y /= widthScale;
|
|
346
|
-
}
|
|
347
617
|
const doc = iframe.contentDocument;
|
|
348
|
-
|
|
349
|
-
|
|
618
|
+
const iframeRect = iframe.getBoundingClientRect();
|
|
619
|
+
const { x, y } = convertViewportToIframeCoords(event.clientX, event.clientY, iframeRect, widthScale);
|
|
620
|
+
const targetElement = findTargetElement(doc, x, y);
|
|
621
|
+
if (!targetElement || !isValidElement(targetElement, heatmapInfo)) {
|
|
622
|
+
reset();
|
|
350
623
|
return;
|
|
351
624
|
}
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
625
|
+
const hash = getElementHash(targetElement);
|
|
626
|
+
if (!!hash)
|
|
627
|
+
return hash;
|
|
628
|
+
reset();
|
|
629
|
+
return;
|
|
630
|
+
}, [heatmapInfo, iframeRef, getRect, widthScale, reset]);
|
|
631
|
+
const handleMouseMove = useCallback(debounce((event) => {
|
|
632
|
+
if (!heatmapInfo) {
|
|
633
|
+
reset();
|
|
358
634
|
return;
|
|
359
635
|
}
|
|
360
|
-
const hash =
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (!hash || !heatmapInfo.elementMapInfo[hash]) {
|
|
364
|
-
setHoveredElement(null);
|
|
636
|
+
const hash = getHashFromEvent(event);
|
|
637
|
+
if (!hash) {
|
|
638
|
+
reset();
|
|
365
639
|
return;
|
|
366
640
|
}
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
hash,
|
|
374
|
-
clicks: info.totalclicks ?? 0,
|
|
375
|
-
rank,
|
|
376
|
-
selector: info.selector ?? '',
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
setHoveredElement(null);
|
|
641
|
+
const selector = heatmapInfo?.elementMapInfo?.[hash];
|
|
642
|
+
const rect = getRect({ hash, selector });
|
|
643
|
+
const elementInfo = buildElementInfo(hash, rect, heatmapInfo);
|
|
644
|
+
if (!elementInfo) {
|
|
645
|
+
reset();
|
|
646
|
+
return;
|
|
381
647
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const handleMouseLeave = useCallback(() => {
|
|
385
|
-
setHoveredElement(null);
|
|
386
|
-
}, []);
|
|
648
|
+
setHoveredElement(elementInfo);
|
|
649
|
+
}, 16), [heatmapInfo, getRect, reset, getHashFromEvent]);
|
|
387
650
|
const handleClick = useCallback(() => {
|
|
388
|
-
if (hoveredElement?.hash
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}, [hoveredElement
|
|
651
|
+
if (!hoveredElement?.hash)
|
|
652
|
+
return;
|
|
653
|
+
onSelect(hoveredElement.hash);
|
|
654
|
+
}, [hoveredElement?.hash]);
|
|
392
655
|
return {
|
|
393
656
|
hoveredElement,
|
|
394
657
|
handleMouseMove,
|
|
@@ -396,17 +659,32 @@ const useHoveredElement = ({ iframeRef, heatmapInfo, widthScale, getRect }) => {
|
|
|
396
659
|
handleClick,
|
|
397
660
|
};
|
|
398
661
|
};
|
|
399
|
-
const
|
|
400
|
-
let
|
|
401
|
-
|
|
402
|
-
|
|
662
|
+
const convertViewportToIframeCoords = (clientX, clientY, iframeRect, scale) => {
|
|
663
|
+
let x = clientX - iframeRect.left;
|
|
664
|
+
let y = clientY - iframeRect.top;
|
|
665
|
+
if (scale !== 1) {
|
|
666
|
+
x /= scale;
|
|
667
|
+
y /= scale;
|
|
403
668
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
669
|
+
return { x, y };
|
|
670
|
+
};
|
|
671
|
+
const findTargetElement = (doc, x, y) => {
|
|
672
|
+
let targetElement = getElementAtPoint(doc, x, y);
|
|
673
|
+
if (!targetElement) {
|
|
674
|
+
targetElement = doc.elementFromPoint(x, y);
|
|
408
675
|
}
|
|
409
|
-
return
|
|
676
|
+
return targetElement;
|
|
677
|
+
};
|
|
678
|
+
const isIframeReady = (iframeRef, heatmapInfo) => {
|
|
679
|
+
return !!(iframeRef.current?.contentDocument && heatmapInfo?.elementMapInfo);
|
|
680
|
+
};
|
|
681
|
+
const isValidElement = (element, heatmapInfo) => {
|
|
682
|
+
if (!element)
|
|
683
|
+
return false;
|
|
684
|
+
const hash = getElementHash(element);
|
|
685
|
+
if (!hash)
|
|
686
|
+
return false;
|
|
687
|
+
return !!heatmapInfo?.elementMapInfo?.[hash];
|
|
410
688
|
};
|
|
411
689
|
|
|
412
690
|
const useHeatmapRender = () => {
|
|
@@ -422,6 +700,15 @@ const useHeatmapRender = () => {
|
|
|
422
700
|
if (!iframe?.contentWindow)
|
|
423
701
|
return;
|
|
424
702
|
await visualizer.html(payloads, iframe.contentWindow);
|
|
703
|
+
// iframe.contentDocument?.body.querySelectorAll('body>*').forEach((element) => {
|
|
704
|
+
// console.log(`🚀 🐥 ~ useHeatmapRender ~ element:`, element);
|
|
705
|
+
// const isClosedEcomsendWidget = element.closest('#ecomsend-widget');
|
|
706
|
+
// const isEcomsendWidget = element.querySelector('#ecomsend-widget');
|
|
707
|
+
// const isRemove = !(isClosedEcomsendWidget || isEcomsendWidget);
|
|
708
|
+
// if (isRemove) {
|
|
709
|
+
// element.remove();
|
|
710
|
+
// }
|
|
711
|
+
// });
|
|
425
712
|
setVizRef(visualizer);
|
|
426
713
|
}, []);
|
|
427
714
|
useEffect(() => {
|
|
@@ -608,15 +895,16 @@ const useContainerDimensions = (props) => {
|
|
|
608
895
|
};
|
|
609
896
|
|
|
610
897
|
const useContentDimensions = (props) => {
|
|
611
|
-
const
|
|
898
|
+
const contentWidth = useHeatmapDataStore((state) => state.config?.width ?? 0);
|
|
899
|
+
const { iframeRef } = props;
|
|
612
900
|
useEffect(() => {
|
|
613
|
-
if (!
|
|
901
|
+
if (!contentWidth)
|
|
614
902
|
return;
|
|
615
903
|
if (!iframeRef.current)
|
|
616
904
|
return;
|
|
617
|
-
iframeRef.current.width = `${
|
|
618
|
-
}, [
|
|
619
|
-
return { contentWidth
|
|
905
|
+
iframeRef.current.width = `${contentWidth}px`;
|
|
906
|
+
}, [contentWidth, iframeRef]);
|
|
907
|
+
return { contentWidth };
|
|
620
908
|
};
|
|
621
909
|
|
|
622
910
|
// Hook 3: Iframe Height Observer
|
|
@@ -725,52 +1013,51 @@ const useScaleCalculation = (props) => {
|
|
|
725
1013
|
return { scale };
|
|
726
1014
|
};
|
|
727
1015
|
|
|
728
|
-
const useScrollSync = (
|
|
729
|
-
const
|
|
1016
|
+
const useScrollSync = ({ iframeRef }) => {
|
|
1017
|
+
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
730
1018
|
const handleScroll = useCallback((scrollTop) => {
|
|
731
1019
|
const iframe = iframeRef.current;
|
|
732
|
-
if (!iframe ||
|
|
1020
|
+
if (!iframe || widthScale <= 0)
|
|
733
1021
|
return;
|
|
734
1022
|
try {
|
|
735
1023
|
const iframeWindow = iframe.contentWindow;
|
|
736
1024
|
const iframeDocument = iframe.contentDocument;
|
|
737
1025
|
if (iframeWindow && iframeDocument) {
|
|
738
|
-
const iframeScrollTop = scrollTop /
|
|
1026
|
+
const iframeScrollTop = scrollTop / widthScale;
|
|
739
1027
|
iframe.style.top = `${iframeScrollTop}px`;
|
|
1028
|
+
// iframeWindow.scrollTo({ top: iframeScrollTop, behavior: 'smooth' });
|
|
740
1029
|
}
|
|
741
1030
|
}
|
|
742
1031
|
catch (error) {
|
|
743
1032
|
console.warn('Cannot sync scroll to iframe:', error);
|
|
744
1033
|
}
|
|
745
|
-
}, [iframeRef,
|
|
1034
|
+
}, [iframeRef, widthScale]);
|
|
746
1035
|
return { handleScroll };
|
|
747
1036
|
};
|
|
748
1037
|
|
|
749
1038
|
const useHeatmapScale = (props) => {
|
|
750
|
-
const { wrapperRef, iframeRef,
|
|
1039
|
+
const { wrapperRef, iframeRef, visualRef } = props;
|
|
751
1040
|
// 1. Observe container dimensions
|
|
752
1041
|
const { containerWidth, containerHeight } = useContainerDimensions({ wrapperRef });
|
|
753
1042
|
// 2. Get content dimensions from config
|
|
754
|
-
const { contentWidth } = useContentDimensions({ iframeRef
|
|
1043
|
+
const { contentWidth } = useContentDimensions({ iframeRef });
|
|
755
1044
|
// 3. Observe iframe height (now reacts to width changes)
|
|
756
1045
|
const { iframeHeight } = useIframeHeight({ iframeRef, contentWidth });
|
|
757
1046
|
// 4. Calculate scale
|
|
758
1047
|
const { scale } = useScaleCalculation({ containerWidth, contentWidth });
|
|
759
1048
|
// 5. Setup scroll sync
|
|
760
|
-
const { handleScroll } = useScrollSync({ iframeRef
|
|
1049
|
+
const { handleScroll } = useScrollSync({ iframeRef });
|
|
761
1050
|
return {
|
|
762
1051
|
containerWidth,
|
|
763
1052
|
containerHeight,
|
|
764
|
-
contentWidth,
|
|
765
1053
|
iframeHeight,
|
|
766
|
-
scale,
|
|
767
1054
|
scaledWidth: contentWidth * scale,
|
|
768
1055
|
scaledHeight: iframeHeight * scale,
|
|
769
1056
|
handleScroll,
|
|
770
1057
|
};
|
|
771
1058
|
};
|
|
772
1059
|
|
|
773
|
-
const BoxStack = ({ children, ...props }) => {
|
|
1060
|
+
const BoxStack = forwardRef(({ children, ...props }, ref) => {
|
|
774
1061
|
const id = props.id;
|
|
775
1062
|
const flexDirection = props.flexDirection;
|
|
776
1063
|
const overflow = props.overflow || 'hidden';
|
|
@@ -781,6 +1068,8 @@ const BoxStack = ({ children, ...props }) => {
|
|
|
781
1068
|
const style = props.style || {};
|
|
782
1069
|
const gap = props.gap || 0;
|
|
783
1070
|
const height = props.height || 'auto';
|
|
1071
|
+
const zIndex = props.zIndex || 0;
|
|
1072
|
+
const backgroundColor = props.backgroundColor || 'transparent';
|
|
784
1073
|
const styleGap = useMemo(() => {
|
|
785
1074
|
switch (flexDirection) {
|
|
786
1075
|
case 'row':
|
|
@@ -802,15 +1091,17 @@ const BoxStack = ({ children, ...props }) => {
|
|
|
802
1091
|
justifyContent,
|
|
803
1092
|
alignItems,
|
|
804
1093
|
height,
|
|
1094
|
+
zIndex,
|
|
1095
|
+
backgroundColor,
|
|
805
1096
|
...styleGap,
|
|
806
1097
|
...style,
|
|
807
1098
|
};
|
|
808
|
-
return (jsx("div", { id: id, style: styleProps, children: children }));
|
|
809
|
-
};
|
|
1099
|
+
return (jsx("div", { id: id, style: styleProps, ref: ref, children: children }));
|
|
1100
|
+
});
|
|
810
1101
|
|
|
811
1102
|
const ContentTopBar = () => {
|
|
812
1103
|
const controls = useHeatmapControlStore((state) => state.controls);
|
|
813
|
-
return (jsx(BoxStack, { id: "gx-hm-content-header", flexDirection: "row", alignItems: "center", overflow: "auto", style: {
|
|
1104
|
+
return (jsx(BoxStack, { id: "gx-hm-content-header", flexDirection: "row", alignItems: "center", overflow: "auto", zIndex: 1, backgroundColor: "white", style: {
|
|
814
1105
|
borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
|
|
815
1106
|
}, children: controls.TopBar ?? null }));
|
|
816
1107
|
};
|
|
@@ -848,277 +1139,82 @@ const useHeatmapVizCanvas = ({ type }) => {
|
|
|
848
1139
|
return heatmapRender?.();
|
|
849
1140
|
};
|
|
850
1141
|
|
|
851
|
-
const CLICKED_ELEMENT_ID = '
|
|
852
|
-
const SECONDARY_CLICKED_ELEMENT_ID = '
|
|
853
|
-
const HOVERED_ELEMENT_ID = '
|
|
854
|
-
const SECONDARY_HOVERED_ELEMENT_ID = '
|
|
1142
|
+
const CLICKED_ELEMENT_ID = 'gx-hm-clicked-element';
|
|
1143
|
+
const SECONDARY_CLICKED_ELEMENT_ID = 'gx-hm-secondary-clicked-element';
|
|
1144
|
+
const HOVERED_ELEMENT_ID = 'gx-hm-hovered-element';
|
|
1145
|
+
const SECONDARY_HOVERED_ELEMENT_ID = 'gx-hm-secondary-hovered-element';
|
|
1146
|
+
|
|
1147
|
+
const RankBadge = ({ index, elementRect, widthScale, clickOnElement, }) => {
|
|
1148
|
+
const style = calculateRankPosition(elementRect, widthScale);
|
|
1149
|
+
return (jsx("div", { className: "gx-hm-rank-badge", style: style, onClick: clickOnElement, children: index }));
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
const NUMBER_OF_TOP_ELEMENTS = 10;
|
|
1153
|
+
const DefaultRankBadges = ({ getRect, hidden }) => {
|
|
1154
|
+
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
1155
|
+
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
1156
|
+
const elements = heatmapInfo?.sortedElements?.slice(0, NUMBER_OF_TOP_ELEMENTS) ?? [];
|
|
1157
|
+
if (hidden || elements.length === 0)
|
|
1158
|
+
return null;
|
|
1159
|
+
return (jsx(Fragment, { children: elements.map((element, index) => {
|
|
1160
|
+
const rect = getRect(element);
|
|
1161
|
+
if (!rect)
|
|
1162
|
+
return null;
|
|
1163
|
+
return (jsx(RankBadge, { index: index + 1, elementRect: rect, widthScale: widthScale }, element.hash));
|
|
1164
|
+
}) }));
|
|
1165
|
+
};
|
|
855
1166
|
|
|
856
|
-
const
|
|
1167
|
+
const DEFAULT_POSITION = {
|
|
1168
|
+
top: 0,
|
|
1169
|
+
left: 0,
|
|
1170
|
+
placement: 'top',
|
|
1171
|
+
horizontalAlign: 'center',
|
|
1172
|
+
};
|
|
1173
|
+
const ElementCallout = (props) => {
|
|
1174
|
+
const CompElementCallout = useHeatmapControlStore((state) => state.controls.ElementCallout);
|
|
1175
|
+
const { element, target, visualRef, hozOffset, alignment = 'left' } = props;
|
|
857
1176
|
const calloutRef = useRef(null);
|
|
858
|
-
const [position, setPosition] = useState(
|
|
859
|
-
top: 0,
|
|
860
|
-
left: 0,
|
|
861
|
-
placement: 'top',
|
|
862
|
-
});
|
|
863
|
-
const percentage = formatPercentage(((element.clicks ?? 0) / totalClicks) * 100, 2);
|
|
1177
|
+
const [position, setPosition] = useState(DEFAULT_POSITION);
|
|
864
1178
|
useEffect(() => {
|
|
865
|
-
const
|
|
866
|
-
const
|
|
867
|
-
if (!
|
|
1179
|
+
const targetElm = document.querySelector(target);
|
|
1180
|
+
const calloutElm = calloutRef.current;
|
|
1181
|
+
if (!targetElm || !calloutElm)
|
|
868
1182
|
return;
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
let left = 0;
|
|
878
|
-
let placement = 'top';
|
|
879
|
-
const positions = [
|
|
880
|
-
{
|
|
881
|
-
placement: 'top',
|
|
882
|
-
top: targetRect.top - calloutRect.height - padding - arrowSize,
|
|
883
|
-
left: targetRect.left + targetRect.width / 2 - calloutRect.width / 2,
|
|
884
|
-
valid: targetRect.top - calloutRect.height - padding - arrowSize > 0,
|
|
885
|
-
},
|
|
886
|
-
{
|
|
887
|
-
placement: 'bottom',
|
|
888
|
-
top: targetRect.bottom + padding + arrowSize,
|
|
889
|
-
left: targetRect.left + targetRect.width / 2 - calloutRect.width / 2,
|
|
890
|
-
valid: targetRect.bottom + calloutRect.height + padding + arrowSize < viewportHeight,
|
|
891
|
-
},
|
|
892
|
-
{
|
|
893
|
-
placement: 'right',
|
|
894
|
-
top: targetRect.top + targetRect.height / 2 - calloutRect.height / 2,
|
|
895
|
-
left: targetRect.right + padding + arrowSize,
|
|
896
|
-
valid: targetRect.right + calloutRect.width + padding + arrowSize < viewportWidth,
|
|
897
|
-
},
|
|
898
|
-
{
|
|
899
|
-
placement: 'left',
|
|
900
|
-
top: targetRect.top + targetRect.height / 2 - calloutRect.height / 2,
|
|
901
|
-
left: targetRect.left - calloutRect.width - padding - arrowSize,
|
|
902
|
-
valid: targetRect.left - calloutRect.width - padding - arrowSize > 0,
|
|
903
|
-
},
|
|
904
|
-
];
|
|
905
|
-
// Find first valid position
|
|
906
|
-
const validPosition = positions.find((p) => p.valid) || positions[0];
|
|
907
|
-
top = validPosition.top;
|
|
908
|
-
left = validPosition.left;
|
|
909
|
-
placement = validPosition.placement;
|
|
910
|
-
// Keep within viewport bounds
|
|
911
|
-
left = Math.max(padding, Math.min(left, viewportWidth - calloutRect.width - padding));
|
|
912
|
-
top = Math.max(padding, Math.min(top, viewportHeight - calloutRect.height - padding));
|
|
913
|
-
setPosition({ top, left, placement });
|
|
914
|
-
};
|
|
915
|
-
calculatePosition();
|
|
1183
|
+
const positionFn = calcCalloutPosition({
|
|
1184
|
+
targetElm,
|
|
1185
|
+
calloutElm,
|
|
1186
|
+
setPosition,
|
|
1187
|
+
hozOffset,
|
|
1188
|
+
alignment,
|
|
1189
|
+
});
|
|
1190
|
+
positionFn();
|
|
916
1191
|
const handleUpdate = () => {
|
|
917
|
-
requestAnimationFrame(
|
|
1192
|
+
requestAnimationFrame(positionFn);
|
|
918
1193
|
};
|
|
919
1194
|
window.addEventListener('scroll', handleUpdate, true);
|
|
920
1195
|
window.addEventListener('resize', handleUpdate);
|
|
921
|
-
|
|
1196
|
+
visualRef?.current?.addEventListener('scroll', handleUpdate);
|
|
922
1197
|
return () => {
|
|
923
1198
|
window.removeEventListener('scroll', handleUpdate, true);
|
|
924
1199
|
window.removeEventListener('resize', handleUpdate);
|
|
925
|
-
|
|
1200
|
+
visualRef?.current?.removeEventListener('scroll', handleUpdate);
|
|
926
1201
|
};
|
|
927
|
-
}, [target,
|
|
928
|
-
const calloutContent = (
|
|
1202
|
+
}, [target, visualRef, hozOffset, alignment]);
|
|
1203
|
+
const calloutContent = (jsx("div", { ref: calloutRef, className: `clarity-callout clarity-callout--${position.placement} clarity-callout--align-${position.horizontalAlign}`, style: {
|
|
929
1204
|
position: 'fixed',
|
|
930
1205
|
top: position.top,
|
|
931
1206
|
left: position.left,
|
|
932
1207
|
zIndex: 2147483647,
|
|
933
|
-
}, "aria-live": "assertive", role: "tooltip", children:
|
|
934
|
-
|
|
935
|
-
background: white;
|
|
936
|
-
border-radius: 8px;
|
|
937
|
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
938
|
-
padding: 16px;
|
|
939
|
-
min-width: 200px;
|
|
940
|
-
max-width: 280px;
|
|
941
|
-
animation: clarity-callout-fade-in 0.2s ease-out;
|
|
942
|
-
pointer-events: auto;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
@keyframes clarity-callout-fade-in {
|
|
946
|
-
from {
|
|
947
|
-
opacity: 0;
|
|
948
|
-
transform: scale(0.95);
|
|
949
|
-
}
|
|
950
|
-
to {
|
|
951
|
-
opacity: 1;
|
|
952
|
-
transform: scale(1);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
.clarity-callout__arrow {
|
|
957
|
-
position: absolute;
|
|
958
|
-
width: 16px;
|
|
959
|
-
height: 16px;
|
|
960
|
-
background: white;
|
|
961
|
-
transform: rotate(45deg);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
/* Arrow positions */
|
|
965
|
-
.clarity-callout--top .clarity-callout__arrow {
|
|
966
|
-
bottom: -8px;
|
|
967
|
-
left: 50%;
|
|
968
|
-
margin-left: -8px;
|
|
969
|
-
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
.clarity-callout--bottom .clarity-callout__arrow {
|
|
973
|
-
top: -8px;
|
|
974
|
-
left: 50%;
|
|
975
|
-
margin-left: -8px;
|
|
976
|
-
box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.1);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
.clarity-callout--left .clarity-callout__arrow {
|
|
980
|
-
right: -8px;
|
|
981
|
-
top: 50%;
|
|
982
|
-
margin-top: -8px;
|
|
983
|
-
box-shadow: 2px -2px 4px rgba(0, 0, 0, 0.1);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
.clarity-callout--right .clarity-callout__arrow {
|
|
987
|
-
left: -8px;
|
|
988
|
-
top: 50%;
|
|
989
|
-
margin-top: -8px;
|
|
990
|
-
box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.1);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
.clarity-callout__content {
|
|
994
|
-
position: relative;
|
|
995
|
-
z-index: 1;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
.clarity-callout__rank {
|
|
999
|
-
display: inline-flex;
|
|
1000
|
-
align-items: center;
|
|
1001
|
-
justify-content: center;
|
|
1002
|
-
width: 32px;
|
|
1003
|
-
height: 32px;
|
|
1004
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1005
|
-
color: white;
|
|
1006
|
-
border-radius: 8px;
|
|
1007
|
-
font-weight: 600;
|
|
1008
|
-
font-size: 16px;
|
|
1009
|
-
margin-bottom: 12px;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
.clarity-callout--secondary .clarity-callout__rank {
|
|
1013
|
-
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
.clarity-callout__stats {
|
|
1017
|
-
margin-bottom: 12px;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
.clarity-callout__label {
|
|
1021
|
-
font-size: 12px;
|
|
1022
|
-
color: #6b7280;
|
|
1023
|
-
margin-bottom: 4px;
|
|
1024
|
-
font-weight: 500;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
.clarity-callout__value {
|
|
1028
|
-
display: flex;
|
|
1029
|
-
align-items: baseline;
|
|
1030
|
-
gap: 6px;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
.clarity-callout__count {
|
|
1034
|
-
font-size: 20px;
|
|
1035
|
-
font-weight: 700;
|
|
1036
|
-
color: #111827;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
.clarity-callout__percentage {
|
|
1040
|
-
font-size: 14px;
|
|
1041
|
-
color: #6b7280;
|
|
1042
|
-
font-weight: 500;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
.clarity-callout__button {
|
|
1046
|
-
display: flex;
|
|
1047
|
-
align-items: center;
|
|
1048
|
-
justify-content: center;
|
|
1049
|
-
gap: 6px;
|
|
1050
|
-
width: 100%;
|
|
1051
|
-
padding: 8px 12px;
|
|
1052
|
-
background: #667eea;
|
|
1053
|
-
color: white;
|
|
1054
|
-
border: none;
|
|
1055
|
-
border-radius: 6px;
|
|
1056
|
-
font-size: 13px;
|
|
1057
|
-
font-weight: 600;
|
|
1058
|
-
cursor: pointer;
|
|
1059
|
-
transition: all 0.2s;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
.clarity-callout__button:hover {
|
|
1063
|
-
background: #5568d3;
|
|
1064
|
-
transform: translateY(-1px);
|
|
1065
|
-
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
.clarity-callout__button:active {
|
|
1069
|
-
transform: translateY(0);
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
.clarity-callout__button svg {
|
|
1073
|
-
width: 14px;
|
|
1074
|
-
height: 14px;
|
|
1075
|
-
}
|
|
1076
|
-
` })] }));
|
|
1077
|
-
return createPortal(calloutContent, document.body);
|
|
1078
|
-
};
|
|
1079
|
-
|
|
1080
|
-
const RankBadge = ({ index, elementRect, widthScale, clickOnElement, }) => {
|
|
1081
|
-
const style = calculateRankPosition(elementRect, widthScale);
|
|
1082
|
-
return (jsx("div", { className: "rankBadge", style: style, onClick: clickOnElement, "data-testid": "elementRank", children: index }));
|
|
1208
|
+
}, "aria-live": "assertive", role: "tooltip", children: CompElementCallout && jsx(CompElementCallout, { elementHash: element.hash }) }));
|
|
1209
|
+
return createPortal(calloutContent, document.getElementById('gx-hm-viz-container'));
|
|
1083
1210
|
};
|
|
1084
1211
|
|
|
1085
|
-
const
|
|
1086
|
-
if (!
|
|
1087
|
-
return null;
|
|
1088
|
-
return (jsxs(Fragment$1, { children: [jsx("div", { className: "heatmapElement", id: targetId, style: {
|
|
1089
|
-
top: element.top,
|
|
1090
|
-
left: element.left,
|
|
1091
|
-
width: element.width,
|
|
1092
|
-
height: element.height,
|
|
1093
|
-
} }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: widthScale }), shouldShowCallout && (jsx(ElementCallout, { element: element, target: `#${targetId}`, totalClicks: totalClicks, isSecondary: isSecondary, isRecordingView: isRecordingView, isCompareMode: isCompareMode, deviceType: deviceType, heatmapType: heatmapType, widthScale: widthScale }))] }));
|
|
1094
|
-
};
|
|
1095
|
-
|
|
1096
|
-
const DefaultRankBadges = ({ elements, getRect, widthScale, hidden }) => {
|
|
1097
|
-
if (hidden || elements.length === 0)
|
|
1212
|
+
const ElementMissing = ({ show = true }) => {
|
|
1213
|
+
if (!show)
|
|
1098
1214
|
return null;
|
|
1099
|
-
|
|
1100
|
-
const rect = getRect(element);
|
|
1101
|
-
if (!rect)
|
|
1102
|
-
return null;
|
|
1103
|
-
return (jsx(RankBadge, { index: index + 1, elementRect: rect, widthScale: widthScale }, element.hash));
|
|
1104
|
-
}) }));
|
|
1105
|
-
};
|
|
1106
|
-
|
|
1107
|
-
const HoveredElementOverlay = ({ element, onClick, isSecondary, targetId, totalClicks = 1, }) => {
|
|
1108
|
-
if (!element)
|
|
1109
|
-
return null;
|
|
1110
|
-
return (jsxs(Fragment$1, { children: [jsx("div", { onClick: onClick, className: "heatmapElement hovered", id: targetId, style: {
|
|
1111
|
-
top: element.top,
|
|
1112
|
-
left: element.left,
|
|
1113
|
-
width: element.width,
|
|
1114
|
-
height: element.height,
|
|
1115
|
-
cursor: 'pointer',
|
|
1116
|
-
} }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: 1, clickOnElement: onClick })] }));
|
|
1117
|
-
};
|
|
1118
|
-
|
|
1119
|
-
const MissingElementMessage = ({ widthScale }) => {
|
|
1215
|
+
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
1120
1216
|
return (jsx("div", { className: "missingElement", style: {
|
|
1121
|
-
position: '
|
|
1217
|
+
position: 'fixed',
|
|
1122
1218
|
top: '50%',
|
|
1123
1219
|
left: '50%',
|
|
1124
1220
|
transform: `translate(-50%, -50%) scale(${1 / widthScale})`,
|
|
@@ -1134,27 +1230,58 @@ const MissingElementMessage = ({ widthScale }) => {
|
|
|
1134
1230
|
}, "aria-live": "assertive", children: "Element not visible on current screen" }));
|
|
1135
1231
|
};
|
|
1136
1232
|
|
|
1233
|
+
const TARGET_ID_BY_TYPE = {
|
|
1234
|
+
hovered: {
|
|
1235
|
+
default: HOVERED_ELEMENT_ID,
|
|
1236
|
+
secondary: SECONDARY_HOVERED_ELEMENT_ID,
|
|
1237
|
+
},
|
|
1238
|
+
clicked: {
|
|
1239
|
+
default: CLICKED_ELEMENT_ID,
|
|
1240
|
+
secondary: SECONDARY_CLICKED_ELEMENT_ID,
|
|
1241
|
+
},
|
|
1242
|
+
};
|
|
1243
|
+
const ElementOverlay = ({ type, element, onClick, isSecondary }) => {
|
|
1244
|
+
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
1245
|
+
if (!element || (element.width === 0 && element.height === 0))
|
|
1246
|
+
return null;
|
|
1247
|
+
// Iframe has border, so we need to add it to the top position
|
|
1248
|
+
const top = element.top + HEATMAP_CONFIG['borderWidthIframe'];
|
|
1249
|
+
const left = element.left + HEATMAP_CONFIG['borderWidthIframe'];
|
|
1250
|
+
const width = element.width;
|
|
1251
|
+
const height = element.height;
|
|
1252
|
+
const targetId = TARGET_ID_BY_TYPE[type][isSecondary ? 'secondary' : 'default'];
|
|
1253
|
+
return (jsxs(Fragment$1, { children: [jsx("div", { onClick: onClick, className: "heatmapElement", id: targetId, style: {
|
|
1254
|
+
top,
|
|
1255
|
+
left,
|
|
1256
|
+
width,
|
|
1257
|
+
height,
|
|
1258
|
+
cursor: 'pointer',
|
|
1259
|
+
} }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: type === 'hovered' ? 1 : widthScale, clickOnElement: onClick })] }));
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
const ELEMENT_CALLOUT = {
|
|
1263
|
+
hozOffset: -8,
|
|
1264
|
+
alignment: 'left',
|
|
1265
|
+
};
|
|
1137
1266
|
const HeatmapElements = (props) => {
|
|
1138
1267
|
const height = useHeatmapVizStore((state) => state.iframeHeight);
|
|
1139
|
-
const {
|
|
1268
|
+
const { iframeRef, wrapperRef, visualRef, visualizer, iframeDimensions, isElementSidebarOpen, isVisible = true, areDefaultRanksHidden, isSecondary, ...rest } = props;
|
|
1140
1269
|
const getRect = useHeatmapElementPosition({
|
|
1141
1270
|
iframeRef,
|
|
1142
|
-
|
|
1271
|
+
wrapperRef,
|
|
1143
1272
|
visualizer,
|
|
1144
|
-
heatmapWidth: heatmapInfo?.width,
|
|
1145
|
-
widthScale,
|
|
1146
|
-
projectId: props.projectId,
|
|
1147
1273
|
});
|
|
1148
1274
|
const { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout } = useClickedElement({
|
|
1149
|
-
|
|
1275
|
+
visualRef,
|
|
1150
1276
|
getRect,
|
|
1151
1277
|
});
|
|
1152
|
-
console.log(`🚀 🐥 ~ HeatmapElements ~ clickedElement:`, clickedElement);
|
|
1153
1278
|
const { hoveredElement, handleMouseMove, handleMouseLeave, handleClick } = useHoveredElement({
|
|
1154
1279
|
iframeRef,
|
|
1155
|
-
heatmapInfo,
|
|
1156
1280
|
getRect,
|
|
1157
|
-
|
|
1281
|
+
});
|
|
1282
|
+
useElementCalloutVisible({
|
|
1283
|
+
visualRef,
|
|
1284
|
+
getRect,
|
|
1158
1285
|
});
|
|
1159
1286
|
const resetAll = () => {
|
|
1160
1287
|
// setShouldShowCallout(false);
|
|
@@ -1167,12 +1294,12 @@ const HeatmapElements = (props) => {
|
|
|
1167
1294
|
});
|
|
1168
1295
|
if (!isVisible)
|
|
1169
1296
|
return null;
|
|
1170
|
-
|
|
1171
|
-
return (jsxs("div", { onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, className: "heatmapElements gx-hm-elements", style: { ...iframeDimensions, height }, children: [jsx(DefaultRankBadges, { elements: top10, getRect: getRect, widthScale: widthScale, hidden: areDefaultRanksHidden }), jsx(ClickedElementOverlay, { widthScale: widthScale, element: clickedElement, shouldShowCallout: shouldShowCallout, isSecondary: isSecondary, targetId: isSecondary ? SECONDARY_CLICKED_ELEMENT_ID : CLICKED_ELEMENT_ID, ...rest }), showMissingElement && jsx(MissingElementMessage, { widthScale: widthScale }), jsx(HoveredElementOverlay, { element: hoveredElement, onClick: handleClick, isSecondary: isSecondary, targetId: isSecondary ? SECONDARY_HOVERED_ELEMENT_ID : HOVERED_ELEMENT_ID, totalClicks: heatmapInfo?.totalClicks ?? 1 }), hoveredElement?.hash !== clickedElement?.hash && hoveredElement && (jsx(ElementCallout, { element: hoveredElement, target: `#${props.isSecondary ? SECONDARY_HOVERED_ELEMENT_ID : HOVERED_ELEMENT_ID}`, totalClicks: props.heatmapInfo?.totalClicks ?? 1, isSecondary: props.isSecondary, parentRef: props.parentRef }))] }));
|
|
1297
|
+
return (jsxs("div", { onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, className: "gx-hm-elements", style: { ...iframeDimensions, height }, children: [jsx(ElementMissing, { show: showMissingElement }), jsx(DefaultRankBadges, { getRect: getRect, hidden: areDefaultRanksHidden }), jsx(ElementOverlay, { type: "clicked", element: clickedElement, isSecondary: isSecondary }), jsx(ElementOverlay, { type: "hovered", element: hoveredElement, isSecondary: isSecondary, onClick: handleClick }), hoveredElement?.hash !== clickedElement?.hash && hoveredElement && (jsx(ElementCallout, { element: hoveredElement, target: `#${HOVERED_ELEMENT_ID}`, visualRef: visualRef, ...ELEMENT_CALLOUT })), shouldShowCallout && clickedElement && (jsx(ElementCallout, { element: clickedElement, target: `#${CLICKED_ELEMENT_ID}`, visualRef: visualRef, ...ELEMENT_CALLOUT }))] }));
|
|
1172
1298
|
};
|
|
1173
1299
|
|
|
1174
|
-
const VizElements = ({
|
|
1300
|
+
const VizElements = ({ iframeRef, visualRef, wrapperRef }) => {
|
|
1175
1301
|
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
1302
|
+
const contentWidth = useHeatmapDataStore((state) => state.config?.width ?? 0);
|
|
1176
1303
|
const visualizer = {
|
|
1177
1304
|
get: (hash) => {
|
|
1178
1305
|
const doc = iframeRef.current?.contentDocument;
|
|
@@ -1191,8 +1318,8 @@ const VizElements = ({ width, iframeRef, wrapperRef, widthScale, }) => {
|
|
|
1191
1318
|
};
|
|
1192
1319
|
if (!iframeRef.current)
|
|
1193
1320
|
return null;
|
|
1194
|
-
return (jsx(HeatmapElements, { visualizer: visualizer,
|
|
1195
|
-
width,
|
|
1321
|
+
return (jsx(HeatmapElements, { visualizer: visualizer, visualRef: visualRef, iframeRef: iframeRef, wrapperRef: wrapperRef, heatmapInfo: heatmapInfo, isVisible: true, iframeDimensions: {
|
|
1322
|
+
width: contentWidth,
|
|
1196
1323
|
position: 'absolute',
|
|
1197
1324
|
top: 0,
|
|
1198
1325
|
left: 0,
|
|
@@ -1232,14 +1359,20 @@ const ReplayControls = () => {
|
|
|
1232
1359
|
const VizDomRenderer = ({ mode = 'heatmap' }) => {
|
|
1233
1360
|
const config = useHeatmapDataStore((state) => state.config);
|
|
1234
1361
|
const setIframeHeight = useHeatmapVizStore((state) => state.setIframeHeight);
|
|
1362
|
+
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
1363
|
+
const setSelectedElement = useHeatmapInteractionStore((state) => state.setSelectedElement);
|
|
1235
1364
|
const wrapperRef = useRef(null);
|
|
1236
1365
|
const visualRef = useRef(null);
|
|
1237
1366
|
const { iframeRef } = useHeatmapVizRender(mode);
|
|
1238
|
-
const {
|
|
1367
|
+
const { iframeHeight, scaledHeight, handleScroll } = useHeatmapScale({
|
|
1239
1368
|
wrapperRef,
|
|
1240
1369
|
iframeRef,
|
|
1241
|
-
|
|
1370
|
+
visualRef,
|
|
1242
1371
|
});
|
|
1372
|
+
const contentWidth = config?.width ?? 0;
|
|
1373
|
+
const contentHeight = scaledHeight > 0
|
|
1374
|
+
? `${scaledHeight + HEATMAP_CONFIG['heightToolbar'] + HEATMAP_CONFIG['padding'] * 2}px`
|
|
1375
|
+
: 'auto';
|
|
1243
1376
|
const onScroll = (e) => {
|
|
1244
1377
|
const scrollTop = e.currentTarget.scrollTop;
|
|
1245
1378
|
handleScroll(scrollTop);
|
|
@@ -1247,6 +1380,7 @@ const VizDomRenderer = ({ mode = 'heatmap' }) => {
|
|
|
1247
1380
|
useHeatmapVizCanvas({ type: config?.heatmapType });
|
|
1248
1381
|
const cleanUp = () => {
|
|
1249
1382
|
setIframeHeight(0);
|
|
1383
|
+
setSelectedElement(null);
|
|
1250
1384
|
};
|
|
1251
1385
|
useEffect(() => {
|
|
1252
1386
|
return cleanUp;
|
|
@@ -1264,17 +1398,17 @@ const VizDomRenderer = ({ mode = 'heatmap' }) => {
|
|
|
1264
1398
|
display: 'flex',
|
|
1265
1399
|
justifyContent: 'center',
|
|
1266
1400
|
alignItems: 'flex-start',
|
|
1267
|
-
height:
|
|
1401
|
+
height: contentHeight,
|
|
1268
1402
|
padding: HEATMAP_STYLE['wrapper']['padding'],
|
|
1269
1403
|
paddingBottom: HEATMAP_STYLE['viz']['paddingBottom'],
|
|
1270
1404
|
background: HEATMAP_STYLE['viz']['background'],
|
|
1271
1405
|
}, children: jsxs("div", { className: "gx-hm-wrapper", ref: wrapperRef, style: {
|
|
1272
1406
|
width: contentWidth,
|
|
1273
1407
|
height: iframeHeight,
|
|
1274
|
-
transform: `scale(${
|
|
1408
|
+
transform: `scale(${widthScale})`,
|
|
1275
1409
|
transformOrigin: 'top center',
|
|
1276
1410
|
paddingBlockEnd: '0',
|
|
1277
|
-
}, children: [jsx(VizElements, {
|
|
1411
|
+
}, children: [jsx(VizElements, { iframeRef: iframeRef, visualRef: visualRef, wrapperRef: wrapperRef }), jsx("iframe", { ref: iframeRef, ...HEATMAP_IFRAME, width: contentWidth, height: iframeHeight, scrolling: "no" })] }) }), mode === 'replay' && jsx(ReplayControls, {})] }));
|
|
1278
1412
|
};
|
|
1279
1413
|
|
|
1280
1414
|
const VizDomContainer = () => {
|
|
@@ -1289,14 +1423,21 @@ const VizDomContainer = () => {
|
|
|
1289
1423
|
|
|
1290
1424
|
const ContentMetricBar = () => {
|
|
1291
1425
|
const controls = useHeatmapControlStore((state) => state.controls);
|
|
1292
|
-
return (jsx(BoxStack, { id: "gx-hm-content-
|
|
1426
|
+
return (jsx(BoxStack, { id: "gx-hm-content-metric-bar", flexDirection: "row", alignItems: "center", overflow: "auto", zIndex: 1, backgroundColor: "white", style: {
|
|
1293
1427
|
borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
|
|
1294
1428
|
}, children: controls.MetricBar ?? null }));
|
|
1295
1429
|
};
|
|
1296
1430
|
|
|
1297
1431
|
const ContentToolbar = () => {
|
|
1298
1432
|
const controls = useHeatmapControlStore((state) => state.controls);
|
|
1299
|
-
return (jsx("div", { id: "gx-hm-content-toolbar", style: {
|
|
1433
|
+
return (jsx("div", { id: "gx-hm-content-toolbar", style: {
|
|
1434
|
+
position: 'absolute',
|
|
1435
|
+
bottom: 0,
|
|
1436
|
+
left: '8px',
|
|
1437
|
+
right: '24px',
|
|
1438
|
+
padding: '8px',
|
|
1439
|
+
paddingBlock: '16px',
|
|
1440
|
+
}, children: controls.Toolbar ?? null }));
|
|
1300
1441
|
};
|
|
1301
1442
|
|
|
1302
1443
|
const LeftSidebar = () => {
|
|
@@ -1344,6 +1485,7 @@ const HeatmapLayout = ({ data, clickmap, controls, dataInfo, }) => {
|
|
|
1344
1485
|
function getVariableStyle() {
|
|
1345
1486
|
return {
|
|
1346
1487
|
'--gx-hm-border-width': `${HEATMAP_CONFIG.borderWidth}px`,
|
|
1488
|
+
'--gx-hm-border-width-iframe': `${HEATMAP_CONFIG.borderWidthIframe}px`,
|
|
1347
1489
|
'--gx-hm-border-color': `${HEATMAP_CONFIG.borderColor}`,
|
|
1348
1490
|
};
|
|
1349
1491
|
}
|