@gemx-dev/heatmap-react 3.5.34 → 3.5.37
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/LeftSidebar.d.ts.map +1 -1
- package/dist/esm/components/VizDom/VizDomContainer.d.ts.map +1 -1
- package/dist/esm/components/VizElement/ElementCallout.d.ts +5 -6
- 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/{umd/components/VizElement/HoveredElementOverlay.d.ts → esm/components/VizElement/ElementOverlay.d.ts} +3 -3
- package/dist/esm/components/VizElement/ElementOverlay.d.ts.map +1 -0
- package/dist/esm/components/VizElement/HeatmapElements.d.ts +2 -1
- package/dist/esm/components/VizElement/HeatmapElements.d.ts.map +1 -1
- package/dist/esm/configs/iframe.d.ts +1 -1
- package/dist/esm/configs/iframe.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 +5 -0
- package/dist/esm/helpers/index.d.ts.map +1 -1
- package/dist/esm/helpers/viewport-fixer.d.ts +13 -0
- package/dist/esm/helpers/viewport-fixer.d.ts.map +1 -0
- package/dist/esm/helpers/viewport-replacer.d.ts +25 -0
- package/dist/esm/helpers/viewport-replacer.d.ts.map +1 -0
- 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 +2 -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/useHoveredElement.d.ts +1 -2
- 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 +1 -1
- package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts.map +1 -1
- package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
- package/dist/esm/hooks/viz-scale/useIframeHeight.d.ts +0 -1
- package/dist/esm/hooks/viz-scale/useIframeHeight.d.ts.map +1 -1
- package/dist/esm/hooks/viz-scale/useScrollSync.d.ts.map +1 -1
- package/dist/esm/index.js +766 -283
- package/dist/esm/index.mjs +766 -283
- 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/stores/viz.d.ts +2 -0
- package/dist/esm/stores/viz.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 +2 -0
- package/dist/esm/types/index.d.ts.map +1 -1
- package/dist/esm/types/viewport-fixer.d.ts +28 -0
- package/dist/esm/types/viewport-fixer.d.ts.map +1 -0
- package/dist/esm/types/viz-element.d.ts +6 -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 -109
- 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/LeftSidebar.d.ts.map +1 -1
- package/dist/umd/components/VizDom/VizDomContainer.d.ts.map +1 -1
- package/dist/umd/components/VizElement/ElementCallout.d.ts +5 -6
- 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/{esm/components/VizElement/HoveredElementOverlay.d.ts → umd/components/VizElement/ElementOverlay.d.ts} +3 -3
- package/dist/umd/components/VizElement/ElementOverlay.d.ts.map +1 -0
- package/dist/umd/components/VizElement/HeatmapElements.d.ts +2 -1
- package/dist/umd/components/VizElement/HeatmapElements.d.ts.map +1 -1
- package/dist/umd/configs/iframe.d.ts +1 -1
- package/dist/umd/configs/iframe.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 +5 -0
- package/dist/umd/helpers/index.d.ts.map +1 -1
- package/dist/umd/helpers/viewport-fixer.d.ts +13 -0
- package/dist/umd/helpers/viewport-fixer.d.ts.map +1 -0
- package/dist/umd/helpers/viewport-replacer.d.ts +25 -0
- package/dist/umd/helpers/viewport-replacer.d.ts.map +1 -0
- 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 +2 -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/useHoveredElement.d.ts +1 -2
- 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 +1 -1
- package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts.map +1 -1
- package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
- package/dist/umd/hooks/viz-scale/useIframeHeight.d.ts +0 -1
- package/dist/umd/hooks/viz-scale/useIframeHeight.d.ts.map +1 -1
- 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/stores/viz.d.ts +2 -0
- package/dist/umd/stores/viz.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 +2 -0
- package/dist/umd/types/index.d.ts.map +1 -1
- package/dist/umd/types/viewport-fixer.d.ts +28 -0
- package/dist/umd/types/viewport-fixer.d.ts.map +1 -0
- package/dist/umd/types/viz-element.d.ts +6 -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 -10
- package/dist/esm/components/VizElement/ClickedElementOverlay.d.ts.map +0 -1
- package/dist/esm/components/VizElement/HoveredElementOverlay.d.ts.map +0 -1
- package/dist/esm/components/VizElement/MissingElementMessage.d.ts +0 -6
- package/dist/esm/components/VizElement/MissingElementMessage.d.ts.map +0 -1
- package/dist/umd/components/VizElement/ClickedElementOverlay.d.ts +0 -10
- package/dist/umd/components/VizElement/ClickedElementOverlay.d.ts.map +0 -1
- package/dist/umd/components/VizElement/HoveredElementOverlay.d.ts.map +0 -1
- package/dist/umd/components/VizElement/MissingElementMessage.d.ts +0 -6
- 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';
|
|
@@ -72,6 +72,7 @@ const useHeatmapControlStore = create()((set, get) => {
|
|
|
72
72
|
Toolbar: null,
|
|
73
73
|
MetricBar: null,
|
|
74
74
|
VizLoading: null,
|
|
75
|
+
ElementCallout: null,
|
|
75
76
|
},
|
|
76
77
|
registerControl: (key, control) => {
|
|
77
78
|
set({
|
|
@@ -120,11 +121,15 @@ const useHeatmapInteractionStore = create()((set, get) => {
|
|
|
120
121
|
setSelectedElement: (selectedElement) => set({ selectedElement }),
|
|
121
122
|
hoveredElement: null,
|
|
122
123
|
setHoveredElement: (hoveredElement) => set({ hoveredElement }),
|
|
124
|
+
shouldShowCallout: false,
|
|
125
|
+
setShouldShowCallout: (shouldShowCallout) => set({ shouldShowCallout }),
|
|
123
126
|
};
|
|
124
127
|
});
|
|
125
128
|
|
|
126
129
|
const useHeatmapVizStore = create()((set, get) => {
|
|
127
130
|
return {
|
|
131
|
+
isRenderViz: false,
|
|
132
|
+
setIsRenderViz: (isRenderViz) => set({ isRenderViz }),
|
|
128
133
|
scale: 1,
|
|
129
134
|
vizRef: undefined,
|
|
130
135
|
iframeHeight: 0,
|
|
@@ -152,6 +157,7 @@ const useRegisterControl = (control) => {
|
|
|
152
157
|
registerControl('Toolbar', control.Toolbar);
|
|
153
158
|
registerControl('MetricBar', control.MetricBar);
|
|
154
159
|
registerControl('VizLoading', control.VizLoading);
|
|
160
|
+
registerControl('ElementCallout', control.ElementCallout);
|
|
155
161
|
};
|
|
156
162
|
|
|
157
163
|
const useRegisterData = (data, dataInfo) => {
|
|
@@ -189,47 +195,498 @@ const useRegisterHeatmap = (clickmap) => {
|
|
|
189
195
|
}, [clickmap]);
|
|
190
196
|
};
|
|
191
197
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
198
|
+
const PADDING = 0;
|
|
199
|
+
const ARROW_SIZE = 8;
|
|
200
|
+
const HORIZONTAL_OFFSET = 0;
|
|
201
|
+
// ============================================================================
|
|
202
|
+
// Viewport & Dimensions
|
|
203
|
+
// ============================================================================
|
|
204
|
+
const getViewportDimensions = () => ({
|
|
205
|
+
width: window.innerWidth,
|
|
206
|
+
height: window.innerHeight,
|
|
207
|
+
});
|
|
208
|
+
const getElementDimensions = (targetElm, calloutElm) => ({
|
|
209
|
+
targetRect: targetElm.getBoundingClientRect(),
|
|
210
|
+
calloutRect: calloutElm.getBoundingClientRect(),
|
|
211
|
+
});
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// Alignment Order
|
|
214
|
+
// ============================================================================
|
|
215
|
+
const getAlignmentOrder = (alignment) => {
|
|
216
|
+
switch (alignment) {
|
|
217
|
+
case 'center':
|
|
218
|
+
return ['center', 'left', 'right'];
|
|
219
|
+
case 'left':
|
|
220
|
+
return ['left', 'center', 'right'];
|
|
221
|
+
case 'right':
|
|
222
|
+
return ['right', 'center', 'left'];
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// Position Calculation
|
|
227
|
+
// ============================================================================
|
|
228
|
+
const calculateLeftPosition = ({ targetRect, calloutRect, hozOffset, align, }) => {
|
|
229
|
+
switch (align) {
|
|
230
|
+
case 'left':
|
|
231
|
+
return targetRect.left + hozOffset;
|
|
232
|
+
case 'right':
|
|
233
|
+
return targetRect.right - calloutRect.width - hozOffset;
|
|
234
|
+
case 'center':
|
|
235
|
+
default:
|
|
236
|
+
return targetRect.left + targetRect.width / 2 - calloutRect.width / 2;
|
|
237
|
+
}
|
|
207
238
|
};
|
|
208
|
-
const
|
|
209
|
-
|
|
239
|
+
const calculateVerticalPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
|
|
240
|
+
return placement === 'top'
|
|
241
|
+
? targetRect.top - calloutRect.height - padding - arrowSize
|
|
242
|
+
: targetRect.bottom + padding + arrowSize;
|
|
243
|
+
};
|
|
244
|
+
const calculateHorizontalPlacementPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
|
|
245
|
+
const top = targetRect.top + targetRect.height / 2 - calloutRect.height / 2;
|
|
246
|
+
const left = placement === 'right'
|
|
247
|
+
? targetRect.right + padding + arrowSize
|
|
248
|
+
: targetRect.left - calloutRect.width - padding - arrowSize;
|
|
249
|
+
return { top, left };
|
|
250
|
+
};
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// Validation
|
|
253
|
+
// ============================================================================
|
|
254
|
+
const isLeftPositionValid = (leftPos, calloutWidth, viewportWidth, padding) => {
|
|
255
|
+
return leftPos >= padding && leftPos + calloutWidth <= viewportWidth - padding;
|
|
256
|
+
};
|
|
257
|
+
const isVerticalPositionValid = (targetRect, calloutRect, placement, viewportHeight, padding, arrowSize) => {
|
|
258
|
+
return placement === 'top'
|
|
259
|
+
? targetRect.top - calloutRect.height - padding - arrowSize > 0
|
|
260
|
+
: targetRect.bottom + calloutRect.height + padding + arrowSize < viewportHeight;
|
|
261
|
+
};
|
|
262
|
+
const isHorizontalPlacementValid = (targetRect, calloutRect, placement, viewportWidth, padding, arrowSize) => {
|
|
263
|
+
return placement === 'right'
|
|
264
|
+
? targetRect.right + calloutRect.width + padding + arrowSize < viewportWidth
|
|
265
|
+
: targetRect.left - calloutRect.width - padding - arrowSize > 0;
|
|
266
|
+
};
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// Position Candidates Generation
|
|
269
|
+
// ============================================================================
|
|
270
|
+
const generateVerticalPositionCandidates = (targetRect, calloutRect, viewportHeight, viewportWidth, alignment, hozOffset, padding, arrowSize) => {
|
|
271
|
+
const candidates = [];
|
|
272
|
+
const placements = ['top', 'bottom'];
|
|
273
|
+
placements.forEach((placement) => {
|
|
274
|
+
const verticalPos = calculateVerticalPosition(targetRect, calloutRect, placement, padding, arrowSize);
|
|
275
|
+
const verticalValid = isVerticalPositionValid(targetRect, calloutRect, placement, viewportHeight, padding, arrowSize);
|
|
276
|
+
const alignmentOrder = getAlignmentOrder(alignment);
|
|
277
|
+
alignmentOrder.forEach((align) => {
|
|
278
|
+
const horizontalPos = calculateLeftPosition({
|
|
279
|
+
targetRect,
|
|
280
|
+
calloutRect,
|
|
281
|
+
hozOffset,
|
|
282
|
+
align,
|
|
283
|
+
});
|
|
284
|
+
candidates.push({
|
|
285
|
+
placement,
|
|
286
|
+
top: verticalPos,
|
|
287
|
+
left: horizontalPos,
|
|
288
|
+
horizontalAlign: align,
|
|
289
|
+
valid: verticalValid &&
|
|
290
|
+
isLeftPositionValid(horizontalPos, calloutRect.width, viewportWidth, padding),
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
return candidates;
|
|
295
|
+
};
|
|
296
|
+
const generateHorizontalPositionCandidates = (targetRect, calloutRect, viewportWidth, padding, arrowSize) => {
|
|
297
|
+
const placements = ['left', 'right'];
|
|
298
|
+
return placements.map((placement) => {
|
|
299
|
+
const { top, left } = calculateHorizontalPlacementPosition(targetRect, calloutRect, placement, padding, arrowSize);
|
|
300
|
+
return {
|
|
301
|
+
placement,
|
|
302
|
+
top,
|
|
303
|
+
left,
|
|
304
|
+
horizontalAlign: 'center',
|
|
305
|
+
valid: isHorizontalPlacementValid(targetRect, calloutRect, placement, viewportWidth, padding, arrowSize),
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
const generateAllPositionCandidates = (rectDimensions, viewport, alignment, hozOffset, padding, arrowSize) => {
|
|
310
|
+
const { targetRect, calloutRect } = rectDimensions;
|
|
311
|
+
const verticalCandidates = generateVerticalPositionCandidates(targetRect, calloutRect, viewport.height, viewport.width, alignment, hozOffset, padding, arrowSize);
|
|
312
|
+
const horizontalCandidates = generateHorizontalPositionCandidates(targetRect, calloutRect, viewport.width, padding, arrowSize);
|
|
313
|
+
return [...verticalCandidates, ...horizontalCandidates];
|
|
314
|
+
};
|
|
315
|
+
// ============================================================================
|
|
316
|
+
// Position Selection
|
|
317
|
+
// ============================================================================
|
|
318
|
+
const selectBestPosition = (candidates) => {
|
|
319
|
+
return candidates.find((p) => p.valid) || candidates[0];
|
|
320
|
+
};
|
|
321
|
+
// ============================================================================
|
|
322
|
+
// Viewport Boundary Adjustment
|
|
323
|
+
// ============================================================================
|
|
324
|
+
const constrainToViewport = (position, calloutRect, viewport, padding) => {
|
|
325
|
+
const left = Math.max(padding, Math.min(position.left, viewport.width - calloutRect.width - padding));
|
|
326
|
+
const top = Math.max(padding, Math.min(position.top, viewport.height - calloutRect.height - padding));
|
|
327
|
+
return { top, left };
|
|
328
|
+
};
|
|
329
|
+
// ============================================================================
|
|
330
|
+
// Main Function
|
|
331
|
+
// ============================================================================
|
|
332
|
+
const calcCalloutPosition = ({ targetElm, calloutElm, setPosition, hozOffset = HORIZONTAL_OFFSET, alignment = 'center', }) => {
|
|
333
|
+
return () => {
|
|
334
|
+
// 1. Get dimensions
|
|
335
|
+
const rectDimensions = getElementDimensions(targetElm, calloutElm);
|
|
336
|
+
const viewport = getViewportDimensions();
|
|
337
|
+
const padding = PADDING;
|
|
338
|
+
const arrowSize = ARROW_SIZE;
|
|
339
|
+
// 2. Generate all position candidates
|
|
340
|
+
const candidates = generateAllPositionCandidates(rectDimensions, viewport, alignment, hozOffset, padding, arrowSize);
|
|
341
|
+
// 3. Select best position
|
|
342
|
+
const bestPosition = selectBestPosition(candidates);
|
|
343
|
+
// 4. Constrain to viewport
|
|
344
|
+
const constrainedPosition = constrainToViewport({ top: bestPosition.top, left: bestPosition.left }, rectDimensions.calloutRect, viewport, padding);
|
|
345
|
+
// 5. Create final position object
|
|
346
|
+
const finalPosition = {
|
|
347
|
+
top: constrainedPosition.top,
|
|
348
|
+
left: constrainedPosition.left,
|
|
349
|
+
placement: bestPosition.placement,
|
|
350
|
+
horizontalAlign: bestPosition.horizontalAlign,
|
|
351
|
+
};
|
|
352
|
+
setPosition(finalPosition);
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
function getElementLayout(element) {
|
|
357
|
+
if (!element?.getBoundingClientRect)
|
|
358
|
+
return null;
|
|
359
|
+
const rect = element.getBoundingClientRect();
|
|
360
|
+
if (rect.width === 0 && rect.height === 0)
|
|
361
|
+
return null;
|
|
362
|
+
return {
|
|
363
|
+
top: rect.top,
|
|
364
|
+
left: rect.left,
|
|
365
|
+
width: rect.width,
|
|
366
|
+
height: rect.height,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const getElementAtPoint = (doc, x, y) => {
|
|
370
|
+
let el = null;
|
|
371
|
+
if ('caretPositionFromPoint' in doc) {
|
|
372
|
+
el = doc.caretPositionFromPoint(x, y)?.offsetNode ?? null;
|
|
373
|
+
}
|
|
374
|
+
el = el ?? doc.elementFromPoint(x, y);
|
|
375
|
+
let element = el;
|
|
376
|
+
while (element && element.nodeType === Node.TEXT_NODE) {
|
|
377
|
+
element = element.parentElement;
|
|
378
|
+
}
|
|
379
|
+
return element;
|
|
380
|
+
};
|
|
381
|
+
function getElementHash(element) {
|
|
382
|
+
return (element.getAttribute('data-clarity-hash') ||
|
|
383
|
+
element.getAttribute('data-clarity-hashalpha') ||
|
|
384
|
+
element.getAttribute('data-clarity-hashbeta'));
|
|
385
|
+
}
|
|
386
|
+
const getElementRank = (hash, elements) => {
|
|
387
|
+
if (!elements)
|
|
210
388
|
return 0;
|
|
211
|
-
return
|
|
389
|
+
return elements.findIndex((e) => e.hash === hash) + 1;
|
|
212
390
|
};
|
|
213
|
-
const buildElementInfo = (
|
|
391
|
+
const buildElementInfo = (hash, rect, heatmapInfo) => {
|
|
392
|
+
if (!rect || !heatmapInfo)
|
|
393
|
+
return null;
|
|
394
|
+
const info = heatmapInfo.elementMapInfo?.[hash];
|
|
395
|
+
if (!info)
|
|
396
|
+
return null;
|
|
397
|
+
const rank = getElementRank(hash, heatmapInfo.sortedElements);
|
|
398
|
+
const clicks = info.totalclicks ?? 0;
|
|
399
|
+
const selector = info.selector ?? '';
|
|
214
400
|
const baseInfo = {
|
|
215
|
-
hash
|
|
216
|
-
clicks
|
|
401
|
+
hash,
|
|
402
|
+
clicks,
|
|
217
403
|
rank,
|
|
218
|
-
selector
|
|
404
|
+
selector,
|
|
219
405
|
};
|
|
220
|
-
if (rect) {
|
|
221
|
-
return { ...baseInfo, ...rect };
|
|
222
|
-
}
|
|
223
406
|
return {
|
|
224
407
|
...baseInfo,
|
|
225
|
-
|
|
226
|
-
top: 0,
|
|
227
|
-
width: 0,
|
|
228
|
-
height: 0,
|
|
408
|
+
...rect,
|
|
229
409
|
};
|
|
230
410
|
};
|
|
231
|
-
|
|
232
|
-
|
|
411
|
+
|
|
412
|
+
function calculateRankPosition(rect, widthScale) {
|
|
413
|
+
const top = rect.top <= 18 ? rect.top + 3 : rect.top - 18;
|
|
414
|
+
const left = rect.left <= 18 ? rect.left + 3 : rect.left - 18;
|
|
415
|
+
return {
|
|
416
|
+
transform: `scale(${1.2 * widthScale})`,
|
|
417
|
+
top: Number.isNaN(top) ? undefined : top,
|
|
418
|
+
left: Number.isNaN(left) ? undefined : left,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function isElementInViewport(elementRect, visualRef, scale) {
|
|
422
|
+
if (!elementRect)
|
|
423
|
+
return false;
|
|
424
|
+
const visualRect = visualRef.current?.getBoundingClientRect();
|
|
425
|
+
if (!visualRect)
|
|
426
|
+
return false;
|
|
427
|
+
// Element position relative to the document (or container's content)
|
|
428
|
+
const elementTop = elementRect.top * scale;
|
|
429
|
+
const elementBottom = (elementRect.top + elementRect.height) * scale;
|
|
430
|
+
// Current scroll position
|
|
431
|
+
const scrollTop = visualRef.current?.scrollTop || 0;
|
|
432
|
+
const viewportHeight = visualRect.height;
|
|
433
|
+
// Visible viewport range in the scrollable content
|
|
434
|
+
const viewportTop = scrollTop;
|
|
435
|
+
const viewportBottom = scrollTop + viewportHeight;
|
|
436
|
+
// Check if element is within the visible viewport
|
|
437
|
+
// Element is visible if it overlaps with the viewport
|
|
438
|
+
return elementBottom > viewportTop && elementTop < viewportBottom;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
class ViewportUnitsFixer {
|
|
442
|
+
iframe = null;
|
|
443
|
+
config;
|
|
444
|
+
constructor(config) {
|
|
445
|
+
this.config = config;
|
|
446
|
+
this.iframe = config.iframe;
|
|
447
|
+
this.init();
|
|
448
|
+
}
|
|
449
|
+
async init() {
|
|
450
|
+
if (!this.iframe) {
|
|
451
|
+
console.error('[Parent] Required elements not found');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
// this.injectScriptContent = await generateIframeInjectScript();
|
|
455
|
+
window.addEventListener('message', this.handleMessage.bind(this));
|
|
456
|
+
if (this.iframe.contentDocument?.readyState === 'complete') {
|
|
457
|
+
await this.injectScript();
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
this.iframe.addEventListener('load', () => this.injectScript());
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async injectScript() {
|
|
464
|
+
if (!this.iframe?.contentWindow || !this.iframe.contentDocument)
|
|
465
|
+
return;
|
|
466
|
+
const win = this.iframe.contentWindow;
|
|
467
|
+
const doc = this.iframe.contentDocument;
|
|
468
|
+
win.__viewportConfig = this.config;
|
|
469
|
+
const script = doc.createElement('script');
|
|
470
|
+
script.textContent = `
|
|
471
|
+
(function() {
|
|
472
|
+
'use strict';
|
|
473
|
+
${(await Promise.resolve().then(function () { return viewportReplacer; })).default}
|
|
474
|
+
new ViewportUnitsReplacer()
|
|
475
|
+
})();
|
|
476
|
+
`;
|
|
477
|
+
script.type = 'text/javascript';
|
|
478
|
+
script.id = 'viewport-replacer';
|
|
479
|
+
script.onload = () => console.log('[Parent] Viewport replacer module loaded');
|
|
480
|
+
script.onerror = () => console.error('[Parent] Không load được replacer script');
|
|
481
|
+
doc.head.appendChild(script);
|
|
482
|
+
}
|
|
483
|
+
handleMessage(event) {
|
|
484
|
+
const data = event.data;
|
|
485
|
+
if (!data || data.type !== 'IFRAME_HEIGHT_CALCULATED')
|
|
486
|
+
return;
|
|
487
|
+
this.config.onSuccess?.(data);
|
|
488
|
+
}
|
|
489
|
+
recalculate() {
|
|
490
|
+
this.injectScript();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function initViewportFixer(config) {
|
|
494
|
+
const fixer = new ViewportUnitsFixer(config);
|
|
495
|
+
window.viewportFixer = fixer;
|
|
496
|
+
window.addEventListener('iframe-dimensions-applied', ((e) => {
|
|
497
|
+
const ev = e;
|
|
498
|
+
console.log('Iframe dimensions finalized:', ev.detail);
|
|
499
|
+
}));
|
|
500
|
+
return fixer;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
class ViewportUnitsReplacer {
|
|
504
|
+
config;
|
|
505
|
+
regex = /([-.\d]+)(vh|svh|lvh|dvh|vw|svw|lvw|dvw)/gi;
|
|
506
|
+
constructor() {
|
|
507
|
+
if (!window.__viewportConfig) {
|
|
508
|
+
throw new Error('[Iframe] Do not have viewport config');
|
|
509
|
+
}
|
|
510
|
+
this.config = window.__viewportConfig;
|
|
511
|
+
console.log('[Iframe] ViewportUnitsReplacer started with config:', this.config);
|
|
512
|
+
this.init();
|
|
513
|
+
}
|
|
514
|
+
px(value) {
|
|
515
|
+
return `${value.toFixed(2)}px`;
|
|
516
|
+
}
|
|
517
|
+
convert(value, unit) {
|
|
518
|
+
const num = parseFloat(value);
|
|
519
|
+
if (isNaN(num))
|
|
520
|
+
return value;
|
|
521
|
+
const map = {
|
|
522
|
+
vh: this.config.targetHeight,
|
|
523
|
+
svh: this.config.targetHeight,
|
|
524
|
+
lvh: this.config.targetHeight,
|
|
525
|
+
dvh: this.config.targetHeight,
|
|
526
|
+
vw: this.config.targetWidth,
|
|
527
|
+
svw: this.config.targetWidth,
|
|
528
|
+
lvw: this.config.targetWidth,
|
|
529
|
+
dvw: this.config.targetWidth,
|
|
530
|
+
};
|
|
531
|
+
return this.px((num / 100) * (map[unit.toLowerCase()] || 0));
|
|
532
|
+
}
|
|
533
|
+
replaceInText(cssText) {
|
|
534
|
+
return cssText.replace(this.regex, (_, value, unit) => this.convert(value, unit));
|
|
535
|
+
}
|
|
536
|
+
processInlineStyles() {
|
|
537
|
+
let count = 0;
|
|
538
|
+
document.querySelectorAll('[style]').forEach((el) => {
|
|
539
|
+
const style = el.getAttribute('style');
|
|
540
|
+
if (style && this.regex.test(style)) {
|
|
541
|
+
el.setAttribute('style', this.replaceInText(style));
|
|
542
|
+
count++;
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
console.log(`[Iframe] Replaced ${count} inline style elements`);
|
|
546
|
+
return count;
|
|
547
|
+
}
|
|
548
|
+
processStyleTags() {
|
|
549
|
+
let count = 0;
|
|
550
|
+
document.querySelectorAll('style').forEach((tag) => {
|
|
551
|
+
const css = tag.textContent || '';
|
|
552
|
+
if (this.regex.test(css)) {
|
|
553
|
+
tag.textContent = this.replaceInText(css);
|
|
554
|
+
count++;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
console.log(`[Iframe] Replaced ${count} <style> tags`);
|
|
558
|
+
return count;
|
|
559
|
+
}
|
|
560
|
+
processRule(rule) {
|
|
561
|
+
let count = 0;
|
|
562
|
+
if ('style' in rule && rule.style) {
|
|
563
|
+
const style = rule.style;
|
|
564
|
+
for (let i = 0; i < style.length; i++) {
|
|
565
|
+
const prop = style[i];
|
|
566
|
+
const value = style.getPropertyValue(prop);
|
|
567
|
+
if (value && this.regex.test(value)) {
|
|
568
|
+
style.setProperty(prop, this.replaceInText(value), style.getPropertyPriority(prop));
|
|
569
|
+
count++;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if ('cssRules' in rule) {
|
|
574
|
+
const rules = rule;
|
|
575
|
+
for (const r of Array.from(rules.cssRules || [])) {
|
|
576
|
+
count += this.processRule(r);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return count;
|
|
580
|
+
}
|
|
581
|
+
processStylesheets() {
|
|
582
|
+
let total = 0;
|
|
583
|
+
Array.from(document.styleSheets).forEach((sheet) => {
|
|
584
|
+
try {
|
|
585
|
+
// Bỏ qua external CSS (cross-origin)
|
|
586
|
+
if (sheet.href && !sheet.href.startsWith(location.origin)) {
|
|
587
|
+
console.log('[Iframe] Skipping external CSS:', sheet.href);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const rules = sheet.cssRules || sheet.rules;
|
|
591
|
+
if (!rules)
|
|
592
|
+
return;
|
|
593
|
+
for (const rule of Array.from(rules)) {
|
|
594
|
+
total += this.processRule(rule);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
catch (e) {
|
|
598
|
+
console.warn('[Iframe] Cannot read stylesheet (CORS?):', e.message);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
console.log(`[Iframe] Replaced ${total} rules in stylesheets`);
|
|
602
|
+
return total;
|
|
603
|
+
}
|
|
604
|
+
async processLinkedStylesheets() {
|
|
605
|
+
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
606
|
+
let count = 0;
|
|
607
|
+
for (const link of Array.from(links)) {
|
|
608
|
+
if (!link.href.startsWith(location.origin)) {
|
|
609
|
+
console.log('[Iframe] Skipping external CSS:', link.href);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const res = await fetch(link.href);
|
|
614
|
+
let css = await res.text();
|
|
615
|
+
if (this.regex.test(css)) {
|
|
616
|
+
css = this.replaceInText(css);
|
|
617
|
+
const style = document.createElement('style');
|
|
618
|
+
style.textContent = css;
|
|
619
|
+
style.dataset.originalHref = link.href;
|
|
620
|
+
link.parentNode?.insertBefore(style, link);
|
|
621
|
+
link.remove();
|
|
622
|
+
count++;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch (e) {
|
|
626
|
+
console.warn('[Iframe] Cannot load CSS:', link.href, e);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
console.log(`[Iframe] Replaced ${count} linked CSS files`);
|
|
630
|
+
return count;
|
|
631
|
+
}
|
|
632
|
+
getFinalHeight() {
|
|
633
|
+
// Trigger reflow
|
|
634
|
+
void document.body.offsetHeight;
|
|
635
|
+
return Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight, document.documentElement.clientHeight);
|
|
636
|
+
}
|
|
637
|
+
notifyParent(height) {
|
|
638
|
+
window.parent.postMessage({
|
|
639
|
+
type: 'IFRAME_HEIGHT_CALCULATED',
|
|
640
|
+
height,
|
|
641
|
+
width: document.body.scrollWidth,
|
|
642
|
+
}, '*');
|
|
643
|
+
console.log('[Iframe] Sent height to parent:', height);
|
|
644
|
+
}
|
|
645
|
+
async waitForResources() {
|
|
646
|
+
if ('fonts' in document) {
|
|
647
|
+
await document.fonts.ready;
|
|
648
|
+
}
|
|
649
|
+
const images = Array.from(document.images).filter((img) => !img.complete);
|
|
650
|
+
if (images.length > 0) {
|
|
651
|
+
await Promise.all(images.map((img) => new Promise((resolve) => {
|
|
652
|
+
img.onload = img.onerror = resolve;
|
|
653
|
+
})));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async run() {
|
|
657
|
+
try {
|
|
658
|
+
this.processInlineStyles();
|
|
659
|
+
this.processStyleTags();
|
|
660
|
+
this.processStylesheets();
|
|
661
|
+
await this.processLinkedStylesheets();
|
|
662
|
+
// await this.waitForResources();
|
|
663
|
+
requestAnimationFrame(() => {
|
|
664
|
+
const height = this.getFinalHeight();
|
|
665
|
+
this.notifyParent(height);
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
console.error('[Iframe] Critical error:', err);
|
|
670
|
+
this.notifyParent(document.body.scrollHeight || 1000);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
init() {
|
|
674
|
+
if (document.readyState === 'loading') {
|
|
675
|
+
document.addEventListener('DOMContentLoaded', () => this.run());
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
this.run();
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
var viewportReplacer = /*#__PURE__*/Object.freeze({
|
|
684
|
+
__proto__: null,
|
|
685
|
+
default: ViewportUnitsReplacer
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const scrollToElementIfNeeded = (visualRef, rect, scale) => {
|
|
689
|
+
if (!visualRef.current)
|
|
233
690
|
return;
|
|
234
691
|
const visualRect = visualRef.current.getBoundingClientRect();
|
|
235
692
|
if (isElementInViewport(rect, visualRef, scale)) {
|
|
@@ -245,13 +702,14 @@ const scrollToElementIfNeeded = (visualRef, wrapperRef, rect, scale) => {
|
|
|
245
702
|
behavior: 'smooth',
|
|
246
703
|
});
|
|
247
704
|
};
|
|
248
|
-
const useClickedElement = ({ visualRef,
|
|
705
|
+
const useClickedElement = ({ visualRef, getRect }) => {
|
|
249
706
|
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
250
707
|
const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
|
|
708
|
+
const shouldShowCallout = useHeatmapInteractionStore((state) => state.shouldShowCallout);
|
|
709
|
+
const setShouldShowCallout = useHeatmapInteractionStore((state) => state.setShouldShowCallout);
|
|
251
710
|
const scale = useHeatmapVizStore((state) => state.scale);
|
|
252
711
|
const [clickedElement, setClickedElement] = useState(null);
|
|
253
712
|
const [showMissingElement, setShowMissingElement] = useState(false);
|
|
254
|
-
const [shouldShowCallout, setShouldShowCallout] = useState(false);
|
|
255
713
|
const reset = () => {
|
|
256
714
|
setClickedElement(null);
|
|
257
715
|
setShowMissingElement(false);
|
|
@@ -267,26 +725,58 @@ const useClickedElement = ({ visualRef, wrapperRef, getRect }) => {
|
|
|
267
725
|
setClickedElement(null);
|
|
268
726
|
return;
|
|
269
727
|
}
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
scrollToElementIfNeeded(visualRef, wrapperRef, rect, scale);
|
|
276
|
-
setShouldShowCallout(true);
|
|
277
|
-
requestAnimationFrame(() => {
|
|
278
|
-
setClickedElement(elementInfo);
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
728
|
+
const hash = selectedElement;
|
|
729
|
+
const selector = info.selector;
|
|
730
|
+
const rect = getRect({ hash: selectedElement, selector });
|
|
731
|
+
const elementInfo = buildElementInfo(hash, rect, heatmapInfo);
|
|
732
|
+
if (!rect) {
|
|
282
733
|
setClickedElement(elementInfo);
|
|
283
734
|
setShowMissingElement(true);
|
|
284
735
|
setShouldShowCallout(false);
|
|
736
|
+
return;
|
|
285
737
|
}
|
|
286
|
-
|
|
738
|
+
setShowMissingElement(false);
|
|
739
|
+
scrollToElementIfNeeded(visualRef, rect, scale);
|
|
740
|
+
setShouldShowCallout(true);
|
|
741
|
+
requestAnimationFrame(() => {
|
|
742
|
+
setClickedElement(elementInfo);
|
|
743
|
+
});
|
|
744
|
+
}, [selectedElement, heatmapInfo, getRect, visualRef, scale]);
|
|
287
745
|
return { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout };
|
|
288
746
|
};
|
|
289
747
|
|
|
748
|
+
const useElementCalloutVisible = ({ visualRef, getRect }) => {
|
|
749
|
+
const scale = useHeatmapVizStore((state) => state.scale);
|
|
750
|
+
const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
|
|
751
|
+
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
752
|
+
const setShouldShowCallout = useHeatmapInteractionStore((state) => state.setShouldShowCallout);
|
|
753
|
+
useEffect(() => {
|
|
754
|
+
if (!selectedElement)
|
|
755
|
+
return;
|
|
756
|
+
const elementIsInViewportFn = () => {
|
|
757
|
+
const elementInfo = heatmapInfo?.elementMapInfo?.[selectedElement];
|
|
758
|
+
if (!elementInfo)
|
|
759
|
+
return;
|
|
760
|
+
const rect = getRect({ hash: selectedElement, selector: elementInfo.selector });
|
|
761
|
+
const isInViewport = isElementInViewport(rect, visualRef, scale);
|
|
762
|
+
setShouldShowCallout(isInViewport);
|
|
763
|
+
};
|
|
764
|
+
elementIsInViewportFn();
|
|
765
|
+
const handleUpdate = () => {
|
|
766
|
+
requestAnimationFrame(elementIsInViewportFn);
|
|
767
|
+
};
|
|
768
|
+
window.addEventListener('scroll', handleUpdate, true);
|
|
769
|
+
window.addEventListener('resize', handleUpdate);
|
|
770
|
+
visualRef?.current?.addEventListener('scroll', handleUpdate);
|
|
771
|
+
return () => {
|
|
772
|
+
window.removeEventListener('scroll', handleUpdate, true);
|
|
773
|
+
window.removeEventListener('resize', handleUpdate);
|
|
774
|
+
visualRef?.current?.removeEventListener('scroll', handleUpdate);
|
|
775
|
+
};
|
|
776
|
+
}, [selectedElement, visualRef, getRect, scale, heatmapInfo, setShouldShowCallout]);
|
|
777
|
+
return {};
|
|
778
|
+
};
|
|
779
|
+
|
|
290
780
|
const useHeatmapEffects = ({ isVisible, isElementSidebarOpen, setShouldShowCallout, resetAll, }) => {
|
|
291
781
|
const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
|
|
292
782
|
// Reset khi ẩn
|
|
@@ -305,32 +795,6 @@ const useHeatmapEffects = ({ isVisible, isElementSidebarOpen, setShouldShowCallo
|
|
|
305
795
|
}, [isElementSidebarOpen, selectedElement, setShouldShowCallout]);
|
|
306
796
|
};
|
|
307
797
|
|
|
308
|
-
function getElementLayout(element) {
|
|
309
|
-
if (!element?.getBoundingClientRect)
|
|
310
|
-
return null;
|
|
311
|
-
const rect = element.getBoundingClientRect();
|
|
312
|
-
if (rect.width === 0 && rect.height === 0)
|
|
313
|
-
return null;
|
|
314
|
-
return {
|
|
315
|
-
top: rect.top,
|
|
316
|
-
left: rect.left,
|
|
317
|
-
width: rect.width,
|
|
318
|
-
height: rect.height,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
function formatPercentage(value, decimals = 2) {
|
|
322
|
-
return value.toFixed(decimals);
|
|
323
|
-
}
|
|
324
|
-
function calculateRankPosition(rect, widthScale) {
|
|
325
|
-
const top = rect.top <= 18 ? rect.top + 3 : rect.top - 18;
|
|
326
|
-
const left = rect.left <= 18 ? rect.left + 3 : rect.left - 18;
|
|
327
|
-
return {
|
|
328
|
-
transform: `scale(${1.2 * widthScale})`,
|
|
329
|
-
top: Number.isNaN(top) ? undefined : top,
|
|
330
|
-
left: Number.isNaN(left) ? undefined : left,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
798
|
const useHeatmapElementPosition = ({ iframeRef, wrapperRef, visualizer }) => {
|
|
335
799
|
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
336
800
|
const iframeHeight = useHeatmapVizStore((state) => state.iframeHeight);
|
|
@@ -380,70 +844,63 @@ const debounce = (fn, delay) => {
|
|
|
380
844
|
timeout = setTimeout(() => fn(...args), delay);
|
|
381
845
|
};
|
|
382
846
|
};
|
|
847
|
+
|
|
383
848
|
const useHoveredElement = ({ iframeRef, getRect }) => {
|
|
384
849
|
const hoveredElement = useHeatmapInteractionStore((state) => state.hoveredElement);
|
|
385
850
|
const setHoveredElement = useHeatmapInteractionStore((state) => state.setHoveredElement);
|
|
386
851
|
const onSelect = useHeatmapInteractionStore((state) => state.setSelectedElement);
|
|
387
852
|
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
388
853
|
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
854
|
+
const reset = useCallback(() => {
|
|
855
|
+
setHoveredElement(null);
|
|
856
|
+
}, [setHoveredElement]);
|
|
857
|
+
const handleMouseLeave = useCallback(() => {
|
|
858
|
+
reset();
|
|
859
|
+
}, [reset]);
|
|
860
|
+
const getHashFromEvent = useCallback((event) => {
|
|
861
|
+
if (!heatmapInfo || !isIframeReady(iframeRef, heatmapInfo)) {
|
|
862
|
+
reset();
|
|
392
863
|
return;
|
|
393
864
|
}
|
|
394
865
|
const iframe = iframeRef.current;
|
|
395
|
-
const iframeRect = iframe.getBoundingClientRect();
|
|
396
|
-
let x = event.clientX - iframeRect.left;
|
|
397
|
-
let y = event.clientY - iframeRect.top;
|
|
398
|
-
if (widthScale !== 1) {
|
|
399
|
-
x /= widthScale;
|
|
400
|
-
y /= widthScale;
|
|
401
|
-
}
|
|
402
866
|
const doc = iframe.contentDocument;
|
|
403
|
-
|
|
404
|
-
|
|
867
|
+
const iframeRect = iframe.getBoundingClientRect();
|
|
868
|
+
const { x, y } = convertViewportToIframeCoords(event.clientX, event.clientY, iframeRect, widthScale);
|
|
869
|
+
const targetElement = findTargetElement(doc, x, y);
|
|
870
|
+
if (!targetElement || !isValidElement(targetElement, heatmapInfo)) {
|
|
871
|
+
reset();
|
|
405
872
|
return;
|
|
406
873
|
}
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
874
|
+
const hash = getElementHash(targetElement);
|
|
875
|
+
if (!!hash)
|
|
876
|
+
return hash;
|
|
877
|
+
reset();
|
|
878
|
+
return;
|
|
879
|
+
}, [heatmapInfo, iframeRef, getRect, widthScale, reset]);
|
|
880
|
+
const handleMouseMove = useCallback(debounce((event) => {
|
|
881
|
+
if (!heatmapInfo) {
|
|
882
|
+
reset();
|
|
413
883
|
return;
|
|
414
884
|
}
|
|
415
|
-
const hash =
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (!hash || !heatmapInfo.elementMapInfo[hash]) {
|
|
419
|
-
setHoveredElement(null);
|
|
885
|
+
const hash = getHashFromEvent(event);
|
|
886
|
+
if (!hash) {
|
|
887
|
+
reset();
|
|
420
888
|
return;
|
|
421
889
|
}
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
hash,
|
|
429
|
-
clicks: info.totalclicks ?? 0,
|
|
430
|
-
rank,
|
|
431
|
-
selector: info.selector ?? '',
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
setHoveredElement(null);
|
|
890
|
+
const selector = heatmapInfo?.elementMapInfo?.[hash];
|
|
891
|
+
const rect = getRect({ hash, selector });
|
|
892
|
+
const elementInfo = buildElementInfo(hash, rect, heatmapInfo);
|
|
893
|
+
if (!elementInfo) {
|
|
894
|
+
reset();
|
|
895
|
+
return;
|
|
436
896
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const handleMouseLeave = useCallback(() => {
|
|
440
|
-
setHoveredElement(null);
|
|
441
|
-
}, []);
|
|
897
|
+
setHoveredElement(elementInfo);
|
|
898
|
+
}, 16), [heatmapInfo, getRect, reset, getHashFromEvent]);
|
|
442
899
|
const handleClick = useCallback(() => {
|
|
443
|
-
if (hoveredElement?.hash
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}, [hoveredElement
|
|
900
|
+
if (!hoveredElement?.hash)
|
|
901
|
+
return;
|
|
902
|
+
onSelect(hoveredElement.hash);
|
|
903
|
+
}, [hoveredElement?.hash]);
|
|
447
904
|
return {
|
|
448
905
|
hoveredElement,
|
|
449
906
|
handleMouseMove,
|
|
@@ -451,41 +908,54 @@ const useHoveredElement = ({ iframeRef, getRect }) => {
|
|
|
451
908
|
handleClick,
|
|
452
909
|
};
|
|
453
910
|
};
|
|
454
|
-
const
|
|
455
|
-
let
|
|
456
|
-
|
|
457
|
-
|
|
911
|
+
const convertViewportToIframeCoords = (clientX, clientY, iframeRect, scale) => {
|
|
912
|
+
let x = clientX - iframeRect.left;
|
|
913
|
+
let y = clientY - iframeRect.top;
|
|
914
|
+
if (scale !== 1) {
|
|
915
|
+
x /= scale;
|
|
916
|
+
y /= scale;
|
|
458
917
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
918
|
+
return { x, y };
|
|
919
|
+
};
|
|
920
|
+
const findTargetElement = (doc, x, y) => {
|
|
921
|
+
let targetElement = getElementAtPoint(doc, x, y);
|
|
922
|
+
if (!targetElement) {
|
|
923
|
+
targetElement = doc.elementFromPoint(x, y);
|
|
463
924
|
}
|
|
464
|
-
return
|
|
925
|
+
return targetElement;
|
|
926
|
+
};
|
|
927
|
+
const isIframeReady = (iframeRef, heatmapInfo) => {
|
|
928
|
+
return !!(iframeRef.current?.contentDocument && heatmapInfo?.elementMapInfo);
|
|
929
|
+
};
|
|
930
|
+
const isValidElement = (element, heatmapInfo) => {
|
|
931
|
+
if (!element)
|
|
932
|
+
return false;
|
|
933
|
+
const hash = getElementHash(element);
|
|
934
|
+
if (!hash)
|
|
935
|
+
return false;
|
|
936
|
+
return !!heatmapInfo?.elementMapInfo?.[hash];
|
|
465
937
|
};
|
|
466
938
|
|
|
467
939
|
const useHeatmapRender = () => {
|
|
468
940
|
const data = useHeatmapDataStore((state) => state.data);
|
|
469
941
|
const config = useHeatmapDataStore((state) => state.config);
|
|
470
942
|
const setVizRef = useHeatmapVizStore((state) => state.setVizRef);
|
|
943
|
+
const setIsRenderViz = useHeatmapVizStore((state) => state.setIsRenderViz);
|
|
944
|
+
const setIframeHeight = useHeatmapVizStore((state) => state.setIframeHeight);
|
|
471
945
|
const iframeRef = useRef(null);
|
|
472
946
|
const renderHeatmap = useCallback(async (payloads) => {
|
|
473
947
|
if (!payloads || payloads.length === 0)
|
|
474
948
|
return;
|
|
949
|
+
setIsRenderViz(false);
|
|
475
950
|
const visualizer = new Visualizer();
|
|
476
951
|
const iframe = iframeRef.current;
|
|
477
952
|
if (!iframe?.contentWindow)
|
|
478
953
|
return;
|
|
479
954
|
await visualizer.html(payloads, iframe.contentWindow);
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
// const isRemove = !(isClosedEcomsendWidget || isEcomsendWidget);
|
|
485
|
-
// if (isRemove) {
|
|
486
|
-
// element.remove();
|
|
487
|
-
// }
|
|
488
|
-
// });
|
|
955
|
+
reset(iframe, payloads, (height) => {
|
|
956
|
+
height && setIframeHeight(height);
|
|
957
|
+
setIsRenderViz(true);
|
|
958
|
+
});
|
|
489
959
|
setVizRef(visualizer);
|
|
490
960
|
}, []);
|
|
491
961
|
useEffect(() => {
|
|
@@ -500,6 +970,50 @@ const useHeatmapRender = () => {
|
|
|
500
970
|
iframeRef,
|
|
501
971
|
};
|
|
502
972
|
};
|
|
973
|
+
function sort(a, b) {
|
|
974
|
+
return a.time - b.time;
|
|
975
|
+
}
|
|
976
|
+
function findLastSizeOfDom(data) {
|
|
977
|
+
const firstDoc = data.find((item) => item.envelope.sequence === 1)?.doc;
|
|
978
|
+
const docSorted = firstDoc?.sort(sort);
|
|
979
|
+
const firstEvent = docSorted?.[0];
|
|
980
|
+
const docSize = {
|
|
981
|
+
width: firstEvent?.data.width,
|
|
982
|
+
height: firstEvent?.data.height,
|
|
983
|
+
};
|
|
984
|
+
const newData = JSON.parse(JSON.stringify(data));
|
|
985
|
+
const reversedData = newData.reverse();
|
|
986
|
+
const lastResizeEvent = reversedData.find((item) => !!item.resize);
|
|
987
|
+
const firstEventResize = lastResizeEvent?.resize?.[0];
|
|
988
|
+
const resize = {
|
|
989
|
+
width: firstEventResize?.data.width,
|
|
990
|
+
height: firstEventResize?.data.height,
|
|
991
|
+
};
|
|
992
|
+
return {
|
|
993
|
+
doc: docSize,
|
|
994
|
+
resize: resize,
|
|
995
|
+
size: {
|
|
996
|
+
width: resize.width ?? docSize.width,
|
|
997
|
+
height: resize.height ?? docSize.height,
|
|
998
|
+
},
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function reset(iframe, payloads, onSuccess) {
|
|
1002
|
+
const { size } = findLastSizeOfDom(payloads);
|
|
1003
|
+
const docWidth = size.width ?? 0;
|
|
1004
|
+
const docHeight = size.height ?? 0;
|
|
1005
|
+
const viewportFixer = initViewportFixer({
|
|
1006
|
+
targetWidth: docWidth,
|
|
1007
|
+
targetHeight: docHeight,
|
|
1008
|
+
iframe: iframe,
|
|
1009
|
+
onSuccess: (data) => {
|
|
1010
|
+
onSuccess(data.height);
|
|
1011
|
+
iframe.height = `${data.height}px`;
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
viewportFixer.recalculate();
|
|
1015
|
+
return iframe;
|
|
1016
|
+
}
|
|
503
1017
|
|
|
504
1018
|
function isMobileDevice(userAgent) {
|
|
505
1019
|
if (!userAgent)
|
|
@@ -671,24 +1185,24 @@ const useContainerDimensions = (props) => {
|
|
|
671
1185
|
return { containerWidth, containerHeight };
|
|
672
1186
|
};
|
|
673
1187
|
|
|
674
|
-
const useContentDimensions = (
|
|
675
|
-
const
|
|
676
|
-
const
|
|
1188
|
+
const useContentDimensions = ({ iframeRef, }) => {
|
|
1189
|
+
const config = useHeatmapDataStore((state) => state.config);
|
|
1190
|
+
const contentWidth = config?.width ?? 0;
|
|
677
1191
|
useEffect(() => {
|
|
678
1192
|
if (!contentWidth)
|
|
679
1193
|
return;
|
|
680
1194
|
if (!iframeRef.current)
|
|
681
1195
|
return;
|
|
682
|
-
iframeRef.current.width = `${contentWidth}px`;
|
|
1196
|
+
// iframeRef.current.width = `${contentWidth}px`;
|
|
683
1197
|
}, [contentWidth, iframeRef]);
|
|
684
1198
|
return { contentWidth };
|
|
685
1199
|
};
|
|
686
1200
|
|
|
687
|
-
// Hook 3: Iframe Height Observer
|
|
688
1201
|
const useIframeHeight = (props) => {
|
|
689
|
-
const { iframeRef
|
|
1202
|
+
const { iframeRef } = props;
|
|
690
1203
|
const iframeHeight = useHeatmapVizStore((state) => state.iframeHeight);
|
|
691
1204
|
const setIframeHeight = useHeatmapVizStore((state) => state.setIframeHeight);
|
|
1205
|
+
const isRenderViz = useHeatmapVizStore((state) => state.isRenderViz);
|
|
692
1206
|
const resizeObserverRef = useRef(null);
|
|
693
1207
|
const mutationObserverRef = useRef(null);
|
|
694
1208
|
const updateIframeHeight = useCallback(() => {
|
|
@@ -710,17 +1224,9 @@ const useIframeHeight = (props) => {
|
|
|
710
1224
|
console.warn('Cannot measure iframe content:', error);
|
|
711
1225
|
}
|
|
712
1226
|
}, [iframeRef, setIframeHeight]);
|
|
713
|
-
useEffect(() => {
|
|
714
|
-
if (contentWidth > 0) {
|
|
715
|
-
const timeoutId = setTimeout(() => {
|
|
716
|
-
updateIframeHeight();
|
|
717
|
-
}, 100);
|
|
718
|
-
return () => clearTimeout(timeoutId);
|
|
719
|
-
}
|
|
720
|
-
}, [contentWidth]);
|
|
721
1227
|
useEffect(() => {
|
|
722
1228
|
const iframe = iframeRef.current;
|
|
723
|
-
if (!iframe)
|
|
1229
|
+
if (!iframe || !isRenderViz)
|
|
724
1230
|
return;
|
|
725
1231
|
const setupObservers = () => {
|
|
726
1232
|
try {
|
|
@@ -772,7 +1278,7 @@ const useIframeHeight = (props) => {
|
|
|
772
1278
|
}
|
|
773
1279
|
iframe.removeEventListener('load', setupObservers);
|
|
774
1280
|
};
|
|
775
|
-
}, [iframeRef, updateIframeHeight]);
|
|
1281
|
+
}, [iframeRef, isRenderViz, updateIframeHeight]);
|
|
776
1282
|
return { iframeHeight };
|
|
777
1283
|
};
|
|
778
1284
|
|
|
@@ -802,7 +1308,6 @@ const useScrollSync = ({ iframeRef }) => {
|
|
|
802
1308
|
if (iframeWindow && iframeDocument) {
|
|
803
1309
|
const iframeScrollTop = scrollTop / widthScale;
|
|
804
1310
|
iframe.style.top = `${iframeScrollTop}px`;
|
|
805
|
-
// iframeWindow.scrollTo({ top: iframeScrollTop, behavior: 'smooth' });
|
|
806
1311
|
}
|
|
807
1312
|
}
|
|
808
1313
|
catch (error) {
|
|
@@ -813,13 +1318,14 @@ const useScrollSync = ({ iframeRef }) => {
|
|
|
813
1318
|
};
|
|
814
1319
|
|
|
815
1320
|
const useHeatmapScale = (props) => {
|
|
1321
|
+
// const iframeHeight = useHeatmapVizStore((state) => state.iframeHeight);
|
|
816
1322
|
const { wrapperRef, iframeRef, visualRef } = props;
|
|
817
1323
|
// 1. Observe container dimensions
|
|
818
1324
|
const { containerWidth, containerHeight } = useContainerDimensions({ wrapperRef });
|
|
819
1325
|
// 2. Get content dimensions from config
|
|
820
1326
|
const { contentWidth } = useContentDimensions({ iframeRef });
|
|
821
1327
|
// 3. Observe iframe height (now reacts to width changes)
|
|
822
|
-
const { iframeHeight } = useIframeHeight({ iframeRef
|
|
1328
|
+
const { iframeHeight } = useIframeHeight({ iframeRef });
|
|
823
1329
|
// 4. Calculate scale
|
|
824
1330
|
const { scale } = useScaleCalculation({ containerWidth, contentWidth });
|
|
825
1331
|
// 5. Setup scroll sync
|
|
@@ -834,7 +1340,7 @@ const useHeatmapScale = (props) => {
|
|
|
834
1340
|
};
|
|
835
1341
|
};
|
|
836
1342
|
|
|
837
|
-
const BoxStack = ({ children, ...props }) => {
|
|
1343
|
+
const BoxStack = forwardRef(({ children, ...props }, ref) => {
|
|
838
1344
|
const id = props.id;
|
|
839
1345
|
const flexDirection = props.flexDirection;
|
|
840
1346
|
const overflow = props.overflow || 'hidden';
|
|
@@ -845,6 +1351,9 @@ const BoxStack = ({ children, ...props }) => {
|
|
|
845
1351
|
const style = props.style || {};
|
|
846
1352
|
const gap = props.gap || 0;
|
|
847
1353
|
const height = props.height || 'auto';
|
|
1354
|
+
const isZIndexDefined = typeof props.zIndex !== undefined;
|
|
1355
|
+
const zIndex = props.zIndex;
|
|
1356
|
+
const backgroundColor = props.backgroundColor || 'transparent';
|
|
848
1357
|
const styleGap = useMemo(() => {
|
|
849
1358
|
switch (flexDirection) {
|
|
850
1359
|
case 'row':
|
|
@@ -866,15 +1375,17 @@ const BoxStack = ({ children, ...props }) => {
|
|
|
866
1375
|
justifyContent,
|
|
867
1376
|
alignItems,
|
|
868
1377
|
height,
|
|
1378
|
+
backgroundColor,
|
|
1379
|
+
...(isZIndexDefined ? { zIndex } : {}),
|
|
869
1380
|
...styleGap,
|
|
870
1381
|
...style,
|
|
871
1382
|
};
|
|
872
|
-
return (jsx("div", { id: id, style: styleProps, children: children }));
|
|
873
|
-
};
|
|
1383
|
+
return (jsx("div", { id: id, style: styleProps, ref: ref, children: children }));
|
|
1384
|
+
});
|
|
874
1385
|
|
|
875
1386
|
const ContentTopBar = () => {
|
|
876
1387
|
const controls = useHeatmapControlStore((state) => state.controls);
|
|
877
|
-
return (jsx(BoxStack, { id: "gx-hm-content-header", flexDirection: "row", alignItems: "center", overflow: "auto", style: {
|
|
1388
|
+
return (jsx(BoxStack, { id: "gx-hm-content-header", flexDirection: "row", alignItems: "center", overflow: "auto", zIndex: 1, backgroundColor: "white", style: {
|
|
878
1389
|
borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
|
|
879
1390
|
}, children: controls.TopBar ?? null }));
|
|
880
1391
|
};
|
|
@@ -912,110 +1423,14 @@ const useHeatmapVizCanvas = ({ type }) => {
|
|
|
912
1423
|
return heatmapRender?.();
|
|
913
1424
|
};
|
|
914
1425
|
|
|
915
|
-
const CLICKED_ELEMENT_ID = '
|
|
916
|
-
const SECONDARY_CLICKED_ELEMENT_ID = '
|
|
917
|
-
const HOVERED_ELEMENT_ID = '
|
|
918
|
-
const SECONDARY_HOVERED_ELEMENT_ID = '
|
|
919
|
-
|
|
920
|
-
const ElementCallout = (props) => {
|
|
921
|
-
const { element, target, isSecondary, language, wrapperRef } = props;
|
|
922
|
-
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
923
|
-
const calloutRef = useRef(null);
|
|
924
|
-
const [position, setPosition] = useState({
|
|
925
|
-
top: 0,
|
|
926
|
-
left: 0,
|
|
927
|
-
placement: 'top',
|
|
928
|
-
});
|
|
929
|
-
const totalClicks = heatmapInfo?.totalClicks ?? 1;
|
|
930
|
-
const percentage = formatPercentage(((element.clicks ?? 0) / totalClicks) * 100, 2);
|
|
931
|
-
useEffect(() => {
|
|
932
|
-
const targetElement = document.querySelector(target);
|
|
933
|
-
const calloutElement = calloutRef.current;
|
|
934
|
-
if (!targetElement || !calloutElement)
|
|
935
|
-
return;
|
|
936
|
-
const calculatePosition = () => {
|
|
937
|
-
const targetRect = targetElement.getBoundingClientRect();
|
|
938
|
-
const calloutRect = calloutElement.getBoundingClientRect();
|
|
939
|
-
const viewportWidth = window.innerWidth;
|
|
940
|
-
const viewportHeight = window.innerHeight;
|
|
941
|
-
const padding = 12; // Space between target and callout
|
|
942
|
-
const arrowSize = 8;
|
|
943
|
-
let top = 0;
|
|
944
|
-
let left = 0;
|
|
945
|
-
let placement = 'top';
|
|
946
|
-
const positions = [
|
|
947
|
-
{
|
|
948
|
-
placement: 'top',
|
|
949
|
-
top: targetRect.top - calloutRect.height - padding - arrowSize,
|
|
950
|
-
left: targetRect.left + targetRect.width / 2 - calloutRect.width / 2,
|
|
951
|
-
valid: targetRect.top - calloutRect.height - padding - arrowSize > 0,
|
|
952
|
-
},
|
|
953
|
-
{
|
|
954
|
-
placement: 'bottom',
|
|
955
|
-
top: targetRect.bottom + padding + arrowSize,
|
|
956
|
-
left: targetRect.left + targetRect.width / 2 - calloutRect.width / 2,
|
|
957
|
-
valid: targetRect.bottom + calloutRect.height + padding + arrowSize < viewportHeight,
|
|
958
|
-
},
|
|
959
|
-
{
|
|
960
|
-
placement: 'right',
|
|
961
|
-
top: targetRect.top + targetRect.height / 2 - calloutRect.height / 2,
|
|
962
|
-
left: targetRect.right + padding + arrowSize,
|
|
963
|
-
valid: targetRect.right + calloutRect.width + padding + arrowSize < viewportWidth,
|
|
964
|
-
},
|
|
965
|
-
{
|
|
966
|
-
placement: 'left',
|
|
967
|
-
top: targetRect.top + targetRect.height / 2 - calloutRect.height / 2,
|
|
968
|
-
left: targetRect.left - calloutRect.width - padding - arrowSize,
|
|
969
|
-
valid: targetRect.left - calloutRect.width - padding - arrowSize > 0,
|
|
970
|
-
},
|
|
971
|
-
];
|
|
972
|
-
// Find first valid position
|
|
973
|
-
const validPosition = positions.find((p) => p.valid) || positions[0];
|
|
974
|
-
top = validPosition.top;
|
|
975
|
-
left = validPosition.left;
|
|
976
|
-
placement = validPosition.placement;
|
|
977
|
-
// Keep within viewport bounds
|
|
978
|
-
left = Math.max(padding, Math.min(left, viewportWidth - calloutRect.width - padding));
|
|
979
|
-
top = Math.max(padding, Math.min(top, viewportHeight - calloutRect.height - padding));
|
|
980
|
-
setPosition({ top, left, placement });
|
|
981
|
-
};
|
|
982
|
-
calculatePosition();
|
|
983
|
-
const handleUpdate = () => {
|
|
984
|
-
requestAnimationFrame(calculatePosition);
|
|
985
|
-
};
|
|
986
|
-
window.addEventListener('scroll', handleUpdate, true);
|
|
987
|
-
window.addEventListener('resize', handleUpdate);
|
|
988
|
-
wrapperRef?.current?.addEventListener('scroll', handleUpdate);
|
|
989
|
-
return () => {
|
|
990
|
-
window.removeEventListener('scroll', handleUpdate, true);
|
|
991
|
-
window.removeEventListener('resize', handleUpdate);
|
|
992
|
-
wrapperRef?.current?.removeEventListener('scroll', handleUpdate);
|
|
993
|
-
};
|
|
994
|
-
}, [target, wrapperRef]);
|
|
995
|
-
const calloutContent = (jsxs("div", { ref: calloutRef, className: `clarity-callout clarity-callout--${position.placement} ${isSecondary ? 'clarity-callout--secondary' : ''}`, style: {
|
|
996
|
-
position: 'fixed',
|
|
997
|
-
top: position.top,
|
|
998
|
-
left: position.left,
|
|
999
|
-
zIndex: 2147483647,
|
|
1000
|
-
}, "aria-live": "assertive", role: "tooltip", children: [jsx("div", { className: "clarity-callout__arrow" }), jsx("div", { className: "clarity-callout__content", children: jsxs("div", { className: "clarity-callout__stats", children: [jsx("div", { className: "clarity-callout__label", children: "Clicks" }), jsxs("div", { className: "clarity-callout__value", children: [jsx("span", { className: "clarity-callout__count", children: element.clicks?.toLocaleString(language) }), jsxs("span", { className: "clarity-callout__percentage", children: ["(", percentage, "%)"] })] })] }) })] }));
|
|
1001
|
-
return createPortal(calloutContent, document.getElementById('gx-hm-viz-container'));
|
|
1002
|
-
};
|
|
1426
|
+
const CLICKED_ELEMENT_ID = 'gx-hm-clicked-element';
|
|
1427
|
+
const SECONDARY_CLICKED_ELEMENT_ID = 'gx-hm-secondary-clicked-element';
|
|
1428
|
+
const HOVERED_ELEMENT_ID = 'gx-hm-hovered-element';
|
|
1429
|
+
const SECONDARY_HOVERED_ELEMENT_ID = 'gx-hm-secondary-hovered-element';
|
|
1003
1430
|
|
|
1004
1431
|
const RankBadge = ({ index, elementRect, widthScale, clickOnElement, }) => {
|
|
1005
1432
|
const style = calculateRankPosition(elementRect, widthScale);
|
|
1006
|
-
return (jsx("div", { className: "
|
|
1007
|
-
};
|
|
1008
|
-
|
|
1009
|
-
const ClickedElementOverlay = ({ element, shouldShowCallout, isSecondary, targetId, }) => {
|
|
1010
|
-
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
1011
|
-
if (!element || (element.width === 0 && element.height === 0))
|
|
1012
|
-
return null;
|
|
1013
|
-
return (jsxs(Fragment$1, { children: [jsx("div", { className: "heatmapElement", id: targetId, style: {
|
|
1014
|
-
top: element.top,
|
|
1015
|
-
left: element.left,
|
|
1016
|
-
width: element.width,
|
|
1017
|
-
height: element.height,
|
|
1018
|
-
} }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: widthScale }), shouldShowCallout && (jsx(ElementCallout, { element: element, target: `#${targetId}`, isSecondary: isSecondary }))] }));
|
|
1433
|
+
return (jsx("div", { className: "gx-hm-rank-badge", style: style, onClick: clickOnElement, children: index }));
|
|
1019
1434
|
};
|
|
1020
1435
|
|
|
1021
1436
|
const NUMBER_OF_TOP_ELEMENTS = 10;
|
|
@@ -1033,27 +1448,57 @@ const DefaultRankBadges = ({ getRect, hidden }) => {
|
|
|
1033
1448
|
}) }));
|
|
1034
1449
|
};
|
|
1035
1450
|
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1451
|
+
const DEFAULT_POSITION = {
|
|
1452
|
+
top: 0,
|
|
1453
|
+
left: 0,
|
|
1454
|
+
placement: 'top',
|
|
1455
|
+
horizontalAlign: 'center',
|
|
1456
|
+
};
|
|
1457
|
+
const ElementCallout = (props) => {
|
|
1458
|
+
const CompElementCallout = useHeatmapControlStore((state) => state.controls.ElementCallout);
|
|
1459
|
+
const { element, target, visualRef, hozOffset, alignment = 'left' } = props;
|
|
1460
|
+
const calloutRef = useRef(null);
|
|
1461
|
+
const [position, setPosition] = useState(DEFAULT_POSITION);
|
|
1462
|
+
useEffect(() => {
|
|
1463
|
+
const targetElm = document.querySelector(target);
|
|
1464
|
+
const calloutElm = calloutRef.current;
|
|
1465
|
+
if (!targetElm || !calloutElm)
|
|
1466
|
+
return;
|
|
1467
|
+
const positionFn = calcCalloutPosition({
|
|
1468
|
+
targetElm,
|
|
1469
|
+
calloutElm,
|
|
1470
|
+
setPosition,
|
|
1471
|
+
hozOffset,
|
|
1472
|
+
alignment,
|
|
1473
|
+
});
|
|
1474
|
+
positionFn();
|
|
1475
|
+
const handleUpdate = () => {
|
|
1476
|
+
requestAnimationFrame(positionFn);
|
|
1477
|
+
};
|
|
1478
|
+
window.addEventListener('scroll', handleUpdate, true);
|
|
1479
|
+
window.addEventListener('resize', handleUpdate);
|
|
1480
|
+
visualRef?.current?.addEventListener('scroll', handleUpdate);
|
|
1481
|
+
return () => {
|
|
1482
|
+
window.removeEventListener('scroll', handleUpdate, true);
|
|
1483
|
+
window.removeEventListener('resize', handleUpdate);
|
|
1484
|
+
visualRef?.current?.removeEventListener('scroll', handleUpdate);
|
|
1485
|
+
};
|
|
1486
|
+
}, [target, visualRef, hozOffset, alignment]);
|
|
1487
|
+
const calloutContent = (jsx("div", { ref: calloutRef, className: `clarity-callout clarity-callout--${position.placement} clarity-callout--align-${position.horizontalAlign}`, style: {
|
|
1488
|
+
position: 'fixed',
|
|
1489
|
+
top: position.top,
|
|
1490
|
+
left: position.left,
|
|
1491
|
+
zIndex: 2147483647,
|
|
1492
|
+
}, "aria-live": "assertive", role: "tooltip", children: CompElementCallout && jsx(CompElementCallout, { elementHash: element.hash }) }));
|
|
1493
|
+
return createPortal(calloutContent, document.getElementById('gx-hm-viz-container'));
|
|
1051
1494
|
};
|
|
1052
1495
|
|
|
1053
|
-
const
|
|
1496
|
+
const ElementMissing = ({ show = true }) => {
|
|
1497
|
+
if (!show)
|
|
1498
|
+
return null;
|
|
1054
1499
|
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
1055
1500
|
return (jsx("div", { className: "missingElement", style: {
|
|
1056
|
-
position: '
|
|
1501
|
+
position: 'fixed',
|
|
1057
1502
|
top: '50%',
|
|
1058
1503
|
left: '50%',
|
|
1059
1504
|
transform: `translate(-50%, -50%) scale(${1 / widthScale})`,
|
|
@@ -1069,6 +1514,39 @@ const MissingElementMessage = () => {
|
|
|
1069
1514
|
}, "aria-live": "assertive", children: "Element not visible on current screen" }));
|
|
1070
1515
|
};
|
|
1071
1516
|
|
|
1517
|
+
const TARGET_ID_BY_TYPE = {
|
|
1518
|
+
hovered: {
|
|
1519
|
+
default: HOVERED_ELEMENT_ID,
|
|
1520
|
+
secondary: SECONDARY_HOVERED_ELEMENT_ID,
|
|
1521
|
+
},
|
|
1522
|
+
clicked: {
|
|
1523
|
+
default: CLICKED_ELEMENT_ID,
|
|
1524
|
+
secondary: SECONDARY_CLICKED_ELEMENT_ID,
|
|
1525
|
+
},
|
|
1526
|
+
};
|
|
1527
|
+
const ElementOverlay = ({ type, element, onClick, isSecondary }) => {
|
|
1528
|
+
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
1529
|
+
if (!element || (element.width === 0 && element.height === 0))
|
|
1530
|
+
return null;
|
|
1531
|
+
// Iframe has border, so we need to add it to the top position
|
|
1532
|
+
const top = element.top + HEATMAP_CONFIG['borderWidthIframe'];
|
|
1533
|
+
const left = element.left + HEATMAP_CONFIG['borderWidthIframe'];
|
|
1534
|
+
const width = element.width;
|
|
1535
|
+
const height = element.height;
|
|
1536
|
+
const targetId = TARGET_ID_BY_TYPE[type][isSecondary ? 'secondary' : 'default'];
|
|
1537
|
+
return (jsxs(Fragment$1, { children: [jsx("div", { onClick: onClick, className: "heatmapElement", id: targetId, style: {
|
|
1538
|
+
top,
|
|
1539
|
+
left,
|
|
1540
|
+
width,
|
|
1541
|
+
height,
|
|
1542
|
+
cursor: 'pointer',
|
|
1543
|
+
} }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: type === 'hovered' ? 1 : widthScale, clickOnElement: onClick })] }));
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
const ELEMENT_CALLOUT = {
|
|
1547
|
+
hozOffset: -8,
|
|
1548
|
+
alignment: 'left',
|
|
1549
|
+
};
|
|
1072
1550
|
const HeatmapElements = (props) => {
|
|
1073
1551
|
const height = useHeatmapVizStore((state) => state.iframeHeight);
|
|
1074
1552
|
const { iframeRef, wrapperRef, visualRef, visualizer, iframeDimensions, isElementSidebarOpen, isVisible = true, areDefaultRanksHidden, isSecondary, ...rest } = props;
|
|
@@ -1078,15 +1556,17 @@ const HeatmapElements = (props) => {
|
|
|
1078
1556
|
visualizer,
|
|
1079
1557
|
});
|
|
1080
1558
|
const { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout } = useClickedElement({
|
|
1081
|
-
iframeRef,
|
|
1082
1559
|
visualRef,
|
|
1083
|
-
wrapperRef,
|
|
1084
1560
|
getRect,
|
|
1085
1561
|
});
|
|
1086
1562
|
const { hoveredElement, handleMouseMove, handleMouseLeave, handleClick } = useHoveredElement({
|
|
1087
1563
|
iframeRef,
|
|
1088
1564
|
getRect,
|
|
1089
1565
|
});
|
|
1566
|
+
useElementCalloutVisible({
|
|
1567
|
+
visualRef,
|
|
1568
|
+
getRect,
|
|
1569
|
+
});
|
|
1090
1570
|
const resetAll = () => {
|
|
1091
1571
|
// setShouldShowCallout(false);
|
|
1092
1572
|
};
|
|
@@ -1098,7 +1578,7 @@ const HeatmapElements = (props) => {
|
|
|
1098
1578
|
});
|
|
1099
1579
|
if (!isVisible)
|
|
1100
1580
|
return null;
|
|
1101
|
-
return (jsxs("div", { onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, className: "
|
|
1581
|
+
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 }))] }));
|
|
1102
1582
|
};
|
|
1103
1583
|
|
|
1104
1584
|
const VizElements = ({ iframeRef, visualRef, wrapperRef }) => {
|
|
@@ -1218,16 +1698,17 @@ const VizDomRenderer = ({ mode = 'heatmap' }) => {
|
|
|
1218
1698
|
const VizDomContainer = () => {
|
|
1219
1699
|
const controls = useHeatmapControlStore((state) => state.controls);
|
|
1220
1700
|
const isRendering = useHeatmapDataStore((state) => state.isRendering);
|
|
1701
|
+
const iframeHeight = useHeatmapVizStore((state) => state.iframeHeight);
|
|
1221
1702
|
if (isRendering)
|
|
1222
1703
|
return controls.VizLoading ?? null;
|
|
1223
|
-
return (jsx(BoxStack, { id: "gx-hm-viz-container", flexDirection: "column", flex: "1 1 auto", overflow: "auto", children:
|
|
1704
|
+
return (jsx(BoxStack, { id: "gx-hm-viz-container", flexDirection: "column", flex: "1 1 auto", overflow: "auto", zIndex: 1, children: jsxs(BoxStack, { id: "gx-hm-content", flexDirection: "column", flex: "1 1 auto", overflow: "hidden", style: {
|
|
1224
1705
|
minWidth: '394px',
|
|
1225
|
-
}, children: jsx(VizDomRenderer, {}) }) }));
|
|
1706
|
+
}, children: [jsx(VizDomRenderer, {}), iframeHeight === 0 && (jsxs("div", { className: "gx-hm-loading", children: [jsx("div", { className: "gx-hm-loading--spinner" }), jsx("p", { className: "gx-hm-loading--text", children: "Loading visualization..." })] }))] }) }));
|
|
1226
1707
|
};
|
|
1227
1708
|
|
|
1228
1709
|
const ContentMetricBar = () => {
|
|
1229
1710
|
const controls = useHeatmapControlStore((state) => state.controls);
|
|
1230
|
-
return (jsx(BoxStack, { id: "gx-hm-content-
|
|
1711
|
+
return (jsx(BoxStack, { id: "gx-hm-content-metric-bar", flexDirection: "row", alignItems: "center", overflow: "auto", zIndex: 1, backgroundColor: "white", style: {
|
|
1231
1712
|
borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
|
|
1232
1713
|
}, children: controls.MetricBar ?? null }));
|
|
1233
1714
|
};
|
|
@@ -1235,6 +1716,7 @@ const ContentMetricBar = () => {
|
|
|
1235
1716
|
const ContentToolbar = () => {
|
|
1236
1717
|
const controls = useHeatmapControlStore((state) => state.controls);
|
|
1237
1718
|
return (jsx("div", { id: "gx-hm-content-toolbar", style: {
|
|
1719
|
+
zIndex: 2,
|
|
1238
1720
|
position: 'absolute',
|
|
1239
1721
|
bottom: 0,
|
|
1240
1722
|
left: '8px',
|
|
@@ -1255,6 +1737,7 @@ const LeftSidebar = () => {
|
|
|
1255
1737
|
return (jsx("div", { className: "gx-hm-sidebar", style: {
|
|
1256
1738
|
height: '100%',
|
|
1257
1739
|
display: 'flex',
|
|
1740
|
+
zIndex: 1,
|
|
1258
1741
|
...(isHideSidebar
|
|
1259
1742
|
? {
|
|
1260
1743
|
width: '0',
|