@gemx-dev/heatmap-react 3.5.53 → 3.5.54

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.
Files changed (171) hide show
  1. package/dist/esm/components/Layout/HeatmapLayout.d.ts +2 -1
  2. package/dist/esm/components/Layout/HeatmapLayout.d.ts.map +1 -1
  3. package/dist/esm/components/Layout/HeatmapPreview.d.ts.map +1 -1
  4. package/dist/esm/components/Layout/TopBar/ContentTopBar.d.ts.map +1 -1
  5. package/dist/esm/components/Layout/VizByMode/ContentVizByMode.d.ts.map +1 -1
  6. package/dist/esm/components/VizAreaClick/VizAreaClick.d.ts.map +1 -1
  7. package/dist/esm/components/VizDom/VizDomHeatmap.d.ts.map +1 -1
  8. package/dist/esm/components/VizDom/VizDomRenderer.d.ts.map +1 -1
  9. package/dist/esm/components/VizElement/DefaultRankBadges.d.ts.map +1 -1
  10. package/dist/esm/components/VizElement/RankBadge.d.ts.map +1 -1
  11. package/dist/esm/components/VizScrollmap/HoverZones.d.ts.map +1 -1
  12. package/dist/esm/components/VizScrollmap/ScrollMapOverlay.d.ts.map +1 -1
  13. package/dist/esm/components/VizScrollmapV2/ScrollmapOverlayV2.d.ts.map +1 -1
  14. package/dist/esm/components/VizScrollmapV2/useScrollmapOverlay.d.ts.map +1 -1
  15. package/dist/esm/helpers/iframe-helper/fixer.d.ts.map +1 -1
  16. package/dist/esm/helpers/iframe-helper/navigation-blocker-v2.d.ts.map +1 -1
  17. package/dist/esm/helpers/iframe-helper/style-replacer.d.ts.map +1 -1
  18. package/dist/esm/helpers/viz-area-click/area-builder.d.ts +2 -2
  19. package/dist/esm/helpers/viz-area-click/area-builder.d.ts.map +1 -1
  20. package/dist/esm/helpers/viz-area-click/area-color.d.ts.map +1 -1
  21. package/dist/esm/helpers/viz-area-click/area-graph.d.ts +63 -0
  22. package/dist/esm/helpers/viz-area-click/area-graph.d.ts.map +1 -0
  23. package/dist/esm/helpers/viz-area-click/area-hydration.d.ts +36 -0
  24. package/dist/esm/helpers/viz-area-click/area-hydration.d.ts.map +1 -0
  25. package/dist/esm/helpers/viz-area-click/area-overlap.d.ts.map +1 -1
  26. package/dist/esm/helpers/viz-area-click/index.d.ts +2 -0
  27. package/dist/esm/helpers/viz-area-click/index.d.ts.map +1 -1
  28. package/dist/esm/helpers/viz-canvas/area-clustering.d.ts.map +1 -1
  29. package/dist/esm/helpers/viz-canvas/area-overlay-manager-v2.d.ts.map +1 -1
  30. package/dist/esm/helpers/viz-canvas/hierarchical-area-clustering.d.ts.map +1 -1
  31. package/dist/esm/helpers/viz-dom/find-elm.d.ts +8 -0
  32. package/dist/esm/helpers/viz-dom/find-elm.d.ts.map +1 -0
  33. package/dist/esm/helpers/viz-dom/index.d.ts +1 -0
  34. package/dist/esm/helpers/viz-dom/index.d.ts.map +1 -1
  35. package/dist/esm/helpers/viz-elm-callout/dimensions.d.ts.map +1 -1
  36. package/dist/esm/helpers/viz-elm-callout/position-candidates.d.ts.map +1 -1
  37. package/dist/esm/helpers/viz-elm-callout/position-selector.d.ts.map +1 -1
  38. package/dist/esm/helpers/viz-elm-callout/position-validator.d.ts.map +1 -1
  39. package/dist/esm/hooks/register/useRegisterHeatmap.d.ts +3 -2
  40. package/dist/esm/hooks/register/useRegisterHeatmap.d.ts.map +1 -1
  41. package/dist/esm/hooks/view-context/useHeatmapClick.d.ts.map +1 -1
  42. package/dist/esm/hooks/view-context/useHeatmapData.d.ts +4 -1
  43. package/dist/esm/hooks/view-context/useHeatmapData.d.ts.map +1 -1
  44. package/dist/esm/hooks/viz-area-click/index.d.ts +1 -0
  45. package/dist/esm/hooks/viz-area-click/index.d.ts.map +1 -1
  46. package/dist/esm/hooks/viz-area-click/useAreaCreation.d.ts.map +1 -1
  47. package/dist/esm/hooks/viz-area-click/useAreaEditMode.d.ts.map +1 -1
  48. package/dist/esm/hooks/viz-area-click/useAreaFilterVisible.d.ts.map +1 -1
  49. package/dist/esm/hooks/viz-area-click/useAreaHydration.d.ts +9 -0
  50. package/dist/esm/hooks/viz-area-click/useAreaHydration.d.ts.map +1 -0
  51. package/dist/esm/hooks/viz-area-click/useAreaInteraction.d.ts +0 -6
  52. package/dist/esm/hooks/viz-area-click/useAreaInteraction.d.ts.map +1 -1
  53. package/dist/esm/hooks/viz-area-click/useAreaRectSync.d.ts.map +1 -1
  54. package/dist/esm/hooks/viz-area-click/useAreaTopAutoDetect.d.ts +1 -1
  55. package/dist/esm/hooks/viz-area-click/useAreaTopAutoDetect.d.ts.map +1 -1
  56. package/dist/esm/hooks/viz-canvas/useScrollmap.d.ts.map +1 -1
  57. package/dist/esm/hooks/viz-render/useHeatmapRenderByMode.d.ts.map +1 -1
  58. package/dist/esm/hooks/viz-scale/useContainerDimensions.d.ts.map +1 -1
  59. package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts +1 -1
  60. package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts.map +1 -1
  61. package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts +0 -3
  62. package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
  63. package/dist/esm/hooks/viz-scale/useObserveIframeHeight.d.ts +0 -1
  64. package/dist/esm/hooks/viz-scale/useObserveIframeHeight.d.ts.map +1 -1
  65. package/dist/esm/hooks/viz-scale/useScaleCalculation.d.ts.map +1 -1
  66. package/dist/esm/hooks/viz-scale/useScrollSync.d.ts +1 -1
  67. package/dist/esm/hooks/viz-scale/useScrollSync.d.ts.map +1 -1
  68. package/dist/esm/hooks/viz-scale/useWrapperRefHeight.d.ts.map +1 -1
  69. package/dist/esm/hooks/viz-scroll/useScrollmapZones.d.ts.map +1 -1
  70. package/dist/esm/index.d.ts +1 -0
  71. package/dist/esm/index.d.ts.map +1 -1
  72. package/dist/esm/index.js +990 -721
  73. package/dist/esm/index.mjs +990 -721
  74. package/dist/esm/performance/hooks.d.ts.map +1 -1
  75. package/dist/esm/performance/performance-logger.d.ts.map +1 -1
  76. package/dist/esm/performance/types.d.ts.map +1 -1
  77. package/dist/esm/performance/utils.d.ts.map +1 -1
  78. package/dist/esm/stores/comp.d.ts.map +1 -1
  79. package/dist/esm/stores/data.d.ts +4 -1
  80. package/dist/esm/stores/data.d.ts.map +1 -1
  81. package/dist/esm/stores/mode-compare.d.ts.map +1 -1
  82. package/dist/esm/types/viz-area-click.d.ts +14 -0
  83. package/dist/esm/types/viz-area-click.d.ts.map +1 -1
  84. package/dist/esm/ui/BoxStack/BoxStack.d.ts.map +1 -1
  85. package/dist/esm/utils/retry.d.ts.map +1 -1
  86. package/dist/style.css +9 -11
  87. package/dist/umd/components/Layout/HeatmapLayout.d.ts +2 -1
  88. package/dist/umd/components/Layout/HeatmapLayout.d.ts.map +1 -1
  89. package/dist/umd/components/Layout/HeatmapPreview.d.ts.map +1 -1
  90. package/dist/umd/components/Layout/TopBar/ContentTopBar.d.ts.map +1 -1
  91. package/dist/umd/components/Layout/VizByMode/ContentVizByMode.d.ts.map +1 -1
  92. package/dist/umd/components/VizAreaClick/VizAreaClick.d.ts.map +1 -1
  93. package/dist/umd/components/VizDom/VizDomHeatmap.d.ts.map +1 -1
  94. package/dist/umd/components/VizDom/VizDomRenderer.d.ts.map +1 -1
  95. package/dist/umd/components/VizElement/DefaultRankBadges.d.ts.map +1 -1
  96. package/dist/umd/components/VizElement/RankBadge.d.ts.map +1 -1
  97. package/dist/umd/components/VizScrollmap/HoverZones.d.ts.map +1 -1
  98. package/dist/umd/components/VizScrollmap/ScrollMapOverlay.d.ts.map +1 -1
  99. package/dist/umd/components/VizScrollmapV2/ScrollmapOverlayV2.d.ts.map +1 -1
  100. package/dist/umd/components/VizScrollmapV2/useScrollmapOverlay.d.ts.map +1 -1
  101. package/dist/umd/helpers/iframe-helper/fixer.d.ts.map +1 -1
  102. package/dist/umd/helpers/iframe-helper/navigation-blocker-v2.d.ts.map +1 -1
  103. package/dist/umd/helpers/iframe-helper/style-replacer.d.ts.map +1 -1
  104. package/dist/umd/helpers/viz-area-click/area-builder.d.ts +2 -2
  105. package/dist/umd/helpers/viz-area-click/area-builder.d.ts.map +1 -1
  106. package/dist/umd/helpers/viz-area-click/area-color.d.ts.map +1 -1
  107. package/dist/umd/helpers/viz-area-click/area-graph.d.ts +63 -0
  108. package/dist/umd/helpers/viz-area-click/area-graph.d.ts.map +1 -0
  109. package/dist/umd/helpers/viz-area-click/area-hydration.d.ts +36 -0
  110. package/dist/umd/helpers/viz-area-click/area-hydration.d.ts.map +1 -0
  111. package/dist/umd/helpers/viz-area-click/area-overlap.d.ts.map +1 -1
  112. package/dist/umd/helpers/viz-area-click/index.d.ts +2 -0
  113. package/dist/umd/helpers/viz-area-click/index.d.ts.map +1 -1
  114. package/dist/umd/helpers/viz-canvas/area-clustering.d.ts.map +1 -1
  115. package/dist/umd/helpers/viz-canvas/area-overlay-manager-v2.d.ts.map +1 -1
  116. package/dist/umd/helpers/viz-canvas/hierarchical-area-clustering.d.ts.map +1 -1
  117. package/dist/umd/helpers/viz-dom/find-elm.d.ts +8 -0
  118. package/dist/umd/helpers/viz-dom/find-elm.d.ts.map +1 -0
  119. package/dist/umd/helpers/viz-dom/index.d.ts +1 -0
  120. package/dist/umd/helpers/viz-dom/index.d.ts.map +1 -1
  121. package/dist/umd/helpers/viz-elm-callout/dimensions.d.ts.map +1 -1
  122. package/dist/umd/helpers/viz-elm-callout/position-candidates.d.ts.map +1 -1
  123. package/dist/umd/helpers/viz-elm-callout/position-selector.d.ts.map +1 -1
  124. package/dist/umd/helpers/viz-elm-callout/position-validator.d.ts.map +1 -1
  125. package/dist/umd/hooks/register/useRegisterHeatmap.d.ts +3 -2
  126. package/dist/umd/hooks/register/useRegisterHeatmap.d.ts.map +1 -1
  127. package/dist/umd/hooks/view-context/useHeatmapClick.d.ts.map +1 -1
  128. package/dist/umd/hooks/view-context/useHeatmapData.d.ts +4 -1
  129. package/dist/umd/hooks/view-context/useHeatmapData.d.ts.map +1 -1
  130. package/dist/umd/hooks/viz-area-click/index.d.ts +1 -0
  131. package/dist/umd/hooks/viz-area-click/index.d.ts.map +1 -1
  132. package/dist/umd/hooks/viz-area-click/useAreaCreation.d.ts.map +1 -1
  133. package/dist/umd/hooks/viz-area-click/useAreaEditMode.d.ts.map +1 -1
  134. package/dist/umd/hooks/viz-area-click/useAreaFilterVisible.d.ts.map +1 -1
  135. package/dist/umd/hooks/viz-area-click/useAreaHydration.d.ts +9 -0
  136. package/dist/umd/hooks/viz-area-click/useAreaHydration.d.ts.map +1 -0
  137. package/dist/umd/hooks/viz-area-click/useAreaInteraction.d.ts +0 -6
  138. package/dist/umd/hooks/viz-area-click/useAreaInteraction.d.ts.map +1 -1
  139. package/dist/umd/hooks/viz-area-click/useAreaRectSync.d.ts.map +1 -1
  140. package/dist/umd/hooks/viz-area-click/useAreaTopAutoDetect.d.ts +1 -1
  141. package/dist/umd/hooks/viz-area-click/useAreaTopAutoDetect.d.ts.map +1 -1
  142. package/dist/umd/hooks/viz-canvas/useScrollmap.d.ts.map +1 -1
  143. package/dist/umd/hooks/viz-render/useHeatmapRenderByMode.d.ts.map +1 -1
  144. package/dist/umd/hooks/viz-scale/useContainerDimensions.d.ts.map +1 -1
  145. package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts +1 -1
  146. package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts.map +1 -1
  147. package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts +0 -3
  148. package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
  149. package/dist/umd/hooks/viz-scale/useObserveIframeHeight.d.ts +0 -1
  150. package/dist/umd/hooks/viz-scale/useObserveIframeHeight.d.ts.map +1 -1
  151. package/dist/umd/hooks/viz-scale/useScaleCalculation.d.ts.map +1 -1
  152. package/dist/umd/hooks/viz-scale/useScrollSync.d.ts +1 -1
  153. package/dist/umd/hooks/viz-scale/useScrollSync.d.ts.map +1 -1
  154. package/dist/umd/hooks/viz-scale/useWrapperRefHeight.d.ts.map +1 -1
  155. package/dist/umd/hooks/viz-scroll/useScrollmapZones.d.ts.map +1 -1
  156. package/dist/umd/index.d.ts +1 -0
  157. package/dist/umd/index.d.ts.map +1 -1
  158. package/dist/umd/index.js +2 -2
  159. package/dist/umd/performance/hooks.d.ts.map +1 -1
  160. package/dist/umd/performance/performance-logger.d.ts.map +1 -1
  161. package/dist/umd/performance/types.d.ts.map +1 -1
  162. package/dist/umd/performance/utils.d.ts.map +1 -1
  163. package/dist/umd/stores/comp.d.ts.map +1 -1
  164. package/dist/umd/stores/data.d.ts +4 -1
  165. package/dist/umd/stores/data.d.ts.map +1 -1
  166. package/dist/umd/stores/mode-compare.d.ts.map +1 -1
  167. package/dist/umd/types/viz-area-click.d.ts +14 -0
  168. package/dist/umd/types/viz-area-click.d.ts.map +1 -1
  169. package/dist/umd/ui/BoxStack/BoxStack.d.ts.map +1 -1
  170. package/dist/umd/utils/retry.d.ts.map +1 -1
  171. package/package.json +1 -1
