@gemx-dev/heatmap-react 3.5.53 → 3.5.55

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