@gemx-dev/heatmap-react 3.5.34 → 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/ContentTopBar.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/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 +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/index.js +450 -254
- package/dist/esm/index.mjs +450 -254
- 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 +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 +13 -124
- package/dist/umd/components/Layout/ContentMetricBar.d.ts.map +1 -1
- package/dist/umd/components/Layout/ContentTopBar.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/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 +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/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 +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.js
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,6 +121,8 @@ 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
|
|
|
@@ -152,6 +155,7 @@ const useRegisterControl = (control) => {
|
|
|
152
155
|
registerControl('Toolbar', control.Toolbar);
|
|
153
156
|
registerControl('MetricBar', control.MetricBar);
|
|
154
157
|
registerControl('VizLoading', control.VizLoading);
|
|
158
|
+
registerControl('ElementCallout', control.ElementCallout);
|
|
155
159
|
};
|
|
156
160
|
|
|
157
161
|
const useRegisterData = (data, dataInfo) => {
|
|
@@ -189,47 +193,251 @@ const useRegisterHeatmap = (clickmap) => {
|
|
|
189
193
|
}, [clickmap]);
|
|
190
194
|
};
|
|
191
195
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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;
|
|
207
378
|
};
|
|
208
|
-
|
|
209
|
-
|
|
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)
|
|
210
386
|
return 0;
|
|
211
|
-
return
|
|
387
|
+
return elements.findIndex((e) => e.hash === hash) + 1;
|
|
212
388
|
};
|
|
213
|
-
const buildElementInfo = (
|
|
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 ?? '';
|
|
214
398
|
const baseInfo = {
|
|
215
|
-
hash
|
|
216
|
-
clicks
|
|
399
|
+
hash,
|
|
400
|
+
clicks,
|
|
217
401
|
rank,
|
|
218
|
-
selector
|
|
402
|
+
selector,
|
|
219
403
|
};
|
|
220
|
-
if (rect) {
|
|
221
|
-
return { ...baseInfo, ...rect };
|
|
222
|
-
}
|
|
223
404
|
return {
|
|
224
405
|
...baseInfo,
|
|
225
|
-
|
|
226
|
-
top: 0,
|
|
227
|
-
width: 0,
|
|
228
|
-
height: 0,
|
|
406
|
+
...rect,
|
|
229
407
|
};
|
|
230
408
|
};
|
|
231
|
-
|
|
232
|
-
|
|
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)
|
|
233
441
|
return;
|
|
234
442
|
const visualRect = visualRef.current.getBoundingClientRect();
|
|
235
443
|
if (isElementInViewport(rect, visualRef, scale)) {
|
|
@@ -245,13 +453,14 @@ const scrollToElementIfNeeded = (visualRef, wrapperRef, rect, scale) => {
|
|
|
245
453
|
behavior: 'smooth',
|
|
246
454
|
});
|
|
247
455
|
};
|
|
248
|
-
const useClickedElement = ({ visualRef,
|
|
456
|
+
const useClickedElement = ({ visualRef, getRect }) => {
|
|
249
457
|
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
250
458
|
const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
|
|
459
|
+
const shouldShowCallout = useHeatmapInteractionStore((state) => state.shouldShowCallout);
|
|
460
|
+
const setShouldShowCallout = useHeatmapInteractionStore((state) => state.setShouldShowCallout);
|
|
251
461
|
const scale = useHeatmapVizStore((state) => state.scale);
|
|
252
462
|
const [clickedElement, setClickedElement] = useState(null);
|
|
253
463
|
const [showMissingElement, setShowMissingElement] = useState(false);
|
|
254
|
-
const [shouldShowCallout, setShouldShowCallout] = useState(false);
|
|
255
464
|
const reset = () => {
|
|
256
465
|
setClickedElement(null);
|
|
257
466
|
setShowMissingElement(false);
|
|
@@ -267,26 +476,58 @@ const useClickedElement = ({ visualRef, wrapperRef, getRect }) => {
|
|
|
267
476
|
setClickedElement(null);
|
|
268
477
|
return;
|
|
269
478
|
}
|
|
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 {
|
|
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) {
|
|
282
484
|
setClickedElement(elementInfo);
|
|
283
485
|
setShowMissingElement(true);
|
|
284
486
|
setShouldShowCallout(false);
|
|
487
|
+
return;
|
|
285
488
|
}
|
|
286
|
-
|
|
489
|
+
setShowMissingElement(false);
|
|
490
|
+
scrollToElementIfNeeded(visualRef, rect, scale);
|
|
491
|
+
setShouldShowCallout(true);
|
|
492
|
+
requestAnimationFrame(() => {
|
|
493
|
+
setClickedElement(elementInfo);
|
|
494
|
+
});
|
|
495
|
+
}, [selectedElement, heatmapInfo, getRect, visualRef, scale]);
|
|
287
496
|
return { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout };
|
|
288
497
|
};
|
|
289
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
|
+
|
|
290
531
|
const useHeatmapEffects = ({ isVisible, isElementSidebarOpen, setShouldShowCallout, resetAll, }) => {
|
|
291
532
|
const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
|
|
292
533
|
// Reset khi ẩn
|
|
@@ -305,32 +546,6 @@ const useHeatmapEffects = ({ isVisible, isElementSidebarOpen, setShouldShowCallo
|
|
|
305
546
|
}, [isElementSidebarOpen, selectedElement, setShouldShowCallout]);
|
|
306
547
|
};
|
|
307
548
|
|
|
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
549
|
const useHeatmapElementPosition = ({ iframeRef, wrapperRef, visualizer }) => {
|
|
335
550
|
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
336
551
|
const iframeHeight = useHeatmapVizStore((state) => state.iframeHeight);
|
|
@@ -380,70 +595,63 @@ const debounce = (fn, delay) => {
|
|
|
380
595
|
timeout = setTimeout(() => fn(...args), delay);
|
|
381
596
|
};
|
|
382
597
|
};
|
|
598
|
+
|
|
383
599
|
const useHoveredElement = ({ iframeRef, getRect }) => {
|
|
384
600
|
const hoveredElement = useHeatmapInteractionStore((state) => state.hoveredElement);
|
|
385
601
|
const setHoveredElement = useHeatmapInteractionStore((state) => state.setHoveredElement);
|
|
386
602
|
const onSelect = useHeatmapInteractionStore((state) => state.setSelectedElement);
|
|
387
603
|
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
388
604
|
const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
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();
|
|
392
614
|
return;
|
|
393
615
|
}
|
|
394
616
|
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
617
|
const doc = iframe.contentDocument;
|
|
403
|
-
|
|
404
|
-
|
|
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();
|
|
405
623
|
return;
|
|
406
624
|
}
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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();
|
|
413
634
|
return;
|
|
414
635
|
}
|
|
415
|
-
const hash =
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (!hash || !heatmapInfo.elementMapInfo[hash]) {
|
|
419
|
-
setHoveredElement(null);
|
|
636
|
+
const hash = getHashFromEvent(event);
|
|
637
|
+
if (!hash) {
|
|
638
|
+
reset();
|
|
420
639
|
return;
|
|
421
640
|
}
|
|
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);
|
|
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;
|
|
436
647
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const handleMouseLeave = useCallback(() => {
|
|
440
|
-
setHoveredElement(null);
|
|
441
|
-
}, []);
|
|
648
|
+
setHoveredElement(elementInfo);
|
|
649
|
+
}, 16), [heatmapInfo, getRect, reset, getHashFromEvent]);
|
|
442
650
|
const handleClick = useCallback(() => {
|
|
443
|
-
if (hoveredElement?.hash
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}, [hoveredElement
|
|
651
|
+
if (!hoveredElement?.hash)
|
|
652
|
+
return;
|
|
653
|
+
onSelect(hoveredElement.hash);
|
|
654
|
+
}, [hoveredElement?.hash]);
|
|
447
655
|
return {
|
|
448
656
|
hoveredElement,
|
|
449
657
|
handleMouseMove,
|
|
@@ -451,17 +659,32 @@ const useHoveredElement = ({ iframeRef, getRect }) => {
|
|
|
451
659
|
handleClick,
|
|
452
660
|
};
|
|
453
661
|
};
|
|
454
|
-
const
|
|
455
|
-
let
|
|
456
|
-
|
|
457
|
-
|
|
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;
|
|
458
668
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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);
|
|
463
675
|
}
|
|
464
|
-
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];
|
|
465
688
|
};
|
|
466
689
|
|
|
467
690
|
const useHeatmapRender = () => {
|
|
@@ -834,7 +1057,7 @@ const useHeatmapScale = (props) => {
|
|
|
834
1057
|
};
|
|
835
1058
|
};
|
|
836
1059
|
|
|
837
|
-
const BoxStack = ({ children, ...props }) => {
|
|
1060
|
+
const BoxStack = forwardRef(({ children, ...props }, ref) => {
|
|
838
1061
|
const id = props.id;
|
|
839
1062
|
const flexDirection = props.flexDirection;
|
|
840
1063
|
const overflow = props.overflow || 'hidden';
|
|
@@ -845,6 +1068,8 @@ const BoxStack = ({ children, ...props }) => {
|
|
|
845
1068
|
const style = props.style || {};
|
|
846
1069
|
const gap = props.gap || 0;
|
|
847
1070
|
const height = props.height || 'auto';
|
|
1071
|
+
const zIndex = props.zIndex || 0;
|
|
1072
|
+
const backgroundColor = props.backgroundColor || 'transparent';
|
|
848
1073
|
const styleGap = useMemo(() => {
|
|
849
1074
|
switch (flexDirection) {
|
|
850
1075
|
case 'row':
|
|
@@ -866,15 +1091,17 @@ const BoxStack = ({ children, ...props }) => {
|
|
|
866
1091
|
justifyContent,
|
|
867
1092
|
alignItems,
|
|
868
1093
|
height,
|
|
1094
|
+
zIndex,
|
|
1095
|
+
backgroundColor,
|
|
869
1096
|
...styleGap,
|
|
870
1097
|
...style,
|
|
871
1098
|
};
|
|
872
|
-
return (jsx("div", { id: id, style: styleProps, children: children }));
|
|
873
|
-
};
|
|
1099
|
+
return (jsx("div", { id: id, style: styleProps, ref: ref, children: children }));
|
|
1100
|
+
});
|
|
874
1101
|
|
|
875
1102
|
const ContentTopBar = () => {
|
|
876
1103
|
const controls = useHeatmapControlStore((state) => state.controls);
|
|
877
|
-
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: {
|
|
878
1105
|
borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
|
|
879
1106
|
}, children: controls.TopBar ?? null }));
|
|
880
1107
|
};
|
|
@@ -912,110 +1139,14 @@ const useHeatmapVizCanvas = ({ type }) => {
|
|
|
912
1139
|
return heatmapRender?.();
|
|
913
1140
|
};
|
|
914
1141
|
|
|
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
|
-
};
|
|
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';
|
|
1003
1146
|
|
|
1004
1147
|
const RankBadge = ({ index, elementRect, widthScale, clickOnElement, }) => {
|
|
1005
1148
|
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 }))] }));
|
|
1149
|
+
return (jsx("div", { className: "gx-hm-rank-badge", style: style, onClick: clickOnElement, children: index }));
|
|
1019
1150
|
};
|
|
1020
1151
|
|
|
1021
1152
|
const NUMBER_OF_TOP_ELEMENTS = 10;
|
|
@@ -1033,27 +1164,57 @@ const DefaultRankBadges = ({ getRect, hidden }) => {
|
|
|
1033
1164
|
}) }));
|
|
1034
1165
|
};
|
|
1035
1166
|
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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;
|
|
1176
|
+
const calloutRef = useRef(null);
|
|
1177
|
+
const [position, setPosition] = useState(DEFAULT_POSITION);
|
|
1178
|
+
useEffect(() => {
|
|
1179
|
+
const targetElm = document.querySelector(target);
|
|
1180
|
+
const calloutElm = calloutRef.current;
|
|
1181
|
+
if (!targetElm || !calloutElm)
|
|
1182
|
+
return;
|
|
1183
|
+
const positionFn = calcCalloutPosition({
|
|
1184
|
+
targetElm,
|
|
1185
|
+
calloutElm,
|
|
1186
|
+
setPosition,
|
|
1187
|
+
hozOffset,
|
|
1188
|
+
alignment,
|
|
1189
|
+
});
|
|
1190
|
+
positionFn();
|
|
1191
|
+
const handleUpdate = () => {
|
|
1192
|
+
requestAnimationFrame(positionFn);
|
|
1193
|
+
};
|
|
1194
|
+
window.addEventListener('scroll', handleUpdate, true);
|
|
1195
|
+
window.addEventListener('resize', handleUpdate);
|
|
1196
|
+
visualRef?.current?.addEventListener('scroll', handleUpdate);
|
|
1197
|
+
return () => {
|
|
1198
|
+
window.removeEventListener('scroll', handleUpdate, true);
|
|
1199
|
+
window.removeEventListener('resize', handleUpdate);
|
|
1200
|
+
visualRef?.current?.removeEventListener('scroll', handleUpdate);
|
|
1201
|
+
};
|
|
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: {
|
|
1204
|
+
position: 'fixed',
|
|
1205
|
+
top: position.top,
|
|
1206
|
+
left: position.left,
|
|
1207
|
+
zIndex: 2147483647,
|
|
1208
|
+
}, "aria-live": "assertive", role: "tooltip", children: CompElementCallout && jsx(CompElementCallout, { elementHash: element.hash }) }));
|
|
1209
|
+
return createPortal(calloutContent, document.getElementById('gx-hm-viz-container'));
|
|
1051
1210
|
};
|
|
1052
1211
|
|
|
1053
|
-
const
|
|
1212
|
+
const ElementMissing = ({ show = true }) => {
|
|
1213
|
+
if (!show)
|
|
1214
|
+
return null;
|
|
1054
1215
|
const widthScale = useHeatmapVizStore((state) => state.scale);
|
|
1055
1216
|
return (jsx("div", { className: "missingElement", style: {
|
|
1056
|
-
position: '
|
|
1217
|
+
position: 'fixed',
|
|
1057
1218
|
top: '50%',
|
|
1058
1219
|
left: '50%',
|
|
1059
1220
|
transform: `translate(-50%, -50%) scale(${1 / widthScale})`,
|
|
@@ -1069,6 +1230,39 @@ const MissingElementMessage = () => {
|
|
|
1069
1230
|
}, "aria-live": "assertive", children: "Element not visible on current screen" }));
|
|
1070
1231
|
};
|
|
1071
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
|
+
};
|
|
1072
1266
|
const HeatmapElements = (props) => {
|
|
1073
1267
|
const height = useHeatmapVizStore((state) => state.iframeHeight);
|
|
1074
1268
|
const { iframeRef, wrapperRef, visualRef, visualizer, iframeDimensions, isElementSidebarOpen, isVisible = true, areDefaultRanksHidden, isSecondary, ...rest } = props;
|
|
@@ -1078,15 +1272,17 @@ const HeatmapElements = (props) => {
|
|
|
1078
1272
|
visualizer,
|
|
1079
1273
|
});
|
|
1080
1274
|
const { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout } = useClickedElement({
|
|
1081
|
-
iframeRef,
|
|
1082
1275
|
visualRef,
|
|
1083
|
-
wrapperRef,
|
|
1084
1276
|
getRect,
|
|
1085
1277
|
});
|
|
1086
1278
|
const { hoveredElement, handleMouseMove, handleMouseLeave, handleClick } = useHoveredElement({
|
|
1087
1279
|
iframeRef,
|
|
1088
1280
|
getRect,
|
|
1089
1281
|
});
|
|
1282
|
+
useElementCalloutVisible({
|
|
1283
|
+
visualRef,
|
|
1284
|
+
getRect,
|
|
1285
|
+
});
|
|
1090
1286
|
const resetAll = () => {
|
|
1091
1287
|
// setShouldShowCallout(false);
|
|
1092
1288
|
};
|
|
@@ -1098,7 +1294,7 @@ const HeatmapElements = (props) => {
|
|
|
1098
1294
|
});
|
|
1099
1295
|
if (!isVisible)
|
|
1100
1296
|
return null;
|
|
1101
|
-
return (jsxs("div", { onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, className: "
|
|
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 }))] }));
|
|
1102
1298
|
};
|
|
1103
1299
|
|
|
1104
1300
|
const VizElements = ({ iframeRef, visualRef, wrapperRef }) => {
|
|
@@ -1227,7 +1423,7 @@ const VizDomContainer = () => {
|
|
|
1227
1423
|
|
|
1228
1424
|
const ContentMetricBar = () => {
|
|
1229
1425
|
const controls = useHeatmapControlStore((state) => state.controls);
|
|
1230
|
-
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: {
|
|
1231
1427
|
borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
|
|
1232
1428
|
}, children: controls.MetricBar ?? null }));
|
|
1233
1429
|
};
|