package/dist/esm/index.js CHANGED
@@ -186,8 +186,20 @@ const useHeatmapDataStore = create()(subscribeWithSelector((set) => {
186
186
  return {
187
187
  data: { [DEFAULT_VIEW_ID]: undefined },
188
188
  clickmap: { [DEFAULT_VIEW_ID]: undefined },
189
+ clickAreas: { [DEFAULT_VIEW_ID]: undefined },
189
190
  dataInfo: { [DEFAULT_VIEW_ID]: undefined },
190
191
  scrollmap: { [DEFAULT_VIEW_ID]: undefined },
192
+ setClickAreas: (clickAreas, viewId = DEFAULT_VIEW_ID) => set((state) => ({
193
+ clickAreas: { ...state.clickAreas, [viewId]: clickAreas },
194
+ })),
195
+ removeClickArea: (areaId, viewId = DEFAULT_VIEW_ID) => set((state) => {
196
+ const currentAreas = state.clickAreas[viewId] || [];
197
+ const filtered = currentAreas.filter((a) => a.id !== areaId);
198
+ console.log(`🚀 🐥 ~ filtered:`, filtered);
199
+ return {
200
+ clickAreas: { ...state.clickAreas, [viewId]: filtered },
201
+ };
202
+ }),
191
203
  setDataInfo: (dataInfo, viewId = DEFAULT_VIEW_ID) => set((state) => ({
192
204
  dataInfo: { ...state.dataInfo, [viewId]: dataInfo },
193
205
  })),
@@ -203,21 +215,25 @@ const useHeatmapDataStore = create()(subscribeWithSelector((set) => {
203
215
  copyView: (fromViewId, toViewId) => set((state) => ({
204
216
  data: { ...state.data, [toViewId]: state.data[fromViewId] },
205
217
  clickmap: { ...state.clickmap, [toViewId]: state.clickmap[fromViewId] },
218
+ clickAreas: { ...state.clickAreas, [toViewId]: state.clickAreas[fromViewId] },
206
219
  dataInfo: { ...state.dataInfo, [toViewId]: state.dataInfo[fromViewId] },
207
220
  scrollmap: { ...state.scrollmap, [toViewId]: state.scrollmap[fromViewId] },
208
221
  })),
209
222
  clearView: (viewId) => set((state) => {
210
223
  const newData = { ...state.data };
211
224
  const newClickmap = { ...state.clickmap };
225
+ const newClickAreas = { ...state.clickAreas };
212
226
  const newDataInfo = { ...state.dataInfo };
213
227
  const newScrollmap = { ...state.scrollmap };
214
228
  delete newData[viewId];
215
229
  delete newClickmap[viewId];
230
+ delete newClickAreas[viewId];
216
231
  delete newDataInfo[viewId];
217
232
  delete newScrollmap[viewId];
218
233
  return {
219
234
  data: newData,
220
235
  clickmap: newClickmap,
236
+ clickAreas: newClickAreas,
221
237
  dataInfo: newDataInfo,
222
238
  scrollmap: newScrollmap,
223
239
  };
@@ -225,6 +241,7 @@ const useHeatmapDataStore = create()(subscribeWithSelector((set) => {
225
241
  resetAll: () => set({
226
242
  data: { [DEFAULT_VIEW_ID]: undefined },
227
243
  clickmap: { [DEFAULT_VIEW_ID]: undefined },
244
+ clickAreas: { [DEFAULT_VIEW_ID]: undefined },
228
245
  dataInfo: { [DEFAULT_VIEW_ID]: undefined },
229
246
  scrollmap: { [DEFAULT_VIEW_ID]: undefined },
230
247
  }),
@@ -787,13 +804,7 @@ const useHeatmapClick = (props) => {
787
804
  setSelectedElement: (element) => setSelectedElementStore(element, viewId),
788
805
  setHoveredElement: (element) => setHoveredElementStore(element, viewId),
789
806
  setShouldShowCallout: (value) => setShouldShowCalloutStore(value, viewId),
790
- }), [
791
- setStateStore,
792
- setSelectedElementStore,
793
- setHoveredElementStore,
794
- setShouldShowCalloutStore,
795
- viewId,
796
- ]);
807
+ }), [setStateStore, setSelectedElementStore, setHoveredElementStore, setShouldShowCalloutStore, viewId]);
797
808
  return {
798
809
  state,
799
810
  selectedElement,
@@ -808,24 +819,29 @@ const useHeatmapData = (props) => {
808
819
  const viewId = props?.viewId || useViewIdContext();
809
820
  const data = useHeatmapDataStore((state) => state.data[viewId]);
810
821
  const clickmap = useHeatmapDataStore((state) => state.clickmap[viewId]);
822
+ const clickAreas = useHeatmapDataStore((state) => state.clickAreas[viewId]);
811
823
  const scrollmap = useHeatmapDataStore((state) => state.scrollmap[viewId]);
812
824
  const dataInfo = useHeatmapDataStore((state) => state.dataInfo[viewId]);
813
825
  const setData = useHeatmapDataStore((state) => state.setData);
814
826
  const setClickmap = useHeatmapDataStore((state) => state.setClickmap);
827
+ const setClickAreas = useHeatmapDataStore((state) => state.setClickAreas);
815
828
  const setScrollmap = useHeatmapDataStore((state) => state.setScrollmap);
816
829
  const setDataInfo = useHeatmapDataStore((state) => state.setDataInfo);
830
+ const removeClickArea = useHeatmapDataStore((state) => state.removeClickArea);
817
831
  const memoizedSetters = useMemo(() => ({
818
832
  setData: (newData) => setData(newData, viewId),
819
833
  setClickmap: (newClickmap) => setClickmap(newClickmap, viewId),
834
+ setClickAreas: (newClickAreas) => setClickAreas(newClickAreas, viewId),
820
835
  setScrollmap: (newScrollmap) => setScrollmap(newScrollmap, viewId),
821
836
  setDataInfo: (newDataInfo) => setDataInfo(newDataInfo, viewId),
822
- }), [setData, setClickmap, setScrollmap, setDataInfo, viewId]);
837
+ removeClickArea: (areaId) => removeClickArea(areaId, viewId),
838
+ }), [setData, setClickmap, setClickAreas, setScrollmap, setDataInfo, removeClickArea, viewId]);
823
839
  return {
824
840
  data,
825
841
  clickmap,
842
+ clickAreas,
826
843
  scrollmap,
827
844
  dataInfo,
828
- // Setters (auto-inject viewId)
829
845
  ...memoizedSetters,
830
846
  };
831
847
  };
@@ -998,24 +1014,32 @@ const useRegisterData = (data, dataInfo) => {
998
1014
  }, [dataInfo]);
999
1015
  };
1000
1016
 
1001
- const useRegisterHeatmap = ({ clickmap, scrollmap }) => {
1002
- const { setClickmap, setScrollmap } = useHeatmapData();
1003
- const handleSetClickmap = useCallback((clickmap) => {
1017
+ const useRegisterHeatmap = ({ clickmap, scrollmap, clickAreas }) => {
1018
+ const { setClickmap, setScrollmap, setClickAreas } = useHeatmapData();
1019
+ const handleSetClickmap = useCallback(() => {
1004
1020
  if (!clickmap)
1005
1021
  return;
1006
1022
  setClickmap(clickmap);
1007
- }, [clickmap]);
1008
- const handleSetScrollmap = useCallback((scrollmap) => {
1023
+ }, [clickmap]); // eslint-disable-line react-hooks/exhaustive-deps
1024
+ const handleSetClickAreas = useCallback(() => {
1025
+ if (!clickAreas)
1026
+ return;
1027
+ setClickAreas(clickAreas);
1028
+ }, [clickAreas]); // eslint-disable-line react-hooks/exhaustive-deps
1029
+ const handleSetScrollmap = useCallback(() => {
1009
1030
  if (!scrollmap)
1010
1031
  return;
1011
1032
  setScrollmap(scrollmap);
1012
- }, [scrollmap]);
1033
+ }, [scrollmap]); // eslint-disable-line react-hooks/exhaustive-deps
1013
1034
  useEffect(() => {
1014
- handleSetClickmap(clickmap);
1015
- }, [clickmap]);
1035
+ handleSetClickmap();
1036
+ }, [handleSetClickmap]);
1016
1037
  useEffect(() => {
1017
- handleSetScrollmap(scrollmap);
1018
- }, [scrollmap]);
1038
+ handleSetClickAreas();
1039
+ }, [handleSetClickAreas]);
1040
+ useEffect(() => {
1041
+ handleSetScrollmap();
1042
+ }, [handleSetScrollmap]);
1019
1043
  };
1020
1044
 
1021
1045
  /**
@@ -1078,6 +1102,52 @@ function isIgnoredCanvas(element) {
1078
1102
  return false;
1079
1103
  }
1080
1104
 
1105
+ /**
1106
+ * Get all elements at a specific point (x, y), with support for Shadow DOM
1107
+ */
1108
+ function getElementsAtPoint(doc, x, y, options = {}) {
1109
+ const { filterFn, ignoreCanvas = true, visitedShadowRoots = new Set() } = options;
1110
+ // Get all elements at this point
1111
+ let elementsAtPoint = doc.elementsFromPoint(x, y);
1112
+ // Filter out canvas elements if requested
1113
+ if (ignoreCanvas) {
1114
+ elementsAtPoint = elementsAtPoint.filter((el) => !isIgnoredCanvas(el));
1115
+ }
1116
+ // Apply custom filter if provided
1117
+ if (filterFn) {
1118
+ const matchedElement = elementsAtPoint.find(filterFn);
1119
+ // If matched element has Shadow DOM and we haven't visited it yet, recurse
1120
+ if (matchedElement?.shadowRoot && !visitedShadowRoots.has(matchedElement.shadowRoot)) {
1121
+ visitedShadowRoots.add(matchedElement.shadowRoot);
1122
+ return getElementsAtPoint(matchedElement.shadowRoot, x, y, {
1123
+ ...options,
1124
+ visitedShadowRoots,
1125
+ });
1126
+ }
1127
+ }
1128
+ return elementsAtPoint;
1129
+ }
1130
+ /**
1131
+ * Get the element at a specific point (x, y)
1132
+ */
1133
+ const getElementAtPoint = (doc, x, y) => {
1134
+ let el = null;
1135
+ if ('caretPositionFromPoint' in doc) {
1136
+ el = doc.caretPositionFromPoint(x, y)?.offsetNode ?? null;
1137
+ }
1138
+ el = el ?? doc.elementFromPoint(x, y);
1139
+ let element = el;
1140
+ while (element && element.nodeType === Node.TEXT_NODE) {
1141
+ element = element.parentElement;
1142
+ }
1143
+ return element;
1144
+ };
1145
+ function getElementHash(element) {
1146
+ return (element.getAttribute('data-clarity-hash') ||
1147
+ element.getAttribute('data-clarity-hashalpha') ||
1148
+ element.getAttribute('data-clarity-hashbeta'));
1149
+ }
1150
+
1081
1151
  const AREA_HOVER_BOX_SHADOW = '0 0 0 1px #0078D4, 0 0 0 1px #0078D4 inset, 0 0 0 2px white inset';
1082
1152
  const AREA_HOVER_ELEMENT_ID = 'clarity-edit-hover';
1083
1153
  const AREA_MAP_DIV_ATTRIBUTE = 'data-clarity-area-map-div';
@@ -1125,7 +1195,6 @@ const SECONDARY_HOVERED_ELEMENT_ID_BASE = 'gx-hm-secondary-hovered-element';
1125
1195
  function getColorFromClickDist(clickDist) {
1126
1196
  // Ensure clickDist is in range [0, 100]
1127
1197
  const normalizedDist = Math.max(0, Math.min(100, clickDist));
1128
- console.log(`🚀 🐥 ~ getColorFromClickDist ~ normalizedDist:`, normalizedDist);
1129
1198
  // Calculate gradient index
1130
1199
  const maxIndex = AREA_COLOR_GRADIENT.length - 1;
1131
1200
  const index = Math.floor((normalizedDist / 100) * maxIndex);
@@ -1253,21 +1322,46 @@ function getElementSelector(element) {
1253
1322
  }
1254
1323
  return element.tagName.toLowerCase();
1255
1324
  }
1256
- function buildAreaNode(element, hash, heatmapInfo, shadowRoot) {
1325
+ /**
1326
+ * Calculate total clicks for an element including all its child elements
1327
+ * @param element - The parent element
1328
+ * @param elementMapInfo - Map of hash to element click info
1329
+ * @returns Total clicks for element + all descendants
1330
+ */
1331
+ function calculateTotalClicksWithChildren(element, elementMapInfo) {
1332
+ let totalClicks = 0;
1333
+ // Get clicks for the element itself
1334
+ const elementHash = getElementHash(element);
1335
+ if (elementHash) {
1336
+ const elementInfo = elementMapInfo[elementHash];
1337
+ totalClicks += elementInfo?.totalclicks || 0;
1338
+ }
1339
+ const children = element.querySelectorAll('*');
1340
+ children.forEach((child) => {
1341
+ const childHash = getElementHash(child);
1342
+ if (childHash) {
1343
+ const childInfo = elementMapInfo[childHash];
1344
+ totalClicks += childInfo?.totalclicks || 0;
1345
+ }
1346
+ });
1347
+ return totalClicks;
1348
+ }
1349
+ function buildAreaNode(element, hash, heatmapInfo, shadowRoot, persistedData) {
1257
1350
  if (!heatmapInfo.elementMapInfo)
1258
1351
  return;
1259
1352
  const totalClicks = heatmapInfo.totalClicks || 0;
1260
1353
  const elementInfo = heatmapInfo.elementMapInfo[hash];
1261
- const elementClicks = elementInfo?.totalclicks || 0;
1354
+ // Calculate total clicks including all child elements
1355
+ const elementClicks = calculateTotalClicksWithChildren(element, heatmapInfo.elementMapInfo);
1262
1356
  const clickDist = calculateClickDistribution(elementClicks, totalClicks);
1263
1357
  const rect = getElementRect(element);
1264
1358
  const color = getColorFromClickDist(clickDist);
1265
1359
  const hoverColor = getHoverColorFromClickDist(clickDist);
1266
1360
  const areaNode = {
1267
- kind: 'area',
1268
- id: `${hash}_${Date.now()}`,
1361
+ kind: persistedData?.kind || 'area',
1362
+ id: persistedData?.id || `${hash}_${Date.now()}`,
1269
1363
  hash,
1270
- selector: elementInfo?.selector || getElementSelector(element),
1364
+ selector: persistedData?.selector || elementInfo?.selector || getElementSelector(element),
1271
1365
  // DOM references
1272
1366
  element,
1273
1367
  areaElement: null,
@@ -1307,207 +1401,183 @@ function getTopElementsByClicks(elementMapInfo, topN = 10) {
1307
1401
  }
1308
1402
 
1309
1403
  /**
1310
- * Resolve overlapping areas by priority rules
1311
- *
1312
- * Priority Rules (in order):
1313
- * 1. Priority flag (manually set areas win)
1314
- * 2. Click distribution (higher % wins)
1315
- * 3. Total clicks (more clicks wins)
1316
- * 4. DOM containment (parent contains child, parent wins)
1317
- * 5. Size (smaller areas win - more specific)
1404
+ * Build parent-child relationships between areas based on DOM hierarchy
1405
+ * @param areas - Array of area nodes to build relationships for
1318
1406
  */
1319
- function resolveOverlaps(areas, iframeDocument) {
1320
- if (areas.length === 0)
1321
- return [];
1322
- // Group overlapping areas
1323
- const overlapGroups = findOverlapGroups(areas);
1324
- // Resolve each group
1325
- const visibleAreas = new Set();
1326
- overlapGroups.forEach((group) => {
1327
- const winner = resolveOverlapGroup(group, iframeDocument);
1328
- visibleAreas.add(winner);
1329
- });
1330
- // Add non-overlapping areas
1407
+ function buildAreaGraph(areas) {
1408
+ // Clear existing relationships
1331
1409
  areas.forEach((area) => {
1332
- const hasOverlap = overlapGroups.some((group) => group.areas.includes(area));
1333
- if (!hasOverlap) {
1334
- visibleAreas.add(area);
1335
- }
1410
+ area.parentNode = null;
1411
+ area.childNodes.clear();
1336
1412
  });
1337
- return Array.from(visibleAreas);
1338
- }
1339
- /**
1340
- * Find groups of overlapping areas
1341
- */
1342
- function findOverlapGroups(areas) {
1343
- const groups = [];
1344
- const processed = new Set();
1345
- areas.forEach((area) => {
1346
- if (processed.has(area.id))
1347
- return;
1348
- // Find all areas that overlap with this one
1349
- const overlapping = areas.filter((other) => other.id !== area.id && doAreasOverlap(area, other));
1350
- if (overlapping.length === 0) {
1351
- // No overlap, skip grouping
1352
- return;
1413
+ // Build relationships based on DOM containment
1414
+ for (let i = 0; i < areas.length; i++) {
1415
+ const area = areas[i];
1416
+ for (let j = 0; j < areas.length; j++) {
1417
+ if (i === j)
1418
+ continue;
1419
+ const otherArea = areas[j];
1420
+ // Check if area's element is contained within otherArea's element
1421
+ if (otherArea.element.contains(area.element)) {
1422
+ // Find the closest parent (not just any ancestor)
1423
+ if (!area.parentNode || area.parentNode.element.contains(otherArea.element)) {
1424
+ // Remove from old parent if exists
1425
+ if (area.parentNode) {
1426
+ area.parentNode.childNodes.delete(area);
1427
+ }
1428
+ // Set new parent
1429
+ area.parentNode = otherArea;
1430
+ otherArea.childNodes.add(area);
1431
+ }
1432
+ }
1353
1433
  }
1354
- // Create group with this area and all overlapping
1355
- const groupAreas = [area, ...overlapping];
1356
- groupAreas.forEach((a) => processed.add(a.id));
1357
- // Placeholder - will be resolved later
1358
- groups.push({
1359
- areas: groupAreas,
1360
- winner: area,
1361
- hidden: [],
1362
- });
1363
- });
1364
- return groups;
1434
+ }
1365
1435
  }
1366
- /**
1367
- * Resolve a single overlap group to find the winner
1368
- */
1369
- function resolveOverlapGroup(group, iframeDocument) {
1370
- const { areas } = group;
1371
- if (areas.length === 1)
1372
- return areas[0];
1373
- // Sort by priority rules
1374
- const sorted = [...areas].sort((a, b) => {
1375
- // Rule 1: Priority flag
1376
- if (a.priority !== b.priority) {
1377
- return a.priority ? -1 : 1;
1378
- }
1379
- // Rule 2: Click distribution
1380
- if (a.clickDist !== b.clickDist) {
1381
- return b.clickDist - a.clickDist;
1382
- }
1383
- // Rule 3: Total clicks
1384
- if (a.totalclicks !== b.totalclicks) {
1385
- return b.totalclicks - a.totalclicks;
1436
+
1437
+ class Logger {
1438
+ config = {
1439
+ enabled: false,
1440
+ prefix: '',
1441
+ timestamp: false,
1442
+ };
1443
+ /**
1444
+ * Cấu hình logger
1445
+ * @param config - Cấu hình logger
1446
+ */
1447
+ configure(config) {
1448
+ this.config = { ...this.config, ...config };
1449
+ }
1450
+ /**
1451
+ * Lấy cấu hình hiện tại
1452
+ */
1453
+ getConfig() {
1454
+ return { ...this.config };
1455
+ }
1456
+ /**
1457
+ * Bật logger
1458
+ */
1459
+ enable() {
1460
+ this.config.enabled = true;
1461
+ }
1462
+ /**
1463
+ * Tắt logger
1464
+ */
1465
+ disable() {
1466
+ this.config.enabled = false;
1467
+ }
1468
+ /**
1469
+ * Format message với prefix và timestamp
1470
+ */
1471
+ formatMessage(...args) {
1472
+ const parts = [];
1473
+ if (this.config.timestamp) {
1474
+ parts.push(`[${new Date().toISOString()}]`);
1386
1475
  }
1387
- // Rule 4: DOM containment - parent beats child
1388
- if (iframeDocument) {
1389
- const aContainsB = isElementAncestorOf(a.element, b.element);
1390
- const bContainsA = isElementAncestorOf(b.element, a.element);
1391
- if (aContainsB)
1392
- return -1; // a is parent, a wins
1393
- if (bContainsA)
1394
- return 1; // b is parent, b wins
1476
+ if (this.config.prefix) {
1477
+ parts.push(`[${this.config.prefix}]`);
1395
1478
  }
1396
- // Rule 5: Size - smaller (more specific) wins
1397
- const aSize = (a.rect.value?.width || 0) * (a.rect.value?.height || 0);
1398
- const bSize = (b.rect.value?.width || 0) * (b.rect.value?.height || 0);
1399
- return aSize - bSize;
1400
- });
1401
- const winner = sorted[0];
1402
- group.winner = winner;
1403
- group.hidden = sorted.slice(1);
1404
- return winner;
1405
- }
1406
- /**
1407
- * Filter out areas that are completely contained within others
1408
- * and have lower priority
1409
- */
1410
- function filterContainedAreas(areas) {
1411
- const visible = [];
1412
- areas.forEach((area) => {
1413
- // Check if this area is contained by a higher priority area
1414
- const isContained = areas.some((other) => {
1415
- if (other.id === area.id)
1416
- return false;
1417
- // Check containment
1418
- if (!isAreaContainedIn(area, other))
1419
- return false;
1420
- // Check priority
1421
- if (other.priority && !area.priority)
1422
- return true;
1423
- if (!other.priority && area.priority)
1424
- return false;
1425
- // Compare by click dist
1426
- if (other.clickDist > area.clickDist)
1427
- return true;
1428
- if (other.clickDist < area.clickDist)
1429
- return false;
1430
- // Compare by total clicks
1431
- return other.totalclicks > area.totalclicks;
1432
- });
1433
- if (!isContained) {
1434
- visible.push(area);
1435
- }
1436
- });
1437
- return visible;
1438
- }
1439
- /**
1440
- * Get visible areas after resolving overlaps
1441
- */
1442
- function getVisibleAreas(areas, iframeDocument) {
1443
- // First pass: filter contained areas
1444
- let visible = filterContainedAreas(areas);
1445
- // Second pass: resolve overlaps
1446
- visible = resolveOverlaps(visible, iframeDocument);
1447
- // Sort by click dist for rendering order
1448
- return sortAreasByClickDist(visible);
1449
- }
1450
-
1451
- /**
1452
- * Helper functions for setting up area renderer
1453
- */
1454
- /**
1455
- * Create the outer container for area rendering
1456
- */
1457
- function createAreaContainer(iframeDocument) {
1458
- const container = iframeDocument.createElement('div');
1459
- container.setAttribute(AREA_MAP_DIV_ATTRIBUTE, 'true');
1460
- container.style.cssText = AREA_CONTAINER_STYLES;
1461
- return container;
1462
- }
1463
- /**
1464
- * Create the inner container for React portal
1465
- */
1466
- function createInnerContainer(iframeDocument) {
1467
- const innerContainer = iframeDocument.createElement('div');
1468
- innerContainer.className = AREA_RENDERER_SELECTORS.innerContainerClass;
1469
- innerContainer.style.cssText = AREA_INNER_CONTAINER_STYLES;
1470
- return innerContainer;
1471
- }
1472
- /**
1473
- * Get or create the outer container element
1474
- */
1475
- function getOrCreateAreaContainer(iframeDocument, customShadowRoot) {
1476
- let container = iframeDocument.querySelector(AREA_RENDERER_SELECTORS.containerSelector);
1477
- if (!container) {
1478
- container = createAreaContainer(iframeDocument);
1479
- const targetRoot = customShadowRoot || iframeDocument.body;
1480
- if (targetRoot) {
1481
- targetRoot.appendChild(container);
1479
+ if (parts.length > 0) {
1480
+ return [parts.join(' '), ...args];
1482
1481
  }
1482
+ return args;
1483
1483
  }
1484
- return container;
1485
- }
1486
- function getOrCreateContainerShadowRoot(container) {
1487
- if (container.shadowRoot) {
1488
- return container.shadowRoot;
1484
+ /**
1485
+ * Log message
1486
+ */
1487
+ log(...args) {
1488
+ if (!this.config.enabled)
1489
+ return;
1490
+ console.log(...this.formatMessage(...args));
1489
1491
  }
1490
- return container.attachShadow({ mode: 'open' });
1491
- }
1492
- function getOrCreateInnerContainer(shadowRoot, iframeDocument) {
1493
- let innerContainer = shadowRoot.querySelector(AREA_RENDERER_SELECTORS.innerContainerSelector);
1494
- if (!innerContainer) {
1495
- innerContainer = createInnerContainer(iframeDocument);
1496
- shadowRoot.appendChild(innerContainer);
1492
+ /**
1493
+ * Log info message
1494
+ */
1495
+ info(...args) {
1496
+ if (!this.config.enabled)
1497
+ return;
1498
+ console.info(...this.formatMessage(...args));
1497
1499
  }
1498
- return innerContainer;
1499
- }
1500
- function setupAreaRenderingContainer(iframeDocument, customShadowRoot) {
1501
- const container = getOrCreateAreaContainer(iframeDocument, customShadowRoot);
1502
- const shadowRoot = getOrCreateContainerShadowRoot(container);
1503
- const innerContainer = getOrCreateInnerContainer(shadowRoot, iframeDocument);
1504
- return innerContainer;
1505
- }
1506
- function cleanupAreaRenderingContainer(container) {
1507
- if (container && container.parentNode) {
1508
- container.parentNode.removeChild(container);
1500
+ /**
1501
+ * Log warning message
1502
+ */
1503
+ warn(...args) {
1504
+ if (!this.config.enabled)
1505
+ return;
1506
+ console.warn(...this.formatMessage(...args));
1507
+ }
1508
+ /**
1509
+ * Log error message
1510
+ */
1511
+ error(...args) {
1512
+ if (!this.config.enabled)
1513
+ return;
1514
+ console.error(...this.formatMessage(...args));
1515
+ }
1516
+ /**
1517
+ * Log debug message
1518
+ */
1519
+ debug(...args) {
1520
+ if (!this.config.enabled)
1521
+ return;
1522
+ console.debug(...this.formatMessage(...args));
1523
+ }
1524
+ /**
1525
+ * Log table data
1526
+ */
1527
+ table(data) {
1528
+ if (!this.config.enabled)
1529
+ return;
1530
+ console.table(data);
1531
+ }
1532
+ /**
1533
+ * Start a group
1534
+ */
1535
+ group(label) {
1536
+ if (!this.config.enabled)
1537
+ return;
1538
+ console.group(...this.formatMessage(label));
1539
+ }
1540
+ /**
1541
+ * Start a collapsed group
1542
+ */
1543
+ groupCollapsed(label) {
1544
+ if (!this.config.enabled)
1545
+ return;
1546
+ console.groupCollapsed(...this.formatMessage(label));
1547
+ }
1548
+ /**
1549
+ * End a group
1550
+ */
1551
+ groupEnd() {
1552
+ if (!this.config.enabled)
1553
+ return;
1554
+ console.groupEnd();
1555
+ }
1556
+ /**
1557
+ * Start a timer
1558
+ */
1559
+ time(label) {
1560
+ if (!this.config.enabled)
1561
+ return;
1562
+ console.time(label);
1563
+ }
1564
+ /**
1565
+ * End a timer
1566
+ */
1567
+ timeEnd(label) {
1568
+ if (!this.config.enabled)
1569
+ return;
1570
+ console.timeEnd(label);
1509
1571
  }
1510
1572
  }
1573
+ // Export singleton instance
1574
+ const logger$3 = new Logger();
1575
+ // Export factory function để tạo logger với config riêng
1576
+ function createLogger(config = {}) {
1577
+ const instance = new Logger();
1578
+ instance.configure(config);
1579
+ return instance;
1580
+ }
1511
1581
 
1512
1582
  function findLastSizeOfDom(data) {
1513
1583
  const listDocs = data
@@ -1542,424 +1612,520 @@ function decodePayloads(payload) {
1542
1612
  }
1543
1613
  }
1544
1614
 
1545
- /**
1546
- * Generate unique element ID for a specific view
1547
- * @param baseId - Base element ID
1548
- * @param viewId - View ID
1549
- * @returns Unique element ID (e.g., 'gx-hm-clicked-element-view-0')
1550
- */
1551
- const getElementId = (baseId, viewId) => {
1552
- return `${baseId}-${viewId}`;
1553
- };
1554
- const getClickedElementId = (viewId, isSecondary = false) => {
1555
- const baseId = isSecondary ? SECONDARY_CLICKED_ELEMENT_ID_BASE : CLICKED_ELEMENT_ID_BASE;
1556
- return getElementId(baseId, viewId);
1557
- };
1558
- const getHoveredElementId = (viewId, isSecondary = false) => {
1559
- const baseId = isSecondary ? SECONDARY_HOVERED_ELEMENT_ID_BASE : HOVERED_ELEMENT_ID_BASE;
1560
- return getElementId(baseId, viewId);
1561
- };
1562
-
1563
- function getElementLayout(element) {
1564
- if (!element?.getBoundingClientRect)
1565
- return null;
1566
- const rect = element.getBoundingClientRect();
1567
- if (rect.width === 0 && rect.height === 0)
1615
+ function findElementByHash(props) {
1616
+ const { hash, selector, iframeDocument, vizRef } = props;
1617
+ if (vizRef) {
1618
+ const element = vizRef.get(hash);
1619
+ return element;
1620
+ }
1621
+ // Fallback
1622
+ if (!iframeDocument)
1568
1623
  return null;
1569
- return {
1570
- top: rect.top,
1571
- left: rect.left,
1572
- width: rect.width,
1573
- height: rect.height,
1574
- };
1624
+ try {
1625
+ const element = selector ? iframeDocument.querySelector(selector) : null;
1626
+ if (element) {
1627
+ return element;
1628
+ }
1629
+ }
1630
+ catch (error) {
1631
+ logger$3.warn(`Invalid selector "${selector}":`, error);
1632
+ }
1633
+ const elementByHash = iframeDocument.querySelector(`[data-clarity-hashalpha="${hash}"], [data-clarity-hash="${hash}"], [data-clarity-hashbeta="${hash}"]`);
1634
+ return elementByHash;
1575
1635
  }
1576
- const getElementRank = (hash, elements) => {
1577
- if (!elements)
1578
- return 0;
1579
- return elements.findIndex((e) => e.hash === hash) + 1;
1580
- };
1581
- const buildElementInfo = (hash, rect, heatmapInfo) => {
1582
- if (!rect || !heatmapInfo)
1636
+
1637
+ /**
1638
+ * Hydrates persisted area data into full area node
1639
+ * Finds element in DOM and calculates all runtime values
1640
+ *
1641
+ * @param persistedData - Minimal data from database
1642
+ * @param iframeDocument - Document to find element in
1643
+ * @param heatmapInfo - Heatmap data for click calculations
1644
+ * @param vizRef - Map of hash to elements
1645
+ * @param shadowRoot - Optional shadow root for rect calculation
1646
+ * @returns Full area node or null if element not found
1647
+ */
1648
+ function hydrateAreaNode(props) {
1649
+ const { persistedData, iframeDocument, heatmapInfo, vizRef, shadowRoot } = props;
1650
+ const { id, hash, selector } = persistedData;
1651
+ const element = findElementByHash({ hash, selector, iframeDocument, vizRef });
1652
+ if (!element) {
1653
+ logger$3.warn(`Cannot hydrate area ${id}: element not found for hash ${hash} or selector ${selector}`);
1583
1654
  return null;
1584
- const info = heatmapInfo.elementMapInfo?.[hash];
1585
- if (!info)
1655
+ }
1656
+ const areaNode = buildAreaNode(element, hash, heatmapInfo, shadowRoot, persistedData);
1657
+ if (!areaNode)
1586
1658
  return null;
1587
- const rank = getElementRank(hash, heatmapInfo.sortedElements);
1588
- const clicks = info.totalclicks ?? 0;
1589
- const selector = info.selector ?? '';
1590
- const baseInfo = {
1591
- hash,
1592
- clicks,
1593
- rank,
1594
- selector,
1595
- };
1596
- return {
1597
- ...baseInfo,
1598
- ...rect,
1599
- };
1600
- };
1601
-
1602
- function calculateRankPosition(rect, widthScale) {
1603
- const top = rect.top <= 18 ? rect.top + 3 : rect.top - 18;
1604
- const left = rect.left <= 18 ? rect.left + 3 : rect.left - 18;
1605
- return {
1606
- transform: `scale(${1.2 * widthScale})`,
1607
- top: Number.isNaN(top) ? undefined : top,
1608
- left: Number.isNaN(left) ? undefined : left,
1609
- };
1659
+ return areaNode;
1610
1660
  }
1611
-
1612
- const getViewportDimensions = (containerElm) => {
1613
- if (containerElm) {
1614
- const containerRect = containerElm.getBoundingClientRect();
1615
- return {
1616
- width: containerRect.width,
1617
- height: containerRect.height,
1618
- };
1661
+ function hydrateAreas(props) {
1662
+ const { clickAreas, iframeDocument, heatmapInfo, vizRef, shadowRoot } = props;
1663
+ const hydratedAreas = [];
1664
+ for (const persistedData of clickAreas) {
1665
+ const area = hydrateAreaNode({ persistedData, iframeDocument, heatmapInfo, vizRef, shadowRoot });
1666
+ if (area) {
1667
+ hydratedAreas.push(area);
1668
+ }
1619
1669
  }
1670
+ logger$3.info(`Hydrated ${hydratedAreas.length} of ${clickAreas.length} persisted areas`);
1671
+ return hydratedAreas;
1672
+ }
1673
+ /**
1674
+ * Serializes area node to persisted data for database storage
1675
+ */
1676
+ function serializeAreaNode(area) {
1620
1677
  return {
1621
- width: window.innerWidth,
1622
- height: window.innerHeight,
1678
+ kind: area.kind,
1679
+ id: area.id,
1680
+ hash: area.hash,
1681
+ selector: area.selector,
1623
1682
  };
1624
- };
1625
- const getElementDimensions = (targetElm, calloutElm) => ({
1626
- targetRect: targetElm.getBoundingClientRect(),
1627
- calloutRect: calloutElm.getBoundingClientRect(),
1628
- });
1683
+ }
1684
+ /**
1685
+ * Serializes multiple areas for database storage
1686
+ */
1687
+ function serializeAreas(areas) {
1688
+ return areas.map(serializeAreaNode);
1689
+ }
1629
1690
 
1630
- const getAlignmentOrder = (alignment) => {
1631
- switch (alignment) {
1632
- case 'center':
1633
- return ['center', 'left', 'right'];
1634
- case 'left':
1635
- return ['left', 'center', 'right'];
1636
- case 'right':
1637
- return ['right', 'center', 'left'];
1638
- }
1639
- };
1640
- const calculateLeftPosition = ({ targetRect, calloutRect, hozOffset, align, }) => {
1641
- switch (align) {
1642
- case 'left':
1643
- return targetRect.left + hozOffset;
1644
- case 'right':
1645
- return targetRect.right - calloutRect.width - hozOffset;
1646
- case 'center':
1647
- default:
1648
- return targetRect.left + targetRect.width / 2 - calloutRect.width / 2;
1649
- }
1650
- };
1651
- const calculateVerticalPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
1652
- return placement === 'top'
1653
- ? targetRect.top - calloutRect.height - padding - arrowSize
1654
- : targetRect.bottom + padding + arrowSize;
1655
- };
1656
- const calculateHorizontalPlacementPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
1657
- const top = targetRect.top + targetRect.height / 2 - calloutRect.height / 2;
1658
- const left = placement === 'right'
1659
- ? targetRect.right + padding + arrowSize
1660
- : targetRect.left - calloutRect.width - padding - arrowSize;
1661
- return { top, left };
1662
- };
1663
-
1664
- const isLeftPositionValid = (leftPos, calloutWidth, viewportWidth, padding, containerRect) => {
1665
- if (containerRect) {
1666
- return (leftPos >= containerRect.left + padding &&
1667
- leftPos + calloutWidth <= containerRect.right - padding);
1668
- }
1669
- return leftPos >= padding && leftPos + calloutWidth <= viewportWidth - padding;
1670
- };
1671
- const isVerticalPositionValid = (targetRect, calloutRect, placement, viewportHeight, padding, arrowSize, containerRect) => {
1672
- if (containerRect) {
1673
- return placement === 'top'
1674
- ? targetRect.top - calloutRect.height - padding - arrowSize >= containerRect.top
1675
- : targetRect.bottom + calloutRect.height + padding + arrowSize <= containerRect.bottom;
1676
- }
1677
- return placement === 'top'
1678
- ? targetRect.top - calloutRect.height - padding - arrowSize > 0
1679
- : targetRect.bottom + calloutRect.height + padding + arrowSize < viewportHeight;
1680
- };
1681
- const isHorizontalPlacementValid = (targetRect, calloutRect, placement, viewportWidth, padding, arrowSize, containerRect) => {
1682
- if (containerRect) {
1683
- return placement === 'right'
1684
- ? targetRect.right + calloutRect.width + padding + arrowSize <= containerRect.right
1685
- : targetRect.left - calloutRect.width - padding - arrowSize >= containerRect.left;
1686
- }
1687
- return placement === 'right'
1688
- ? targetRect.right + calloutRect.width + padding + arrowSize < viewportWidth
1689
- : targetRect.left - calloutRect.width - padding - arrowSize > 0;
1690
- };
1691
-
1692
- const generateVerticalPositionCandidates = (targetRect, calloutRect, viewportHeight, viewportWidth, alignment, hozOffset, padding, arrowSize, containerRect) => {
1693
- const candidates = [];
1694
- const placements = ['top', 'bottom'];
1695
- placements.forEach((placement) => {
1696
- const verticalPos = calculateVerticalPosition(targetRect, calloutRect, placement, padding, arrowSize);
1697
- const verticalValid = isVerticalPositionValid(targetRect, calloutRect, placement, viewportHeight, padding, arrowSize, containerRect);
1698
- const alignmentOrder = getAlignmentOrder(alignment);
1699
- alignmentOrder.forEach((align) => {
1700
- const horizontalPos = calculateLeftPosition({
1701
- targetRect,
1702
- calloutRect,
1703
- hozOffset,
1704
- align,
1705
- });
1706
- candidates.push({
1707
- placement,
1708
- top: verticalPos,
1709
- left: horizontalPos,
1710
- horizontalAlign: align,
1711
- valid: verticalValid &&
1712
- isLeftPositionValid(horizontalPos, calloutRect.width, viewportWidth, padding, containerRect),
1713
- });
1691
+ /**
1692
+ * Resolve overlapping areas by priority rules
1693
+ *
1694
+ * Priority Rules (in order):
1695
+ * 1. Priority flag (manually set areas win)
1696
+ * 2. Click distribution (higher % wins)
1697
+ * 3. Total clicks (more clicks wins)
1698
+ * 4. DOM containment (parent contains child, parent wins)
1699
+ * 5. Size (smaller areas win - more specific)
1700
+ */
1701
+ function resolveOverlaps(areas, iframeDocument) {
1702
+ if (areas.length === 0)
1703
+ return [];
1704
+ // Group overlapping areas
1705
+ const overlapGroups = findOverlapGroups(areas);
1706
+ // Resolve each group
1707
+ const visibleAreas = new Set();
1708
+ overlapGroups.forEach((group) => {
1709
+ const winner = resolveOverlapGroup(group, iframeDocument);
1710
+ visibleAreas.add(winner);
1711
+ });
1712
+ // Add non-overlapping areas
1713
+ areas.forEach((area) => {
1714
+ const hasOverlap = overlapGroups.some((group) => group.areas.includes(area));
1715
+ if (!hasOverlap) {
1716
+ visibleAreas.add(area);
1717
+ }
1718
+ });
1719
+ return Array.from(visibleAreas);
1720
+ }
1721
+ /**
1722
+ * Find groups of overlapping areas
1723
+ */
1724
+ function findOverlapGroups(areas) {
1725
+ const groups = [];
1726
+ const processed = new Set();
1727
+ areas.forEach((area) => {
1728
+ if (processed.has(area.id))
1729
+ return;
1730
+ // Find all areas that overlap with this one
1731
+ const overlapping = areas.filter((other) => other.id !== area.id && doAreasOverlap(area, other));
1732
+ if (overlapping.length === 0) {
1733
+ // No overlap, skip grouping
1734
+ return;
1735
+ }
1736
+ // Create group with this area and all overlapping
1737
+ const groupAreas = [area, ...overlapping];
1738
+ groupAreas.forEach((a) => processed.add(a.id));
1739
+ // Placeholder - will be resolved later
1740
+ groups.push({
1741
+ areas: groupAreas,
1742
+ winner: area,
1743
+ hidden: [],
1714
1744
  });
1715
1745
  });
1716
- return candidates;
1717
- };
1718
- const generateHorizontalPositionCandidates = (targetRect, calloutRect, viewportWidth, padding, arrowSize, containerRect) => {
1719
- const placements = ['left', 'right'];
1720
- return placements.map((placement) => {
1721
- const { top, left } = calculateHorizontalPlacementPosition(targetRect, calloutRect, placement, padding, arrowSize);
1722
- return {
1723
- placement,
1724
- top,
1725
- left,
1726
- horizontalAlign: 'center',
1727
- valid: isHorizontalPlacementValid(targetRect, calloutRect, placement, viewportWidth, padding, arrowSize, containerRect),
1728
- };
1746
+ return groups;
1747
+ }
1748
+ /**
1749
+ * Resolve a single overlap group to find the winner
1750
+ */
1751
+ function resolveOverlapGroup(group, iframeDocument) {
1752
+ const { areas } = group;
1753
+ if (areas.length === 1)
1754
+ return areas[0];
1755
+ // Sort by priority rules
1756
+ const sorted = [...areas].sort((a, b) => {
1757
+ // Rule 1: Priority flag
1758
+ if (a.priority !== b.priority) {
1759
+ return a.priority ? -1 : 1;
1760
+ }
1761
+ // Rule 2: Click distribution
1762
+ if (a.clickDist !== b.clickDist) {
1763
+ return b.clickDist - a.clickDist;
1764
+ }
1765
+ // Rule 3: Total clicks
1766
+ if (a.totalclicks !== b.totalclicks) {
1767
+ return b.totalclicks - a.totalclicks;
1768
+ }
1769
+ // Rule 4: DOM containment - parent beats child
1770
+ if (iframeDocument) {
1771
+ const aContainsB = isElementAncestorOf(a.element, b.element);
1772
+ const bContainsA = isElementAncestorOf(b.element, a.element);
1773
+ if (aContainsB)
1774
+ return -1; // a is parent, a wins
1775
+ if (bContainsA)
1776
+ return 1; // b is parent, b wins
1777
+ }
1778
+ // Rule 5: Size - smaller (more specific) wins
1779
+ const aSize = (a.rect.value?.width || 0) * (a.rect.value?.height || 0);
1780
+ const bSize = (b.rect.value?.width || 0) * (b.rect.value?.height || 0);
1781
+ return aSize - bSize;
1729
1782
  });
1730
- };
1731
- const generateAllPositionCandidates = (rectDimensions, viewport, alignment, hozOffset, padding, arrowSize, containerRect) => {
1732
- const { targetRect, calloutRect } = rectDimensions;
1733
- const verticalCandidates = generateVerticalPositionCandidates(targetRect, calloutRect, viewport.height, viewport.width, alignment, hozOffset, padding, arrowSize, containerRect);
1734
- const horizontalCandidates = generateHorizontalPositionCandidates(targetRect, calloutRect, viewport.width, padding, arrowSize, containerRect);
1735
- return [...verticalCandidates, ...horizontalCandidates];
1736
- };
1737
-
1738
- const selectBestPosition = (candidates) => {
1739
- return candidates.find((p) => p.valid) || candidates[0];
1740
- };
1741
- const constrainToViewport = (position, calloutRect, viewport, padding, containerRect) => {
1742
- if (containerRect) {
1743
- const left = Math.max(containerRect.left + padding, Math.min(position.left, containerRect.right - calloutRect.width - padding));
1744
- const top = Math.max(containerRect.top + padding, Math.min(position.top, containerRect.bottom - calloutRect.height - padding));
1745
- return { top, left };
1746
- }
1747
- const left = Math.max(padding, Math.min(position.left, viewport.width - calloutRect.width - padding));
1748
- const top = Math.max(padding, Math.min(position.top, viewport.height - calloutRect.height - padding));
1749
- return { top, left };
1750
- };
1751
-
1752
- const calcCalloutPosition = (options) => {
1753
- const { targetElm, calloutElm, setPosition, hozOffset = CALLOUT_HORIZONTAL_OFFSET, alignment = 'center', containerElm, } = options;
1754
- return () => {
1755
- // 1. Get dimensions
1756
- const rectDimensions = getElementDimensions(targetElm, calloutElm);
1757
- const viewport = getViewportDimensions(containerElm);
1758
- const containerRect = containerElm?.getBoundingClientRect();
1759
- const padding = CALLOUT_PADDING;
1760
- const arrowSize = CALLOUT_ARROW_SIZE;
1761
- // 2. Generate all position candidates
1762
- const candidates = generateAllPositionCandidates(rectDimensions, viewport, alignment, hozOffset, padding, arrowSize, containerRect);
1763
- // 3. Select best position
1764
- const bestPosition = selectBestPosition(candidates);
1765
- // 4. Constrain to viewport
1766
- const constrainedPosition = constrainToViewport({ top: bestPosition.top, left: bestPosition.left }, rectDimensions.calloutRect, viewport, padding, containerRect);
1767
- // 5. Create final position object
1768
- const finalPosition = {
1769
- top: constrainedPosition.top,
1770
- left: constrainedPosition.left,
1771
- placement: bestPosition.placement,
1772
- horizontalAlign: bestPosition.horizontalAlign,
1773
- };
1774
- setPosition(finalPosition);
1775
- };
1776
- };
1777
-
1783
+ const winner = sorted[0];
1784
+ group.winner = winner;
1785
+ group.hidden = sorted.slice(1);
1786
+ return winner;
1787
+ }
1778
1788
  /**
1779
- * Get all elements at a specific point (x, y), with support for Shadow DOM
1789
+ * Filter out areas that are completely contained within others
1790
+ * and have lower priority
1780
1791
  */
1781
- function getElementsAtPoint(doc, x, y, options = {}) {
1782
- const { filterFn, ignoreCanvas = true, visitedShadowRoots = new Set() } = options;
1783
- // Get all elements at this point
1784
- let elementsAtPoint = doc.elementsFromPoint(x, y);
1785
- // Filter out canvas elements if requested
1786
- if (ignoreCanvas) {
1787
- elementsAtPoint = elementsAtPoint.filter((el) => !isIgnoredCanvas(el));
1788
- }
1789
- // Apply custom filter if provided
1790
- if (filterFn) {
1791
- const matchedElement = elementsAtPoint.find(filterFn);
1792
- // If matched element has Shadow DOM and we haven't visited it yet, recurse
1793
- if (matchedElement?.shadowRoot && !visitedShadowRoots.has(matchedElement.shadowRoot)) {
1794
- visitedShadowRoots.add(matchedElement.shadowRoot);
1795
- return getElementsAtPoint(matchedElement.shadowRoot, x, y, {
1796
- ...options,
1797
- visitedShadowRoots,
1798
- });
1792
+ function filterContainedAreas(areas) {
1793
+ const visible = [];
1794
+ areas.forEach((area) => {
1795
+ // Check if this area is contained by a higher priority area
1796
+ const isContained = areas.some((other) => {
1797
+ if (other.id === area.id)
1798
+ return false;
1799
+ // Check containment
1800
+ if (!isAreaContainedIn(area, other))
1801
+ return false;
1802
+ // Check priority
1803
+ if (other.priority && !area.priority)
1804
+ return true;
1805
+ if (!other.priority && area.priority)
1806
+ return false;
1807
+ // Compare by click dist
1808
+ if (other.clickDist > area.clickDist)
1809
+ return true;
1810
+ if (other.clickDist < area.clickDist)
1811
+ return false;
1812
+ // Compare by total clicks
1813
+ return other.totalclicks > area.totalclicks;
1814
+ });
1815
+ if (!isContained) {
1816
+ visible.push(area);
1799
1817
  }
1800
- }
1801
- return elementsAtPoint;
1818
+ });
1819
+ return visible;
1802
1820
  }
1803
1821
  /**
1804
- * Get the element at a specific point (x, y)
1822
+ * Get visible areas after resolving overlaps
1805
1823
  */
1806
- const getElementAtPoint = (doc, x, y) => {
1807
- let el = null;
1808
- if ('caretPositionFromPoint' in doc) {
1809
- el = doc.caretPositionFromPoint(x, y)?.offsetNode ?? null;
1810
- }
1811
- el = el ?? doc.elementFromPoint(x, y);
1812
- let element = el;
1813
- while (element && element.nodeType === Node.TEXT_NODE) {
1814
- element = element.parentElement;
1815
- }
1816
- return element;
1817
- };
1818
- function getElementHash(element) {
1819
- return (element.getAttribute('data-clarity-hash') ||
1820
- element.getAttribute('data-clarity-hashalpha') ||
1821
- element.getAttribute('data-clarity-hashbeta'));
1824
+ function getVisibleAreas(areas, iframeDocument) {
1825
+ // First pass: filter contained areas
1826
+ let visible = filterContainedAreas(areas);
1827
+ // Second pass: resolve overlaps
1828
+ visible = resolveOverlaps(visible, iframeDocument);
1829
+ // Sort by click dist for rendering order
1830
+ return sortAreasByClickDist(visible);
1822
1831
  }
1823
1832
 
1824
- class Logger {
1825
- config = {
1826
- enabled: false,
1827
- prefix: '',
1828
- timestamp: false,
1829
- };
1830
- /**
1831
- * Cấu hình logger
1832
- * @param config - Cấu hình logger
1833
- */
1834
- configure(config) {
1835
- this.config = { ...this.config, ...config };
1836
- }
1837
- /**
1838
- * Lấy cấu hình hiện tại
1839
- */
1840
- getConfig() {
1841
- return { ...this.config };
1842
- }
1843
- /**
1844
- * Bật logger
1845
- */
1846
- enable() {
1847
- this.config.enabled = true;
1848
- }
1849
- /**
1850
- * Tắt logger
1851
- */
1852
- disable() {
1853
- this.config.enabled = false;
1854
- }
1855
- /**
1856
- * Format message với prefix và timestamp
1857
- */
1858
- formatMessage(...args) {
1859
- const parts = [];
1860
- if (this.config.timestamp) {
1861
- parts.push(`[${new Date().toISOString()}]`);
1862
- }
1863
- if (this.config.prefix) {
1864
- parts.push(`[${this.config.prefix}]`);
1865
- }
1866
- if (parts.length > 0) {
1867
- return [parts.join(' '), ...args];
1833
+ /**
1834
+ * Helper functions for setting up area renderer
1835
+ */
1836
+ /**
1837
+ * Create the outer container for area rendering
1838
+ */
1839
+ function createAreaContainer(iframeDocument) {
1840
+ const container = iframeDocument.createElement('div');
1841
+ container.setAttribute(AREA_MAP_DIV_ATTRIBUTE, 'true');
1842
+ container.style.cssText = AREA_CONTAINER_STYLES;
1843
+ return container;
1844
+ }
1845
+ /**
1846
+ * Create the inner container for React portal
1847
+ */
1848
+ function createInnerContainer(iframeDocument) {
1849
+ const innerContainer = iframeDocument.createElement('div');
1850
+ innerContainer.className = AREA_RENDERER_SELECTORS.innerContainerClass;
1851
+ innerContainer.style.cssText = AREA_INNER_CONTAINER_STYLES;
1852
+ return innerContainer;
1853
+ }
1854
+ /**
1855
+ * Get or create the outer container element
1856
+ */
1857
+ function getOrCreateAreaContainer(iframeDocument, customShadowRoot) {
1858
+ let container = iframeDocument.querySelector(AREA_RENDERER_SELECTORS.containerSelector);
1859
+ if (!container) {
1860
+ container = createAreaContainer(iframeDocument);
1861
+ const targetRoot = customShadowRoot || iframeDocument.body;
1862
+ if (targetRoot) {
1863
+ targetRoot.appendChild(container);
1868
1864
  }
1869
- return args;
1870
- }
1871
- /**
1872
- * Log message
1873
- */
1874
- log(...args) {
1875
- if (!this.config.enabled)
1876
- return;
1877
- console.log(...this.formatMessage(...args));
1878
1865
  }
1879
- /**
1880
- * Log info message
1881
- */
1882
- info(...args) {
1883
- if (!this.config.enabled)
1884
- return;
1885
- console.info(...this.formatMessage(...args));
1866
+ return container;
1867
+ }
1868
+ function getOrCreateContainerShadowRoot(container) {
1869
+ if (container.shadowRoot) {
1870
+ return container.shadowRoot;
1886
1871
  }
1887
- /**
1888
- * Log warning message
1889
- */
1890
- warn(...args) {
1891
- if (!this.config.enabled)
1892
- return;
1893
- console.warn(...this.formatMessage(...args));
1872
+ return container.attachShadow({ mode: 'open' });
1873
+ }
1874
+ function getOrCreateInnerContainer(shadowRoot, iframeDocument) {
1875
+ let innerContainer = shadowRoot.querySelector(AREA_RENDERER_SELECTORS.innerContainerSelector);
1876
+ if (!innerContainer) {
1877
+ innerContainer = createInnerContainer(iframeDocument);
1878
+ shadowRoot.appendChild(innerContainer);
1894
1879
  }
1895
- /**
1896
- * Log error message
1897
- */
1898
- error(...args) {
1899
- if (!this.config.enabled)
1900
- return;
1901
- console.error(...this.formatMessage(...args));
1880
+ return innerContainer;
1881
+ }
1882
+ function setupAreaRenderingContainer(iframeDocument, customShadowRoot) {
1883
+ const container = getOrCreateAreaContainer(iframeDocument, customShadowRoot);
1884
+ const shadowRoot = getOrCreateContainerShadowRoot(container);
1885
+ const innerContainer = getOrCreateInnerContainer(shadowRoot, iframeDocument);
1886
+ return innerContainer;
1887
+ }
1888
+ function cleanupAreaRenderingContainer(container) {
1889
+ if (container && container.parentNode) {
1890
+ container.parentNode.removeChild(container);
1902
1891
  }
1903
- /**
1904
- * Log debug message
1905
- */
1906
- debug(...args) {
1907
- if (!this.config.enabled)
1908
- return;
1909
- console.debug(...this.formatMessage(...args));
1892
+ }
1893
+
1894
+ /**
1895
+ * Generate unique element ID for a specific view
1896
+ * @param baseId - Base element ID
1897
+ * @param viewId - View ID
1898
+ * @returns Unique element ID (e.g., 'gx-hm-clicked-element-view-0')
1899
+ */
1900
+ const getElementId = (baseId, viewId) => {
1901
+ return `${baseId}-${viewId}`;
1902
+ };
1903
+ const getClickedElementId = (viewId, isSecondary = false) => {
1904
+ const baseId = isSecondary ? SECONDARY_CLICKED_ELEMENT_ID_BASE : CLICKED_ELEMENT_ID_BASE;
1905
+ return getElementId(baseId, viewId);
1906
+ };
1907
+ const getHoveredElementId = (viewId, isSecondary = false) => {
1908
+ const baseId = isSecondary ? SECONDARY_HOVERED_ELEMENT_ID_BASE : HOVERED_ELEMENT_ID_BASE;
1909
+ return getElementId(baseId, viewId);
1910
+ };
1911
+
1912
+ function getElementLayout(element) {
1913
+ if (!element?.getBoundingClientRect)
1914
+ return null;
1915
+ const rect = element.getBoundingClientRect();
1916
+ if (rect.width === 0 && rect.height === 0)
1917
+ return null;
1918
+ return {
1919
+ top: rect.top,
1920
+ left: rect.left,
1921
+ width: rect.width,
1922
+ height: rect.height,
1923
+ };
1924
+ }
1925
+ const getElementRank = (hash, elements) => {
1926
+ if (!elements)
1927
+ return 0;
1928
+ return elements.findIndex((e) => e.hash === hash) + 1;
1929
+ };
1930
+ const buildElementInfo = (hash, rect, heatmapInfo) => {
1931
+ if (!rect || !heatmapInfo)
1932
+ return null;
1933
+ const info = heatmapInfo.elementMapInfo?.[hash];
1934
+ if (!info)
1935
+ return null;
1936
+ const rank = getElementRank(hash, heatmapInfo.sortedElements);
1937
+ const clicks = info.totalclicks ?? 0;
1938
+ const selector = info.selector ?? '';
1939
+ const baseInfo = {
1940
+ hash,
1941
+ clicks,
1942
+ rank,
1943
+ selector,
1944
+ };
1945
+ return {
1946
+ ...baseInfo,
1947
+ ...rect,
1948
+ };
1949
+ };
1950
+
1951
+ function calculateRankPosition(rect, widthScale) {
1952
+ const top = rect.top <= 18 ? rect.top + 3 : rect.top - 18;
1953
+ const left = rect.left <= 18 ? rect.left + 3 : rect.left - 18;
1954
+ return {
1955
+ transform: `scale(${1.2 * widthScale})`,
1956
+ top: Number.isNaN(top) ? undefined : top,
1957
+ left: Number.isNaN(left) ? undefined : left,
1958
+ };
1959
+ }
1960
+
1961
+ const getViewportDimensions = (containerElm) => {
1962
+ if (containerElm) {
1963
+ const containerRect = containerElm.getBoundingClientRect();
1964
+ return {
1965
+ width: containerRect.width,
1966
+ height: containerRect.height,
1967
+ };
1910
1968
  }
1911
- /**
1912
- * Log table data
1913
- */
1914
- table(data) {
1915
- if (!this.config.enabled)
1916
- return;
1917
- console.table(data);
1969
+ return {
1970
+ width: window.innerWidth,
1971
+ height: window.innerHeight,
1972
+ };
1973
+ };
1974
+ const getElementDimensions = (targetElm, calloutElm) => ({
1975
+ targetRect: targetElm.getBoundingClientRect(),
1976
+ calloutRect: calloutElm.getBoundingClientRect(),
1977
+ });
1978
+
1979
+ const getAlignmentOrder = (alignment) => {
1980
+ switch (alignment) {
1981
+ case 'center':
1982
+ return ['center', 'left', 'right'];
1983
+ case 'left':
1984
+ return ['left', 'center', 'right'];
1985
+ case 'right':
1986
+ return ['right', 'center', 'left'];
1918
1987
  }
1919
- /**
1920
- * Start a group
1921
- */
1922
- group(label) {
1923
- if (!this.config.enabled)
1924
- return;
1925
- console.group(...this.formatMessage(label));
1988
+ };
1989
+ const calculateLeftPosition = ({ targetRect, calloutRect, hozOffset, align, }) => {
1990
+ switch (align) {
1991
+ case 'left':
1992
+ return targetRect.left + hozOffset;
1993
+ case 'right':
1994
+ return targetRect.right - calloutRect.width - hozOffset;
1995
+ case 'center':
1996
+ default:
1997
+ return targetRect.left + targetRect.width / 2 - calloutRect.width / 2;
1926
1998
  }
1927
- /**
1928
- * Start a collapsed group
1929
- */
1930
- groupCollapsed(label) {
1931
- if (!this.config.enabled)
1932
- return;
1933
- console.groupCollapsed(...this.formatMessage(label));
1999
+ };
2000
+ const calculateVerticalPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
2001
+ return placement === 'top'
2002
+ ? targetRect.top - calloutRect.height - padding - arrowSize
2003
+ : targetRect.bottom + padding + arrowSize;
2004
+ };
2005
+ const calculateHorizontalPlacementPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
2006
+ const top = targetRect.top + targetRect.height / 2 - calloutRect.height / 2;
2007
+ const left = placement === 'right'
2008
+ ? targetRect.right + padding + arrowSize
2009
+ : targetRect.left - calloutRect.width - padding - arrowSize;
2010
+ return { top, left };
2011
+ };
2012
+
2013
+ const isLeftPositionValid = (leftPos, calloutWidth, viewportWidth, padding, containerRect) => {
2014
+ if (containerRect) {
2015
+ return leftPos >= containerRect.left + padding && leftPos + calloutWidth <= containerRect.right - padding;
1934
2016
  }
1935
- /**
1936
- * End a group
1937
- */
1938
- groupEnd() {
1939
- if (!this.config.enabled)
1940
- return;
1941
- console.groupEnd();
2017
+ return leftPos >= padding && leftPos + calloutWidth <= viewportWidth - padding;
2018
+ };
2019
+ const isVerticalPositionValid = (targetRect, calloutRect, placement, viewportHeight, padding, arrowSize, containerRect) => {
2020
+ if (containerRect) {
2021
+ return placement === 'top'
2022
+ ? targetRect.top - calloutRect.height - padding - arrowSize >= containerRect.top
2023
+ : targetRect.bottom + calloutRect.height + padding + arrowSize <= containerRect.bottom;
1942
2024
  }
1943
- /**
1944
- * Start a timer
1945
- */
1946
- time(label) {
1947
- if (!this.config.enabled)
1948
- return;
1949
- console.time(label);
2025
+ return placement === 'top'
2026
+ ? targetRect.top - calloutRect.height - padding - arrowSize > 0
2027
+ : targetRect.bottom + calloutRect.height + padding + arrowSize < viewportHeight;
2028
+ };
2029
+ const isHorizontalPlacementValid = (targetRect, calloutRect, placement, viewportWidth, padding, arrowSize, containerRect) => {
2030
+ if (containerRect) {
2031
+ return placement === 'right'
2032
+ ? targetRect.right + calloutRect.width + padding + arrowSize <= containerRect.right
2033
+ : targetRect.left - calloutRect.width - padding - arrowSize >= containerRect.left;
1950
2034
  }
1951
- /**
1952
- * End a timer
1953
- */
1954
- timeEnd(label) {
1955
- if (!this.config.enabled)
1956
- return;
1957
- console.timeEnd(label);
2035
+ return placement === 'right'
2036
+ ? targetRect.right + calloutRect.width + padding + arrowSize < viewportWidth
2037
+ : targetRect.left - calloutRect.width - padding - arrowSize > 0;
2038
+ };
2039
+
2040
+ const generateVerticalPositionCandidates = (targetRect, calloutRect, viewportHeight, viewportWidth, alignment, hozOffset, padding, arrowSize, containerRect) => {
2041
+ const candidates = [];
2042
+ const placements = ['top', 'bottom'];
2043
+ placements.forEach((placement) => {
2044
+ const verticalPos = calculateVerticalPosition(targetRect, calloutRect, placement, padding, arrowSize);
2045
+ const verticalValid = isVerticalPositionValid(targetRect, calloutRect, placement, viewportHeight, padding, arrowSize, containerRect);
2046
+ const alignmentOrder = getAlignmentOrder(alignment);
2047
+ alignmentOrder.forEach((align) => {
2048
+ const horizontalPos = calculateLeftPosition({
2049
+ targetRect,
2050
+ calloutRect,
2051
+ hozOffset,
2052
+ align,
2053
+ });
2054
+ candidates.push({
2055
+ placement,
2056
+ top: verticalPos,
2057
+ left: horizontalPos,
2058
+ horizontalAlign: align,
2059
+ valid: verticalValid && isLeftPositionValid(horizontalPos, calloutRect.width, viewportWidth, padding, containerRect),
2060
+ });
2061
+ });
2062
+ });
2063
+ return candidates;
2064
+ };
2065
+ const generateHorizontalPositionCandidates = (targetRect, calloutRect, viewportWidth, padding, arrowSize, containerRect) => {
2066
+ const placements = ['left', 'right'];
2067
+ return placements.map((placement) => {
2068
+ const { top, left } = calculateHorizontalPlacementPosition(targetRect, calloutRect, placement, padding, arrowSize);
2069
+ return {
2070
+ placement,
2071
+ top,
2072
+ left,
2073
+ horizontalAlign: 'center',
2074
+ valid: isHorizontalPlacementValid(targetRect, calloutRect, placement, viewportWidth, padding, arrowSize, containerRect),
2075
+ };
2076
+ });
2077
+ };
2078
+ const generateAllPositionCandidates = (rectDimensions, viewport, alignment, hozOffset, padding, arrowSize, containerRect) => {
2079
+ const { targetRect, calloutRect } = rectDimensions;
2080
+ const verticalCandidates = generateVerticalPositionCandidates(targetRect, calloutRect, viewport.height, viewport.width, alignment, hozOffset, padding, arrowSize, containerRect);
2081
+ const horizontalCandidates = generateHorizontalPositionCandidates(targetRect, calloutRect, viewport.width, padding, arrowSize, containerRect);
2082
+ return [...verticalCandidates, ...horizontalCandidates];
2083
+ };
2084
+
2085
+ const selectBestPosition = (candidates) => {
2086
+ return candidates.find((p) => p.valid) || candidates[0];
2087
+ };
2088
+ const constrainToViewport = (position, calloutRect, viewport, padding, containerRect) => {
2089
+ if (containerRect) {
2090
+ const left = Math.max(containerRect.left + padding, Math.min(position.left, containerRect.right - calloutRect.width - padding));
2091
+ const top = Math.max(containerRect.top + padding, Math.min(position.top, containerRect.bottom - calloutRect.height - padding));
2092
+ return { top, left };
1958
2093
  }
1959
- }
1960
- // Export singleton instance
1961
- const logger = new Logger();
2094
+ const left = Math.max(padding, Math.min(position.left, viewport.width - calloutRect.width - padding));
2095
+ const top = Math.max(padding, Math.min(position.top, viewport.height - calloutRect.height - padding));
2096
+ return { top, left };
2097
+ };
2098
+
2099
+ const calcCalloutPosition = (options) => {
2100
+ const { targetElm, calloutElm, setPosition, hozOffset = CALLOUT_HORIZONTAL_OFFSET, alignment = 'center', containerElm, } = options;
2101
+ return () => {
2102
+ // 1. Get dimensions
2103
+ const rectDimensions = getElementDimensions(targetElm, calloutElm);
2104
+ const viewport = getViewportDimensions(containerElm);
2105
+ const containerRect = containerElm?.getBoundingClientRect();
2106
+ const padding = CALLOUT_PADDING;
2107
+ const arrowSize = CALLOUT_ARROW_SIZE;
2108
+ // 2. Generate all position candidates
2109
+ const candidates = generateAllPositionCandidates(rectDimensions, viewport, alignment, hozOffset, padding, arrowSize, containerRect);
2110
+ // 3. Select best position
2111
+ const bestPosition = selectBestPosition(candidates);
2112
+ // 4. Constrain to viewport
2113
+ const constrainedPosition = constrainToViewport({ top: bestPosition.top, left: bestPosition.left }, rectDimensions.calloutRect, viewport, padding, containerRect);
2114
+ // 5. Create final position object
2115
+ const finalPosition = {
2116
+ top: constrainedPosition.top,
2117
+ left: constrainedPosition.left,
2118
+ placement: bestPosition.placement,
2119
+ horizontalAlign: bestPosition.horizontalAlign,
2120
+ };
2121
+ setPosition(finalPosition);
2122
+ };
2123
+ };
1962
2124
 
2125
+ const logger$2 = createLogger({
2126
+ enabled: false,
2127
+ prefix: 'IframeNavigationBlockerV2',
2128
+ });
1963
2129
  class IframeNavigationBlockerV2 {
1964
2130
  doc;
1965
2131
  win;
@@ -1974,11 +2140,11 @@ class IframeNavigationBlockerV2 {
1974
2140
  this.doc = iframe.contentDocument;
1975
2141
  this.win = iframe.contentWindow;
1976
2142
  this.originalWindowOpen = this.win.open.bind(this.win);
1977
- logger.configure({ enabled: !!config?.debug, prefix: '[IframeNavigationBlockerV2]' });
2143
+ logger$2.configure({ enabled: !!config?.debug });
1978
2144
  this.init();
1979
2145
  }
1980
2146
  init() {
1981
- logger.log('Initializing...');
2147
+ logger$2.log('Initializing...');
1982
2148
  try {
1983
2149
  // Chặn navigation qua links
1984
2150
  this.blockLinkNavigation();
@@ -1994,7 +2160,7 @@ class IframeNavigationBlockerV2 {
1994
2160
  this.injectCSP();
1995
2161
  }
1996
2162
  catch (error) {
1997
- logger.error('Init error:', error);
2163
+ logger$2.error('Init error:', error);
1998
2164
  }
1999
2165
  }
2000
2166
  blockLinkNavigation() {
@@ -2008,11 +2174,11 @@ class IframeNavigationBlockerV2 {
2008
2174
  const href = link.getAttribute('href');
2009
2175
  // Cho phép hash links và empty links
2010
2176
  if (!href || href === '' || href === '#' || href.startsWith('#')) {
2011
- logger.log('Allowed hash navigation:', href);
2177
+ logger$2.log('Allowed hash navigation:', href);
2012
2178
  return;
2013
2179
  }
2014
2180
  // Chặn tất cả các loại navigation
2015
- logger.log('Blocked link navigation to:', href);
2181
+ logger$2.log('Blocked link navigation to:', href);
2016
2182
  e.preventDefault();
2017
2183
  e.stopPropagation();
2018
2184
  e.stopImmediatePropagation();
@@ -2028,7 +2194,7 @@ class IframeNavigationBlockerV2 {
2028
2194
  if (link) {
2029
2195
  const href = link.getAttribute('href');
2030
2196
  if (href && !href.startsWith('#')) {
2031
- logger.log('Blocked auxclick navigation');
2197
+ logger$2.log('Blocked auxclick navigation');
2032
2198
  e.preventDefault();
2033
2199
  e.stopPropagation();
2034
2200
  e.stopImmediatePropagation();
@@ -2061,13 +2227,13 @@ class IframeNavigationBlockerV2 {
2061
2227
  const action = form.getAttribute('action');
2062
2228
  // Cho phép forms không có action
2063
2229
  if (!action || action === '' || action === '#') {
2064
- logger.log('Allowed same-page form');
2230
+ logger$2.log('Allowed same-page form');
2065
2231
  e.preventDefault();
2066
2232
  this.handleFormSubmit(form);
2067
2233
  return;
2068
2234
  }
2069
2235
  // Chặn tất cả external submissions
2070
- logger.log('Blocked form submission to:', action);
2236
+ logger$2.log('Blocked form submission to:', action);
2071
2237
  e.preventDefault();
2072
2238
  e.stopPropagation();
2073
2239
  e.stopImmediatePropagation();
@@ -2081,7 +2247,7 @@ class IframeNavigationBlockerV2 {
2081
2247
  return this.originalWindowOpen(...args);
2082
2248
  }
2083
2249
  const url = args[0]?.toString() || 'popup';
2084
- logger.log('Blocked window.open:', url);
2250
+ logger$2.log('Blocked window.open:', url);
2085
2251
  this.notifyBlockedNavigation(url);
2086
2252
  return null;
2087
2253
  });
@@ -2091,7 +2257,7 @@ class IframeNavigationBlockerV2 {
2091
2257
  this.win.addEventListener('beforeunload', (e) => {
2092
2258
  if (!this.isEnabled)
2093
2259
  return;
2094
- logger.log('Blocked beforeunload');
2260
+ logger$2.log('Blocked beforeunload');
2095
2261
  e.preventDefault();
2096
2262
  e.returnValue = '';
2097
2263
  return '';
@@ -2100,7 +2266,7 @@ class IframeNavigationBlockerV2 {
2100
2266
  this.win.addEventListener('unload', (e) => {
2101
2267
  if (!this.isEnabled)
2102
2268
  return;
2103
- logger.log('Blocked unload');
2269
+ logger$2.log('Blocked unload');
2104
2270
  e.preventDefault();
2105
2271
  e.stopPropagation();
2106
2272
  }, true);
@@ -2108,7 +2274,7 @@ class IframeNavigationBlockerV2 {
2108
2274
  this.win.addEventListener('popstate', (e) => {
2109
2275
  if (!this.isEnabled)
2110
2276
  return;
2111
- logger.log('Blocked popstate');
2277
+ logger$2.log('Blocked popstate');
2112
2278
  e.preventDefault();
2113
2279
  e.stopPropagation();
2114
2280
  }, true);
@@ -2161,11 +2327,11 @@ class IframeNavigationBlockerV2 {
2161
2327
  meta.httpEquiv = 'Content-Security-Policy';
2162
2328
  meta.content = "navigate-to 'none'"; // Chặn tất cả navigation
2163
2329
  this.doc.head.appendChild(meta);
2164
- logger.log('Injected CSP');
2330
+ logger$2.log('Injected CSP');
2165
2331
  }
2166
2332
  }
2167
2333
  catch (error) {
2168
- logger.warn('Could not inject CSP:', error);
2334
+ logger$2.warn('Could not inject CSP:', error);
2169
2335
  }
2170
2336
  }
2171
2337
  handleFormSubmit(form) {
@@ -2174,13 +2340,13 @@ class IframeNavigationBlockerV2 {
2174
2340
  formData.forEach((value, key) => {
2175
2341
  data[key] = value;
2176
2342
  });
2177
- logger.log('Handling form data:', data);
2343
+ logger$2.log('Handling form data:', data);
2178
2344
  window.dispatchEvent(new CustomEvent('iframe-form-submit', {
2179
2345
  detail: { form, data },
2180
2346
  }));
2181
2347
  }
2182
2348
  notifyBlockedNavigation(url) {
2183
- logger.warn('Navigation blocked to:', url);
2349
+ logger$2.warn('Navigation blocked to:', url);
2184
2350
  window.dispatchEvent(new CustomEvent('iframe-navigation-blocked', {
2185
2351
  detail: { url, timestamp: Date.now() },
2186
2352
  }));
@@ -2230,19 +2396,19 @@ class IframeNavigationBlockerV2 {
2230
2396
  }
2231
2397
  enable() {
2232
2398
  this.isEnabled = true;
2233
- logger.log('Enabled');
2399
+ logger$2.log('Enabled');
2234
2400
  }
2235
2401
  enableMessage() {
2236
2402
  this.showMessage = true;
2237
- logger.log('Enabled message');
2403
+ logger$2.log('Enabled message');
2238
2404
  }
2239
2405
  disable() {
2240
2406
  this.isEnabled = false;
2241
- logger.log('Disabled');
2407
+ logger$2.log('Disabled');
2242
2408
  }
2243
2409
  disableMessage() {
2244
2410
  this.showMessage = false;
2245
- logger.log('Disabled message');
2411
+ logger$2.log('Disabled message');
2246
2412
  }
2247
2413
  destroy() {
2248
2414
  this.isEnabled = false;
@@ -2250,10 +2416,14 @@ class IframeNavigationBlockerV2 {
2250
2416
  // Cleanup observers
2251
2417
  this.observers.forEach((observer) => observer.disconnect());
2252
2418
  this.observers = [];
2253
- logger.log('Destroyed');
2419
+ logger$2.log('Destroyed');
2254
2420
  }
2255
2421
  }
2256
2422
 
2423
+ const logger$1 = createLogger({
2424
+ enabled: false,
2425
+ prefix: 'IframeStyleReplacer',
2426
+ });
2257
2427
  class IframeStyleReplacer {
2258
2428
  doc;
2259
2429
  win;
@@ -2266,7 +2436,7 @@ class IframeStyleReplacer {
2266
2436
  this.doc = iframe.contentDocument;
2267
2437
  this.win = iframe.contentWindow;
2268
2438
  this.config = config;
2269
- logger.configure({ enabled: !!config?.debug, prefix: '[IframeStyleReplacer]' });
2439
+ logger$1.configure({ enabled: !!config?.debug });
2270
2440
  }
2271
2441
  px(value) {
2272
2442
  return `${value.toFixed(2)}px`;
@@ -2300,7 +2470,7 @@ class IframeStyleReplacer {
2300
2470
  count++;
2301
2471
  }
2302
2472
  });
2303
- logger.log(`Replaced ${count} inline style elements`);
2473
+ logger$1.log(`Replaced ${count} inline style elements`);
2304
2474
  return count;
2305
2475
  }
2306
2476
  processStyleTags() {
@@ -2313,7 +2483,7 @@ class IframeStyleReplacer {
2313
2483
  count++;
2314
2484
  }
2315
2485
  });
2316
- logger.log(`Replaced ${count} <style> tags`);
2486
+ logger$1.log(`Replaced ${count} <style> tags`);
2317
2487
  return count;
2318
2488
  }
2319
2489
  processRule(rule) {
@@ -2344,7 +2514,7 @@ class IframeStyleReplacer {
2344
2514
  try {
2345
2515
  // Bỏ qua external CSS (cross-origin)
2346
2516
  if (sheet.href && !sheet.href.startsWith(this.win.location.origin)) {
2347
- logger.log('Skipping external CSS:', sheet.href);
2517
+ logger$1.log('Skipping external CSS:', sheet.href);
2348
2518
  return;
2349
2519
  }
2350
2520
  const rules = sheet.cssRules || sheet.rules;
@@ -2355,10 +2525,10 @@ class IframeStyleReplacer {
2355
2525
  }
2356
2526
  }
2357
2527
  catch (e) {
2358
- logger.warn('Cannot read stylesheet (CORS?):', e.message);
2528
+ logger$1.warn('Cannot read stylesheet (CORS?):', e.message);
2359
2529
  }
2360
2530
  });
2361
- logger.log(`Replaced ${total} rules in stylesheets`);
2531
+ logger$1.log(`Replaced ${total} rules in stylesheets`);
2362
2532
  return total;
2363
2533
  }
2364
2534
  async processLinkedStylesheets() {
@@ -2366,7 +2536,7 @@ class IframeStyleReplacer {
2366
2536
  let count = 0;
2367
2537
  for (const link of Array.from(links)) {
2368
2538
  if (!link.href.startsWith(this.win.location.origin)) {
2369
- logger.log('Skipping external CSS:', link.href);
2539
+ logger$1.log('Skipping external CSS:', link.href);
2370
2540
  continue;
2371
2541
  }
2372
2542
  try {
@@ -2384,10 +2554,10 @@ class IframeStyleReplacer {
2384
2554
  }
2385
2555
  }
2386
2556
  catch (e) {
2387
- logger.warn('Cannot load CSS:', link.href, e);
2557
+ logger$1.warn('Cannot load CSS:', link.href, e);
2388
2558
  }
2389
2559
  }
2390
- logger.log(`Replaced ${count} linked CSS files`);
2560
+ logger$1.log(`Replaced ${count} linked CSS files`);
2391
2561
  return count;
2392
2562
  }
2393
2563
  getFinalHeight() {
@@ -2411,7 +2581,7 @@ class IframeStyleReplacer {
2411
2581
  }
2412
2582
  async run() {
2413
2583
  try {
2414
- logger.log('Starting viewport units replacement...');
2584
+ logger$1.log('Starting viewport units replacement...');
2415
2585
  this.processInlineStyles();
2416
2586
  this.processStyleTags();
2417
2587
  this.processStylesheets();
@@ -2421,13 +2591,13 @@ class IframeStyleReplacer {
2421
2591
  requestAnimationFrame(() => {
2422
2592
  const height = this.getFinalHeight();
2423
2593
  const width = this.getFinalWidth();
2424
- logger.log('Calculated dimensions:', { height, width });
2594
+ logger$1.log('Calculated dimensions:', { height, width });
2425
2595
  resolve({ height, width });
2426
2596
  });
2427
2597
  });
2428
2598
  }
2429
2599
  catch (err) {
2430
- logger.error('Critical error:', err);
2600
+ logger$1.error('Critical error:', err);
2431
2601
  return {
2432
2602
  height: this.doc.body.scrollHeight || 1000,
2433
2603
  width: this.doc.body.scrollWidth || 1000,
@@ -2439,6 +2609,10 @@ class IframeStyleReplacer {
2439
2609
  }
2440
2610
  }
2441
2611
 
2612
+ const logger = createLogger({
2613
+ enabled: false,
2614
+ prefix: 'IframeHelper',
2615
+ });
2442
2616
  class IframeHelperFixer {
2443
2617
  iframe;
2444
2618
  config;
@@ -2448,7 +2622,7 @@ class IframeHelperFixer {
2448
2622
  this.config = config;
2449
2623
  this.iframe = config.iframe;
2450
2624
  this.init();
2451
- logger.configure({ enabled: !!config?.debug, prefix: '[IframeHelper]' });
2625
+ logger.configure({ enabled: !!config?.debug });
2452
2626
  }
2453
2627
  async init() {
2454
2628
  if (!this.iframe) {
@@ -2475,7 +2649,7 @@ class IframeHelperFixer {
2475
2649
  // Create replacer instance
2476
2650
  this.replacer = new IframeStyleReplacer(this.iframe, this.config);
2477
2651
  // Create navigation blocker
2478
- this.navigationBlocker = new IframeNavigationBlockerV2(this.iframe);
2652
+ this.navigationBlocker = new IframeNavigationBlockerV2(this.iframe, { debug: this.config.debug });
2479
2653
  // Run replacement
2480
2654
  const result = await this.replacer.run();
2481
2655
  logger.log('Process completed:', result);
@@ -2535,49 +2709,103 @@ function initIframeHelperFixer(config) {
2535
2709
  return fixer;
2536
2710
  }
2537
2711
 
2712
+ function validateAreaCreation(dataInfo, hash, areas) {
2713
+ if (!dataInfo?.elementMapInfo || !dataInfo?.totalClicks) {
2714
+ logger$3.warn('Cannot create area: missing heatmap data');
2715
+ return false;
2716
+ }
2717
+ if (!hash) {
2718
+ logger$3.warn('Cannot create area: missing hash');
2719
+ return false;
2720
+ }
2721
+ const alreadyExists = areas.some((area) => area.hash === hash);
2722
+ if (alreadyExists) {
2723
+ logger$3.warn(`Area already exists for element: ${hash}`);
2724
+ return false;
2725
+ }
2726
+ return true;
2727
+ }
2728
+ function identifyConflictingAreas(area) {
2729
+ const conflicts = {
2730
+ parentId: null,
2731
+ childrenIds: [],
2732
+ };
2733
+ // Case 1: New area is a child of an existing area
2734
+ if (area.parentNode) {
2735
+ conflicts.parentId = area.parentNode.id;
2736
+ logger$3.info(`New area "${area.selector}" is a child of existing area "${area.parentNode.selector}". Will remove parent.`);
2737
+ }
2738
+ // Case 2: New area is a parent of existing area(s)
2739
+ if (area.childNodes.size > 0) {
2740
+ area.childNodes.forEach((childArea) => {
2741
+ conflicts.childrenIds.push(childArea.id);
2742
+ });
2743
+ logger$3.info(`New area "${area.selector}" is a parent of ${area.childNodes.size} existing area(s). Will remove children.`);
2744
+ }
2745
+ return conflicts;
2746
+ }
2747
+ function clearGraphRelationships(area) {
2748
+ area.parentNode = null;
2749
+ area.childNodes.clear();
2750
+ }
2751
+ function removeConflictingAreas(conflicts, removeArea) {
2752
+ if (conflicts.parentId) {
2753
+ removeArea(conflicts.parentId);
2754
+ }
2755
+ conflicts.childrenIds.forEach((childId) => {
2756
+ removeArea(childId);
2757
+ });
2758
+ }
2538
2759
  function useAreaCreation(options = {}) {
2539
2760
  const { customShadowRoot, onAreaCreated } = options;
2540
2761
  const { dataInfo } = useHeatmapData();
2541
- const { areas, addArea } = useHeatmapAreaClick();
2542
- const handleCreateAreaFromElement = useCallback((element) => {
2543
- if (!dataInfo?.elementMapInfo || !dataInfo?.totalClicks) {
2544
- logger.warn('Cannot create area: missing heatmap data');
2762
+ const { areas, addArea, removeArea } = useHeatmapAreaClick();
2763
+ const onAreaCreatedElement = useCallback((element) => {
2764
+ if (!dataInfo)
2545
2765
  return;
2546
- }
2547
2766
  const hash = getElementHash(element);
2548
- if (!hash) {
2549
- logger.warn('Cannot create area: missing hash');
2550
- return;
2551
- }
2552
- const alreadyExists = areas.some((area) => area.hash === hash);
2553
- if (alreadyExists) {
2554
- logger.warn(`Area already exists for element: ${hash}`);
2767
+ if (!validateAreaCreation(dataInfo, hash, areas)) {
2555
2768
  return;
2556
2769
  }
2557
2770
  try {
2558
2771
  const area = buildAreaNode(element, hash, dataInfo, customShadowRoot);
2559
2772
  if (!area)
2560
2773
  return;
2774
+ const tempAreas = [...areas, area];
2775
+ buildAreaGraph(tempAreas);
2776
+ const conflicts = identifyConflictingAreas(area);
2777
+ clearGraphRelationships(area);
2561
2778
  addArea(area);
2779
+ removeConflictingAreas(conflicts, removeArea);
2562
2780
  if (onAreaCreated) {
2563
2781
  onAreaCreated(area);
2564
2782
  }
2565
2783
  }
2566
2784
  catch (error) {
2567
- logger.error('Failed to create area:', error);
2785
+ logger$3.error('Failed to create area:', error);
2568
2786
  }
2569
- }, [dataInfo, areas, addArea, customShadowRoot]);
2787
+ }, [dataInfo, areas, addArea, removeArea, customShadowRoot, onAreaCreated]);
2570
2788
  return {
2571
- onAreaCreatedElement: handleCreateAreaFromElement,
2789
+ onAreaCreatedElement,
2572
2790
  };
2573
2791
  }
2574
2792
 
2575
2793
  function useAreaEditMode({ iframeRef, onAreaCreatedElement, enabled = false, }) {
2576
2794
  const [hoveredElement, setHoveredElement] = useState(null);
2577
2795
  const [isHovering, setIsHovering] = useState(false);
2796
+ // Use ref to always get latest hoveredElement without causing re-renders
2797
+ const hoveredElementRef = useRef(null);
2798
+ const onAreaCreatedElementRef = useRef(onAreaCreatedElement);
2578
2799
  const { isEditingMode } = useHeatmapAreaClick();
2579
2800
  const iframeDocument = iframeRef.current?.contentDocument;
2580
2801
  const isActive = enabled && isEditingMode;
2802
+ // Keep refs in sync
2803
+ useEffect(() => {
2804
+ hoveredElementRef.current = hoveredElement;
2805
+ }, [hoveredElement]);
2806
+ useEffect(() => {
2807
+ onAreaCreatedElementRef.current = onAreaCreatedElement;
2808
+ }, [onAreaCreatedElement]);
2581
2809
  const handleMouseMove = useCallback((e) => {
2582
2810
  if (!isActive || !iframeDocument)
2583
2811
  return;
@@ -2596,15 +2824,17 @@ function useAreaEditMode({ iframeRef, onAreaCreatedElement, enabled = false, })
2596
2824
  setHoveredElement(null);
2597
2825
  setIsHovering(false);
2598
2826
  }, []);
2599
- useCallback((e) => {
2600
- if (!isActive || !hoveredElement)
2827
+ const handleClick = useCallback((e) => {
2828
+ const currentHoveredElement = hoveredElementRef.current;
2829
+ const currentCallback = onAreaCreatedElementRef.current;
2830
+ if (!isActive || !currentHoveredElement)
2601
2831
  return;
2602
2832
  e.stopPropagation();
2603
2833
  e.preventDefault();
2604
- if (!onAreaCreatedElement)
2834
+ if (!currentCallback)
2605
2835
  return;
2606
- onAreaCreatedElement(hoveredElement);
2607
- }, [isActive, hoveredElement]);
2836
+ currentCallback(currentHoveredElement);
2837
+ }, [isActive]);
2608
2838
  useEffect(() => {
2609
2839
  if (!isActive || !iframeDocument) {
2610
2840
  setHoveredElement(null);
@@ -2624,7 +2854,7 @@ function useAreaEditMode({ iframeRef, onAreaCreatedElement, enabled = false, })
2624
2854
  iframeDocument.addEventListener('mousemove', throttledMouseMove);
2625
2855
  iframeDocument.addEventListener('scroll', handleMouseLeave);
2626
2856
  iframeDocument.removeEventListener('mouseleave', handleMouseLeave);
2627
- // iframeDocument.addEventListener('click', handleClick);
2857
+ iframeDocument.addEventListener('click', handleClick);
2628
2858
  return () => {
2629
2859
  if (rafId) {
2630
2860
  cancelAnimationFrame(rafId);
@@ -2632,7 +2862,7 @@ function useAreaEditMode({ iframeRef, onAreaCreatedElement, enabled = false, })
2632
2862
  iframeDocument.removeEventListener('mousemove', throttledMouseMove);
2633
2863
  iframeDocument.removeEventListener('mouseleave', handleMouseLeave);
2634
2864
  iframeDocument.removeEventListener('scroll', handleMouseLeave);
2635
- // iframeDocument.removeEventListener('click', handleClick);
2865
+ iframeDocument.removeEventListener('click', handleClick);
2636
2866
  };
2637
2867
  }, [isActive, iframeDocument]);
2638
2868
  return {
@@ -2660,12 +2890,48 @@ const useAreaFilterVisible = (props) => {
2660
2890
  return {};
2661
2891
  };
2662
2892
 
2663
- /**
2664
- * Hook to handle area interaction (click, hover)
2665
- *
2666
- * @param options - Configuration options
2667
- * @returns Event handlers for area interactions
2668
- */
2893
+ function useAreaHydration(options) {
2894
+ const { shadowRoot, enabled = true } = options;
2895
+ const [isInitializing, setIsInitializing] = useState(false);
2896
+ const { dataInfo, clickAreas } = useHeatmapData();
2897
+ const { vizRef } = useHeatmapViz();
2898
+ const { areas, setAreas } = useHeatmapAreaClick();
2899
+ const hydratePersistedAreas = useCallback(() => {
2900
+ if (isInitializing)
2901
+ return;
2902
+ if (!vizRef)
2903
+ return;
2904
+ if (!clickAreas)
2905
+ return;
2906
+ if (!dataInfo)
2907
+ return;
2908
+ logger$3.info(`Hydrating ${clickAreas.length} persisted areas...`);
2909
+ const hydratedAreas = hydrateAreas({ clickAreas, heatmapInfo: dataInfo, vizRef, shadowRoot });
2910
+ if (!hydratedAreas?.length) {
2911
+ logger$3.warn('No areas could be hydrated - all elements may have been removed from DOM');
2912
+ return;
2913
+ }
2914
+ setIsInitializing(true);
2915
+ buildAreaGraph(hydratedAreas);
2916
+ setAreas(hydratedAreas);
2917
+ logger$3.info(`Successfully hydrated ${hydratedAreas.length} areas`);
2918
+ }, [dataInfo, vizRef, isInitializing, clickAreas]);
2919
+ useEffect(() => {
2920
+ if (!enabled)
2921
+ return;
2922
+ if (!dataInfo)
2923
+ return;
2924
+ if (!clickAreas)
2925
+ return;
2926
+ if (areas.length)
2927
+ return;
2928
+ hydratePersistedAreas();
2929
+ }, [enabled, dataInfo, clickAreas, areas.length, hydratePersistedAreas]);
2930
+ return {
2931
+ hydratePersistedAreas,
2932
+ };
2933
+ }
2934
+
2669
2935
  function useAreaInteraction(options = {}) {
2670
2936
  const { onAreaClick } = options;
2671
2937
  const { selectedArea, hoveredArea, isEditingMode, setSelectedArea, setHoveredArea } = useHeatmapAreaClick();
@@ -2760,9 +3026,10 @@ function useAreaRectSync(options) {
2760
3026
  area.rect.update(newRect);
2761
3027
  }
2762
3028
  catch (error) {
2763
- logger.error(`Failed to update rect for area ${area.id}:`, error);
3029
+ logger$3.error(`Failed to update rect for area ${area.id}:`, error);
2764
3030
  }
2765
3031
  });
3032
+ buildAreaGraph(areas);
2766
3033
  }, [areas, iframeDocument, shadowRoot, enabled, vizRef]);
2767
3034
  }
2768
3035
 
@@ -3224,22 +3491,26 @@ function useAreaScrollSync(options) {
3224
3491
  }
3225
3492
 
3226
3493
  const useAreaTopAutoDetect = (props) => {
3227
- const { iframeRef, autoCreateTopN, shadowRoot } = props;
3228
- const iframeDocument = iframeRef.current?.contentDocument;
3229
- const { dataInfo } = useHeatmapData();
3494
+ const { autoCreateTopN, shadowRoot, disabled = false } = props;
3495
+ const [isInitializing, setIsInitializing] = useState(disabled);
3496
+ const { dataInfo, clickAreas } = useHeatmapData();
3230
3497
  const { vizRef } = useHeatmapViz();
3231
3498
  const { areas, addArea } = useHeatmapAreaClick();
3232
3499
  useEffect(() => {
3500
+ if (isInitializing)
3501
+ return;
3233
3502
  if (!dataInfo?.elementMapInfo || !dataInfo?.totalClicks)
3234
3503
  return;
3235
- if (autoCreateTopN <= 0)
3504
+ if (!autoCreateTopN)
3505
+ return;
3506
+ if (clickAreas?.length)
3236
3507
  return;
3237
- if (areas.length > 0)
3508
+ if (areas?.length)
3238
3509
  return;
3239
3510
  const topElements = getTopElementsByClicks(dataInfo.elementMapInfo, autoCreateTopN);
3240
3511
  const newAreas = [];
3241
3512
  topElements.forEach(({ hash }) => {
3242
- const element = vizRef?.get(hash);
3513
+ const element = findElementByHash({ hash, vizRef });
3243
3514
  if (!element)
3244
3515
  return;
3245
3516
  const area = buildAreaNode(element, hash, dataInfo);
@@ -3247,8 +3518,16 @@ const useAreaTopAutoDetect = (props) => {
3247
3518
  return;
3248
3519
  newAreas.push(area);
3249
3520
  });
3250
- newAreas.forEach((area) => addArea(area));
3251
- }, [dataInfo, autoCreateTopN, areas.length, iframeDocument, shadowRoot]); // eslint-disable-line react-hooks/exhaustive-deps
3521
+ buildAreaGraph(newAreas);
3522
+ const areasToAdd = newAreas.filter((area) => {
3523
+ if (area.parentNode) {
3524
+ return false;
3525
+ }
3526
+ return true;
3527
+ });
3528
+ setIsInitializing(true);
3529
+ areasToAdd.forEach((area) => addArea(area));
3530
+ }, [dataInfo, autoCreateTopN, areas.length, shadowRoot, vizRef, isInitializing, clickAreas]); // eslint-disable-line react-hooks/exhaustive-deps
3252
3531
  return {};
3253
3532
  };
3254
3533
 
@@ -3289,16 +3568,14 @@ const useScrollmap = () => {
3289
3568
  const { vizRef } = useHeatmapViz();
3290
3569
  const { scrollmap } = useHeatmapData();
3291
3570
  const start = useCallback(() => {
3292
- // if (isInitialized) return;
3293
3571
  if (!vizRef || !scrollmap || scrollmap.length === 0)
3294
3572
  return;
3295
3573
  try {
3296
3574
  vizRef?.clearmap?.();
3297
3575
  vizRef?.scrollmap?.(scrollmap);
3298
- // setIsInitialized(true);
3299
3576
  }
3300
3577
  catch (error) {
3301
- console.error(`🚀 🐥 ~ useScrollmap ~ error:`, error);
3578
+ logger$3.error(`🚀 🐥 ~ useScrollmap ~ error:`, error);
3302
3579
  }
3303
3580
  }, [vizRef, scrollmap]);
3304
3581
  return { start };
@@ -3478,6 +3755,7 @@ payloads, onSuccess) {
3478
3755
  targetWidth: docWidth,
3479
3756
  targetHeight: docHeight,
3480
3757
  iframe: iframe,
3758
+ debug: false,
3481
3759
  onSuccess: (data) => {
3482
3760
  iframe.height = `${data.height}px`;
3483
3761
  onSuccess(data.height);
@@ -3656,7 +3934,7 @@ const useContainerDimensions = (props) => {
3656
3934
  return { containerWidth, containerHeight };
3657
3935
  };
3658
3936
 
3659
- const useContentDimensions = ({ iframeRef, }) => {
3937
+ const useContentDimensions = ({ iframeRef }) => {
3660
3938
  const contentWidth = useHeatmapConfigStore((state) => state.width);
3661
3939
  useEffect(() => {
3662
3940
  if (!contentWidth)
@@ -3669,8 +3947,8 @@ const useContentDimensions = ({ iframeRef, }) => {
3669
3947
  };
3670
3948
 
3671
3949
  const useObserveIframeHeight = (props) => {
3672
- const { iframeRef, isRenderViz } = props;
3673
- const { setIframeHeight } = useHeatmapViz();
3950
+ const { iframeRef } = props;
3951
+ const { iframeHeight, setIframeHeight, isRenderViz } = useHeatmapViz();
3674
3952
  const resizeObserverRef = useRef(null);
3675
3953
  const mutationObserverRef = useRef(null);
3676
3954
  const debounceTimerRef = useRef(null);
@@ -3728,7 +4006,7 @@ const useObserveIframeHeight = (props) => {
3728
4006
  }, [updateIframeHeight]);
3729
4007
  useEffect(() => {
3730
4008
  const iframe = iframeRef.current;
3731
- if (!iframe || !isRenderViz)
4009
+ if (!iframe || !iframeHeight || !isRenderViz)
3732
4010
  return;
3733
4011
  const setupObservers = () => {
3734
4012
  try {
@@ -3790,7 +4068,7 @@ const useObserveIframeHeight = (props) => {
3790
4068
  }
3791
4069
  iframe.removeEventListener('load', setupObservers);
3792
4070
  };
3793
- }, [iframeRef, isRenderViz, updateIframeHeight, debouncedUpdate, immediateUpdate]);
4071
+ }, [iframeRef, iframeHeight, isRenderViz, updateIframeHeight, debouncedUpdate, immediateUpdate]);
3794
4072
  return {};
3795
4073
  };
3796
4074
 
@@ -3839,7 +4117,7 @@ const useScaleCalculation = (props) => {
3839
4117
  return { widthScale, isScaledToFit, minZoomRatio };
3840
4118
  };
3841
4119
 
3842
- const useScrollSync = ({ widthScale, iframeRef, }) => {
4120
+ const useScrollSync = ({ widthScale, iframeRef }) => {
3843
4121
  const handleScroll = useCallback((scrollTop) => {
3844
4122
  const iframe = iframeRef.current;
3845
4123
  if (!iframe || widthScale <= 0)
@@ -3860,13 +4138,13 @@ const useScrollSync = ({ widthScale, iframeRef, }) => {
3860
4138
  };
3861
4139
 
3862
4140
  const useHeatmapScale = (props) => {
3863
- const { wrapperRef, iframeRef, iframeHeight, isRenderViz } = props;
4141
+ const { wrapperRef, iframeRef, iframeHeight } = props;
3864
4142
  // 1. Observe container dimensions
3865
4143
  const { containerWidth, containerHeight } = useContainerDimensions({ wrapperRef });
3866
4144
  // 2. Get content dimensions from config
3867
4145
  const { contentWidth } = useContentDimensions({ iframeRef });
3868
4146
  // 3. Observe iframe height (now reacts to width changes)
3869
- useObserveIframeHeight({ iframeRef, isRenderViz });
4147
+ useObserveIframeHeight({ iframeRef });
3870
4148
  // 4. Calculate scale
3871
4149
  const { widthScale } = useScaleCalculation({
3872
4150
  containerWidth,
@@ -3879,8 +4157,6 @@ const useHeatmapScale = (props) => {
3879
4157
  const scaledHeight = iframeHeight * widthScale;
3880
4158
  const scaledWidth = contentWidth * widthScale;
3881
4159
  return {
3882
- containerWidth,
3883
- containerHeight,
3884
4160
  scaledWidth,
3885
4161
  scaledHeight,
3886
4162
  handleScroll,
@@ -4061,10 +4337,10 @@ const useScrollmapZones = (options) => {
4061
4337
  const newZones = createZones(scrollmap);
4062
4338
  setZones(newZones);
4063
4339
  setIsReady(true);
4064
- console.log(`[useScrollmap] Created ${newZones.length} zones in ${mode} mode`);
4340
+ logger$3.log(`[useScrollmap] Created ${newZones.length} zones in ${mode} mode`);
4065
4341
  }
4066
4342
  catch (error) {
4067
- console.error('[useScrollmap] Error:', error);
4343
+ logger$3.error('[useScrollmap] Error:', error);
4068
4344
  setIsReady(false);
4069
4345
  }
4070
4346
  }, [enabled, scrollmap, mode, createZones]);
@@ -4199,12 +4475,8 @@ class PerformanceLogger {
4199
4475
  const totalRenders = renderMetrics.length;
4200
4476
  const totalHookCalls = hookMetrics.length;
4201
4477
  const totalStoreUpdates = storeMetrics.length;
4202
- const renderDurations = renderMetrics
4203
- .filter((m) => m.duration)
4204
- .map((m) => m.duration);
4205
- const averageRenderTime = renderDurations.length > 0
4206
- ? renderDurations.reduce((a, b) => a + b, 0) / renderDurations.length
4207
- : 0;
4478
+ const renderDurations = renderMetrics.filter((m) => m.duration).map((m) => m.duration);
4479
+ const averageRenderTime = renderDurations.length > 0 ? renderDurations.reduce((a, b) => a + b, 0) / renderDurations.length : 0;
4208
4480
  // View-specific metrics
4209
4481
  const viewMetrics = {};
4210
4482
  this.metrics.forEach((metric) => {
@@ -4752,6 +5024,7 @@ function useAreaRenderer(options) {
4752
5024
  }
4753
5025
 
4754
5026
  const VizAreaClick = ({ iframeRef, visualRef, shadowRoot, autoCreateTopN = 10, enableOverlapResolution = true, onAreaClick, }) => {
5027
+ const { clickAreas } = useHeatmapData();
4755
5028
  const { resetView } = useHeatmapAreaClick();
4756
5029
  const { areasPortal, editHighlightPortal, isReady } = useAreaRenderer({
4757
5030
  iframeRef,
@@ -4759,8 +5032,9 @@ const VizAreaClick = ({ iframeRef, visualRef, shadowRoot, autoCreateTopN = 10, e
4759
5032
  shadowRoot,
4760
5033
  onAreaClick,
4761
5034
  });
4762
- useAreaTopAutoDetect({ iframeRef, autoCreateTopN, shadowRoot });
5035
+ useAreaTopAutoDetect({ autoCreateTopN, shadowRoot, disabled: !!clickAreas?.length });
4763
5036
  useAreaFilterVisible({ iframeRef, enableOverlapResolution });
5037
+ useAreaHydration({ shadowRoot });
4764
5038
  useEffect(() => {
4765
5039
  return () => {
4766
5040
  resetView();
@@ -4772,7 +5046,7 @@ const VizAreaClick = ({ iframeRef, visualRef, shadowRoot, autoCreateTopN = 10, e
4772
5046
  };
4773
5047
  VizAreaClick.displayName = 'VizAreaClick';
4774
5048
 
4775
- const RankBadge = ({ index, elementRect, widthScale, clickOnElement, }) => {
5049
+ const RankBadge = ({ index, elementRect, widthScale, clickOnElement }) => {
4776
5050
  const style = calculateRankPosition(elementRect, widthScale);
4777
5051
  return (jsx("div", { className: "gx-hm-rank-badge", style: style, onClick: clickOnElement, children: index }));
4778
5052
  };
@@ -4788,7 +5062,7 @@ const DefaultRankBadges = ({ getRect, hidden }) => {
4788
5062
  const rect = getRect(element);
4789
5063
  if (!rect)
4790
5064
  return null;
4791
- return (jsx(RankBadge, { index: index + 1, elementRect: rect, widthScale: widthScale }, element.hash));
5065
+ return jsx(RankBadge, { index: index + 1, elementRect: rect, widthScale: widthScale }, element.hash);
4792
5066
  }) }));
4793
5067
  };
4794
5068
 
@@ -4945,7 +5219,7 @@ const VizClickmap = ({ iframeRef, visualRef, wrapperRef }) => {
4945
5219
  case IClickMode.Default:
4946
5220
  return jsx(VizElements, { iframeRef: iframeRef, visualRef: visualRef, wrapperRef: wrapperRef });
4947
5221
  case IClickMode.Area:
4948
- return (jsx(VizAreaClick, { iframeRef: iframeRef, visualRef: visualRef, autoCreateTopN: 0, onAreaClick: (area) => {
5222
+ return (jsx(VizAreaClick, { iframeRef: iframeRef, visualRef: visualRef, autoCreateTopN: 10, onAreaClick: (area) => {
4949
5223
  console.log('area clicked', area);
4950
5224
  } }));
4951
5225
  }
@@ -5043,7 +5317,7 @@ const TooltipByZone = ({ zone }) => {
5043
5317
  return jsx(BasicTooltipContent, { zone: zone });
5044
5318
  }
5045
5319
  };
5046
- return (jsx("div", { style: { paddingTop: '12px', borderTop: '1px solid #E5E7EB' }, children: contentMarkup() }));
5320
+ return jsx("div", { style: { paddingTop: '12px', borderTop: '1px solid #E5E7EB' }, children: contentMarkup() });
5047
5321
  };
5048
5322
  const BasicTooltipContent = ({ zone }) => {
5049
5323
  if (!zone)
@@ -5053,11 +5327,11 @@ const BasicTooltipContent = ({ zone }) => {
5053
5327
  const MetricsTooltipContent = ({ zone }) => {
5054
5328
  if (!zone)
5055
5329
  return null;
5056
- return (jsxs(Fragment, { children: [jsx("div", { style: { fontWeight: 600, marginBottom: '8px' }, children: zone.label }), jsxs("div", { style: { fontSize: '20px', fontWeight: 700, marginBottom: '8px' }, children: [zone.percUsers.toFixed(2), "% users"] }), zone.metrics && (jsxs("div", { style: { display: 'grid', gap: '6px', fontSize: '13px' }, children: [zone.metrics.revenue !== undefined && (jsx(MetricRow, { label: "Revenue", value: `$${zone.metrics.revenue.toFixed(2)}` })), zone.metrics.conversionRate !== undefined && (jsx(MetricRow, { label: "Conversion", value: `${zone.metrics.conversionRate.toFixed(2)}%` })), zone.metrics.orders !== undefined && (jsx(MetricRow, { label: "Orders", value: zone.metrics.orders.toString() }))] }))] }));
5330
+ return (jsxs(Fragment, { children: [jsx("div", { style: { fontWeight: 600, marginBottom: '8px' }, children: zone.label }), jsxs("div", { style: { fontSize: '20px', fontWeight: 700, marginBottom: '8px' }, children: [zone.percUsers.toFixed(2), "% users"] }), zone.metrics && (jsxs("div", { style: { display: 'grid', gap: '6px', fontSize: '13px' }, children: [zone.metrics.revenue !== undefined && (jsx(MetricRow, { label: "Revenue", value: `$${zone.metrics.revenue.toFixed(2)}` })), zone.metrics.conversionRate !== undefined && (jsx(MetricRow, { label: "Conversion", value: `${zone.metrics.conversionRate.toFixed(2)}%` })), zone.metrics.orders !== undefined && jsx(MetricRow, { label: "Orders", value: zone.metrics.orders.toString() })] }))] }));
5057
5331
  };
5058
5332
  const MetricRow = ({ label, value }) => (jsxs("div", { style: { display: 'flex', justifyContent: 'space-between' }, children: [jsxs("span", { style: { color: '#605E5C' }, children: [label, ":"] }), jsx("span", { style: { fontWeight: 600 }, children: value })] }));
5059
5333
 
5060
- const HoverZones = ({ iframeRef, wrapperRef, position, currentScrollPercent, }) => {
5334
+ const HoverZones = ({ iframeRef, wrapperRef, position, currentScrollPercent }) => {
5061
5335
  const { scrollmap } = useHeatmapData();
5062
5336
  // const hoveredZone = useHeatmapVizScrollmapStore((state) => state.hoveredZone);
5063
5337
  // const setHoveredZone = useHeatmapVizScrollmapStore((state) => state.setHoveredZone);
@@ -5278,15 +5552,13 @@ const VizDomRenderer = ({ mode = 'heatmap' }) => {
5278
5552
  const contentWidth = useHeatmapConfigStore((state) => state.width || 0);
5279
5553
  const wrapperRef = useRef(null);
5280
5554
  const visualRef = useRef(null);
5281
- const { setSelectedElement } = useHeatmapClick();
5282
- const { iframeHeight, setIframeHeight, isRenderViz } = useHeatmapViz();
5283
5555
  const { iframeRef } = useHeatmapRenderByMode(mode);
5556
+ const { iframeHeight } = useHeatmapViz();
5284
5557
  const { scaledHeight, handleScroll } = useHeatmapScale({
5285
5558
  wrapperRef,
5286
5559
  iframeRef,
5287
5560
  visualRef,
5288
5561
  iframeHeight,
5289
- isRenderViz,
5290
5562
  });
5291
5563
  useHeatmapCanvas();
5292
5564
  useRenderCount('VizDomRenderer');
@@ -5294,13 +5566,6 @@ const VizDomRenderer = ({ mode = 'heatmap' }) => {
5294
5566
  const scrollTop = e.currentTarget.scrollTop;
5295
5567
  handleScroll(scrollTop);
5296
5568
  };
5297
- const cleanUp = () => {
5298
- setIframeHeight(0);
5299
- setSelectedElement(null);
5300
- };
5301
- useEffect(() => {
5302
- return cleanUp;
5303
- }, []);
5304
5569
  return (jsxs(WrapperVisual, { visualRef: visualRef, wrapperRef: wrapperRef, scaledHeight: scaledHeight, onScroll: onScroll, iframeHeight: iframeHeight, children: [jsx(VizClickmap, { iframeRef: iframeRef, visualRef: visualRef, wrapperRef: wrapperRef }), jsx("iframe", { ref: iframeRef, ...HEATMAP_IFRAME, width: contentWidth, scrolling: "no" }), jsx(VizScrollMap, { iframeRef: iframeRef, wrapperRef: visualRef })] }));
5305
5570
  };
5306
5571
 
@@ -5309,13 +5574,22 @@ const VizLoading = () => {
5309
5574
  };
5310
5575
 
5311
5576
  const VizDomHeatmap = () => {
5312
- const { iframeHeight, setIframeHeight } = useHeatmapViz();
5577
+ const { iframeHeight, setIframeHeight, setVizRef, setIsRenderViz } = useHeatmapViz();
5578
+ const { setSelectedElement, setHoveredElement } = useHeatmapClick();
5579
+ const { setSelectedArea, setHoveredArea, setAreas } = useHeatmapAreaClick();
5313
5580
  useRenderCount('VizDomHeatmap');
5581
+ const cleanUp = () => {
5582
+ setVizRef(null);
5583
+ setIframeHeight(0);
5584
+ setIsRenderViz(false);
5585
+ setSelectedElement(null);
5586
+ setHoveredElement(null);
5587
+ setSelectedArea(null);
5588
+ setHoveredArea(null);
5589
+ setAreas([]);
5590
+ };
5314
5591
  useEffect(() => {
5315
- return () => {
5316
- console.log('🚀 🐥 ~ useEffect ~ return:');
5317
- setIframeHeight(0);
5318
- };
5592
+ return cleanUp;
5319
5593
  }, []);
5320
5594
  return (jsxs(VizContainer, { isActive: true, children: [jsx(VizDomRenderer, {}), iframeHeight === 0 && jsx(VizLoading, {})] }));
5321
5595
  };
@@ -5378,21 +5652,16 @@ const HeatmapPreview = () => {
5378
5652
 
5379
5653
  const ContentTopBar = () => {
5380
5654
  const controls = useHeatmapControlStore((state) => state.controls);
5381
- useHeatmapConfigStore((state) => state.mode);
5382
5655
  const TopBar = controls.TopBar;
5383
- // In compare mode, hide individual top bars since we have a global header
5384
- // if (mode === 'compare') {
5385
- // return null;
5386
- // }
5387
5656
  return (jsx(BoxStack, { id: "gx-hm-content-header", flexDirection: "row", alignItems: "center", overflow: "auto", zIndex: 1, backgroundColor: "white", style: {
5388
5657
  borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
5389
5658
  }, children: TopBar && jsx(TopBar, {}) }));
5390
5659
  };
5391
5660
 
5392
- const HeatmapLayout = ({ data, clickmap, scrollmap, controls, dataInfo, }) => {
5661
+ const HeatmapLayout = ({ data, clickmap, clickAreas, scrollmap, controls, dataInfo, }) => {
5393
5662
  useRegisterControl(controls);
5394
5663
  useRegisterData(data, dataInfo);
5395
- useRegisterHeatmap({ clickmap, scrollmap });
5664
+ useRegisterHeatmap({ clickmap, scrollmap, clickAreas });
5396
5665
  useRegisterConfig();
5397
5666
  performanceLogger.configure({
5398
5667
  enabled: true,
@@ -5520,4 +5789,4 @@ const AreaOverlay = ({ area, onClick, onMouseEnter, onMouseLeave, isSelected, is
5520
5789
  ` }), showLabel && jsx(AreaLabel, { clickDist: area.clickDist, totalClicks: area.totalclicks, kind: area.kind })] }));
5521
5790
  };
5522
5791
 
5523
- export { AreaEditHighlight, AreaOverlay, DEFAULT_SIDEBAR_WIDTH, DEFAULT_VIEW_ID, GraphView, HEATMAP_CONFIG, HEATMAP_IFRAME, HEATMAP_STYLE, HeatmapLayout, IClickMode, IClickType, IHeatmapType, IScrollType, ViewIdContext, Z_INDEX, compareViewPerformance, convertViewportToIframeCoords, createStorePerformanceTracker, downloadPerformanceReport, getCompareViewId, getMetricsByViewId, getPerformanceReportJSON, getScrollGradientColor, performanceLogger, printPerformanceSummary, scrollToElementIfNeeded, sendPerformanceReport, trackStoreAction, useAreaCreation, useAreaEditMode, useAreaFilterVisible, useAreaInteraction, useAreaPortals, useAreaRectSync, useAreaRendererContainer, useAreaScrollSync, useAreaTopAutoDetect, useClickedElement, useDebounceCallback, useElementCalloutVisible, useHeatmapAreaClick, useHeatmapCanvas, useHeatmapClick, useHeatmapCompareStore, useHeatmapConfigStore, useHeatmapCopyView, useHeatmapData, useHeatmapEffects, useHeatmapElementPosition, useHeatmapLiveStore, useHeatmapRenderByMode, useHeatmapScale, useHeatmapScroll, useHeatmapViz, useHoveredElement, useMeasureFunction, useRegisterConfig, useRegisterControl, useRegisterData, useRegisterHeatmap, useRenderCount, useScrollmapZones, useTrackHookCall, useViewIdContext, useVizLiveRender, useWhyDidYouUpdate, useWrapperRefHeight, useZonePositions, withPerformanceTracking };
5792
+ export { AreaEditHighlight, AreaOverlay, DEFAULT_SIDEBAR_WIDTH, DEFAULT_VIEW_ID, GraphView, HEATMAP_CONFIG, HEATMAP_IFRAME, HEATMAP_STYLE, HeatmapLayout, IClickMode, IClickType, IHeatmapType, IScrollType, ViewIdContext, Z_INDEX, compareViewPerformance, convertViewportToIframeCoords, createStorePerformanceTracker, downloadPerformanceReport, getCompareViewId, getMetricsByViewId, getPerformanceReportJSON, getScrollGradientColor, performanceLogger, printPerformanceSummary, scrollToElementIfNeeded, sendPerformanceReport, serializeAreas, trackStoreAction, useAreaCreation, useAreaEditMode, useAreaFilterVisible, useAreaHydration, useAreaInteraction, useAreaPortals, useAreaRectSync, useAreaRendererContainer, useAreaScrollSync, useAreaTopAutoDetect, useClickedElement, useDebounceCallback, useElementCalloutVisible, useHeatmapAreaClick, useHeatmapCanvas, useHeatmapClick, useHeatmapCompareStore, useHeatmapConfigStore, useHeatmapCopyView, useHeatmapData, useHeatmapEffects, useHeatmapElementPosition, useHeatmapLiveStore, useHeatmapRenderByMode, useHeatmapScale, useHeatmapScroll, useHeatmapViz, useHoveredElement, useMeasureFunction, useRegisterConfig, useRegisterControl, useRegisterData, useRegisterHeatmap, useRenderCount, useScrollmapZones, useTrackHookCall, useViewIdContext, useVizLiveRender, useWhyDidYouUpdate, useWrapperRefHeight, useZonePositions, withPerformanceTracking };